2 Commits

Author SHA1 Message Date
115f3bad62 WIP 2026-05-26 13:05:51 -04:00
52d7bca677 WIP 2026-05-26 11:22:45 -04:00
92 changed files with 1279 additions and 19690 deletions

View File

@@ -1,16 +0,0 @@
;;; 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))))

View File

@@ -1,210 +0,0 @@
name: libakgl CI Build
run-name: ${{ gitea.actor }} libakgl test
on: [push]
jobs:
cmake_build:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install build dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y \
cmake doxygen gcc gcovr pkg-config \
libasound2-dev libfreetype-dev libharfbuzz-dev \
libpng-dev libtiff-dev libwebp-dev \
libudev-dev libx11-dev libxcursor-dev libxext-dev \
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
libxss-dev libxtst-dev
- name: Configure and build
run: |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DAKGL_COVERAGE=ON
cmake --build build --parallel
- name: Build API documentation
run: doxygen Doxyfile
# The perf suites run here too -- they are ordinary CTest tests -- but this
# is a Debug build with coverage instrumentation, where a timing is a
# measurement of gcov. AKGL_BENCH_SCALE cuts their iteration counts so
# they contribute path coverage without spending ten minutes proving
# nothing; benchutil.h already refuses to enforce budgets in an
# unoptimized build. The performance job below is where the timings are
# taken and the budgets are checked.
- name: Test (JUnit)
env:
AKGL_BENCH_SCALE: '0.02'
run: |
export LD_LIBRARY_PATH="$PWD/build:$PWD/build/deps/SDL:$PWD/build/deps/SDL_image:$PWD/build/deps/SDL_mixer:$PWD/build/deps/SDL_ttf"
ctest \
--test-dir build \
-E '^character$' \
--output-on-failure \
--output-junit "$(pwd)/ctest-junit.xml"
- name: Publish test results
if: always()
uses: mikepenz/action-junit-report@v4
with:
report_paths: 'ctest-junit.xml'
annotate_only: true
detailed_summary: true
include_passed: true
fail_on_failure: 'true'
- name: Upload coverage reports
if: always()
uses: actions/upload-artifact@v4
with:
name: code-coverage
path: build/coverage/
if-no-files-found: warn
# The benchmarks, in the only build where their numbers mean anything.
# tests/benchutil.h enforces a budget per measurement only when it is compiled
# optimized and running at full scale, which is exactly this job and no other:
# the coverage job above is Debug, and the memory job below is under valgrind.
# A budget is roughly ten times the baseline recorded in PERFORMANCE.md, which
# is what makes a shared, slower runner viable -- it catches an algorithmic
# regression and ignores a busy machine. If a budget still proves flaky here,
# raise that one budget with a measurement behind it and re-record the
# baseline; do not drop the gate.
performance:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install build dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y \
cmake gcc pkg-config \
libasound2-dev libfreetype-dev libharfbuzz-dev \
libpng-dev libtiff-dev libwebp-dev \
libudev-dev libx11-dev libxcursor-dev libxext-dev \
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
libxss-dev libxtst-dev
- name: Configure and build
run: |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build --parallel
# --verbose rather than --output-on-failure: a passing benchmark's output
# is the entire point of running it, and --output-on-failure prints
# nothing at all for a green run. pipefail because the step ends in a
# pipe, whose status would otherwise be tee's -- always 0 -- and a blown
# budget would be reported as a passing step.
- name: Benchmarks (JUnit)
run: |
set -o pipefail
export LD_LIBRARY_PATH="$PWD/build:$PWD/build/deps/SDL:$PWD/build/deps/SDL_image:$PWD/build/deps/SDL_mixer:$PWD/build/deps/SDL_ttf"
ctest \
--test-dir build \
-L perf \
--verbose \
--output-junit "$(pwd)/perf-junit.xml" \
2>&1 | tee "$(pwd)/perf-baseline.txt"
- name: Publish benchmark results
if: always()
uses: mikepenz/action-junit-report@v4
with:
report_paths: 'perf-junit.xml'
annotate_only: true
detailed_summary: true
include_passed: true
fail_on_failure: 'true'
# Kept whether the job passed or failed: the tables are the point. A run
# that went red says which measurement moved, and a run that went green is
# the next baseline if somebody re-records PERFORMANCE.md.
- name: Upload benchmark tables
if: always()
uses: actions/upload-artifact@v4
with:
name: performance-baseline
path: perf-baseline.txt
if-no-files-found: warn
# Every suite under valgrind. No memory-check programs of their own: the perf
# suites scale themselves down when they detect valgrind, which turns them
# into the broadest path coverage in the tree at a cost this job can afford.
#
# This job gates. A definite leak, a read past the end of an allocation, or a
# branch on uninitialised memory fails the build on the push that introduced
# it -- not on the day somebody gets around to caring. The six findings this
# job had on its first run were fixed to make that possible rather than
# excused into a warning.
memory_check:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install memory-check dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y \
cmake gcc pkg-config valgrind \
libasound2-dev libfreetype-dev libharfbuzz-dev \
libpng-dev libtiff-dev libwebp-dev \
libudev-dev libx11-dev libxcursor-dev libxext-dev \
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
libxss-dev libxtst-dev
# RelWithDebInfo rather than Debug: valgrind needs the symbols, and -O0
# would leave every inlined frame in the stacks it prints.
- name: Configure and build
run: |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build --parallel
# The character suite is excluded here for the same reason as in the
# coverage job: it fails deliberately, and a failing suite would be
# reported as "valgrind found nothing but ctest exited non-zero".
- name: Memory check
run: scripts/memcheck.sh -E '^character$'
- name: Upload valgrind logs
if: always()
uses: actions/upload-artifact@v4
with:
name: valgrind-logs
path: build/Testing/Temporary/MemoryChecker.*.log
if-no-files-found: warn
mutation_test:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install mutation-test dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y \
cmake gcc pkg-config python3 \
libasound2-dev libfreetype-dev libharfbuzz-dev \
libpng-dev libtiff-dev libwebp-dev \
libudev-dev libx11-dev libxcursor-dev libxext-dev \
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
libxss-dev libxtst-dev
# Keep CI bounded to a focused source file. Run the default target locally
# for the complete libakgl-owned src/ tree.
- name: Mutation testing
run: |
python3 scripts/mutation_test.py \
--target src/staticstring.c \
--junit mutation-junit.xml \
--threshold 50
- name: Publish mutation results
if: always()
uses: mikepenz/action-junit-report@v4
with:
report_paths: 'mutation-junit.xml'
annotate_only: true
detailed_summary: true
include_passed: true
fail_on_failure: 'false'

10
.gitignore vendored
View File

@@ -1,11 +1,3 @@
# 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*/
./build/*
.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

4
.gitmodules vendored
View File

@@ -15,10 +15,10 @@
url = git@github.com:libsdl-org/SDL_ttf.git
[submodule "deps/libsdlerror"]
path = deps/libakerror
url = https://source.starfort.tech/andrew/libakerror.git
url = https://source.home.aklabs.net/andrew/libsdlerror.git
[submodule "deps/libakstdlib"]
path = deps/libakstdlib
url = https://source.starfort.tech/andrew/libakstdlib.git
url = https://source.home.aklabs.net/andrew/libakstdlib.git
[submodule "deps/jansson"]
path = deps/jansson
url = git@github.com:akheron/jansson.git

371
AGENTS.md
View File

@@ -1,371 +0,0 @@
# Repository Guidelines
## 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/` 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
Configure an out-of-tree development build:
```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
```
Build all library, utility, and test targets:
```sh
cmake --build build --parallel
```
Run the complete test suite with failure details:
```sh
ctest --test-dir build --output-on-failure
```
Run one test while iterating, for example `ctest --test-dir build -R sprite --output-on-failure`. The `rebuild.sh` script also installs into a developer-specific `/home/andrew/local` prefix and removes existing outputs; prefer the portable commands above unless that exact workflow is intended.
Run only the performance suites with `ctest --test-dir build -L perf --output-on-failure`, or leave them out of an ordinary run with `-LE perf`. They take about 30 seconds together, print a table of nanoseconds per operation, and fail only when a measurement exceeds a budget set at roughly ten times the recorded baseline. `AKGL_BENCH_SCALE` scales every iteration count — `AKGL_BENCH_SCALE=0.1 ctest --test-dir build -L perf` for a quick look — and below 1.0 the budgets are reported but not enforced. The recorded baseline and what it means are in `PERFORMANCE.md`.
Check for memory defects with `cmake --build build --target memcheck`, or
`scripts/memcheck.sh` directly — it takes ctest's selection flags, so
`scripts/memcheck.sh -R tilemap` and `scripts/memcheck.sh -LE perf` both work.
It runs the suites that already exist under valgrind (`ctest -T memcheck`) with
the headless drivers forced, and exits non-zero when valgrind finds a definite
leak, an invalid access, or a read of uninitialised memory — which `ctest -T
memcheck` on its own will not do. The whole run takes about thirty seconds
because the perf suites scale themselves down under valgrind. Suppressions for
third-party findings live in `scripts/valgrind.supp`; add one only when the
finding genuinely cannot be fixed from this repository.
Run mutation testing with `cmake --build build --target mutation`. For a quick smoke run, use `scripts/mutation_test.py --target src/tilemap.c --max-mutants 10`; the harness mutates only a scratch copy and excludes the intentionally failing character test.
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.
## Continuous integration
`.gitea/workflows/ci.yaml` runs four jobs on every push, and each one exists
because the others cannot do its work:
| Job | Build | What it is for |
|---|---|---|
| `cmake_build` | Debug + `AKGL_COVERAGE=ON` | The unit suites and the coverage report. The perf suites run here too, at `AKGL_BENCH_SCALE=0.02`, for path coverage only — a timing taken under gcov is a measurement of gcov. |
| `performance` | RelWithDebInfo | `ctest -L perf`. The only job where budgets are enforced, because `tests/benchutil.h` enforces them only when compiled optimized at full scale. Keeps the tables as an artifact. |
| `memory_check` | RelWithDebInfo | `scripts/memcheck.sh`, every suite under valgrind. Gates: a definite leak, an invalid access or a branch on uninitialised memory fails the build. Keeps the valgrind logs as an artifact. |
| `mutation_test` | Debug | One focused source file, so CI stays bounded. |
The `character` suite is excluded wherever a whole run is selected: it fails
deliberately, and pinning current-but-wrong behavior is not what it is for.
## Coding Style
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.
- **Never `return` from inside a `HANDLE` block either.** `FINISH` ends with
`RELEASE_ERROR`, so leaving before it means the handled context is never given
back to `AKERR_ARRAY_ERROR` — one leaked slot per call, and the 129th call
aborts the process with "Unable to pull an error context from the array!".
`SUCCEED_RETURN` is safe because it releases first; a bare `return`, or a
`return f(...)` that tail-calls the fallback, is not. Set a flag in the
`HANDLE` block and act on it after `FINISH`. This has already cost a
process-killing leak in `akgl_path_relative`; see TODO.md, "Performance".
- 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
- Add yourself (agent program name, model name and version) as a co-author on every commit message
- Avoid dynamic memory allocation. Use the `akgl_heap_*` methods to retrieve necessary objects at runtime, and to release them when done. If the akgl heap facilities don't provide the kind of object needed, create a new heap layer to support that type of object.
## Testing Guidelines
Tests use simple executable return codes and are registered through CTest in `CMakeLists.txt`; there is no declared coverage threshold. Add focused tests as `tests/<feature>.c`, create a matching `test_<feature>` target, and register it with `add_test`. Put reusable fixtures in `tests/assets/` and keep paths compatible with tests launched from the build tree. Coverage mode wraps the suite in a CTest fixture so counters are reset before tests and reports are generated afterward.
### Performance suites
`tests/perf.c` (nothing that draws) and `tests/perf_render.c` (everything that
does) are benchmarks, built and registered like any other suite but listed in
`AKGL_PERF_SUITES` so they carry the `perf` label and a longer timeout. The
harness is `tests/benchutil.h`. Three rules keep the numbers honest, and all
three were learned by getting them wrong first:
- **Nothing that checks an error goes inside the clock.** `PASS` and `CATCH`
call `akerr_valid_error_address`, which walks `AKERR_ARRAY_ERROR` — more work
than several of the calls being measured. Use `BENCH_LOOP`, which stashes the
context and stops at the first failure, and hand it to `PASS` afterwards.
- **Flush the renderer before stopping the clock.** SDL batches: a `draw_*` call
queues a command and returns. `BENCH_FLUSH_STOP` in `tests/perf_render.c` is
there because the first version of that suite measured queueing, reported a
tilemap frame 250 times faster than it is, and paid the real cost at teardown
inside `SDL_DestroyTexture`.
- **A drawing benchmark needs a raw-SDL control that does the same pixel work.**
Without one there is no way to separate what libakgl costs from what the
rasterizer costs, and the answer is not the one you would guess — see
`PERFORMANCE.md`.
Budgets are per-operation ceilings at roughly ten times the recorded baseline;
they are enforced only in an optimized build at full scale. When a change moves
a number for a good reason, re-record the baseline in `PERFORMANCE.md` in the
same commit and say why it moved.
These two suites are also the memory-check vehicle. `tests/benchutil.h` detects
valgrind from `LD_PRELOAD` and drops the scale to
`AKGL_BENCH_VALGRIND_SCALE`, so the same binaries walk every path once instead
of a hundred thousand times, and the timings a checked run prints are labelled
as meaningless. **Do not add memory-check suites**: a new path worth checking
belongs in a benchmark, where it gets both.
## Commit & Pull Request Guidelines
Recent commits use concise, imperative summaries such as `Fix a scale bug...` and often explain related changes in one sentence. Keep each commit scoped and describe user-visible behavior. Pull requests should summarize the change, identify affected modules, list the CMake/CTest commands run, and link relevant issues. Include screenshots only for rendering, map, or `charviewer` changes.

View File

@@ -1,157 +1,44 @@
cmake_minimum_required(VERSION 3.10)
# 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.4.0 LANGUAGES C)
# Memory checking reuses the suites that already exist -- `ctest -T memcheck`
# runs every registered test under valgrind -- rather than adding programs of its
# own. The perf suites carry most of the weight there: they are the only things
# in the tree that load assets, draw a scene, and run a frame loop in one
# process, and tests/benchutil.h drops their iteration counts by three orders of
# magnitude when it finds itself under valgrind, which turns a benchmark into
# exactly the broad, once-through path coverage a leak check wants.
#
# These have to be set before include(CTest): that is what writes them into
# DartConfiguration.tcl, and a memcheck run reads them from there.
find_program(MEMORYCHECK_COMMAND valgrind)
# Set with FORCE, but only when empty. include(CTest) declares both of these as
# empty cache entries, so a build tree configured before this block existed has
# them already and a plain set(... CACHE ...) would be ignored -- the values
# would silently never reach DartConfiguration.tcl. Guarding on emptiness still
# leaves a deliberate -DMEMORYCHECK_SUPPRESSIONS_FILE=... alone.
if(NOT MEMORYCHECK_SUPPRESSIONS_FILE)
set(MEMORYCHECK_SUPPRESSIONS_FILE
"${CMAKE_CURRENT_SOURCE_DIR}/scripts/valgrind.supp"
CACHE FILEPATH "Suppressions for third-party findings the memcheck run cannot fix" FORCE)
endif()
# Definite losses only. "Still reachable" is every global SDL and FreeType keeps
# for the process lifetime and says nothing about libakgl; "possibly lost" is
# dominated by interior pointers into pools and thread stacks. Neither is worth
# the false positives.
if(NOT MEMORYCHECK_COMMAND_OPTIONS)
set(MEMORYCHECK_COMMAND_OPTIONS
"--leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite --track-origins=yes --num-callers=25"
CACHE STRING "Options passed to the memory checker" FORCE)
endif()
project(akgl LANGUAGES C)
include(CTest)
option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF)
if(AKGL_COVERAGE)
if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
message(FATAL_ERROR "AKGL_COVERAGE requires GCC or Clang")
endif()
find_program(GCOVR_EXECUTABLE gcovr REQUIRED)
endif()
# Vendored projects own their test suites. Suppress their CTest registration so
# the suite that runs contains only the targets built here. The override is
# lifted again below, before this project registers its own tests.
#
# **Only when this project is top-level, and that guard is load-bearing.** CMake
# exposes an overridden command as `_name` and chains exactly one level deep: a
# second override rebinds `_add_test` to the *first* override and the builtin
# becomes unreachable to everybody, forever. So two projects in one tree cannot
# both shadow add_test() -- whoever goes second breaks, and an embedding
# consumer that shadows it first sees its own registrations recurse until CMake
# stops at depth 1000.
#
# An embedded libakgl therefore leaves the override alone and lets its consumer
# suppress what it does not want; the consumer has to have that machinery
# anyway, for libakerror and libakstdlib. libakstdlib guards its own shadow the
# same way and for the same reason.
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKGL_SUPPRESS_DEPENDENCY_TESTS TRUE)
function(add_test)
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
_add_test(${ARGV})
endif()
endfunction()
function(set_tests_properties)
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
_set_tests_properties(${ARGV})
endif()
endfunction()
endif()
add_subdirectory(deps/jansson EXCLUDE_FROM_ALL)
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_image EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL)
set(JANSSON_WITHOUT_TESTS ON CACHE BOOL "Do not build vendored Jansson tests" FORCE)
set(JANSSON_EXAMPLES OFF CACHE BOOL "Do not build vendored Jansson examples" FORCE)
set(JANSSON_BUILD_DOCS OFF CACHE BOOL "Do not build vendored Jansson docs" FORCE)
else()
# Add one vendored dependency, if nobody has already declared it and the
# submodule is actually checked out.
#
# A macro rather than a function on purpose: add_subdirectory() inside a
# function runs with that function's variable scope, so every cache-ish variable
# these projects set for their own subdirectories would be discarded on return.
macro(akgl_add_vendored_dependency target dir)
if(NOT TARGET ${target})
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/CMakeLists.txt")
add_subdirectory(${dir} EXCLUDE_FROM_ALL)
set(AKGL_VENDORED_DEPENDENCIES TRUE)
endif()
endif()
endmacro()
# Added on both paths, not just when this is the top-level project. Embedded
# with add_subdirectory(), the submodules under deps/ are sitting right there --
# the recursive clone the consumer just did put them there -- and refusing to
# configure until SDL3 is installed system-wide is a failure with its own answer
# three directories away. if(NOT TARGET ...) means a consumer that has already
# declared one of these wins; the EXISTS check means a checkout without
# submodules falls through to find_package below.
akgl_add_vendored_dependency(jansson::jansson deps/jansson)
akgl_add_vendored_dependency(akerror::akerror deps/libakerror)
akgl_add_vendored_dependency(akstdlib::akstdlib deps/libakstdlib)
akgl_add_vendored_dependency(SDL3::SDL3 deps/SDL)
akgl_add_vendored_dependency(SDL3_image::SDL3_image deps/SDL_image)
akgl_add_vendored_dependency(SDL3_mixer::SDL3_mixer deps/SDL_mixer)
akgl_add_vendored_dependency(SDL3_ttf::SDL3_ttf deps/SDL_ttf)
# libakerror 1.0.0 sizes its own status-name registry; consumers no longer do.
# libakgl claims its status codes at runtime in akgl_heap_init() instead.
set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE)
# Anything the vendored block did not supply has to come from the system.
if(NOT (TARGET SDL3::SDL3 AND TARGET SDL3_image::SDL3_image AND
TARGET SDL3_mixer::SDL3_mixer AND TARGET SDL3_ttf::SDL3_ttf AND
TARGET akerror::akerror AND TARGET akstdlib::akstdlib AND
TARGET jansson::jansson))
# Only needed to locate installed copies; a fully vendored build does not
# require pkg-config to be present at all.
find_package(PkgConfig REQUIRED)
endif()
if(NOT TARGET SDL3::SDL3)
find_package(SDL3 REQUIRED)
endif()
if(NOT TARGET SDL3_image::SDL3_image)
find_package(SDL3_image REQUIRED)
endif()
if(NOT TARGET SDL3_mixer::SDL3_mixer)
find_package(SDL3_mixer REQUIRED)
endif()
if(NOT TARGET SDL3_ttf::SDL3_ttf)
find_package(SDL3_ttf REQUIRED)
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)
find_package(akerror REQUIRED)
endif()
# 0.2 rather than bare: libakstdlib 0.2.0 ships an akstdlibConfigVersion.cmake
# with SameMinorVersion compatibility, mirroring its soname, so this accepts
# any 0.2.x and refuses 0.3 and 1.0. Unversioned, this path would silently
# accept an ABI-incompatible libakstdlib.
if(NOT TARGET akstdlib::akstdlib)
find_package(akstdlib 0.2 REQUIRED)
endif()
if(NOT TARGET jansson::jansson)
find_package(jansson)
if(NOT TARGET SDL3::SDL3)
find_package(SDL3 REQUIRED)
endif()
if(NOT TARGET SDL3_image::SDL3_image)
find_package(SDL3_image REQUIRED)
endif()
if(NOT TARGET SDL3_mixer::SDL3_mixer)
find_package(SDL3_mixer REQUIRED)
endif()
if(NOT TARGET SDL3_ttf::SDL3_ttf)
find_package(SDL3_ttf REQUIRED)
endif()
if(NOT TARGET akerror::akerror)
find_package(akerror REQUIRED)
endif()
if(NOT TARGET akstdlib::akstdlib)
find_package(akstdlib REQUIRED)
endif()
if(NOT TARGET jansson::jansson)
find_package(jansson)
endif()
endif()
set(GAMECONTROLLERDB_H "include/akgl/SDL_GameControllerDB.h")
@@ -160,13 +47,6 @@ set(exec_prefix "\${prefix}")
set(libdir "\${exec_prefix}/lib")
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}/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
# test executables in every out-of-tree build.
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/tests/assets"
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
add_custom_command(
OUTPUT ${GAMECONTROLLERDB_H}
@@ -180,168 +60,50 @@ add_library(akgl SHARED
deps/semver/semver.c
src/actor.c
src/actor_state_string_names.c
src/audio.c
src/text.c
src/assets.c
src/character.c
src/draw.c
src/error.c
src/game.c
src/controller.c
src/heap.c
src/json_helpers.c
src/registry.c
src/renderer.c
src/physics.c
src/sprite.c
src/staticstring.c
src/tilemap.c
src/util.c
src/version.c
${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_executable(charviewer util/charviewer.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
# akgl_test_<name> and registered with CTest under <name>.
set(AKGL_TEST_SUITES
actor
audio
bitmasks
character
controller
draw
error
game
headers
heap
json_helpers
physics
registry
renderer
sprite
staticstring
text
tilemap
util
version
)
# The performance suites are built and registered exactly like the unit suites,
# but they are benchmarks: they drive hot paths for millions of iterations, print
# a table of nanoseconds per operation, and fail only when a measurement exceeds
# a budget set at roughly ten times the recorded baseline. They carry the `perf`
# label, so `ctest -L perf` runs only them and `ctest -LE perf` leaves them out
# of an ordinary run, and they get a much longer timeout for obvious reasons.
#
# Set AKGL_BENCH_SCALE in the environment to change how long they run --
# `AKGL_BENCH_SCALE=0.1 ctest -L perf` for a tenth of the iterations. Below 1.0
# the budgets are measured and reported but not enforced, because a short run is
# a noisy one.
set(AKGL_PERF_SUITES
perf
perf_render
)
# 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 AKGL_PERF_SUITES)
add_executable(akgl_test_${suite} tests/${suite}.c)
add_test(NAME ${suite} COMMAND akgl_test_${suite})
endforeach()
add_test(NAME semver_unit COMMAND akgl_test_semver_unit)
# TIMEOUT is generous because CTest applies the same property to `ctest -T
# memcheck`, where every suite runs under valgrind at something like twenty
# times its normal cost. The unit suites finish in well under a second each
# without it; the ceiling is there for the checked run, not the ordinary one.
set_tests_properties(
${AKGL_TEST_SUITES} semver_unit
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 300
)
set_tests_properties(
${AKGL_PERF_SUITES}
PROPERTIES
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
TIMEOUT 900
LABELS perf
)
add_executable(test_actor tests/actor.c)
add_executable(test_bitmasks tests/bitmasks.c)
add_executable(test_character tests/character.c)
add_executable(test_registry tests/registry.c)
add_executable(test_sprite tests/sprite.c)
add_executable(test_staticstring tests/staticstring.c)
add_executable(test_tilemap tests/tilemap.c)
add_executable(test_util tests/util.c)
add_executable(test_semver_unit deps/semver/semver_unit.c)
add_test(NAME actor COMMAND test_actor)
add_test(NAME bitmasks COMMAND test_bitmasks)
add_test(NAME character COMMAND test_character)
add_test(NAME registry COMMAND test_registry)
add_test(NAME sprite COMMAND test_sprite)
add_test(NAME staticstring COMMAND test_staticstring)
add_test(NAME tilemap COMMAND test_tilemap)
add_test(NAME util COMMAND test_util)
add_test(NAME semver_unit COMMAND test_semver_unit)
# Specify include directories for the library's headers (if applicable)
target_include_directories(akgl PUBLIC
include/
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)
target_compile_options(akgl PRIVATE --coverage -O0 -g)
target_link_options(akgl PRIVATE --coverage)
set(AKGL_COVERAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/coverage")
file(MAKE_DIRECTORY "${AKGL_COVERAGE_DIR}")
add_test(
NAME coverage_reset
COMMAND ${GCOVR_EXECUTABLE}
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
--delete
)
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP akgl_coverage)
# Any suite missing from this list runs outside the fixture and has its
# counters discarded by coverage_reset. The perf suites are deliberately
# outside it: an instrumented -O0 build measures gcov, not libakgl, and the
# lines they cover are covered by the unit suites anyway.
set_tests_properties(
${AKGL_TEST_SUITES} semver_unit
PROPERTIES FIXTURES_REQUIRED akgl_coverage
)
add_test(
NAME coverage_report
COMMAND ${GCOVR_EXECUTABLE}
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
--filter "${CMAKE_CURRENT_SOURCE_DIR}/src/"
--xml-pretty
--xml "${AKGL_COVERAGE_DIR}/coverage.xml"
--html-details "${AKGL_COVERAGE_DIR}/index.html"
)
set_tests_properties(
coverage_report
PROPERTIES
FIXTURES_CLEANUP akgl_coverage
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)
endif()
target_link_libraries(akgl
PUBLIC
SDL3::SDL3
@@ -353,113 +115,25 @@ target_link_libraries(akgl
jansson::jansson
)
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
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(akgl_test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
endforeach()
target_link_libraries(test_actor 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(test_bitmasks 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(test_character 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(test_registry 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(test_sprite 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(test_staticstring 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(test_tilemap 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(test_util 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)
# When the vendored SDL satellite libraries are built in-tree they land in per-
# project subdirectories that are not on the loader's default search path, so a
# freshly built test aborts before main() with "cannot open shared object file".
# Bake those directories into the build-tree RPATH. Installed builds resolve the
# same libraries through find_package and need no help, so this keys on whether
# anything was actually vendored rather than on being the top-level project.
if(AKGL_VENDORED_DEPENDENCIES)
set(AKGL_VENDORED_RPATH
"$<TARGET_FILE_DIR:SDL3::SDL3>"
"$<TARGET_FILE_DIR:SDL3_image::SDL3_image>"
"$<TARGET_FILE_DIR:SDL3_ttf::SDL3_ttf>"
"$<TARGET_FILE_DIR:SDL3_mixer::SDL3_mixer>"
"$<TARGET_FILE_DIR:akerror::akerror>"
"$<TARGET_FILE_DIR:akstdlib::akstdlib>"
)
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
set_target_properties(akgl_test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
endforeach()
set_target_properties(charviewer akgl PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
# RPATH alone is not enough: LD_LIBRARY_PATH is searched first, so a developer
# who has previously run rebuild.sh has an installed libakgl.so ahead of the
# one under test. Prepend the build tree for the CTest run so the suite always
# exercises what was just compiled.
set(AKGL_TEST_LIBPATH
"${CMAKE_CURRENT_BINARY_DIR}"
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL"
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_image"
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_ttf"
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_mixer"
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakerror"
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakstdlib"
)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
set(AKGL_TEST_ENV_MOD "")
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
list(APPEND AKGL_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
endforeach()
set_tests_properties(
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
PROPERTIES ENVIRONMENT_MODIFICATION "${AKGL_TEST_ENV_MOD}"
)
else()
string(REPLACE ";" ":" AKGL_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
set_tests_properties(
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
PROPERTIES ENVIRONMENT "LD_LIBRARY_PATH=${AKGL_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}"
)
endif()
endif()
# Mutation testing copies the repository to scratch space, applies one small
# source change at a time, and verifies that the passing tests detect it. The
# intentionally failing character test is excluded by the harness.
find_package(Python3 COMPONENTS Interpreter)
if(Python3_FOUND)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKGL_MUTATION_TARGET mutation)
else()
set(AKGL_MUTATION_TARGET akgl_mutation)
endif()
add_custom_target(${AKGL_MUTATION_TARGET}
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
USES_TERMINAL
COMMENT "Running mutation tests (breaks a scratch copy, expects tests to fail)"
)
endif()
# Memory checking runs the whole registered suite under valgrind. The wrapper
# script exists because `ctest -T memcheck` records defects and still exits 0,
# which cannot gate anything; it also forces the headless drivers, so the vendor
# GPU stack is never loaded and never has to be suppressed.
if(MEMORYCHECK_COMMAND)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKGL_MEMCHECK_TARGET memcheck)
else()
set(AKGL_MEMCHECK_TARGET akgl_memcheck)
endif()
add_custom_target(${AKGL_MEMCHECK_TARGET}
COMMAND ${CMAKE_COMMAND} -E env
AKGL_BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/scripts/memcheck.sh
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
USES_TERMINAL
COMMENT "Running every test suite under valgrind (slow; the perf suites scale themselves down)"
)
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}/include/akgl/version.h DESTINATION "include/akgl/")
install(TARGETS akgl DESTINATION "lib/")
install(FILES "deps/semver/semver.h" DESTINATION "include/")
install(FILES "include/akgl/actor.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/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/error.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/draw.h" DESTINATION "include/akgl/")
@@ -468,8 +142,6 @@ install(FILES "include/akgl/controller.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/heap.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/iterator.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/json_helpers.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/renderer.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/physics.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/registry.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/sprite.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/staticstring.h" DESTINATION "include/akgl/")

View File

@@ -1,18 +0,0 @@
PROJECT_NAME = libakgl
PROJECT_BRIEF = "Game development support library"
OUTPUT_DIRECTORY = build/docs
INPUT = include/akgl src
FILE_PATTERNS = *.h *.c
RECURSIVE = YES
EXCLUDE_PATTERNS = */SDL_GameControllerDB.h
EXTRACT_ALL = YES
EXTRACT_STATIC = YES
WARN_IF_UNDOCUMENTED = YES
WARN_IF_DOC_ERROR = YES
WARN_AS_ERROR = FAIL_ON_WARNINGS
GENERATE_HTML = YES
GENERATE_LATEX = NO
QUIET = YES

View File

@@ -1,344 +0,0 @@
# libakgl performance baseline
This is where the library actually spends its time, measured rather than guessed.
The numbers below are the first recorded baseline: libakgl 0.3.0, at commit
`f35443e` plus the perf suites themselves. Everything here is reproducible with
two commands, and the suites that produced it are checked in as
`tests/perf.c` and `tests/perf_render.c`.
Read it in this order if you only want the short version: **the frame budget** is
the part that matters and **what the numbers say** is the argument. The six
defects the suites turned up on the way — two of them process-killing — are in
`TODO.md` under **Performance**, along with the targets these numbers are
measured against.
## Reproducing it
```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build --parallel
ctest --test-dir build -L perf --output-on-failure
```
Both suites print a table and exit non-zero if any measurement blew its budget.
They are ordinary CTest tests, so `ctest --test-dir build` runs them along with
everything else; `ctest --test-dir build -LE perf` leaves them out when you only
want the unit suites. `AKGL_BENCH_SCALE` scales every iteration count —
`AKGL_BENCH_SCALE=0.1` for a quick look, `10` for a long one. Below 1.0 the
budgets are reported but not enforced, because a short run is a noisy one.
The whole thing takes about 30 seconds: 4 s for `perf`, 25 s for `perf_render`.
## The machine
| | |
|---|---|
| CPU | AMD Ryzen 5 7535HS, 6 cores / 12 threads, 4.6 GHz max |
| Memory | 62 GiB |
| OS | Linux 6.8.0-136-generic |
| Compiler | gcc 13.3.0, `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (`-O2 -g`) |
| SDL | vendored SDL 3.4.8, `dummy` video driver, `software` renderer |
| Target | 640x480, offscreen |
One machine, one build type, one afternoon. Treat the absolute numbers as this
laptop's and the *ratios* as the library's.
## How it is measured
- **Best of five.** Each benchmark runs five times and the harness keeps the
fastest. The mean is the wrong statistic on a shared machine: every source of
noise makes a run slower and none makes it faster.
- **No error checking inside the clock.** `PASS` and `CATCH` call
`akerr_valid_error_address`, which walks `AKERR_ARRAY_ERROR` — more work than
several of the calls being measured. The timed loop stashes the context and
checks it after the clock stops.
- **The renderer is flushed inside the measurement.** SDL batches: a `draw_*`
call queues a command and returns. The first version of this suite measured
*queueing* and reported a tilemap frame at 65 µs; the deferred work then took
114 seconds to come out at teardown, inside `SDL_DestroyTexture`. Every
drawing benchmark now flushes before it stops the clock, and the numbers below
are 250x larger and true.
- **SDL's log output goes to a sink that discards it.** The library logs on
paths this suite calls hundreds of thousands of times; timing a write to a
terminal measures the terminal. What the *formatting* costs is measured
deliberately, by the pair of actor-spawn benchmarks.
- **Budgets are set at roughly 10x the measured baseline.** Loose enough that a
busy machine does not turn CI red, tight enough that a linear scan becoming
quadratic cannot hide. They are enforced only in an optimized build at full
scale — a coverage build measures gcov, not libakgl.
**The renderer caveat, stated once and loudly.** These draw benchmarks run
against SDL's *software* renderer. No shipped game does that. What a software
renderer buys is that all the work stays in this process where it can be timed,
and that the per-blit cost is at least honest about how much pixel traffic was
asked for. Read every drawing number as *a count of work libakgl asked for*, not
as a frame rate anyone would ship. That is exactly why the raw-SDL control rows
exist: they do the same pixel work with none of the library in the path, so the
difference between the two is libakgl's share, and that part *does* carry over
to a GPU backend.
## Static footprint
libakgl does not call `malloc`. Every one of these arrays exists from process
start whether the game uses one slot or all of them.
| Pool | Slots | Bytes each | Total |
|---|---:|---:|---:|
| `HEAP_ACTOR` | 64 | 400 | 25,600 |
| `HEAP_SPRITE` | 1024 | 176 | 180,224 |
| `HEAP_SPRITESHEET` | 1024 | 536 | 548,864 |
| `HEAP_CHARACTER` | 256 | 184 | 47,104 |
| `HEAP_STRING` | 256 | 4,100 | 1,049,600 |
| **pools, total** | | | **1,851,392** |
| `akgl_Tilemap` (one, as `_akgl_gamemap`) | 1 | 26,388,008 | 26,388,008 |
| — of which layers | 16 | 1,120,296 | 17,924,736 |
| — of which tilesets | 16 | 528,944 | 8,463,104 |
**28 MB of BSS before `main` runs, and 94% of it is one tilemap.** A layer is
512x512 `int` cells whether the map is 512x512 or 2x2, and a tileset carries a
65,536-entry offset table whether the image holds 65,536 tiles or 1,728. This is
not a hypothetical cost: zeroing that struct is 1.37 ms, and `akgl_tilemap_load`
pays it before it has read a byte of the map file.
## Results: everything that does not draw
`tests/perf.c`, full scale. `ns/op` is the best of five runs; `ops/sec` is its
reciprocal.
| Benchmark | Unit | ns/op | ops/sec |
|---|---|---:|---:|
| `heap_next_actor`, empty pool | call | 4.0 | 248,392,158 |
| `heap_next_actor`, one slot left | call | 36.9 | 27,077,696 |
| heap string claim + release cycle | cycle | 49.8 | 20,084,967 |
| `heap_next_string`, empty pool | call | 3.9 | 255,059,748 |
| `heap_next_string`, one slot left | call | 250.9 | 3,986,007 |
| `heap_release_string`, 4 KiB wipe | call | 47.2 | 21,191,918 |
| `heap_init`, all five pools | call | 45,141 | 22,153 |
| actor spawn + release, library logging on | actor | 202.3 | 4,942,862 |
| actor spawn + release, logging suppressed | actor | 162.5 | 6,153,324 |
| `akgl_actor_set_character`, registry lookup | call | 39.3 | 25,438,092 |
| `akgl_set_property` | call | 96.9 | 10,316,516 |
| `akgl_get_property`, 4 KiB copy | call | 85.3 | 11,722,134 |
| `akgl_character_sprite_get`, state to sprite | call | 37.4 | 26,734,605 |
| `akgl_actor_update`, animation advancing | actor | 68.4 | 14,609,892 |
| `akgl_actor_update`, no sprite for state | actor | 616.5 | 1,621,937 |
| `akgl_physics_simulate`, 64 live actors | frame | 1,216.8 | 821,841 |
| `akgl_physics_simulate`, empty pool | frame | 63.9 | 15,650,101 |
| logic frame: 64 updates + simulate | frame | 5,763.1 | 173,517 |
| `akgl_rectangle_points` | call | 4.0 | 248,412,026 |
| `akgl_collide_rectangles`, overlapping | call | 24.9 | 40,181,108 |
| `akgl_collide_rectangles`, disjoint | call | 57.9 | 17,261,906 |
| all-pairs collision sweep, 64 actors (2016 pairs) | sweep | 115,023.6 | 8,694 |
| `akgl_string_initialize` | call | 32.1 | 31,197,432 |
| `akgl_string_copy`, full length | call | 32.2 | 31,075,004 |
| `json_load_file`, small document | load | 11,338.8 | 88,193 |
| `akgl_get_json_string_value` | call | 40.7 | 24,552,958 |
| `akgl_get_json_integer_value` | call | 13.8 | 72,704,562 |
| `akgl_path_relative`, `realpath` on an existing file | call | 3,481.5 | 287,232 |
## Results: everything that draws
`tests/perf_render.c`, full scale, 640x480 software renderer. The two **control**
rows are raw `SDL_RenderTexture` loops with no libakgl in the path.
| Benchmark | Unit | ns/op | ops/sec |
|---|---|---:|---:|
| `frame_start` + `frame_end` (clear + present) | frame | 23,161.7 | 43,175 |
| `akgl_draw_point` | call | 132.5 | 7,547,141 |
| `akgl_draw_line`, screen diagonal | call | 605.9 | 1,650,375 |
| `akgl_draw_rect`, 200x150 outline | call | 520.8 | 1,920,055 |
| `akgl_draw_filled_rect`, 200x150 | call | 43,695.1 | 22,886 |
| `akgl_draw_circle`, radius 64 | call | 2,516.2 | 397,423 |
| `akgl_draw_copy_region`, 64x64 readback | call | 954.7 | 1,047,471 |
| `akgl_draw_paste_region`, 64x64 upload | call | 5,352.1 | 186,841 |
| `akgl_draw_flood_fill`, full 640x480 target | call | 1,967,295.6 | 508 |
| `akgl_text_measure`, 15 characters | call | 37.3 | 26,804,657 |
| `akgl_text_rendertextat`, 15 characters | call | 12,601.7 | 79,354 |
| `akgl_sprite_load_json`, sheet already loaded | load | 17,011.3 | 58,784 |
| `akgl_character_load_json`, two mappings | load | 14,506.8 | 68,933 |
| `akgl_tilemap_load` + release, fixture map | load | 11,881,190.8 | 84 |
| zeroing one `akgl_Tilemap` | call | 1,372,396.7 | 729 |
| `akgl_tilemap_compute_tileset_offsets`, 1728 tiles | call | 2,240.8 | 446,264 |
| `akgl_tilemap_draw`, 40x30 tiles, 1 tileset | frame | 16,260,566.6 | 61 |
| `akgl_tilemap_draw`, 40x30 tiles, 8 tilesets | frame | 16,395,182.4 | 61 |
| **control**: raw SDL blits, one source tile | frame | 471,676.6 | 2,120 |
| **control**: raw SDL blits, map order | frame | 16,229,501.0 | 62 |
| `akgl_actor_render`, on camera | actor | 2,992.0 | 334,220 |
| `draw_world`, 1200 tiles + 64 actors | frame | 16,484,493.6 | 61 |
| `akgl_game_update`, full frame | frame | 16,576,902.2 | 60 |
## The frame budget
At 60 fps a frame is 16.67 ms. Here is where it goes for a 640x480 game with a
full screen of 16-pixel tiles and 64 actors:
| Part of the frame | Cost | Share of 16.67 ms |
|---|---:|---:|
| Logic: 64 actor updates + one physics sweep | 0.006 ms | 0.03% |
| All-pairs collision over 64 actors, if you do it | 0.115 ms | 0.7% |
| Clear + present | 0.023 ms | 0.1% |
| 64 actor renders (48x48 blits) | 0.191 ms | 1.1% |
| 1200 tile blits | 16.26 ms | 97.6% |
| Six lines of HUD text | 0.076 ms | 0.5% |
**Everything libakgl decides is free. The pixels are the whole frame.** Every
piece of bookkeeping this library does — pool scans, registry lookups, state-to-
sprite mapping, physics, the error-context machinery — adds up to well under 1%
of a frame that is 97% software rasterization. On a GPU backend those blits get
cheap and libakgl's own share rises, which is exactly why the per-operation
numbers above matter more than the frame totals.
## What the numbers say
### The tilemap draw is SDL, not libakgl — and I can prove it
`akgl_tilemap_draw` takes 16.26 ms for a 1200-tile screen. A raw
`SDL_RenderTexture` loop issuing the *same 1200 blits from the same scattered
source tiles* takes 16.23 ms. The library's own per-tile work — the bounds
arithmetic, the tileset scan, the offset-table lookup, the backend indirection,
the error macros — is **0.03 ms per frame, under 0.2%**.
That took three attempts to measure honestly. A control that walked the sheet
sequentially said libakgl cost 67%; a control that blitted one source tile over
and over said it cost 3400%. Both were wrong, and both were wrong the same way:
they changed the *memory access pattern of the source texture* rather than
isolating the library. A 16x16 tile read from a random place in a 768x576 sheet
costs about 13 µs on this software rasterizer; the same tile read from cache
costs 0.4 µs. That factor of thirty is the whole story, and none of it is
libakgl's.
Related: the `FIXME` in `src/tilemap.c` worrying that the per-tile tileset scan
"is probably not very efficient" is, at eight tilesets, worth **0.8% of the
frame** (16.40 ms vs 16.26 ms). It is a real O(tiles x tilesets) loop and it
should still be fixed, but it is not where the time is, and nobody should
reorganise the loader for it.
### The pools are linear scans, and only the string pool cares
Claiming an actor from an empty pool is 4.0 ns; claiming the last free slot is
36.9 ns — nine times the cost, and still nothing.
The string pool is the exception, and it is instructive. Claiming from an empty
string pool is 3.9 ns; claiming the *last free slot* is 250.9 ns, **64 times**
slower. Same algorithm, same 256-ish entries. The difference is that each
`akgl_String` is `PATH_MAX` + 4 bytes, so the scan touches one reference count
every 4 KiB and takes a cache miss on every candidate. A pool of 256 strings is
a megabyte, and walking it is walking a megabyte.
`akgl_heap_release_string` costs 47.2 ns because it `memset`s all 4,100 bytes
whether the string held a path or one character. Same for `akgl_string_copy`
(32.2 ns) and `akgl_get_property` (85.3 ns), which move `AKGL_MAX_STRING_LENGTH`
bytes unconditionally. None of these is expensive in isolation; all of them are
the same avoidable habit of paying for `PATH_MAX` when you used eleven bytes.
### Errors cost about ten times what success costs
`akgl_actor_update` on an actor whose character has a sprite for its state:
68.4 ns. The same call on an actor whose character does *not*: **616.5 ns**.
That path is not an error in any meaningful sense — the library handles
`AKERR_KEY` and carries on, and `akgl_actor_render` logs it and draws nothing.
It is a normal condition on a partly authored character. But raising it means
claiming a context out of `AKERR_ARRAY_ERROR`, formatting a message with
`vsnprintf`, appending a stack-trace frame, walking the `PROCESS` switch, and
releasing it again. Nine times the cost of the update it replaced.
The design conclusion is not "make errors cheaper". It is that a *routine*
condition should not be reported as an error. A character that has no sprite for
a state should answer that question with a boolean.
### `akgl_game_update` updates every actor sixteen times
`src/game.c:617` loops over `AKGL_TILEMAP_MAX_LAYERS`, and the actor sweep
nested inside it does not filter by layer. Every live actor's `updatefunc` runs
**16 times per frame**. At 68.4 ns per update and 64 actors that is 70 µs of
work to do 4.4 µs of work.
It is invisible in the frame total here because the tilemap blits are three
orders of magnitude larger. On a GPU backend, where the frame might be 2 ms, it
is 3.5% of the frame doing nothing. Filed in `TODO.md`.
### Text has no cache at all
`akgl_text_rendertextat` is 12.6 µs for fifteen characters: it rasterizes the
string, uploads it as a texture, blits it, and destroys the texture — every
call, every frame, for a score that changes once a second. Measuring the string
first with `akgl_text_measure` is 37.3 ns, i.e. free, which tells you the whole
cost is the rasterize-and-upload.
Six HUD readouts is 76 µs a frame. That is fine at 60 fps on this machine and it
is 4% of a 2 ms GPU frame. A one-line cache keyed on (font, string, colour)
would take it to nothing, and it is the single clearest optimisation in the
library.
### Loading is dominated by things that are not the file
A 2x2 fixture map with one tileset takes **11.9 ms** to load and release. Of
that, 1.37 ms — 11.5% — is `memset`ing the 26 MB `akgl_Tilemap` before anything
is read. Most of the rest is decoding the tileset PNG. The JSON is noise:
parsing a small document is 11.3 µs, and the accessors are 14-41 ns each.
`akgl_path_relative` is 3.5 µs, because it is a `realpath(3)` syscall. A map
naming twenty assets pays 70 µs. Also noise, but worth knowing it is a syscall
and not a string operation.
### The collision helpers are fine; the missing broad phase is the problem
`akgl_collide_rectangles` is 24.9 ns when the rectangles overlap (it returns at
the first corner that hits) and 57.9 ns when they do not (all eight corner tests
run). Both are fine.
What the library does not provide is a broad phase, so a caller that wants
collision writes the all-pairs loop: 2016 pairs for 64 actors, 115 µs a frame,
0.7% of a 60 fps budget. That is affordable. It is also O(n²): raise
`AKGL_MAX_HEAP_ACTOR` to 256 and the same loop is 32,640 pairs and 1.9 ms — over
10% of the frame, for a game that has done nothing yet.
### Spawning is cheap, and a third of it is a log line
An actor spawn — pool claim, `memset`, registry insert, release — is 202.3 ns
with the library's logging on and 162.5 ns with SDL's log priority raised so the
message is never formatted. **20% of a spawn is formatting a log line nobody
reads**, and that is with output going to a sink that throws it away; write it to
a terminal and it is far worse. `akgl_actor_initialize` and
`akgl_character_sprite_add` both log unconditionally at `INFO`.
## Defects these tests found
Six, and they are filed where defects live: `TODO.md`, under **Performance ->
Defects the perf suites found**, items 28-33, each with its file, line,
functional consequence, and what fixing it would touch. They are not repeated
here.
Worth saying in this document, because it is the argument for having written the
suites at all: stress testing breaks things unit tests do not reach. Two of the
six are process-killing — an error-context leak that aborted on the 129th path
resolution (fixed, with a regression test), and a pooled-string leak that turns
into a segfault rather than an `AKGL_ERR_HEAP` around the 52nd map load. Neither
is reachable by a suite that calls each function a handful of times, and neither
had anything to do with speed. They came out of loops that ran the same call ten
thousand times in one process, which is what a running game does and what
nothing else in this tree did.
## What this report does not cover
- **One machine.** No ARM, no Raspberry Pi, no Windows, no macOS. The ratios
should travel; the absolute numbers will not.
- **No GPU backend.** Everything drawing-related is a software rasterizer under
the dummy video driver. The share of a frame that belongs to libakgl is a
floor, not an estimate.
- **No audio.** `src/audio.c` is not benchmarked at all; the mixer under the
dummy driver does not do the work a real device would.
- **No controller input.** The event path is driven by SDL and has no gamepad to
drive it under the dummy driver.
- **No heap profiling beyond the static footprint.** Nothing here measures peak
or steady-state heap usage. Correctness of the heap *is* covered now, by
`cmake --build build --target memcheck`, which runs these same binaries under
valgrind — `tests/benchutil.h` cuts the iteration counts by three orders of
magnitude when it finds itself there, so a benchmark becomes a path-coverage
program. That run found six more defects, including a leak of the parsed JSON
document in every asset loader; they are in `TODO.md` under **Memory
checking**. What is still missing is a profile: how much a running game
actually holds at once, and how that grows over an hour.
- **Single-threaded throughout.** The library is not thread-safe by design
(`akgl_game_state_lock` guards one field), and nothing here tests contention.

404
README.md
View File

@@ -1,404 +0,0 @@
## How do I initialize a game
Initialize the global game object with info about your game
```c
strncpy((char *)&game.name, "sdl3-gametest", 256);
strncpy((char *)&game.version, "0.0.1", 32);
strncpy((char *)&game.uri, "net.aklabs.games.sdl3-gametest", 256);
```
Call the game initialization routines and lock the game state for further initialization
```c
PASS(e, akgl_game_init());
PASS(e, akgl_game_state_lock());
```
If you have a registry properties file, load it. If you don't have a properties file, use `akgl_set_property("prop_name", "prop_value")` to populate the required game properties.
```c
PASS(e, akgl_registry_load_properties(YOUR_REGISTR_FILEPATH));
```
Initialize your physics engine and renderer of choice
```c
PASS(e, akgl_render_init2d(renderer));
PASS(e, akgl_physics_init_arcade(physics));
```
Unlock the game state
```c
PASS(e, akgl_game_state_unlock());
```
## What is in a properties file (or, What properties must I set if I don't have one?)
```json
{
"properties": {
"game.screenwidth": "640",
"game.screenheight": "480",
"physics.gravity.y": "1024.0",
"physics.drag.y": "1.0"
}
}
```
Physics properties (gravity and drag along X, Y and Z) are optional and default to `0`.
## How do I update and render the game world in my main loop
In your game loop (or in your `SDL_AppIterate` method), lock the game state, call the game update function, and then unlock the game state
```c
PASS(e, akgl_game_state_lock());
PASS(e, renderer->frame_start(renderer));
SDL_RenderClear(renderer->sdl_renderer);
PASS(e, akgl_game_update(NULL));
PASS(e, renderer->frame_end(renderer));
PASS(e, akgl_game_state_unlock());
```
## How do I get an actor on screen
Load a sprite for a character. Sprites are JSON documents describing 2D sprites, frames, and looping. Sprites are named, and sprite names must be unique.
```c
PASS(e, akgl_sprite_load_json(SOME_FILENAME))
```
Load a character from a JSON file. Characters map sprites to actor states and define physics characteristics like movement speed. Each character is named, and character names must be unique.
```c
PASS(e, akgl_character_load_json(SOME_FILENAME))
```
You don't strictly have to load sprites and characters from json files, you can initialize them yourself, it's just tedious work. Here's an example of initializing a 32x32 sprite from a spritesheet that uses the first 4 frames in a looping animation.
```c
akgl_SpriteSheet *sheet;
akgl_Sprite *sprite;
akgl_Character *character;
PASS(e, akgl_heap_next_spritesheet(&sheet);
PASS(e, akgl_spritesheet_initialize(sheet, 32, 32, IMAGE_FILENAME));
PASS(e, akgl_heap_next_sprite(&sprite));
PASS(e, akgl_sprite_initialize(sprite, SPRITE_NAME, &sheet);
sprite->frames = 4;
sprite->frameids = [0, 1, 2, 3];
sprite->width = 32;
sprite->height = 32;
sprite->speed = 1000;
sprite->loop = true;
strncpy((char *)&sprite->name, "SPRITE NAME", AKGL_SPRITE_MAX_NAME_LENGTH);
PASS(e, akgl_heap_next_character(&character));
PASS(e, akgl_character_initialize(&character, "CHAR NAME"));
PASS(e, akgl_character_sprite_add(&character, &sprite, STATE_MASK));
// Set the character acceleration and scale if desired
character->ax = 64.0;
character->ay = 64.0;
character->sx = 2.0;
character->sy = 2.0;
```
Initialize an actor. Actors are named ("player", "Quest NPC", whatever) and names must be unique.
```c
akgl_Actor *myactor = NULL;
PASS(e, akgl_heap_next_actor(&myactor);
PASS(e, akgl_actor_initialize(&myactor, "ACTOR_NAME"));
```
Assign a character to the actor by looking up the `akgl_Character` from the AKGL registry and assign it.
```c
myactor->basechar = SDL_GetPointerProperty(
AKGL_REGISTRY_CHARACTER,
"CHARACTER_NAME",
NULL);
FAIL_ZERO_BREAK(e, myactor->basechar, AKERR_REGISTRY, "Character missing");
```
Give the actor a position and a state, and turn it visible.
```c
myactor->state = 9AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT);
myactor->x = 320;
myactor->y = 240;
myactor->visible = true;
```
## What are in Sprite and Character files
Sprite files:
```json
{
"spritesheet": {
"filename": "RELATIVE_IMAGE_FILE_REFERENCE",
"frame_width": int,
"frame_height": int
},
"name": "UNIQUE_SPRITE_NAME",
"width": int,
"height": int,
"speed": int,
"loop": boolean,
"loopReverse": boolean,
"frames": [
int
]
}
```
* `frames` references the frame indexes in the spritesheet that should be used for this animation. Spritesheets are counted from the top left corner going to the right according to the spritesheet `frame_width` and `frame_height`.
* `loop` says whether or not we should loop the animation
* `loopReverse` says whether or not we should "bounce" the animation (when we reach the end of the frames, start counting back to the beginning, then count to the end, etc). Otherwise the frames are displayed from 0..n and then cycles back to 0.
* `speed` is the number of milliseconds each frame in the animation should appear on the screen
Character files:
```c
{
"name": "UNIQUE_CHARACTER_NAME",
"speedtime": 8,
"speed_x": 0,
"speed_y": 0,
"acceleration_x": 0,
"acceleration_y": 0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_UP",
"AKGL_ACTOR_STATE_MOVING_UP"
],
"sprite": "menupointer"
}[, ...]
]
}
```
* `speedtime` appears to be legacy and unused.
* `speed_[xy]` and `acceleration_[xy]` are physics parameters that specify the top speed and acceleration rate (in pixels per nanosecond) of the character in physics simulations. The effect of acceleration depends on the physics simulation being used at the time (which may or may not account for gravity, drag, etc).
* `sprite_mappings` map a set of actor state flag bitmasks (assume everything in `state` is `OR`ed together) to a sprite name. The game engine uses this to automatically pick the correct sprite (by name) for a given set of state flags. You need one of these for every possible state the character may be used in.
## How do I load a tilemap from the filesystem and display it on screen with my actors
The engine ONLY supports TilED TMJ tilemaps with tileset external references. Load a tilemap into the global `akgl_Tilemap *gamemap` object.
```c
PASS(e, akgl_tilemap_load(PATHSTRING, gamemap));
```
Actors will be automatically populated from objects in the tilemap object layers. Actor state flags here must be expressed as an integer, you can't (yet) use the same array of strings that is used in character json files.
```json
"objects":[
{
"gid":147,
"height":16,
"id":1,
"name":"player",
"properties":[
{
"name":"character",
"type":"string",
"value":"little guy"
},
{
"name":"state",
"type":"int",
"value":24
}],
"rotation":0,
"type":"actor",
"visible":true,
"width":16,
"x":440.510088317656,
"y":140.347239175702
}[, ... ]
```
Check if the tilemap wants to use its own physics, and if you want to allow that, override the global physics simulation
```c
if ( gamemap->use_own_physics == true ) {
physics = &gamemap->physics;
}
```
Tilemap physics specification follows. A map can specify its own physics properties (drag, gravity) without specifying a custom model.
```json
"properties":[
{
"name":"physics.drag.y",
"type":"float",
"value":0
},
{
"name":"physics.gravity.y",
"type":"float",
"value":0
},
{
"name":"physics.model",
"type":"string",
"value":"arcade"
}],
```
The global `gamemap` object is automatically displayed if it is populated. Actors are drawn at the appropriate map layer depending on the actor's `layer` property (warning: this may be replaced with a `z` property soon.)
## How do I get the screen width and height
The most direct is to call `SDL_GetCurrentDisplayMode` to get the parameters from the returned `SDL_DisplayMode` structure (`->w` and `->h`).
The simplest way is to check the global `camera` object's `camera->w` and `camera->h` object. You may have more than one camera on a scene, and it's theoretically possible that the global camera object has been overriden and no longer represents the full screen.
The most reliable engine-centric way is to use `akgl_get_property` to get the property from the engine. Properties are read and stored as strings, so if you need to do these kinds of things a lot, cache the integer value somewhere.
```c
akgl_String *width = NULL;
int screenwidth = NULL;
PASS(e, akgl_get_property("game.screenwidth", &width, "0"));
PASS(e, aksl_atoi(width->data, &screenwidth));
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
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.
```sh
cmake --build build --target mutation
scripts/mutation_test.py --target src/tilemap.c --list
scripts/mutation_test.py --target src/tilemap.c --max-mutants 10
scripts/mutation_test.py --threshold 40 --junit mutation-junit.xml
```
The default run covers all libakgl-owned files under `src/`. Use repeated `--target` options to narrow the scope. A surviving mutant identifies behavior that the current tests do not verify; the script prints its file, line, operator, and exact edit. The real working tree is never mutated.
## Performance testing
`tests/perf.c` and `tests/perf_render.c` are benchmarks rather than unit tests: they drive the hot paths — pool acquire and release, the registry, the physics sweep, the per-actor update and render, the tilemap draw, text, and asset loading — and print what each one costs per operation.
```sh
ctest --test-dir build -L perf --output-on-failure # benchmarks only, about 30 seconds
ctest --test-dir build -LE perf # everything except the benchmarks
AKGL_BENCH_SCALE=0.1 ctest --test-dir build -L perf # a tenth of the iterations
```
Each measurement is the best of five runs and is held to a budget set at roughly ten times the recorded baseline, so a suite that turns red means an algorithmic regression rather than a busy machine. Budgets are enforced only in an optimized build at full scale; below `AKGL_BENCH_SCALE=1.0`, and in a coverage build, they are reported without failing.
`PERFORMANCE.md` carries the recorded baseline, the frame budget it adds up to, and what the numbers say — including the raw-SDL control rows that separate what libakgl costs from what the rasterizer costs. The six defects the stress tests turned up, and the targets the numbers are measured against, are in `TODO.md` under **Performance**.
## Memory checking
`memcheck` runs the suites that already exist under valgrind rather than adding suites of its own. The perf binaries carry most of it: they are the only programs in the tree that load assets, draw a scene, and run a frame loop in one process, and `tests/benchutil.h` cuts their iteration counts by three orders of magnitude when it detects valgrind — a benchmark walks one path a hundred thousand times, a leak check wants every path walked once.
```sh
cmake --build build --target memcheck # everything, about 30 seconds
scripts/memcheck.sh -R tilemap # one suite; ctest selection flags pass through
scripts/memcheck.sh -LE perf # skip the benchmarks
```
The run forces `SDL_VIDEO_DRIVER=dummy`, `SDL_RENDER_DRIVER=software` and `SDL_AUDIO_DRIVER=dummy` so the vendor GPU stack is never loaded — that removes thousands of unfixable findings inside the driver without suppressing anything. Definite leaks, invalid accesses and uninitialised reads are counted and set the exit status; "still reachable" is not, because that is what SDL and FreeType keep for the process lifetime. Suppressions for genuine third-party findings are in `scripts/valgrind.supp`.
What it found the first time it ran is in `TODO.md` under **Memory checking**.

1766
TODO.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
#ifndef _SDL_GAMECONTROLLERDB_H_
#define _SDL_GAMECONTROLLERDB_H_
// Taken from https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt on Fri Jul 31 01:11:07 PM EDT 2026
// Taken from https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt on Tue May 26 01:00:21 PM EDT 2026
#define AKGL_SDL_GAMECONTROLLER_DB_LEN 2253
#define AKGL_SDL_GAMECONTROLLER_DB_LEN 2228
const char *SDL_GAMECONTROLLER_DB[] = {
"03000000300f00000a01000000000000,3 In 1 Conversion Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b8,x:b3,y:b0,platform:Windows",
@@ -86,7 +86,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000c82d00001b30000000000000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000c82d00001c30000000000000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b2,paddle2:b5,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000c82d00001d30000000000000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000c82d00000a31000000000000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000c82d00001530000000000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000c82d00001630000000000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000c82d00001730000000000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
@@ -248,7 +247,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000ac0500001a06000000000000,GameSir T3 2.02,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000373500000410000000000000,GameSir T4 Kaleid,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows",
"03000000373500009410000000000000,GameSir Tegenaria Lite,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000373500001611000000000000,GameSir X5 Lite,a:b0,b:b1,x:b3,y:b4,back:b10,guide:b12,start:b11,leftstick:b13,rightstick:b14,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b8,righttrigger:b9,platform:Windows",
"030000004c0e00001035000000000000,Gamester,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows",
"030000000d0f00001110000000000000,GameStick Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows",
"0300000047530000616d000000000000,GameStop,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows",
@@ -432,7 +430,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000380700008084000000000000,Mad Catz TE2 PS4 Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
"030000002a0600001024000000000000,Matricom,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:Windows",
"030000009f000000adbb000000000000,MaxJoypad Virtual Controller,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows",
"030000008f0e00001330000000000000,Mayflash N64 Adapter,a:b1,b:b2,back:b8,dpdown:h0.8,dpleft:h0.2,dpright:h0.1,dpup:h0.4,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a3~,righty:a2,start:b9,x:b0,y:b3,platform:Windows",
"030000008f0e00001330000000000000,Mayflash Adapter,a:b1,b:b2,back:b8,dpdown:h0.8,dpleft:h0.2,dpright:h0.1,dpup:h0.4,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a3~,righty:a2,start:b9,x:b0,y:b3,platform:Windows",
"03000000250900000128000000000000,Mayflash Arcade Stick,a:b1,b:b2,back:b8,leftshoulder:b0,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b7,start:b9,x:b5,y:b6,platform:Windows",
"03000000242f00003700000000000000,Mayflash F101,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows",
"03000000790000003018000000000000,Mayflash F300 Arcade Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows",
@@ -520,7 +518,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"030000006f0e00000901000000000000,PDP PS3 Versus Fighting,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows",
"030000006f0e00008901000000000000,PDP Realmz Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
"030000008f0e00004100000000000000,PlaySega,a:b1,b:b0,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,start:b8,x:b4,y:b3,platform:Windows",
"03000000d620000020a7000000000000,PowerA Advantage Nintendo Switch 2 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,misc2:b16,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
"03000000d620000011a7000000000000,PowerA Core Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
"03000000dd62000015a7000000000000,PowerA Fusion Nintendo Switch Arcade Stick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
"03000000d620000012a7000000000000,PowerA Fusion Nintendo Switch Fight Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
@@ -553,7 +550,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000250900000500000000000000,PS3 Controller,a:b2,b:b1,back:b9,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b0,y:b3,platform:Windows",
"030000004c0500006802000000000000,PS3 Controller,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b10,lefttrigger:a3~,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:a4~,rightx:a2,righty:a5,start:b8,x:b3,y:b0,platform:Windows",
"030000004f1f00000800000000000000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows",
"03000000632500007505000000000000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
"03000000632500007505000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows",
"03000000888800000803000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b9,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b0,platform:Windows",
"03000000888800000804000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Windows",
"030000008f0e00000300000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b3,y:b0,platform:Windows",
@@ -643,8 +640,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000921200004547000000000000,Retro Bit Sega Genesis Adapter,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,start:b6,x:b3,y:b4,platform:Windows",
"03000000790000001100000000000000,Retro Controller,a:b1,b:b2,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b6,lefttrigger:b7,rightshoulder:b4,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows",
"03000000830500006020000000000000,Retro Controller,a:b0,b:b1,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b8,righttrigger:b9,start:b7,x:b2,y:b3,platform:Windows",
"03000000632500007805000000000000,Retro Fighters D6,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b10,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows",
"03000000790000001600000000000000,Retro Fighters D6,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows",
"03000000632500007805000000000000,Retro Fighters Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows",
"0300000003040000c197000000000000,Retrode Adapter,a:b0,b:b4,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Windows",
"03000000bd12000013d0000000000000,Retrolink Sega Saturn Classic Controller,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b5,lefttrigger:b6,rightshoulder:b2,righttrigger:b7,start:b8,x:b3,y:b4,platform:Windows",
"03000000bd12000015d0000000000000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Windows",
@@ -691,7 +687,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000a306000020f6000000000000,Saitek PS2700 PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows",
"03000000300f00001101000000000000,Saitek Rumble,a:b2,b:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows",
"03000000e804000000a0000000000000,Samsung EIGP20,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000bd12000010d1000000000000,Sanwa 4Button,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,x:b2,y:b3,platform:Windows",
"03000000bd12000010d1000000000000,Sanwa 4Button,a:b0,b:b1,x:b2,y:b3,dpup:-a1,dpdown:+a1,dpleft:-a0,dpright:+a0,platform:Windows",
"03000000c01100000252000000000000,Sanwa Easy Grip,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows",
"03000000c01100004350000000000000,Sanwa Micro Grip P3,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,x:b3,y:b2,platform:Windows",
"03000000411200004550000000000000,Sanwa Micro Grip Pro,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a1,righty:a2,start:b9,x:b1,y:b3,platform:Windows",
@@ -705,14 +701,14 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"030000004f04000028b3000000000000,Score A,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows",
"03000000952e00002577000000000000,Scuf PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
"03000000a30c00002500000000000000,Sega Genesis Mini 3B Controller,a:b2,b:b1,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,righttrigger:b5,start:b9,platform:Windows",
"03000000a30c00002400000000000000,Sega Mega Drive Mini 6B Controller,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b6,lefttrigger:b7,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Windows",
"03000000a30c00002400000000000000,Sega Mega Drive Mini 6B Controller,a:b2,b:b1,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,rightshoulder:b4,righttrigger:b5,leftshoulder:b6,lefttrigger:b7,back:b8,start:b9,x:b3,y:b0,platform:Windows",
"03000000d804000086e6000000000000,Sega Multi Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:a2,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Windows",
"0300000000050000289b000000000000,Sega Saturn Adapter,a:b1,b:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows",
"0300000000f000000800000000000000,Sega Saturn Controller,a:b1,b:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,rightshoulder:b7,righttrigger:b3,start:b0,x:b5,y:b6,platform:Windows",
"03000000730700000601000000000000,Sega Saturn Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows",
"03000000b40400000a01000000000000,Sega Saturn Controller,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Windows",
"030000003b07000004a1000000000000,SFX,a:b0,b:b2,back:b7,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b9,righttrigger:b5,start:b8,x:b1,y:b3,platform:Windows",
"03000000632500002705000000000000,ShanWan Gamepad,+lefty:+a2,-lefty:-a1,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,rightshoulder:b7,rightstick:b14,righttrigger:b9,start:b11,x:b3,y:b4,platform:Windows",
"03000000632500002705000000000000,ShanWan Q36,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,rightshoulder:b7,righttrigger:b9,start:b11,x:b3,y:b4,platform:Windows",
"03000000f82100001900000000000000,Shogun Bros Chameleon X1,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows",
"03000000120c00001c1e000000000000,SnakeByte 4S PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
"03000000140300000918000000000000,SNES Controller,a:b0,b:b1,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Windows",
@@ -741,7 +737,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000120c0000160e000000000000,Steel Play Metaltech PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
"03000000110100001914000000000000,SteelSeries,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftstick:b13,lefttrigger:b6,leftx:a0,lefty:a1,rightstick:b14,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000381000001214000000000000,SteelSeries Free,a:b0,b:b1,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Windows",
"03000000381000004114000000000000,SteelSeries Nimbus Cloud,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000110100003114000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
"03000000381000003014000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows",
"03000000381000003114000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows",
@@ -785,7 +780,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"030000005f140000c501000000000000,Trust Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows",
"03000000b80500000210000000000000,Trust Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows",
"030000005f1400002a01000000000000,Trust Predator GM1200,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,x:b3,y:b4,platform:Windows",
"03000000580400000a10000000000000,Trust Sight Fighter,a:b0,b:b1,back:b5,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,start:b2,x:b3,y:b4,platform:Windows",
"030000004f04000087b6000000000000,TWCS Throttle,dpdown:b8,dpleft:b9,dpright:b7,dpup:b6,leftstick:b5,lefttrigger:-a5,leftx:a0,lefty:a1,righttrigger:+a5,platform:Windows",
"03000000411200000450000000000000,Twin Shock,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a3,righty:a4,start:b11,x:b3,y:b0,platform:Windows",
"03000000d90400000200000000000000,TwinShock PS2 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows",
@@ -809,8 +803,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"030000006f0e00000302000000000000,Victrix PS4 Pro Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
"030000006f0e00000702000000000000,Victrix PS4 Pro Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
"0300000034120000adbe000000000000,vJoy Device,a:b0,b:b1,back:b15,dpdown:b6,dpleft:b7,dpright:b8,dpup:b5,guide:b16,leftshoulder:b9,leftstick:b13,lefttrigger:b11,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b14,righttrigger:b12,rightx:a3,righty:a4,start:b4,x:b2,y:b3,platform:Windows",
"03000000d23800001010000000000000,Void Genesis,a:b0,b:b1,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b2,leftstick:b9,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b18,misc2:b21,misc3:b20,paddle1:b22,paddle2:b15,paddle3:b23,paddle4:b17,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a5,start:b19,x:b3,y:b4,platform:Windows",
"03000000d23800001210000000000000,Void Genesis,a:b0,b:b1,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b2,leftstick:b9,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b18,misc2:b21,misc3:b20,paddle1:b22,paddle2:b15,paddle3:b23,paddle4:b17,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a5,start:b19,x:b3,y:b4,platform:Windows",
"03000000120c0000ab57000000000000,Warrior Joypad JS083,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows",
"030000007e0500003003000000000000,Wii U Pro,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,leftshoulder:b6,leftstick:b11,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b12,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Windows",
"0300000032150000030a000000000000,Wildcat,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows",
@@ -919,7 +911,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000c82d00000351000000010000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
"03000000c82d00001290000001000000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X",
"03000000c82d00004028000000010000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X",
"03000000c82d00000160000001000000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
"03000000c82d00000160000001000000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
"03000000c82d00000161000000010000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
"03000000c82d00000260000001000000,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
"03000000c82d00000261000000010000,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
@@ -949,7 +941,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000503200000110000045010000,Atari VCS Classic,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b3,start:b2,platform:Mac OS X",
"03000000503200000110000047010000,Atari VCS Classic Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b3,start:b2,platform:Mac OS X",
"03000000503200000210000047010000,Atari VCS Modern Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a4,rightx:a2,righty:a3,start:b8,x:b2,y:b3,platform:Mac OS X",
"03000000e30500008106000000010000,Austgame GameCube Adapter,a:b0,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:+a4,leftx:a0,lefty:a1,misc3:b5,misc4:b6,rightshoulder:b4,righttrigger:-a4,rightx:a2,righty:a3,start:b7,x:b1,y:b3,platform:Mac OS X",
"030000008a3500000102000000010000,Backbone One,a:b0,b:b1,back:b16,dpdown:b11,dpleft:b13,dpright:b12,dpup:b10,guide:b17,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3~,start:b15,x:b2,y:b3,platform:Mac OS X",
"030000008a3500000201000000010000,Backbone One,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X",
"030000008a3500000202000000010000,Backbone One,a:b0,b:b1,back:b16,dpdown:b11,dpleft:b13,dpright:b12,dpup:b10,guide:b17,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3~,start:b15,x:b2,y:b3,platform:Mac OS X",
@@ -1127,7 +1118,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000110100002014000000000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X",
"03000000110100002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X",
"03000000381000002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X",
"03000000381000004114000000010000,SteelSeries Nimbus Cloud,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X",
"05000000484944204465766963650000,SteelSeries Nimbus Plus,a:b0,b:b1,back:b15,dpdown:b11,dpleft:b13,dpright:b12,dpup:b10,guide:b16,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3~,start:b14,x:b2,y:b3,platform:Mac OS X",
"050000004e696d6275732b0000000000,SteelSeries Nimbus Plus,a:b0,b:b1,back:b15,dpdown:b11,dpleft:b13,dpright:b12,dpup:b10,guide:b16,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3~,start:b14,x:b2,y:b3,platform:Mac OS X",
"03000000381000003014000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X",
@@ -1259,16 +1249,12 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000c82d00001230000011010000,8BitDo Ultimate,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b2,paddle2:b5,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000c82d00001330000011010000,8BitDo Ultimate,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b26,paddle1:b23,paddle2:b19,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"05000000c82d00001230000000010000,8BitDo Ultimate,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000c82d00000b31000014010000,8BitDo Ultimate 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"03000000c82d00001260000011010000,8BitDo Ultimate 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b17,paddle2:b16,paddle3:b2,paddle4:b5,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"05000000c82d00001260000001000000,8BitDo Ultimate 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b17,paddle2:b16,paddle3:b2,paddle4:b5,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000c82d00000a31000014010000,8BitDo Ultimate 2C,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"03000000c82d00001d30000011010000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"05000000c82d00001b30000001000000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000c82d00001530000011010000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000c82d00001630000011010000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000c82d00001730000011010000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"06000000c82d00000320000007010000,8BitDo Ultimate Wired Controller for Xbox,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"03000000c82d00000121000011010000,8BitDo Xbox One SN30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"05000000c82d00000121000000010000,8BitDo Xbox One SN30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"05000000a00500003232000001000000,8BitDo Zero,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Linux",
@@ -1290,7 +1276,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000020500000913000010010000,Anbernic RG P01,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000373500000710000010010000,Anbernic RG P01,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"05000000373500004610000001000000,Anbernic RG P01,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"19000000010000000100000000010000,Anbernic RG28XX Controller,a:b4,b:b3,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b7,leftstick:b12,misc1:b1,rightshoulder:b8,rightstick:b13,start:b10,x:b5,y:b6,platform:Linux",
"03000000190e00000110000010010000,Aquaplus Piece,a:b1,b:b0,back:b3,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,start:b2,platform:Linux",
"03000000790000003018000011010000,Arcade Fightstick F300,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux",
"03000000a30c00002700000011010000,Astro City Mini,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux",
@@ -1325,7 +1310,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"05000000bc2000000055000001000000,Betop AX1 BFM,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000bc2000006412000011010000,Betop Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b30,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux",
"030000006b1400000209000011010000,Bigben,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
"030000006b1400000055000011010000,Bigben Interactive Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux",
"03000000120c0000300e000011010000,Brook Audio Fighting Board PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
"03000000120c0000310e000011010000,Brook Audio Fighting Board PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
"03000000120c0000200e000011010000,Brook Mars PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
@@ -1363,7 +1347,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000558500001b06000010010000,GameSir G4 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"05000000ac0500002d0200001b010000,GameSir G4s,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b33,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000ac0500007a05000011010000,GameSir G5,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b16,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000373500005810000001020000,GameSir K1,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000373500009710000001020000,GameSir Kaleid Flux,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000ac0500001a06000011010000,GameSir T3 2.02,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"03000000373500000410000044010000,GameSir T4 Kaleid,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
@@ -1463,7 +1446,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000242f00008a00000011010000,JYS Adapter,a:b1,b:b4,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b0,y:b3,platform:Linux",
"03000000314100001935000000010000,LeadJoy Xeno Plus,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:+a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:+a5,rightx:a4,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"030000006f0e00000103000000020000,Logic3 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"030000006f0e000001f5000010010000,Logic3 Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"030000006d040000d1ca000000000000,Logitech Chillstream,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
"030000006d040000d1ca000011010000,Logitech Chillstream,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
"030000006d04000016c2000010010000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
@@ -1554,10 +1536,10 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000853200000706000012010000,Nacon GC100,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"030000006b140000010c000010010000,Nacon GC400ES,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux",
"05000000853200000503000000010000,Nacon MGX Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"0300000085320000160d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
"0300000085320000170d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
"0300000085320000180d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux",
"0300000085320000190d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
"0300000085320000180d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux",
"0300000085320000160d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
"030000000d0f00000900000010010000,Natec Genesis P44,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
"030000004f1f00000800000011010000,NeoGeo PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux",
"0300000092120000474e000000010000,NeoGeo X Arcade Stick,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b9,x:b3,y:b2,platform:Linux",
@@ -1636,7 +1618,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000d62000000520000050010000,PowerA Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"03000000d62000000b20000001010000,PowerA Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"03000000d62000000f20000001010000,PowerA Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b7,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"03000000d62000006520000002010000,PowerA Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"030000006d040000d2ca000011010000,Precision Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
"03000000250900000017000010010000,PS SS N64 Adapter,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b5,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2~,righty:a3,start:b8,platform:Linux",
"03000000ff1100004133000010010000,PS2 Controller,a:b2,b:b1,back:b8,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux",
@@ -1720,8 +1701,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"0300000032150000140a000001010000,Razer Wolverine Ultimate Xbox,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"030000000d0f0000c100000010010000,Retro Bit Legacy16,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,guide:b12,leftshoulder:b4,lefttrigger:b6,misc1:b13,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux",
"03000000790000001100000010010000,Retro Controller,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b4,righttrigger:b5,start:b9,x:b0,y:b3,platform:Linux",
"03000000632500007805000011010000,Retro Fighters D6,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux",
"03000000790000001600000011010000,Retro Fighters D6,a:b1,b:b2,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b10,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux",
"0300000003040000c197000011010000,Retrode Adapter,a:b0,b:b4,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Linux",
"190000004b4800000111000000010000,RetroGame Joypad,a:b1,b:b0,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux",
"0300000081170000990a000001010000,Retronic Adapter,a:b0,leftx:a0,lefty:a1,platform:Linux",
@@ -1772,8 +1751,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"030000005e0400008e02000073050000,Speedlink Torid,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"030000005e0400008e02000020200000,SpeedLink Xeox Pro Analog,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"03000000de280000ff11000001000000,Steam Virtual Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"05000000381000001214000001000000,SteelSeries Free,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a3,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"05000000381000004114000000010000,SteelSeries Nimbus Cloud,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"05000000381000001214000001000000,SteelSeries Free,a:b0,b:b1,x:b3,y:b4,back:b10,guide:b12,start:b11,leftstick:b13,rightstick:b14,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a2,righttrigger:a3,platform:Linux",
"050000004e696d6275732b0000000000,SteelSeries Nimbus Plus,a:b0,b:b1,back:b10,guide:b11,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b12,x:b2,y:b3,platform:Linux",
"03000000381000003014000075010000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"03000000381000003114000075010000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
@@ -1819,10 +1797,9 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"03000000e00d00000300000003000000,TRBot Virtual Joypad,a:b11,b:b12,back:b15,dpdown:b6,dpleft:b3,dpright:b4,dpup:b5,leftshoulder:b17,leftstick:b21,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b22,righttrigger:a2,rightx:a3,righty:a4,start:b16,x:b13,y:b14,platform:Linux",
"03000000f00600000300000003000000,TRBot Virtual Joypad,a:b11,b:b12,back:b15,dpdown:b6,dpleft:b3,dpright:b4,dpup:b5,leftshoulder:b17,leftstick:b21,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b22,righttrigger:a2,rightx:a3,righty:a4,start:b16,x:b13,y:b14,platform:Linux",
"030000005f140000c501000010010000,Trust Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux",
"03000000f51000001370000000000000,Turtle Beach ReactR,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"03000000f51000001370000000010000,Turtle Beach ReactR,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"06000000f51000000870000003010000,Turtle Beach Recon,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"05000000f51000008f01000001000000,Turtle Beach Rematch Nintendo Switch Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux",
"03000000f51000001370000000010000,Turtle Beach ReactR,a:b0,b:b1,x:b2,y:b3,back:b6,guide:b8,start:b7,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux",
"03000000f51000001370000000000000,Turtle Beach ReactR,a:b0,b:b1,x:b2,y:b3,back:b6,guide:b8,start:b7,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux",
"03000000100800000100000010010000,Twin PlayStation Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux",
"03000000c0160000e105000010010000,Ultimate Atari Fight Stick,a:b1,b:b2,back:b9,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,rightshoulder:b4,righttrigger:b5,start:b8,x:b0,y:b3,platform:Linux",
"03000000151900005678000010010000,Uniplay U6,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
@@ -1909,7 +1886,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"060000005e040000120b00000d050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"060000005e040000120b00000f050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"050000005e040000130b000022050000,Xbox Series X Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"050000005e040000130b000023050000,Xbox Series X Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
"060000005e040000120b000011050000,Xbox Series X Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
"03000000450c00002043000010010000,XEOX SL6556 BK,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux",
"05000000172700004431000029010000,XiaoMi Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b6,leftstick:b13,lefttrigger:a7,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Linux",
@@ -2037,7 +2013,6 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"5553422c322d6178697320382d627574,iBuffalo Super Famicom Controller,a:b1,b:b0,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b17,rightshoulder:b18,start:b10,x:b3,y:b2,platform:Android",
"64306137363261396266353433303531,InterAct GoPad,a:b24,b:b25,leftshoulder:b23,lefttrigger:b27,leftx:a0,lefty:a1,rightshoulder:b26,righttrigger:b28,x:b21,y:b22,platform:Android",
"532e542e442e20496e74657261637420,InterAct HammerHead FX,a:b23,b:b24,back:b30,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b26,leftstick:b22,lefttrigger:b28,leftx:a0,lefty:a1,rightshoulder:b27,rightstick:b25,righttrigger:b29,rightx:a2,righty:a3,start:b31,x:b20,y:b21,platform:Android",
"05000000300f00001201000000780f00,Jess Tech Dual Analog Pad,a:b22,b:b23,back:b28,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b24,leftstick:b30,lefttrigger:b25,leftx:a0,lefty:a1,rightshoulder:b26,rightstick:b31,righttrigger:b27,rightx:a3,righty:a2,start:b29,x:b20,y:b21,platform:Android",
"65346535636333663931613264643164,Joy-Con,a:b21,b:b22,back:b29,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b25,lefttrigger:b27,leftx:a0,lefty:a1,rightshoulder:b26,righttrigger:b28,rightx:a2,righty:a3,start:b30,x:b23,y:b24,platform:Android",
"33346566643039343630376565326335,Joy-Con (L),a:b0,b:b1,back:b7,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b3,leftstick:b15,rightshoulder:b20,start:b17,x:b19,y:b2,platform:Android",
"35313531613435623366313835326238,Joy-Con (L),a:b0,b:b1,back:b7,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b3,leftstick:b15,rightshoulder:b20,start:b17,x:b19,y:b2,platform:Android",

View File

@@ -1,34 +1,3 @@
/**
* @file actor.h
* @brief A live thing in the world: state bits, motion, hierarchy, and behaviour hooks.
*
* An actor is an instance; the akgl_Character it points at is its template. The
* actor carries only what is unique to it -- where it is, which way it is
* moving, which animation frame it is on -- and borrows speed, acceleration and
* the state-to-sprite map from the character. That is what makes a hundred of
* one kind of thing cheap.
*
* **State is a bitmask**, not an enum. An actor is facing left *and* moving left
* *and* alive at the same time, and the whole combination is the key that
* selects a sprite (see akgl_character_sprite_get). Adding a state means adding
* a bit here *and* a name in `src/actor_state_string_names.c`, or character JSON
* cannot refer to it.
*
* **Behaviour attaches as function pointers**, not by inheritance. Every actor
* gets the library's default `updatefunc`, `renderfunc`, `facefunc`,
* `movementlogicfunc`, `changeframefunc` and `addchild` from
* akgl_actor_initialize; a game that wants a different flavour of anything
* replaces the pointer on that one actor.
*
* **Motion is split four ways** -- thrust, environmental, acceleration, and the
* velocity that is their sum -- so that a character's own walking and the
* world's gravity and drag can be reasoned about separately. See physics.h for
* the model and which fields mean what.
*
* Actors are pool objects (akgl_heap_next_actor) published in
* #AKGL_REGISTRY_ACTOR under their name.
*/
#ifndef _AKGL_ACTOR_H_
#define _AKGL_ACTOR_H_
@@ -74,386 +43,82 @@
#define AKGL_ACTOR_STATE_UNDEFINED_30 1 << 30 // 1073741824 0100 0000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_31 1 << 31 // 2147483648 1000 0000 0000 0000
/** @brief Bits in an actor's state word. Fixed by the width of `int32_t state`. */
#define AKGL_ACTOR_MAX_STATES 32
// This is an array of strings equal to actor states from 1-32.
// This is built by a utility script and not kept in git, see
// the Makefile for lib_src/actor_state_string_names.c
/**
* @brief Bit position -> state name, as text. Index `i` names the bit `1 << i`.
*
* akgl_registry_init_actor_state_strings builds
* #AKGL_REGISTRY_ACTOR_STATE_STRINGS out of this, which is what lets character
* JSON write `"AKGL_ACTOR_STATE_FACE_LEFT"` instead of `2`. A bit whose name
* here does not match its `#define` above cannot be referred to from JSON at
* all.
*
* @warning Three known defects, all tracked in TODO.md items 24-26: the
* definition in `src/actor_state_string_names.c` is 32 entries while
* this declares 33, so reading index 32 reads past the object; bits 11
* and 12 are named `UNDEFINED_11`/`UNDEFINED_12` rather than
* `MOVING_IN`/`MOVING_OUT`; and the comment above about a generator
* script is stale -- there is no such script, and the file is tracked
* in git and maintained by hand.
*/
extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1];
/** @brief Every facing bit. Clear this before setting one, so an actor faces exactly one way. */
#define AKGL_ACTOR_STATE_FACE_ALL (AKGL_ACTOR_STATE_FACE_DOWN | AKGL_ACTOR_STATE_FACE_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_FACE_UP)
/** @brief Every planar movement bit. The two depth bits, MOVING_IN and MOVING_OUT, are not in it. */
#define AKGL_ACTOR_STATE_MOVING_ALL (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_MOVING_UP | AKGL_ACTOR_STATE_MOVING_DOWN)
/** @brief Longest actor name, including the terminator. Names are truncated, not rejected. */
#define AKGL_ACTOR_MAX_NAME_LENGTH 128
/** @brief Children one actor can carry. A child moves with its parent rather than simulating. */
#define AKGL_ACTOR_MAX_CHILDREN 8
/** @brief Actors in the pool. Override before including heap.h to change it; see heap.h. */
#define AKGL_MAX_HEAP_ACTOR 64
/** @brief Represents a live actor, including state, motion, hierarchy, and behavior callbacks. */
typedef struct akgl_Actor {
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. One reference per parent, plus one for the registry. */
char name[AKGL_ACTOR_MAX_NAME_LENGTH]; /**< Registry key. Truncated, not rejected, if the source name is longer. */
akgl_Character *basechar; /**< The template this actor instantiates. Borrowed; no reference is taken. Required before the actor can update, render, or simulate. */
uint8_t curSpriteFrameId; /**< Index into the current sprite's `frameids`, not a frame number on the sheet. */
SDL_Time curSpriteFrameTimer; /**< When the current frame was shown. The next frame is due once the sprite's `speed` has elapsed. */
bool curSpriteReversing; /**< Walking the animation backwards, for a sprite with `loopReverse` set. */
uint32_t layer; /**< Which tilemap layer the actor is drawn and simulated on. Set from the object layer it was placed in. */
int32_t state; /**< The `AKGL_ACTOR_STATE_*` bitmask. The whole value is the key that selects a sprite. */
bool movement_controls_face; /**< When set, the default `facefunc` turns the actor to face whichever way it is moving. Clear it for an actor that aims independently. */
void *actorData; /**< The game's own per-actor data. Never read or freed by the library. */
bool visible; /**< Whether to draw at all. An actor off-camera is skipped separately, so this is for deliberate hiding. */
SDL_Time movetimer; /**< Unused. Nothing in the library reads or writes it. */
uint8_t refcount;
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
akgl_Character *basechar;
uint8_t curSpriteFrameId;
SDL_Time curSpriteFrameTimer;
bool curSpriteReversing;
uint32_t layer;
int32_t state;
bool movement_controls_face;
void *actorData;
bool visible;
SDL_Time movetimer;
// Velocity. Combined effect of all forces acting on the actor resulting
// in energy along an axis.
float32_t vx; /**< Velocity along x, world units per second. Recomputed each step as `ex + tx`; writing it directly is overwritten. */
float32_t vy; /**< Velocity along y. On a child actor this is read as an offset from the parent instead. */
float32_t vz; /**< Velocity along z. */
// Environmental velocity. These are the forces acting on the actor by the
// environment (such as gravity and atmospheric drag)
float32_t ex; /**< Environmental velocity along x: what gravity and drag have done. Accumulates across steps. */
float32_t ey; /**< Environmental velocity along y. This is what carries a falling actor. */
float32_t ez; /**< Environmental velocity along z. */
float32_t vx;
float32_t vy;
float32_t vz;
// Gravity. Forces acting on the actor on a given axis due to the forces
// of gravity and drag.
float32_t gx;
float32_t gy;
float32_t gz;
// Thrust. Energy originating only from the actor's own acceleration on a
// given axis, before the effects of gravity and drag.
float32_t tx; /**< Thrust along x: the actor's own effort. Capped at `sx`, which is why gravity can outrun top speed and walking cannot. */
float32_t ty; /**< Thrust along y, capped at `sy`. */
float32_t tz; /**< Thrust along z, capped at `sz`. */
// Acceleration. These are borrowed from the base character object.
// A given axis resets to 0 when the actor stops moving in a given axis.
float32_t ax; /**< Acceleration along x, signed by the direction of travel. Copied from the character each step by the default movement logic. */
float32_t ay; /**< Acceleration along y, signed the same way. */
float32_t az; /**< Acceleration along z. Not set by the default movement logic. */
// Max speed. These are borrowed from the base character object.
float32_t sx; /**< Maximum thrust along x, copied from the character. */
float32_t sy; /**< Maximum thrust along y. */
float32_t sz; /**< Maximum thrust along z. */
float32_t tx;
float32_t ty;
float32_t tz;
// Position.
float32_t x; /**< Position in map pixels. For a child, an offset from the parent. */
float32_t y; /**< Position in map pixels. Also what the tilemap's perspective band scales against. */
float32_t z; /**< Depth. Carried through the simulation but not used at draw time. */
float32_t scale; /**< Draw scale, 1.0 for natural size. Written by akgl_tilemap_scale_actor, or forced to 1.0 when tilemap scaling is off. */
struct akgl_Actor *children[AKGL_ACTOR_MAX_CHILDREN]; /**< Attached actors, `NULL` in unused slots. Each holds a reference and is released with the parent. */
struct akgl_Actor *parent; /**< The actor this one is attached to, or `NULL`. A child is positioned relative to it and does not simulate on its own. */
akerr_ErrorContext AKERR_NOIGNORE *(*updatefunc)(struct akgl_Actor *obj); /**< Per-frame logic: facing, then animation frame. Defaults to akgl_actor_update. */
akerr_ErrorContext AKERR_NOIGNORE *(*renderfunc)(struct akgl_Actor *obj); /**< Per-frame draw. Defaults to akgl_actor_render. */
akerr_ErrorContext AKERR_NOIGNORE *(*facefunc)(struct akgl_Actor *obj); /**< Chooses the facing bits. Defaults to akgl_actor_automatic_face. */
akerr_ErrorContext AKERR_NOIGNORE *(*movementlogicfunc)(struct akgl_Actor *obj, float32_t dt); /**< Called by the physics step before gravity. Defaults to akgl_actor_logic_movement. May raise AKGL_ERR_LOGICINTERRUPT to skip the rest of the step. */
akerr_ErrorContext AKERR_NOIGNORE *(*changeframefunc)(struct akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems); /**< Advances the animation. Defaults to akgl_actor_logic_changeframe. */
akerr_ErrorContext AKERR_NOIGNORE *(*addchild)(struct akgl_Actor *obj, struct akgl_Actor *child); /**< Attaches a child. Defaults to akgl_actor_add_child. */
float32_t x;
float32_t y;
float32_t z;
float32_t scale;
struct akgl_Actor *children[AKGL_ACTOR_MAX_CHILDREN];
struct akgl_Actor *parent;
akerr_ErrorContext AKERR_NOIGNORE *(*updatefunc)(struct akgl_Actor *obj);
akerr_ErrorContext AKERR_NOIGNORE *(*renderfunc)(struct akgl_Actor *obj);
akerr_ErrorContext AKERR_NOIGNORE *(*facefunc)(struct akgl_Actor *obj);
akerr_ErrorContext AKERR_NOIGNORE *(*movementlogicfunc)(struct akgl_Actor *obj, float32_t dt);
akerr_ErrorContext AKERR_NOIGNORE *(*changeframefunc)(struct akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems);
akerr_ErrorContext AKERR_NOIGNORE *(*addchild)(struct akgl_Actor *obj, struct akgl_Actor *child);
} akgl_Actor;
/**
* @brief Zero a pooled actor, name it, wire up its default behaviour, and register it.
*
* Sets `scale` to 1.0 and `movement_controls_face` to true, installs all six
* default function pointers, publishes the actor in #AKGL_REGISTRY_ACTOR, and
* takes the first reference. It does *not* set a character: the actor cannot
* update, render, or simulate until akgl_actor_set_character has run.
*
* @param obj Pooled actor to initialize, normally from akgl_heap_next_actor.
* Required. Any previous contents are discarded -- including its
* child list, so release an actor rather than reinitializing it.
* @param name Registry key, NUL-terminated. Required. Truncated at
* #AKGL_ACTOR_MAX_NAME_LENGTH. An existing entry with the same name
* is silently replaced, and the actor it displaced becomes
* unreachable rather than being released.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p name is `NULL`.
* @throws AKERR_KEY If the actor cannot be written into #AKGL_REGISTRY_ACTOR --
* in practice, because akgl_registry_init has not run.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_initialize(akgl_Actor *obj, char *name);
/**
* @brief Bind an actor to a registered character, and copy its top speeds.
*
* Looks the character up by name and takes its `sx`/`sy` as this actor's speed
* limits, zeroing `ax` and `ay` so the actor starts from rest. The character is
* borrowed, not referenced: releasing it out from under a live actor leaves a
* dangling pointer.
*
* @param obj The actor to bind. Required.
* @param basecharname Registry name of the character. Required. Rebinding an
* actor mid-life is allowed, and is how a character swaps
* its whole sprite set at once.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p basecharname is `NULL`, or if no
* character is registered under that name. The last case is a lookup
* miss reported with a pointer status rather than AKERR_KEY.
*
* @note `sz` is not copied and `az` is not zeroed, so an actor's depth speed
* keeps whatever it had. The default movement logic re-copies all three
* each step, which papers over it for anything that simulates.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_set_character(akgl_Actor *obj, char *basecharname);
/**
* @brief Draw an actor's current animation frame, if it is on camera.
*
* Selects the sprite for the actor's current state, checks it against the
* camera, and blits the frame at the actor's position translated into screen
* space. A child actor is drawn at its parent's position plus its own, which is
* what makes an offset mean an offset.
*
* Several things are skipped rather than reported, because a frame is not the
* place to fail: an actor with no sprite for its state, one off camera, one with
* `visible` clear, and one whose frame index has run past its current sprite
* are all simply not drawn.
*
* @param obj The actor to draw. Required, along with its `basechar`.
* @return `NULL` on success -- including every skipped case above -- otherwise
* an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
* @throws AKERR_* Whatever akgl_sprite_sheet_coords_for_frame or the renderer's
* `draw_texture` raises.
*
* @note The destination height is computed from the sprite's *width*, so a
* non-square sprite is drawn square.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_render(akgl_Actor *obj);
/**
* @brief Per-frame logic: turn the actor to face its direction, then advance its animation.
*
* Calls the actor's `facefunc`, then selects the sprite for the resulting state
* and calls `changeframefunc` if that sprite's frame time has elapsed. An actor
* with no sprite registered for its current state is left alone and reported as
* success -- states change faster than art gets drawn, and a missing sprite
* should not stop the frame.
*
* @param obj The actor to update. Required, along with its `basechar`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
* @throws AKERR_* Whatever `facefunc` or `changeframefunc` raises. Note that an
* AKERR_KEY from either is swallowed along with the missing-sprite case.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_update(akgl_Actor *obj);
/**
* @brief The default `movementlogicfunc`: turn movement bits into signed acceleration.
*
* Re-copies the character's speed limits onto the actor -- so a change to the
* character takes effect next step -- and sets `ax`/`ay` to plus or minus the
* character's acceleration according to which movement bits are set. It does not
* integrate anything; the physics backend does that.
*
* Opposing bits do not cancel: `MOVING_LEFT` wins over `MOVING_RIGHT` because it
* is tested first. An actor with no movement bits keeps its previous
* acceleration rather than being zeroed, which is why the input handlers clear
* it themselves on key release.
*
* @param obj The actor to compute acceleration for. Required, along with its
* `basechar`.
* @param dt Seconds since the previous step. Accepted for the hook's signature;
* this implementation does not use it, since it sets an acceleration
* rather than integrating one.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
*
* @note A replacement for this hook may raise AKGL_ERR_LOGICINTERRUPT to tell
* the physics step to skip the rest of this actor's tick. This
* implementation never does.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_movement(akgl_Actor *obj, float32_t dt);
/**
* @brief The default `changeframefunc`: step to the next animation frame.
*
* Walks forward through the sprite's frames; at the last one it either wraps to
* 0, or -- for a sprite with both `loop` and `loopReverse` -- turns round and
* walks back down, turning again at frame 0. A sprite with `loop` clear also
* wraps to 0 rather than holding the final frame.
*
* @param obj The actor whose frame index to advance. Required.
* @param curSprite The sprite currently selected for the actor, whose frame
* count and loop flags decide what happens at the end.
* Required in practice, and **not** checked -- a `NULL` here is
* a crash, not an error.
* @param curtimems The current clock reading. Accepted for the hook's signature;
* this implementation does not use it, because the caller has
* already decided the frame is due.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems);
/**
* @brief The default `facefunc`: turn the actor to face whichever way it is moving.
*
* Clears every facing bit and sets the one matching the first movement bit it
* finds, in the order left, right, up, down -- so an actor moving diagonally
* faces the horizontal component. An actor with `movement_controls_face` clear
* is left alone entirely, which is the hook for something that aims
* independently of the way it walks.
*
* @param obj The actor to turn. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
*
* @note An actor that has stopped moving is left with no facing bit at all,
* rather than keeping the way it was last facing -- so its state no longer
* matches any "standing still facing left" sprite. The implementation
* carries a TODO saying as much.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_automatic_face(akgl_Actor *obj);
/**
* @brief Attach one actor to another so it moves with it.
*
* A child is positioned relative to its parent rather than simulated: the
* physics step snaps it to the parent's position plus its own velocity fields,
* used here as a fixed offset. That is what a carried lantern or a turret on a
* tank wants.
*
* The parent takes a reference on the child, and releasing the parent releases
* every child with it.
*
* @param obj The parent. Required.
* @param child The actor to attach. Required. Must not already have a parent --
* this builds a tree, not a graph. Its `x`/`y` become an offset
* from the parent's position rather than a world position.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p child is `NULL`.
* @throws AKERR_RELATIONSHIP If @p child already has a parent. Detach it first --
* though note there is no detach function, so in practice this means
* releasing and rebuilding it.
* @throws AKERR_OUTOFBOUNDS If @p obj already has #AKGL_ACTOR_MAX_CHILDREN
* children.
*
* @warning Nothing checks for a cycle. Making an actor its own ancestor makes
* akgl_heap_release_actor recurse until the stack runs out.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child);
/*
* The control-map handler functions ("cmhf"). These are what
* akgl_controller_default binds the arrow keys and the D-pad to, and what a
* game's own akgl_Control bindings can point at. They are called with the
* control map's target actor, not with a global "player", so they work for any
* number of locally controlled actors.
*
* Each pair is symmetric: the `_on` handler starts movement in a direction and
* turns the actor to face it, and the `_off` handler stops it dead -- zeroing
* acceleration, thrust, environmental velocity and velocity on that axis, so
* there is no coasting. That is an arcade feel rather than a physical one, and
* it is deliberate; a game wanting momentum binds its own handlers instead.
*
* An `_on` handler clears *every* facing and movement bit before setting its
* own, so holding two directions at once moves in whichever was pressed last
* rather than diagonally.
*/
/**
* @brief Start the actor moving left, and turn it to face left.
* @param obj The actor to move, supplied by the control map. Required, along
* with its `basechar`, whose acceleration is negated onto `ax`.
* @param event The event that triggered this. Required, but not read -- the
* binding has already decided what the event means.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Stop the actor moving left, zeroing everything on the x axis.
* @param obj The actor to stop. Required. `basechar` is not needed here.
* @param event The event that triggered this. Required, but not read.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
* @note It clears the x axis whichever way the actor was going, so releasing
* left while holding right also stops the rightward movement.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Start the actor moving right, and turn it to face right.
* @param obj The actor to move. Required, along with its `basechar`.
* @param event The event that triggered this. Required, but not read.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Stop the actor moving right, zeroing everything on the x axis.
* @param obj The actor to stop. Required.
* @param event The event that triggered this. Required, but not read.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Start the actor moving up the screen, and turn it to face up.
* @param obj The actor to move. Required, along with its `basechar`, whose y
* acceleration is negated -- y grows downward.
* @param event The event that triggered this. Required, but not read.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Stop the actor moving up, zeroing everything on the y axis.
* @param obj The actor to stop. Required.
* @param event The event that triggered this. Required, but not read.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
* @note This also zeroes `ey`, so an actor under gravity has its accumulated
* fall cancelled by releasing a movement key.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Start the actor moving down the screen, and turn it to face down.
* @param obj The actor to move. Required, along with its `basechar`.
* @param event The event that triggered this. Required, but not read.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Stop the actor moving down, zeroing everything on the y axis.
* @param obj The actor to stop. Required.
* @param event The event that triggered this. Required, but not read.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
* @note Zeroes `ey` as well; see akgl_Actor_cmhf_up_off.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event);
/**
* @brief `SDL_EnumerateProperties` callback that applies an akgl_Iterator to one registered actor.
*
* Runs the operations the iterator asks for, in a fixed order regardless of the
* order the bits were set: layer filter, update, tilemap scaling, render. With
* #AKGL_ITERATOR_OP_LAYERMASK set, an actor on any other layer is skipped
* entirely; with #AKGL_ITERATOR_OP_TILEMAPSCALE clear, `scale` is forced to 1.0
* rather than left alone.
*
* @param userdata The akgl_Iterator carrying the operation flags, passed through
* by `SDL_EnumerateProperties`. Required despite the `void *`.
* @param registry #AKGL_REGISTRY_ACTOR, supplied by SDL.
* @param name The actor's registry key. Required.
*
* @warning This is an SDL callback, so it returns `void` and has nowhere to
* propagate an error to. It ends in `FINISH_NORETURN`, which logs the
* stack trace and then calls libakerror's unhandled-error handler --
* whose default implementation **exits the process** with the error
* status. An actor whose `updatefunc` fails therefore terminates the
* game rather than skipping a frame. Install your own
* `akerr_handler_unhandled_error` if that is not what you want.
*/
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name);
#endif // _AKGL_ACTOR_H_

View File

@@ -1,44 +1,8 @@
/**
* @file assets.h
* @brief Loads the game's startup assets into the global mixer and track table.
*/
#ifndef _ASSETS_H_
#define _ASSETS_H_
#include <akerror.h>
/**
* @brief Load a music file, bind it to the background-music track, and start it playing.
*
* Loads @p fname through `akgl_mixer`, creates a track for it, stores that track
* in `akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM]`, and starts playback. The loaded
* audio is also published as the global `bgm`. This is a *startup* helper: it
* assumes the mixer already exists, so `akgl_game_init` (or a bare
* `akgl_audio_init`) has to have run first, and it overwrites whatever the BGM
* track slot held without releasing it.
*
* Loading is deferred inside SDL_mixer (`MIX_LoadAudio(..., true)` predecodes),
* so a large file costs its decode time here rather than at first play.
*
* On any failure after the audio loads, the audio is destroyed again before the
* error is returned; the track, if one was created, is not.
*
* @param fname Path to a music file in any format SDL_mixer can open. Required.
* Used verbatim -- it is not resolved against `SDL_GetBasePath()`,
* so a relative path is relative to the process working directory.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fname is `NULL`, if SDL_mixer cannot load the
* file (missing, unreadable, or an unsupported format), or if it cannot
* allocate a track for it. The message carries `SDL_GetError()`.
* @throws AKGL_ERR_SDL If the loaded audio cannot be bound to the new track, or
* if playback fails to start.
*
* @note The infinite-loop request does not currently take effect:
* `MIX_PROP_PLAY_LOOPS_NUMBER` is set on property set 0, which is the
* "no properties" sentinel rather than a set this function owns, so the
* call is rejected and the music plays once.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_load_start_bgm(char *fname);
#endif //_ASSETS_H_

View File

@@ -1,321 +0,0 @@
/**
* @file audio.h
* @brief Declares the public audio API.
*
* A small tone generator: a fixed set of voices, each with a waveform, a
* frequency -- held, or walking toward another one -- 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 How often a frequency sweep takes one step, in steps per second.
*
* Sixty, because the machine whose SOUND statement this serves advanced its
* sweep on a 60 Hz interrupt and its tunes are written for that rate. It
* divides #AKGL_AUDIO_SAMPLE_RATE exactly, so a step boundary always lands on a
* whole frame.
*/
#define AKGL_AUDIO_SWEEP_TICK_HZ 60
/** @brief Frames between two steps of a sweep. #AKGL_AUDIO_SAMPLE_RATE / #AKGL_AUDIO_SWEEP_TICK_HZ. */
#define AKGL_AUDIO_SWEEP_TICK_FRAMES (AKGL_AUDIO_SAMPLE_RATE / AKGL_AUDIO_SWEEP_TICK_HZ)
/**
* @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; /**< Whether this voice is sounding. Cleared by the mixer once gate and release are both spent, so it goes quiet without being told to. */
akgl_AudioWaveform waveform; /**< Oscillator shape. Persists across notes; set once with akgl_audio_waveform(). */
float32_t hz; /**< Frequency of the current note. 0.0 when the voice has never sounded. */
/**
* @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;
/** @brief Frames to rise from silence to full level. 0 starts at full level. */
uint32_t attack_frames;
/** @brief Frames to fall from full level to `sustain`. 0 drops to it at once. */
uint32_t decay_frames;
/** @brief Frames to fall to silence after the gate closes. 0 cuts off at once. */
uint32_t release_frames;
/** @brief Level the envelope decays to and holds, 0.0 to 1.0. */
float32_t sustain;
/** @brief Pitch the current sweep started from. Equal to `sweep_to_hz` when the voice is not sweeping. */
float32_t sweep_from_hz;
/** @brief Pitch the sweep is heading for. The pitch holds there once it arrives. */
float32_t sweep_to_hz;
/** @brief Hertz added or subtracted per sweep step. 0.0 means this voice holds one pitch. */
float32_t sweep_step_hz;
} 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.
*
* The device is resumed immediately rather than left paused, which is SDL's
* default -- a caller who set up a voice and heard nothing would have no error
* to explain it.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL If no playback device can be opened -- none present, none
* permitted, or SDL's audio subsystem never initialized -- or if the
* device cannot be resumed. The message carries `SDL_GetError()`.
*/
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, and safe to call twice. It also puts the
* voice table back to its defaults, so a subsequent akgl_audio_init() starts
* from a known state rather than from whatever was left sounding.
*
* @return `NULL`. There is no failure path -- SDL's stream teardown reports
* nothing.
*/
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, and clears any sweep
* akgl_audio_sweep() left on it -- this is one pitch, held.
*
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
* @param hz Frequency in hertz. Must be greater than 0. There is no upper
* bound check, so a frequency above half the sample rate aliases
* rather than being refused.
* @param ms Gate length in milliseconds. Must be non-zero -- a zero-length
* tone is refused rather than treated as "stop", because
* akgl_audio_stop() already means that. Rounded down to a whole
* number of frames, so a duration under ~0.023 ms rounds to
* nothing.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, if @p hz is not
* positive, or if @p ms is 0. Each message says which.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms);
/**
* @brief Sound a note whose pitch walks from one frequency toward another.
*
* A siren, a laser, a falling bomb: akgl_audio_tone() with the pitch moving.
* The note starts at @p from_hz and steps by @p step_hz every
* 1/#AKGL_AUDIO_SWEEP_TICK_HZ of a second toward @p to_hz, stopping when it
* arrives and holding there for whatever is left of @p ms. Direction comes from
* the two frequencies, not from the sign of @p step_hz -- a sweep to a lower
* pitch is @p to_hz below @p from_hz, and @p step_hz stays positive.
*
* The step is taken on the mixer's own frame counter, which is the whole reason
* this is here rather than in a caller's step loop: a caller re-issuing tones
* ties audible pitch to how often it happens to run, so the same siren changes
* shape with the frame rate. Nothing outside this library can see that counter.
*
* Everything else matches akgl_audio_tone(): the envelope, the waveform, the
* gate, and the release that follows it. Equal frequencies are legal and are
* simply a held tone, so a caller translating a statement that computes its own
* limits does not have to special-case them.
*
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
* @param from_hz Pitch the note starts at. Must be greater than 0.
* @param to_hz Pitch the sweep walks toward and then holds. Must be greater
* than 0. Below @p from_hz sweeps down, above it sweeps up,
* equal to it holds.
* @param step_hz Hertz per step. Must be greater than 0; the last step is
* clamped to @p to_hz rather than overshooting it. A step larger
* than the whole interval arrives in one tick.
* @param ms Gate length in milliseconds. Must be non-zero. A gate shorter
* than the sweep needs is not an error -- the note ends partway
* through, which is what a short siren sounds like.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, if @p from_hz,
* @p to_hz or @p step_hz is not positive, or if @p ms is 0. Each
* message says which.
*
* @note A swept voice accumulates its phase one frame at a time instead of
* deriving it from the frame counter the way a held note does. Deriving
* it assumes a constant frequency, and using it here would jump the
* waveform -- an audible click -- at every step. The cost is the small
* drift the derived form exists to avoid, which a note that is changing
* pitch anyway cannot be said to suffer from.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms);
/**
* @brief Silence one voice immediately, skipping its release.
*
* A hard cut, not a note-off: the release stage is not run, so the sound stops
* on the next sample. Stopping a voice that is already silent is a no-op.
*
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table.
*/
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, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
* @param waveform One of the ::akgl_AudioWaveform shapes. The setting persists
* across notes until changed.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, or @p waveform is
* not one of the five defined shapes.
*/
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, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
* @param attack Milliseconds to rise from silence to full level. 0 starts at
* full level.
* @param decay Milliseconds to fall from full level to @p sustain. 0 drops to
* it at once.
* @param sustain Held level while the gate is open, 0.0 to 1.0 inclusive. 0.0 is
* legal and means the note is audible only through its attack and
* decay -- a plucked sound.
* @param release Milliseconds to fall to silence once the gate closes. 0 cuts
* off at once. An attack plus decay longer than the note's gate
* is not an error: the release simply starts partway up the ramp.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, or @p sustain is
* outside 0.0 to 1.0. The three durations are unbounded.
*/
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.
*
* Applied after the voices are summed and before the mix is clamped, so it
* takes effect on notes already sounding. akgl_audio_shutdown() puts it back
* to 1.0.
*
* @param level Master level, 0.0 to 1.0 inclusive. 0.0 is silence.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If @p level is outside 0.0 to 1.0. The message
* reports it.
*/
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, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
* @param active Receives `true` while the voice is sounding, including during
* its release. Required -- the return value is the error context.
* The flag is only cleared by the mixer, so a voice whose time is
* up still reads active until samples are next generated; with no
* device open and nobody calling akgl_audio_mix(), it never
* changes.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table.
* @throws AKERR_NULLPOINTER If @p active is `NULL`.
*/
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 Receives @p frames samples. Required, and must have room for all
* of them -- the count is trusted, not checked against anything.
* Overwritten, not accumulated into.
* @param frames How many samples to generate. 0 is a no-op, not an error.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p frames is negative.
*
* @warning This mutates the voice table -- it advances each voice's frame
* counter and clears `active` on voices that have finished. Calling it
* while a device opened by akgl_audio_init() is running means two
* threads advancing the same voices, and this path does not take the
* stream lock. Use one or the other, not both.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames);
#endif // _AKGL_AUDIO_H_

View File

@@ -1,17 +1,3 @@
/**
* @file character.h
* @brief The reusable half of an actor: movement constants and a state-to-sprite map.
*
* A character is the template; an akgl_Actor is the instance. Everything that is
* the same for every goblin on the map -- top speed, acceleration, frame dwell
* time, and which sprite to draw for which combination of actor-state bits --
* lives here and is shared by pointer, so a hundred actors cost one character.
*
* Characters are pool objects (akgl_heap_next_character) and are published in
* the #AKGL_REGISTRY_CHARACTER property registry under their name, which is how
* akgl_actor_set_character finds them. akgl_registry_init must have run first.
*/
#ifndef _AKGL_CHARACTER_H_
#define _AKGL_CHARACTER_H_
@@ -22,143 +8,29 @@
#define AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH 128
#define AKGL_MAX_HEAP_CHARACTER 256
/** @brief Defines reusable movement parameters and actor-state sprite bindings. */
typedef struct akgl_Character {
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. */
char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH]; /**< Registry key. Truncated, not rejected, if the source name is longer. */
SDL_PropertiesID state_sprites; /**< State bitmask (decimal, as a string) -> akgl_Sprite *. */
uint64_t speedtime; /**< Nanoseconds one sprite frame is held before advancing. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONESEC_MS, which despite its name is nanoseconds-per-millisecond. TODO.md item 6. */
float32_t ax; /**< Acceleration along x, world units per second squared. Copied into an actor by akgl_actor_set_character. */
float32_t ay; /**< Acceleration along y. */
float32_t az; /**< Acceleration along z. Not read from JSON; stays 0 unless set by hand. */
float32_t sx; /**< Maximum speed along x, world units per second. */
float32_t sy; /**< Maximum speed along y. */
float32_t sz; /**< Maximum speed along z. Not read from JSON; stays 0 unless set by hand. */
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_add)(struct akgl_Character *, akgl_Sprite *, int); /**< Bound to akgl_character_sprite_add by akgl_character_initialize. */
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_get)(struct akgl_Character *, int, akgl_Sprite **); /**< Bound to akgl_character_sprite_get by akgl_character_initialize. */
uint8_t refcount;
char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH];
SDL_PropertiesID state_sprites;
uint64_t speedtime;
float32_t ax;
float32_t ay;
float32_t az;
float32_t sx;
float32_t sy;
float32_t sz;
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_add)(struct akgl_Character *, akgl_Sprite *, int);
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_get)(struct akgl_Character *, int, akgl_Sprite **);
} akgl_Character;
/**
* @brief Zero a pooled character, name it, and publish it in the character registry.
*
* Wipes the struct, copies @p name into it, creates the empty state-to-sprite
* property set, binds the `sprite_add`/`sprite_get` function pointers, inserts
* the character into #AKGL_REGISTRY_CHARACTER under @p name, and takes the first
* reference. Everything numeric (speeds, accelerations, `speedtime`) is left at
* zero for the caller -- or akgl_character_load_json -- to fill in.
*
* @param basechar Pooled character to initialize, normally straight from
* akgl_heap_next_character. Required. Any previous contents are
* discarded without releasing the sprites they referenced.
* @param name Registry key, NUL-terminated. Required. Truncated at
* #AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH. A name already in the
* registry is silently replaced, and the character it displaced
* becomes unreachable rather than being released.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p basechar or @p name is `NULL`, or if SDL
* cannot create the state-to-sprite property set (out of memory).
* @throws AKERR_KEY If the character cannot be written into
* #AKGL_REGISTRY_CHARACTER -- in practice, because akgl_registry_init
* has not run and the registry id is still 0.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *basechar, char *name);
/**
* @brief Bind a sprite to one exact combination of actor-state bits.
*
* The map key is the decimal spelling of @p state, so lookups match the *whole*
* value: a sprite added for `FACE_LEFT|MOVING_LEFT` is not found by a lookup for
* `FACE_LEFT` alone. Callers are expected to register every combination they
* intend to draw. Adding a sprite takes a reference on it.
*
* @param basechar Character to add the mapping to. Required. Must already have
* been through akgl_character_initialize.
* @param ref Sprite to draw for @p state. Required. Its `refcount` is
* incremented, so the character keeps it alive.
* @param state The exact actor-state bitmask (`AKGL_ACTOR_STATE_*`) this
* sprite is for. 0 is accepted and is a usable key.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p basechar or @p ref is `NULL`.
*
* @note Re-adding a different sprite for a @p state that is already mapped
* replaces the entry without releasing the sprite it displaced, so the
* displaced sprite's reference is never given back.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state);
/**
* @brief Look up the sprite bound to one exact combination of actor-state bits.
*
* The counterpart to akgl_character_sprite_add, and matched the same way: an
* exact match on the whole @p state value, with no fallback to a subset of the
* bits and no default sprite. Unlike most lookups in this codebase, *not finding
* one is an error* rather than a successful "nothing here" -- an actor with no
* sprite for its current state cannot be drawn.
*
* @param basechar Character to search. Required.
* @param state The exact actor-state bitmask to look up.
* @param dest Receives the mapped sprite. Required -- the return value is
* spoken for by the error context. Set to `NULL` when there is
* no mapping, alongside the AKERR_KEY error.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p basechar or @p dest is `NULL`.
* @throws AKERR_KEY If no sprite is bound to exactly that @p state. The message
* carries the state both as a decimal and as a bit pattern.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest);
// This is an SDL iterator so we can't return our error state from it.
/**
* @brief `SDL_EnumerateProperties` callback that applies an akgl_Iterator to one state-to-sprite entry.
*
* Currently implements exactly one operation: with #AKGL_ITERATOR_OP_RELEASE
* set, it hands the mapped sprite back to its heap layer. Every other flag is
* ignored here. Used to tear a character's sprite map down entry by entry.
*
* @param userdata The akgl_Iterator carrying the operation flags, passed through
* by `SDL_EnumerateProperties`. Required despite the `void *` --
* a `NULL` here is an error, not "no operations".
* @param props The character's `state_sprites` set, supplied by SDL.
* @param name The property key: one state bitmask in decimal. Required.
*
* @warning This is an SDL callback, so it returns `void` and has nowhere to
* propagate an error to. It ends in `FINISH_NORETURN`, which logs the
* stack trace and then calls libakerror's unhandled-error handler --
* whose default implementation **exits the process** with the error
* status. A missing sprite or a `NULL` @p userdata therefore terminates
* the game rather than skipping an entry. Install your own
* `akerr_handler_unhandled_error` if that is not what you want.
*/
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name);
/**
* @brief Build a character from a JSON definition file and register it.
*
* Claims a character from the pool, then reads: `name` (the registry key),
* `sprite_mappings` (an array of `{ "sprite": <name>, "state": [<state names>] }`
* objects), `speedtime` in seconds, `speed_x`, `speed_y`, `acceleration_x`, and
* `acceleration_y`. Each mapping's `state` array is OR-ed together into one
* bitmask by looking every name up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS, so
* `["FACE_LEFT", "MOVING_LEFT"]` becomes a single key.
*
* Every referenced sprite must already be in #AKGL_REGISTRY_SPRITE: this loads
* characters, not sprites, so akgl_sprite_load_json runs first. On any failure
* the pooled character is released again.
*
* @param filename Path to the JSON document. Required. Used verbatim -- it is
* not resolved against `SDL_GetBasePath()`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p filename is `NULL`; if the file cannot be
* opened or does not parse (the message carries jansson's line number
* and text); or if a mapping names a sprite that is not in the sprite
* registry.
* @throws AKERR_KEY If a `state` array contains a name that is not a known
* actor state, if a required top-level key is absent, or if the
* character cannot be added to the registry.
* @throws AKERR_TYPE If a key is present but holds the wrong JSON type -- for
* example `speedtime` as a string. The message names the key.
* @throws AKERR_OUTOFBOUNDS If a `state` array is indexed past its end.
* @throws AKGL_ERR_HEAP If the character pool or the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_load_json(char *filename);
#endif // _AKGL_CHARACTER_H_

View File

@@ -1,337 +1,49 @@
/**
* @file controller.h
* @brief Input binding: SDL events in, actor state changes out.
*
* A control map ties one actor to one keyboard and one gamepad, and holds up to
* #AKGL_MAX_CONTROLS bindings. A binding says "when event X arrives from device
* Y carrying button or key Z, call this handler". Eight maps means up to eight
* locally controlled players, each on its own device.
*
* The host pumps SDL events into akgl_controller_handle_event(), which scans the
* maps in order and stops at the first binding that claims the event -- so a key
* bound in two maps only fires once, in the lower-numbered one.
*
* Alongside that, and independent of it, every key press is pushed into a small
* ring buffer that akgl_controller_poll_key() and akgl_controller_poll_keystroke()
* drain. That serves a caller that wants "is there a key waiting" without owning
* an event loop, and it sees keys whether or not a control map also claimed them.
*
* The two pollers read the same buffer and differ in what they hand back. A game
* asking "was the up arrow pressed" wants a keycode and nothing else, which is
* akgl_controller_poll_key(). A line editor asking "what did the user type" needs
* the modifier state and the character SDL composed -- there is no `"` keycode,
* and no keycode at all for a dead key or an IME commit -- which is
* akgl_controller_poll_keystroke().
*/
#ifndef _CONTROLLER_H_
#define _CONTROLLER_H_
#include <SDL3/SDL.h>
#include <akerror.h>
#include "types.h"
// The binding handlers below take an akgl_Actor *, so this header cannot be
// included before akgl/actor.h without one. akgl_Actor is a typedef of a named
// struct, and repeating a typedef is not C99, so it is included rather than
// forward-declared. actor.h reaches only types.h and character.h, so this does
// not close a cycle.
#include <akgl/actor.h>
/** @brief How many control maps exist -- effectively the local player limit. */
#define AKGL_MAX_CONTROL_MAPS 8
/** @brief Bindings per control map. The default map installed by akgl_controller_default uses 8 of them. */
#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 Bytes of composed text one buffered keystroke can carry, NUL included.
*
* A UTF-8 code point is at most four bytes, so this holds one character and its
* terminator with room to spare. An input event carrying more than fits -- an
* IME committing a whole word at once -- is truncated on a code point boundary
* rather than split through the middle of one.
*/
#define AKGL_CONTROLLER_KEYSTROKE_TEXT 8
/** @brief One buffered keystroke: which key, which modifiers, and what it typed. */
typedef struct {
SDL_Keycode key; /**< The keycode, or 0 for an entry that carries only composed text -- an IME commit, or a character finished by a dead key. */
SDL_Keymod mod; /**< Modifier state when the key went down. 0 on a text-only entry, whose text already reflects them. */
char text[AKGL_CONTROLLER_KEYSTROKE_TEXT]; /**< What the keystroke composed to, UTF-8. The empty string for a key that produces no character, such as an arrow or a bare modifier. */
} akgl_Keystroke;
/** @brief Maps one SDL input to pressed and released callbacks. */
typedef struct {
uint32_t event_on; /**< SDL event type that fires `handler_on`, e.g. `SDL_EVENT_KEY_DOWN`. */
uint32_t event_off; /**< SDL event type that fires `handler_off`, e.g. `SDL_EVENT_KEY_UP`. */
uint8_t button; /**< Gamepad button (`SDL_GamepadButton`) this binding is for. Only consulted for gamepad events. */
SDL_Keycode key; /**< Keycode this binding is for. Only consulted for keyboard events. */
uint8_t axis; /**< Analogue axis. Declared but not yet consulted by akgl_controller_handle_event. */
uint8_t axis_range_min; /**< Low end of the axis range that counts as "on". Not yet consulted. */
uint8_t axis_range_max; /**< High end of that range. Not yet consulted. */
akerr_ErrorContext AKERR_NOIGNORE *(*handler_on)(akgl_Actor *obj, SDL_Event *event); /**< Called with the map's target on `event_on`. Required if `event_on` can fire. */
akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event); /**< Called with the map's target on `event_off`. */
uint32_t event_on;
uint32_t event_off;
uint8_t button;
SDL_Keycode key;
uint8_t axis;
uint8_t axis_range_min;
uint8_t axis_range_max;
akerr_ErrorContext AKERR_NOIGNORE *(*handler_on)(akgl_Actor *obj, SDL_Event *event);
akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event);
} akgl_Control;
/** @brief Groups input bindings for one actor and its input devices. */
typedef struct {
akgl_Actor *target; /**< The actor these bindings drive. A `NULL` target makes the whole map inert, which is how an unused slot is spelled. */
uint16_t nextMap; /**< Number of bindings in use; the index akgl_controller_pushmap writes to next. */
akgl_Control controls[AKGL_MAX_CONTROLS]; /**< The bindings, scanned in order. */
SDL_KeyboardID kbid; /**< Keyboard this map listens to. A keyboard event from any other id is ignored, which is what keeps two players on two keyboards apart. */
SDL_JoystickID jsid; /**< Gamepad this map listens to, matched the same way. */
SDL_MouseID mouseid; /**< Mouse this map listens to. Declared but not yet consulted. */
SDL_PenID penid; /**< Pen this map listens to. Declared but not yet consulted. */
akgl_Actor *target;
uint16_t nextMap;
akgl_Control controls[AKGL_MAX_CONTROLS];
SDL_KeyboardID kbid;
SDL_JoystickID jsid;
SDL_MouseID mouseid;
SDL_PenID penid;
} akgl_ControlMap;
/** @brief Every control map. Zeroed by akgl_game_init; index it by the same id the functions below take. */
extern akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
/**
* @brief Log every attached keyboard and its SDL id.
*
* A diagnostic, not a query: it writes to the SDL log rather than returning
* anything. Its use is finding the `kbid` to hand akgl_controller_default when
* more than one keyboard is attached.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL cannot enumerate keyboards. The message
* carries `SDL_GetError()`; note that "no keyboards attached" is
* reported by SDL as an empty list, not as a failure.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void);
/**
* @brief Dispatch one SDL event to whichever control map binds it.
*
* The entry point for the whole subsystem -- call it for every event the host
* pumps. A key press is recorded in the poll buffer first, whether or not any
* map wants it, and an `SDL_EVENT_TEXT_INPUT` is attached to the press it
* belongs to; then the maps are scanned in index order and, within a map,
* bindings in the order they were pushed. The **first** binding whose event type
* and device id and button/key all match wins, and the scan stops there.
*
* An event nothing binds is not an error: it returns success having done
* nothing, which is what lets a host pass every event through unconditionally.
*
* @param appstate Passed through from SDL's callback. Required -- but only as a
* non-`NULL` token: nothing here reads it. Pass any non-`NULL`
* pointer if your program has no app state.
* @param event The event to dispatch. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
* @throws AKERR_* Whatever the matched binding's handler raises.
*
* @warning A matched binding's handler pointer is not checked, so a control
* pushed with a `NULL` `handler_on` or `handler_off` crashes when its
* event arrives.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_event(void *appstate, SDL_Event *event);
/**
* @brief Declared but not defined under this name. Do not call.
*
* The implementation exists as `gamepad_handle_button_down` in `src/controller.c`
* and is `static`-in-spirit -- it is not declared anywhere -- so a caller that
* uses this declaration compiles and then fails to link. TODO.md, "Known and
* still open" item 10.
*
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @return Nothing; it cannot be called.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_down(void *appstate, SDL_Event *event);
/**
* @brief Declared but not defined under this name. Do not call.
*
* See akgl_controller_handle_button_down. The implementation is
* `gamepad_handle_button_up`.
*
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @return Nothing; it cannot be called.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_up(void *appstate, SDL_Event *event);
/**
* @brief Declared but not defined under this name. Do not call.
*
* See akgl_controller_handle_button_down. The implementation is
* `gamepad_handle_added`, which opens a newly plugged-in gamepad.
*
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @return Nothing; it cannot be called.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_added(void *appstate, SDL_Event *event);
/**
* @brief Declared but not defined under this name. Do not call.
*
* See akgl_controller_handle_button_down. The implementation is
* `gamepad_handle_removed`, which closes an unplugged gamepad.
*
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @return Nothing; it cannot be called.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate, SDL_Event *event);
/**
* @brief Append a binding to a control map.
*
* The binding is copied, so the caller's `akgl_Control` can be a stack local
* reused across pushes -- which is exactly what akgl_controller_default does.
* Bindings can only be appended; there is no remove, and no way to reset a map
* short of zeroing it in ::GAME_ControlMaps directly.
*
* @param controlmapid Which map to append to, 0 through
* #AKGL_MAX_CONTROL_MAPS - 1.
* @param control The binding to copy in. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p control is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p controlmapid is at or above
* #AKGL_MAX_CONTROL_MAPS, or if the map already holds
* #AKGL_MAX_CONTROLS bindings.
*
* @warning A **negative** @p controlmapid is not rejected -- only the upper
* bound is checked -- and indexes before the start of
* ::GAME_ControlMaps. TODO.md, "Known and still open" item 11.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control);
/**
* @brief Bind an actor to the arrow keys and the D-pad, in one call.
*
* Points the map at the named actor and pushes eight bindings: the four arrow
* keys and the four D-pad directions, each wired to the matching
* `akgl_Actor_cmhf_*_on`/`_off` pair. It is the "just give me something that
* works" path -- a game wanting different keys builds its own bindings with
* akgl_controller_pushmap.
*
* @param controlmapid Which map to configure, 0 through
* #AKGL_MAX_CONTROL_MAPS - 1.
* @param actorname Registry name of the actor to drive. Required in practice,
* though a `NULL` is reported as "not found" rather than as
* a null pointer.
* @param kbid SDL keyboard id to listen to. Only events from this
* keyboard match; see akgl_controller_list_keyboards for
* how to find it.
* @param jsid SDL gamepad id to listen to, matched the same way.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If @p controlmapid is at or above
* #AKGL_MAX_CONTROL_MAPS, or if the map cannot hold eight more bindings.
* @throws AKGL_ERR_REGISTRY If @p actorname is not in #AKGL_REGISTRY_ACTOR --
* usually because the actor has not been created yet.
*
* @warning A negative @p controlmapid is not rejected. See
* akgl_controller_pushmap.
* @note It appends rather than replaces, so calling it twice on the same map
* leaves sixteen bindings and the first eight are the ones that fire.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid);
/**
* @brief Open every gamepad currently attached.
*
* SDL will not deliver button events from a gamepad nobody has opened, so this
* runs once at startup -- akgl_game_init calls it. Devices plugged in later are
* SDL_EVENT_GAMEPAD_ADDED events, handled elsewhere.
*
* No gamepads attached is success, not an error.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL reports gamepads present but cannot
* enumerate them, or if one of them cannot be opened. The message
* carries `SDL_GetError()`.
*
* @note The enumeration array is only freed on the success path, so a failure
* part-way through leaks it.
*/
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.
*
* Entries that carry only composed text and no keycode are discarded on the way
* past rather than reported as keycode 0: this form is for a caller that acts on
* keys, and there is no key to report. A caller that wants those characters uses
* akgl_controller_poll_keystroke() instead.
*
* @param keycode Receives the SDL keycode, or 0 when nothing was waiting.
* Required -- the return value is the error context.
* @param available Receives `true` when a keystroke was taken, `false` when the
* buffer was empty. Required. Check this rather than testing
* @p keycode against 0.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p keycode or @p available is `NULL`. Both are
* required; there is no "I only want to know whether one is waiting"
* form.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available);
/**
* @brief Take the oldest waiting keystroke whole: key, modifiers and text.
*
* akgl_controller_poll_key() with nothing thrown away. A line editor needs all
* three: the keycode to recognise Backspace and Return, the modifier state to
* tell Ctrl-C from a `c`, and the composed text because a keycode cannot express
* what a shifted key produces on the user's own layout. `"`, `!`, `(`, `)`, `:`
* and `;` are all unreachable from a keycode alone, and so is every lower-case
* letter.
*
* The text is what SDL composed, taken from the `SDL_EVENT_TEXT_INPUT` that
* follows the key press -- the only correct way to get a character out of SDL,
* and what makes a keyboard layout, a compose key and a dead key work. **SDL
* only sends those events while text input is started**, so a host that wants
* the `text` field populated calls `SDL_StartTextInput()` on its window first.
* Without it, `key` and `mod` still arrive and `text` is always empty.
*
* The two pollers drain the same buffer, so a keystroke taken by one is not
* waiting for the other.
*
* @param dest Receives the keystroke. Required. Written only when a
* keystroke was waiting; zeroed otherwise, so `key` is 0 and
* `text` is the empty string.
* @param available Receives `true` when a keystroke was taken, `false` when the
* buffer was empty. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p available is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, 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.
*
* This empties only the polling buffer. Keys already dispatched to control maps
* have had their effect and cannot be taken back.
*
* @return `NULL`. There is no failure path -- it resets two counters.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void);
#endif // _CONTROLLER_H_

View File

@@ -1,236 +1,6 @@
/**
* @file draw.h
* @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_
#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 Paint an 8x8 grey checkerboard over a region, the way an image editor shows transparency.
*
* A diagnostic backdrop, not a general primitive -- `charviewer` uses it so a
* sprite's transparent pixels are visible rather than blending into black. It
* is the one function in this file that does not follow the file's conventions:
* it draws through the *global* `renderer` rather than a backend the caller
* passes in, it leaves the renderer's draw colour changed, and it reports
* nothing.
*
* @param w Width of the region to cover, in pixels, starting at x = 0.
* @param h Height of the region, starting at y = 0. Both round up to whole 8px
* cells, so a 12-pixel height paints 16.
*/
void akgl_draw_background(int w, int h);
/**
* @brief Plot a single pixel.
* @param self The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param x Horizontal position in render-target pixels. Outside the target
* is clipped by SDL, not reported.
* @param y Vertical position.
* @param color Colour to plot in, including alpha. Blending follows the
* renderer's current blend mode, which this does not change.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the plot
* itself fails. The message carries `SDL_GetError()`.
*/
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, endpoints included.
* @param self The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param x1 Horizontal position of the first endpoint.
* @param y1 Vertical position of the first endpoint.
* @param x2 Horizontal position of the second endpoint.
* @param y2 Vertical position of the second endpoint. Coincident endpoints
* draw a single pixel rather than nothing.
* @param color Colour to draw in, including alpha.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the line
* cannot be drawn.
*/
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 The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param rect The rectangle, in render-target pixels. Required. A zero or
* negative width or height draws nothing and is not reported.
* @param color Colour to draw in, including alpha.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p rect is
* `NULL`.
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the
* outline cannot be drawn.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color);
/**
* @brief Fill a rectangle, outline included.
* @param self The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param rect The rectangle, in render-target pixels. Required. A zero or
* negative width or height fills nothing and is not reported.
* @param color Colour to fill with, including alpha.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p rect is
* `NULL`.
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the fill
* cannot be drawn.
*/
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 The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param x Horizontal position of the centre. Rounded to the nearest whole
* pixel -- the algorithm is integer-only.
* @param y Vertical position of the centre, rounded the same way.
* @param radius Radius in pixels, rounded to the nearest whole pixel. Must not
* be negative; 0 draws the centre pixel alone.
* @param color Colour to draw in, including alpha. The outline is one pixel
* wide and is not anti-aliased.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p radius is negative. The message reports it.
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if any of
* the plotting passes fails. Failures inside the loop are recorded and
* reported once at the end rather than aborting mid-circle, so a partial
* arc may already be on the target.
*/
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.
*
* The region is four-connected -- it spreads up, down, left and right, not
* diagonally -- and its boundary is any pixel whose colour differs from the
* seed's, exactly. There is no tolerance, so an anti-aliased edge stops the fill
* at its first blended pixel and leaves a fringe.
*
* @param self The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param x Horizontal position of the seed pixel, in render-target pixels.
* Must be inside the target.
* @param y Vertical position of the seed pixel.
* @param color Colour to fill with. Written over the region rather than blended,
* since the pixels going back are the ones just read out.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
* @throws AKERR_OUTOFBOUNDS If the seed is outside the render target -- the
* message reports both the seed and the target size -- or if the region
* needs more than #AKGL_DRAW_MAX_FLOOD_SPANS pending spans, in which
* case **it is left partially filled**.
* @throws AKGL_ERR_SDL If the render target cannot be read back, converted,
* uploaded, or blitted.
*
* @warning Not reentrant, and not safe from two threads: the span stack is a
* single file-scope array. Nothing that touches an `SDL_Renderer` is
* thread-safe either way.
*/
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 The backend to read from. Required, along with its `sdl_renderer`.
* @param src Rectangle of the render target to read, in pixels. Required. It
* must fit *entirely* inside the target: SDL would otherwise clip
* the read and hand back a smaller surface than was asked for, which
* a caller pasting it back would not notice.
* @param dest Address of the destination surface. Required, and `*dest` must be
* initialized -- `NULL` to have one allocated (the caller then owns
* it and frees it with `SDL_DestroySurface`), or an existing surface
* of exactly @p src's dimensions to reuse.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, @p src, or @p dest
* is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p src has no area, if it does not fit inside the
* render target, or if a supplied `*dest` is a different size from
* @p src. Each message reports both sets of dimensions.
* @throws AKGL_ERR_SDL If the target size cannot be queried, the pixels cannot
* be read, or the copy into a supplied `*dest` 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 The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param src The surface to paste. Required. Drawn at its own size -- there is
* no scaling -- and *replacing* what is on the target rather than
* blending with it, which is GSHAPE's default behaviour and means a
* saved region's transparent pixels come back as transparent rather
* than letting the background show through.
* @param x Horizontal position of the paste's left edge.
* @param y Vertical position of its top edge. Parts falling outside the
* target are clipped by SDL, not reported.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p src is
* `NULL`.
* @throws AKGL_ERR_SDL If the surface cannot be uploaded as a texture, its blend
* mode cannot be set, or the draw fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y);
#endif //_DRAW_H_

View File

@@ -1,78 +1,18 @@
/**
* @file error.h
* @brief Declares the public error API.
*/
#ifndef _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.
// 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
// to keep the compiler happy.
#define DISABLE_GCC_WARNING_FORMAT_TRUNCATION \
_Pragma("GCC diagnostic push") \
#define DISABLE_GCC_WARNING_FORMAT_TRUNCATION \
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wformat-truncation\"")
#define RESTORE_GCC_WARNINGS \
#define RESTORE_GCC_WARNINGS \
_Pragma("GCC diagnostic pop")
// libakerror reserves statuses 0-255 for the host's errno values and its own
// AKERR_* codes; consumers allocate from AKERR_FIRST_CONSUMER_STATUS upward.
// These are fixed offsets from that base rather than from AKERR_LAST_ERRNO_VALUE
// so that a libc which grows an errno cannot move them out from under us.
//
// 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);
#define AKGL_ERR_SDL AKERR_LAST_ERRNO_VALUE + 1
#define AKGL_ERR_LOGICINTERRUPT AKERR_LAST_ERRNO_VALUE + 2
#endif // _ERROR_H_

View File

@@ -1,32 +1,3 @@
/**
* @file game.h
* @brief Process-wide game state, the startup sequence, and the per-frame tick.
*
* This is the top of the library. `akgl_game_init` brings up SDL, the pools, the
* registries and the audio and font engines; `akgl_game_update` is one frame --
* update every actor, step the physics, draw the world.
*
* There is exactly one of everything. `renderer`, `physics`, `camera`, and
* `gamemap` are globals pointing at the `_akgl_*` storage below them, so a
* program can swap in its own instance by reassigning the pointer without the
* rest of the library knowing. That is the whole extent of the indirection:
* there is no notion of two worlds at once.
*
* The startup order that actually works:
*
* 1. fill in `game.name`, `game.version`, and `game.uri` -- akgl_game_init
* refuses to run without them;
* 2. akgl_game_init();
* 3. akgl_registry_load_properties() or akgl_set_property(), to configure
* screen size, physics constants and so on;
* 4. akgl_render_init2d(renderer) and akgl_physics_factory(physics, ...), both
* of which read that configuration;
* 5. load assets, then loop on akgl_game_update().
*
* @warning None of this is thread-safe beyond the `game.state` mutex, and that
* mutex protects the state flags, not the pools or the registries.
*/
#ifndef _AKGL_GAME_H_
#define _AKGL_GAME_H_
@@ -36,264 +7,66 @@
#include "tilemap.h"
#include "renderer.h"
#include "physics.h"
// AKGL_VERSION used to be defined here by hand, which is how akgl.pc came to
// ship an empty Version field: nothing tied the two together.
#include <akgl/version.h>
/** @brief Slot in ::akgl_tracks reserved for background music. Note that slot 0 is unused. */
#define AKGL_VERSION "0.1.0"
#define AKGL_GAME_AUDIO_TRACK_BGM 1
/** @brief Size of the ::akgl_tracks table. Every simultaneous sound needs its own slot. */
#define AKGL_GAME_AUDIO_MAX_TRACKS 64
/** @brief Nanoseconds in one second. The unit `SDL_GetTicksNS` reports in. */
#define AKGL_TIME_ONESEC_NS 1000000000
/**
* @brief Misnamed: this is nanoseconds per **millisecond**, not milliseconds per second.
*
* One second in milliseconds is 1000. 1000000 is one millisecond in
* nanoseconds, which is how sprite and character load actually use it -- as a
* milliseconds-to-nanoseconds scale factor -- and is not how
* akgl_game_state_lock uses it. TODO.md item 6 proposes renaming it
* `AKGL_TIME_ONEMS_NS`.
*/
#define AKGL_TIME_ONESEC_MS 1000000
/* ==================== GAME STATE VARIABLES =================== */
/** @brief Describes a renderable frame. Declared but not used by anything in the library. */
typedef struct {
float32_t w; /**< Width in pixels. */
float32_t h; /**< Height in pixels. */
SDL_Texture *texture; /**< The frame's texture. */
float32_t w;
float32_t h;
SDL_Texture *texture;
} akgl_Frame;
/** @brief Stores application-defined game-state flags. */
typedef struct {
int32_t flags; /**< Meaning is entirely the application's; the library never reads it. Guard changes with akgl_game_state_lock. */
int32_t flags;
} akgl_GameState;
/** @brief Stores game metadata, timing, synchronization, and FPS accounting. */
typedef struct {
char libversion[32]; /**< libakgl's version, stamped by akgl_game_init. Compared on load to refuse a save from another build. */
char version[32]; /**< The *application's* version. Caller-supplied, required, and must be a semver string. */
char name[256]; /**< Application name. Caller-supplied and required; also becomes SDL's app metadata. */
char uri[256]; /**< Application URI, e.g. a reverse-DNS identifier. Caller-supplied, required, and used as the window title. */
akgl_GameState state; /**< The application's own state flags. */
SDL_Mutex *statelock; /**< Guards `state`. Created by akgl_game_init. */
int16_t fps; /**< Frames drawn during the last completed second. Recomputed once per second, not per frame. */
SDL_Time gameStartTime; /**< `SDL_GetTicksNS()` at akgl_game_init. */
SDL_Time lastIterTime; /**< Timestamp of the most recent akgl_game_updateFPS call. */
SDL_Time lastFPSTime; /**< When `fps` was last recomputed. */
int16_t framesSinceUpdate; /**< Frames counted so far in the current second. */
void (*lowfpsfunc)(void); /**< Called every frame while `fps` is under 30. Defaults to akgl_game_lowfps; replace it to do something more useful than log. */
char libversion[32];
char version[32];
char name[256];
char uri[256];
akgl_GameState state;
SDL_Mutex *statelock;
int16_t fps;
SDL_Time gameStartTime;
SDL_Time lastIterTime;
SDL_Time lastFPSTime;
int16_t framesSinceUpdate;
void (*lowfpsfunc)(void);
} akgl_Game;
/** @brief The SDL window, created by akgl_render_init2d. `NULL` until then. */
extern SDL_Window *window;
/** @brief The background music, loaded by akgl_load_start_bgm. `NULL` until then. */
extern akgl_Tilemap gamemap;
extern MIX_Audio *bgm;
/** @brief The mixer device, created by akgl_game_init. Everything audio goes through it. */
extern MIX_Mixer *akgl_mixer;
/** @brief Playback tracks by slot. #AKGL_GAME_AUDIO_TRACK_BGM is the music track; the rest are the application's to assign. */
extern MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
/** @brief Storage behind the default `camera`. Point `camera` elsewhere rather than reaching for this. */
extern SDL_FRect _akgl_camera;
/** @brief The one game object: metadata, timing, and FPS accounting. */
extern SDL_FRect camera;
extern akgl_Game game;
/** @brief Storage behind the default `renderer`. */
extern akgl_RenderBackend _akgl_renderer;
/** @brief Storage behind the default `physics`. */
extern akgl_PhysicsBackend _akgl_physics;
/** @brief Storage behind the default `gamemap`. */
extern akgl_Tilemap _akgl_gamemap;
extern akgl_RenderBackend renderer;
extern akgl_PhysicsBackend physics;
/** @brief Currently active tilemap. */
extern akgl_Tilemap *gamemap;
/** @brief Currently active renderer. */
extern akgl_RenderBackend *renderer;
/** @brief Currently active physics backend. */
extern akgl_PhysicsBackend *physics;
/** @brief Currently active camera. */
extern SDL_FRect *camera;
/**
* @brief True when every bit of `y` is set in `x`. Not "any of them" -- all of them.
* @warning Unparenthesized: it expands to `(x & y) == y`, so `!AKGL_BITMASK_HAS(a, b)`
* parses as `!(a & b) == b`. Use #AKGL_BITMASK_HASNOT rather than
* negating this. TODO.md item 21.
*/
#define AKGL_BITMASK_HAS(x, y) (x & y) == y
/** @brief True when at least one bit of `y` is missing from `x`. Same parenthesization caveat as #AKGL_BITMASK_HAS. */
#define AKGL_BITMASK_HASNOT(x, y) (x & y) != y
/** @brief Set every bit of `y` in `x`. Modifies `x`. */
#define AKGL_BITMASK_ADD(x, y) x |= y
/** @brief Clear every bit of `y` in `x`. Modifies `x`. */
#define AKGL_BITMASK_DEL(x, y) x &= ~(y)
/** @brief Clear every bit of `x`. Carries its own trailing semicolon, so do not add another. */
#define AKGL_BITMASK_CLEAR(x) x = 0;
/**
* @brief Bring the whole library up: error codes, pools, registries, SDL, audio, fonts, gamepads.
*
* In order: claim the libakgl status band (so every later error has a name),
* stamp the library version, start the frame clock, create the state mutex,
* check that the caller filled in the three required `game` fields, zero the
* pools, create the registries, hand SDL the app metadata, clear the control
* maps, `SDL_Init` video/gamepad/audio, load the bundled controller database,
* open any attached gamepads, start SDL_mixer and SDL_ttf, and finally point
* `renderer`, `physics`, `camera`, and `gamemap` at their default storage.
*
* What it does *not* do: create the window, choose a physics backend, or load
* any configuration. Those read properties, so they come after the caller has
* set them. See the sequence at the top of this file.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If `game.name`, `game.version`, or `game.uri` is
* empty. All three are required and there are no defaults -- the
* window title, SDL's app metadata, and the savegame compatibility
* check are all built from them.
* @throws AKGL_ERR_SDL If the state mutex cannot be created, if `SDL_Init`
* fails, if a controller-database entry is rejected, if SDL_mixer
* cannot start or open the default playback device, or if SDL_ttf
* cannot start. Each message carries `SDL_GetError()`.
* @throws AKERR_STATUS_RANGE_OVERLAP If another component already owns part of
* the libakgl status band. See akgl_error_init.
* @throws AKERR_* Whatever the heap, registry, and controller initializers raise.
*
* @note It takes the state lock on the way in and releases it on the way out, so
* a failure part-way through leaves the mutex held.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init();
/**
* @brief Declared but never defined. Do not call.
*
* There is no definition anywhere in the library, so a translation unit that
* calls this compiles and then fails to link. Screen setup is akgl_render_init2d.
* Tracked in TODO.md under header/implementation surface drift.
*
* @return Nothing; it cannot be called.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init_screen();
/**
* @brief Count this frame, and recompute the frame rate once a second has passed.
*
* Called at the top of akgl_game_update, so a caller running its own loop needs
* to call it itself. `game.fps` is only refreshed when a full second has
* elapsed, so it is a completed-second average rather than an instantaneous
* figure -- and it reads 0 for the first second of the process, which is under
* the low-FPS threshold and so fires `lowfpsfunc` on every frame until the first
* second is up.
*/
void akgl_game_updateFPS();
/**
* @brief Write the game state and the name-to-pointer tables to a save file.
*
* Writes the `akgl_Game` struct verbatim, then four name tables -- actors,
* sprites, spritesheets, characters -- each mapping a registered name to the
* address the object had at save time, and each terminated by a zeroed name and
* a zeroed pointer. The tables are what let akgl_game_load reconnect pointers
* between objects that will be at different addresses next run.
*
* @param fpath Path to write. Required. Opened with `"wb"`; an existing file is
* truncated.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fpath is `NULL`.
* @throws ENOENT, EACCES Or whatever else `fopen(3)` reports, with the path in
* the message.
* @throws AKERR_IO On a stream error or a short write.
*
* @note This is a partial implementation: the name tables are written but the
* objects themselves are not, so the file is not yet enough to restore a
* session. See also TODO.md, "Known and still open" item 7 -- the writer
* and the reader disagree on the name-field widths, so a save with any
* registered spritesheet cannot be read back.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath);
/**
* @brief Read a save file back, refusing one that does not match this build.
*
* Reads the saved `akgl_Game`, then rejects the file unless the library version,
* the game version, the game name, and the game URI all match the running
* program -- versions by exact semver equality, name and URI by string compare.
* Only then does it copy the saved state over `game` and rebuild the four
* old-address-to-current-object maps.
*
* @param fpath Path to read. Required. Opened with `"rb"`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fpath is `NULL`.
* @throws ENOENT, EACCES Or whatever else `fopen(3)` reports.
* @throws AKERR_EOF If a read runs off the end of the file -- which is what a
* truncated or corrupt name table looks like.
* @throws AKERR_IO On a stream error.
* @throws AKERR_VALUE If either version string in the save file, or either in
* the running game, is not valid semver.
* @throws AKERR_API If the save file is from a different library version, game
* version, game name, or game URI.
*
* @note Like akgl_game_save, this is partial: it rebuilds the pointer maps but
* does not yet read back any objects. The four `SDL_CreateProperties`
* sets it builds are never destroyed, so each call leaks them.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath);
/**
* @brief The default `game.lowfpsfunc`: log the current frame rate.
*
* Called from akgl_game_updateFPS on every frame where `game.fps` is under 30.
* It is a placeholder -- the point of the hook is that a game can replace it
* with something that actually sheds work.
*/
void akgl_game_lowfps(void);
/**
* @brief Take the game-state mutex, retrying rather than blocking.
*
* Polls with `SDL_TryLockMutex` on a 100 ms cadence instead of blocking
* outright, so a deadlock reports an error rather than hanging the process.
*
* @return `NULL` once the lock is held, otherwise an error context owned by the
* caller.
* @throws AKGL_ERR_SDL If the budget runs out with the lock still held
* elsewhere. The message carries `SDL_GetError()`, which after a failed
* `SDL_TryLockMutex` is usually stale or empty -- the status is the
* signal, not the text.
*
* @note The budget is meant to be one second but is not: the loop counts against
* #AKGL_TIME_ONESEC_MS, which is nanoseconds-per-millisecond (1000000)
* rather than milliseconds-per-second, so it retries 10,000 times at 100 ms
* and gives up after roughly 16 minutes. TODO.md item 6.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void);
/**
* @brief Release the game-state mutex.
*
* @return `NULL`. `SDL_UnlockMutex` reports nothing, so there is no failure path
* -- including for the case that matters, unlocking a mutex this thread
* does not hold, which is undefined behaviour in SDL rather than an
* error here.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
/**
* @brief One frame: update every actor layer by layer, step the physics, draw the world.
*
* Takes the state lock, counts the frame, then walks layers 0 through
* #AKGL_TILEMAP_MAX_LAYERS calling each live actor's `updatefunc`, optionally
* rescaling it to the tilemap first. Then it steps `physics` and draws through
* `renderer`, and releases the lock.
*
* @param opflags Iterator flags. Optional -- `NULL` selects a default set that
* sweeps one layer at a time, in which case `layerid` is advanced
* by the loop. Only #AKGL_ITERATOR_OP_TILEMAPSCALE is read here;
* with it clear every actor's `scale` is forced to 1.0. Note that
* the flags are *not* forwarded to the physics or render calls,
* both of which are passed `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL If the state lock cannot be taken.
* @throws AKERR_* Whatever an actor's `updatefunc`, akgl_tilemap_scale_actor,
* the physics backend, or the renderer raises.
*
* @warning Every failure path returns with the state lock still held, and a live
* actor's `updatefunc` is called without a `NULL` check -- a hand-built
* actor that never went through akgl_actor_initialize crashes here.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags);
#endif //_AKGL_GAME_H_

View File

@@ -1,31 +1,3 @@
/**
* @file heap.h
* @brief The object pools. This library does not call `malloc`, it claims slots.
*
* Every runtime object -- actors, sprites, spritesheets, characters, strings --
* comes out of a fixed, statically allocated array declared here. A "heap layer"
* is one such array plus its `next`/`release` pair. Allocation is a linear scan
* for a slot whose `refcount` is 0; release decrements, and the slot is zeroed
* and unregistered when the count reaches 0.
*
* The consequence to design around is that exhaustion is a *normal* error, not
* an out-of-memory catastrophe: AKGL_ERR_HEAP means the pool is full, and the
* fix is usually a missing release rather than a bigger pool. Every ceiling
* below is overridable at compile time, so a game that needs 256 actors defines
* `AKGL_MAX_HEAP_ACTOR` before including this -- but the arrays are sized at
* compile time, so the library and everything linking it must agree.
*
* If you need a new kind of runtime object, add a layer here. Do not reach for
* the allocator.
*
* @warning The acquire functions are asymmetric: akgl_heap_next_string takes the
* reference for you, and the other four do not -- their caller is
* expected to take it, which in practice the `*_initialize` function
* does. Until one is taken the slot is still free and the next
* allocation hands out the same pointer. TODO.md, "Known and still
* open" item 8.
*/
#ifndef _AKGL_HEAP_H_
#define _AKGL_HEAP_H_
@@ -51,170 +23,24 @@
#define AKGL_MAX_HEAP_STRING 256
#endif
/** @brief The actor pool. Public so the render and physics sweeps can walk it directly instead of going through the registry. */
extern akgl_Actor HEAP_ACTOR[AKGL_MAX_HEAP_ACTOR];
/** @brief The sprite pool. 16 per actor, on the assumption of one sprite per state combination. */
extern akgl_Sprite HEAP_SPRITE[AKGL_MAX_HEAP_SPRITE];
/** @brief The spritesheet pool. Sized like the sprite pool, though sharing means far fewer are used in practice. */
extern akgl_SpriteSheet HEAP_SPRITESHEET[AKGL_MAX_HEAP_SPRITESHEET];
/** @brief The character pool. */
extern akgl_Character HEAP_CHARACTER[AKGL_MAX_HEAP_CHARACTER];
/** @brief The string pool. Every entry is PATH_MAX bytes, so this is the largest of the five by a wide margin. */
extern akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING];
/**
* @brief Zero every pool, marking every slot free.
*
* Call once at startup, before anything allocates. Calling it again is a reset,
* not a refresh: it does not release textures, clear registries, or consult
* reference counts, so every live object becomes a dangling pointer and every
* registry entry points at a zeroed slot. akgl_game_init calls it for you.
*
* @return `NULL`. There is no failure path today -- it is a series of `memset`s
* -- but check it anyway; the signature exists so a layer that needs
* real setup has somewhere to report from.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init();
/**
* @brief Zero the actor pool only.
*
* Split out from akgl_heap_init so a caller can reset the actors between levels
* while keeping the loaded sprites, sheets, and characters -- those are the
* expensive ones, since they own textures. Pair it with
* akgl_registry_init_actor, which clears the matching registry.
*
* @return `NULL`. No failure path today; see akgl_heap_init.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor();
/**
* @brief Claim a free actor slot.
* @param dest Receives a pointer to the free slot. Required, and **not**
* checked -- a `NULL` here is a crash, not an error context. The
* slot is *not* zeroed and its `refcount` is not incremented; it
* stays free until akgl_actor_initialize takes the reference.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_ACTOR slots is in use.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_actor(akgl_Actor **dest);
/**
* @brief Claim a free sprite slot.
* @param dest Receives a pointer to the free slot. Required and unchecked; not
* zeroed, and no reference taken. See akgl_heap_next_actor.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_SPRITE slots is in use.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_sprite(akgl_Sprite **dest);
/**
* @brief Claim a free spritesheet slot.
* @param dest Receives a pointer to the free slot. Required and unchecked; not
* zeroed, and no reference taken. See akgl_heap_next_actor.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_SPRITESHEET slots is
* in use.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest);
/**
* @brief Claim a free character slot.
* @param dest Receives a pointer to the free slot. Required and unchecked; not
* zeroed, and no reference taken. See akgl_heap_next_actor.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_CHARACTER slots is in
* use.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_character(akgl_Character **dest);
/**
* @brief Claim a free string slot, and take the reference on it.
*
* The odd one out: this *does* increment `refcount`, so the slot is yours the
* moment it returns and stays yours until you call akgl_heap_release_string.
* That is what makes the scratch-buffer idiom -- claim, use, release in
* `CLEANUP` -- safe. The contents are whatever the last holder left; call
* akgl_string_initialize if you need it clean.
*
* @param dest Receives a pointer to the claimed slot. Required and **not**
* checked -- a `NULL` here is a crash, not an error context.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_STRING slots is in
* use. In practice this means a missing release somewhere, not a pool
* that is genuinely too small.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_string(akgl_String **dest);
/**
* @brief Drop a reference to an actor, tearing it down when the last one goes.
*
* At zero it releases every child recursively, clears the actor's entry from
* #AKGL_REGISTRY_ACTOR, and zeroes the slot.
*
* @param ptr The actor to release. Required. A slot whose `refcount` is already
* 0 is re-torn-down rather than rejected, which is harmless on a
* zeroed slot and destructive on a live one that was never
* registered.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*
* @warning Children are released unconditionally, and the recursion has no cycle
* check: an actor reachable from its own child list recurses until the
* stack runs out.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_actor(akgl_Actor *ptr);
/**
* @brief Drop a reference to a sprite, tearing it down when the last one goes.
*
* At zero it clears the sprite's entry from #AKGL_REGISTRY_SPRITE and zeroes the
* slot. The spritesheet it pointed at is *not* released -- the sprite only
* borrowed it.
*
* @param ptr The sprite to release. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_sprite(akgl_Sprite *ptr);
/**
* @brief Drop a reference to a spritesheet, destroying its texture when the last one goes.
*
* The only release that frees a resource outside the pool: at zero it clears the
* entry from #AKGL_REGISTRY_SPRITESHEET, destroys the `SDL_Texture`, and zeroes
* the slot. Any sprite still pointing at the sheet is left with a dangling
* pointer, since nothing takes a reference on a sheet on a sprite's behalf.
*
* @param ptr The spritesheet to release. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*
* @note Destroying a texture is a main-thread operation in SDL, so this must be
* called from the thread that owns the renderer.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_spritesheet(akgl_SpriteSheet *ptr);
/**
* @brief Drop a reference to a character, tearing it down when the last one goes.
*
* At zero it clears the entry from #AKGL_REGISTRY_CHARACTER and zeroes the slot.
*
* @param ptr The character to release. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*
* @note It does not walk the state-to-sprite map, so the references the
* character took in akgl_character_sprite_add are never given back, and
* the SDL property set holding the map is leaked. Release the sprites
* first with akgl_character_state_sprites_iterate and
* #AKGL_ITERATOR_OP_RELEASE if you need them back.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_character(akgl_Character *ptr);
/**
* @brief Drop a reference to a pooled string, zeroing it when the last one goes.
*
* Strings are not registered anywhere, so this is just the reference count and a
* wipe. It is safe to call on a string a function may or may not have claimed,
* which is why the `CLEANUP` blocks in this library call it unconditionally.
*
* @param ptr The string to release. Required -- unlike the pattern elsewhere, a
* `NULL` here is an error rather than a no-op, so `CLEANUP` blocks
* wrap it in `IGNORE()`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_string(akgl_String *ptr);
#endif //_AKGL_HEAP_H_

View File

@@ -1,36 +1,16 @@
/**
* @file iterator.h
* @brief The work order handed to a registry sweep.
*
* There is no iterator object with a `next()` on it. Traversal is SDL's --
* `SDL_EnumerateProperties` over a registry -- and this struct is the `userdata`
* carried into each callback, telling it *which* entries to touch and *what* to
* do to each one. akgl_registry_iterate_actor and
* akgl_registry_iterate_character are the callbacks that read it;
* akgl_game_update, akgl_physics_simulate, and akgl_render_2d_draw_world are the
* entry points that take one.
*
* The operations are independent bits, not an enum: a single sweep can update,
* scale, and render, and they run in that fixed order regardless of how the bits
* were set. Passing `NULL` where an `akgl_Iterator *` is expected is not an
* error at the top-level entry points -- each substitutes its own default set --
* but it *is* an error once inside a callback.
*/
#ifndef _AKGL_ITERATOR_H_
#define _AKGL_ITERATOR_H_
/** @brief Selects operations and an optional layer for actor traversal. */
typedef struct {
uint32_t flags; /**< Bitwise OR of the `AKGL_ITERATOR_OP_*` values below. */
uint8_t layerid; /**< Layer to restrict the sweep to. Read only when #AKGL_ITERATOR_OP_LAYERMASK is set. */
uint32_t flags;
uint8_t layerid;
} akgl_Iterator;
#define AKGL_ITERATOR_OP_UPDATE 1 // 1 Call the actor's updatefunc
#define AKGL_ITERATOR_OP_RENDER 1 << 1 // 2 Call the actor's renderfunc
#define AKGL_ITERATOR_OP_RELEASE 1 << 2 // 4 Release the object back to its heap layer
#define AKGL_ITERATOR_OP_LAYERMASK 1 << 3 // 8 Skip anything whose layer != layerid
#define AKGL_ITERATOR_OP_TILEMAPSCALE 1 << 4 // 16 Scale actors to the tilemap; otherwise force scale 1.0
#define AKGL_ITERATOR_OP_UPDATE 1 // 1
#define AKGL_ITERATOR_OP_RENDER 1 << 1 // 2
#define AKGL_ITERATOR_OP_RELEASE 1 << 2 // 4
#define AKGL_ITERATOR_OP_LAYERMASK 1 << 3 // 8
#define AKGL_ITERATOR_OP_TILEMAPSCALE 1 << 4 // 16
#define AKGL_ITERATOR_OP_UNDEFINED_5 1 << 5 // 32
#define AKGL_ITERATOR_OP_UNDEFINED_6 1 << 6 // 64
#define AKGL_ITERATOR_OP_UNDEFINED_7 1 << 7 // 128

View File

@@ -1,236 +1,17 @@
/**
* @file json_helpers.h
* @brief Typed jansson accessors that report "missing" and "wrong type" as errors.
*
* Every asset in this library is described by a JSON document, and jansson's own
* accessors answer "missing key", "wrong type", and "index past the end" all
* with the same `NULL`. These wrappers split those apart -- AKERR_KEY,
* AKERR_TYPE, AKERR_OUTOFBOUNDS -- so a malformed asset file produces a message
* naming the key and what was wrong with it, instead of a `NULL` dereference
* three frames later.
*
* Conventions that run through the whole set, so they need not be repeated per
* function:
*
* - **Absence is an error here.** Unlike the search functions in libakstdlib,
* these treat a missing key as AKERR_KEY. An *optional* key is expressed by
* passing the resulting error to akgl_get_json_with_default() rather than by
* the accessor staying quiet.
* - **The result comes back through @p dest**, because the return value is the
* error context.
* - **`json_t *` results are borrowed, not owned.** Objects and arrays are
* returned as pointers into the document; they are valid until the document is
* freed and must not be `json_decref`'d.
* - **@p dest is not `NULL`-checked** except where noted, so a `NULL` there is a
* crash rather than an error context.
* - **The document itself is never modified.**
*/
#ifndef _JSON_HELPERS_H_
#define _JSON_HELPERS_H_
#include <akerror.h>
#include "staticstring.h"
/**
* @brief Read a nested object out of a JSON object.
* @param obj The object to read from. Required.
* @param key The member name to look up. A `NULL` key is reported as a missing
* key rather than as a `NULL` pointer.
* @param dest Receives a borrowed pointer to the nested object; not written on
* any failure path. Not checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent. The message names it.
* @throws AKERR_TYPE If @p key is present but is not an object.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest);
/**
* @brief Read a boolean out of a JSON object.
* @param obj The object to read from. Required.
* @param key The member name to look up.
* @param dest Receives the value; not written on any failure path. Not checked
* for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not `true` or `false`. A `0`
* or `1` is a number in JSON, and is refused here.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest);
/**
* @brief Read an integer out of a JSON object.
* @param obj The object to read from. Required.
* @param key The member name to look up.
* @param dest Receives the value, narrowed to `int`. Not written on any failure
* path, and not checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not an integer. This is strict:
* `3.0` is a real in JSON and is refused, not truncated. Use
* akgl_get_json_number_value() where either spelling should be accepted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_integer_value(json_t *obj, char *key, int *dest);
/**
* @brief Read a number out of a JSON object as a `float`.
* @param obj The object to read from. Required.
* @param key The member name to look up.
* @param dest Receives the value, narrowed to `float`. Not written on any
* failure path, and not checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not a number. Integers and
* reals are both accepted, so `1` and `1.0` behave alike.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char *key, float *dest);
/**
* @brief Read a number out of a JSON object as a `double`.
*
* The full-precision form of akgl_get_json_number_value(), for the physics
* constants, which are `double`.
*
* @param obj The object to read from. Required.
* @param key The member name to look up.
* @param dest Receives the value. Not written on any failure path, and not
* checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not a number.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest);
/**
* @brief Read a string out of a JSON object into a pooled akgl_String.
*
* Unlike the other accessors this copies, because the caller wants a buffer it
* can keep rather than a pointer into the document. `*dest` doubles as an input:
* `NULL` means "claim one for me", non-`NULL` means "write into this one".
*
* @param obj The object to read from. Required.
* @param key The member name to look up. Required -- checked here, unlike the
* other accessors in this file.
* @param dest Address of the destination string. Required, and it must be
* *initialized*: set `*dest` to `NULL` to have a pool string claimed
* for you, or to a claimed string to write in place. An
* indeterminate `*dest` is dereferenced. Either way the caller
* releases it with akgl_heap_release_string().
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj, @p key, or @p dest is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not a string.
* @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
*
* @note A value longer than #AKGL_MAX_STRING_LENGTH is truncated silently, and
* is left without a terminator -- this is `strncpy`, not `strlcpy`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest);
/**
* @brief Read an array out of a JSON object.
* @param obj The object to read from. Required.
* @param key The member name to look up.
* @param dest Receives a borrowed pointer to the array; not written on any
* failure path. Not checked for `NULL`. Use `json_array_size()` on
* it and the `akgl_get_json_array_index_*` family to walk it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not an array.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest);
/**
* @brief Read one element of a JSON array as an object.
* @param array The array to read from. Required.
* @param index Zero-based element index. A negative index is reported the same
* way as one past the end.
* @param dest Receives a borrowed pointer to the element; not written on any
* failure path. Not checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p array is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array. The message
* reports the index.
* @throws AKERR_TYPE If the element is not an object.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest);
/**
* @brief Read one element of a JSON array as an integer.
* @param array The array to read from. Required.
* @param index Zero-based element index.
* @param dest Receives the value, narrowed to `int`; not written on any failure
* path. Not checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p array is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array.
* @throws AKERR_TYPE If the element is not an integer. Strict, as in
* akgl_get_json_integer_value().
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_integer(json_t *array, int index, int *dest);
/**
* @brief Read one element of a JSON array into a pooled akgl_String.
* @param array The array to read from. Required.
* @param index Zero-based element index.
* @param dest Address of the destination string, with the same claim-or-reuse
* contract as akgl_get_json_string_value(): required, must be
* initialized, and released by the caller.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p array or @p dest is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array.
* @throws AKERR_TYPE If the element is not a string.
* @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
*
* @note Truncates silently at #AKGL_MAX_STRING_LENGTH, as
* akgl_get_json_string_value() does.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest);
/**
* @brief Turn a "key not found" error from one of the accessors above into a default value.
*
* This is how an optional key is spelled. Run the accessor, hand its error
* context here along with the fallback, and a missing key becomes @p dest
* holding @p defval and a `NULL` return; anything else propagates untouched:
*
* ```c
* int width = 0;
* int width_default = 32;
* PASS(errctx,
* akgl_get_json_with_default(
* akgl_get_json_integer_value(json, "width", &width),
* &width_default,
* &width,
* sizeof(int)
* ));
* ```
*
* A `NULL` @p e -- the accessor succeeded -- is the ordinary case and returns at
* once without touching @p dest. When the error *is* consumed it is also
* released, so the caller must not release it again.
*
* @param e The error context to inspect, straight from an accessor.
* `NULL` means "no error", which is not itself an error.
* @param defval The fallback value to copy. Required when @p e is non-`NULL`.
* @param dest Where to copy it. Required when @p e is non-`NULL`. Must be the
* same destination the accessor was given, and at least
* @p defsize bytes.
* @param defsize Bytes to copy out of @p defval. Trusted, not derived -- it must
* match the type both sides actually are, since this is a
* `memcpy` through `void *` with no type information.
* @return `NULL` when the error was consumed or there was none, otherwise an
* error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p e is non-`NULL` and @p defval or @p dest is
* `NULL`.
* @throws AKERR_* Whatever @p e carried, if it is not one of the statuses this
* defaults on -- an AKERR_TYPE from a key that exists but is the wrong
* type propagates, which is right: that is a malformed document, not an
* omitted setting.
*
* @note It defaults on AKERR_KEY and AKERR_INDEX, but *not* on
* AKERR_OUTOFBOUNDS -- which is the status the
* `akgl_get_json_array_index_*` family actually raises for a short array.
* So this pairs with the object accessors and does not currently give an
* array index a default.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize);
#endif // _JSON_HELPERS_H_

View File

@@ -1,30 +1,3 @@
/**
* @file physics.h
* @brief The pluggable physics backend, and the two implementations that ship.
*
* Same shape as the renderer: a record of function pointers plus an initializer
* that fills it in. `null` accepts every call and changes nothing, which is what
* a menu screen or a test harness wants; `arcade` applies gravity, drag, thrust
* and a speed cap. Selecting between them is akgl_physics_factory, driven by the
* `physics.engine` configuration property -- not a branch in the simulation.
*
* The model an actor is simulated under, and the fields on akgl_Actor that carry
* it:
*
* | Term | Fields | Comes from |
* |-----------------------|--------------|-----------------------------------------------|
* | thrust | `tx, ty, tz` | the actor's own acceleration while it is moving |
* | environmental | `ex, ey, ez` | gravity, less drag |
* | velocity | `vx, vy, vz` | thrust + environmental |
* | max speed | `sx, sy, sz` | the character; caps thrust, not velocity |
* | acceleration | `ax, ay, az` | the character |
*
* Each step: movement logic, then gravity, then drag, then velocity, then move.
* Because the cap is applied to thrust rather than to velocity, gravity can
* carry an actor faster than its stated top speed -- which is the point, for a
* falling one.
*/
#ifndef _PHYSICS_H_
#define _PHYSICS_H_
@@ -32,207 +5,36 @@
#include <akerror.h>
#include <akgl/actor.h>
#include <akgl/iterator.h>
#include <akgl/staticstring.h>
/** @brief Defines a pluggable physics backend and its environmental parameters. */
typedef struct akgl_PhysicsBackend {
akerr_ErrorContext AKERR_NOIGNORE *(*simulate)(struct akgl_PhysicsBackend *self, akgl_Iterator *opflags); /**< Step the whole world. Both backends point this at akgl_physics_simulate. */
akerr_ErrorContext AKERR_NOIGNORE *(*gravity)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /**< Apply environmental acceleration to one actor. */
akerr_ErrorContext AKERR_NOIGNORE *(*collide)(struct akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2); /**< Resolve a collision between two actors. Not called by the simulation loop yet. */
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /**< Commit one actor's velocity to its position. */
akerr_ErrorContext AKERR_NOIGNORE *(*simulate)(struct akgl_PhysicsBackend *self, akgl_Iterator *opflags);
akerr_ErrorContext AKERR_NOIGNORE *(*gravity)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
akerr_ErrorContext AKERR_NOIGNORE *(*collide)(struct akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
double drag_x; /**< Fraction of environmental velocity shed per second along x. 0 disables. From `physics.drag.x`. */
double drag_y; /**< Drag along y. From `physics.drag.y`. */
double drag_z; /**< Drag along z. From `physics.drag.z`. */
double gravity_x; /**< Acceleration along x, world units per second squared. Applied toward screen left. From `physics.gravity.x`. */
double gravity_y; /**< Acceleration along y. Applied down the screen, since y grows downward. From `physics.gravity.y`. */
double gravity_z; /**< Acceleration along z. Applied away from the camera. From `physics.gravity.z`. */
SDL_Time gravity_time; /**< Timestamp of the previous step, in nanoseconds. The simulation's `dt` is measured from this. */
SDL_Time timer_gravity; /**< Unused. Nothing in the library reads or writes it. */
double drag_x;
double drag_y;
double drag_z;
double gravity_x;
double gravity_y;
double gravity_z;
SDL_Time gravity_time;
SDL_Time timer_gravity;
} akgl_PhysicsBackend;
/**
* @brief Gravity for the null backend: accept the call and change nothing.
* @param self The backend. Required -- the one thing this does check.
* @param actor The actor that would have been accelerated. Ignored; `NULL` is
* accepted.
* @param dt Seconds since the previous step. Ignored.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
/**
* @brief Collision for the null backend: accept the call and change nothing.
*
* Note that this *succeeds* where the arcade backend's collide refuses -- the
* null backend's contract is "nothing collides", which is an answer, not a gap.
*
* @param self The backend. Required.
* @param a1 First actor. Ignored; `NULL` is accepted.
* @param a2 Second actor. Ignored; `NULL` is accepted.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
/**
* @brief Movement for the null backend: accept the call and change nothing.
*
* Actors under the null backend do not move on their own. Anything that sets an
* actor's `x`/`y` directly still works -- this only declines to integrate
* velocity.
*
* @param self The backend. Required.
* @param actor The actor that would have moved. Ignored; `NULL` is accepted.
* @param dt Seconds since the previous step. Ignored.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
/**
* @brief Install the null backend's methods.
*
* Reads no configuration and touches no gravity or drag fields, so whatever was
* in them stays -- harmlessly, since none of the null methods look.
*
* @param self The backend to initialize. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *self);
/**
* @brief Accelerate one actor's environmental velocity by gravity.
*
* Adds `gravity * dt` to the actor's `ex`/`ey`/`ez` on each axis whose gravity
* is non-zero, with the sign chosen for screen space: x pulls left, y pulls
* down, z pulls away from the camera. It touches only the environmental term, so
* an actor's own thrust is unaffected and the speed cap does not apply -- which
* is why a falling actor keeps accelerating past its character's top speed.
*
* @param self The backend supplying the gravity constants. Required.
* @param actor The actor to accelerate. Required.
* @param dt Seconds since the previous step. A `dt` of 0 is a no-op; a
* negative one accelerates backwards.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or @p actor is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
/**
* @brief Collision for the arcade backend. Not implemented.
*
* Deliberately loud rather than silently permissive: unlike
* akgl_physics_null_collide, which means "nothing collides", this means "nobody
* has written this yet", and a caller that reaches it should find out.
*
* @param self The backend. Required, and checked before the refusal.
* @param a1 First actor. Never examined.
* @param a2 Second actor. Never examined.
* @return Never `NULL`.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_API Otherwise, always, with the message "Not implemented".
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
/**
* @brief Commit an actor's velocity to its position.
*
* The last step of a simulation tick: `position += velocity * dt` on all three
* axes. It does not clamp to the map, consult the tilemap, or test for
* collisions -- an actor will walk straight through a wall and off the edge of
* the world.
*
* @param self The backend. Required, though nothing on it is read.
* @param actor The actor to move. Required. Its `vx`/`vy`/`vz` must already have
* been computed; akgl_physics_simulate does that immediately
* before calling this.
* @param dt Seconds since the previous step.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or @p actor is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
/**
* @brief Install the arcade backend's methods and read its constants from configuration.
*
* Reads `physics.gravity.x`, `.y`, `.z` and `physics.drag.x`, `.y`, `.z` from
* the property registry, all defaulting to `"0.0"` -- so an arcade backend with
* no configuration behaves like the null one until something is set.
*
* @param self The backend to initialize. Required. Its method pointers are
* installed before the properties are read, so a failure part-way
* leaves a usable backend with some constants still at their
* previous values.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_VALUE If one of the six properties is set to something that is
* not a number.
* @throws ERANGE If one of them does not fit a `double`.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @note With #AKGL_REGISTRY_PROPERTIES uninitialized every property reads back
* as its default, so this silently configures zero gravity and zero drag
* rather than reporting anything. See the warning in registry.h.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self);
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_sidescroller(akgl_PhysicsBackend *self);
/**
* @brief Select and initialize a physics backend by name.
*
* The dispatch point for the whole subsystem: `"null"` and `"arcade"` are the
* two names, and akgl_game_init passes whatever the `physics.engine` property
* holds. Adding a backend means adding a name here, not a branch in the
* simulation.
*
* @param self The backend to initialize. Required.
* @param type The backend name. Required. Matched on its leading characters, so
* `"nullify"` selects `null` and `"arcadia"` selects `arcade`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or @p type is `NULL`.
* @throws AKERR_KEY If @p type matches neither name. The message quotes what was
* asked for.
* @throws AKERR_* Whatever the selected initializer raises.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type);
/**
* @brief Step every live actor forward by the time elapsed since the previous call.
*
* Shared by both backends -- what differs is the `gravity` and `move` they point
* at. For each live actor, in pool order:
*
* - a child actor is snapped to its parent's position plus its own `v` as an
* offset, and skipped entirely -- children do not simulate;
* - an actor with no character is skipped, since the speed and acceleration
* constants live there;
* - thrust accumulates along an axis the actor is moving on, then is clamped to
* the character's maximum speed;
* - the backend's `gravity` runs, drag is applied to the environmental term,
* velocity becomes environmental plus thrust, and the backend's `move`
* commits it.
*
* `dt` is measured from `self->gravity_time`, which is stamped at the end. The
* first call after initialization therefore measures from 0 and produces an
* enormous `dt` -- initialize `gravity_time` from `SDL_GetTicksNS()` before the
* first step if that matters.
*
* @param self The backend. Required, along with its `move` pointer.
* @param opflags Iterator flags. Optional -- `NULL` means "simulate everything".
* Only #AKGL_ITERATOR_OP_LAYERMASK is honoured, restricting the
* step to actors on `layerid`; the update, render, and release
* bits are ignored here.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or `self->move` is `NULL`.
* @throws AKERR_* Whatever an actor's `movementlogicfunc`, or the backend's
* `gravity` or `move`, raises. The first failure aborts the whole step,
* leaving the actors already processed advanced and the rest not.
*
* @note An actor's `movementlogicfunc` can raise AKGL_ERR_LOGICINTERRUPT to say
* "skip the rest of the simulation for me this tick". It is handled here
* and does not propagate -- it is a control signal wearing an error's
* clothes, not a failure. Only `movementlogicfunc` gets that treatment:
* the `gravity` and `move` calls return straight out of the loop on any
* status, LOGICINTERRUPT included, so a backend must not use it to opt an
* actor out from there.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags);
#endif // _PHYSICS_H_

View File

@@ -1,218 +1,29 @@
/**
* @file registry.h
* @brief The name-to-object lookup tables, and the string configuration store.
*
* Nothing in this library is passed around by pointer where a name will do. An
* actor names the character it instantiates, a character names the sprites it
* draws, a sprite names the sheet it cuts frames from -- all resolved at load
* time through these eight registries. That is what lets the whole asset graph
* be described in JSON files that reference each other by name.
*
* They are SDL property sets, not akgl types, so a caller can enumerate one with
* `SDL_EnumerateProperties` -- which is exactly what akgl_registry_iterate_actor
* and akgl_character_state_sprites_iterate are for.
*
* Every id starts at 0, which SDL treats as "no such property set": reads return
* the default and writes are silently dropped. So an uninitialized registry does
* not fail loudly, it fails quietly, and akgl_registry_init has to run first.
*
* @warning akgl_registry_init does *not* initialize
* #AKGL_REGISTRY_PROPERTIES -- akgl_registry_init_properties is a
* separate call, made by akgl_game_init but by nothing else. A program
* that builds its own startup path and skips it gets a silently
* no-op akgl_set_property and an akgl_get_property that always hands
* back the caller's default, which in turn means akgl_render_init2d
* and akgl_physics_init_arcade quietly ignore their configuration.
* TODO.md, "Known and still open" item 3.
*/
#ifndef _REGISTRY_H_
#define _REGISTRY_H_
#include <akgl/error.h>
#include <akgl/staticstring.h>
/** @brief Actor name -> `akgl_Actor *`. Written by akgl_actor_initialize, cleared when an actor's last reference goes. */
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR;
/** @brief Sprite name -> `akgl_Sprite *`. The `name` field from a sprite JSON. */
extern SDL_PropertiesID AKGL_REGISTRY_SPRITE;
/** @brief Resolved image path -> `akgl_SpriteSheet *`. Keyed by path so two sprites sharing an image share the texture. */
extern SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET;
/** @brief Character name -> `akgl_Character *`. What akgl_actor_set_character resolves against. */
extern SDL_PropertiesID AKGL_REGISTRY_CHARACTER;
/** @brief Actor-state name -> its bit value, as a number. Lets character JSON say "AKGL_ACTOR_STATE_FACE_LEFT" instead of 2. */
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR_STATE_STRINGS;
/** @brief Font name -> `TTF_Font *`. Names are chosen by the caller of akgl_text_loadfont, not derived from the file. */
extern SDL_PropertiesID AKGL_REGISTRY_FONT;
/** @brief Music name -> audio handle. Created but not yet populated by anything in the library. */
extern SDL_PropertiesID AKGL_REGISTRY_MUSIC;
/** @brief Configuration key -> string value. Read through akgl_get_property; everything is a string, including numbers. */
extern SDL_PropertiesID AKGL_REGISTRY_PROPERTIES;
/**
* @brief Create the seven asset registries, in dependency order.
*
* Spritesheet, sprite, character, actor, actor-state-strings, font, music. Call
* it once at startup, before loading anything.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL cannot allocate any one of the property sets;
* the message names which registry failed. The registries created before
* the failure are left in place rather than torn down.
*
* @warning This does not create #AKGL_REGISTRY_PROPERTIES -- see the warning on
* this file.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init();
/**
* @brief Create the music registry.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
* @note Overwrites the existing id without destroying the old set, so calling it
* twice leaks the first one along with everything registered in it.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music();
/**
* @brief Create the font registry.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
* @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font();
/**
* @brief Create the actor registry, destroying any previous one first.
*
* The one initializer here that cleans up after itself, which is what makes it
* safe to call between levels: it destroys the old property set before creating
* the replacement, so the actors from the previous map are unregistered in one
* step. Note that it destroys the *registry*, not the actors -- releasing those
* is akgl_heap_release_actor's job.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set. The old one
* has already been destroyed at that point, so the registry is left at 0.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor();
/**
* @brief Create the sprite registry.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
* @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite();
/**
* @brief Create the spritesheet registry.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
* @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet();
/**
* @brief Create the character registry.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
* @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character();
/**
* @brief Create the configuration-property registry.
*
* Deliberately separate from akgl_registry_init, because configuration has to be
* in place before the subsystems that read it start up. akgl_game_init creates
* it; filling it in -- with akgl_registry_load_properties or akgl_set_property --
* is the caller's job, and has to happen before akgl_render_init2d or
* akgl_physics_init_arcade run.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
* @note Overwrites the existing id without destroying the old set, so every
* property set before a second call is lost.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_properties();
/**
* @brief Load a JSON configuration file into the property registry.
*
* Expects a document with a top-level `properties` object whose members are all
* strings; each becomes one entry. Numbers are configured as strings here and
* parsed by whoever reads them -- `game.screenwidth` is `"800"`, not `800`.
*
* @param fname Path to the JSON document. Required. Used verbatim.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fname is `NULL`, or if the file cannot be
* opened or does not parse. The message carries jansson's line number
* and text.
* @throws AKERR_KEY If the document has no top-level `properties` member.
* @throws AKERR_TYPE If `properties` is not an object, or if one of its members
* is not a string.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @note Writes into #AKGL_REGISTRY_PROPERTIES, so akgl_registry_init_properties
* has to have run -- otherwise every entry is silently dropped and this
* still returns success.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname);
/**
* @brief Create the actor-state-name registry and fill it from the name table.
*
* Walks `AKGL_ACTOR_STATE_STRING_NAMES` and maps entry `i` to the value `1 << i`,
* which is what lets a character JSON write `"AKGL_ACTOR_STATE_FACE_LEFT"` and
* have akgl_character_load_json turn it into a bit.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set. The
* individual name registrations are not checked.
*
* @note The name table it reads disagrees with actor.h in two places: bits 11
* and 12 are named `UNDEFINED_11`/`UNDEFINED_12` rather than `MOVING_IN`
* and `MOVING_OUT`, so those two states cannot be named from JSON at all.
* TODO.md items 24-26.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor_state_strings();
/**
* @brief Set one configuration property.
*
* @param name Property key. Required.
* @param value Property value. Required. SDL copies it, so the caller's buffer
* can go away afterwards.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p name or @p value is `NULL`.
*
* @note The write itself is unchecked. If #AKGL_REGISTRY_PROPERTIES is still 0 --
* akgl_registry_init_properties never having run -- this discards the
* value and returns success.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *value);
/**
* @brief Read one configuration property into a pooled string, with a fallback.
*
* Absence is not an error: an unset property yields @p def, which is how every
* caller in the library gets a working default without checking first.
*
* @param name Property key. Required.
* @param dest Receives the value. Required. If `*dest` is `NULL` a string is
* claimed from the pool for you, so initialize it to `NULL` on the
* first call; either way the caller releases it with
* akgl_heap_release_string. If `*dest` is non-`NULL` it is written
* in place.
* @param def Value to use when @p name is not set. Effectively required --
* a `NULL` here on an unset property is an AKERR_NULLPOINTER rather
* than an empty result.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p name or @p dest is `NULL`, or if the property
* is unset and @p def is `NULL`.
* @throws AKERR_OUTOFBOUNDS If the stored value is #AKGL_MAX_STRING_LENGTH bytes
* or longer, which will not fit an akgl_String with its terminator.
* @throws AKERR_VALUE If the value and the destination overlap in memory.
* @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
*
* @note Only the value and its terminator are copied. The rest of @p dest keeps
* whatever the previous holder left there, so read the result as a C
* string rather than as #AKGL_MAX_STRING_LENGTH bytes.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def);
#endif //_REGISTRY_H_

View File

@@ -1,26 +1,3 @@
/**
* @file renderer.h
* @brief The pluggable rendering backend: a record of function pointers plus an initializer.
*
* There is one backend shipped, the 2D SDL one, and `akgl_render_init2d` is its
* initializer -- it creates the window and the `SDL_Renderer` and then fills in
* the six `akgl_render_2d_*` entry points. A different renderer is a different
* initializer populating the same struct, not a branch inside these functions.
*
* Those are two separable jobs, and a host that already owns an `SDL_Renderer`
* -- an embedded interpreter, which must not create the window -- wants only the
* second. akgl_render_bind2d() is that half on its own; akgl_render_init2d()
* makes the window and then calls it.
*
* Callers do not normally name the `akgl_render_2d_*` functions directly; they
* go through the pointers on the backend (`renderer->frame_start(renderer)`),
* which is what makes the swap possible. The global `renderer` in game.h is the
* instance the rest of the library draws through.
*
* A frame is `frame_start` (clear), any number of `draw_*` calls, then
* `frame_end` (present).
*/
#ifndef _RENDERER_H_
#define _RENDERER_H_
@@ -30,171 +7,23 @@
#include <akgl/iterator.h>
/** @brief Defines a pluggable renderer backend and drawing callbacks. */
typedef struct akgl_RenderBackend {
SDL_Renderer *sdl_renderer; /**< The SDL renderer, created by the initializer. Owned by SDL, not freed here. */
akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self); /**< Tear the backend down. */
akerr_ErrorContext AKERR_NOIGNORE *(*frame_start)(struct akgl_RenderBackend *self); /**< Begin a frame: clear the target. */
akerr_ErrorContext AKERR_NOIGNORE *(*frame_end)(struct akgl_RenderBackend *self); /**< End a frame: present it. */
akerr_ErrorContext AKERR_NOIGNORE *(*draw_texture)(struct akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip); /**< Blit one texture. */
akerr_ErrorContext AKERR_NOIGNORE *(*draw_mesh)(struct akgl_RenderBackend *self); /**< Reserved for a 3D backend; the 2D one refuses. */
akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags); /**< Draw the whole scene, layer by layer. */
SDL_Renderer *sdl_renderer;
akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self);
akerr_ErrorContext AKERR_NOIGNORE *(*frame_start)(struct akgl_RenderBackend *self);
akerr_ErrorContext AKERR_NOIGNORE *(*frame_end)(struct akgl_RenderBackend *self);
akerr_ErrorContext AKERR_NOIGNORE *(*draw_texture)(struct akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip);
akerr_ErrorContext AKERR_NOIGNORE *(*draw_mesh)(struct akgl_RenderBackend *self);
akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags);
} akgl_RenderBackend;
/**
* @brief Tear the 2D backend down.
*
* A no-op placeholder that always succeeds. The window and `SDL_Renderer` this
* backend created are still owned by SDL and are reclaimed by `SDL_Quit`, so
* there is nothing here to release yet; the hook exists so a backend that *does*
* own resources has somewhere to free them.
*
* @param self The backend to shut down. Ignored -- not even dereferenced, so
* `NULL` is currently harmless.
* @return `NULL` always. It has no failure path today; check it anyway, because
* a backend that acquires anything will grow one.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self);
/**
* @brief Begin a frame: clear the render target to opaque black.
* @param self The backend to draw through. Required, and dereferenced *before*
* it is checked -- a `NULL` @p self is a crash, not an error.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If `self->sdl_renderer` is `NULL`, which means the
* backend was never run through akgl_render_init2d.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self);
/**
* @brief End a frame: present everything drawn since akgl_render_2d_frame_start.
* @param self The backend to draw through. Required, and dereferenced *before*
* it is checked -- a `NULL` @p self is a crash, not an error.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If `self->sdl_renderer` is `NULL`, which means the
* backend was never run through akgl_render_init2d.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self);
/**
* @brief Blit a texture, rotating it only when asked to.
*
* A rotation of exactly 0 takes the plain `SDL_RenderTexture` path, which is why
* @p center and @p flip are only consulted when @p angle is non-zero.
*
* @param self The backend to draw through. Required.
* @param texture The texture to blit. Required.
* @param src The rectangle of @p texture to take, in pixels. Optional --
* `NULL` means the whole texture.
* @param dest Where to put it on the render target, in pixels. Optional --
* `NULL` means stretch to fill the whole target. Not an output
* parameter despite the name.
* @param angle Clockwise rotation in degrees. Exactly 0 skips rotation
* entirely, along with @p center and @p flip.
* @param center Pivot for the rotation, relative to @p dest. Required *when*
* @p angle is non-zero, ignored otherwise. SDL's own "NULL means
* the centre of dest" convention is not available here.
* @param flip Horizontal/vertical mirroring. Applied only on the rotated
* path; ignored when @p angle is 0.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or @p texture is `NULL`; if @p angle is
* non-zero and @p center is `NULL`; or if the SDL draw call itself
* fails, in which case the message carries `SDL_GetError()`. That last
* case is a reused status rather than a pointer problem.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_texture(akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip);
/**
* @brief Draw geometry. Not implemented by the 2D backend.
*
* The hook exists so a 3D backend has a slot to fill; this one refuses every
* call. It is not a stub that quietly does nothing -- it fails loudly, so a
* caller that reaches it finds out at once.
*
* @param self The backend. Ignored.
* @return Never `NULL`.
* @throws AKERR_API Always, with the message "Not implemented".
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self);
/**
* @brief Draw the whole scene: every tilemap layer, and the actors standing on it.
*
* Walks layers from 0 to #AKGL_TILEMAP_MAX_LAYERS. For each, it draws that layer
* of the global `gamemap` through `camera` (if the map has that many layers),
* then sweeps the actor pool and calls `renderfunc` on every live actor whose
* `layer` matches. Sweeping in layer order is what puts actors in front of the
* scenery they stand on and behind the scenery they walk under.
*
* It reads the globals `gamemap`, `camera`, and `HEAP_ACTOR` directly rather
* than taking them as arguments, so there is exactly one world to draw.
*
* @param self The backend to draw through. Required.
* @param opflags Iterator flags. Optional -- `NULL` substitutes a zeroed set.
* Currently ignored either way: this function sweeps the actor
* pool itself instead of going through
* akgl_registry_iterate_actor, so no `AKGL_ITERATOR_OP_*` bit
* reaches anything. The parameter is here for the backend
* signature and for the layer-mask support that has yet to be
* wired up.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_* Whatever akgl_tilemap_draw or an actor's own `renderfunc`
* raises; the first failure aborts the frame and propagates unchanged.
*
* @warning Neither the global `gamemap` nor a live actor's `renderfunc` is
* checked before use, so drawing before akgl_tilemap_load, or with a
* hand-built actor that was never run through akgl_actor_initialize,
* is a crash rather than an error context.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags);
/**
* @brief Install the 2D backend's methods on a backend the caller owns.
*
* The vtable half of akgl_render_init2d(), with no window, no renderer, and no
* property registry involved: it points @p self's six function pointers at the
* `akgl_render_2d_*` entry points and returns. `sdl_renderer` is not touched,
* so a caller who has already put its own `SDL_Renderer` there keeps it, and a
* caller who has not gets a backend whose entry points all report
* `AKERR_NULLPOINTER` rather than crash.
*
* This is the entry point for a host that owns its own window -- it can drive
* akgl_actor_render(), akgl_tilemap_draw() and the rest through a backend
* libakgl never created. akgl_render_init2d() is the same thing with a window
* in front of it.
*
* @param self The backend to bind. Required. Only the method pointers are
* written.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_bind2d(akgl_RenderBackend *self);
/**
* @brief Create the window and SDL renderer, and bind the 2D backend's methods.
*
* Reads `game.screenwidth` and `game.screenheight` from the property registry
* (both defaulting to the string "0", which asks SDL for a zero-sized window),
* creates the window and renderer with `game.uri` as the title, points `camera`
* at the full screen rectangle, and then calls akgl_render_bind2d() to install
* the six `akgl_render_2d_*` function pointers on @p self.
*
* Because the dimensions come from the registry, akgl_registry_init_properties
* and the property writes have to happen first -- see the note on
* akgl_registry_init about the properties registry not being initialized for
* callers that skip akgl_game_init.
*
* @param self The backend to initialize. Required. Its `sdl_renderer` and method
* pointers are overwritten; anything it held before is not released.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_VALUE If `game.screenwidth` or `game.screenheight` is set to
* something that is not a base-10 integer.
* @throws ERANGE If either dimension does not fit in an `int`.
* @throws AKGL_ERR_SDL If the window and renderer cannot be created. The message
* carries `SDL_GetError()`.
* @throws AKGL_ERR_HEAP If the string pool is exhausted while reading the two
* properties.
*
* @note The two pooled strings holding the dimensions are only released on the
* success path, so each failed initialization leaks two string slots.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self);
#endif // _RENDERER_H_

View File

@@ -1,20 +1,3 @@
/**
* @file sprite.h
* @brief Spritesheets (one texture, many frames) and the animations cut out of them.
*
* The split is deliberate: an akgl_SpriteSheet owns the `SDL_Texture` and the
* frame grid, and any number of akgl_Sprite animations point at the same sheet
* and name the frame indices they use. Loading two sprites from the same image
* loads the image once -- akgl_sprite_load_json looks the sheet up in
* #AKGL_REGISTRY_SPRITESHEET by resolved path before creating one.
*
* Both are pool objects (akgl_heap_next_sprite, akgl_heap_next_spritesheet) and
* both publish themselves in a registry under their name, which is how
* akgl_character_sprite_add and the tilemap loader find them. Because loading a
* sheet uploads a texture, the renderer has to exist first -- so
* akgl_render_init2d, or akgl_game_init, before any of this.
*/
#ifndef _AKGL_SPRITE_H_
#define _AKGL_SPRITE_H_
@@ -31,141 +14,33 @@
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
/** @brief Stores a loaded spritesheet texture and frame geometry. */
typedef struct {
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. Destroys the texture when it reaches 0. */
SDL_Texture *texture; /**< The whole sheet as one GPU texture. Owned by this struct. */
char name[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH]; /**< Registry key: the resolved path the image was loaded from. */
uint16_t sprite_w; /**< Frame width. Vestigial: nothing in the library writes or reads it; the grid comes from akgl_Sprite::width. */
uint16_t sprite_h; /**< Frame height. Vestigial, same as sprite_w. */
uint8_t refcount;
SDL_Texture *texture;
char name[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
uint16_t sprite_w;
uint16_t sprite_h;
} akgl_SpriteSheet;
/** @brief Describes an animated sprite and its spritesheet reference. */
typedef struct {
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. One reference per character that maps it. */
uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; /**< Frame numbers on the sheet, in playback order. Counted left to right, then wrapping to the next row. */
uint32_t frames; /**< How many entries of frameids are in use. */
uint32_t width; /**< Frame width in pixels; also the horizontal stride used to find a frame on the sheet. */
uint32_t height; /**< Frame height in pixels; the vertical stride once frames wrap to the next row. */
uint32_t speed; /**< Nanoseconds one frame is held. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONESEC_MS, which despite its name is nanoseconds-per-millisecond. TODO.md item 6. */
bool loop; /**< Restart from frame 0 when the last frame is reached, instead of holding it. */
bool loopReverse; /**< Play back down to frame 0 instead of jumping to it -- a ping-pong loop. */
char name[AKGL_SPRITE_MAX_NAME_LENGTH]; /**< Registry key, from the JSON `name` field. */
akgl_SpriteSheet *sheet; /**< The sheet these frames are cut from. Borrowed, not owned. */
uint8_t refcount;
uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; // which IDs on the spritesheet belong to our frames
uint32_t frames; // how many frames are in this animation
uint32_t width;
uint32_t height;
uint32_t speed; // how many milliseconds a given sprite frame should be visible before cycling
bool loop; // when this sprite is done playing, it should immediately start again
bool loopReverse; // when this sprite is done playing, it should go in reverse order through its frames
char name[AKGL_SPRITE_MAX_NAME_LENGTH];
akgl_SpriteSheet *sheet;
} akgl_Sprite;
// initializes a new sprite to use the given sheet and otherwise sets to zero
/**
* @brief Zero a pooled sprite, bind it to a sheet, and publish it in the sprite registry.
*
* Sets the name and the sheet and takes the first reference; everything else --
* frame list, geometry, speed, loop flags -- is left at zero for the caller, or
* akgl_sprite_load_json, to fill in. The sheet is borrowed, so this does *not*
* take a reference on it: releasing the sheet out from under a live sprite
* leaves a dangling pointer.
*
* @param spr Pooled sprite to initialize, normally from akgl_heap_next_sprite.
* Required. Any previous contents are discarded.
* @param name Registry key, NUL-terminated. Required. Copied at a fixed
* #AKGL_SPRITE_MAX_NAME_LENGTH bytes, so a shorter string reads
* past its end -- pass a name from an akgl_String or another
* buffer of at least that size. An existing entry with the same
* name is silently replaced.
* @param sheet The spritesheet this sprite's frames come from. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p spr, @p name, or @p sheet is `NULL`.
* @throws AKERR_KEY If the sprite cannot be written into #AKGL_REGISTRY_SPRITE
* -- in practice, because akgl_registry_init has not run.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet);
// loads a given image file into a new spritesheet
/**
* @brief Load an image file as a texture and publish it as a spritesheet.
*
* Uploads @p filename through SDL_image using the global `renderer`, so the
* renderer must already be initialized. The sheet is registered in
* #AKGL_REGISTRY_SPRITESHEET under @p filename, which is the key
* akgl_sprite_load_json looks it up by to avoid loading the same image twice.
*
* @param sheet Pooled spritesheet to initialize, normally from
* akgl_heap_next_spritesheet. Required. Zeroed first, so a
* texture it already held is leaked rather than destroyed.
* @param sprite_w Frame width in pixels. Accepted and currently discarded --
* the frame grid actually used at draw time comes from the
* akgl_Sprite's `width`/`height`, not from here.
* @param sprite_h Frame height in pixels. Same caveat as @p sprite_w.
* @param filename Path to the image, in any format SDL_image can decode.
* Required. Doubles as the registry key, so callers should pass
* an already-resolved path; two spellings of the same file are
* two sheets. Truncated at
* #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p sheet or @p filename is `NULL`.
* @throws AKGL_ERR_SDL If the image cannot be loaded -- missing, unreadable, or
* a format SDL_image was not built with. The message carries
* `SDL_GetError()`.
* @throws AKERR_KEY If the sheet cannot be written into
* #AKGL_REGISTRY_SPRITESHEET.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename);
/**
* @brief Build a sprite -- and, if needed, its spritesheet -- from a JSON definition file.
*
* Reads `name`, `width`, `height`, `speed` (seconds, scaled to milliseconds),
* `loop`, `loopReverse`, a `frames` array of frame indices, and a `spritesheet`
* object holding `filename`, `frame_width`, and `frame_height`. The sheet's
* `filename` is resolved *relative to the directory of @p filename*, so a sprite
* definition can sit next to its image and move with it.
*
* If a spritesheet with that resolved path is already registered it is reused;
* otherwise one is claimed from the pool and loaded. On failure both the sprite
* and any sheet loaded for it are released again.
*
* @param filename Path to the JSON document. Required. Must be shorter than
* #AKGL_MAX_STRING_LENGTH -- it is copied into a pooled string
* so `dirname` can be taken without modifying the caller's
* buffer.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p filename is `NULL`, or if the file cannot be
* opened or does not parse. The message carries jansson's line number
* and text.
* @throws AKERR_OUTOFBOUNDS If @p filename is at least #AKGL_MAX_STRING_LENGTH
* bytes long, or if the `frames` array is indexed past its end.
* @throws AKERR_KEY If a required key is absent, or if the sprite or sheet
* cannot be added to its registry. The message names the key.
* @throws AKERR_TYPE If a key is present with the wrong JSON type -- `speed` as
* a string, `frames` as an object.
* @throws AKGL_ERR_SDL If the spritesheet image fails to load.
* @throws AKGL_ERR_HEAP If the sprite, spritesheet, or string pool is exhausted.
*
* @note The `frames` array is not bounded against #AKGL_SPRITE_MAX_FRAMES. A
* definition with more than 16 frames writes past `frameids` into the
* rest of the struct.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_load_json(char *filename);
/**
* @brief Work out where one animation frame sits on its spritesheet.
*
* Frames are numbered in reading order across the sheet: multiply the frame
* number by the sprite width, then wrap whole rows off the right-hand edge of
* the texture and step down by the sprite height for each. Callers use the
* result as the source rectangle for `SDL_RenderTexture`.
*
* @param self The sprite whose sheet and frame geometry to use. Required,
* along with its `sheet`.
* @param srccoords Receives the frame's rectangle on the sheet, in pixels.
* Required -- the return value is the error context.
* @param frameid Index into the sprite's `frameids` array, not a frame number
* on the sheet. Not bounds-checked against `frames` or
* #AKGL_SPRITE_MAX_FRAMES; an out-of-range index reads a
* neighbouring struct member and yields a nonsense rectangle
* rather than an error.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self, @p srccoords, or `self->sheet` is
* `NULL`. Note that `sheet->texture` is dereferenced without a check,
* so a registered-but-unloaded sheet is a crash rather than an error.
*/
akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid);
#endif //_AKGL_SPRITE_H_

View File

@@ -1,13 +1,3 @@
/**
* @file staticstring.h
* @brief A fixed-capacity string object handed out by the akgl string heap layer.
*
* The library allocates nothing at runtime, so a "string" here is a
* PATH_MAX-sized buffer claimed from the pool with akgl_heap_next_string and
* given back with akgl_heap_release_string. Capacity is fixed at compile time:
* these functions truncate rather than grow, and truncation is silent.
*/
#ifndef _STRING_H_
#define _STRING_H_
@@ -17,55 +7,12 @@
#define AKGL_MAX_STRING_LENGTH PATH_MAX
/** @brief Provides a fixed-capacity, heap-managed string buffer. */
typedef struct
{
int refcount; /**< Pool bookkeeping; 0 means the slot is free. Owned by the heap layer. */
char data[AKGL_MAX_STRING_LENGTH]; /**< The characters. Not guaranteed NUL-terminated when filled to capacity. */
int refcount;
char data[AKGL_MAX_STRING_LENGTH];
} akgl_String;
/**
* @brief Set a pooled string's contents and mark the slot in use.
*
* Copies at most #AKGL_MAX_STRING_LENGTH bytes out of @p init, or zeroes the
* buffer when @p init is `NULL`, then sets `refcount` to 1. Callers normally
* reach this through akgl_heap_next_string rather than calling it directly.
*
* @param obj The pooled string to (re)initialize. Required. Its previous
* contents are discarded without inspection.
* @param init Initial contents, NUL-terminated. Optional -- `NULL` zero-fills
* the buffer instead. An @p init longer than
* #AKGL_MAX_STRING_LENGTH is truncated *and left unterminated*,
* because this is `strncpy` semantics, not `strlcpy`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
*
* @note Known defect: the `NULL` @p init path zeroes `sizeof(akgl_String)`
* bytes starting at `data`, which is four bytes past the end of the
* buffer -- `refcount` sits in front of it. TODO.md, "Known and still
* open" item 6.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_initialize(akgl_String *obj, char *init);
/**
* @brief Copy the contents of one pooled string into another.
*
* A bounded `strncpy` between two already-claimed pool slots. It copies bytes
* only: `refcount` is left alone, so @p dst keeps whatever pool state it had.
*
* @param src Source string. Required. Read up to @p count bytes.
* @param dst Destination string. Required. Overwritten in place; the pool
* slot must already have been claimed.
* @param count Maximum bytes to copy. 0 selects #AKGL_MAX_STRING_LENGTH, the
* whole buffer. A @p count shorter than the source truncates
* without writing a terminator; a @p count longer than the source
* zero-pads the remainder, per `strncpy`. Values above
* #AKGL_MAX_STRING_LENGTH overrun both buffers and are not
* rejected.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p src or @p dst is `NULL`.
* @throws errno Whatever `errno` holds if `strncpy` returns something other
* than @p dst. In practice `strncpy` always returns its destination, so
* this path is unreachable rather than merely rare.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_copy(akgl_String *src, akgl_String *dst, int count);
#endif //_STRING_H_

View File

@@ -1,158 +1,9 @@
/**
* @file text.h
* @brief Loading fonts and drawing or measuring strings with them.
*
* Fonts are `TTF_Font *` handles kept in the #AKGL_REGISTRY_FONT property
* registry under a caller-chosen name; there is no akgl font type wrapping them.
* SDL_ttf must be initialized (akgl_game_init does it) before any of this.
*
* The two measure functions do not touch the renderer, so they are usable
* before -- or entirely without -- a window. Drawing is immediate mode: each
* akgl_text_rendertextat() call rasterizes, uploads, blits, and throws the
* texture away, which is fine for a HUD line and wrong for a large body of
* static text redrawn every frame.
*/
#ifndef _TEXT_H_
#define _TEXT_H_
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <akerror.h>
/**
* @brief Open a TrueType font at one size and publish it in the font registry.
*
* A size is baked into the handle, so the same file at two sizes is two calls
* under two names. Nothing releases these: the handles live until the process
* ends.
*
* @param name Registry key to publish the font under. Required. An existing
* entry with the same name is replaced, and the font it
* displaced is closed through akgl_text_unloadfont -- but only
* after the new one has opened, so a failed load leaves the
* caller with the font they already had.
* @param filepath Path to a `.ttf`/`.otf` file. Required. Used verbatim -- not
* resolved against `SDL_GetBasePath()`.
* @param size Point size to rasterize at. Passed straight to SDL_ttf, which
* rejects anything that is not positive.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p name or @p filepath is `NULL`.
* @throws AKGL_ERR_SDL If the font cannot be opened -- missing, unreadable, not
* a font, or a @p size SDL_ttf refuses. The message carries
* `SDL_GetError()`.
* @throws AKERR_KEY If the font cannot be written into #AKGL_REGISTRY_FONT --
* in practice, because akgl_registry_init has not run.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size);
/**
* @brief Close a loaded font and take it out of the font registry.
*
* The other half of akgl_text_loadfont, and for a while the half that did not
* exist: a font could be opened and published but never handed back, so a game
* that changed fonts between scenes had no way to reclaim the one it had
* finished with. A `TTF_Font` is about ten kilobytes once FreeType's own
* structures are counted.
*
* The registry entry is cleared before the font is closed, so a font is never
* reachable through #AKGL_REGISTRY_FONT after it has gone back to SDL_ttf.
*
* @param name Registry key the font was published under. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p name is `NULL`.
* @throws AKERR_KEY If no font is registered under @p name -- including the
* case where it was already unloaded, which makes a double unload an
* error rather than a double close.
*
* @warning Anything still holding the `TTF_Font *` -- a caller that fetched it
* from the registry earlier, a pending akgl_text_rendertextat -- is
* left with a dangling pointer. Fonts are not reference counted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name);
/**
* @brief Rasterize a string and blit it at a screen position, in one call.
*
* Renders blended (anti-aliased, alpha-blended) through SDL_ttf, uploads the
* result to a texture, draws it through the global `renderer`, and destroys both
* the texture and the surface before returning. The text is drawn at its natural
* size -- @p x and @p y are the top-left corner, not a centre.
*
* Coordinates are screen coordinates, not world ones: this does not go through
* the camera, so a HUD stays put while the world scrolls under it.
*
* @param font Font to render with, from akgl_text_loadfont. Required.
* @param text UTF-8 text. Required. May contain newlines, which break
* lines on either path.
* @param color Text colour, including alpha.
* @param wraplength Wrap width in pixels. Greater than 0 wraps on word
* boundaries at that width; 0 or less draws a single line and
* breaks only on newlines in @p text.
* @param x Left edge of the text, in screen pixels.
* @param y Top edge of the text, in screen pixels.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p font or @p text is `NULL`; if the global
* `renderer`, its `sdl_renderer`, or its `draw_texture` is `NULL` --
* that last one is the state a backend is in between being allocated
* and being run through akgl_render_bind2d(); if SDL_ttf cannot
* rasterize the string; or if the surface cannot be uploaded as a
* texture. The last two carry `SDL_GetError()` and are a reused status
* rather than a pointer problem.
* @throws AKERR_* Whatever the backend's `draw_texture` raises.
*
* @note On a failure after rasterizing -- the texture upload, or the draw -- the
* surface and texture are not destroyed, because the error returns before
* the cleanup. Repeated failures leak.
* @note The empty string is **refused**, not drawn as nothing: SDL_ttf reports
* "Text has zero width" and this passes that on as `AKERR_NULLPOINTER`.
* akgl_text_measure() accepts it, so the two disagree. A caller drawing a
* line of text that may be empty has to check for it. TODO.md, "Known and
* still open".
*/
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 to measure with, from akgl_text_loadfont. Required.
* @param text UTF-8 text to measure. Required. The empty string is legal and
* measures 0 wide by one line high.
* @param w Receives the width in pixels. Required.
* @param h Receives the height in pixels -- one line, whatever @p text
* contains, since this form does not wrap. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p font, @p text, @p w, or @p h is `NULL`.
* @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font,
* or text that is not valid UTF-8. The message carries `SDL_GetError()`.
*/
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 to measure with, from akgl_text_loadfont. Required.
* @param text UTF-8 text to measure. Required.
* @param wraplength Wrap width in pixels. 0 wraps on newlines only. Negative is
* refused rather than passed through: SDL_ttf reads a negative
* width as a very large unsigned one and silently stops
* wrapping, which would return a measurement that is wrong
* rather than an error.
* @param w Receives the width in pixels: the longest line, not
* @p wraplength. Required.
* @param h Receives the height in pixels, covering every line the text
* wraps onto. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p font, @p text, @p w, or @p h is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p wraplength is negative.
* @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font,
* or text that is not valid UTF-8. The message carries `SDL_GetError()`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h);
#endif // _TEXT_H_

View File

@@ -1,113 +1,63 @@
/**
* @file tilemap.h
* @brief Loading and drawing Tiled maps: tilesets, layers, embedded actors, per-map physics.
*
* The map format is the JSON export from the Tiled editor, so the level design
* tool is somebody else's and the loader is the only thing that has to know
* about it. A map is a stack of layers -- tile grids, images, and object groups
* -- drawn over a set of tilesets, each tileset being one image cut into a grid.
*
* Three things beyond ordinary Tiled semantics are worth knowing:
*
* - **Object layers can spawn actors.** An object of type `actor` names a
* character in its custom properties, and loading the map creates and
* registers the actor rather than just recording a rectangle.
* - **A map can carry its own physics.** A `physics.model` custom property on
* the map selects a backend, and `physics.gravity.*` / `physics.drag.*` set
* its constants, so a swimming level and a walking level differ by data
* rather than by code.
* - **A map can carry a pseudo-3D perspective.** Two objects named
* `p_foreground` and `p_vanishing` in an object layer, each with a `scale`
* property, define a band down the screen over which actors are scaled --
* which is what makes a character look further away as they walk up the
* screen. akgl_tilemap_scale_actor applies it.
*
* All paths inside a map -- tileset images, layer images -- are resolved
* relative to the map file, so a map and its art move together.
*
* Note the size of akgl_Tilemap: a layer's tile grid alone is 512x512 ints, and
* a tileset's offset table is 65536 pairs. It is a megabytes-large object and
* belongs in static storage, which is where `_akgl_gamemap` puts it. Do not put
* one on the stack.
*/
#ifndef _TILEMAP_H_
#define _TILEMAP_H_
#include <limits.h>
#include <akgl/actor.h>
#include <akgl/staticstring.h>
#include <akgl/physics.h>
#include "actor.h"
#include "staticstring.h"
#include <jansson.h>
/** @brief Widest map, in tiles. Width times height is what is actually bounded. */
#define AKGL_TILEMAP_MAX_WIDTH 512
/** @brief Tallest map, in tiles. */
#define AKGL_TILEMAP_MAX_HEIGHT 512
/** @brief Layers per map. Also the number of draw passes akgl_render_2d_draw_world makes. */
#define AKGL_TILEMAP_MAX_LAYERS 16
/** @brief Tilesets per map. */
#define AKGL_TILEMAP_MAX_TILESETS 16
/** @brief Entries in a tileset's offset table. Indexed by *local* tile id, so a tileset with a high `firstgid` still starts at 0. */
#define AKGL_TILEMAP_MAX_TILES_PER_IMAGE 65536
/** @brief Longest tileset name. */
#define AKGL_TILEMAP_MAX_TILESET_NAME_SIZE 512
/** @brief Longest resolved tileset image path. */
#define AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE PATH_MAX
/** @brief Longest object name. Note that an object naming an actor is truncated at #AKGL_ACTOR_MAX_NAME_LENGTH (128) instead. */
#define AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE 512
/** @brief Objects in one object layer. Not enforced by the loader -- a longer group writes past the array. */
#define AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER 128
/** @brief Object `type` for a Tiled object whose `type` field is the string "actor". */
#define AKGL_TILEMAP_OBJECT_TYPE_ACTOR 1
/** @brief Layer `type` for a Tiled `tilelayer`: a grid of tile ids. */
#define AKGL_TILEMAP_LAYER_TYPE_TILES 1
/** @brief Layer `type` for a Tiled `objectgroup`: actors and perspective markers. */
#define AKGL_TILEMAP_LAYER_TYPE_OBJECTS 2
/** @brief Layer `type` for a Tiled `imagelayer`: one image drawn at the origin. */
#define AKGL_TILEMAP_LAYER_TYPE_IMAGE 3
/** @brief One object placed in a Tiled object layer. */
typedef struct {
float x; /**< Position in map pixels, from the Tiled object. Copied onto the actor it spawns. */
float y; /**< Position in map pixels. */
int gid; /**< Global tile id, for a tile object. Not read by the loader. */
int id; /**< Tiled's object id. Not read by the loader. */
int height; /**< Height in map pixels. Read into the map's perspective bands, not into this field. */
int width; /**< Width in map pixels. Not read by the loader. */
int rotation; /**< Rotation in degrees. Not read by the loader. */
int type; /**< #AKGL_TILEMAP_OBJECT_TYPE_ACTOR, or 0 for anything else. */
bool visible; /**< Copied onto the actor. Forced to `false` for perspective markers, which are geometry rather than scenery. */
akgl_Actor *actorptr; /**< The actor this object spawned or attached to, for an actor object. `NULL` otherwise. Borrowed. */
char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE]; /**< Object name. For an actor object this is the registry key; for a perspective marker it is `p_foreground` or `p_vanishing`. */
float x;
float y;
int gid;
int id;
int height;
int width;
int rotation;
int type;
bool visible;
akgl_Actor *actorptr;
char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE];
} akgl_TilemapObject;
/** @brief One layer of a tilemap: a tile grid, an image, or a group of objects. */
typedef struct {
short type; /**< Which of the `AKGL_TILEMAP_LAYER_TYPE_*` kinds this is; decides which of the members below mean anything. */
float opacity; /**< 0.0 to 1.0, from Tiled. Recorded but not yet applied at draw time. */
bool visible; /**< From Tiled. Recorded but not yet consulted at draw time. */
int height; /**< Tile layer: height in tiles. Image layer: the texture's height in pixels. */
int width; /**< Tile layer: width in tiles. Image layer: the texture's width in pixels. */
int x; /**< Layer offset from Tiled. Recorded but not applied at draw time. */
int y; /**< Layer offset from Tiled. */
int id; /**< Tiled's layer id. Not the same as the index into akgl_Tilemap::layers. */
SDL_Texture *texture; /**< Image layers only: the whole layer as one texture. `NULL` otherwise. Owned by this struct. */
int data[AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT]; /**< Tile layers only: global tile ids in row-major order. 0 means an empty cell. */
akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER]; /**< Object layers only. */
short type;
float opacity;
bool visible;
int height;
int width;
int x;
int y;
int id;
SDL_Texture *texture;
int data[AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT];
akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER];
} akgl_TilemapLayer;
/** @brief One tileset: an image cut into a grid, plus the lookup table that finds a tile in it. */
typedef struct {
int columns; /**< Tiles per row in the image. What turns a linear tile id into a row and column. */
int firstgid; /**< Global id of this tileset's first tile. Subtracting it from a map cell gives the local tile id. */
char imagefilename[AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE]; /**< Resolved absolute path to the image, from the map file's directory. */
int imageheight; /**< Image height in pixels, as declared by Tiled. */
int imagewidth; /**< Image width in pixels, as declared. */
char name[AKGL_TILEMAP_MAX_TILESET_NAME_SIZE]; /**< Tileset name from Tiled. Diagnostic only; lookups go by `firstgid`. */
SDL_Texture *texture; /**< The image as one texture. Owned by this struct. */
int columns;
int firstgid;
char imagefilename[AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE];
int imageheight;
int imagewidth;
char name[AKGL_TILEMAP_MAX_TILESET_NAME_SIZE];
SDL_Texture *texture;
// Use this as a lookup table instead of storing tiles
// in individual textures to blit them from a single
// texture at runtime
@@ -122,124 +72,36 @@ typedef struct {
// lead to premature exhaustion of AKGL_TILEMAP_MAX_TILES_PER_IMAGE
// because set D or E may only have 64 tiles but they may be
// at the upper end of the array bound already because of this.
int tile_offsets[AKGL_TILEMAP_MAX_TILES_PER_IMAGE][2]; /**< Local tile id -> {x, y} pixel offset into the image. Computed once at load by akgl_tilemap_compute_tileset_offsets. */
int tilecount; /**< How many tiles the image holds. */
int tileheight; /**< Height of one tile in pixels. */
int tilewidth; /**< Width of one tile in pixels. */
int spacing; /**< Pixels between adjacent tiles in the image. */
int margin; /**< Pixels of border around the whole grid. Recorded but **not** accounted for by the offset computation. */
int tile_offsets[AKGL_TILEMAP_MAX_TILES_PER_IMAGE][2];
int tilecount;
int tileheight;
int tilewidth;
int spacing;
int margin;
} akgl_Tileset;
/** @brief Represents a complete tilemap and its optional physics backend. */
typedef struct {
int tilewidth; /**< Width of one map cell in pixels. Tiles from a tileset with a different tile size are not rescaled. */
int tileheight; /**< Height of one map cell in pixels. */
int width; /**< Map width in tiles. */
int height; /**< Map height in tiles. */
int numlayers; /**< Layers actually loaded, at most #AKGL_TILEMAP_MAX_LAYERS. */
int orientation; /**< 0 = orthogonal, 1 = isometric. Always set to 0 by the loader; isometric is not implemented. */
int numtilesets; /**< Tilesets actually loaded. */
int p_foreground_y; /**< Y of the `p_foreground` marker: the row at which actors are at full size. 0 disables perspective. */
int p_vanishing_y; /**< Y of the `p_vanishing` marker: the row at which actors are smallest. 0 disables perspective. */
int p_foreground_h; /**< Height of the `p_foreground` marker object. Recorded; not used in the current rate calculation. */
int p_vanishing_h; /**< Height of the `p_vanishing` marker object. Recorded, likewise. */
float p_foreground_scale; /**< Actor scale at `p_foreground_y`. Defaults to 1.0. */
float p_vanishing_scale; /**< Actor scale at `p_vanishing_y`. Defaults to 1.0. */
float p_scale; /**< Unused. Left from an earlier formulation of the perspective maths. */
float p_rate; /**< Scale change per pixel of y between the two markers. Derived at load; what akgl_tilemap_scale_actor interpolates with. */
akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS]; /**< The tilesets, in the order Tiled listed them. */
akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS]; /**< The layers, in draw order: index 0 is furthest back. */
// Different levels may have different physics.
bool use_own_physics; /**< Set when the map declared a `physics.model` property. A caller that honours it simulates through `physics` instead of the global backend. */
akgl_PhysicsBackend physics; /**< This map's own backend, valid only when `use_own_physics` is set. */
int tilewidth;
int tileheight;
int width;
int height;
int numlayers;
int orientation; // 0 = orthogonal, 1 = isometric
int numtilesets;
int p_foreground_y;
int p_vanishing_y;
int p_foreground_h;
int p_vanishing_h;
float p_foreground_scale;
float p_vanishing_scale;
float p_scale;
float p_rate;
akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS];
akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS];
} akgl_Tilemap;
/**
* @brief Load a Tiled JSON map: physics, geometry, layers, tilesets, and any actors it spawns.
*
* Zeroes @p dest, then reads the map's own physics properties, its tile
* dimensions, its size in tiles, its layers, and its tilesets -- in that order,
* because layers reference tilesets by global id and objects reference
* characters by name. Finally, if the map carried both perspective markers, it
* works out the per-pixel scaling rate between them.
*
* Loading a map has side effects beyond @p dest: tileset and layer images are
* uploaded as textures, and every `actor` object in an object layer is created
* in the actor pool and published in #AKGL_REGISTRY_ACTOR. So the renderer, the
* pools, the registries, and the characters the map names all have to be in
* place first.
*
* @param fname Path to the map JSON. Required. Its directory becomes the root
* that every tileset and layer image inside is resolved against.
* @param dest The tilemap to fill in. Required. Zeroed first, so a map already
* loaded into it has its textures leaked rather than destroyed --
* call akgl_tilemap_release first if you are reusing one. Do not
* put one of these on the stack; see the note on this file.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fname or @p dest is `NULL`; if the map file
* cannot be opened or does not parse, with jansson's line and text; or
* if a tileset image cannot be loaded.
* @throws AKERR_OUTOFBOUNDS If the map is larger than
* #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT tiles, if it has
* more than #AKGL_TILEMAP_MAX_LAYERS layers, or if a layer's own
* declared size exceeds the same bound.
* @throws AKERR_KEY If a required key is absent, if an `actor` object has an
* empty name, or if the map names a physics model that does not exist.
* @throws AKERR_TYPE If a key is present with the wrong JSON type.
* @throws ENOENT If @p fname or a path referenced inside it does not exist.
* @throws AKGL_ERR_SDL If a layer image cannot be loaded.
* @throws AKGL_ERR_HEAP If the string or actor pool is exhausted.
*
* @note Nothing is released on a failure part-way through: textures already
* uploaded and actors already created stay. Treat a failed load as
* needing akgl_tilemap_release and a fresh actor registry.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load(char *fname, akgl_Tilemap *dest);
/**
* @brief Draw one layer of a map, clipped to a viewport.
*
* Only the tiles that intersect @p viewport are drawn, and the tiles at its
* edges are drawn partially, so scrolling is smooth rather than snapping to the
* tile grid. An image layer ignores the viewport entirely and is drawn once at
* the origin.
*
* Drawing goes through the global `renderer`, not through a backend passed in.
*
* @param dest The map to draw from. Required. Not an output parameter
* despite the name.
* @param viewport The rectangle of the map, in map pixels, that is on screen.
* Required. Usually the global `camera`.
* @param layeridx Which layer to draw, 0 to `numlayers - 1`. **Not**
* bounds-checked: an index past the end reads a neighbouring
* layer, or past the array.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p viewport is `NULL`.
* @throws AKERR_* Whatever the renderer's `draw_texture` raises. The first
* failure abandons the rest of the layer.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRect *viewport, int layeridx);
/**
* @brief Draw a whole tileset to the screen, tile by tile, from its offset table.
*
* A debugging tool, not a rendering path. It reconstructs the original tileset
* image out of the offsets computed at load time, so if the picture that appears
* matches the source image the offset table is right -- and if tiles are shifted
* or duplicated, it is not. The `charviewer` utility uses it.
*
* @param dest The map holding the tileset. Required. Not an output
* parameter despite the name.
* @param tilesetidx Which tileset to draw, 0 to `numtilesets - 1`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p tilesetidx is at or above `numtilesets`. A
* negative index is not rejected.
* @throws AKERR_* Whatever the renderer's `draw_texture` raises.
*
* @note Tiles are laid out using the *map's* tile size rather than the
* tileset's, so a tileset whose tiles are a different size from the map's
* is reconstructed at the wrong pitch.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest, int tilesetidx);
/*
@@ -247,247 +109,16 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest,
* They are only exposed here for unit testing.
*/
/**
* @brief Find one entry in a Tiled `properties` array by name, checking its declared type.
*
* Tiled does not store custom properties as JSON object members. It stores them
* as an array of `{"name": ..., "type": ..., "value": ...}` objects, so getting
* one means a linear scan and a type-string comparison rather than a lookup.
* That is what this does; the `akgl_get_json_properties_*` wrappers put a typed
* face on it.
*
* @param obj The Tiled object -- map, layer, or object -- whose `properties`
* array to search. Required.
* @param key The property name to find. Required.
* @param type The type string Tiled wrote, e.g. `"string"`, `"int"`, `"float"`.
* A property found under the right name but the wrong type is an
* error, not a miss. Not `NULL`-checked.
* @param dest Receives a borrowed pointer to the whole property object -- the
* one with `name`, `type`, and `value` -- not to its value. Not
* `NULL`-checked.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If @p obj has no `properties` array, or if no entry in it
* has that name.
* @throws AKERR_TYPE If `properties` is not an array, an entry is malformed, or
* the named property's declared type is not @p type. That last message
* reports both the expected and the actual type.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest);
/**
* @brief Read a Tiled custom property declared as `string`.
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value in a *newly claimed* pool string. Required. Note
* that this always claims -- unlike akgl_get_json_string_value it
* does not reuse a string already in `*dest`, so passing one leaks
* it. The caller releases the result with akgl_heap_release_string.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `string`.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest);
/**
* @brief Read a Tiled custom property declared as `int`.
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `int`, or its `value` is
* not an integer.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest);
/**
* @brief Build a tileset's local-id-to-pixel-offset table.
*
* Run once per tileset at load time so drawing a tile is an array lookup rather
* than a division. Walks the grid left to right and top to bottom, stepping by
* tile size plus `spacing`.
*
* @param dest The map holding the tileset. Required in practice, though
* not checked -- it is dereferenced immediately.
* @param tilesetidx Which tileset, 0 to `numtilesets - 1`. Not bounds-checked.
* @return `NULL`. There is no failure path: it is arithmetic into an already
* allocated table.
*
* @note Two known limits. `margin` is not accounted for, so a tileset image with
* a border produces offsets shifted by it. And the table is indexed by
* local id from 0, while akgl_tilemap_draw indexes it by
* `tilenum - firstgid`, so the entries a second tileset needs sit at the
* front of its own table -- see the FIXME on akgl_Tileset::tile_offsets
* for why that wastes space.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx);
/**
* @brief Load one object layer: spawn its actors and record its perspective markers.
*
* Two kinds of object are understood. `actor` objects name a registered actor,
* or create one -- claiming it from the pool, initializing it, and binding the
* character named in its `character` property -- and place it at the object's
* coordinates on this layer. `perspective` objects named `p_foreground` or
* `p_vanishing` set the map's scaling band and are themselves invisible.
* Anything else is recorded and otherwise ignored.
*
* An actor named by two objects is not duplicated: the second takes another
* reference on the first.
*
* @param dest The map to load into. Required.
* @param root The layer's JSON object -- not the map root. Required.
* @param layerid Index of the layer being loaded, which becomes each spawned
* actor's `layer`. Not bounds-checked.
* @param dirname Directory to resolve paths against. Accepted for signature
* consistency with the other layer loaders; nothing here uses it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p root is `NULL`.
* @throws AKERR_KEY If the layer has no `objects` array, if a required key is
* absent, or if an `actor` object has an empty name.
* @throws AKERR_TYPE If a key is present with the wrong JSON type.
* @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
* @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
* @throws AKERR_* Whatever akgl_actor_initialize or akgl_actor_set_character
* raises -- notably AKERR_KEY if the named character is not registered.
*
* @warning The object count is not checked against
* #AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER, so a layer with more than 128
* objects writes past the array.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
/**
* @brief Load one tile layer's grid of global tile ids.
*
* @param dest The map to load into. Required.
* @param root The layer's JSON object -- not the map root. Required.
* @param layerid Index of the layer being loaded. Not bounds-checked.
* @param dirname Directory to resolve paths against. Required, though a tile
* layer references no files and nothing here uses it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
* @throws AKERR_OUTOFBOUNDS If the layer's declared `width` times `height`
* reaches #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT, or if the
* `data` array is shorter than that product.
* @throws AKERR_KEY If `height`, `width`, or `data` is absent.
* @throws AKERR_TYPE If one of them has the wrong JSON type, or a cell is not an
* integer.
*
* @note The declared `width` and `height` are trusted over the actual length of
* `data`: a layer declaring more cells than it lists fails on the read
* past the end rather than on a length check.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
/**
* @brief Load every layer of a map, dispatching on each layer's declared type.
*
* Reads the common fields -- id, opacity, visibility, offset -- then hands the
* layer to the loader for its kind: `objectgroup`, `tilelayer`, or `imagelayer`.
* A layer of any other type keeps its common fields and is otherwise skipped,
* with `type` left at 0.
*
* @param dest The map to load into. Required. `numlayers` is set from the
* array's length before any bound is checked, so it may briefly
* exceed #AKGL_TILEMAP_MAX_LAYERS on the failure path.
* @param root The map's root JSON object. Required.
* @param dirname Directory to resolve layer image paths against. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
* @throws AKERR_OUTOFBOUNDS If the map has more than #AKGL_TILEMAP_MAX_LAYERS
* layers.
* @throws AKERR_KEY If the map has no `layers` array, or a layer is missing a
* common field.
* @throws AKERR_TYPE If a field has the wrong JSON type.
* @throws AKERR_* Whatever the per-kind loader raises.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
/**
* @brief Load one tileset's metadata and upload its image.
*
* @param tileset The tileset's JSON object. Required in practice, though not
* checked -- it is passed straight to the accessors, which
* report it.
* @param dest The map to load into. Required, unchecked, dereferenced at once.
* @param tsidx Which slot to fill, 0 to #AKGL_TILEMAP_MAX_TILESETS - 1. Not
* bounds-checked.
* @param dirname Directory to resolve the tileset's `image` path against --
* the map file's own directory. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If a required key is missing its object, or if the
* tileset image fails to load. The image message carries
* `SDL_GetError()`.
* @throws AKERR_KEY If one of `columns`, `firstgid`, `imageheight`,
* `imagewidth`, `margin`, `spacing`, `tilecount`, `tileheight`,
* `tilewidth`, `name`, or `image` is absent.
* @throws AKERR_TYPE If one of them has the wrong JSON type.
* @throws AKERR_OUTOFBOUNDS If the resolved image path is too long for a pooled
* string.
* @throws ENOENT If the image path does not exist.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @note This does not go through the spritesheet registry, so a tileset image
* shared between two maps is loaded twice.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname);
/**
* @brief Load every tileset of a map and compute each one's offset table.
*
* @param dest The map to load into. Required. `numtilesets` is reset to 0 and
* incremented as each tileset succeeds, so it always reflects
* what actually loaded.
* @param root The map's root JSON object. Required.
* @param dirname Directory to resolve tileset image paths against. Required in
* practice; not checked here.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p root is `NULL`.
* @throws AKERR_KEY If the map has no `tilesets` array.
* @throws AKERR_TYPE If `tilesets` is not an array of objects.
* @throws AKERR_* Whatever akgl_tilemap_load_tilesets_each raises.
*
* @warning The tileset count is not checked against
* #AKGL_TILEMAP_MAX_TILESETS, so a map with more than 16 tilesets
* writes past the array.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
/**
* @brief Destroy the textures a map owns.
*
* Call before reusing a tilemap struct for a different map; akgl_tilemap_load
* zeroes rather than releases, so without this the previous map's textures are
* leaked.
*
* @param dest The map to release. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
*
* @warning Known defect: the layer loop destroys `tilesets[i].texture` rather
* than `layers[i].texture`, so tileset textures are destroyed twice and
* image-layer textures never. Nothing is set to `NULL` either, so a
* second call is a use-after-free. It also does not release the actors
* the map's object layers created. TODO.md, "Known and still open"
* item 2.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest);
/**
* @brief Scale an actor for its distance up the screen, using the map's perspective band.
*
* The pseudo-3D trick: an actor at or below `p_foreground_y` is drawn at
* `p_foreground_scale`, one at or above `p_vanishing_y` at `p_vanishing_scale`,
* and one in between is interpolated linearly. A map with no perspective markers
* has both scales at 1.0 and a rate of 0, so this is a no-op rather than a
* special case.
*
* Only the actor's `scale` is written; nothing is drawn. akgl_game_update calls
* it before each actor's update when #AKGL_ITERATOR_OP_TILEMAPSCALE is set.
*
* @param map The map supplying the perspective band. Required.
* @param actor The actor to scale. Required. Its `y` is read and its `scale`
* written.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p map or @p actor is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor);
#endif //_TILEMAP_H_

View File

@@ -1,23 +1,9 @@
/**
* @file types.h
* @brief Width-named floating point aliases, to match the `<stdint.h>` spelling.
*
* C names its integers by width and its floats by rank, so a struct full of
* `uint32_t` and `float` reads inconsistently. These are the float spellings the
* rest of the library uses. They are plain aliases -- no guarantee beyond what
* the platform's `float` and `double` already provide -- and are deliberately
* *not* C23's `_Float32`/`_Float64`, which carry IEEE-754 requirements this
* library does not need.
*/
#ifndef _AKGL_TYPES_H_
#define _AKGL_TYPES_H_
#include <stdint.h>
/** @brief Single-precision float. Positions, velocities, and scale factors. */
typedef float float32_t;
/** @brief Double-precision float. Declared for symmetry; unused so far. */
typedef double float64_t;
#endif // _AKGL_TYPES_H_

View File

@@ -1,191 +1,32 @@
/**
* @file util.h
* @brief Axis-aligned collision tests, path resolution, and two test-only image helpers.
*
* The grab bag. Three unrelated groups live here: rectangle/point overlap for
* the physics backend, path resolution for the asset loaders, and a pair of
* pixel-comparison routines that exist only so tests can assert on what was
* actually drawn.
*
* All the geometry here is axis-aligned and treats edges as touching: a point
* exactly on a boundary is inside. There is no rotation support and no
* separating-axis test.
*/
#ifndef _UTIL_H_
#define _UTIL_H_
#include <akerror.h>
#include <akgl/staticstring.h>
/** @brief An integer point. Carries a `z` the collision routines do not use. */
typedef struct point {
int x; /**< Horizontal position, in whatever space the caller is working in. */
int y; /**< Vertical position. */
int z; /**< Depth. Never written by akgl_rectangle_points and never read by the collision tests. */
int x;
int y;
int z;
} point;
/**
* @brief The four corners of an axis-aligned rectangle, precomputed.
*
* akgl_collide_rectangles works corner by corner rather than by comparing edge
* spans, so it wants the corners as points. akgl_rectangle_points derives one of
* these from an `SDL_FRect`.
*/
typedef struct RectanglePoints {
point topleft; /**< (x, y). */
point topright; /**< (x + w, y). */
point bottomleft; /**< (x, y + h). */
point bottomright; /**< (x + w, y + h). */
point topleft;
point topright;
point bottomleft;
point bottomright;
} RectanglePoints;
/**
* @brief Do not use. Three open parentheses, two closes -- any expansion is a
* syntax error. It duplicates akgl_collide_rectangles(), has no callers,
* and is slated for deletion. TODO.md item 20.
*/
#define AKGL_COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x)
/**
* @brief Expand a rectangle into its four corner points.
*
* Coordinates are truncated from `float` to `int` on the way in, so a rectangle
* at x = 10.9 has its corners at 10. That is deliberate for tile-grid work and
* wrong for sub-pixel work; callers needing the latter should not round-trip
* through this.
*
* @param dest Receives the corners. Required.
* @param rect The rectangle, in any coordinate space. Required. `w` and `h` are
* taken as extents from `x`/`y`, so a negative one produces a
* rectangle whose "bottom right" is above and left of its "top
* left" -- which every test here then reports as empty.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p rect is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect);
/**
* @brief Test whether a point falls inside a rectangle, edges included.
*
* Compares against `topleft` and `bottomright` only, so it assumes @p r is
* well-formed -- the two corners actually being the minimum and maximum. `z` is
* ignored on both sides: this is a 2D test.
*
* @param p The point to test. Required.
* @param r The rectangle, as corners from akgl_rectangle_points. Required.
* @param collide Receives `true` when the point is inside or exactly on an edge,
* `false` otherwise. Required -- the return value is the error
* context. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p p, @p r, or @p collide is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(point *p, RectanglePoints *r, bool *collide);
/**
* @brief Test whether two rectangles overlap, edges included.
*
* Tests all eight corners -- each rectangle's four against the other -- and
* stops at the first hit. Checking both directions is what catches the case
* where one rectangle is entirely inside the other and so has no corner within
* its neighbour.
*
* @param r1 First rectangle. Required.
* @param r2 Second rectangle. Required. Order does not matter.
* @param collide Receives `true` on any overlap or shared edge, `false`
* otherwise. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p r1, @p r2, or @p collide is `NULL`.
*
* @note A corner-containment test misses the one arrangement where two
* rectangles overlap in a cross without either enclosing a corner of the
* other -- a tall thin rectangle crossing a short wide one. Both are
* reported as not colliding.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);
/**
* @brief Resolve an asset path, trying the working directory before the given root.
*
* Asset files name their neighbours relatively -- a sprite definition names its
* spritesheet, a tilemap names its tilesets -- and "relative" has to mean
* relative to the file doing the naming, not to wherever the game was launched
* from. So this tries @p path against the process working directory first, and
* only if that does not exist joins it onto @p root and resolves that. Either
* way the result is absolute, with symlinks and `..` folded out.
*
* @param root Directory to fall back to, normally `dirname` of the file that
* contained @p path. Required, even when unused.
* @param path The path to resolve, relative or absolute. Required.
* @param dst Receives the resolved absolute path. Required, and must already be
* a claimed pool string -- this writes into it, it does not claim
* one for you.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dst is `NULL`.
* @throws AKERR_OUTOFBOUNDS If `root + "/" + path` would not fit in
* #AKGL_MAX_STRING_LENGTH.
* @throws ENOENT If neither spelling names an existing file. Any other `errno`
* `realpath(3)` can raise -- EACCES on an unsearchable directory,
* ELOOP, ENOTDIR -- propagates the same way.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @note The fallback path -- the common one, since most asset references are
* relative to their own file rather than to the working directory --
* returns straight out of the ENOENT handler and so never releases the
* error context it was handling. Each such call consumes one slot of
* libakerror's fixed 128-entry context array for the life of the process.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, akgl_String *dst);
// These are REALLY slow routines that are only useful in testing harnesses
/**
* @brief Assert that two surfaces hold byte-identical pixels.
*
* A `memcmp` over the raw pixel buffer, so it is exact: one differing byte in
* one pixel is a failure. Meant for test harnesses asserting on rendered output,
* not for anything on a frame path.
*
* @param s1 First surface. Required. Its `pitch * h` is what determines how many
* bytes are compared.
* @param s2 Second surface. Required.
* @return `NULL` when the pixels match, otherwise an error context owned by the
* caller. "Not equal" is reported as an error, not as an out-param.
* @throws AKERR_NULLPOINTER If @p s1 or @p s2 is `NULL`.
* @throws AKERR_VALUE If the pixels differ.
*
* @warning The surfaces' dimensions, pitch, and format are not compared, so a
* smaller @p s2 is read past its end rather than reported as a
* mismatch. TODO.md, "Known and still open" item 5.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2);
/**
* @brief Draw two textures in turn, read the framebuffer back after each, and compare.
*
* The test-harness counterpart to akgl_compare_sdl_surfaces: it answers "do
* these two textures *render* the same", which is not the same question as "are
* these two textures identical", because the renderer's scaling and blending sit
* in between. Both are drawn into the same rectangle against a cleared target.
*
* @param t1 First texture. Required.
* @param t2 Second texture. Required.
* @param x Left edge of the region, in pixels. Used for the source
* rectangle, the destination, and the readback alike.
* @param y Top edge of the region.
* @param w Width of the region.
* @param h Height of the region.
* @param writeout Optional filename for a PNG of the *first* render, written
* under `SDL_GetBasePath()`. `NULL` skips it. This is a
* debugging aid -- when an image assertion fails, this is how
* you see what was actually drawn.
* @return `NULL` when the two renders match, otherwise an error context owned by
* the caller.
* @throws AKERR_NULLPOINTER If @p t1 or @p t2 is `NULL`.
* @throws AKGL_ERR_SDL If the framebuffer cannot be read back.
* @throws AKERR_IO If @p writeout is given and the PNG cannot be written.
* @throws AKERR_VALUE If the two renders differ.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @warning Known defect: both passes draw @p t1 -- @p t2 is never rendered -- so
* this currently always reports a match. TODO.md, "Known and still
* open" item 1.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout);
#endif // _UTIL_H_

View File

@@ -1,56 +0,0 @@
/**
* @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_

View File

@@ -1,98 +0,0 @@
#!/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

View File

@@ -1,108 +0,0 @@
#!/bin/bash
#
# Run the existing CTest suites under valgrind and report what leaked.
#
# scripts/memcheck.sh check every suite in ./build
# scripts/memcheck.sh -R tilemap check only the suites matching a regex
# scripts/memcheck.sh -LE perf skip the perf suites
#
# Anything after the build directory is handed straight to ctest, so every
# selection flag ctest understands works here.
#
# Environment:
# AKGL_BUILD_DIR build tree to check (default: build)
# AKGL_MEMCHECK_LOG directory for the per-suite valgrind logs
# (default: $AKGL_BUILD_DIR/Testing/Temporary)
#
# Exit status: 0 when valgrind found nothing, 1 when it found something, 2 on a
# usage or environment error. This is the part `ctest -T memcheck` will not do
# on its own -- it records defects and still exits 0, which is no use as a gate.
set -eu
AKGL_BUILD_DIR="${AKGL_BUILD_DIR:-build}"
function memcheck_die()
{
echo "memcheck: $*" >&2
exit 2
}
# The suite runs headless on purpose. Loading the real GPU stack costs thousands
# of unfixable findings inside the vendor driver, and none of them are libakgl's.
# A suite that sets these hints itself is unaffected; the ones that do not --
# tests/tilemap.c and tests/sprite.c -- pick them up from here.
export SDL_VIDEO_DRIVER="${SDL_VIDEO_DRIVER:-dummy}"
export SDL_RENDER_DRIVER="${SDL_RENDER_DRIVER:-software}"
export SDL_AUDIO_DRIVER="${SDL_AUDIO_DRIVER:-dummy}"
command -v valgrind >/dev/null 2>&1 || memcheck_die "valgrind not found"
command -v ctest >/dev/null 2>&1 || memcheck_die "ctest not found"
[ -f "$AKGL_BUILD_DIR/CMakeCache.txt" ] || memcheck_die "$AKGL_BUILD_DIR is not a configured build tree"
AKGL_MEMCHECK_LOG="${AKGL_MEMCHECK_LOG:-$AKGL_BUILD_DIR/Testing/Temporary}"
# Old logs would be counted as this run's findings. CTest numbers them by test
# id, so a run narrowed with -R leaves the others behind.
rm -f "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log
# The run itself is allowed to fail: a suite that valgrind makes slow enough to
# trip an assertion still produced a log worth reading, and the log is what
# decides the exit status below.
set +e
ctest --test-dir "$AKGL_BUILD_DIR" -T memcheck --output-on-failure "$@"
AKGL_CTEST_STATUS=$?
set -e
# What counts as a finding. "Definitely lost" is a leak nobody can argue with;
# the invalid-access lines are memcheck telling us the library read or wrote
# outside an allocation, which is worse than a leak and easier to fix.
#
# "are definitely lost", not "definitely lost": the LEAK SUMMARY block ends every
# clean log with "definitely lost: 0 bytes in 0 blocks", and matching that
# reported a finding in all twenty-three suites, including the ones that never
# call malloc.
AKGL_MEMCHECK_PATTERN='are definitely lost|Invalid read|Invalid write|Invalid free|Mismatched free|Source and destination overlap|depends on uninitialised|Use of uninitialised'
total=0
echo
echo "== valgrind findings by suite =="
for log in "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log; do
[ -f "$log" ] || continue
# CTest writes the test name into the log's first line as the command it ran.
suite=$(grep -m1 -oE 'akgl_test_[a-z_]+' "$log" || basename "$log")
count=$(grep -cE "$AKGL_MEMCHECK_PATTERN" "$log" || true)
total=$((total + count))
if [ "$count" -gt 0 ]; then
printf '%-24s %3d finding(s) %s\n' "$suite" "$count" "$log"
fi
done
if [ "$total" -eq 0 ]; then
echo "none"
echo
# A clean valgrind run and a failing test are different things: a suite can
# assert its way to a non-zero exit without leaking a byte, and saying
# "clean" and returning 0 for that would hide it.
if [ "$AKGL_CTEST_STATUS" -ne 0 ]; then
echo "memcheck: valgrind found nothing, but ctest exited $AKGL_CTEST_STATUS -- a suite failed on its own terms" >&2
exit 1
fi
echo "memcheck: clean ($AKGL_MEMCHECK_LOG)"
exit 0
fi
echo
echo "== library frames in those stacks, most frequent first =="
# Keyed on the function name rather than the file, because src/physics.c and
# tests/physics.c are both "physics.c" in a valgrind frame and only one of them
# is the library. Anything a caller can reach is an akgl_ symbol by convention,
# which makes the prefix a usable filter; a static helper appears under whatever
# akgl_ function called it, one frame further down.
grep -hoE 'by 0x[0-9A-Fa-f]+: akgl_[A-Za-z0-9_]+ \([a-z_]+\.c:[0-9]+\)' \
"$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log 2>/dev/null |
sed -E 's/^by 0x[0-9A-Fa-f]+: //' | sort | uniq -c | sort -rn | head -20
echo
echo "memcheck: $total finding(s); full stacks in $AKGL_MEMCHECK_LOG" >&2
exit 1

View File

@@ -1,466 +0,0 @@
#!/usr/bin/env python3
"""
Mutation testing harness for libakgl.
Mutation testing measures how good the test suite is at catching bugs. It works
by making many small, deliberate breakages ("mutants") to the library source --
flipping a comparison, deleting a statement, swapping true/false -- and then
running the whole CTest suite against each one. If the tests fail, the mutant is
"killed" (good: the tests noticed the bug). If the tests still pass, the mutant
"survived" (bad: a real bug of that shape would slip through unnoticed).
The mutation score is killed / (killed + survived). Surviving mutants are printed
with file:line and the exact change so they can be turned into new test cases.
This harness has no third-party dependencies (Python stdlib + the project's
normal cmake/ctest toolchain). It never mutates the real working tree: it copies
the repo to a scratch directory and mutates there.
Usage:
scripts/mutation_test.py [options]
--source-root DIR repo root to copy (default: parent of this script's dir)
--target FILE source file to mutate, relative to root; repeatable.
Default: all libakgl-owned C files under src/
--work DIR scratch dir for the mutated copy (default: a temp dir)
--timeout SECONDS per-suite ctest timeout (default: 120)
--exclude-test REGEX CTest regex to exclude (default: ^character$)
--threshold PCT exit non-zero if mutation score < PCT (default: 0 = off)
--list only list the mutants that would be run, then exit
--keep keep the scratch working copy on exit (for debugging)
-j N (reserved) currently runs sequentially
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
# --------------------------------------------------------------------------- #
# Mutation operators
#
# Each operator yields zero or more (start, end, replacement) edits for a single
# line of source. The driver applies exactly one edit per mutant so every mutant
# differs from the original by one localized change.
# --------------------------------------------------------------------------- #
# Relational operator replacement: map each operator to the alternatives that
# meaningfully change behaviour (not merely the strict negation).
_REL = {
"==": ["!="],
"!=": ["=="],
"<=": ["<", "=="],
">=": [">", "=="],
"<": ["<=", ">"],
">": [">=", "<"],
}
# Match a relational operator that is NOT part of ->, <<, >>, =>, <=, >=, ==, !=
# unless we intend it. We tokenize the two-char operators first, then single.
_REL_TWO = re.compile(r"(==|!=|<=|>=)")
_REL_ONE = re.compile(r"(?<![-<>=!+])([<>])(?![=<>])")
_LOGICAL = {"&&": "||", "||": "&&"}
_LOG_RE = re.compile(r"(&&|\|\|)")
_BOOL = {"true": "false", "false": "true"}
_BOOL_RE = re.compile(r"\b(true|false)\b")
# Arithmetic / compound-assignment on whitespace-delimited operands only, to
# avoid touching ++, --, ->, unary signs, or pointer/format punctuation.
_ARITH_RE = re.compile(r"(?<=\s)([+\-])(?=\s)")
_ARITH = {"+": "-", "-": "+"}
_COMPOUND_RE = re.compile(r"(\+=|-=)")
_COMPOUND = {"+=": "-=", "-=": "+="}
# Integer literal replacement: 0 <-> 1 (word-bounded, not inside identifiers or
# larger numbers, not a float).
_INT_RE = re.compile(r"(?<![\w.])([01])(?![\w.])")
_INT = {"0": "1", "1": "0"}
def _op_edits(line):
"""Yield (tag, start, end, replacement) for every candidate mutation."""
# Relational (two-char first so we don't split them with the one-char pass)
for m in _REL_TWO.finditer(line):
for alt in _REL[m.group(1)]:
yield ("ROR", m.start(1), m.end(1), alt)
for m in _REL_ONE.finditer(line):
for alt in _REL[m.group(1)]:
yield ("ROR", m.start(1), m.end(1), alt)
for m in _LOG_RE.finditer(line):
yield ("LCR", m.start(1), m.end(1), _LOGICAL[m.group(1)])
for m in _BOOL_RE.finditer(line):
yield ("BCR", m.start(1), m.end(1), _BOOL[m.group(1)])
for m in _COMPOUND_RE.finditer(line):
yield ("AOR", m.start(1), m.end(1), _COMPOUND[m.group(1)])
for m in _ARITH_RE.finditer(line):
yield ("AOR", m.start(1), m.end(1), _ARITH[m.group(1)])
for m in _INT_RE.finditer(line):
yield ("ICR", m.start(1), m.end(1), _INT[m.group(1)])
# Statement-deletion: neutralize a whole statement. We only delete statements
# that are safe to drop without guaranteeing a compile error, so a surviving
# deletion is a genuine test gap rather than compiler noise.
_STMT_DELETABLE = re.compile(
r"""^\s*(
break |
return\b[^;]* |
[A-Za-z_][-\w>().\[\]* ]*\s*=\s*[^;]* | # assignments
[A-Za-z_][\w]*\s*\([^;]*\) # bare function calls
)\s*;\s*(\\?)\s*$""",
re.VERBOSE,
)
# --------------------------------------------------------------------------- #
# Deciding which lines are eligible to mutate
# --------------------------------------------------------------------------- #
# Skip preprocessor control and the block of constant/error-code #defines in the
# template header: mutating buffer sizes or renumbering error codes produces
# equivalent or uninteresting mutants that swamp the signal.
_SKIP_LINE = re.compile(
r"""^\s*(
\#\s*(include|ifn?def|ifdef|if|elif|else|endif|error|pragma|undef) |
\#\s*define\s+AKERR_(MAX|LAST|NULLPOINTER|OUTOFBOUNDS|API|ATTRIBUTE|
TYPE|KEY|INDEX|FORMAT|IO|VALUE|RELATIONSHIP|EOF|CIRCULAR_REFERENCE|
ITERATOR_BREAK|NOT_IMPLEMENTED|BADEXC|NOIGNORE|USE_STDLIB)\b |
\* | // # comment bodies / line comments
)""",
re.VERBOSE,
)
def _is_comment_or_blank(line):
s = line.strip()
return (not s) or s.startswith("//") or s.startswith("/*") or s.startswith("*")
def eligible(line):
if _is_comment_or_blank(line):
return False
if _SKIP_LINE.match(line):
return False
return True
class Mutant:
__slots__ = ("path", "lineno", "op", "before", "after", "col")
def __init__(self, path, lineno, op, before, after, col):
self.path = path
self.lineno = lineno
self.op = op
self.before = before
self.after = after
self.col = col
def describe(self):
return (f"{self.path}:{self.lineno} [{self.op}] "
f"col{self.col}: {self.before.strip()} -> {self.after.strip()}")
def generate_mutants(root, rel_target):
"""Enumerate all mutants for one target file."""
abspath = os.path.join(root, rel_target)
with open(abspath, "r") as fh:
lines = fh.readlines()
mutants = []
for i, line in enumerate(lines, start=1):
if not eligible(line):
continue
# substitution operators
seen = set()
for tag, s, e, repl in _op_edits(line):
key = (s, e, repl)
if key in seen:
continue
seen.add(key)
mutated = line[:s] + repl + line[e:]
if mutated == line:
continue
mutants.append(Mutant(rel_target, i, tag, line, mutated, s))
# statement deletion
m = _STMT_DELETABLE.match(line)
if m:
indent = line[: len(line) - len(line.lstrip())]
cont = "\\" if line.rstrip().endswith("\\") else ""
deleted = f"{indent}/* mutant: deleted */ {cont}\n" if cont else f"{indent};\n"
mutants.append(Mutant(rel_target, i, "SDL", line, deleted, 0))
return mutants
# --------------------------------------------------------------------------- #
# Build / test orchestration against a scratch copy
# --------------------------------------------------------------------------- #
class Runner:
def __init__(self, work, timeout, exclude_test):
self.work = work
self.build = os.path.join(work, "build")
self.timeout = timeout
self.exclude_test = exclude_test
self.env = os.environ.copy()
library_dirs = [
self.build,
os.path.join(self.build, "deps", "SDL"),
os.path.join(self.build, "deps", "SDL_image"),
os.path.join(self.build, "deps", "SDL_mixer"),
os.path.join(self.build, "deps", "SDL_ttf"),
]
current_library_path = self.env.get("LD_LIBRARY_PATH", "")
if current_library_path:
library_dirs.append(current_library_path)
self.env["LD_LIBRARY_PATH"] = os.pathsep.join(library_dirs)
def _run(self, cmd, timeout=None):
return subprocess.run(
cmd, cwd=self.work, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
timeout=timeout, env=self.env,
)
def configure(self):
r = self._run(["cmake", "-S", ".", "-B", "build"], timeout=self.timeout)
return r.returncode == 0, r.stdout
def ctest_command(self):
command = ["ctest", "--test-dir", "build", "--output-on-failure",
"--stop-on-failure"]
if self.exclude_test:
command.extend(["-E", self.exclude_test])
return command
def build_and_test(self):
"""Return ('killed-compile' | 'killed-test' | 'killed-timeout' | 'survived')."""
try:
b = self._run(["cmake", "--build", "build"], timeout=self.timeout)
except subprocess.TimeoutExpired:
return "killed-timeout"
if b.returncode != 0:
return "killed-compile"
try:
t = subprocess.run(
self.ctest_command(),
env=self.env,
cwd=self.work, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
timeout=self.timeout,
)
except subprocess.TimeoutExpired:
return "killed-timeout"
return "survived" if t.returncode == 0 else "killed-test"
def _xml_escape(s):
return (s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
.replace('"', "&quot;"))
def write_junit(path, records, targets):
"""Write a JUnit XML report. One <testcase> per mutant; a surviving mutant
is a <failure> (test-suite gap), a killed mutant is a passing case."""
by_file = {t: [] for t in targets}
for m, result in records:
by_file.setdefault(m.path, []).append((m, result))
total = len(records)
total_fail = sum(1 for _, r in records if r == "survived")
out = ['<?xml version="1.0" encoding="UTF-8"?>',
f'<testsuites name="mutation" tests="{total}" failures="{total_fail}">']
for f, items in by_file.items():
if not items:
continue
fails = sum(1 for _, r in items if r == "survived")
out.append(f' <testsuite name="mutation:{_xml_escape(f)}" '
f'tests="{len(items)}" failures="{fails}">')
for m, result in items:
name = _xml_escape(m.describe())
cls = "mutation." + _xml_escape(m.path)
if result == "survived":
detail = _xml_escape(f"{m.before.strip()} -> {m.after.strip()}")
out.append(f' <testcase name="{name}" classname="{cls}" time="0">')
out.append(f' <failure message="survived mutant '
f'({_xml_escape(m.op)} at {_xml_escape(m.path)}:'
f'{m.lineno})">{detail}</failure>')
out.append(' </testcase>')
else:
out.append(f' <testcase name="{name}" classname="{cls}" '
f'time="0"><system-out>{_xml_escape(result)}'
'</system-out></testcase>')
out.append(' </testsuite>')
out.append('</testsuites>')
with open(path, "w") as fh:
fh.write("\n".join(out) + "\n")
def copy_tree(src, dst):
ignore = shutil.ignore_patterns("build", ".git", "*.o", "*.so", "*~",
"#*#", "*.iso")
shutil.copytree(src, dst, ignore=ignore, symlinks=True)
def read_lines(path):
with open(path) as fh:
return fh.readlines()
def write_lines(path, lines):
with open(path, "w") as fh:
fh.writelines(lines)
def main():
# Line-buffer stdout so progress is visible live under CI / the cmake target.
try:
sys.stdout.reconfigure(line_buffering=True)
except (AttributeError, ValueError):
pass
here = os.path.dirname(os.path.abspath(__file__))
default_root = os.path.dirname(here)
ap = argparse.ArgumentParser(description="Mutation testing for libakgl")
ap.add_argument("--source-root", default=default_root)
ap.add_argument("--target", action="append", default=None)
ap.add_argument("--work", default=None)
ap.add_argument("--timeout", type=int, default=120)
ap.add_argument("--exclude-test", default="^character$",
help="CTest regex to exclude (default: ^character$)")
ap.add_argument("--threshold", type=float, default=0.0)
ap.add_argument("--junit", default=None,
help="write a JUnit XML report to this path")
ap.add_argument("--max-mutants", type=int, default=0,
help="cap the run at N evenly-sampled mutants (0 = all)")
ap.add_argument("--list", action="store_true")
ap.add_argument("--keep", action="store_true")
ap.add_argument("-j", type=int, default=1)
args = ap.parse_args()
root = os.path.abspath(args.source_root)
targets = args.target or [
"src/actor.c", "src/actor_state_string_names.c", "src/assets.c",
"src/character.c", "src/controller.c", "src/draw.c",
"src/game.c", "src/heap.c", "src/json_helpers.c",
"src/physics.c", "src/registry.c", "src/renderer.c",
"src/sprite.c", "src/staticstring.c", "src/text.c",
"src/tilemap.c", "src/util.c",
]
# Enumerate mutants from the pristine sources.
all_mutants = []
for t in targets:
all_mutants.extend(generate_mutants(root, t))
print(f"Generated {len(all_mutants)} mutants across {len(targets)} file(s):")
for t in targets:
n = sum(1 for m in all_mutants if m.path == t)
print(f" {t}: {n}")
# Optional even-strided sampling to bound run time (CI / smoke tests).
if args.max_mutants and len(all_mutants) > args.max_mutants:
step = len(all_mutants) / args.max_mutants
sampled = [all_mutants[int(i * step)] for i in range(args.max_mutants)]
print(f"Sampling {len(sampled)} of {len(all_mutants)} mutants "
f"(--max-mutants {args.max_mutants}).")
all_mutants = sampled
if args.list:
for m in all_mutants:
print(" " + m.describe())
return 0
if not all_mutants:
print("No mutants generated; nothing to do.")
return 0
# Scratch working copy.
work_parent = args.work or tempfile.mkdtemp(prefix="akgl_mut_")
work = os.path.join(work_parent, "src") if args.work else work_parent
if os.path.exists(work):
shutil.rmtree(work)
print(f"\nCopying sources to scratch dir: {work}")
copy_tree(root, work)
runner = Runner(work, args.timeout, args.exclude_test)
print("Configuring baseline ...")
ok, out = runner.configure()
if not ok:
sys.stderr.write(out.decode(errors="replace"))
sys.stderr.write("\nBaseline configure FAILED; aborting.\n")
return 2
print("Verifying baseline is green (no mutation) ...")
baseline = runner.build_and_test()
if baseline != "survived":
sys.stderr.write(f"Baseline is not green ({baseline}); aborting. "
"Fix the suite before mutation testing.\n")
return 2
print("Baseline OK.\n")
# Group mutants by file so we mutate one file at a time and restore it.
killed = {"killed-compile": 0, "killed-test": 0, "killed-timeout": 0}
survivors = []
records = []
total = len(all_mutants)
# Cache pristine contents per target.
pristine = {t: read_lines(os.path.join(work, t)) for t in targets}
for idx, m in enumerate(all_mutants, start=1):
tgt_abs = os.path.join(work, m.path)
lines = list(pristine[m.path])
lines[m.lineno - 1] = m.after
write_lines(tgt_abs, lines)
try:
result = runner.build_and_test()
finally:
write_lines(tgt_abs, pristine[m.path]) # always restore
records.append((m, result))
if result == "survived":
survivors.append(m)
mark = "SURVIVED"
else:
killed[result] += 1
mark = result.upper()
print(f"[{idx}/{total}] {mark:16} {m.describe()}")
total_killed = sum(killed.values())
score = 100.0 * total_killed / total if total else 100.0
print("\n" + "=" * 72)
print("MUTATION TESTING SUMMARY")
print("=" * 72)
print(f" total mutants : {total}")
print(f" killed (test) : {killed['killed-test']}")
print(f" killed (compile): {killed['killed-compile']}")
print(f" killed (timeout): {killed['killed-timeout']}")
print(f" survived : {len(survivors)}")
print(f" mutation score : {score:.1f}%")
if survivors:
print("\nSurviving mutants (test-suite gaps -- turn these into tests):")
for m in survivors:
print(" " + m.describe())
if args.junit:
junit_path = os.path.abspath(args.junit)
write_junit(junit_path, records, targets)
print(f"\nJUnit report written to: {junit_path}")
if not args.keep and not args.work:
shutil.rmtree(work_parent, ignore_errors=True)
else:
print(f"\nScratch working copy kept at: {work}")
if args.threshold > 0 and score < args.threshold:
print(f"\nFAIL: mutation score {score:.1f}% < threshold {args.threshold:.1f}%")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,87 +0,0 @@
;;; 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

View File

@@ -1,101 +0,0 @@
#!/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

@@ -1,85 +0,0 @@
# Valgrind suppressions for the libakgl memory-check run.
#
# Used by `cmake --build build --target memcheck` and by any hand-run
# `ctest -T memcheck`; see AGENTS.md -> "Memory checking".
#
# Every entry here is a deliberate decision that a finding belongs to somebody
# else's code and cannot be fixed from this repository. Nothing that libakgl
# allocates is suppressed, and nothing is suppressed merely because it is noisy.
# If a suppression starts hiding a real finding, delete it -- a false negative in
# a leak check is worth more than a quiet log.
#
# The memcheck target runs the suite headless (SDL_VIDEO_DRIVER=dummy,
# SDL_RENDER_DRIVER=software, SDL_AUDIO_DRIVER=dummy) precisely so that the GPU
# stack is never loaded. That removes thousands of unfixable findings inside
# amdgpu_dri.so / Mesa / libGLX without suppressing anything, which is why there
# are no driver entries below. Run the suite against a real driver and you will
# need them; do not add them here on that account.
# SDL allocates its global state -- the hint table, the property registry, the
# log category array, the clipboard -- on first use and reclaims it in SDL_Quit.
# A test that fails before SDL_Quit, and every test that deliberately leaves SDL
# up so a later assertion can look at it, leaves those blocks behind. They are
# one-per-process, not per-call, so they cannot accumulate into a leak.
{
sdl3-global-state-still-reachable
Memcheck:Leak
match-leak-kinds: reachable
...
fun:SDL_InitSubSystem_REAL
}
# dlopen keeps the link map and the loaded objects' TLS blocks for the process
# lifetime; a dlclose does not return them. SDL loads its video, audio, and
# render backends this way, and SDL_image and SDL_mixer load their codecs the
# same way.
{
dl-open-keeps-its-link-map
Memcheck:Leak
match-leak-kinds: reachable,possible
...
fun:_dl_open
}
# FreeType, reached through SDL_ttf, caches per-face and per-size structures
# inside the library instance and frees them in FT_Done_FreeType, which SDL_ttf
# calls from TTF_Quit. A suite that opens a font and exits without TTF_Quit
# reports them; a font libakgl itself failed to close is a different finding and
# is NOT covered here -- it appears under akgl_text_loadfont, not under
# FT_Init_FreeType.
{
freetype-library-instance
Memcheck:Leak
match-leak-kinds: reachable
...
fun:FT_Init_FreeType
}
# deps/semver's own unit test leaks the buffers it cuts strings into --
# semver_unit.c:8 and :21, one calloc each per case. Vendored code, and its test
# program at that: not ours to edit, and not worth carrying a local patch for.
# Named by function rather than by file so that a rewrite of those cases stops
# being suppressed and comes back as a finding.
#
# This is the only entry here that hides a real leak in a program this build
# runs. It is bounded, it is in a test, and the alternative is either a fork of
# a vendored dependency or a memcheck run that is always red -- see TODO.md,
# "Memory checking" item 39.
{
vendored-semver-unit-test-strcut-first
Memcheck:Leak
match-leak-kinds: definite
fun:calloc
fun:test_strcut_first
fun:test_strcut
fun:main
}
{
vendored-semver-unit-test-strcut-second
Memcheck:Leak
match-leak-kinds: definite
fun:calloc
fun:test_strcut_second
fun:test_strcut
fun:main
}

View File

@@ -1,8 +1,3 @@
/**
* @file actor.c
* @brief Implements the actor subsystem.
*/
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <string.h>
@@ -28,14 +23,14 @@ akerr_ErrorContext *akgl_actor_initialize(akgl_Actor *obj, char *name)
obj->curSpriteReversing = false;
obj->scale = 1.0;
obj->movement_controls_face = true;
obj->updatefunc = &akgl_actor_update;
obj->renderfunc = &akgl_actor_render;
obj->facefunc = &akgl_actor_automatic_face;
obj->movementlogicfunc = &akgl_actor_logic_movement;
obj->changeframefunc = &akgl_actor_logic_changeframe;
obj->addchild = &akgl_actor_add_child;
FAIL_ZERO_RETURN(
errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj),
@@ -56,10 +51,6 @@ akerr_ErrorContext *akgl_actor_set_character(akgl_Actor *obj, char *basecharname
obj->basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, basecharname, NULL);
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Character not found in the registry");
obj->ax = 0;
obj->ay = 0;
obj->sx = obj->basechar->sx;
obj->sy = obj->basechar->sy;
SUCCEED_RETURN(errctx);
}
@@ -111,7 +102,7 @@ akerr_ErrorContext *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *c
} else {
// we are at the end of the animation and we either loop forward or do not loop
obj->curSpriteFrameId = 0;
}
}
// we are not looping in reverse and we are not at the end of the animation
} else {
obj->curSpriteFrameId += 1;
@@ -122,25 +113,14 @@ akerr_ErrorContext *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *c
SUCCEED_RETURN(errctx);
}
// raises AKGL_ERR_LOGICINTERRUPT if we don't want the physics simulator to process us
akerr_ErrorContext *akgl_actor_logic_movement(akgl_Actor *actor, float32_t dt)
// raises AKGL_ERR_LOGICINTERRUPT if we don't want the physics object to process us
akerr_ErrorContext *akgl_actor_logic_movement(akgl_Actor *obj, float32_t dt)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
FAIL_ZERO_RETURN(errctx, actor->basechar, AKERR_NULLPOINTER, "actor->basechar");
actor->sx = actor->basechar->sx;
actor->sy = actor->basechar->sy;
actor->sz = actor->basechar->sz;
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ) {
actor->ax = -actor->basechar->ax;
} else if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
actor->ax = actor->basechar->ax;
}
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
actor->ay = -actor->basechar->ay;
} else if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
actor->ay = actor->basechar->ay;
}
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj->basechar");
// Effectively a NOOP, this is handled by the physics engine now
// These functions are still present in case the library user wants per-actor behavior
SUCCEED_RETURN(errctx);
}
@@ -149,7 +129,7 @@ akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
PREPARE_ERROR(errctx);
SDL_Time curtime = 0;
akgl_Sprite *curSprite = NULL;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor reference");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
@@ -169,37 +149,18 @@ akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
// or changeframefunc, both of which should never return AKERR_KEY...
SUCCEED_RETURN(errctx);
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Decide whether an actor is worth drawing this frame.
*
* Two questions at once: is the actor on camera, and does it want to be drawn.
* The camera test allows a sprite's own width and height on the near edges, so
* an actor partly on screen still counts as visible rather than popping in once
* its origin crosses the boundary.
*
* An actor with no sprite for its current state is reported as not visible
* rather than as an error -- there is nothing to draw, which is an answer.
*
* @param obj The actor to test. Required, along with its `basechar`.
* @param camera The visible rectangle in map coordinates. Required in practice;
* not checked, and dereferenced once a sprite has been found.
* @param visible Receives the verdict. Required; not checked. Written on the
* no-sprite path as well as the ordinary ones.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
*/
static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, bool *visible)
{
PREPARE_ERROR(errctx);
akgl_Sprite *curSprite = NULL;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
ATTEMPT {
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
} CLEANUP {
@@ -209,7 +170,7 @@ static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, boo
*visible = false;
SUCCEED_RETURN(errctx);
} FINISH(errctx, true);
if ( (obj->x < (camera->x - curSprite->width)) ||
(obj->x > (camera->x + camera->w)) ||
(obj->y < (camera->y - curSprite->height)) ||
@@ -228,13 +189,13 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
bool visible = false;
SDL_FRect src;
SDL_FRect dest;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
ATTEMPT {
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));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_KEY) {
@@ -242,7 +203,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
LOG_ERROR(errctx);
} FINISH(errctx, true);
if ( ! visible ) {
SUCCEED_RETURN(errctx);
}
@@ -264,79 +225,47 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
if ( obj->parent != NULL ) {
dest.x = (obj->parent->x + obj->x - camera->x);
dest.y = (obj->parent->y + obj->y - camera->y);
dest.x = (obj->parent->x + obj->x - camera.x);
dest.y = (obj->parent->y + obj->y - camera.y);
} else {
dest.x = (obj->x - camera->x);
dest.y = (obj->y - camera->y);
dest.x = (obj->x - camera.x);
dest.y = (obj->y - camera.y);
}
dest.w = curSprite->width * obj->scale;
dest.h = curSprite->width * obj->scale;
PASS(errctx, renderer->draw_texture(renderer, curSprite->sheet->texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
PASS(errctx, renderer.draw_texture(&renderer, curSprite->sheet->texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child)
{
int i = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL parent 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");
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN ; i++ ) {
if ( obj->children[i] == NULL ) {
obj->children[i] = child;
child->parent = obj;
child->refcount += 1;
SUCCEED_RETURN(errctx);
}
int i = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL parent 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");
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN ; i++ ) {
if ( obj->children[i] == NULL ) {
obj->children[i] = child;
child->parent = obj;
child->refcount += 1;
SUCCEED_RETURN(errctx);
}
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)
{
PREPARE_ERROR(errctx);
akgl_Iterator *opflags = (akgl_Iterator *)userdata;
akgl_Actor *obj = NULL;
ATTEMPT {
FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "registry_iterate_actor received NULL property name");
FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "received NULL iterator flags");
obj = SDL_GetPointerProperty(registry, name, NULL);
FAIL_ZERO_BREAK(errctx, obj, AKERR_KEY, "registry_iterate_actor received property name that was not in the registry");
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) && obj->layer != opflags->layerid) {
break;
}
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_UPDATE)) {
CATCH(errctx, obj->updatefunc(obj));
}
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE)) {
CATCH(errctx, akgl_tilemap_scale_actor(gamemap, obj));
} else {
obj->scale = 1.0;
}
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RENDER)) {
CATCH(errctx, obj->renderfunc(obj));
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
//SDL_Log("event %d (button %d / key %d) moves actor left", event->type, event->gbutton.which, event->key.key);
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
obj->ax = -(obj->basechar->ax);
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT));
//SDL_Log("new target actor state: %b", obj->state);
SUCCEED_RETURN(errctx);
@@ -348,8 +277,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
//SDL_Log("event %d (button %d / key %d) stops moving actor left", event->type, event->gbutton.which, event->key.key);
obj->ax = 0;
obj->ex = 0;
obj->tx = 0;
obj->vx = 0;
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT);
@@ -362,9 +289,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
//SDL_Log("event %d (button %d / key %d) moves actor right", event->type, event->gbutton.which, event->key.key);
obj->ax = obj->basechar->ax;
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT));
//SDL_Log("new target actor state: %b", obj->state);
@@ -377,8 +302,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SD
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
//SDL_Log("event %d (button %d / key %d) stops moving actor right", event->type, event->gbutton.which, event->key.key);
obj->ax = 0;
obj->ex = 0;
obj->tx = 0;
obj->vx = 0;
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
@@ -391,9 +314,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Ev
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
//SDL_Log("event %d (button %d / key %d) moves actor up", event->type, event->gbutton.which, event->key.key);
obj->ay = -(obj->basechar->ay);
//SDL_Log("event %d (button %d / key %d) moves actor up", event->type, event->gbutton.which, event->key.key);
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_UP | AKGL_ACTOR_STATE_MOVING_UP));
//SDL_Log("new target actor state: %b", obj->state);
@@ -406,8 +327,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_E
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
//SDL_Log("event %d (button %d / key %d) stops moving actor up", event->type, event->gbutton.which, event->key.key);
obj->ay = 0;
obj->ey = 0;
obj->ty = 0;
obj->vy = 0;
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_UP);
@@ -420,9 +339,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
//SDL_Log("event %d (button %d / key %d) moves actor down", event->type, event->gbutton.which, event->key.key);
obj->ay = obj->basechar->ay;
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN));
//SDL_Log("new target actor state: %b", obj->state);
@@ -436,8 +353,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
//SDL_Log("event %d (button %d / key %d) stops moving actor down", event->type, event->gbutton.which, event->key.key);
obj->ty = 0;
obj->ey = 0;
obj->ay = 0;
obj->vy = 0;
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_DOWN);
//SDL_Log("new target actor state: %b", obj->state);

View File

@@ -1,8 +1,3 @@
/**
* @file actor_state_string_names.c
* @brief Implements the actor state string names subsystem.
*/
char *AKGL_ACTOR_STATE_STRING_NAMES[32] = {
"AKGL_ACTOR_STATE_FACE_DOWN",
"AKGL_ACTOR_STATE_FACE_LEFT",

View File

@@ -1,8 +1,3 @@
/**
* @file assets.c
* @brief Implements the assets subsystem.
*/
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
@@ -14,46 +9,46 @@
akerr_ErrorContext *akgl_load_start_bgm(char *fname)
{
PREPARE_ERROR(errctx);
//akgl_String *tmpstr = NULL;
MIX_Track *bgmtrack = NULL;
SDL_PropertiesID bgmprops = 0;
PREPARE_ERROR(errctx);
//akgl_String *tmpstr = NULL;
MIX_Track *bgmtrack = NULL;
SDL_PropertiesID bgmprops = 0;
ATTEMPT {
FAIL_ZERO_BREAK(errctx, fname, AKERR_NULLPOINTER, "akgl_load_start_bgm received NULL filename");
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
ATTEMPT {
FAIL_ZERO_BREAK(errctx, fname, AKERR_NULLPOINTER, "akgl_load_start_bgm received NULL filename");
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//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());
//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);
FAIL_ZERO_BREAK(errctx, bgmtrack, AKERR_NULLPOINTER, "Failed to create audio track for background music: %s", SDL_GetError());
bgmtrack = MIX_CreateTrack(akgl_mixer);
FAIL_ZERO_BREAK(errctx, bgmtrack, AKERR_NULLPOINTER, "Failed to create audio track for background music: %s", SDL_GetError());
akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM] = bgmtrack;
FAIL_ZERO_BREAK(
errctx,
MIX_SetTrackAudio(bgmtrack, bgm),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM] = bgmtrack;
FAIL_ZERO_BREAK(
errctx,
MIX_SetTrackAudio(bgmtrack, bgm),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SDL_SetNumberProperty(bgmprops, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
if (!MIX_PlayTrack(bgmtrack, bgmprops)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Failed to play music asset %s", fname);
}
} 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);
SDL_SetNumberProperty(bgmprops, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
if (!MIX_PlayTrack(bgmtrack, bgmprops)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Failed to play music asset %s", fname);
}
} 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);
}

View File

@@ -1,518 +0,0 @@
/**
* @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;
akgl_audio_voices[i].sweep_from_hz = 0.0f;
akgl_audio_voices[i].sweep_to_hz = 0.0f;
akgl_audio_voices[i].sweep_step_hz = 0.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 Pitch @p voice sounds at where its sweep currently stands.
*
* Recomputed from the frame counter each time rather than added to as it goes,
* so a sweep lands on exactly the frequencies its step size describes however
* the caller happens to have chopped up its calls to akgl_audio_mix().
*
* A voice that is not sweeping keeps the frequency it was given.
*/
static float32_t voice_sweep_hz(akgl_AudioVoice *voice)
{
uint32_t ticks = 0;
float32_t moved = 0.0f;
float32_t hz = 0.0f;
if ( voice->sweep_step_hz <= 0.0f ) {
return voice->hz;
}
ticks = voice->elapsed_frames / AKGL_AUDIO_SWEEP_TICK_FRAMES;
moved = voice->sweep_step_hz * (float32_t)ticks;
if ( voice->sweep_to_hz < voice->sweep_from_hz ) {
hz = voice->sweep_from_hz - moved;
if ( hz < voice->sweep_to_hz ) {
hz = voice->sweep_to_hz;
}
} else {
hz = voice->sweep_from_hz + moved;
if ( hz > voice->sweep_to_hz ) {
hz = voice->sweep_to_hz;
}
}
return hz;
}
/**
* @brief Start a note on @p voice, sweeping or held.
*
* The whole of what akgl_audio_tone() and akgl_audio_sweep() do once their
* arguments have been checked. A held note is a sweep with a step of 0, which
* is also what makes a voice reused for a plain tone stop sweeping.
*/
static void start_note(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
{
ensure_voices();
lock_voices();
akgl_audio_voices[voice].hz = from_hz;
akgl_audio_voices[voice].phase = 0.0f;
akgl_audio_voices[voice].sweep_from_hz = from_hz;
akgl_audio_voices[voice].sweep_to_hz = to_hz;
akgl_audio_voices[voice].sweep_step_hz = step_hz;
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();
}
/** @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");
// A step of 0 is what says "one pitch, held", so this also clears a sweep
// left on the voice by an earlier akgl_audio_sweep().
start_note(voice, hz, hz, 0.0f, ms);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
FAIL_NONZERO_RETURN(errctx, (from_hz <= 0.0f), AKERR_OUTOFBOUNDS, "Start frequency %f is not positive", from_hz);
FAIL_NONZERO_RETURN(errctx, (to_hz <= 0.0f), AKERR_OUTOFBOUNDS, "Target frequency %f is not positive", to_hz);
FAIL_NONZERO_RETURN(
errctx,
(step_hz <= 0.0f),
AKERR_OUTOFBOUNDS,
"Step %f is not positive; the direction of a sweep comes from the two frequencies",
step_hz);
FAIL_ZERO_RETURN(errctx, ms, AKERR_OUTOFBOUNDS, "A tone needs a duration; use akgl_audio_stop to silence a voice");
start_note(voice, from_hz, to_hz, step_hz, ms);
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;
bool sweeping = false;
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;
}
sweeping = (voice->sweep_step_hz > 0.0f);
if ( sweeping ) {
voice->hz = voice_sweep_hz(voice);
} else {
// 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);
if ( sweeping ) {
// The derived form above assumes one frequency for the whole note.
// Under a sweep it would jump the waveform at every step -- an
// audible click -- so a swept voice accumulates instead and takes
// the drift the derived form exists to avoid.
voice->phase = (float32_t)SDL_fmod(
(double)voice->phase + ((double)voice->hz / (double)AKGL_AUDIO_SAMPLE_RATE),
1.0);
}
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

@@ -1,8 +1,3 @@
/**
* @file character.c
* @brief Implements the character subsystem.
*/
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <akerror.h>
@@ -27,10 +22,10 @@ akerr_ErrorContext *akgl_character_initialize(akgl_Character *obj, char *name)
strncpy(obj->name, name, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH);
obj->state_sprites = SDL_CreateProperties();
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_get = &akgl_character_sprite_get;
FAIL_ZERO_RETURN(
errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_CHARACTER, name, (void *)obj),
@@ -90,30 +85,6 @@ void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID regis
} FINISH_NORETURN(errctx);
}
/**
* @brief OR a JSON array of actor-state names together into one bitmask.
*
* This is what lets a character JSON say `["FACE_LEFT", "MOVING_LEFT"]` rather
* than a number: each name is looked up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS
* and OR-ed in. A name that is not a known state is an error rather than being
* skipped -- a typo in a state name would otherwise bind a sprite to the wrong
* combination and show up as art that never appears.
*
* @param states JSON array of state-name strings. Required. An empty array is
* legal and leaves @p dest as it was.
* @param dest Receives the OR of every named bit. Required in practice, and
* **not** checked -- the guard that should test it tests @p states
* a second time instead, so a `NULL` here is a crash. It is OR-ed
* into rather than assigned, so the caller must zero it first.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p states is `NULL`.
* @throws AKERR_KEY If a name is not in the actor-state registry. The message
* quotes it. Note that bits 11 and 12 are unreachable by name; see
* akgl_registry_init_actor_state_strings.
* @throws AKERR_TYPE If an array element is not a string.
* @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_t *states, int *dest)
{
int i = 0;
@@ -122,7 +93,7 @@ static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL states array");
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL destination integer");
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpstring));
for ( i = 0; i < json_array_size((json_t *)states) ; i++ ) {
@@ -138,29 +109,6 @@ static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_
SUCCEED_RETURN(errctx);
}
/**
* @brief Name a character and build its state-to-sprite map from a JSON document.
*
* The half of akgl_character_load_json that deals with identity and sprites; the
* caller handles the numeric fields afterwards. Reads `name`, initializes the
* character with it, then walks `sprite_mappings` binding each named sprite to
* the bitmask its `state` array adds up to.
*
* Every sprite named must already be in #AKGL_REGISTRY_SPRITE.
*
* @param json The parsed character document. Required in practice; not checked,
* and passed straight to accessors that report it.
* @param obj The pooled character to fill in. Required, unchecked -- it is
* handed to akgl_character_initialize, which does check it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If a mapping names a sprite that is not registered.
* The message names the character, the state, and the sprite.
* @throws AKERR_KEY If `name`, `sprite_mappings`, or a mapping's `sprite` or
* `state` is absent, or if a state name is unknown.
* @throws AKERR_TYPE If one of those keys has the wrong JSON type.
* @throws AKERR_OUTOFBOUNDS If an array is indexed past its end.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Character *obj)
{
PREPARE_ERROR(errctx);
@@ -172,7 +120,7 @@ static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Cha
akgl_String *tmpstr = NULL;
akgl_String *tmpstr2 = NULL;
int stateval = 0;
ATTEMPT {
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr));
CATCH(errctx, akgl_character_initialize((akgl_Character *)obj, tmpstr->data));
@@ -187,15 +135,15 @@ static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Cha
NULL
);
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_character_load_json_state_int_from_strings(statearray, &stateval));
CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
FAIL_ZERO_BREAK(
errctx,
spriteptr,
AKERR_NULLPOINTER,
AKERR_NULLPOINTER,
"Character %s for state %b references sprite %s but not found in the registry",
tmpstr2->data,
stateval,
@@ -218,11 +166,11 @@ static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Cha
akerr_ErrorContext *akgl_character_load_json(char *filename)
{
PREPARE_ERROR(errctx);
json_t *json = NULL;
json_t *json;
json_error_t error;
akgl_Character *obj = NULL;
//akgl_String *tmpstr = NULL;
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
ATTEMPT {
CATCH(errctx, akgl_heap_next_character(&obj));
@@ -245,13 +193,6 @@ akerr_ErrorContext *akgl_character_load_json(char *filename)
CATCH(errctx, akgl_get_json_number_value(json, "acceleration_y", &obj->ay));
} CLEANUP {
//IGNORE(akgl_heap_release_string(tmpstr));
// The character keeps nothing that points into the document -- every
// field above is copied out of it -- so the whole parsed tree goes back
// here, on the success path as well as the failure one.
if ( json != NULL ) {
json_decref(json);
json = NULL;
}
if ( errctx != NULL ) {
if ( errctx->status != 0 ) {
IGNORE(akgl_heap_release_character(obj));

View File

@@ -1,8 +1,3 @@
/**
* @file controller.c
* @brief Implements the controller subsystem.
*/
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/heap.h>
@@ -12,176 +7,6 @@
akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
/*
* Keystrokes waiting for akgl_controller_poll_key() and
* akgl_controller_poll_keystroke(). 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 akgl_Keystroke keybuffer[AKGL_CONTROLLER_KEY_BUFFER];
static int keybuffer_head = 0;
static int keybuffer_count = 0;
/*
* Whether the newest entry is a key press that has not been given its composed
* text yet. SDL reports one keystroke as two events -- the key going down, and
* then the text it composed to, if it composed to anything -- so the text that
* arrives next belongs to the press that arrived last. Without this flag a
* press dropped by a full buffer would hand its text to whatever older press
* happened to still be sitting at the end of the ring.
*/
static bool keybuffer_awaiting_text = false;
/**
* @brief Length in bytes of the UTF-8 sequence @p src begins with, or 0.
*
* 0 both for a byte that cannot start a sequence and for a sequence the string
* ends in the middle of. SDL should hand over neither, and reading past a
* terminator to find out otherwise is not a risk worth taking for input that
* arrives from a keyboard driver.
*
* @param src UTF-8 text, NUL terminated. Required.
*/
static size_t utf8_sequence_length(const char *src)
{
unsigned char lead = (unsigned char)src[0];
size_t len = 0;
size_t i = 0;
if ( lead < 0x80 ) {
return 1;
} else if ( (lead & 0xe0) == 0xc0 ) {
len = 2;
} else if ( (lead & 0xf0) == 0xe0 ) {
len = 3;
} else if ( (lead & 0xf8) == 0xf0 ) {
len = 4;
} else {
return 0;
}
for ( i = 1; i < len; i++ ) {
// A NUL fails this too, which is what stops the scan at the end of a
// truncated string instead of past it.
if ( ((unsigned char)src[i] & 0xc0) != 0x80 ) {
return 0;
}
}
return len;
}
/**
* @brief Copy as much of @p src as fits in @p dest without splitting a character.
*
* SDL hands over whatever the platform composed, which for an input method can
* be several characters at once, and the ring entry holds one. Truncating on a
* byte boundary would leave a partial UTF-8 sequence that nothing downstream
* can render, so this stops at the last code point that fits whole.
*
* @param dest Receives the prefix, always NUL terminated. Required.
* @param destsize Bytes available in @p dest, terminator included.
* @param src UTF-8 text to copy. Required.
*/
static void copy_utf8_prefix(char *dest, size_t destsize, const char *src)
{
size_t used = 0;
size_t len = 0;
dest[0] = '\0';
while ( src[used] != '\0' ) {
len = utf8_sequence_length(&src[used]);
if ( len == 0 ) {
// Not valid UTF-8 from here on. Keep what is already known good.
break;
}
if ( (used + len) >= destsize ) {
break;
}
memcpy(&dest[used], &src[used], len);
used += len;
}
dest[used] = '\0';
}
/**
* @brief Record one key press, 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.
*
* The entry starts with no text. Whatever the press composes to arrives as a
* separate event and is attached by keybuffer_attach_text().
*/
static void keybuffer_push_key(SDL_Keycode key, SDL_Keymod mod)
{
int tail = 0;
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
keybuffer_awaiting_text = false;
return;
}
tail = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
keybuffer[tail].key = key;
keybuffer[tail].mod = mod;
keybuffer[tail].text[0] = '\0';
keybuffer_count += 1;
keybuffer_awaiting_text = true;
}
/**
* @brief Give composed text to the press it belongs to, or buffer it alone.
*
* An entry with no keycode is not a degenerate case to be avoided: an input
* method or a dead key finishes a character with no key press of its own, and a
* line editor wants that character. akgl_controller_poll_keystroke() reports it;
* akgl_controller_poll_key(), which has no keycode to give, discards it.
*/
static void keybuffer_attach_text(const char *text)
{
int newest = 0;
if ( (keybuffer_awaiting_text == true) && (keybuffer_count > 0) ) {
newest = (keybuffer_head + keybuffer_count - 1) % AKGL_CONTROLLER_KEY_BUFFER;
copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text);
keybuffer_awaiting_text = false;
return;
}
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
return;
}
newest = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
keybuffer[newest].key = 0;
keybuffer[newest].mod = 0;
copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text);
keybuffer_count += 1;
}
/**
* @brief Take the oldest entry, whatever it carries.
*
* @param dest Receives the entry. Required.
* @return `true` when an entry was taken, `false` when the buffer was empty.
*/
static bool keybuffer_take(akgl_Keystroke *dest)
{
if ( keybuffer_count == 0 ) {
return false;
}
memcpy(dest, &keybuffer[keybuffer_head], sizeof(akgl_Keystroke));
keybuffer_head = (keybuffer_head + 1) % AKGL_CONTROLLER_KEY_BUFFER;
keybuffer_count -= 1;
if ( keybuffer_count == 0 ) {
// Nothing is left for a text event to attach itself to.
keybuffer_awaiting_text = false;
}
return true;
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
{
int count;
@@ -189,20 +14,11 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, keyboards, AKERR_NULLPOINTER, "%s", SDL_GetError());
// The array is SDL's to allocate and ours to free -- that is the contract on
// every SDL_Get*s() enumeration. Released in CLEANUP so the loop can fail
// without taking the array with it.
ATTEMPT {
for (int i = 0; i < count; i++) {
const char *name = SDL_GetKeyboardNameForID(keyboards[i]);
SDL_Log("Keyboard %d: ID %u, Name: %s\n", i, keyboards[i], name);
}
} CLEANUP {
SDL_free(keyboards);
keyboards = NULL;
} PROCESS(e) {
} FINISH(e, true);
for (int i = 0; i < count; i++) {
const char *name = SDL_GetKeyboardNameForID(keyboards[i]);
SDL_Log("Keyboard %d: ID %u, Name: %s\n", i, keyboards[i], name);
}
SUCCEED_RETURN(e);
}
@@ -244,18 +60,6 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
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_key(event->key.key, event->key.mod);
} else if ( event->type == SDL_EVENT_TEXT_INPUT ) {
// The character SDL composed for the press recorded a moment ago. A
// keycode cannot express it: the layout, the shift state, a compose key
// and a dead key all have their say between the two events.
keybuffer_attach_text(event->text.text);
}
ATTEMPT {
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
curmap = &GAME_ControlMaps[i];
@@ -271,7 +75,7 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
// This controlmap processes this control
eventButtonComboMatch = (
((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.button == curcontrol->button) ||
((event->type == SDL_EVENT_KEY_DOWN ||
@@ -295,136 +99,95 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
}
}
}
_akgl_controller_handle_event_success:
_akgl_controller_handle_event_success:
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Set the movement and facing bits on the "player" actor for a D-pad or arrow press.
*
* A whole-application shortcut rather than a control-map binding: it looks the
* actor up by the literal name `"player"` in #AKGL_REGISTRY_ACTOR instead of
* being told which actor to act on, so it only ever drives one. The
* akgl_Actor_cmhf_* handlers are the general form.
*
* Facing follows movement unless the actor sets `movement_controls_face`, which
* is how an actor that aims independently of the direction it walks opts out.
*
* Declared nowhere -- `controller.h` advertises this as
* `akgl_controller_handle_button_down`, which does not exist. TODO.md, "Known
* and still open" item 10.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only;
* never read.
* @param event The button or key event. Required. Both the gamepad and
* keyboard unions are read on every call, so the arm that does
* not correspond to the event type is read as garbage -- which
* works only because no real button and keycode pair collides.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
{
akgl_Actor *player = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "Player actor does not exist");
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);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_DOWN);
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_DOWN);
}
SDL_Log("New state : %d", player->state);
} 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");
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_UP);
if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_UP);
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_UP);
}
SDL_Log("New state : %d", player->state);
} 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");
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_LEFT);
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_LEFT);
}
SDL_Log("New state : %d", player->state);
} 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");
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_RIGHT);
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_RIGHT);
}
SDL_Log("New state : %d", player->state);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Clear the movement bit on the "player" actor for a D-pad or arrow release, and reset its animation.
*
* The counterpart to gamepad_handle_button_down. It clears only the movement
* bit, leaving the facing bits alone so the actor keeps looking the way it was
* walking, and rewinds `curSpriteFrameId` to 0 so the next step starts from the
* first frame of the walk cycle rather than wherever it stopped.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The button or key release event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
{
akgl_Actor *player = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "Player actor does not exist");
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
event->key.key == SDLK_DOWN ) {
event->key.key == SDLK_DOWN ) {
SDL_Log("processing down release");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state);
} 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");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_UP);
player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state);
} 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");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state);
} 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");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
player->curSpriteFrameId = 0;
@@ -433,53 +196,29 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
SUCCEED_RETURN(errctx);
}
/**
* @brief Open a gamepad that has just been plugged in, and log its mapping.
*
* SDL delivers no button events from an unopened gamepad, so a device that
* appears mid-session has to be opened here the way akgl_controller_open_gamepads
* opens the ones present at startup. A gamepad SDL has already opened is logged
* and left alone.
*
* The mapping is logged because a controller with no entry in the database
* produces no button events at all, and that is otherwise indistinguishable
* from a broken binding.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_ADDED event. Required. The joystick id
* is read out of the `gbutton` arm rather than `gdevice`, which
* works because the two share their `which` field.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*
* @note A failed `SDL_OpenGamepad` is logged rather than reported: this returns
* success and the device stays silent.
*/
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
{
SDL_JoystickID which;
SDL_Gamepad *gamepad = NULL;
char *mapping = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
which = event->gbutton.which;
gamepad = SDL_GetGamepadFromID(which);
if (!gamepad) {
SDL_Log("Gamepad #%u add, but not opened: %s", (unsigned int) which, SDL_GetError());
gamepad = SDL_OpenGamepad(which);
SDL_Log("Gamepad #%u opened: %s", (unsigned int) which, SDL_GetError());
mapping = SDL_GetGamepadMapping(gamepad);
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 ) {
SDL_Log("Gamepad #%u mapping : %s", (unsigned int) which, mapping);
SDL_free(mapping);
SDL_Log("Gamepad #%u mapping : %s", (unsigned int) which, mapping);
SDL_free(mapping);
}
} else {
SDL_Log("Gamepad #%u ('%s') added", (unsigned int) which, SDL_GetGamepadName(gamepad));
@@ -487,21 +226,6 @@ akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
SUCCEED_RETURN(errctx);
}
/**
* @brief Close a gamepad that has been unplugged.
*
* An unplugged device SDL still has open is a leaked handle, so this closes it.
* A removal for a device that was never opened is logged and otherwise ignored.
* Any control map still holding that `jsid` simply stops matching -- the map is
* not torn down.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_REMOVED event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*/
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
{
SDL_JoystickID which;
@@ -509,11 +233,11 @@ akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
which = event->gbutton.which;
gamepad = SDL_GetGamepadFromID(which);
if (gamepad) {
SDL_CloseGamepad(gamepad); /* the joystick was unplugged. */
}
@@ -535,14 +259,14 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akg
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid)
{
akgl_ControlMap *controlmap;
akgl_Control control;
PREPARE_ERROR(errctx);
ATTEMPT {
// set up the control map
@@ -551,12 +275,12 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
controlmap = &GAME_ControlMaps[controlmapid];
controlmap->kbid = kbid;
controlmap->jsid = jsid;
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, AKERR_REGISTRY, "Actor %s not found in registry", actorname);
// ---- KEYBOARD CONTROLS ----
// Move down
control.key = SDLK_DOWN;
control.event_on = SDL_EVENT_KEY_DOWN;
@@ -572,7 +296,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
control.handler_on = &akgl_Actor_cmhf_up_on;
control.handler_off = &akgl_Actor_cmhf_up_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move left
control.key = SDLK_LEFT;
control.event_on = SDL_EVENT_KEY_DOWN;
@@ -580,7 +304,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
control.handler_on = &akgl_Actor_cmhf_left_on;
control.handler_off = &akgl_Actor_cmhf_left_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move right
control.key = SDLK_RIGHT;
control.event_on = SDL_EVENT_KEY_DOWN;
@@ -606,7 +330,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
control.handler_on = &akgl_Actor_cmhf_up_on;
control.handler_off = &akgl_Actor_cmhf_up_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move left
control.button = SDL_GAMEPAD_BUTTON_DPAD_LEFT;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
@@ -614,7 +338,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
control.handler_on = &akgl_Actor_cmhf_left_on;
control.handler_off = &akgl_Actor_cmhf_left_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move right
control.button = SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
@@ -622,55 +346,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
control.handler_on = &akgl_Actor_cmhf_right_on;
control.handler_off = &akgl_Actor_cmhf_right_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
SUCCEED_RETURN(errctx);
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available)
{
akgl_Keystroke keystroke;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, keycode, AKERR_NULLPOINTER, "NULL keycode destination");
FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");
// 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;
while ( keybuffer_take(&keystroke) == true ) {
if ( keystroke.key == 0 ) {
// Composed text with no key behind it. This form has no way to
// report it, so it is dropped rather than handed back as key 0.
continue;
}
*keycode = (int)keystroke.key;
*available = true;
break;
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL keystroke destination");
FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");
memset(dest, 0x00, sizeof(akgl_Keystroke));
*available = keybuffer_take(dest);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void)
{
PREPARE_ERROR(errctx);
keybuffer_head = 0;
keybuffer_count = 0;
keybuffer_awaiting_text = false;
SUCCEED_RETURN(errctx);
}

View File

@@ -1,39 +1,14 @@
/**
* @file draw.c
* @brief Implements the draw subsystem.
*/
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <akerror.h>
#include <akgl/draw.h>
#include <akgl/error.h>
#include <akgl/game.h>
/** @brief One horizontal run of pixels the flood fill has still to examine. */
typedef struct {
int x1; /**< Leftmost column of the run, inclusive. */
int x2; /**< Rightmost column of the run, inclusive. */
int y; /**< The row the run is on. */
} 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 */
void akgl_draw_background(int w, int h)
{
SDL_Color col[2] = {
{ 0x66, 0x66, 0x66, 0xff },
{ 0x99, 0x99, 0x99, 0xff },
{ 0x66, 0x66, 0x66, 0xff },
{ 0x99, 0x99, 0x99, 0xff },
};
int i, x, y;
SDL_FRect rect;
@@ -42,549 +17,14 @@ void akgl_draw_background(int w, int h)
rect.w = (float)dx;
rect.h = (float)dy;
for (y = 0; y < h; y += dy) {
for (x = 0; x < w; x += dx) {
/* use an 8x8 checkerboard pattern */
i = (((x ^ y) >> 3) & 1);
SDL_SetRenderDrawColor(renderer->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a);
for (x = 0; x < w; x += dx) {
/* use an 8x8 checkerboard pattern */
i = (((x ^ y) >> 3) & 1);
SDL_SetRenderDrawColor(renderer.sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a);
rect.x = (float)x;
rect.y = (float)y;
SDL_RenderFillRect(renderer->sdl_renderer, &rect);
}
rect.x = (float)x;
rect.y = (float)y;
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.
*
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`;
* every caller has already checked both.
* @param color The colour to install.
* @param previous Receives the colour that was in place. Assumed non-`NULL`.
* Pre-filled with opaque black, so it is safe to restore from
* even if the query below fails.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL If the current draw colour cannot be read or the new one
* cannot be set. The message carries `SDL_GetError()`.
*/
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.
*
* Called from `CLEANUP` blocks under `IGNORE()`, so its error is logged rather
* than propagated -- failing to restore a colour must not mask the failure the
* cleanup is unwinding from.
*
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`.
* @param previous The colour push_draw_color() recorded. Assumed non-`NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL If the draw colour cannot be set.
*/
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.
*
* @param surface The pixels to fill, in SDL_PIXELFORMAT_RGBA32. Assumed
* non-`NULL` and locked-or-lockless; the caller converts.
* @param x Seed column. Assumed inside the surface -- akgl_draw_flood_fill
* has already range-checked it.
* @param y Seed row, likewise.
* @param oldpixel The packed pixel value the region is made of. Everything else
* is a boundary.
* @param newpixel The packed pixel value to write. Must differ from @p oldpixel:
* if they are equal the fill never terminates making progress,
* which is why the caller checks that case first.
* @param dirty Receives the bounding box of everything written, so the caller
* can put back only the pixels that changed. Assumed non-`NULL`.
* Left describing an empty box if nothing matched.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If the region needs more than
* #AKGL_DRAW_MAX_FLOOD_SPANS pending spans. @p dirty is then not written
* and the surface is partially filled.
*/
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);
}

View File

@@ -1,24 +0,0 @@
/**
* @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

@@ -1,8 +1,3 @@
/**
* @file game.c
* @brief Implements the game subsystem.
*/
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
@@ -21,108 +16,22 @@
#include <akgl/staticstring.h>
#include <akgl/iterator.h>
#include <akgl/physics.h>
#include <akgl/error.h>
#include <akgl/SDL_GameControllerDB.h>
SDL_Window *window = NULL;
// Currently active objects
akgl_RenderBackend *renderer;
akgl_PhysicsBackend *physics;
SDL_FRect *camera;
akgl_Tilemap *gamemap;
// Default objects
akgl_RenderBackend _akgl_renderer;
akgl_PhysicsBackend _akgl_physics;
SDL_FRect _akgl_camera;
akgl_Tilemap _akgl_gamemap;
akgl_RenderBackend renderer;
akgl_PhysicsBackend physics;
akgl_Frame ball;
akgl_Frame paddle1;
akgl_Frame paddle2;
akgl_Frame table;
akgl_Tilemap gamemap;
MIX_Audio *bgm = NULL;
MIX_Mixer *akgl_mixer = NULL;
MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
SDL_FRect camera;
akgl_Game game;
static akerr_ErrorContext *write_exact(const void *ptr, size_t size, size_t nmemb, FILE *fp)
{
size_t transferred;
return aksl_fwrite(ptr, size, nmemb, fp, &transferred);
}
/**
* @brief Widest name field any of the save tables writes.
*
* The spritesheet table is the widest at #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH;
* the other three are name lengths half that or less.
*/
#define AKGL_GAME_SAVE_MAX_NAME_FIELD AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH
/**
* @brief Fail the build if a table ever declares a field wider than the staging buffer.
*
* A negative array size is a compile error, so widening one of these constants
* without widening #AKGL_GAME_SAVE_MAX_NAME_FIELD cannot get past the compiler
* and turn back into the overread this buffer exists to prevent.
*/
typedef char akgl_game_save_name_field_fits[
((AKGL_ACTOR_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
(AKGL_SPRITE_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
(AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
(AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD)) ? 1 : -1];
/**
* @brief Write a registry key as a fixed-width, zero-padded field.
*
* The name tables are read back without a length prefix, so every entry has to
* occupy exactly @p width bytes. Writing @p width bytes *from the key itself*
* is what the four iterators used to do, and the key is an SDL allocation sized
* to the name -- 40 bytes for `benchactor0`. That read past the end of it on
* every entry, and put whatever was next to it in the heap into the save file:
* up to half a kilobyte of this process's memory per registered object, in a
* file a player might send to somebody else.
*
* Staging through a zeroed buffer costs one `memset` per entry and makes the
* padding deterministic, which the reader wanted anyway.
*
* @param name The registry key. Required, and NUL-terminated -- it comes from
* SDL's property table, which stores C strings.
* @param width Field width in bytes, including the padding. Required to be at
* most #AKGL_GAME_SAVE_MAX_NAME_FIELD; the compile-time check
* above is what keeps that true for the four call sites.
* @param fp Open output stream. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p name or @p fp is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p width exceeds the staging buffer.
*/
static akerr_ErrorContext *write_name_field(const char *name, size_t width, FILE *fp)
{
char field[AKGL_GAME_SAVE_MAX_NAME_FIELD];
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL name");
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
FAIL_NONZERO_RETURN(
e,
(width > sizeof(field)),
AKERR_OUTOFBOUNDS,
"Name field width %zu exceeds the staging buffer (%zu)",
width,
sizeof(field));
memset(&field, 0x00, sizeof(field));
strncpy((char *)&field, name, (width - 1));
PASS(e, write_exact((char *)&field, 1, width, fp));
SUCCEED_RETURN(e);
}
static akerr_ErrorContext *read_exact(void *ptr, size_t size, size_t nmemb, FILE *fp)
{
size_t transferred;
return aksl_fread(ptr, size, nmemb, fp, &transferred);
}
void akgl_game_lowfps(void)
{
SDL_Log("Low FPS! %d", game.fps);
@@ -133,40 +42,41 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
{
int screenwidth = 0;
int screenheight = 0;
int i = 0;
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);
game.gameStartTime = SDL_GetTicksNS();
game.lastIterTime = game.gameStartTime;
game.lastFPSTime = game.gameStartTime;
game.lowfpsfunc = &akgl_game_lowfps;
game.statelock = SDL_CreateMutex();
FAIL_ZERO_RETURN(e, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
PASS(e, akgl_game_state_lock());
FAIL_ZERO_RETURN(e, strlen((char *)&game.name), AKERR_NULLPOINTER, "Must provide game name");
FAIL_ZERO_RETURN(e, strlen((char *)&game.version), AKERR_NULLPOINTER, "Must provide game version");
FAIL_ZERO_RETURN(e, strlen((char *)&game.uri), AKERR_NULLPOINTER, "Must provide game uri");
PASS(e, akgl_heap_init());
PASS(e, akgl_registry_init_actor());
PASS(e, akgl_registry_init_sprite());
PASS(e, akgl_registry_init_spritesheet());
PASS(e, akgl_registry_init_character());
PASS(e, akgl_registry_init_font());
PASS(e, akgl_registry_init_music());
PASS(e, akgl_registry_init_properties());
PASS(e, akgl_registry_init_actor_state_strings());
ATTEMPT {
strncpy((char *)&game.libversion, AKGL_VERSION, 32);
game.gameStartTime = SDL_GetTicksNS();
game.lastIterTime = game.gameStartTime;
game.lastFPSTime = game.gameStartTime;
game.lowfpsfunc = &akgl_game_lowfps;
game.statelock = SDL_CreateMutex();
FAIL_ZERO_RETURN(e, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
CATCH(e, akgl_game_state_lock());
FAIL_ZERO_BREAK(e, strlen((char *)&game.name), AKERR_NULLPOINTER, "Must provide game name");
FAIL_ZERO_BREAK(e, strlen((char *)&game.version), AKERR_NULLPOINTER, "Must provide game version");
FAIL_ZERO_BREAK(e, strlen((char *)&game.uri), AKERR_NULLPOINTER, "Must provide game uri");
CATCH(e, akgl_heap_init());
CATCH(e, akgl_registry_init_actor());
CATCH(e, akgl_registry_init_sprite());
CATCH(e, akgl_registry_init_spritesheet());
CATCH(e, akgl_registry_init_character());
CATCH(e, akgl_registry_init_font());
CATCH(e, akgl_registry_init_music());
CATCH(e, akgl_registry_init_properties());
CATCH(e, akgl_registry_init_actor_state_strings());
} CLEANUP {
//IGNORE(akgl_game_state_unlock());
} PROCESS(e) {
} FINISH(e, true)
SDL_SetAppMetadata(game.name, game.version, game.uri);
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap));
}
FAIL_ZERO_RETURN(
e,
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO),
@@ -203,12 +113,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
AKGL_ERR_SDL,
"Couldn't initialize front engine: %s",
SDL_GetError());
camera = &_akgl_camera;
renderer = &_akgl_renderer;
physics = &_akgl_physics;
gamemap = &_akgl_gamemap;
PASS(e, akgl_game_state_unlock());
SUCCEED_RETURN(e);
}
@@ -255,26 +159,6 @@ void akgl_game_updateFPS()
* entity name -> pointer map tables
*/
/**
* @brief Write one actor's name and save-time address to the name table.
*
* An `SDL_EnumerateProperties` callback. The name is written at a fixed
* #AKGL_ACTOR_MAX_NAME_LENGTH bytes so the reader can walk the table without a
* length prefix, and the pointer is written raw -- it is not a pointer the
* loader will ever dereference, only a key it maps back to the object that has
* the same name next run.
*
* @param userdata The open output stream, as a `FILE *`. Required.
* @param props #AKGL_REGISTRY_ACTOR, supplied by SDL.
* @param name The actor's registry key. Read for
* #AKGL_ACTOR_MAX_NAME_LENGTH bytes regardless of its actual
* length, so a shorter key is read past its end.
*
* @warning Ends in `FINISH_NORETURN`: an unhandled error here logs a stack trace
* and then calls libakerror's unhandled-error handler, which by default
* **exits the process**. A failed write during a save terminates the
* game rather than returning an error.
*/
void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
FILE *fp = (FILE *)userdata;
@@ -282,27 +166,14 @@ void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, c
PREPARE_ERROR(e);
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
CATCH(e, write_name_field(name, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
actor = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, write_exact(&actor, 1, sizeof(akgl_Actor *), fp));
CATCH(e, aksl_fwrite(&actor, 1, sizeof(akgl_Actor *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
}
/**
* @brief Write one sprite's name and save-time address to the name table.
*
* As akgl_game_save_actorname_iterator, but the name field is
* #AKGL_SPRITE_MAX_NAME_LENGTH wide.
*
* @param userdata The open output stream, as a `FILE *`. Required.
* @param props #AKGL_REGISTRY_SPRITE, supplied by SDL.
* @param name The sprite's registry key, written at fixed width.
*
* @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator.
*/
void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
FILE *fp = (FILE *)userdata;
@@ -311,30 +182,13 @@ void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props,
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
sprite = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, write_name_field(name, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
CATCH(e, write_exact(&sprite, 1, sizeof(akgl_Sprite *), fp));
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
CATCH(e, aksl_fwrite(&sprite, 1, sizeof(akgl_Sprite *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
}
/**
* @brief Write one spritesheet's name and save-time address to the name table.
*
* As akgl_game_save_actorname_iterator, but the name field is
* #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH wide -- the widest of the four, since
* a spritesheet is keyed by its resolved path.
*
* @param userdata The open output stream, as a `FILE *`. Required.
* @param props #AKGL_REGISTRY_SPRITESHEET, supplied by SDL.
* @param name The spritesheet's registry key, written at fixed width.
*
* @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator.
* @note This is the table the loader disagrees with: it reads every table at
* #AKGL_ACTOR_MAX_NAME_LENGTH, so a save containing any spritesheet cannot
* be read back. TODO.md, "Known and still open" item 7.
*/
void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
FILE *fp = (FILE *)userdata;
@@ -343,26 +197,13 @@ void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID pr
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
spritesheet = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, write_name_field(name, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
CATCH(e, write_exact(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp));
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
CATCH(e, aksl_fwrite(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
}
/**
* @brief Write one character's name and save-time address to the name table.
*
* As akgl_game_save_actorname_iterator, but the name field is
* #AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH wide.
*
* @param userdata The open output stream, as a `FILE *`. Required.
* @param props #AKGL_REGISTRY_CHARACTER, supplied by SDL.
* @param name The character's registry key, written at fixed width.
*
* @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator.
*/
void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
FILE *fp = (FILE *)userdata;
@@ -371,46 +212,18 @@ void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID prop
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
character = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, write_name_field(name, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
CATCH(e, write_exact(&character, 1, sizeof(akgl_Character *), fp));
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
CATCH(e, aksl_fwrite(&character, 1, sizeof(akgl_Character *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
}
/**
* @brief Write all four name-to-address tables, each with its terminator.
*
* Actors, sprites, spritesheets, characters, in that order -- which is the order
* akgl_game_load reads them back in. Each table is the registry enumerated one
* entry at a time, followed by a zeroed name field and a zeroed pointer that the
* reader stops on.
*
* @param fp The open save-game stream. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fp is `NULL`.
* @throws AKERR_IO On a stream error or a short write while emitting a
* terminator.
*
* @note Only the terminators are written through the error-reporting path. The
* entries themselves go through `SDL_EnumerateProperties`, whose callbacks
* cannot report failure upward and instead terminate the process -- so a
* write error mid-table never reaches this function's return value.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
{
PREPARE_ERROR(e);
// Each name table ends with a zeroed name field and a zeroed pointer, which
// is what akgl_game_load_objectnamemap() looks for to stop reading. The
// terminator has to come from a buffer at least as long as the longest name
// field: writing N bytes from the address of a single char would emit N-1
// bytes of whatever happened to follow it on the stack, which both leaks
// stack contents into the save file and produces a sentinel the loader
// cannot recognize.
char nullbuf[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
memset((void *)&nullbuf, 0x00, sizeof(nullbuf));
char nullval = 0x00;
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
// write the actor name pointer table
@@ -418,29 +231,29 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
AKGL_REGISTRY_ACTOR,
&akgl_game_save_actorname_iterator,
(void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Actor *), fp));
CATCH(e, aksl_fwrite((void *)&nullval, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_Actor *), fp));
// write the sprite name pointer table
SDL_EnumerateProperties(
AKGL_REGISTRY_SPRITE,
&akgl_game_save_spritename_iterator,
(void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Sprite *), fp));
CATCH(e, aksl_fwrite((void *)&nullval, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_Sprite *), fp));
// write the spritesheet name pointer table
SDL_EnumerateProperties(
AKGL_REGISTRY_SPRITESHEET,
&akgl_game_save_spritesheetname_iterator,
(void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_SpriteSheet *), fp));
CATCH(e, aksl_fwrite((void *)&nullval, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_SpriteSheet *), fp));
// write the character name pointer table
SDL_EnumerateProperties(
AKGL_REGISTRY_CHARACTER,
&akgl_game_save_charactername_iterator,
(void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Character *), fp));
CATCH(e, aksl_fwrite((void *)&nullval, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_Character *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
@@ -455,120 +268,49 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
ATTEMPT {
FAIL_ZERO_BREAK(e, fpath, AKERR_NULLPOINTER, "NULL file path");
CATCH(e, aksl_fopen(fpath, "wb", &fp));
CATCH(e, write_exact(&game, 1, sizeof(akgl_Game), fp));
CATCH(e, aksl_fwrite(&game, 1, sizeof(akgl_Game), fp));
CATCH(e, akgl_game_save_actors(fp));
} PROCESS(e) {
} CLEANUP {
// CLEANUP must precede PROCESS: with the two transposed, the fclose
// lands inside the PROCESS switch and only runs when an error context
// exists and reports success, so an ordinary save never flushed or
// closed its stream.
if ( fp != NULL )
fclose(fp);
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e); // SUCCEED_NORETURN if in main().
}
/**
* @brief Read one name table back, building an old-address to current-object map.
*
* This is the mechanism that makes a save file portable across runs. Objects are
* serialized carrying the addresses they had at save time; those addresses mean
* nothing now, but the table says which *name* lived at each one, and the name
* still resolves through the registry. So each entry becomes
* `"0x7f..." -> current object`, and a deserialized pointer can be translated by
* looking up its printed form.
*
* The keys are the old pointers rendered with `%p`, because SDL property sets
* take string keys only.
*
* Reading stops at the terminator -- a zeroed name and a zeroed pointer.
*
* @param fp The open save-game stream, positioned at the start of a
* table. Required.
* @param map The property set to fill in. Required, and created by the
* caller.
* @param namelength Width of the table's name field, which must match what the
* corresponding save iterator wrote. It also sizes a
* variable-length array on the stack.
* @param ptrlength Width of the table's pointer field, i.e. `sizeof` the
* pointer type that table holds.
* @param registry The registry to resolve names against -- #AKGL_REGISTRY_ACTOR
* for the actor table, and so on.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_EOF If the stream ends before a terminator is found, which is
* what a truncated save file looks like.
* @throws AKERR_IO On a stream error.
* @throws AKERR_NULLPOINTER If @p fp is `NULL`, by way of the read wrapper.
*
* @note A name in the table that is no longer in @p registry maps to `NULL`
* rather than being reported: the entry is written with whatever
* `SDL_GetPointerProperty` returned.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry)
{
void *ptr = NULL;
char ptrstring[32];
char objname[namelength];
int retval = 0;
bool done = false;
PREPARE_ERROR(e);
// The ATTEMPT block sits inside the loop on purpose. CATCH reports a
// failure by breaking, and a break binds to the innermost enclosing switch
// or loop, so a CATCH written directly inside `while` would leave the loop
// and then fall through to SUCCEED_RETURN -- reporting a truncated or
// corrupt name table as a successful load.
while ( done == false ) {
ATTEMPT {
CATCH(e, read_exact((void *)&objname, 1, namelength, fp));
CATCH(e, read_exact((void *)&ptr, 1, ptrlength, fp));
// End of the map
if ( ptr == 0x00 && objname[0] == 0x00 ) {
done = true;
break;
}
// 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
// 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
// from the save game state."
// 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.
CATCH(e, aksl_memset((void *)&ptrstring, 0x00, 32));
snprintf((char *)&ptrstring, 32, "%p", ptr);
SDL_SetPointerProperty(
map,
ptrstring,
SDL_GetPointerProperty(registry, objname, NULL));
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
while ( 1 ) {
CATCH(e, aksl_fread((void *)&objname, 1, namelength, fp));
CATCH(e, aksl_fread((void *)&ptr, 1, ptrlength, fp));
// End of the map
if ( ptr == 0x00 && objname[0] == 0x00 ) {
break;
}
// 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
// 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
// from the save game state."
// 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.
CATCH(e, aksl_memset((void *)&ptrstring, 0x00, 32));
snprintf((char *)&ptrstring, 32, "%p", ptr);
SDL_SetPointerProperty(
map,
ptrstring,
SDL_GetPointerProperty(registry, objname, NULL));
};
SUCCEED_RETURN(e);
}
/**
* @brief Refuse a save file unless its version matches the running one exactly.
*
* Both strings are parsed as semver and compared with `"="` -- exact equality,
* not compatibility. That is deliberate and strict: a save file is a raw memory
* image of `akgl_Game` and the object tables, so any change to a struct layout
* invalidates it, and semver has no way to say "the layout did not move".
*
* @param versiontype What is being compared -- `"library"` or `"game"`. Used
* only to build the message, but required, since a bare
* "incompatible version" error would not say which.
* @param newversion The version read out of the save file. Required.
* @param curversion The version of the running program or library. Required.
* @return `NULL` when the two match, otherwise an error context owned by the
* caller.
* @throws AKERR_NULLPOINTER If any of the three arguments is `NULL`.
* @throws AKERR_VALUE If either string is not valid semver. The message says
* which side it came from.
* @throws AKERR_API If both parse but are not equal. The message quotes both.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion)
{
semver_t current_version = {};
@@ -577,9 +319,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype,
FAIL_ZERO_RETURN(e, versiontype, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, curversion, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, newversion, AKERR_NULLPOINTER, "NULL argument");
ATTEMPT {
// Check save game library version
// Check save game library version
FAIL_NONZERO_BREAK(
e,
semver_parse((const char *)curversion, &current_version),
@@ -618,13 +360,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
SDL_PropertiesID spritesheetmap;
SDL_PropertiesID charactermap;
FILE *fp = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fpath, AKERR_NULLPOINTER, "NULL file path");
ATTEMPT {
CATCH(e, aksl_fopen(fpath, "rb", &fp));
CATCH(e, read_exact((void *)&savegame, 1, sizeof(akgl_Game), fp));
CATCH(e, aksl_fread((void *)&savegame, 1, sizeof(akgl_Game), fp));
CATCH(e, akgl_game_load_versioncmp("library", (char *)&savegame.libversion, (char *)AKGL_VERSION));
CATCH(e, akgl_game_load_versioncmp("game", (char *)&savegame.version, (char *)&game.version));
FAIL_NONZERO_RETURN(
@@ -637,28 +379,28 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
strncmp((char *)&savegame.uri, (char *)&game.uri, 256),
AKERR_API,
"Savegame is not compatible with this game");
memcpy((void *)&game, (void *)&savegame, sizeof(akgl_Game));
// Load actor name map
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
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
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
charactermap = SDL_CreateProperties();
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
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
} CLEANUP {
if ( fp != NULL ) {
fclose(fp);
}
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
@@ -675,7 +417,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags)
if ( opflags == NULL ) {
opflags = &defflags;
}
PASS(e, akgl_game_state_lock());
akgl_game_updateFPS();
@@ -690,15 +432,15 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags)
continue;
}
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE) ) {
PASS(e, akgl_tilemap_scale_actor(gamemap, actor));
PASS(e, akgl_tilemap_scale_actor(&gamemap, actor));
} else {
actor->scale = 1.0;
}
PASS(e, actor->updatefunc(actor));
}
PASS(e, actor->updatefunc(actor));
}
}
PASS(e, physics->simulate(physics, NULL));
PASS(e, renderer->draw_world(renderer, NULL));
PASS(e, physics.simulate(&physics, NULL));
PASS(e, renderer.draw_world(&renderer, NULL));
PASS(e, akgl_game_state_unlock());
SUCCEED_RETURN(e);
}

View File

@@ -1,8 +1,3 @@
/**
* @file heap.c
* @brief Implements the heap subsystem.
*/
#include <stdlib.h>
#include <akerror.h>
@@ -24,6 +19,7 @@ akerr_ErrorContext *akgl_heap_init()
{
PREPARE_ERROR(errctx);
int i = 0;
akerr_name_for_status(AKGL_ERR_SDL, "SDL Error");
PASS(errctx, akgl_heap_init_actor());
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) {
memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite));
@@ -59,7 +55,7 @@ akerr_ErrorContext *akgl_heap_next_actor(akgl_Actor **dest)
*dest = &HEAP_ACTOR[i];
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused actor on the heap");
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused actor on the heap");
}
akerr_ErrorContext *akgl_heap_next_sprite(akgl_Sprite **dest)
@@ -72,7 +68,7 @@ akerr_ErrorContext *akgl_heap_next_sprite(akgl_Sprite **dest)
*dest = &HEAP_SPRITE[i];
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused sprite on the heap");
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused sprite on the heap");
}
akerr_ErrorContext *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest)
@@ -85,7 +81,7 @@ akerr_ErrorContext *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest)
*dest = &HEAP_SPRITESHEET[i];
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused spritesheet on the heap");
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused spritesheet on the heap");
}
akerr_ErrorContext *akgl_heap_next_character(akgl_Character **dest)
@@ -98,7 +94,7 @@ akerr_ErrorContext *akgl_heap_next_character(akgl_Character **dest)
*dest = &HEAP_CHARACTER[i];
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused character on the heap");
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused character on the heap");
}
akerr_ErrorContext *akgl_heap_next_string(akgl_String **dest)
@@ -112,7 +108,7 @@ akerr_ErrorContext *akgl_heap_next_string(akgl_String **dest)
HEAP_STRING[i].refcount += 1;
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused string on the heap");
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused string on the heap");
}
akerr_ErrorContext *akgl_heap_release_actor(akgl_Actor *ptr)
@@ -141,7 +137,7 @@ akerr_ErrorContext *akgl_heap_release_character(akgl_Character *basechar)
akgl_Iterator opflags;
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
AKGL_BITMASK_CLEAR(opflags.flags);
if ( basechar->refcount > 0 ) {
basechar->refcount -= 1;
}
@@ -196,3 +192,4 @@ akerr_ErrorContext *akgl_heap_release_string(akgl_String *ptr)
}
SUCCEED_RETURN(errctx);
}

View File

@@ -1,8 +1,3 @@
/**
* @file json_helpers.c
* @brief Implements the json helpers subsystem.
*/
#include <jansson.h>
#include <string.h>
#include <akerror.h>
@@ -15,156 +10,122 @@
akerr_ErrorContext *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
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, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = value;
SUCCEED_RETURN(errctx);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
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, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = value;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
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, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_boolean_value(value);
SUCCEED_RETURN(errctx);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
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, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_boolean_value(value);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_integer_value(json_t *obj, char *key, int *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
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, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_integer_value(value);
SUCCEED_RETURN(errctx);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
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, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_integer_value(value);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
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, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_number_value(value);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
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, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_number_value(value);
SUCCEED_RETURN(errctx);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
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, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_number_value(value);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest)
{
json_t *value = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, 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");
json_t *value = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, 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");
value = json_object_get(obj, 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);
ATTEMPT {
if ( *dest == NULL ) {
CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL));
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, false);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx);
value = json_object_get(obj, 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);
ATTEMPT {
if ( *dest == NULL ) {
CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL));
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, false);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
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, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = value;
SUCCEED_RETURN(errctx);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
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, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = value;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
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, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
*dest = value;
SUCCEED_RETURN(errctx);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
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, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
*dest = value;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_array_index_integer(json_t *array, int index, int *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
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, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
*dest = json_integer_value(value);
SUCCEED_RETURN(errctx);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
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, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
*dest = json_integer_value(value);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer reference");
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, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
ATTEMPT {
if ( *dest == NULL ) {
CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL));
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, false);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *err, void *defval, void *dest, uint32_t defsize)
{
PREPARE_ERROR(e);
if ( err == NULL ) {
SUCCEED_RETURN(e);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer reference");
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, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
ATTEMPT {
if ( *dest == NULL ) {
CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL));
}
int docopy = 0;
FAIL_ZERO_RETURN(e, err, AKERR_NULLPOINTER, "err");
FAIL_ZERO_RETURN(e, defval, AKERR_NULLPOINTER, "defval");
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
ATTEMPT {
} CLEANUP {
} PROCESS(err) {
} HANDLE_GROUP(err, AKERR_KEY) {
} HANDLE_GROUP(err, AKERR_INDEX) {
memcpy(dest, defval, defsize);
} FINISH(err, true);
SUCCEED_RETURN(e);
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, false);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx);
}

View File

@@ -1,9 +1,3 @@
/**
* @file physics.c
* @brief Implements the physics subsystem.
*/
#include <math.h>
#include <akstdlib.h>
#include <akgl/physics.h>
#include <akgl/actor.h>
@@ -37,7 +31,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *s
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
self->gravity = akgl_physics_null_gravity;
self->collide = akgl_physics_null_collide;
self->move = akgl_physics_null_move;
@@ -47,29 +41,28 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *s
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor");
// Assume the X origin is - (screen left)
actor->gx -= (self->gravity_x * dt);
// Assume Y origin is + (down screen)
actor->gy += (self->gravity_y * dt);
// Assume Z origin is - (behind the camera)
actor->gz -= (self->gravity_z * dt);
if ( self->gravity_x != 0 ) {
// Assume the X origin is - (screen left)
actor->ex -= (self->gravity_x * dt);
}
if ( self->gravity_y != 0 ) {
// Assume Y origin is + (down screen)
actor->ey += (self->gravity_y * dt);
}
if ( self->gravity_z != 0 ) {
// Assume Z origin is - (behind the camera)
actor->ez -= (self->gravity_z * dt);
}
// Counteract velocity with atmospheric drag
actor->gx += actor->vx * self->drag_x * dt;
actor->gy -= actor->vy * self->drag_y * dt;
actor->gz += actor->vz * self->drag_z * dt;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2)
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
@@ -77,27 +70,37 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBacke
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor");
actor->x += actor->vx * dt;
actor->y += actor->vy * dt;
actor->z += actor->vz * dt;
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ) {
actor->x += (-actor->vx * dt) + (actor->gx * dt);
}
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
actor->x += (actor->vx * dt) + (actor->gx * dt);
}
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
actor->y += (-actor->vy * dt) + (actor->gy * dt);
}
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
actor->y += (actor->vy * dt) + (actor->gy * dt);
}
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self)
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_sidescroller(akgl_PhysicsBackend *self)
{
akgl_String *tmp;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
PASS(e, akgl_heap_next_string(&tmp));
self->gravity = akgl_physics_arcade_gravity;
self->collide = akgl_physics_arcade_collide;
self->move = akgl_physics_arcade_move;
self->gravity = akgl_physics_ss_gravity;
self->collide = akgl_physics_ss_collide;
self->move = akgl_physics_ss_move;
self->simulate = akgl_physics_simulate;
ATTEMPT {
@@ -117,7 +120,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend
IGNORE(akgl_heap_release_string(tmp));
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
@@ -128,22 +131,17 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
.flags = 0,
.layerid = 0
};
SDL_Time curtime = 0;
float32_t dt = 0;
SDL_Time curtime = SDL_GetTicksNS();
float32_t dt = (float32_t)(curtime - self->gravity_time) / (float32_t)AKGL_TIME_ONESEC_NS;
akgl_Actor *actor = NULL;
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(e, self->move, AKERR_NULLPOINTER, "self->move");
// Reading the elapsed time requires self, so it cannot be hoisted above
// the null check.
curtime = SDL_GetTicksNS();
dt = (float32_t)(curtime - self->gravity_time) / (float32_t)AKGL_TIME_ONESEC_NS;
if ( opflags == NULL ) {
opflags = &defflags;
}
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
actor = &HEAP_ACTOR[i];
@@ -152,9 +150,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
}
if ( actor->parent != NULL ) {
// Children don't move independently of their parents, they just have an offset
actor->x = actor->parent->x + actor->vx;
actor->y = actor->parent->y + actor->vy;
actor->z = actor->parent->z + actor->vz;
continue;
} else if ( actor->basechar == NULL ) {
continue;
@@ -164,83 +159,35 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
continue;
}
}
// thrust is a function of acceleration on a given axis
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
actor->tx += actor->ax * dt;
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
actor->tx += actor->basechar->ax * dt;
}
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
actor->ty += actor->ay * dt;
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
actor->ty += actor->basechar->ay * dt;
}
// velocity equals thrust unless thrust exceeds max speed
if ( fabsf(actor->tx) > fabsf(actor->sx) ) {
if ( actor->tx < 0 ) {
actor->tx = -actor->sx;
} else {
actor->tx = actor->sx;
}
if ( actor->tx > actor->basechar->sx ) {
actor->vx = actor->basechar->sx;
} else {
actor->vx = actor->tx;
}
if ( fabsf(actor->ty) > fabsf(actor->sy) ) {
if ( actor->ty < 0 ) {
actor->ty = -actor->sy;
} else {
actor->ty = actor->sy;
}
}
if ( fabsf(actor->tz) > fabsf(actor->sz) ) {
if ( actor->tz < 0 ) {
actor->tz = -actor->sz;
} else {
actor->tz = actor->sz;
}
if ( actor->ty > actor->basechar->sy ) {
actor->vy = actor->basechar->sy;
} else {
actor->vy = actor->ty;
}
ATTEMPT {
CATCH(e, actor->movementlogicfunc(actor, dt));
PASS(e, self->gravity(self, actor, dt));
// Counteract velocity with atmospheric drag
if ( self->drag_x != 0 ) {
actor->ex -= actor->ex * self->drag_x * dt;
}
if ( self->drag_y != 0 ) {
actor->ey -= actor->ey * self->drag_y * dt;
}
if ( self->drag_z != 0 ) {
actor->ez -= actor->ez * self->drag_z * dt;
}
actor->vx = actor->ex + actor->tx;
actor->vy = actor->ey + actor->ty;
actor->vz = actor->ez + actor->tz;
PASS(e, self->move(self, actor, dt));
PASS(e, self->move(self, actor, dt));
} CLEANUP {
} PROCESS(e) {
} HANDLE(e, AKGL_ERR_LOGICINTERRUPT) {
// noop
} FINISH(e, true);
}
self->gravity_time = curtime;
self->gravity_time = curtime;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type)
{
uint32_t hashval;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(e, type, AKERR_NULLPOINTER, "type");
if ( strncmp(type->data, "null", 4) == 0) {
PASS(e, akgl_physics_init_null(self));
SUCCEED_RETURN(e);
}
if ( strncmp(type->data, "arcade", 6) == 0) {
PASS(e, akgl_physics_init_arcade(self));
SUCCEED_RETURN(e);
}
FAIL_RETURN(e, AKERR_KEY, "Invalid physics engine %s", type->data);
}

View File

@@ -1,10 +1,4 @@
/**
* @file registry.c
* @brief Implements the registry subsystem.
*/
#include <SDL3/SDL.h>
#include <string.h>
#include <akerror.h>
#include <jansson.h>
@@ -96,7 +90,7 @@ akerr_ErrorContext *akgl_registry_init_actor_state_strings()
akerr_ErrorContext *akgl_registry_init_sprite()
{
PREPARE_ERROR(errctx);
PREPARE_ERROR(errctx);
AKGL_REGISTRY_SPRITE = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_SPRITE, AKERR_NULLPOINTER, "Error initializing sprite registry");
SUCCEED_RETURN(errctx);
@@ -124,19 +118,12 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname)
json_t *props = NULL;
const char *pkey = NULL;
json_t *pvalue = NULL;
json_error_t error;
akgl_String *tmpstr;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "null filename");
// One ATTEMPT around the whole function, rather than one for the parse and
// a bare loop after it. `props` is a borrowed reference into `json` and is
// read by the loop, so the document cannot be released until the loop is
// done -- and with the loop outside the block, every path out of it leaked
// the document. The per-property ATTEMPT stays nested inside the loop,
// which is what AGENTS.md requires: a CATCH written directly in a loop
// breaks the loop rather than leaving the function.
ATTEMPT {
SDL_Log("Loading from %s", fname);
json = json_load_file(fname, 0, &error);
@@ -149,28 +136,21 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname)
error.line,
error.text);
CATCH(errctx, akgl_get_json_object_value(json, "properties", &props));
json_object_foreach(props, pkey, pvalue) {
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpstr));
CATCH(errctx, akgl_get_json_string_value(props, (char *)pkey, &tmpstr));
SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, pkey, tmpstr->data);
SDL_Log("Set property %s = %s", pkey, tmpstr->data);
CATCH(errctx, akgl_heap_release_string(tmpstr));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
}
} CLEANUP {
// Every value was copied into the property store on the way past, so
// nothing outlives the document.
if ( json != NULL ) {
json_decref(json);
json = NULL;
}
} PROCESS(errctx) {
} FINISH(errctx, true);
json_object_foreach(props, pkey, pvalue) {
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpstr));
CATCH(errctx, akgl_get_json_string_value(props, (char *)pkey, &tmpstr));
SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, pkey, tmpstr->data);
SDL_Log("Set property %s = %s", pkey, tmpstr->data);
CATCH(errctx, akgl_heap_release_string(tmpstr));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
}
SDL_Log("Properties loaded");
SUCCEED_RETURN(errctx);
}
@@ -186,9 +166,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *src)
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def)
{
const char *value = NULL;
size_t valuelen = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL char *");
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "NULL akgl_String *");
@@ -196,34 +173,15 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **d
if ( *dest == NULL ) {
CATCH(e, akgl_heap_next_string(dest));
}
// Copy the value's own length, not the destination's capacity. What
// SDL hands back is its strdup of the value -- four bytes for "0.0" --
// so copying a fixed AKGL_MAX_STRING_LENGTH read up to 4 KiB past the
// end of somebody else's allocation on every call. It returned garbage
// past the terminator, and it would have faulted outright on a value
// that happened to sit at the end of a page.
value = SDL_GetStringProperty(AKGL_REGISTRY_PROPERTIES, (const char *)name, (const char *)def);
// An unset property with a NULL default still reports AKERR_NULLPOINTER,
// which is what the header has always promised. It used to arrive from
// inside aksl_memcpy; now it is raised here, before anything is measured.
FAIL_ZERO_BREAK(
e,
value,
AKERR_NULLPOINTER,
"Property %s is not set and no default was given",
name);
valuelen = strlen(value);
FAIL_NONZERO_BREAK(
e,
(valuelen >= AKGL_MAX_STRING_LENGTH),
AKERR_OUTOFBOUNDS,
"Property %s is %zu bytes, which does not fit an akgl_String (%d)",
name,
valuelen,
AKGL_MAX_STRING_LENGTH);
CATCH(e, aksl_memcpy((*dest)->data, (void *)value, (valuelen + 1)));
CATCH(e,
aksl_memcpy(
(*dest)->data,
(void *)SDL_GetStringProperty(AKGL_REGISTRY_PROPERTIES, (const char *)name, (const char *)def),
AKGL_MAX_STRING_LENGTH)
);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}

View File

@@ -1,8 +1,3 @@
/**
* @file renderer.c
* @brief Implements the renderer subsystem.
*/
#include <SDL3/SDL.h>
#include <akgl/renderer.h>
@@ -30,31 +25,19 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self)
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(height));
FAIL_ZERO_RETURN(
e,
SDL_CreateWindowAndRenderer(game.uri, screenwidth, screenheight, 0, &window, &self->sdl_renderer),
AKGL_ERR_SDL,
"Couldn't create window/renderer: %s",
SDL_GetError());
camera.x = 0;
camera.y = 0;
camera.w = screenwidth;
camera.h = screenheight;
camera->x = 0;
camera->y = 0;
camera->w = screenwidth;
camera->h = screenheight;
PASS(e, akgl_render_bind2d(self));
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_bind2d(akgl_RenderBackend *self)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
// Deliberately does not touch self->sdl_renderer: a host that owns its own
// window has already put one there, and this is the only way it gets a
// usable backend without libakgl creating a second window.
self->shutdown = &akgl_render_2d_shutdown;
self->frame_start = &akgl_render_2d_frame_start;
self->frame_end = &akgl_render_2d_frame_end;
@@ -134,8 +117,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend
PASS(e, aksl_memset((void *)opflags, 0x00, sizeof(akgl_Iterator)));
}
for ( int i = 0; i < AKGL_TILEMAP_MAX_LAYERS ; i++ ) {
if ( i < gamemap->numlayers ) {
PASS(e, akgl_tilemap_draw(gamemap, camera, i));
if ( i < gamemap.numlayers ) {
PASS(e, akgl_tilemap_draw((akgl_Tilemap *)&gamemap, &camera, i));
}
for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR ; j++ ) {
actor = &HEAP_ACTOR[j];
@@ -145,9 +128,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend
if ( actor->layer != i ) {
continue;
}
PASS(e, actor->renderfunc(actor));
PASS(e, actor->renderfunc(actor));
}
}
SUCCEED_RETURN(e);
SUCCEED_RETURN(e);
}

View File

@@ -1,8 +1,3 @@
/**
* @file sprite.c
* @brief Implements the sprite subsystem.
*/
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <string.h>
@@ -40,33 +35,6 @@ akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FR
SUCCEED_RETURN(e);
}
/**
* @brief Find or load the spritesheet a sprite definition names.
*
* Resolves the `spritesheet.filename` path relative to the sprite definition's
* own directory, then looks that resolved path up in #AKGL_REGISTRY_SPRITESHEET.
* A hit is reused as is -- which is what keeps ten sprites cut from one image
* down to one texture -- and a miss claims a sheet from the pool and loads it,
* reading `frame_width` and `frame_height` only in that case.
*
* @param json The parsed sprite document, whose `spritesheet` object
* this reads. Required in practice; not checked.
* @param sheet Receives the sheet, whether found or freshly loaded.
* Required; not checked. On the reuse path the sheet's
* reference count is *not* incremented, so the sprite
* borrows it.
* @param relative_path Directory to resolve the image path against -- the
* directory holding the sprite JSON. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_KEY If the document has no `spritesheet` object, or that object
* has no `filename` -- or, on the load path, no `frame_width` or
* `frame_height`.
* @throws AKERR_TYPE If one of those has the wrong JSON type.
* @throws AKERR_OUTOFBOUNDS If the joined path is too long for a pooled string.
* @throws ENOENT If the image path does not exist.
* @throws AKGL_ERR_SDL If the image cannot be decoded.
* @throws AKGL_ERR_HEAP If the spritesheet or string pool is exhausted.
*/
static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
{
PREPARE_ERROR(errctx);
@@ -90,7 +58,7 @@ static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_
if ( *sheet == NULL ) {
CATCH(errctx, akgl_heap_next_spritesheet(sheet));
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_width", &ss_frame_width));
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_height));
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_width));
CATCH(errctx,
akgl_spritesheet_initialize(
(akgl_SpriteSheet *)*sheet,
@@ -104,10 +72,10 @@ static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_
IGNORE(akgl_heap_release_string(tmpstr));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_sprite_load_json(char *filename)
{
PREPARE_ERROR(errctx);
@@ -117,20 +85,12 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
akgl_Sprite *obj = NULL;
akgl_SpriteSheet *sheet = NULL;
akgl_String *spritename = NULL;
akgl_String *filename_copy = NULL;
//string *tmpstr = NULL;
int i = 0;
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
ATTEMPT {
CATCH(errctx, akgl_heap_next_sprite(&obj));
CATCH(errctx, akgl_heap_next_string(&filename_copy));
FAIL_NONZERO_BREAK(
errctx,
strlen(filename) >= AKGL_MAX_STRING_LENGTH,
AKERR_OUTOFBOUNDS,
"Sprite filename exceeds temporary string capacity"
);
SDL_strlcpy(filename_copy->data, filename, AKGL_MAX_STRING_LENGTH);
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
CATCH(errctx, akgl_heap_next_string(&spritename));
@@ -144,8 +104,8 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
AKERR_NULLPOINTER,
"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)));
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
CATCH(errctx,
akgl_sprite_initialize(
@@ -153,33 +113,25 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
spritename->data,
(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, "height", &obj->height));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &obj->speed));
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, "loopReverse", &obj->loopReverse));
CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames));
obj->frames = json_array_size((json_t *)frames);
for ( i = 0 ; i < obj->frames; i++ ) {
CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, (uint32_t *)&obj->frameids[i]));
}
} CLEANUP {
// The sprite copies every field it wants out of the document and the
// sheet is found by resolved path, so nothing here points into the
// parsed tree once this block runs. Released on both paths.
if ( json != NULL ) {
json_decref(json);
json = NULL;
}
if ( errctx != NULL && errctx->status != 0 ) {
IGNORE(akgl_heap_release_sprite(obj));
IGNORE(akgl_heap_release_spritesheet(sheet));
}
IGNORE(akgl_heap_release_string(spritename));
IGNORE(akgl_heap_release_string(filename_copy));
//IGNORE(akgl_heap_release_string(tmpstr));
} PROCESS(errctx) {
} FINISH(errctx, true);
@@ -192,7 +144,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, name, AKERR_NULLPOINTER, "Empty sprite name");
FAIL_ZERO_RETURN(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet reference");
memset(spr, 0x00, sizeof(akgl_Sprite));
memcpy(spr->name, name, AKGL_SPRITE_MAX_NAME_LENGTH);
spr->sheet = sheet;
@@ -217,14 +169,14 @@ akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int spr
memset(sheet, 0x00, sizeof(akgl_SpriteSheet));
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH);
//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,
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet),

View File

@@ -1,23 +1,18 @@
/**
* @file staticstring.c
* @brief Implements the staticstring subsystem.
*/
#include <akerror.h>
#include <akgl/staticstring.h>
#include <errno.h>
akerr_ErrorContext *akgl_string_initialize(akgl_String *obj, char *init)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Attempted to initialize NULL string reference");
if ( init != NULL ) {
strncpy((char *)&obj->data, init, AKGL_MAX_STRING_LENGTH);
} else {
memset(&obj->data, 0x00, sizeof(akgl_String));
}
obj->refcount = 1;
SUCCEED_RETURN(errctx);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Attempted to initialize NULL string reference");
if ( init != NULL ) {
strncpy((char *)&obj->data, init, AKGL_MAX_STRING_LENGTH);
} else {
memset(&obj->data, 0x00, sizeof(akgl_String));
}
obj->refcount = 1;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_string_copy(akgl_String *src, akgl_String *dst, int count)

View File

@@ -1,8 +1,3 @@
/**
* @file text.c
* @brief Implements the text subsystem.
*/
#include <akerror.h>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
@@ -13,19 +8,12 @@
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size)
{
TTF_Font *font = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
FAIL_ZERO_RETURN(errctx, filepath, AKERR_NULLPOINTER, "Null filepath");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null filepath");
font = TTF_OpenFont(filepath, size);
FAIL_ZERO_RETURN(errctx, font, AKGL_ERR_SDL, "%s", SDL_GetError());
// Loading over an existing name used to abandon the font it displaced --
// there was nothing in the library that could close one. The old font goes
// back first now, and only once the new one has opened, so a failed load
// leaves the caller with the font they already had.
if ( SDL_GetPointerProperty(AKGL_REGISTRY_FONT, name, NULL) != NULL ) {
PASS(errctx, akgl_text_unloadfont(name));
}
FAIL_ZERO_RETURN(
errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_FONT, name, (void *)font),
@@ -37,39 +25,15 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name)
{
TTF_Font *font = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
font = (TTF_Font *)SDL_GetPointerProperty(AKGL_REGISTRY_FONT, name, NULL);
FAIL_ZERO_RETURN(errctx, font, AKERR_KEY, "No font named %s in the registry", name);
// Cleared before the close, so a font is never reachable through the
// registry after it has been handed back to SDL_ttf.
SDL_ClearProperty(AKGL_REGISTRY_FONT, name);
TTF_CloseFont(font);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y)
{
SDL_Surface *textsurf = NULL;
SDL_Texture *texture = NULL;
SDL_FRect dest;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
// Checked before anything is rasterized, and checked at all because a
// backend that has an SDL_Renderer but was never run through
// akgl_render_bind2d has a NULL draw_texture -- which is exactly the state
// akgl_render_init2d used to be the only escape from.
FAIL_ZERO_RETURN(errctx, renderer, AKERR_NULLPOINTER, "No renderer backend");
FAIL_ZERO_RETURN(errctx, renderer->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, renderer->draw_texture, AKERR_NULLPOINTER, "Renderer backend has no draw_texture");
if ( wraplength > 0 ) {
textsurf = TTF_RenderText_Blended_Wrapped(
font,
@@ -85,56 +49,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *
color);
}
FAIL_ZERO_RETURN(errctx, textsurf, AKERR_NULLPOINTER, "%s", SDL_GetError());
texture = SDL_CreateTextureFromSurface(renderer->sdl_renderer, textsurf);
texture = SDL_CreateTextureFromSurface(renderer.sdl_renderer, textsurf);
FAIL_ZERO_RETURN(errctx, texture, AKERR_NULLPOINTER, "%s", SDL_GetError());
dest.x = x;
dest.y = y;
SDL_GetTextureSize(texture, &dest.w, &dest.h);
PASS(errctx, renderer->draw_texture(renderer, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE));
PASS(errctx, renderer.draw_texture(&renderer, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE));
SDL_DestroyTexture(texture);
SDL_DestroySurface(textsurf);
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

@@ -1,8 +1,3 @@
/**
* @file tilemap.c
* @brief Implements the tilemap subsystem.
*/
#include <string.h>
#include <libgen.h>
@@ -60,7 +55,7 @@ akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char
}
} PROCESS(errctx) {
} FINISH(errctx, true);
FAIL_RETURN(errctx, AKERR_KEY, "Property not found in properties map");
}
@@ -68,12 +63,16 @@ akerr_ErrorContext *akgl_get_json_properties_string(json_t *obj, char *key, akgl
{
PREPARE_ERROR(errctx);
json_t *property;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "string", &property));
PASS(errctx, akgl_heap_next_string(dest));
PASS(errctx, akgl_string_initialize(*dest, NULL));
PASS(errctx, akgl_get_json_string_value(property, "value", dest));
ATTEMPT {
CATCH(errctx, akgl_get_json_tilemap_property(obj, key, "string", &property));
CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL));
CATCH(errctx, akgl_get_json_string_value(property, "value", dest));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
@@ -81,90 +80,41 @@ akerr_ErrorContext *akgl_get_json_properties_integer(json_t *obj, char *key, int
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "int", &property));
PASS(errctx, akgl_get_json_integer_value(property, "value", dest));
ATTEMPT {
CATCH(errctx, akgl_get_json_tilemap_property(obj, key, "int", &property));
CATCH(errctx, akgl_get_json_integer_value(property, "value", dest));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Read a Tiled custom property declared as `number`.
*
* Tiled writes `float` for a floating-point property, so this matches nothing
* Tiled produces today -- akgl_get_json_properties_float is the one the loader
* uses. Kept because a hand-written or older map may still say `number`.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value as a `float`. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `number`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, float *dest)
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "number", &property));
PASS(errctx, akgl_get_json_number_value(property, "value", dest));
ATTEMPT {
CATCH(errctx, akgl_get_json_tilemap_property(obj, key, "number", &property));
CATCH(errctx, akgl_get_json_number_value(property, "value", dest));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Read a Tiled custom property declared as `float`.
*
* The spelling Tiled actually writes for a floating-point property. This is how
* the `scale` on a perspective marker is read.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float *dest)
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
PASS(errctx, akgl_get_json_number_value(property, "value", dest));
SUCCEED_RETURN(errctx);
}
/**
* @brief Read a Tiled custom property declared as `float`, at full precision.
*
* Same declared type as akgl_get_json_properties_float, but writing a `double`.
* The map's physics constants are `double`, which is the reason both exist.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* akgl_tilemap_load_physics passes this status to
* akgl_get_json_with_default, which turns it into a default of 0.0.
* @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, double *dest)
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
PASS(errctx, akgl_get_json_double_value(property, "value", dest));
ATTEMPT {
CATCH(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
CATCH(errctx, akgl_get_json_number_value(property, "value", dest));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
@@ -173,7 +123,7 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
PREPARE_ERROR(e);
akgl_String *tmpstr = 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, "firstgid", &dest->tilesets[tsidx].firstgid));
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "imageheight", &dest->tilesets[tsidx].imageheight));
@@ -185,14 +135,15 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "tilewidth", &dest->tilesets[tsidx].tilewidth));
PASS(e, akgl_get_json_string_value((json_t *)tileset, "name", &tmpstr));
PASS(e, akgl_heap_next_string(&tmpstr));
PASS(e, akgl_heap_next_string(&tmppath));
ATTEMPT {
strncpy((char *)&dest->tilesets[tsidx].name,
(char *)&tmpstr->data,
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));
strncpy((char *)&dest->tilesets[tsidx].imagefilename, tmppath->data, AKGL_MAX_STRING_LENGTH);
} CLEANUP {
@@ -200,10 +151,10 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
IGNORE(akgl_heap_release_string(tmppath));
} PROCESS(e) {
} 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());
SUCCEED_RETURN(e);
}
@@ -226,7 +177,7 @@ akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int
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
*
*
* 01234567
* 89ABCDEF
*
@@ -248,7 +199,7 @@ akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int
x_offset = (j * (dest->tilesets[tilesetidx].tilewidth + 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][1] = y_offset;
/* SDL_Log("Tileset %s index (%d, %d) is offset (%d, %d)",
@@ -260,8 +211,8 @@ akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int
// SDL_Log("Processed %d total tiles for tileset", j);
}
SUCCEED_RETURN(errctx);
}
}
akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
@@ -271,7 +222,7 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
json_t *tilesets = NULL;
json_t *jstileset = NULL;
int i;
dest->numtilesets = 0;
ATTEMPT {
CATCH(errctx, akgl_get_json_array_value(root, "tilesets", &tilesets))
@@ -284,51 +235,16 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Turn one Tiled object into a live actor, creating it if it is not already registered.
*
* The object's name is the actor's registry key, which is what lets the same
* actor be placed by more than one object: the first placement creates it and
* binds the character named in the object's `character` property, and every
* later one just takes another reference. Either way the object's position,
* visibility, and layer are copied onto the actor and the object keeps a
* pointer to it.
*
* Not declared in tilemap.h -- reachable only through
* akgl_tilemap_load_layer_objects.
*
* @param curobj The tilemap object, with its `name`, `x`, `y`, and
* `visible` already read from JSON. Required, unchecked,
* dereferenced at once.
* @param layerdatavalue The object's JSON, for the custom properties --
* `character` and `state` -- that are not part of Tiled's
* own object fields. Required.
* @param layerid The layer this object sits on, copied onto the actor.
* @param dirname Directory to resolve paths against. Accepted for
* signature consistency; nothing here uses it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_KEY If the object's name is empty -- an actor has to be
* addressable -- if the `character` or `state` property is absent, or if
* the named character is not in #AKGL_REGISTRY_CHARACTER.
* @throws AKERR_TYPE If `character` is not declared `string` or `state` not
* declared `int`.
* @throws AKERR_NULLPOINTER If akgl_actor_initialize refuses the name.
* @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
*
* @note An existing actor's `character` and position are overwritten by each
* later placement, so two objects naming one actor leave it wherever the
* last one put it rather than producing two.
*/
akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
akgl_String *tmpstr = NULL;
akgl_Actor *actorobj = NULL;
curobj->type = AKGL_TILEMAP_OBJECT_TYPE_ACTOR;
if ( strlen((char *)&curobj->name) == 0 ) {
FAIL_RETURN(errctx, AKERR_KEY, "Actor in tile object layer cannot have empty name");
@@ -357,7 +273,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *cur
}
} PROCESS(errctx) {
} FINISH(errctx, true);
actorobj->layer = layerid;
actorobj->x = curobj->x;
actorobj->y = curobj->y;
@@ -381,37 +297,42 @@ akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination tilemap reference");
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");
PASS(errctx, akgl_get_json_array_value(root, "objects", &layerdata));
len = json_array_size((json_t *)layerdata);
curlayer = &dest->layers[layerid];
for ( j = 0; j < len; j++ ) {
PASS(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
curobj = &curlayer->objects[j];
PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
strncpy((char *)curobj->name, tmpstr->data, AKGL_ACTOR_MAX_NAME_LENGTH);
PASS(errctx, akgl_heap_release_string(tmpstr));
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_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
ATTEMPT {
CATCH(errctx, akgl_get_json_array_value(root, "objects", &layerdata));
len = json_array_size((json_t *)layerdata);
curlayer = &dest->layers[layerid];
for ( j = 0; j < len; j++ ) {
CATCH(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
curobj = &curlayer->objects[j];
CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
strncpy((char *)curobj->name, tmpstr->data, AKGL_ACTOR_MAX_NAME_LENGTH);
CATCH(errctx, akgl_heap_release_string(tmpstr));
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
CATCH(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
if ( strcmp(tmpstr->data, "actor") == 0 ) {
PASS(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
} else if ( strcmp(tmpstr->data, "perspective") == 0 ) {
curobj->visible = false;
if ( strcmp((char *)curobj->name, "p_foreground") == 0 ) {
dest->p_foreground_y = curobj->y;
PASS(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_foreground_h));
PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_foreground_scale));
} else if ( strcmp((char *)curobj->name, "p_vanishing") == 0 ) {
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_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
if ( strcmp(tmpstr->data, "actor") == 0 ) {
CATCH(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
} else if ( strcmp(tmpstr->data, "perspective") == 0 ) {
curobj->visible = false;
if ( strcmp((char *)curobj->name, "p_foreground") == 0 ) {
dest->p_foreground_y = curobj->y;
CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_foreground_h));
CATCH(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_foreground_scale));
} else if ( strcmp((char *)curobj->name, "p_vanishing") == 0 ) {
dest->p_vanishing_y = curobj->y;
CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
CATCH(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
}
}
layerdatavalue = NULL;
}
layerdatavalue = NULL;
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
@@ -440,38 +361,10 @@ akerr_ErrorContext *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *roo
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Load one image layer: upload its image as a single texture.
*
* An image layer is one picture rather than a grid, which is what a parallax
* backdrop wants. The layer's `width` and `height` are taken from the loaded
* texture rather than from the JSON, so they are always the true pixel size.
*
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load_layers.
*
* @param dest The map to load into. Required.
* @param root The layer's JSON object. Required.
* @param layerid Which layer slot to fill. Not bounds-checked.
* @param dirname Directory to resolve the layer's `image` path against.
* Required. Joined with a `/` rather than run through
* akgl_path_relative, so unlike a tileset image this path is not
* canonicalized and an absolute one will not work.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
* @throws AKERR_KEY If the layer has no `image`.
* @throws AKERR_TYPE If `image` is not a string.
* @throws AKGL_ERR_SDL If the image cannot be loaded. The message carries
* `SDL_GetError()`.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @note The joined path is truncated at
* #AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE without complaint, so an
* over-long path fails as a missing file rather than as a length error.
*/
akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
@@ -488,15 +381,15 @@ akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *ro
CATCH(errctx, akgl_get_json_string_value(root, "image", &tmpstr));
DISABLE_GCC_WARNING_FORMAT_TRUNCATION
snprintf((char *)&fpath->data,
AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE,
"%s/%s",
dirname->data,
tmpstr->data
);
snprintf((char *)&fpath->data,
AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE,
"%s/%s",
dirname->data,
tmpstr->data
);
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());
dest->layers[layerid].width = dest->layers[layerid].texture->w;
dest->layers[layerid].height = dest->layers[layerid].texture->h;
@@ -505,7 +398,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *ro
IGNORE(akgl_heap_release_string(fpath));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
@@ -521,7 +414,7 @@ akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, a
int i;
int layerid = 0;
int tmpint = 0;
ATTEMPT {
CATCH(errctx, akgl_get_json_array_value(root, "layers", &layers));
dest->numlayers = json_array_size((json_t *)layers);
@@ -558,115 +451,6 @@ akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, a
SUCCEED_RETURN(errctx);
}
/**
* @brief Give the map its own physics backend, if it asked for one.
*
* A map with a `physics.model` custom property gets its own backend, configured
* from `physics.gravity.x`/`.y`/`.z` and `physics.drag.x`/`.y`/`.z`, and
* `use_own_physics` is set so a caller knows to simulate through it. Each
* constant defaults to 0.0 when absent, so a map can name a model and override
* only what it cares about.
*
* Absence is the normal case at every level: a map with no properties at all, or
* with properties but no `physics.model`, uses the game's global backend and
* this returns success. That is why the AKERR_KEY handlers here are not error
* paths -- they are the "map did not ask" path.
*
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load.
*
* @param dest The map to configure. Required in practice; not checked here,
* since akgl_tilemap_load has already done so.
* @param root The map's root JSON object. Required, likewise.
* @return `NULL` on success -- including when the map declared no physics at
* all -- otherwise an error context owned by the caller.
* @throws AKERR_KEY If `physics.model` names a backend that does not exist.
* @throws AKERR_TYPE If `physics.model` is not declared `string`, or one of the
* six constants is not declared `float`.
* @throws AKERR_NULLPOINTER If akgl_physics_factory refuses its arguments.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
{
PREPARE_ERROR(e);
json_t *props = NULL;
akgl_String *tmpval = NULL;
double defzero = 0.0;
ATTEMPT {
CATCH(e, akgl_heap_next_string(&tmpval));
CATCH(e, akgl_get_json_array_value((json_t *)root, "properties", &props));
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpval));
} PROCESS(e) {
} HANDLE(e, AKERR_KEY) {
// Map has no properties, do nothing
SDL_Log("Map has no properties");
SUCCEED_RETURN(e);
} FINISH(e, true);
ATTEMPT {
CATCH(e, akgl_heap_next_string(&tmpval));
CATCH(e, akgl_get_json_properties_string(
root,
"physics.model",
&tmpval
)
);
PASS(e, akgl_physics_factory(&dest->physics, tmpval));
dest->use_own_physics = true;
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.gravity.x", &dest->physics.gravity_x
),
(void *)&defzero,
(void *)&dest->physics.gravity_x,
sizeof(double)));
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.gravity.y", &dest->physics.gravity_y
),
(void *)&defzero,
(void *)&dest->physics.gravity_y,
sizeof(double)));
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.gravity.z", &dest->physics.gravity_z
),
(void *)&defzero,
(void *)&dest->physics.gravity_z,
sizeof(double)));
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.drag.x", &dest->physics.drag_x
),
(void *)&defzero,
(void *)&dest->physics.drag_x,
sizeof(double)));
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.drag.y", &dest->physics.drag_y
),
(void *)&defzero,
(void *)&dest->physics.drag_y,
sizeof(double)));
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.drag.z", &dest->physics.drag_z
),
(void *)&defzero,
(void *)&dest->physics.drag_z,
sizeof(double)));
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpval));
} PROCESS(e) {
} HANDLE(e, AKERR_KEY) {
SDL_Log("Map uses game physics");
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
{
PREPARE_ERROR(errctx);
@@ -674,7 +458,7 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
//akgl_String *tmpstr = NULL;
json_error_t error;
akgl_String *dirnamestr = NULL;
FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "load_tilemap received null filename");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "load_tilemap received null tilemap");
@@ -687,9 +471,9 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
//SDL_snprintf(tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
CATCH(errctx, aksl_realpath(fname, (char *)&dirnamestr->data, sizeof(dirnamestr->data)));
CATCH(errctx, aksl_realpath(fname, (char *)&dirnamestr->data));
dirname((char *)&dirnamestr->data);
json = json_load_file(fname, 0, &error);
FAIL_ZERO_BREAK(
errctx,
@@ -700,7 +484,6 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
error.line,
error.text
);
CATCH(errctx, akgl_tilemap_load_physics(dest, json));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "tileheight", &dest->tileheight));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "tilewidth", &dest->tilewidth));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &dest->height));
@@ -729,14 +512,6 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
}
} CLEANUP {
//IGNORE(akgl_heap_release_string(tmpstr));
// The map is built entirely out of copies -- layer data, tileset
// geometry, object names -- so the document is dead the moment the
// loaders above return, whether they succeeded or not. It is also the
// largest of the four: a map's JSON is the size of its layer data.
if ( json != NULL ) {
json_decref(json);
json = NULL;
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
@@ -795,13 +570,13 @@ akerr_ErrorContext *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, in
dest.x = 0;
dest.y = 0;
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.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);
}
dest.x = 0;
dest.y = 0;
dest.w = map->tilewidth;
@@ -853,7 +628,7 @@ akerr_ErrorContext *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, in
dest.y,
dest.w,
dest.h);*/
PASS(errctx, renderer->draw_texture(renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
PASS(errctx, renderer.draw_texture(&renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
}
}
dest.x += map->tilewidth;
@@ -877,7 +652,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_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++) {
// Render this tile to the correct screen position
// FIXME: These conditionals are probably not very efficient. Need a better way of getting
@@ -908,7 +683,7 @@ akerr_ErrorContext *akgl_tilemap_draw_tileset(akgl_Tilemap *map, int tilesetidx)
dest.y,
dest.w,
dest.h);*/
PASS(errctx, renderer->draw_texture(renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
PASS(errctx, renderer.draw_texture(&renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
}
SUCCEED_RETURN(errctx);
}

View File

@@ -1,8 +1,3 @@
/**
* @file util.c
* @brief Implements the util subsystem.
*/
#include <limits.h>
#include <stdlib.h>
#include <errno.h>
@@ -20,36 +15,6 @@
#include <akstdlib.h>
/**
* @brief Resolve @p path against @p root and write the absolute result to @p dst.
*
* The second half of akgl_path_relative: joins the two with a `/` and resolves
* the join, so symlinks and `..` are folded out and the result is absolute. It
* is the fallback, reached only once resolving @p path on its own has failed.
*
* Not declared in util.h -- it is reachable only through akgl_path_relative.
*
* @param root Directory to resolve against, normally `dirname` of the file that
* named @p path. Required. A trailing `/` is harmless; the join adds
* one unconditionally and `realpath` collapses the double.
* @param path Relative path to resolve. Required. An absolute @p path still gets
* @p root pasted in front of it, which will not exist -- so callers
* must not send absolute paths down this branch.
* @param dst Receives the resolved absolute path. Required, and must already be
* a claimed pool string.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dst is `NULL`.
* @throws AKERR_OUTOFBOUNDS If `root + path` is at least #AKGL_MAX_STRING_LENGTH
* bytes. The message reports both the combined length and the limit.
* @throws ENOENT If the joined path does not exist. Any other `errno`
* `realpath(3)` raises -- EACCES, ELOOP, ENOTDIR -- propagates likewise.
* @throws AKGL_ERR_HEAP If the string pool cannot supply the two scratch buffers.
*
* @note The AKERR_OUTOFBOUNDS check uses `FAIL_RETURN` from inside the `ATTEMPT`
* block, which returns past `CLEANUP` -- so on that one path the two
* scratch strings are never released. This is exactly the hazard AGENTS.md
* warns about; it wants a `FAIL_BREAK`.
*/
akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String *dst)
{
PREPARE_ERROR(e);
@@ -59,14 +24,14 @@ akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String
int rootlen;
int pathlen;
int count;
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, path, 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(&pathbuf));
ATTEMPT {
// Is it relative to the root?
rootlen = strlen(root);
@@ -75,17 +40,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);
}
DISABLE_GCC_WARNING_FORMAT_TRUNCATION
CATCH(e, aksl_snprintf(
&count,
(char *)&pathbuf->data,
sizeof(pathbuf->data),
"%s/%s",
root,
path
));
CATCH(e, aksl_sprintf(&count, (char *)&pathbuf->data, "%s/%s", root, path));
RESTORE_GCC_WARNINGS
CATCH(e, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data, sizeof(strbuf->data)));
CATCH(e, akgl_string_copy(strbuf, dst, 0));
CATCH(e, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data));
CATCH(e, akgl_string_copy(strbuf, dst, 0));
} CLEANUP {
IGNORE(akgl_heap_release_string(strbuf));
IGNORE(akgl_heap_release_string(pathbuf));
@@ -99,63 +57,29 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
PREPARE_ERROR(e);
akgl_String *strbuf;
char *result;
bool relative_to_root = false;
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
PASS(e, akgl_heap_next_string(&strbuf));
ATTEMPT {
// Is path relative to our current working directory?
CATCH(e, aksl_realpath(path, (char *)&strbuf->data, sizeof(strbuf->data)));
CATCH(e, aksl_realpath(path, (char *)&strbuf->data));
// 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 {
IGNORE(akgl_heap_release_string(strbuf));
} PROCESS(e) {
} HANDLE(e, ENOENT) {
// Path is not relative to our current working directory. Resolve it
// against root instead -- but after FINISH, not from in here. Returning
// from inside a HANDLE block skips the RELEASE_ERROR that FINISH ends
// with, so the handled context is never given back to
// AKERR_ARRAY_ERROR. That leaks one context per call, and the 129th
// call takes the whole process down with "Unable to pull an error
// context from the array!". Every map load resolves several paths this
// way.
relative_to_root = true;
// Path is not relative to our current working directory
// Noop - execution proceeds after the break
return akgl_path_relative_root(root, path, dst);
} FINISH(e, true);
if ( relative_to_root == true ) {
PASS(e, akgl_path_relative_root(root, path, dst));
}
SUCCEED_RETURN(e);
}
/**
* @brief Intended to resolve @p path against the directory holding @p from. Unfinished.
*
* The shape is there -- resolve @p from, take its `dirname` -- but the body
* stops before it does anything with @p path, and `*dst` is never written. Not
* declared in util.h, and nothing in the tree calls it.
*
* @param path The path to resolve. Required, and currently ignored past the
* `NULL` check.
* @param from A file whose directory @p path should be taken as relative to.
* Required. Must exist -- it is resolved with `realpath(3)`.
* @param dst Intended to receive the resolved path. Never written. Note the
* double indirection, which disagrees with the rest of the
* `akgl_path_relative*` family; see TODO.md item 40.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p path or @p from is `NULL`.
* @throws ENOENT If @p from does not exist, along with any other `errno`
* `realpath(3)` raises.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @warning Known defect: the scratch string it claims is never released, so 256
* calls exhaust the string pool. TODO.md, "Known and still open" item 4.
*/
akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String **dst)
{
akgl_String *dirnamestr;
@@ -163,104 +87,104 @@ akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path");
FAIL_ZERO_RETURN(e, from, AKERR_NULLPOINTER, "from");
PASS(e, akgl_heap_next_string(&dirnamestr));
PASS(e, aksl_realpath(from, (char *)&dirnamestr->data, sizeof(dirnamestr->data)));
PASS(e, aksl_realpath(from, (char *)&dirnamestr->data));
dirname((char *)&dirnamestr->data);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL Rectangle reference");
dest->topleft.x = rect->x;
dest->topleft.y = rect->y;
dest->bottomleft.x = rect->x;
dest->bottomleft.y = rect->y + rect->h;
dest->topright.x = rect->x + rect->w;
dest->topright.y = rect->y;
dest->bottomright.x = rect->x + rect->w;
dest->bottomright.y = rect->y + rect->h;
SUCCEED_RETURN(errctx);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL Rectangle reference");
dest->topleft.x = rect->x;
dest->topleft.y = rect->y;
dest->bottomleft.x = rect->x;
dest->bottomleft.y = rect->y + rect->h;
dest->topright.x = rect->x + rect->w;
dest->topright.y = rect->y;
dest->bottomright.x = rect->x + rect->w;
dest->bottomright.y = rect->y + rect->h;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_collide_point_rectangle(point *p, RectanglePoints *rp, bool *collide)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, p, AKERR_NULLPOINTER, "NULL Point reference");
FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL boolean reference");
if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) &&
(p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) {
*collide = true;
} else {
*collide = false;
}
SUCCEED_RETURN(errctx);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, p, AKERR_NULLPOINTER, "NULL Point reference");
FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL boolean reference");
if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) &&
(p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) {
*collide = true;
} else {
*collide = false;
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide)
{
RectanglePoints r1p;
RectanglePoints r2p;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, r1, 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");
RectanglePoints r1p;
RectanglePoints r2p;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, r1, 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");
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); }
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 {
} PROCESS(errctx) {
} FINISH(errctx, true);
*collide = false;
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)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, s1, 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");
SUCCEED_RETURN(errctx);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, s1, 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");
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)
@@ -278,11 +202,11 @@ akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, in
FAIL_ZERO_BREAK(errctx, t2, AKERR_NULLPOINTER, "NULL texture");
CATCH(errctx, akgl_heap_next_string(&tmpstring));
SDL_RenderClear(renderer->sdl_renderer);
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
s1 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
SDL_RenderClear(renderer.sdl_renderer);
CATCH(errctx, renderer.draw_texture(&renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
s1 = SDL_RenderReadPixels(renderer.sdl_renderer, &read);
FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer");
if ( writeout != NULL ) {
snprintf((char *)&tmpstring->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), writeout);
FAIL_ZERO_BREAK(
@@ -293,13 +217,13 @@ akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, in
(char *)&tmpstring->data,
SDL_GetError());
}
SDL_RenderClear(renderer->sdl_renderer);
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
s2 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
SDL_RenderClear(renderer.sdl_renderer);
CATCH(errctx, renderer.draw_texture(&renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
s2 = SDL_RenderReadPixels(renderer.sdl_renderer, &read);
FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer");
CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2));
} CLEANUP {
if ( s1 != NULL )

View File

@@ -1,15 +0,0 @@
/**
* @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,8 +1,7 @@
#define UNHANDLED_ERROR_TERMINATION_BEHAVIOR \
#define UNHANDLED_ERROR_TERMINATION_BEHAVIOR \
handle_unhandled_error(errctx);
#include <akerror.h>
#include <akgl/error.h>
#define UNHANDLED_ERROR_EXIT 0
#define UNHANDLED_ERROR_SET 1
@@ -13,13 +12,8 @@
#include <akgl/iterator.h>
#include <akgl/registry.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/sprite.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include "testutil.h"
int UNHANDLED_ERROR_BEHAVIOR;
akerr_ErrorContext *unhandled_error_context;
@@ -48,7 +42,7 @@ akerr_ErrorContext *akgl_actor_update_noop(akgl_Actor *obj)
// Currently the renderer assumes there is a global variable named `renderer`
int akgl_actor_rendered;
akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj)
akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj, SDL_Renderer *r)
{
PREPARE_ERROR(errctx);
akgl_actor_rendered = 1;
@@ -58,7 +52,7 @@ akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj)
akerr_ErrorContext *test_registry_actor_iterator_nullpointers(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
akerr_handler_unhandled_error = handle_unhandled_error_noexit;
@@ -135,13 +129,13 @@ akerr_ErrorContext *test_registry_actor_iterator_updaterender(void)
FAIL_ZERO_BREAK(
unhandled_error_context,
akgl_actor_updated,
AKGL_ERR_BEHAVIOR,
AKERR_BEHAVIOR,
"actor->updatefunc not called by the iterator"
);
FAIL_ZERO_BREAK(
unhandled_error_context,
akgl_actor_rendered,
AKGL_ERR_BEHAVIOR,
AKERR_BEHAVIOR,
"actor->renderfunc not called by the iterator"
);
} CLEANUP {
@@ -151,7 +145,7 @@ akerr_ErrorContext *test_registry_actor_iterator_updaterender(void)
} FINISH(unhandled_error_context, true);
akerr_handler_unhandled_error = defaulthandler;
SUCCEED_RETURN(errctx);
}
@@ -159,7 +153,7 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
{
akgl_Actor *testactor = NULL;
akgl_Character *testchar = NULL;
PREPARE_ERROR(errctx);
ATTEMPT {
@@ -167,10 +161,10 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Handled\n");
printf("Handled\n");
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&testactor));
@@ -178,13 +172,13 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
testactor->layer = 0;
testactor->updatefunc = &akgl_actor_update_noop;
testactor->renderfunc = &akgl_actor_render_noop;
CATCH(errctx, akgl_actor_set_character(testactor, "test"));
} CLEANUP {
IGNORE(akgl_heap_release_actor(testactor));
IGNORE(akgl_heap_release_actor(testactor));
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Handled\n");
printf("Handled\n");
} FINISH(errctx, true);
ATTEMPT {
@@ -195,13 +189,13 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
testactor->layer = 0;
testactor->updatefunc = &akgl_actor_update_noop;
testactor->renderfunc = &akgl_actor_render_noop;
CATCH(errctx, akgl_character_initialize(testchar, "test"));
CATCH(errctx, akgl_actor_set_character(testactor, "test"));
} CLEANUP {
IGNORE(akgl_heap_release_actor(testactor));
IGNORE(akgl_heap_release_character(testchar));
IGNORE(akgl_heap_release_actor(testactor));
IGNORE(akgl_heap_release_character(testchar));
} PROCESS(errctx) {
} FINISH(errctx, true);
@@ -241,20 +235,20 @@ akerr_ErrorContext *test_actor_manage_children(void)
CATCH(errctx, parent->addchild(parent, child));
} CLEANUP {
if ( errctx == NULL ) {
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_RELATIONSHIP when child already has a parent");
FAIL(errctx, AKERR_BEHAVIOR, "addchild does not throw AKERR_RELATIONSHIP when child already has a parent");
}
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_RELATIONSHIP) {
// Expected behavior
SDL_Log("addchild throws AKERR_RELATIONSHIP when child already has a parent");
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&child));
CATCH(errctx, parent->addchild(parent, child));
} CLEANUP {
if ( errctx == NULL ) {
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_OUTOFBOUNDS when all children already set");
FAIL(errctx, AKERR_BEHAVIOR, "addchild does not throw AKERR_OUTOFBOUNDS when all children already set");
}
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_OUTOFBOUNDS) {
@@ -284,7 +278,7 @@ akerr_ErrorContext *test_actor_manage_children(void)
"Child %s was not removed from the registry",
(char *)&tmpstring->data);
}
_test_actor_addchild_heaprelease_cleanup:
_test_actor_addchild_heaprelease_cleanup:
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
@@ -309,601 +303,17 @@ akerr_ErrorContext *test_actor_manage_children(void)
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Build an actor bound to a character, without touching the renderer.
*
* The sprite and spritesheet are taken straight off the heap and populated by
* hand, because akgl_sprite_load_json() would need a live renderer to build a
* texture and none of the logic under test reads one.
*/
static akerr_ErrorContext *make_bound_actor(
akgl_Actor **actor,
akgl_Character **basechar,
akgl_Sprite **sprite,
char *name,
int state)
{
PREPARE_ERROR(e);
akgl_SpriteSheet *sheet = NULL;
char spritename[AKGL_SPRITE_MAX_NAME_LENGTH];
ATTEMPT {
CATCH(e, akgl_heap_next_character(basechar));
CATCH(e, akgl_character_initialize(*basechar, name));
CATCH(e, akgl_heap_next_spritesheet(&sheet));
sheet->refcount += 1;
snprintf((char *)&spritename, AKGL_SPRITE_MAX_NAME_LENGTH, "%s_sprite", name);
CATCH(e, akgl_heap_next_sprite(sprite));
CATCH(e, akgl_sprite_initialize(*sprite, (char *)&spritename, sheet));
(*sprite)->width = 32;
(*sprite)->height = 32;
(*sprite)->frames = 4;
(*sprite)->speed = 100;
CATCH(e, akgl_character_sprite_add(*basechar, *sprite, state));
CATCH(e, akgl_heap_next_actor(actor));
CATCH(e, akgl_actor_initialize(*actor, name));
(*actor)->basechar = *basechar;
(*actor)->state = state;
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_control_handlers_on(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Character basechar;
SDL_Event event;
ATTEMPT {
memset(&event, 0x00, sizeof(SDL_Event));
memset(&basechar, 0x00, sizeof(akgl_Character));
basechar.ax = 7.0f;
basechar.ay = 11.0f;
// Left: clears every facing and movement bit, then sets its own pair and
// takes acceleration from the base character with the sign reversed.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
actor.state = (AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_ALIVE);
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_on(&actor, &event), "left on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
"left on did not set MOVING_LEFT (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"left on did not set FACE_LEFT (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"left on left MOVING_RIGHT set (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"left on left FACE_RIGHT set (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_ALIVE),
"left on cleared an unrelated state bit (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ax, -7.0f, "left on set ax to %f, expected -7", actor.ax);
// Right: same, with the sign preserved.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_on(&actor, &event), "right on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"right on did not set MOVING_RIGHT (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"right on did not set FACE_RIGHT (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ax, 7.0f, "right on set ax to %f, expected 7", actor.ax);
// Up reverses the Y acceleration, because Y grows downward.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_up_on(&actor, &event), "up on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
"up on did not set MOVING_UP (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_UP),
"up on did not set FACE_UP (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ay, -11.0f, "up on set ay to %f, expected -11", actor.ay);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_down_on(&actor, &event), "down on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
"down on did not set MOVING_DOWN (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_DOWN),
"down on did not set FACE_DOWN (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ay, 11.0f, "down on set ay to %f, expected 11", actor.ay);
// Each direction is exclusive: turning right after left leaves only right.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_on(&actor, &event), "left on before turning");
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_on(&actor, &event), "right on after left");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
"turning right left MOVING_LEFT set (state %d)", actor.state);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_control_handlers_off(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Character basechar;
SDL_Event event;
ATTEMPT {
memset(&event, 0x00, sizeof(SDL_Event));
memset(&basechar, 0x00, sizeof(akgl_Character));
basechar.ax = 7.0f;
basechar.ay = 11.0f;
// Releasing a direction zeroes that axis outright: acceleration, thrust,
// environmental force, and velocity all go to zero together.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT);
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_off(&actor, &event), "left off");
TEST_ASSERT_FEQ(e, actor.ax, 0.0f, "left off left ax at %f", actor.ax);
TEST_ASSERT_FEQ(e, actor.ex, 0.0f, "left off left ex at %f", actor.ex);
TEST_ASSERT_FEQ(e, actor.tx, 0.0f, "left off left tx at %f", actor.tx);
TEST_ASSERT_FEQ(e, actor.vx, 0.0f, "left off left vx at %f", actor.vx);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
"left off did not clear MOVING_LEFT (state %d)", actor.state);
// Facing is deliberately sticky, so an idle actor keeps looking where it was.
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"left off cleared FACE_LEFT, which should persist (state %d)", actor.state);
// The Y axis is untouched by a horizontal release.
TEST_ASSERT_FEQ(e, actor.vy, 5.0f, "left off disturbed vy (%f)", actor.vy);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_off(&actor, &event), "right off");
TEST_ASSERT_FEQ(e, actor.vx, 0.0f, "right off left vx at %f", actor.vx);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"right off did not clear MOVING_RIGHT (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_up_off(&actor, &event), "up off");
TEST_ASSERT_FEQ(e, actor.vy, 0.0f, "up off left vy at %f", actor.vy);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
"up off did not clear MOVING_UP (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_down_off(&actor, &event), "down off");
TEST_ASSERT_FEQ(e, actor.vy, 0.0f, "down off left vy at %f", actor.vy);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
"down off did not clear MOVING_DOWN (state %d)", actor.state);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_control_handlers_nullpointers(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
SDL_Event event;
ATTEMPT {
memset(&event, 0x00, sizeof(SDL_Event));
memset(&actor, 0x00, sizeof(akgl_Actor));
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(NULL, &event), "left on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_off(NULL, &event), "left off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(NULL, &event), "right on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_off(NULL, &event), "right off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(NULL, &event), "up on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_off(NULL, &event), "up off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(NULL, &event), "down on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_off(NULL, &event), "down off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(&actor, NULL), "left on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_off(&actor, NULL), "left off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(&actor, NULL), "right on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_off(&actor, NULL), "right off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(&actor, NULL), "up on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_off(&actor, NULL), "up off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(&actor, NULL), "down on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_off(&actor, NULL), "down off, NULL event");
// A movement handler reads acceleration off the base character, so an
// actor without one has to be reported rather than dereferenced.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(&actor, &event),
"left on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(&actor, &event),
"right on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(&actor, &event),
"up on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(&actor, &event),
"down on, actor with no base character");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_automatic_face(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
ATTEMPT {
// With the flag off, facing is left entirely to the caller.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = false;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face with the flag off");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"automatic face changed facing while disabled (state %d)", actor.state);
// With the flag on, facing follows movement.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving left");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"automatic face did not turn the actor left (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"automatic face left the stale facing set (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving right");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"automatic face did not turn the actor right (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving up");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_UP),
"automatic face did not turn the actor up (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving down");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_DOWN),
"automatic face did not turn the actor down (state %d)", actor.state);
// Left wins over up when both are set, per the order of the checks.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_UP);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving diagonally");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"diagonal movement did not resolve to the left facing (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_UP),
"diagonal movement set two facings at once (state %d)", actor.state);
// A stationary actor ends up facing nowhere.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while stationary");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_ALL),
"a stationary actor kept a facing bit (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_ALIVE),
"automatic face cleared an unrelated state bit (state %d)", actor.state);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_automatic_face(NULL),
"automatic face with a NULL actor");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_logic_movement(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Character basechar;
ATTEMPT {
memset(&basechar, 0x00, sizeof(akgl_Character));
basechar.ax = 3.0f; basechar.ay = 4.0f;
basechar.sx = 30.0f; basechar.sy = 40.0f; basechar.sz = 50.0f;
// Max speed is always refreshed from the base character.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while idle");
TEST_ASSERT_FEQ(e, actor.sx, 30.0f, "sx copied as %f, expected 30", actor.sx);
TEST_ASSERT_FEQ(e, actor.sy, 40.0f, "sy copied as %f, expected 40", actor.sy);
TEST_ASSERT_FEQ(e, actor.sz, 50.0f, "sz copied as %f, expected 50", actor.sz);
actor.state = AKGL_ACTOR_STATE_MOVING_LEFT;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving left");
TEST_ASSERT_FEQ(e, actor.ax, -3.0f, "moving left set ax to %f, expected -3", actor.ax);
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving right");
TEST_ASSERT_FEQ(e, actor.ax, 3.0f, "moving right set ax to %f, expected 3", actor.ax);
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving up");
TEST_ASSERT_FEQ(e, actor.ay, -4.0f, "moving up set ay to %f, expected -4", actor.ay);
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving down");
TEST_ASSERT_FEQ(e, actor.ay, 4.0f, "moving down set ay to %f, expected 4", actor.ay);
// Both axes at once.
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_DOWN);
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving diagonally");
TEST_ASSERT_FEQ(e, actor.ax, -3.0f, "diagonal movement set ax to %f, expected -3", actor.ax);
TEST_ASSERT_FEQ(e, actor.ay, 4.0f, "diagonal movement set ay to %f, expected 4", actor.ay);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_movement(NULL, 1.0f),
"movement logic with a NULL actor");
// Every value the movement logic writes is read off the base character,
// so an actor without one has to be reported rather than dereferenced.
memset(&actor, 0x00, sizeof(akgl_Actor));
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_movement(&actor, 1.0f),
"movement logic with no base character");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_logic_changeframe(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Sprite sprite;
ATTEMPT {
memset(&sprite, 0x00, sizeof(akgl_Sprite));
sprite.frames = 4;
// Mid-animation, the frame simply advances.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 1;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "advancing mid-animation");
TEST_ASSERT(e, actor.curSpriteFrameId == 2,
"frame advanced to %d, expected 2", actor.curSpriteFrameId);
// At the last frame without looping, it wraps to the start.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 3;
sprite.loop = false;
sprite.loopReverse = false;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "wrapping a non-looping sprite");
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
"the last frame wrapped to %d, expected 0", actor.curSpriteFrameId);
// A forward-looping sprite behaves the same way at the end.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 3;
sprite.loop = true;
sprite.loopReverse = false;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "wrapping a forward-looping sprite");
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
"the forward loop wrapped to %d, expected 0", actor.curSpriteFrameId);
// A ping-pong sprite turns around at the end instead of wrapping.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 3;
sprite.loop = true;
sprite.loopReverse = true;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "reversing at the end");
TEST_ASSERT(e, actor.curSpriteReversing == true,
"the sprite did not enter its reverse phase at the last frame");
TEST_ASSERT(e, actor.curSpriteFrameId == 2,
"reversing stepped to frame %d, expected 2", actor.curSpriteFrameId);
// While reversing, the frame counts back down.
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "stepping back while reversing");
TEST_ASSERT(e, actor.curSpriteFrameId == 1,
"reversing stepped to frame %d, expected 1", actor.curSpriteFrameId);
// At frame zero it turns around again and resumes going forward.
actor.curSpriteFrameId = 0;
actor.curSpriteReversing = true;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "turning around at frame zero");
TEST_ASSERT(e, actor.curSpriteReversing == false,
"the sprite stayed in its reverse phase at frame zero");
TEST_ASSERT(e, actor.curSpriteFrameId == 1,
"turning around stepped to frame %d, expected 1", actor.curSpriteFrameId);
// A single-frame sprite has nowhere to advance to.
memset(&actor, 0x00, sizeof(akgl_Actor));
sprite.frames = 1;
sprite.loop = false;
sprite.loopReverse = false;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "advancing a single-frame sprite");
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
"a single-frame sprite moved to frame %d, expected 0", actor.curSpriteFrameId);
sprite.frames = 4;
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_changeframe(NULL, &sprite, 0),
"changeframe with a NULL actor");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/** @brief Records calls made to the changeframe stub. */
static int changeframe_calls = 0;
/** @brief Changeframe stub that records its invocation without advancing. */
static akerr_ErrorContext *stub_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems)
{
PREPARE_ERROR(e);
changeframe_calls += 1;
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_update(void)
{
PREPARE_ERROR(e);
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
akgl_Sprite *sprite = NULL;
SDL_Time now = 0;
ATTEMPT {
CATCH(e, akgl_registry_init_actor());
CATCH(e, akgl_registry_init_sprite());
CATCH(e, akgl_registry_init_spritesheet());
CATCH(e, akgl_registry_init_character());
CATCH(e, akgl_heap_init());
CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "updatable", AKGL_ACTOR_STATE_ALIVE));
actor->changeframefunc = &stub_changeframe;
// Long enough since the last frame change, so the sprite advances.
SDL_GetCurrentTime(&now);
sprite->speed = 1;
actor->curSpriteFrameTimer = 0;
changeframe_calls = 0;
TEST_EXPECT_OK(e, akgl_actor_update(actor), "updating an actor whose frame is due");
TEST_ASSERT(e, changeframe_calls == 1,
"a due frame change fired %d times, expected 1", changeframe_calls);
TEST_ASSERT(e, actor->curSpriteFrameTimer != 0,
"the frame timer was not restamped after a frame change");
// Too soon since the last change, so nothing happens.
SDL_GetCurrentTime(&now);
sprite->speed = 1000000000;
actor->curSpriteFrameTimer = now;
changeframe_calls = 0;
TEST_EXPECT_OK(e, akgl_actor_update(actor), "updating an actor whose frame is not due");
TEST_ASSERT(e, changeframe_calls == 0,
"an early frame change fired %d times, expected 0", changeframe_calls);
// An actor in a state with no sprite bound is skipped, not failed: the
// missing binding is reported as AKERR_KEY and swallowed by update.
actor->state = AKGL_ACTOR_STATE_DEAD;
changeframe_calls = 0;
TEST_EXPECT_OK(e, akgl_actor_update(actor),
"updating an actor whose state has no sprite");
TEST_ASSERT(e, changeframe_calls == 0,
"an actor with no sprite for its state still changed frames");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_update(NULL),
"updating a NULL actor");
// An actor with no base character cannot resolve a sprite at all.
actor->basechar = NULL;
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_update(actor),
"updating an actor with no base character");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_character_sprite_binding(void)
{
PREPARE_ERROR(e);
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
akgl_Sprite *sprite = NULL;
akgl_Sprite *found = NULL;
ATTEMPT {
CATCH(e, akgl_registry_init_actor());
CATCH(e, akgl_registry_init_sprite());
CATCH(e, akgl_registry_init_spritesheet());
CATCH(e, akgl_registry_init_character());
CATCH(e, akgl_heap_init());
CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "bound", AKGL_ACTOR_STATE_ALIVE));
TEST_EXPECT_OK(e, akgl_character_sprite_get(basechar, AKGL_ACTOR_STATE_ALIVE, &found),
"reading back a bound sprite");
TEST_ASSERT(e, found == sprite, "the bound sprite came back as a different object");
// A composite state is a distinct key from either of its components.
TEST_EXPECT_STATUS(e, AKERR_KEY,
akgl_character_sprite_get(basechar,
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP),
&found),
"reading a composite state that was never bound");
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_character_sprite_get(basechar, 0, &found),
"reading state zero, which was never bound");
// Binding a composite state makes it resolvable.
TEST_EXPECT_OK(e, akgl_character_sprite_add(basechar, sprite,
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP)),
"binding a sprite to a composite state");
TEST_EXPECT_OK(e, akgl_character_sprite_get(basechar,
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP),
&found),
"reading back the composite binding");
TEST_ASSERT(e, found == sprite, "the composite binding resolved to a different sprite");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_get(NULL, 0, &found),
"sprite_get with a NULL character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
akgl_character_sprite_get(basechar, AKGL_ACTOR_STATE_ALIVE, NULL),
"sprite_get with a NULL destination");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_add(NULL, sprite, 0),
"sprite_add with a NULL character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_add(basechar, NULL, 0),
"sprite_add with a NULL sprite");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_sprite_sheet_coords(void)
{
PREPARE_ERROR(e);
akgl_Sprite sprite;
SDL_FRect coords;
ATTEMPT {
memset(&sprite, 0x00, sizeof(akgl_Sprite));
sprite.width = 32;
sprite.height = 32;
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(NULL, &coords, 0),
"sheet coords with a NULL sprite");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(&sprite, NULL, 0),
"sheet coords with a NULL rectangle");
// The sprite has no sheet attached, so there is no texture to measure against.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(&sprite, &coords, 0),
"sheet coords with a NULL spritesheet");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
int main(void)
{
akgl_actor_updated = 0;
akgl_actor_rendered = 0;
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
PREPARE_ERROR(errctx);
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_registry_init_actor());
CATCH(errctx, akgl_registry_init_sprite());
CATCH(errctx, akgl_registry_init_spritesheet());
@@ -914,16 +324,6 @@ int main(void)
CATCH(errctx, test_registry_actor_iterator_updaterender());
CATCH(errctx, test_akgl_actor_set_character());
CATCH(errctx, test_actor_manage_children());
CATCH(errctx, test_actor_control_handlers_on());
CATCH(errctx, test_actor_control_handlers_off());
CATCH(errctx, test_actor_control_handlers_nullpointers());
CATCH(errctx, test_actor_automatic_face());
CATCH(errctx, test_actor_logic_movement());
CATCH(errctx, test_actor_logic_changeframe());
CATCH(errctx, test_actor_update());
CATCH(errctx, test_actor_character_sprite_binding());
CATCH(errctx, test_actor_sprite_sheet_coords());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);

View File

@@ -1,121 +0,0 @@
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.

View File

@@ -1,19 +0,0 @@
{
"name": "json helper fixture",
"count": 42,
"ratio": 2.5,
"negative": -17,
"enabled": true,
"disabled": false,
"nested": {
"inner": "value",
"innercount": 7
},
"integers": [10, 20, 30],
"strings": ["alpha", "beta"],
"objects": [
{ "id": 1 },
{ "id": 2 }
],
"mixed": [1, "two", { "three": 3 }]
}

View File

@@ -1,11 +1,7 @@
{
"name": "testcharacter",
"movementspeed": 1,
"speedtime": 1,
"speed_x": 0.20,
"speed_y": 0.20,
"acceleration_x": 0.20,
"acceleration_y": 0.20,
"velocity_x": 0.20,
"velocity_y": 0.20,
"sprite_mappings": [
{
"state": [
@@ -16,7 +12,7 @@
},
{
"state": [
"AKGL_ACTOR_STATE_DEAD"
"ACTOR_STATE_DEAD"
],
"sprite": "testsprite2"
}

View File

@@ -1,6 +1,6 @@
{
"spritesheet": {
"filename": "spritesheet.png",
"filename": "assets/spritesheet.png",
"frame_width": 48,
"frame_height": 48
},

View File

@@ -1,6 +1,6 @@
{
"spritesheet": {
"filename": "spritesheet.png",
"filename": "assets/spritesheet.png",
"frame_width": 48,
"frame_height": 48
},

View File

@@ -1,625 +0,0 @@
/**
* @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);
}
/** @brief Mix @p ticks whole sweep steps, one step per call. */
static akerr_ErrorContext *mix_sweep_ticks(int ticks)
{
PREPARE_ERROR(errctx);
int i = 0;
ATTEMPT {
for ( i = 0; i < ticks; i++ ) {
CATCH(errctx, akgl_audio_mix(samples, AKGL_AUDIO_SWEEP_TICK_FRAMES));
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_audio_sweep(void)
{
PREPARE_ERROR(errctx);
bool active = false;
ATTEMPT {
CATCH(errctx, reset_audio());
// 400 Hz climbing to 500 in 25 Hz steps: four steps, at 60 steps per
// second. The pitch is advanced by the mixer's own frame counter, so
// these assertions are about exact frames rather than about how often
// the caller happened to ask for samples.
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 400.0f, 500.0f, 25.0f, 1000),
"sounding a rising sweep");
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
"a sweep starts at %f, expected its start frequency",
akgl_audio_voices[0].hz);
// One tick's worth of frames is not yet a step: the last frame of that
// window is still the first tick.
CATCH(errctx, mix_sweep_ticks(1));
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
"the pitch moved at %f before the first step was due",
akgl_audio_voices[0].hz);
TEST_ASSERT(errctx, peak_of(AKGL_AUDIO_SWEEP_TICK_FRAMES) > 0.5f,
"a sweep is not making a sound (peak %f)",
peak_of(AKGL_AUDIO_SWEEP_TICK_FRAMES));
// The frame after it is.
CATCH(errctx, akgl_audio_mix(samples, 1));
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 425.0f,
"one step in, the pitch is %f, expected 425",
akgl_audio_voices[0].hz);
// It stops when it arrives rather than running past the target.
CATCH(errctx, mix_sweep_ticks(8));
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 500.0f,
"a finished sweep sits at %f, expected its target 500",
akgl_audio_voices[0].hz);
// Downward is the same sweep with the frequencies the other way round.
// The step stays positive; the direction is not its sign.
CATCH(errctx, reset_audio());
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 500.0f, 400.0f, 25.0f, 1000),
"sounding a falling sweep");
CATCH(errctx, mix_sweep_ticks(1));
CATCH(errctx, akgl_audio_mix(samples, 1));
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 475.0f,
"one step down, the pitch is %f, expected 475",
akgl_audio_voices[0].hz);
CATCH(errctx, mix_sweep_ticks(8));
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
"a finished falling sweep sits at %f, expected its target 400",
akgl_audio_voices[0].hz);
// Equal frequencies are a held tone, not an error: a caller translating
// a statement that computes its own limits does not have to special
// case them.
CATCH(errctx, reset_audio());
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 440.0f, 440.0f, 10.0f, 1000),
"sweeping between two identical frequencies");
CATCH(errctx, mix_sweep_ticks(4));
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 440.0f,
"a sweep with nowhere to go moved to %f", akgl_audio_voices[0].hz);
// The gate still governs: a sweep with further to go than the note lasts
// ends with the note, part way up. 30 ms is 1323 frames, which is one
// whole step and most of a second.
CATCH(errctx, reset_audio());
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 400.0f, 4000.0f, 25.0f, 30),
"sounding a sweep longer than its gate");
CATCH(errctx, mix_sweep_ticks(3));
CATCH(errctx, akgl_audio_voice_active(0, &active));
TEST_ASSERT(errctx, active == false, "a swept voice outlived its gate");
TEST_ASSERT(errctx, akgl_audio_voices[0].hz > 400.0f,
"a sweep cut off by its gate never stepped at all (%f)",
akgl_audio_voices[0].hz);
TEST_ASSERT(errctx, akgl_audio_voices[0].hz < 4000.0f,
"a sweep cut off by its gate reached %f anyway",
akgl_audio_voices[0].hz);
// A voice reused for a plain tone stops sweeping.
CATCH(errctx, reset_audio());
CATCH(errctx, akgl_audio_sweep(0, 400.0f, 800.0f, 25.0f, 1000));
CATCH(errctx, mix_sweep_ticks(2));
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000),
"sounding a plain tone on a voice that was sweeping");
CATCH(errctx, mix_sweep_ticks(4));
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, TEST_TONE_HZ,
"a plain tone on a swept voice drifted to %f",
akgl_audio_voices[0].hz);
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_audio_sweep(AKGL_AUDIO_MAX_VOICES, 400.0f, 500.0f, 25.0f, 100),
"sweeping a voice past the last one");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_audio_sweep(0, 0.0f, 500.0f, 25.0f, 100),
"sweeping from zero hertz");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_audio_sweep(0, 400.0f, -1.0f, 25.0f, 100),
"sweeping to a negative frequency");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_audio_sweep(0, 400.0f, 500.0f, 0.0f, 100),
"sweeping with no step");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_audio_sweep(0, 500.0f, 400.0f, -25.0f, 100),
"sweeping down with a negative step");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_audio_sweep(0, 400.0f, 500.0f, 25.0f, 0),
"sweeping with no duration");
} 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_sweep());
CATCH(errctx, test_audio_volume_and_mixing());
CATCH(errctx, test_audio_device());
} CLEANUP {
SDL_Quit();
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -1,336 +0,0 @@
/**
* @file benchutil.h
* @brief The timing harness behind the perf suites: run it, record it, report it, hold it to a budget.
*
* A benchmark here is a timed region: bench_start() takes a timestamp,
* bench_stop() takes another and divides by the number of units of work that
* happened in between. The unit is whatever the measurement is *about* -- one
* call, one actor, one tile, one frame -- and it is printed alongside the
* number so nobody has to guess what "42 ns" was 42 nanoseconds of.
*
* Two things make the numbers usable rather than merely present:
*
* 1. **Best of #AKGL_BENCH_REPETITIONS.** Calling bench_start() again with a
* name already in the table folds the new run into the old entry and keeps
* the *lowest* ns/op seen. A benchmark loop therefore repeats itself and the
* harness reports the run that was interrupted least. The mean is the wrong
* statistic here: every source of noise on a shared machine makes a run
* slower and none makes it faster.
* 2. **The timed region contains no error checking.** `PASS` and `CATCH` both
* call `akerr_valid_error_address`, which walks `AKERR_ARRAY_ERROR` looking
* for a match -- more work than several of the calls being measured. Use
* #BENCH_LOOP, which stashes the context and stops at the first failure, and
* hand it to `PASS` after the clock has stopped.
*
* A budget is a per-op ceiling in nanoseconds, and a benchmark that exceeds it
* fails the suite. Budgets are set at roughly ten times the measured baseline:
* loose enough that a busy machine does not turn CI red, tight enough that an
* algorithmic regression -- a linear scan that becomes quadratic, a per-frame
* allocation that becomes a per-actor one -- cannot hide. They are enforced
* only in an optimized build at full scale; a coverage build measures the
* instrumentation, not the library, and reports without judging.
*
* Set `AKGL_BENCH_SCALE` in the environment to change how long the suite runs:
* 0.1 for a tenth of the iterations, 10 for ten times as many. Below 1.0 the
* budgets are not enforced, because a short run is a noisy one.
*
* **Under valgrind these suites become the memory-check suites**, and the scale
* drops itself to #AKGL_BENCH_VALGRIND_SCALE without being asked. That is the
* whole reason `ctest -T memcheck` does not need benchmark programs of its own:
* a leak check wants every path walked *once*, and a benchmark is a program that
* walks one path a hundred thousand times. Divide the iteration counts by two
* thousand and the same binary is exactly the right shape for memcheck --
* broader coverage of asset loading, drawing, and the frame loop than any unit
* suite, at a runtime valgrind can survive. Timings from such a run are
* meaningless and the report says so.
*/
#ifndef _AKGL_BENCHUTIL_H_
#define _AKGL_BENCHUTIL_H_
#include <stdio.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/error.h>
/** @brief How many benchmarks one suite can record. */
#define AKGL_BENCH_MAX_RESULTS 48
/** @brief Longest benchmark name kept, including the terminator. */
#define AKGL_BENCH_MAX_NAME 56
/** @brief Longest unit name kept, including the terminator. */
#define AKGL_BENCH_MAX_UNIT 12
/** @brief How many times a benchmark is repeated before the best run is reported. */
#define AKGL_BENCH_REPETITIONS 5
/** @brief Environment variable scaling every iteration count. */
#define AKGL_BENCH_SCALE_ENV "AKGL_BENCH_SCALE"
/** @brief Scale forced on when running under valgrind. Enough to walk every path, few enough to finish. */
#define AKGL_BENCH_VALGRIND_SCALE 0.0005
/** @brief One row of the report: what was measured, how fast, and what it was allowed to cost. */
typedef struct akgl_Benchmark {
char name[AKGL_BENCH_MAX_NAME]; /**< What was measured. Also the key runs are merged under. */
char unit[AKGL_BENCH_MAX_UNIT]; /**< What one op is: "call", "actor", "tile", "frame". */
double budget_ns; /**< Per-op ceiling in nanoseconds. 0 reports without judging. */
double best_ns; /**< Lowest ns/op seen across every run under this name. */
uint64_t ops; /**< Units of work in the run that produced best_ns. */
uint64_t elapsed_ns; /**< Wall time of that run. */
int runs; /**< How many runs were folded in. */
} akgl_Benchmark;
/** @brief Every benchmark this suite has recorded, in the order it first ran. */
static akgl_Benchmark bench_results[AKGL_BENCH_MAX_RESULTS];
/** @brief How many entries of #bench_results are in use. */
static int bench_result_count = 0;
/** @brief The entry bench_stop() will write to, claimed by bench_start(). */
static akgl_Benchmark *bench_running = NULL;
/** @brief `SDL_GetTicksNS()` at the last bench_start(). */
static uint64_t bench_started_ns = 0;
/**
* @brief Report whether this process is running under valgrind.
*
* Detected from `LD_PRELOAD`, which valgrind fills with its own
* `vgpreload_*.so` before handing the process over. That is a deliberate choice
* over `RUNNING_ON_VALGRIND` from `valgrind/valgrind.h`: the macro is exact, but
* it makes the test suite fail to compile anywhere the valgrind headers are not
* installed, and a benchmark is not worth a build dependency. The cost of the
* check being wrong is a slow run or a fast one, never a wrong answer.
*/
static bool bench_under_valgrind(void)
{
static int detected = -1;
const char *preload = NULL;
if ( detected >= 0 ) {
return ( detected == 1 );
}
detected = 0;
preload = SDL_getenv("LD_PRELOAD");
if ( preload != NULL && strstr(preload, "vgpreload") != NULL ) {
detected = 1;
}
return ( detected == 1 );
}
/**
* @brief Multiplier applied to every iteration count, from `AKGL_BENCH_SCALE`.
*
* Read once and cached. Anything unparseable, negative, or absent gives 1.0.
* Under valgrind the result is capped at #AKGL_BENCH_VALGRIND_SCALE -- the
* smaller of the two wins, so asking for an even shorter run still works.
*/
static double bench_scale(void)
{
static double scale = -1.0;
const char *env = NULL;
if ( scale >= 0.0 ) {
return scale;
}
scale = 1.0;
env = SDL_getenv(AKGL_BENCH_SCALE_ENV);
if ( env != NULL ) {
scale = SDL_atof(env);
if ( scale <= 0.0 ) {
scale = 1.0;
}
}
if ( bench_under_valgrind() == true && scale > AKGL_BENCH_VALGRIND_SCALE ) {
scale = AKGL_BENCH_VALGRIND_SCALE;
}
return scale;
}
/**
* @brief Scale a nominal iteration count, never down to zero.
* @param count The count the benchmark was written for.
* @return @p count times the scale factor, at least 1.
*/
static int bench_iterations(int count)
{
int scaled = (int)((double)count * bench_scale());
if ( scaled < 1 ) {
scaled = 1;
}
return scaled;
}
/**
* @brief Report whether budgets are being enforced in this run.
*
* An unoptimized build measures the instrumentation rather than the library, a
* scaled-down run is too short to trust, and a run under valgrind is measuring
* valgrind. All three report without failing.
*/
static bool bench_budgets_enforced(void)
{
if ( bench_under_valgrind() == true ) {
return false;
}
#ifdef __OPTIMIZE__
return ( bench_scale() >= 1.0 );
#else
return false;
#endif
}
/**
* @brief Start timing, claiming or reusing the table entry named @p name.
*
* @param name What is being measured. Truncated at #AKGL_BENCH_MAX_NAME.
* Reusing a name folds this run into that entry.
* @param unit What one op is. Truncated at #AKGL_BENCH_MAX_UNIT.
* @param budget_ns Per-op ceiling in nanoseconds; 0 to report only. The value
* from the first run under a name wins.
*
* @note The table is fixed. Past #AKGL_BENCH_MAX_RESULTS entries the run is
* dropped with a message rather than overwriting somebody else's row.
*/
static void bench_start(char *name, char *unit, double budget_ns)
{
int i = 0;
bench_running = NULL;
for ( i = 0; i < bench_result_count; i++ ) {
if ( strncmp(bench_results[i].name, name, AKGL_BENCH_MAX_NAME - 1) == 0 ) {
bench_running = &bench_results[i];
break;
}
}
if ( bench_running == NULL ) {
if ( bench_result_count >= AKGL_BENCH_MAX_RESULTS ) {
SDL_Log("benchutil: no room for benchmark '%s', raise AKGL_BENCH_MAX_RESULTS", name);
return;
}
bench_running = &bench_results[bench_result_count];
bench_result_count += 1;
memset(bench_running, 0x00, sizeof(akgl_Benchmark));
strncpy(bench_running->name, name, AKGL_BENCH_MAX_NAME - 1);
strncpy(bench_running->unit, unit, AKGL_BENCH_MAX_UNIT - 1);
bench_running->budget_ns = budget_ns;
bench_running->best_ns = -1.0;
}
bench_started_ns = SDL_GetTicksNS();
}
/**
* @brief Stop timing and keep the run if it beat every previous one.
* @param ops Units of work done since bench_start(). Zero is ignored -- a
* benchmark that did nothing has no rate to report.
*/
static void bench_stop(uint64_t ops)
{
uint64_t elapsed = SDL_GetTicksNS() - bench_started_ns;
double per_op = 0.0;
if ( bench_running == NULL || ops == 0 ) {
return;
}
per_op = (double)elapsed / (double)ops;
bench_running->runs += 1;
if ( bench_running->best_ns < 0.0 || per_op < bench_running->best_ns ) {
bench_running->best_ns = per_op;
bench_running->ops = ops;
bench_running->elapsed_ns = elapsed;
}
bench_running = NULL;
}
/**
* @brief Run @p stmt @p count times, stopping at the first error.
*
* The timed loop deliberately holds no `PASS` or `CATCH`: their validity check
* walks the whole error array and costs more than some of the calls being
* measured. Hand @p errvar to `PASS` once the clock has stopped.
*
* @param errvar Receives the first non-`NULL` context returned, or `NULL`.
* @param i Loop variable, declared by the caller.
* @param count How many times to run @p stmt.
* @param stmt The call under test. Must evaluate to an `akerr_ErrorContext *`.
*/
#define BENCH_LOOP(errvar, i, count, stmt) \
for ( i = 0; i < (count); i++ ) { \
errvar = (stmt); \
if ( errvar != NULL ) { \
break; \
} \
}
/**
* @brief Print the recorded benchmarks as a table and count the ones over budget.
*
* The table goes to stdout so `ctest --output-on-failure` and a plain run of the
* executable both show it. Rates are printed as ops per second, which is the
* number a frame budget is actually built from.
*
* @return How many benchmarks exceeded their budget. Always 0 when
* bench_budgets_enforced() is false.
*/
static int bench_report(void)
{
int i = 0;
int over = 0;
bool enforced = bench_budgets_enforced();
char *verdict = NULL;
printf("\n");
if ( bench_under_valgrind() == true ) {
printf("running under valgrind: this is a memory check, not a measurement.\n");
printf("The timings below are valgrind's and mean nothing about libakgl.\n");
}
printf("scale %.4gx, best of %d runs, budgets %s\n",
bench_scale(),
AKGL_BENCH_REPETITIONS,
enforced ? "enforced" : "reported only");
printf("%-54s %-6s %9s %12s %16s %10s %s\n",
"benchmark", "unit", "ops", "ns/op", "ops/sec", "budget", "verdict");
printf("%-54s %-6s %9s %12s %16s %10s %s\n",
"------------------------------------------------------",
"------", "---------", "------------", "----------------", "----------", "-------");
for ( i = 0; i < bench_result_count; i++ ) {
verdict = "-";
if ( bench_results[i].budget_ns > 0.0 ) {
if ( bench_results[i].best_ns > bench_results[i].budget_ns ) {
verdict = enforced ? "OVER" : "over";
if ( enforced ) {
over += 1;
}
} else {
verdict = "ok";
}
}
printf("%-54s %-6s %9llu %12.1f %16.0f %10.0f %s\n",
bench_results[i].name,
bench_results[i].unit,
(unsigned long long)bench_results[i].ops,
bench_results[i].best_ns,
( bench_results[i].best_ns > 0.0 ) ? (1000000000.0 / bench_results[i].best_ns) : 0.0,
bench_results[i].budget_ns,
verdict);
}
printf("\n");
fflush(stdout);
return over;
}
/**
* @brief Fail the enclosing ATTEMPT block if any benchmark blew its budget.
*
* The report is printed either way -- a run that fails still has to say what the
* numbers were.
*/
#define BENCH_REPORT_BREAK(e) \
{ \
int __bench_over = bench_report(); \
if ( __bench_over > 0 ) { \
FAIL_BREAK( \
e, \
AKGL_ERR_BEHAVIOR, \
"%d benchmark(s) exceeded their budget", \
__bench_over); \
} \
}
#endif // _AKGL_BENCHUTIL_H_

View File

@@ -3,29 +3,29 @@
int main(void)
{
int mask = 0;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_ALIVE);
if ( mask != AKGL_ACTOR_STATE_ALIVE )
return 1;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT) )
return 1;
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_ALIVE);
if ( mask != (AKGL_ACTOR_STATE_FACE_LEFT) )
return 1;
AKGL_BITMASK_CLEAR(mask);
if ( mask != 0 )
return 1;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
if ( !(AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_FACE_LEFT)) )
return 1;
mask = AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP;
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_FACE_ALL);
if ( mask != AKGL_ACTOR_STATE_ALIVE )
return 1;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_MOVING_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) )
return 1;
return 0;
int mask = 0;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_ALIVE);
if ( mask != AKGL_ACTOR_STATE_ALIVE )
return 1;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT) )
return 1;
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_ALIVE);
if ( mask != (AKGL_ACTOR_STATE_FACE_LEFT) )
return 1;
AKGL_BITMASK_CLEAR(mask);
if ( mask != 0 )
return 1;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
if ( !(AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_FACE_LEFT)) )
return 1;
mask = AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP;
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_FACE_ALL);
if ( mask != AKGL_ACTOR_STATE_ALIVE )
return 1;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_MOVING_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) )
return 1;
return 0;
}

View File

@@ -50,7 +50,6 @@ akerr_ErrorContext *test_character_sprite_mgmt()
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_heap_next_character(&testchar));
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
FAIL_ZERO_BREAK(
@@ -86,11 +85,10 @@ akerr_ErrorContext *test_character_iterate_state_sprites()
akgl_Sprite *testsprite = NULL;
akgl_Sprite *testsprite2 = NULL;
akgl_Iterator opflags = {.flags = AKGL_ITERATOR_OP_RELEASE, .layerid = 0};
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_heap_next_character(&testchar));
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
FAIL_ZERO_BREAK(
@@ -126,7 +124,7 @@ akerr_ErrorContext *test_character_iterate_state_sprites()
} CLEANUP {
IGNORE(akgl_heap_release_sprite(testsprite));
IGNORE(akgl_heap_release_sprite(testsprite2));
IGNORE(akgl_heap_release_character(testchar));
IGNORE(akgl_heap_release_character(testchar));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
@@ -140,7 +138,7 @@ akerr_ErrorContext *test_akgl_character_load_json()
akgl_Sprite *comparesprite = NULL;
int tsrc = 0;
int tsrc2 = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_heap_next_character(&testcharacter));
@@ -158,7 +156,7 @@ akerr_ErrorContext *test_akgl_character_load_json()
testsprite2,
AKERR_KEY,
"Sprite 2 loaded from json but not in registry");
CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));
testcharacter = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL);
FAIL_ZERO_BREAK(
@@ -171,8 +169,8 @@ akerr_ErrorContext *test_akgl_character_load_json()
CATCH(errctx, testcharacter->sprite_get(testcharacter, AKGL_ACTOR_STATE_DEAD, &comparesprite));
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_DEAD");
FAIL_ZERO_BREAK(errctx, (testcharacter->ax != 0.200000003), AKERR_VALUE, "Wrong X acceleration for test character");
FAIL_ZERO_BREAK(errctx, (testcharacter->ay != 0.200000003), AKERR_VALUE, "Wrong Y acceleration for test character");
FAIL_ZERO_BREAK(errctx, (testcharacter->vx != 0.200000003), AKERR_VALUE, "Wrong X velocity for test character");
FAIL_ZERO_BREAK(errctx, (testcharacter->vy != 0.200000003), AKERR_VALUE, "Wrong Y velocity for test character");
// Release our handles on the sprites so the character's heap_release can reduce them to 0
CATCH(errctx, akgl_heap_release_sprite(testsprite));
@@ -195,13 +193,12 @@ int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
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());
}
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());
}

View File

@@ -19,14 +19,14 @@
int numsprites = 8;
char *spritepaths[] = {
"assets/sprites/little_guy_walking_left.json",
"assets/sprites/little_guy_walking_right.json",
"assets/sprites/little_guy_walking_up.json",
"assets/sprites/little_guy_walking_down.json",
"assets/sprites/little_guy_facing_left.json",
"assets/sprites/little_guy_facing_right.json",
"assets/sprites/little_guy_facing_up.json",
"assets/sprites/little_guy_facing_down.json"
"assets/sprites/little_guy_walking_left.json",
"assets/sprites/little_guy_walking_right.json",
"assets/sprites/little_guy_walking_up.json",
"assets/sprites/little_guy_walking_down.json",
"assets/sprites/little_guy_facing_left.json",
"assets/sprites/little_guy_facing_right.json",
"assets/sprites/little_guy_facing_up.json",
"assets/sprites/little_guy_facing_down.json"
};
int main(void)
@@ -34,15 +34,15 @@ int main(void)
PREPARE_ERROR(errctx);
SDL3GControlMap *controlmap;
actor *actorptr = NULL;
ATTEMPT {
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
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());
}
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());
}
@@ -55,7 +55,7 @@ int main(void)
strcpy((char *)&game.uri, "net.aklabs.libakgl.charviewer");
game.screenwidth = 640;
game.screenheight = 480;
CATCH(errctx, akgl_GAME_init());
for ( int i = 0; i < numsprites ; i++) {
@@ -68,7 +68,7 @@ int main(void)
AKGL_REGISTRY_CHARACTER,
"little guy",
NULL);
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKGL_ERR_REGISTRY, "Can't load character 'little guy' from the registry");
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKERR_REGISTRY, "Can't load character 'little guy' from the registry");
actorptr->movement_controls_face = false;
actorptr->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT);
actorptr->x = 320;
@@ -96,7 +96,7 @@ int main(void)
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_off = SDL_EVENT_KEY_UP;
// Move left
controlmap->controls[2].key = SDLK_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].event_on = SDL_EVENT_KEY_DOWN;
controlmap->controls[2].event_off = SDL_EVENT_KEY_UP;
// Move right
controlmap->controls[3].key = SDLK_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_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_off = SDL_EVENT_KEY_UP;
} CLEANUP {
@@ -120,6 +120,6 @@ int main(void)
LOG_ERROR(errctx);
return 1;
} FINISH_NORETURN(errctx);
return 0;
}

View File

@@ -1,884 +0,0 @@
/**
* @file controller.c
* @brief Unit tests for control maps and SDL input dispatch.
*
* SDL events are synthesized directly rather than pumped through the event
* queue, so none of this needs a physical keyboard or gamepad. The device
* enumeration helpers are exercised against the dummy drivers, where the
* expected outcome is "no devices, no crash".
*/
#include <SDL3/SDL.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
// For the akgl_Actor_cmhf_* handlers these tests bind. akgl/controller.h pulls
// actor.h in for itself now; tests/headers.c is what keeps it doing so.
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/game.h>
#include <akgl/controller.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include "testutil.h"
/*
* akgl/controller.h declares these as akgl_controller_handle_*, but src has
* always defined them as gamepad_handle_*. Declared here under the names that
* actually link, so the tests can reach them without renaming shipped symbols.
*/
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_added(void *appstate, SDL_Event *event);
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event);
/** @brief Keyboard id the tests bind their control maps to. */
#define TEST_KBID 11
/** @brief Gamepad id the tests bind their control maps to. */
#define TEST_JSID 22
/** @brief Stand-in for the application state pointer SDL hands to callbacks. */
static int appstate_placeholder = 0;
/** @brief The actor every control map in this file targets. */
static akgl_Actor *player = NULL;
/** @brief Base character supplying the player's acceleration constants. */
static akgl_Character *playerchar = NULL;
/** @brief Clear every control map so each test starts from an empty binding set. */
static void reset_control_maps(void)
{
int i = 0;
for ( i = 0; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap));
}
}
/** @brief Register the "player" actor the gamepad handlers look up by name. */
static akerr_ErrorContext *make_player(void)
{
PREPARE_ERROR(e);
ATTEMPT {
CATCH(e, akgl_registry_init_actor());
CATCH(e, akgl_registry_init_character());
CATCH(e, akgl_heap_init());
CATCH(e, akgl_heap_next_character(&playerchar));
CATCH(e, akgl_character_initialize(playerchar, "playerchar"));
playerchar->ax = 6.0f;
playerchar->ay = 8.0f;
CATCH(e, akgl_heap_next_actor(&player));
CATCH(e, akgl_actor_initialize(player, "player"));
player->basechar = playerchar;
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/** @brief Build a synthetic keyboard event. */
static void make_key_event(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key)
{
memset(event, 0x00, sizeof(SDL_Event));
event->type = type;
event->key.which = which;
event->key.key = key;
}
/** @brief Build a synthetic keyboard event carrying modifier state. */
static void make_key_event_mod(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key, SDL_Keymod mod)
{
make_key_event(event, type, which, key);
event->key.mod = mod;
}
/**
* @brief Build the text input event SDL sends after a key press composes.
*
* @p text is not copied by SDL or by the library at this point, so callers pass
* a string literal.
*/
static void make_text_event(SDL_Event *event, const char *text)
{
memset(event, 0x00, sizeof(SDL_Event));
event->type = SDL_EVENT_TEXT_INPUT;
event->text.text = text;
}
/** @brief Build a synthetic gamepad button event. */
static void make_button_event(SDL_Event *event, uint32_t type, SDL_JoystickID which, uint8_t button)
{
memset(event, 0x00, sizeof(SDL_Event));
event->type = type;
event->gbutton.which = which;
event->gbutton.button = button;
}
akerr_ErrorContext *test_controller_pushmap(void)
{
PREPARE_ERROR(e);
akgl_Control control;
int i = 0;
ATTEMPT {
reset_control_maps();
memset(&control, 0x00, sizeof(akgl_Control));
control.key = SDLK_SPACE;
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.handler_on = &akgl_Actor_cmhf_left_on;
control.handler_off = &akgl_Actor_cmhf_left_off;
TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing one control");
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 1,
"pushing one control left nextMap at %d, expected 1",
GAME_ControlMaps[0].nextMap);
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].key == SDLK_SPACE,
"the pushed control did not land in slot 0");
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].handler_on == &akgl_Actor_cmhf_left_on,
"the pushed control lost its press handler");
// Pushes accumulate rather than overwrite.
TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing a second control");
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 2,
"pushing a second control left nextMap at %d, expected 2",
GAME_ControlMaps[0].nextMap);
// Maps are independent of each other.
TEST_EXPECT_OK(e, akgl_controller_pushmap(3, &control), "pushing into a different map");
TEST_ASSERT(e, GAME_ControlMaps[3].nextMap == 1,
"map 3 nextMap is %d, expected 1", GAME_ControlMaps[3].nextMap);
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 2,
"pushing into map 3 disturbed map 0 (nextMap %d)", GAME_ControlMaps[0].nextMap);
// Filling a map exactly to capacity still succeeds.
reset_control_maps();
for ( i = 0; i < AKGL_MAX_CONTROLS; i++ ) {
akerr_ErrorContext *pushresult = akgl_controller_pushmap(0, &control);
if ( pushresult != NULL ) {
pushresult->handled = true;
pushresult = akerr_release_error(pushresult);
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, "pushing control %d of %d failed", i, AKGL_MAX_CONTROLS);
}
}
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == AKGL_MAX_CONTROLS,
"a full map reports nextMap %d, expected %d",
GAME_ControlMaps[0].nextMap, AKGL_MAX_CONTROLS);
// One past capacity is refused.
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_controller_pushmap(0, &control),
"pushing into a full control map");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_pushmap(0, NULL),
"pushing a NULL control");
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS, &control),
"pushing into a control map id at the limit");
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS + 5, &control),
"pushing into a control map id past the limit");
} CLEANUP {
reset_control_maps();
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_controller_default_bindings(void)
{
PREPARE_ERROR(e);
ATTEMPT {
reset_control_maps();
CATCH(e, make_player());
TEST_EXPECT_OK(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID),
"installing the default control map");
TEST_ASSERT(e, GAME_ControlMaps[0].target == player,
"the default map did not target the player actor");
TEST_ASSERT(e, GAME_ControlMaps[0].kbid == TEST_KBID,
"the default map recorded keyboard %d, expected %d",
GAME_ControlMaps[0].kbid, TEST_KBID);
TEST_ASSERT(e, GAME_ControlMaps[0].jsid == TEST_JSID,
"the default map recorded gamepad %d, expected %d",
GAME_ControlMaps[0].jsid, TEST_JSID);
// Four keyboard bindings then four gamepad bindings.
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 8,
"the default map installed %d controls, expected 8",
GAME_ControlMaps[0].nextMap);
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].key == SDLK_DOWN,
"the first default binding is not the down arrow");
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].handler_on == &akgl_Actor_cmhf_down_on,
"the down arrow is not bound to the down handler");
TEST_ASSERT(e, GAME_ControlMaps[0].controls[1].key == SDLK_UP,
"the second default binding is not the up arrow");
TEST_ASSERT(e, GAME_ControlMaps[0].controls[2].key == SDLK_LEFT,
"the third default binding is not the left arrow");
TEST_ASSERT(e, GAME_ControlMaps[0].controls[3].key == SDLK_RIGHT,
"the fourth default binding is not the right arrow");
// The gamepad half binds buttons and leaves the keycode clear, so a
// keyboard event cannot accidentally match a dpad binding.
TEST_ASSERT(e, GAME_ControlMaps[0].controls[4].button == SDL_GAMEPAD_BUTTON_DPAD_DOWN,
"the fifth default binding is not the dpad down button");
TEST_ASSERT(e, GAME_ControlMaps[0].controls[4].key == 0,
"a gamepad binding also carries a keycode");
TEST_ASSERT(e, GAME_ControlMaps[0].controls[7].button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT,
"the eighth default binding is not the dpad right button");
// An unknown actor name is a registry error, not a crash.
reset_control_maps();
TEST_EXPECT_STATUS(e, AKGL_ERR_REGISTRY,
akgl_controller_default(1, "no_such_actor", TEST_KBID, TEST_JSID),
"installing a default map for an unregistered actor");
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
akgl_controller_default(AKGL_MAX_CONTROL_MAPS, "player", TEST_KBID, TEST_JSID),
"installing a default map at a control map id past the limit");
} CLEANUP {
reset_control_maps();
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_controller_handle_keyboard_events(void)
{
PREPARE_ERROR(e);
SDL_Event event;
ATTEMPT {
reset_control_maps();
CATCH(e, make_player());
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
// Pressing left routes to the left press handler.
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 left key press");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
"a left key press did not start the actor moving left (state %d)", player->state);
TEST_ASSERT_FEQ(e, player->ax, -6.0f,
"a left key press set ax to %f, expected -6", player->ax);
// Releasing it routes to the release handler.
make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_LEFT);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a left key release");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
"a left key release did not stop the actor (state %d)", player->state);
// The other three directions route the same way.
player->state = 0;
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_RIGHT);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a right key press");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"a right key press did not start the actor moving right (state %d)", player->state);
player->state = 0;
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_UP);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching an up key press");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
"an up key press did not start the actor moving up (state %d)", player->state);
player->state = 0;
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_DOWN);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a down key press");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
"a down key press did not start the actor moving down (state %d)", player->state);
// An unbound key is ignored.
player->state = 0;
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_F12);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching an unbound key");
TEST_ASSERT(e, player->state == 0,
"an unbound key changed the actor state to %d", player->state);
// A bound key from a different keyboard is ignored, so split-keyboard
// local multiplayer does not cross-talk.
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID + 1, SDLK_LEFT);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a bound key from an unbound keyboard");
TEST_ASSERT(e, player->state == 0,
"a key from another keyboard changed the actor state to %d", player->state);
} CLEANUP {
reset_control_maps();
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_controller_handle_gamepad_events(void)
{
PREPARE_ERROR(e);
SDL_Event event;
ATTEMPT {
reset_control_maps();
CATCH(e, make_player());
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
player->state = 0;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a dpad left press");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
"a dpad left press did not start the actor moving left (state %d)", player->state);
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_UP, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a dpad left release");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
"a dpad left release did not stop the actor (state %d)", player->state);
player->state = 0;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a dpad up press");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
"a dpad up press did not start the actor moving up (state %d)", player->state);
// A press from an unbound gamepad is ignored.
player->state = 0;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID + 1, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a dpad press from an unbound gamepad");
TEST_ASSERT(e, player->state == 0,
"a press from another gamepad changed the actor state to %d", player->state);
// An unbound button on the bound gamepad is ignored.
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_START);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching an unbound gamepad button");
TEST_ASSERT(e, player->state == 0,
"an unbound gamepad button changed the actor state to %d", player->state);
} CLEANUP {
reset_control_maps();
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_controller_handle_event_edge_cases(void)
{
PREPARE_ERROR(e);
SDL_Event event;
ATTEMPT {
reset_control_maps();
CATCH(e, make_player());
// With no control maps installed at all, every event is a no-op.
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching into an empty control map table");
// A map in a later slot is still reached, so the scan does not stop at
// the first unpopulated entry.
CATCH(e, akgl_controller_default(5, "player", TEST_KBID, TEST_JSID));
player->state = 0;
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching to a control map in a later slot");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
"a control map in slot 5 was not consulted (state %d)", player->state);
// An event type that no binding uses falls through harmlessly.
memset(&event, 0x00, sizeof(SDL_Event));
event.type = SDL_EVENT_MOUSE_MOTION;
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching an event type with no bindings");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_event(NULL, &event),
"dispatching with a NULL appstate");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
akgl_controller_handle_event(&appstate_placeholder, NULL),
"dispatching a NULL event");
} CLEANUP {
reset_control_maps();
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_controller_gamepad_button_handlers(void)
{
PREPARE_ERROR(e);
SDL_Event event;
ATTEMPT {
CATCH(e, make_player());
// These handlers find their actor by looking up "player" in the registry
// rather than taking it as an argument.
player->state = 0;
player->movement_controls_face = false;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event),
"dpad down press handler");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
"the dpad down handler did not set MOVING_DOWN (state %d)", player->state);
// With automatic facing off, the handler sets the facing itself.
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
"the dpad down handler did not set FACE_DOWN (state %d)", player->state);
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event),
"dpad down release handler");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
"the dpad down release handler did not clear MOVING_DOWN (state %d)", player->state);
TEST_ASSERT(e, player->curSpriteFrameId == 0,
"the release handler did not reset the animation frame");
player->state = 0;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP);
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad up press handler");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
"the dpad up handler did not set MOVING_UP (state %d)", player->state);
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad up release handler");
player->state = 0;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad left press handler");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
"the dpad left handler did not set MOVING_LEFT (state %d)", player->state);
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad left release handler");
player->state = 0;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad right press handler");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"the dpad right handler did not set MOVING_RIGHT (state %d)", player->state);
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad right release handler");
// With automatic facing on, the handler leaves facing to the actor's own
// face function.
player->state = 0;
player->movement_controls_face = true;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event),
"dpad down press with automatic facing on");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
"the handler set facing even though the actor faces automatically (state %d)",
player->state);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_down(NULL, &event),
"dpad press handler with a NULL appstate");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_button_down(&appstate_placeholder, NULL),
"dpad press handler with a NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_up(NULL, &event),
"dpad release handler with a NULL appstate");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_button_up(&appstate_placeholder, NULL),
"dpad release handler with a NULL event");
// With no actor named "player" registered there is nothing to drive.
CATCH(e, akgl_registry_init_actor());
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_button_down(&appstate_placeholder, &event),
"dpad press handler with no player actor registered");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_button_up(&appstate_placeholder, &event),
"dpad release handler with no player actor registered");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_controller_device_events(void)
{
PREPARE_ERROR(e);
SDL_Event event;
ATTEMPT {
// The dummy drivers present no gamepads, so the add and remove handlers
// take their "unknown device" paths. Neither may crash.
make_button_event(&event, SDL_EVENT_GAMEPAD_ADDED, 999, 0);
TEST_EXPECT_OK(e, gamepad_handle_added(&appstate_placeholder, &event),
"handling an add for a device that cannot be opened");
make_button_event(&event, SDL_EVENT_GAMEPAD_REMOVED, 999, 0);
TEST_EXPECT_OK(e, gamepad_handle_removed(&appstate_placeholder, &event),
"handling a remove for a device that was never open");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_added(NULL, &event),
"add handler with a NULL appstate");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_added(&appstate_placeholder, NULL),
"add handler with a NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_removed(NULL, &event),
"remove handler with a NULL appstate");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_removed(&appstate_placeholder, NULL),
"remove handler with a NULL event");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_controller_device_enumeration(void)
{
PREPARE_ERROR(e);
ATTEMPT {
// Enumeration has to succeed on a machine with no input devices, which
// is the normal state under the dummy drivers and in CI.
TEST_EXPECT_OK(e, akgl_controller_list_keyboards(), "listing keyboards");
TEST_EXPECT_OK(e, akgl_controller_open_gamepads(), "opening gamepads");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
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);
}
akerr_ErrorContext *test_controller_poll_keystroke(void)
{
PREPARE_ERROR(errctx);
SDL_Event event;
akgl_Keystroke keystroke;
int keycode = -1;
bool available = true;
ATTEMPT {
reset_control_maps();
CATCH(errctx, akgl_controller_flush_keys());
// An empty buffer answers "nothing waiting" and zeroes the destination.
memset(&keystroke, 0xff, sizeof(akgl_Keystroke));
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
"polling an empty keystroke buffer");
TEST_ASSERT(errctx, available == false,
"polling an empty buffer reported a keystroke was available");
TEST_ASSERT(errctx, keystroke.key == 0,
"polling an empty buffer left a keycode of %d behind", (int)keystroke.key);
TEST_ASSERT(errctx, keystroke.text[0] == '\0',
"polling an empty buffer left text behind");
// The case the whole thing exists for: a shifted key. The keycode is
// the unshifted one, so the composed character is the only place the
// double quote can come from.
make_key_event_mod(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_APOSTROPHE, SDL_KMOD_LSHIFT);
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
make_text_event(&event, "\"");
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
"polling for a shifted key");
TEST_ASSERT(errctx, available == true, "a shifted key press never reached the buffer");
TEST_ASSERT(errctx, keystroke.key == SDLK_APOSTROPHE,
"the keystroke reported keycode %d, expected %d",
(int)keystroke.key, (int)SDLK_APOSTROPHE);
TEST_ASSERT(errctx, (keystroke.mod & SDL_KMOD_SHIFT) != 0,
"the keystroke lost its shift state (mod %d)", (int)keystroke.mod);
TEST_ASSERT(errctx, strcmp(keystroke.text, "\"") == 0,
"the keystroke composed to \"%s\", expected a double quote", keystroke.text);
// A key that composes to nothing still arrives, with empty text -- that
// is how a line editor recognises Backspace and the arrows.
CATCH(errctx, akgl_controller_flush_keys());
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
"polling for a non-printing key");
TEST_ASSERT(errctx, keystroke.key == SDLK_LEFT,
"a non-printing key reported keycode %d, expected %d",
(int)keystroke.key, (int)SDLK_LEFT);
TEST_ASSERT(errctx, keystroke.text[0] == '\0',
"a non-printing key composed to \"%s\", expected nothing", keystroke.text);
// Text with no key press behind it -- an input method, or a character
// finished by a dead key -- is buffered on its own rather than dropped.
CATCH(errctx, akgl_controller_flush_keys());
make_text_event(&event, "e");
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
"polling for composed text with no key press");
TEST_ASSERT(errctx, available == true, "composed text with no key press was dropped");
TEST_ASSERT(errctx, keystroke.key == 0,
"text with no key press reported keycode %d, expected 0", (int)keystroke.key);
TEST_ASSERT(errctx, strcmp(keystroke.text, "e") == 0,
"text with no key press composed to \"%s\", expected \"e\"", keystroke.text);
TEST_ASSERT(errctx, keystroke.mod == 0,
"a text-only entry reported modifiers %d; the character it carries already "
"reflects them", (int)keystroke.mod);
// The keycode form has nowhere to report that entry, so it discards it
// on the way past rather than handing back a keystroke with no key.
CATCH(errctx, akgl_controller_flush_keys());
make_text_event(&event, "e");
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B);
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available),
"polling by keycode past a text-only entry");
TEST_ASSERT(errctx, available == true, "the key behind a text-only entry was lost");
TEST_ASSERT(errctx, keycode == SDLK_B,
"polling by keycode returned %d, expected %d", keycode, (int)SDLK_B);
// Both pollers drain the same buffer.
CATCH(errctx, akgl_controller_flush_keys());
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_C);
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available));
TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available),
"polling by keycode after the same key was taken as a keystroke");
TEST_ASSERT(errctx, available == false, "one keystroke was delivered to both pollers");
// Text that arrives after its press has already been drained becomes an
// entry of its own instead of being attached to somebody else's key.
CATCH(errctx, akgl_controller_flush_keys());
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_D);
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available));
make_text_event(&event, "d");
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
"polling for text that outlived its key press");
TEST_ASSERT(errctx, available == true, "text that outlived its key press was dropped");
TEST_ASSERT(errctx, keystroke.key == 0,
"text that outlived its key press was attached to keycode %d",
(int)keystroke.key);
// More text than an entry holds is cut on a character boundary, never
// through the middle of one. Each of these is two bytes, so three fit
// in the eight-byte field and the fourth does not.
CATCH(errctx, akgl_controller_flush_keys());
make_text_event(&event, "\xce\xb1\xce\xb1\xce\xb1\xce\xb1");
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
"polling for text longer than one entry holds");
TEST_ASSERT(errctx, strcmp(keystroke.text, "\xce\xb1\xce\xb1\xce\xb1") == 0,
"oversized text was truncated to \"%s\", expected three whole characters",
keystroke.text);
// A sequence the string ends in the middle of is dropped rather than
// copied by reading past the terminator to find its other half.
CATCH(errctx, akgl_controller_flush_keys());
make_text_event(&event, "a\xce");
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
"polling for text that ends mid-character");
TEST_ASSERT(errctx, strcmp(keystroke.text, "a") == 0,
"a truncated character came back as \"%s\", expected just the whole one",
keystroke.text);
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_controller_poll_keystroke(NULL, &available),
"polling into a NULL keystroke");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_controller_poll_keystroke(&keystroke, NULL),
"polling into a NULL availability flag");
} CLEANUP {
reset_control_maps();
IGNORE(akgl_controller_flush_keys());
} 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_VIDEO | SDL_INIT_GAMEPAD),
AKGL_ERR_SDL,
"Couldn't initialize SDL: %s",
SDL_GetError());
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, test_controller_pushmap());
CATCH(errctx, test_controller_default_bindings());
CATCH(errctx, test_controller_handle_keyboard_events());
CATCH(errctx, test_controller_handle_gamepad_events());
CATCH(errctx, test_controller_handle_event_edge_cases());
CATCH(errctx, test_controller_gamepad_button_handlers());
CATCH(errctx, test_controller_device_events());
CATCH(errctx, test_controller_device_enumeration());
CATCH(errctx, test_controller_poll_key());
CATCH(errctx, test_controller_poll_key_overflow());
CATCH(errctx, test_controller_poll_keystroke());
} CLEANUP {
SDL_Quit();
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -1,600 +0,0 @@
/**
* @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);
}

View File

@@ -1,102 +0,0 @@
/**
* @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

@@ -1,422 +0,0 @@
/**
* @file game.c
* @brief Unit tests for savegame serialization, version gating, and frame accounting.
*
* akgl_game_init() and akgl_game_update() need a window and a live frame loop,
* so they are out of scope here. Everything else in the game module is either
* pure logic or file IO and is covered below.
*/
#include <SDL3/SDL.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/sprite.h>
#include <akgl/staticstring.h>
#include "testutil.h"
/*
* Exported by src/game.c but not declared in akgl/game.h, because they are
* savegame internals rather than part of the supported surface. Declared here
* so the tests can reach them without widening the public API.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp);
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion);
/** @brief Scratch savegame path, created and removed by the tests that use it. */
static char savepath[] = "akgl_test_savegame.bin";
/** @brief Scratch path for deliberately malformed savegames. */
static char truncatedpath[] = "akgl_test_truncated.bin";
/** @brief Populate the process-wide game record with a valid identity. */
static void set_game_identity(void)
{
memset(&game, 0x00, sizeof(akgl_Game));
strncpy((char *)&game.libversion, AKGL_VERSION, 31);
strncpy((char *)&game.version, "1.2.3", 31);
strncpy((char *)&game.name, "libakgl test game", 255);
strncpy((char *)&game.uri, "https://example.invalid/akgl-test", 255);
}
akerr_ErrorContext *test_game_load_versioncmp_matching(void)
{
PREPARE_ERROR(e);
ATTEMPT {
TEST_EXPECT_OK(e, akgl_game_load_versioncmp("library", "1.2.3", "1.2.3"),
"identical versions must be compatible");
TEST_EXPECT_OK(e, akgl_game_load_versioncmp("library", "0.1.0", "0.1.0"),
"identical zero-major versions must be compatible");
TEST_EXPECT_OK(e, akgl_game_load_versioncmp("game", "10.20.30", "10.20.30"),
"identical multi-digit versions must be compatible");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_game_load_versioncmp_mismatched(void)
{
PREPARE_ERROR(e);
ATTEMPT {
// A savegame from a different build is refused on any component.
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load_versioncmp("library", "2.2.3", "1.2.3"),
"a differing major version must be refused");
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load_versioncmp("library", "1.3.3", "1.2.3"),
"a differing minor version must be refused");
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load_versioncmp("library", "1.2.4", "1.2.3"),
"a differing patch version must be refused");
// Unparseable versions are a value error, distinct from a mismatch.
TEST_EXPECT_STATUS(e, AKERR_VALUE, akgl_game_load_versioncmp("library", "1.2.3", "not-a-version"),
"an unparseable current version must be refused");
TEST_EXPECT_STATUS(e, AKERR_VALUE, akgl_game_load_versioncmp("library", "not-a-version", "1.2.3"),
"an unparseable savegame version must be refused");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_load_versioncmp(NULL, "1.2.3", "1.2.3"),
"versioncmp with a NULL version type");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_load_versioncmp("library", NULL, "1.2.3"),
"versioncmp with a NULL new version");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_load_versioncmp("library", "1.2.3", NULL),
"versioncmp with a NULL current version");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_game_load_versioncmp_releases_semver(void)
{
PREPARE_ERROR(e);
int i = 0;
bool leaked = false;
ATTEMPT {
// semver_parse allocates; the comparison must free both sides on the
// success and the failure path or a long session will drift.
for ( i = 0; i < 2000; i++ ) {
akerr_ErrorContext *result = akgl_game_load_versioncmp("library", "1.2.3", "1.2.3");
if ( result != NULL ) {
result->handled = true;
result = akerr_release_error(result);
leaked = true;
}
result = akgl_game_load_versioncmp("library", "9.9.9", "1.2.3");
if ( result != NULL ) {
result->handled = true;
result = akerr_release_error(result);
}
}
TEST_ASSERT(e, leaked == false,
"a matching version comparison started failing partway through a long run");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_game_save_roundtrip(void)
{
PREPARE_ERROR(e);
akgl_Game expected;
ATTEMPT {
CATCH(e, akgl_registry_init());
CATCH(e, akgl_heap_init());
set_game_identity();
game.fps = 60;
game.framesSinceUpdate = 7;
memcpy(&expected, &game, sizeof(akgl_Game));
TEST_EXPECT_OK(e, akgl_game_save((char *)&savepath), "saving a game");
// Scribble over the live state so a successful load has to restore it.
game.fps = 0;
game.framesSinceUpdate = 0;
TEST_EXPECT_OK(e, akgl_game_load((char *)&savepath), "loading the game back");
TEST_ASSERT(e, game.fps == 60, "fps restored as %d, expected 60", game.fps);
TEST_ASSERT(e, game.framesSinceUpdate == 7,
"framesSinceUpdate restored as %d, expected 7", game.framesSinceUpdate);
TEST_ASSERT(e, strncmp((char *)&game.name, (char *)&expected.name, 256) == 0,
"the game name was not preserved across a save and load");
TEST_ASSERT(e, strncmp((char *)&game.version, (char *)&expected.version, 32) == 0,
"the game version was not preserved across a save and load");
} CLEANUP {
unlink((char *)&savepath);
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_game_load_rejects_foreign_saves(void)
{
PREPARE_ERROR(e);
ATTEMPT {
CATCH(e, akgl_registry_init());
CATCH(e, akgl_heap_init());
// A save written by a different game must not load into this one.
set_game_identity();
CATCH(e, akgl_game_save((char *)&savepath));
strncpy((char *)&game.name, "a completely different game", 255);
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load((char *)&savepath),
"a savegame with a foreign game name must be refused");
unlink((char *)&savepath);
// Same for a differing URI.
set_game_identity();
CATCH(e, akgl_game_save((char *)&savepath));
strncpy((char *)&game.uri, "https://example.invalid/other", 255);
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load((char *)&savepath),
"a savegame with a foreign URI must be refused");
unlink((char *)&savepath);
// A save written against a different library version must be refused.
set_game_identity();
strncpy((char *)&game.libversion, "99.98.97", 31);
CATCH(e, akgl_game_save((char *)&savepath));
set_game_identity();
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load((char *)&savepath),
"a savegame from a different library version must be refused");
unlink((char *)&savepath);
// And one written against a different game version.
set_game_identity();
strncpy((char *)&game.version, "4.5.6", 31);
CATCH(e, akgl_game_save((char *)&savepath));
set_game_identity();
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load((char *)&savepath),
"a savegame from a different game version must be refused");
} CLEANUP {
unlink((char *)&savepath);
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_game_save_load_nullpointers(void)
{
PREPARE_ERROR(e);
ATTEMPT {
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_save(NULL),
"akgl_game_save(NULL)");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_load(NULL),
"akgl_game_load(NULL)");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_save_actors(NULL),
"akgl_game_save_actors(NULL)");
// A path under a directory that does not exist cannot be opened.
TEST_EXPECT_ANY_ERROR(e, akgl_game_save("no_such_directory/save.bin"),
"saving into a nonexistent directory");
TEST_EXPECT_ANY_ERROR(e, akgl_game_load("no_such_file_anywhere.bin"),
"loading a nonexistent savegame");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_game_load_truncated_table(void)
{
PREPARE_ERROR(e);
FILE *fp = NULL;
char partial[64];
ATTEMPT {
CATCH(e, akgl_registry_init());
CATCH(e, akgl_heap_init());
set_game_identity();
// A valid header followed by a table that ends before its sentinel. The
// name-map reader loops until it sees the sentinel, so it has to notice
// EOF instead of spinning.
memset(&partial, 0x00, sizeof(partial));
fp = fopen((char *)&truncatedpath, "wb");
FAIL_ZERO_BREAK(e, fp, AKERR_IO, "unable to create the truncated savegame fixture");
FAIL_ZERO_BREAK(e, fwrite(&game, 1, sizeof(akgl_Game), fp), AKERR_IO,
"unable to write the truncated savegame header");
FAIL_ZERO_BREAK(e, fwrite(&partial, 1, sizeof(partial), fp), AKERR_IO,
"unable to write the truncated savegame body");
fclose(fp);
fp = NULL;
TEST_EXPECT_ANY_ERROR(e, akgl_game_load((char *)&truncatedpath),
"loading a savegame whose name table is truncated");
} CLEANUP {
if ( fp != NULL ) {
fclose(fp);
}
unlink((char *)&truncatedpath);
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_game_save_writes_name_tables(void)
{
PREPARE_ERROR(e);
akgl_Actor *actor = NULL;
FILE *fp = NULL;
long filesize = 0;
long minimum = 0;
ATTEMPT {
CATCH(e, akgl_registry_init());
CATCH(e, akgl_heap_init());
set_game_identity();
// One registered actor, so the actor table has a real entry ahead of its
// terminating sentinel.
CATCH(e, akgl_heap_next_actor(&actor));
CATCH(e, akgl_actor_initialize(actor, "saved_actor"));
TEST_EXPECT_OK(e, akgl_game_save((char *)&savepath), "saving a game with one actor");
fp = fopen((char *)&savepath, "rb");
FAIL_ZERO_BREAK(e, fp, AKERR_IO, "unable to reopen the savegame");
fseek(fp, 0, SEEK_END);
filesize = ftell(fp);
// The header, then four name tables each ending in a name-sized and a
// pointer-sized sentinel, plus the one real actor entry.
minimum = (long)sizeof(akgl_Game)
+ (long)(AKGL_ACTOR_MAX_NAME_LENGTH + sizeof(akgl_Actor *)) * 2
+ (long)(AKGL_SPRITE_MAX_NAME_LENGTH + sizeof(akgl_Sprite *))
+ (long)(AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH + sizeof(akgl_SpriteSheet *))
+ (long)(AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH + sizeof(akgl_Character *));
TEST_ASSERT(e, filesize >= minimum,
"the savegame is %ld bytes, expected at least %ld for the header and four name tables",
filesize, minimum);
} CLEANUP {
if ( fp != NULL ) {
fclose(fp);
}
unlink((char *)&savepath);
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_game_state_lock(void)
{
PREPARE_ERROR(e);
ATTEMPT {
set_game_identity();
game.statelock = SDL_CreateMutex();
FAIL_ZERO_BREAK(e, game.statelock, AKGL_ERR_SDL, "unable to create the state mutex");
TEST_EXPECT_OK(e, akgl_game_state_lock(), "taking the state lock");
TEST_EXPECT_OK(e, akgl_game_state_unlock(), "releasing the state lock");
// The lock is reusable after a matched unlock.
TEST_EXPECT_OK(e, akgl_game_state_lock(), "retaking the state lock");
TEST_EXPECT_OK(e, akgl_game_state_unlock(), "releasing the state lock again");
} CLEANUP {
if ( game.statelock != NULL ) {
SDL_DestroyMutex(game.statelock);
game.statelock = NULL;
}
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/** @brief Counts calls made to the low-FPS callback. */
static int lowfps_calls = 0;
/** @brief Low-FPS callback stub that only records that it fired. */
static void stub_lowfps(void)
{
lowfps_calls += 1;
}
akerr_ErrorContext *test_game_updateFPS(void)
{
PREPARE_ERROR(e);
int16_t framesbefore = 0;
ATTEMPT {
set_game_identity();
game.lowfpsfunc = &stub_lowfps;
// Below the 30 FPS floor, every update notifies the callback.
game.fps = 10;
game.lastFPSTime = SDL_GetTicksNS();
lowfps_calls = 0;
framesbefore = game.framesSinceUpdate;
akgl_game_updateFPS();
TEST_ASSERT(e, lowfps_calls == 1,
"a sub-30 FPS update fired the low-FPS callback %d times, expected 1", lowfps_calls);
TEST_ASSERT(e, game.framesSinceUpdate == (framesbefore + 1),
"updateFPS did not count the frame (%d, expected %d)",
game.framesSinceUpdate, framesbefore + 1);
TEST_ASSERT(e, game.lastIterTime != 0, "updateFPS did not stamp lastIterTime");
// At or above the floor, the callback stays quiet.
game.fps = 60;
game.lastFPSTime = SDL_GetTicksNS();
lowfps_calls = 0;
akgl_game_updateFPS();
TEST_ASSERT(e, lowfps_calls == 0,
"a 60 FPS update fired the low-FPS callback %d times, expected 0", lowfps_calls);
// Once a full second has elapsed, the frame counter rolls into fps.
game.fps = 60;
game.framesSinceUpdate = 45;
game.lastFPSTime = SDL_GetTicksNS() - (2 * (SDL_Time)AKGL_TIME_ONESEC_NS);
akgl_game_updateFPS();
TEST_ASSERT(e, game.fps == 45,
"after a second elapsed, fps rolled over as %d, expected 45", game.fps);
TEST_ASSERT(e, game.framesSinceUpdate == 1,
"the frame counter restarted at %d, expected 1", game.framesSinceUpdate);
// The shipped default callback only logs, so it just has to not crash.
game.fps = 1;
akgl_game_lowfps();
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
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());
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, test_game_load_versioncmp_matching());
CATCH(errctx, test_game_load_versioncmp_mismatched());
CATCH(errctx, test_game_load_versioncmp_releases_semver());
CATCH(errctx, test_game_save_roundtrip());
CATCH(errctx, test_game_load_rejects_foreign_saves());
CATCH(errctx, test_game_save_load_nullpointers());
CATCH(errctx, test_game_load_truncated_table());
CATCH(errctx, test_game_save_writes_name_tables());
CATCH(errctx, test_game_state_lock());
CATCH(errctx, test_game_updateFPS());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -1,37 +0,0 @@
/**
* @file headers.c
* @brief Checks that a public header can be the first thing a caller includes.
*
* The assertion here is the compile, not anything main() does. `akgl/controller.h`
* declares its binding handlers as taking an `akgl_Actor *` and once did not
* declare that type, so every translation unit that reached for it before
* `akgl/actor.h` failed with "unknown type name 'akgl_Actor'". `src/controller.c`
* never noticed, because it includes `akgl/game.h` first.
*
* One header per file, deliberately. A second `#include` after the first proves
* nothing about the second: by then the first has already dragged its
* dependencies in. Covering another header means another file like this one.
*/
#include <akgl/controller.h>
#include <string.h>
int main(void)
{
akgl_Control control;
// Enough to require the akgl_Actor declaration to be complete rather than
// merely present: the handler signature names it, and a control map holds
// one by value.
memset(&control, 0x00, sizeof(akgl_Control));
control.key = SDLK_LEFT;
control.handler_on = &akgl_Actor_cmhf_left_on;
if ( control.handler_on == NULL ) {
return 1;
}
if ( sizeof(GAME_ControlMaps) != (sizeof(akgl_ControlMap) * AKGL_MAX_CONTROL_MAPS) ) {
return 1;
}
return 0;
}

View File

@@ -1,379 +0,0 @@
/**
* @file heap.c
* @brief Unit tests for the fixed-size object pools and their refcounting.
*
* The pools are process-wide arrays, so every test that fills one calls
* akgl_heap_init() first and leaves the heap empty behind it.
*/
#include <SDL3/SDL.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/heap.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/sprite.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include "testutil.h"
/**
* @brief Reset every pool and the registries that reference their objects.
*
* The registries hold raw pointers into the pools, so clearing a pool without
* clearing the registry would leave dangling entries for the next test.
*/
static akerr_ErrorContext *reset_all_heaps(void)
{
PREPARE_ERROR(e);
ATTEMPT {
CATCH(e, akgl_registry_init_actor());
CATCH(e, akgl_registry_init_sprite());
CATCH(e, akgl_registry_init_spritesheet());
CATCH(e, akgl_registry_init_character());
CATCH(e, akgl_heap_init());
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_heap_init_clears_every_pool(void)
{
PREPARE_ERROR(e);
bool clean = true;
int i = 0;
ATTEMPT {
// Dirty every pool, then require that init scrubs all of them.
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
HEAP_ACTOR[i].refcount = 5;
}
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
HEAP_SPRITE[i].refcount = 5;
}
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
HEAP_SPRITESHEET[i].refcount = 5;
}
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
HEAP_CHARACTER[i].refcount = 5;
}
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
HEAP_STRING[i].refcount = 5;
}
CATCH(e, akgl_heap_init());
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
TEST_ASSERT_FLAG(clean, HEAP_ACTOR[i].refcount == 0);
}
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the actor pool");
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
TEST_ASSERT_FLAG(clean, HEAP_SPRITE[i].refcount == 0);
}
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the sprite pool");
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
TEST_ASSERT_FLAG(clean, HEAP_SPRITESHEET[i].refcount == 0);
}
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the spritesheet pool");
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
TEST_ASSERT_FLAG(clean, HEAP_CHARACTER[i].refcount == 0);
}
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the character pool");
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
TEST_ASSERT_FLAG(clean, HEAP_STRING[i].refcount == 0);
}
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the string pool");
// akgl_heap_init_actor clears only the actor pool.
HEAP_ACTOR[0].refcount = 9;
HEAP_SPRITE[0].refcount = 9;
CATCH(e, akgl_heap_init_actor());
TEST_ASSERT(e, HEAP_ACTOR[0].refcount == 0,
"akgl_heap_init_actor did not clear the actor pool");
TEST_ASSERT(e, HEAP_SPRITE[0].refcount == 9,
"akgl_heap_init_actor cleared the sprite pool as well");
HEAP_SPRITE[0].refcount = 0;
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_heap_next_string_refcounting(void)
{
PREPARE_ERROR(e);
akgl_String *first = NULL;
akgl_String *second = NULL;
ATTEMPT {
CATCH(e, reset_all_heaps());
// akgl_heap_next_string is the only acquire function that claims the slot
// it hands out; the others leave refcount at zero for the caller to set.
CATCH(e, akgl_heap_next_string(&first));
TEST_ASSERT(e, first->refcount == 1,
"akgl_heap_next_string returned a slot with refcount %d, expected 1",
first->refcount);
CATCH(e, akgl_heap_next_string(&second));
TEST_ASSERT(e, second != first,
"akgl_heap_next_string handed out the same slot twice");
CATCH(e, akgl_heap_release_string(first));
CATCH(e, akgl_heap_release_string(second));
TEST_ASSERT(e, first->refcount == 0,
"releasing a string left refcount at %d", first->refcount);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_heap_exhaustion(void)
{
PREPARE_ERROR(e);
akgl_Actor *actor = NULL;
akgl_Sprite *sprite = NULL;
akgl_SpriteSheet *sheet = NULL;
akgl_Character *basechar = NULL;
akgl_String *str = NULL;
int i = 0;
ATTEMPT {
// Actors: the acquire function does not claim the slot, so the test does.
CATCH(e, reset_all_heaps());
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
HEAP_ACTOR[i].refcount = 1;
}
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_actor(&actor),
"akgl_heap_next_actor with every slot claimed");
CATCH(e, reset_all_heaps());
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
HEAP_SPRITE[i].refcount = 1;
}
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_sprite(&sprite),
"akgl_heap_next_sprite with every slot claimed");
CATCH(e, reset_all_heaps());
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
HEAP_SPRITESHEET[i].refcount = 1;
}
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_spritesheet(&sheet),
"akgl_heap_next_spritesheet with every slot claimed");
CATCH(e, reset_all_heaps());
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
HEAP_CHARACTER[i].refcount = 1;
}
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_character(&basechar),
"akgl_heap_next_character with every slot claimed");
// Strings claim their own slots, so draining the pool needs no help.
CATCH(e, reset_all_heaps());
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
CATCH(e, akgl_heap_next_string(&str));
}
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_string(&str),
"akgl_heap_next_string with the pool drained");
// A single release makes exactly one slot available again.
CATCH(e, akgl_heap_release_string(&HEAP_STRING[0]));
TEST_EXPECT_OK(e, akgl_heap_next_string(&str),
"akgl_heap_next_string after freeing one slot");
TEST_ASSERT(e, str == &HEAP_STRING[0],
"the reclaimed string was not the slot that was released");
CATCH(e, reset_all_heaps());
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_heap_release_refcounting(void)
{
PREPARE_ERROR(e);
akgl_Sprite *sprite = NULL;
ATTEMPT {
CATCH(e, reset_all_heaps());
CATCH(e, akgl_heap_next_sprite(&sprite));
// A shared object survives until the last reference goes away.
sprite->refcount = 2;
strncpy((char *)&sprite->name, "shared", AKGL_SPRITE_MAX_NAME_LENGTH - 1);
CATCH(e, akgl_heap_release_sprite(sprite));
TEST_ASSERT(e, sprite->refcount == 1,
"releasing a twice-referenced sprite left refcount %d, expected 1",
sprite->refcount);
TEST_ASSERT(e, sprite->name[0] == 's',
"releasing a still-referenced sprite cleared its data");
CATCH(e, akgl_heap_release_sprite(sprite));
TEST_ASSERT(e, sprite->refcount == 0,
"the final release left refcount %d, expected 0", sprite->refcount);
TEST_ASSERT(e, sprite->name[0] == 0x00,
"the final release did not clear the sprite");
// Releasing an already-free object is clamped, not wrapped below zero.
CATCH(e, akgl_heap_release_sprite(sprite));
TEST_ASSERT(e, sprite->refcount == 0,
"over-releasing drove refcount to %d, expected a clamp at 0",
sprite->refcount);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_heap_release_actor_children(void)
{
PREPARE_ERROR(e);
akgl_Actor *parent = NULL;
akgl_Actor *child = NULL;
char namebuf[AKGL_ACTOR_MAX_NAME_LENGTH];
bool released = true;
int i = 0;
ATTEMPT {
CATCH(e, reset_all_heaps());
CATCH(e, akgl_heap_next_actor(&parent));
CATCH(e, akgl_actor_initialize(parent, "parent"));
// Fill every child slot, then release the parent once.
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN; i++ ) {
snprintf((char *)&namebuf, AKGL_ACTOR_MAX_NAME_LENGTH, "child%d", i);
CATCH(e, akgl_heap_next_actor(&child));
CATCH(e, akgl_actor_initialize(child, (char *)&namebuf));
CATCH(e, akgl_actor_add_child(parent, child));
}
CATCH(e, akgl_heap_release_actor(parent));
TEST_ASSERT(e, parent->refcount == 0,
"releasing the parent left refcount %d", parent->refcount);
// Each child was initialized to 1 and incremented to 2 by add_child, so
// the recursive release should have brought every one of them back to 1.
for ( i = 1; i <= AKGL_ACTOR_MAX_CHILDREN; i++ ) {
TEST_ASSERT_FLAG(released, HEAP_ACTOR[i].refcount == 1);
}
TEST_ASSERT(e, released,
"releasing a parent did not decrement every child exactly once");
CATCH(e, reset_all_heaps());
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_heap_release_clears_registry(void)
{
PREPARE_ERROR(e);
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
ATTEMPT {
CATCH(e, reset_all_heaps());
CATCH(e, akgl_heap_next_actor(&actor));
CATCH(e, akgl_actor_initialize(actor, "registered"));
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "registered", NULL) != NULL,
"akgl_actor_initialize did not register the actor");
CATCH(e, akgl_heap_release_actor(actor));
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "registered", NULL) == NULL,
"releasing an actor left a dangling registry entry");
CATCH(e, akgl_heap_next_character(&basechar));
CATCH(e, akgl_character_initialize(basechar, "regchar"));
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "regchar", NULL) != NULL,
"akgl_character_initialize did not register the character");
CATCH(e, akgl_heap_release_character(basechar));
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "regchar", NULL) == NULL,
"releasing a character left a dangling registry entry");
CATCH(e, reset_all_heaps());
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_heap_release_nullpointers(void)
{
PREPARE_ERROR(e);
ATTEMPT {
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_actor(NULL),
"akgl_heap_release_actor(NULL)");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_sprite(NULL),
"akgl_heap_release_sprite(NULL)");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_spritesheet(NULL),
"akgl_heap_release_spritesheet(NULL)");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_character(NULL),
"akgl_heap_release_character(NULL)");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_string(NULL),
"akgl_heap_release_string(NULL)");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_heap_release_spritesheet_texture(void)
{
PREPARE_ERROR(e);
akgl_SpriteSheet *sheet = NULL;
ATTEMPT {
CATCH(e, reset_all_heaps());
CATCH(e, akgl_heap_next_spritesheet(&sheet));
// A spritesheet with no texture must still release cleanly; the texture
// branch is exercised by the sprite suite, which has a live renderer.
sheet->refcount = 1;
sheet->texture = NULL;
CATCH(e, akgl_heap_release_spritesheet(sheet));
TEST_ASSERT(e, sheet->refcount == 0,
"releasing a textureless spritesheet left refcount %d", sheet->refcount);
TEST_ASSERT(e, sheet->texture == NULL,
"releasing a spritesheet left a non-NULL texture pointer");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
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());
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, test_heap_init_clears_every_pool());
CATCH(errctx, test_heap_next_string_refcounting());
CATCH(errctx, test_heap_exhaustion());
CATCH(errctx, test_heap_release_refcounting());
CATCH(errctx, test_heap_release_actor_children());
CATCH(errctx, test_heap_release_clears_registry());
CATCH(errctx, test_heap_release_nullpointers());
CATCH(errctx, test_heap_release_spritesheet_texture());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -1,367 +0,0 @@
/**
* @file json_helpers.c
* @brief Unit tests for the typed JSON accessors.
*
* These exercise the accessors directly rather than through sprite, character,
* or tilemap loading, so the error paths are reachable without building a
* malformed asset for every case.
*/
#include <SDL3/SDL.h>
#include <jansson.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/json_helpers.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include "testutil.h"
/** @brief Fixture document shared by every test in this file. */
static json_t *fixture = NULL;
/** @brief Load tests/assets/snippets/test_json_helpers.json into @ref fixture. */
static akerr_ErrorContext *load_fixture(void)
{
PREPARE_ERROR(e);
json_error_t jsonerr;
akgl_String *pathstr = NULL;
ATTEMPT {
CATCH(e, akgl_heap_next_string(&pathstr));
snprintf(
(char *)&pathstr->data,
AKGL_MAX_STRING_LENGTH,
"%s%s",
SDL_GetBasePath(),
"assets/snippets/test_json_helpers.json");
fixture = json_load_file((char *)&pathstr->data, 0, &jsonerr);
FAIL_ZERO_BREAK(e, fixture, AKERR_IO,
"Unable to load the JSON fixture: %s", (char *)jsonerr.text);
} CLEANUP {
IGNORE(akgl_heap_release_string(pathstr));
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_json_scalar_accessors(void)
{
PREPARE_ERROR(e);
int intval = 0;
float floatval = 0;
bool boolval = false;
json_t *objval = NULL;
json_t *arrayval = NULL;
ATTEMPT {
TEST_EXPECT_OK(e, akgl_get_json_integer_value(fixture, "count", &intval), "read count");
TEST_ASSERT(e, intval == 42, "count read as %d, expected 42", intval);
TEST_EXPECT_OK(e, akgl_get_json_integer_value(fixture, "negative", &intval), "read negative");
TEST_ASSERT(e, intval == -17, "negative read as %d, expected -17", intval);
TEST_EXPECT_OK(e, akgl_get_json_number_value(fixture, "ratio", &floatval), "read ratio");
TEST_ASSERT_FEQ(e, floatval, 2.5f, "ratio read as %f, expected 2.5", floatval);
// A JSON integer is a number, so the float accessor accepts it too.
TEST_EXPECT_OK(e, akgl_get_json_number_value(fixture, "count", &floatval), "read count as a number");
TEST_ASSERT_FEQ(e, floatval, 42.0f, "count read as number %f, expected 42", floatval);
TEST_EXPECT_OK(e, akgl_get_json_boolean_value(fixture, "enabled", &boolval), "read enabled");
TEST_ASSERT(e, boolval == true, "enabled read as false");
TEST_EXPECT_OK(e, akgl_get_json_boolean_value(fixture, "disabled", &boolval), "read disabled");
TEST_ASSERT(e, boolval == false, "disabled read as true");
TEST_EXPECT_OK(e, akgl_get_json_object_value(fixture, "nested", &objval), "read nested");
TEST_ASSERT(e, objval != NULL, "nested object came back NULL");
TEST_EXPECT_OK(e, akgl_get_json_integer_value(objval, "innercount", &intval), "read nested innercount");
TEST_ASSERT(e, intval == 7, "nested innercount read as %d, expected 7", intval);
TEST_EXPECT_OK(e, akgl_get_json_array_value(fixture, "integers", &arrayval), "read integers array");
TEST_ASSERT(e, json_array_size(arrayval) == 3,
"integers array had %d entries, expected 3", (int)json_array_size(arrayval));
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_json_double_value(void)
{
PREPARE_ERROR(e);
double dblval = 0;
ATTEMPT {
TEST_EXPECT_OK(e, akgl_get_json_double_value(fixture, "ratio", &dblval), "read ratio as a double");
TEST_ASSERT(e, dblval > 2.4999 && dblval < 2.5001,
"ratio read as double %f, expected 2.5", dblval);
// Integers satisfy json_is_number, so the double accessor takes them.
TEST_EXPECT_OK(e, akgl_get_json_double_value(fixture, "count", &dblval), "read count as a double");
TEST_ASSERT(e, dblval > 41.999 && dblval < 42.001,
"count read as double %f, expected 42", dblval);
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_double_value(fixture, "absent", &dblval),
"double accessor on a missing key");
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_double_value(fixture, "name", &dblval),
"double accessor on a string value");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_double_value(NULL, "ratio", &dblval),
"double accessor on a NULL object");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_json_string_accessor(void)
{
PREPARE_ERROR(e);
akgl_String *allocated = NULL;
akgl_String *reused = NULL;
ATTEMPT {
// A NULL destination makes the accessor claim a heap string.
TEST_EXPECT_OK(e, akgl_get_json_string_value(fixture, "name", &allocated),
"read name into a NULL destination");
TEST_ASSERT(e, allocated != NULL, "the accessor did not allocate a destination string");
TEST_ASSERT(e, strcmp((char *)&allocated->data, "json helper fixture") == 0,
"name read as \"%s\"", (char *)&allocated->data);
// A caller-supplied destination is written in place, not replaced.
CATCH(e, akgl_heap_next_string(&reused));
CATCH(e, akgl_string_initialize(reused, "placeholder"));
{
akgl_String *before = reused;
TEST_EXPECT_OK(e, akgl_get_json_string_value(fixture, "name", &reused),
"read name into a preallocated destination");
TEST_ASSERT(e, reused == before,
"the accessor replaced a caller-supplied destination pointer");
}
TEST_ASSERT(e, strcmp((char *)&reused->data, "json helper fixture") == 0,
"in-place read produced \"%s\"", (char *)&reused->data);
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_string_value(fixture, "absent", &reused),
"string accessor on a missing key");
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_string_value(fixture, "count", &reused),
"string accessor on an integer value");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_string_value(NULL, "name", &reused),
"string accessor on a NULL object");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_string_value(fixture, NULL, &reused),
"string accessor with a NULL key");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_string_value(fixture, "name", NULL),
"string accessor with a NULL destination");
} CLEANUP {
IGNORE(akgl_heap_release_string(allocated));
IGNORE(akgl_heap_release_string(reused));
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_json_array_index_accessors(void)
{
PREPARE_ERROR(e);
json_t *integers = NULL;
json_t *strings = NULL;
json_t *objects = NULL;
json_t *mixed = NULL;
json_t *element = NULL;
akgl_String *strval = NULL;
int intval = 0;
ATTEMPT {
CATCH(e, akgl_get_json_array_value(fixture, "integers", &integers));
CATCH(e, akgl_get_json_array_value(fixture, "strings", &strings));
CATCH(e, akgl_get_json_array_value(fixture, "objects", &objects));
CATCH(e, akgl_get_json_array_value(fixture, "mixed", &mixed));
TEST_EXPECT_OK(e, akgl_get_json_array_index_integer(integers, 0, &intval), "integers[0]");
TEST_ASSERT(e, intval == 10, "integers[0] read as %d, expected 10", intval);
TEST_EXPECT_OK(e, akgl_get_json_array_index_integer(integers, 2, &intval), "integers[2]");
TEST_ASSERT(e, intval == 30, "integers[2] read as %d, expected 30", intval);
TEST_EXPECT_OK(e, akgl_get_json_array_index_string(strings, 1, &strval), "strings[1]");
TEST_ASSERT(e, strcmp((char *)&strval->data, "beta") == 0,
"strings[1] read as \"%s\", expected \"beta\"", (char *)&strval->data);
TEST_EXPECT_OK(e, akgl_get_json_array_index_object(objects, 1, &element), "objects[1]");
TEST_EXPECT_OK(e, akgl_get_json_integer_value(element, "id", &intval), "objects[1].id");
TEST_ASSERT(e, intval == 2, "objects[1].id read as %d, expected 2", intval);
// One past the end, and far past the end, are both out of bounds.
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_integer(integers, 3, &intval),
"integers[3] is one past the end");
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_integer(integers, 99, &intval),
"integers[99] is far past the end");
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_object(objects, 5, &element),
"objects[5] is past the end");
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_string(strings, 5, &strval),
"strings[5] is past the end");
// The mixed array holds one of each type, so every accessor can be shown
// to reject the entries that are not its own.
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_index_integer(mixed, 1, &intval),
"integer accessor on a string element");
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_index_string(mixed, 0, &strval),
"string accessor on an integer element");
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_index_object(mixed, 0, &element),
"object accessor on an integer element");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_integer(NULL, 0, &intval),
"integer index accessor on a NULL array");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_object(NULL, 0, &element),
"object index accessor on a NULL array");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_string(NULL, 0, &strval),
"string index accessor on a NULL array");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_string(strings, 0, NULL),
"string index accessor with a NULL destination");
} CLEANUP {
IGNORE(akgl_heap_release_string(strval));
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_json_type_and_key_errors(void)
{
PREPARE_ERROR(e);
int intval = 0;
float floatval = 0;
bool boolval = false;
json_t *objval = NULL;
json_t *arrayval = NULL;
ATTEMPT {
// Missing keys.
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_integer_value(fixture, "absent", &intval),
"integer accessor on a missing key");
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_number_value(fixture, "absent", &floatval),
"number accessor on a missing key");
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_boolean_value(fixture, "absent", &boolval),
"boolean accessor on a missing key");
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_object_value(fixture, "absent", &objval),
"object accessor on a missing key");
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_array_value(fixture, "absent", &arrayval),
"array accessor on a missing key");
// Wrong types.
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_integer_value(fixture, "name", &intval),
"integer accessor on a string");
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_integer_value(fixture, "ratio", &intval),
"integer accessor on a real");
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_number_value(fixture, "name", &floatval),
"number accessor on a string");
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_boolean_value(fixture, "count", &boolval),
"boolean accessor on an integer");
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_object_value(fixture, "integers", &objval),
"object accessor on an array");
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_value(fixture, "nested", &arrayval),
"array accessor on an object");
// NULL containers.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_integer_value(NULL, "count", &intval),
"integer accessor on a NULL object");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_number_value(NULL, "ratio", &floatval),
"number accessor on a NULL object");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_boolean_value(NULL, "enabled", &boolval),
"boolean accessor on a NULL object");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_object_value(NULL, "nested", &objval),
"object accessor on a NULL object");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_value(NULL, "integers", &arrayval),
"array accessor on a NULL object");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_json_with_default(void)
{
PREPARE_ERROR(e);
int dest = 0;
int defval = 99;
akerr_ErrorContext *keyerr = NULL;
akerr_ErrorContext *typeerr = NULL;
int junk = 0;
ATTEMPT {
// A NULL error means the read succeeded, so the default is not applied.
dest = 1;
TEST_EXPECT_OK(e, akgl_get_json_with_default(NULL, (void *)&defval, (void *)&dest, sizeof(int)),
"with_default on a NULL error");
TEST_ASSERT(e, dest == 1, "with_default overwrote a successful read (dest is now %d)", dest);
// An AKERR_KEY failure substitutes the default.
dest = 1;
keyerr = akgl_get_json_integer_value(fixture, "absent", &junk);
TEST_ASSERT(e, keyerr != NULL, "the missing-key read unexpectedly succeeded");
TEST_EXPECT_OK(e, akgl_get_json_with_default(keyerr, (void *)&defval, (void *)&dest, sizeof(int)),
"with_default on an AKERR_KEY error");
TEST_ASSERT(e, dest == 99, "with_default did not apply the default (dest is %d)", dest);
keyerr = NULL;
// An unrelated failure is not swallowed, so the caller still sees it.
dest = 1;
typeerr = akgl_get_json_integer_value(fixture, "name", &junk);
TEST_ASSERT(e, typeerr != NULL, "the wrong-type read unexpectedly succeeded");
TEST_EXPECT_STATUS(e, AKERR_TYPE,
akgl_get_json_with_default(typeerr, (void *)&defval, (void *)&dest, sizeof(int)),
"with_default must propagate an unrelated error");
TEST_ASSERT(e, dest == 1, "with_default applied the default for an unrelated error");
typeerr = NULL;
// NULL arguments alongside a real error are a contract violation.
keyerr = akgl_get_json_integer_value(fixture, "absent", &junk);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
akgl_get_json_with_default(keyerr, NULL, (void *)&dest, sizeof(int)),
"with_default with a NULL default value");
keyerr = NULL;
keyerr = akgl_get_json_integer_value(fixture, "absent", &junk);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
akgl_get_json_with_default(keyerr, (void *)&defval, NULL, sizeof(int)),
"with_default with a NULL destination");
keyerr = NULL;
} CLEANUP {
if ( keyerr != NULL ) {
keyerr->handled = true;
keyerr = akerr_release_error(keyerr);
}
if ( typeerr != NULL ) {
typeerr->handled = true;
typeerr = akerr_release_error(typeerr);
}
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
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());
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, load_fixture());
CATCH(errctx, test_json_scalar_accessors());
CATCH(errctx, test_json_double_value());
CATCH(errctx, test_json_string_accessor());
CATCH(errctx, test_json_array_index_accessors());
CATCH(errctx, test_json_type_and_key_errors());
CATCH(errctx, test_json_with_default());
} CLEANUP {
if ( fixture != NULL ) {
json_decref(fixture);
}
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -1,928 +0,0 @@
/**
* @file perf.c
* @brief Stress and timing benchmarks for every subsystem that does not need a renderer.
*
* These are not correctness tests. Each one drives a hot path -- the pool scans,
* the registry, the physics sweep, the per-actor update, the geometry helpers,
* the JSON accessors -- hard enough that its per-operation cost is measurable,
* and reports what that cost is. The companion suite in `tests/perf_render.c`
* covers the paths that need a renderer.
*
* The pools are the reason this matters. Every acquire is a linear scan of a
* fixed array looking for a zero reference count, so cost grows with how full
* the pool is, not with how much is being asked for. Several benchmarks below
* are therefore run twice: once against an empty pool, which is the number a
* casual reading of the code predicts, and once against a nearly full one,
* which is the number a game in its fifth minute actually gets.
*
* Everything here runs headless under the dummy drivers, and every benchmark
* rebuilds the state it needs rather than inheriting whatever the previous one
* left behind.
*
* @note SDL's log output is redirected to a sink that discards it. The library
* logs on paths this suite hammers -- akgl_actor_initialize logs every
* spawn -- and timing a write to a terminal or a CTest capture file
* measures the machine's I/O, not libakgl. What the *formatting* costs is
* measured deliberately, by the pair of actor-spawn benchmarks.
*/
#include <SDL3/SDL.h>
#include <string.h>
#include <jansson.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/sprite.h>
#include <akgl/physics.h>
#include <akgl/staticstring.h>
#include <akgl/util.h>
#include <akgl/json_helpers.h>
#include <akgl/tilemap.h>
#include "benchutil.h"
#include "testutil.h"
/** @brief How many actors the frame-shaped benchmarks put on the heap. */
#define BENCH_ACTOR_COUNT AKGL_MAX_HEAP_ACTOR
/** @brief The JSON document the accessor benchmarks read. */
#define BENCH_JSON_FIXTURE "assets/snippets/test_json_helpers.json"
/** @brief A path that exists relative to the CTest working directory, for akgl_path_relative. */
#define BENCH_PATH_FIXTURE "assets/testcharacter.json"
/**
* @brief Print the library's fixed memory footprint.
*
* Not a benchmark, but it belongs in the same report: libakgl does not call
* `malloc`, so every one of these arrays exists from process start whether the
* game uses one slot or all of them, and several of the timings above are
* explained entirely by their size. The string pool is a megabyte of `char`
* arrays, and an akgl_Tilemap is dominated by 16 layers of 512x512 `int` cells
* and 16 tilesets of 65536 offset pairs -- which is why zeroing one costs what
* it costs, and why the header warns against putting one on the stack.
*/
static void bench_report_footprint(void)
{
size_t total = 0;
total = sizeof(HEAP_ACTOR) + sizeof(HEAP_SPRITE) + sizeof(HEAP_SPRITESHEET) +
sizeof(HEAP_CHARACTER) + sizeof(HEAP_STRING);
printf("\n");
printf("static footprint, fixed at compile time:\n");
printf(" %-24s %5d x %7zu = %10zu bytes\n",
"HEAP_ACTOR", AKGL_MAX_HEAP_ACTOR, sizeof(akgl_Actor), sizeof(HEAP_ACTOR));
printf(" %-24s %5d x %7zu = %10zu bytes\n",
"HEAP_SPRITE", AKGL_MAX_HEAP_SPRITE, sizeof(akgl_Sprite), sizeof(HEAP_SPRITE));
printf(" %-24s %5d x %7zu = %10zu bytes\n",
"HEAP_SPRITESHEET", AKGL_MAX_HEAP_SPRITESHEET, sizeof(akgl_SpriteSheet), sizeof(HEAP_SPRITESHEET));
printf(" %-24s %5d x %7zu = %10zu bytes\n",
"HEAP_CHARACTER", AKGL_MAX_HEAP_CHARACTER, sizeof(akgl_Character), sizeof(HEAP_CHARACTER));
printf(" %-24s %5d x %7zu = %10zu bytes\n",
"HEAP_STRING", AKGL_MAX_HEAP_STRING, sizeof(akgl_String), sizeof(HEAP_STRING));
printf(" %-24s %5s %7s %10zu bytes\n", "pools, total", "", "", total);
printf(" %-24s %5d x %7s = %10zu bytes\n",
"akgl_Tilemap", 1, "", sizeof(akgl_Tilemap));
printf(" %-24s %5d x %7zu = %10zu bytes\n",
" of which layers", AKGL_TILEMAP_MAX_LAYERS, sizeof(akgl_TilemapLayer),
(sizeof(akgl_TilemapLayer) * AKGL_TILEMAP_MAX_LAYERS));
printf(" %-24s %5d x %7zu = %10zu bytes\n",
" of which tilesets", AKGL_TILEMAP_MAX_TILESETS, sizeof(akgl_Tileset),
(sizeof(akgl_Tileset) * AKGL_TILEMAP_MAX_TILESETS));
fflush(stdout);
}
/**
* @brief Swallow SDL's log output.
*
* Installed for the whole run. See the note in the file comment: the library
* logs on paths this suite calls hundreds of thousands of times, and the cost of
* writing that out is not a property of libakgl.
*/
static void bench_discard_log(void *userdata, int category, SDL_LogPriority priority, const char *message)
{
return;
}
/**
* @brief Claim a spritesheet slot without loading an image into it.
*
* akgl_spritesheet_initialize uploads a texture and so needs a renderer, which
* this suite deliberately does not have. Nothing here draws, and the only thing
* that reads the sheet is akgl_sprite_initialize storing the pointer, so an
* empty claimed slot is enough.
*/
static akerr_ErrorContext *bench_make_sheet(akgl_SpriteSheet **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
PASS(errctx, akgl_heap_next_spritesheet(dest));
memset(*dest, 0x00, sizeof(akgl_SpriteSheet));
(*dest)->refcount += 1;
SUCCEED_RETURN(errctx);
}
/**
* @brief Build a sprite on a freshly claimed sheet and publish it in the sprite registry.
*
* @param dest Receives the sprite. Required.
* @param name Registry key. Copied at a fixed #AKGL_SPRITE_MAX_NAME_LENGTH
* bytes by akgl_sprite_initialize, so it is staged through a buffer
* of exactly that size first.
* @param speed Nanoseconds one frame is held. 0 makes every akgl_actor_update
* advance the animation, which is the expensive path.
*/
static akerr_ErrorContext *bench_make_sprite(akgl_Sprite **dest, char *name, uint32_t speed)
{
PREPARE_ERROR(errctx);
akgl_SpriteSheet *sheet = NULL;
char namebuf[AKGL_SPRITE_MAX_NAME_LENGTH];
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
memset(&namebuf, 0x00, sizeof(namebuf));
strncpy((char *)&namebuf, name, AKGL_SPRITE_MAX_NAME_LENGTH - 1);
PASS(errctx, bench_make_sheet(&sheet));
PASS(errctx, akgl_heap_next_sprite(dest));
PASS(errctx, akgl_sprite_initialize(*dest, (char *)&namebuf, sheet));
(*dest)->frames = 3;
(*dest)->frameids[0] = 0;
(*dest)->frameids[1] = 1;
(*dest)->frameids[2] = 2;
(*dest)->width = 48;
(*dest)->height = 48;
(*dest)->speed = speed;
(*dest)->loop = true;
SUCCEED_RETURN(errctx);
}
/**
* @brief Build a character with one sprite mapped to the two states these benchmarks use.
*
* State 0 is what akgl_actor_initialize leaves an actor in and what
* akgl_actor_automatic_face leaves it in while it is standing still;
* `MOVING_RIGHT | FACE_RIGHT` is what bench_fill_actor_pool puts it in and what
* the face logic settles on from there. Mapping both keeps the update path off
* the missing-sprite branch, which is measured deliberately elsewhere rather
* than by accident here.
*/
static akerr_ErrorContext *bench_make_character(akgl_Character **dest, char *name, uint32_t spritespeed)
{
PREPARE_ERROR(errctx);
akgl_Sprite *sprite = NULL;
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
PASS(errctx, akgl_heap_next_character(dest));
PASS(errctx, akgl_character_initialize(*dest, name));
(*dest)->sx = 120.0;
(*dest)->sy = 120.0;
(*dest)->sz = 0.0;
(*dest)->ax = 40.0;
(*dest)->ay = 40.0;
(*dest)->speedtime = 16;
PASS(errctx, bench_make_sprite(&sprite, name, spritespeed));
PASS(errctx, (*dest)->sprite_add(*dest, sprite, 0));
PASS(errctx,
(*dest)->sprite_add(
*dest,
sprite,
(AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT))
);
SUCCEED_RETURN(errctx);
}
/**
* @brief Populate the actor pool with @p count live actors bound to @p basechar.
*
* The heap and the actor registry are rebuilt first, so this is the state every
* frame-shaped benchmark starts from rather than something it inherits.
*/
static akerr_ErrorContext *bench_fill_actor_pool(akgl_Character *basechar, int count)
{
PREPARE_ERROR(errctx);
akgl_Actor *actor = NULL;
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
int i = 0;
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "basechar");
PASS(errctx, akgl_heap_init_actor());
PASS(errctx, akgl_registry_init_actor());
for ( i = 0; i < count; i++ ) {
memset(&name, 0x00, sizeof(name));
snprintf((char *)&name, AKGL_ACTOR_MAX_NAME_LENGTH, "benchactor%d", i);
PASS(errctx, akgl_heap_next_actor(&actor));
PASS(errctx, akgl_actor_initialize(actor, (char *)&name));
PASS(errctx, akgl_actor_set_character(actor, basechar->name));
actor->visible = true;
actor->layer = 0;
actor->x = (float32_t)(i * 13);
actor->y = (float32_t)(i * 7);
actor->sx = 120.0;
actor->sy = 120.0;
actor->ax = 40.0;
actor->ay = 40.0;
AKGL_BITMASK_ADD(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time a claim from an empty actor pool against one from a nearly full pool.
*
* The pool scan stops at the first slot with a zero reference count, so an empty
* pool answers from index 0 and a pool with one slot left answers from index
* #AKGL_MAX_HEAP_ACTOR - 1. The ratio between these two numbers *is* the cost of
* the linear-scan allocator.
*/
static akerr_ErrorContext *bench_heap_actor_claim(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_Actor *actor = NULL;
int count = bench_iterations(500000);
int i = 0;
int rep = 0;
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
PASS(errctx, akgl_heap_init_actor());
bench_start("heap_next_actor, empty pool", "call", 100.0);
BENCH_LOOP(inner, i, count, akgl_heap_next_actor(&actor));
bench_stop(count);
PASS(errctx, inner);
for ( i = 0; i < (AKGL_MAX_HEAP_ACTOR - 1); i++ ) {
HEAP_ACTOR[i].refcount = 1;
}
bench_start("heap_next_actor, one slot left", "call", 400.0);
BENCH_LOOP(inner, i, count, akgl_heap_next_actor(&actor));
bench_stop(count);
PASS(errctx, inner);
PASS(errctx, akgl_heap_init_actor());
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time the string pool, which is the one that hurts.
*
* Every entry is #AKGL_MAX_STRING_LENGTH bytes, so 256 of them is a megabyte and
* the scan for a free slot touches one reference count every 4 KiB -- a cache
* miss per candidate. Release then wipes the whole 4 KiB whether the string held
* a path or a single character. Three numbers come out of this: the claim on an
* empty pool, the claim with one slot left, and the release on its own.
*/
static akerr_ErrorContext *bench_heap_string(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_String *str = NULL;
int count = bench_iterations(100000);
int i = 0;
int rep = 0;
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
PASS(errctx, akgl_heap_init());
// Claim and release together: the scratch-buffer idiom the library uses
// everywhere, and the only string benchmark whose cost a caller can
// actually observe from outside.
bench_start("heap string claim + release cycle", "cycle", 500.0);
for ( i = 0; i < count; i++ ) {
inner = akgl_heap_next_string(&str);
if ( inner != NULL ) {
break;
}
inner = akgl_heap_release_string(str);
if ( inner != NULL ) {
break;
}
}
bench_stop(count);
PASS(errctx, inner);
// The claim on its own. The reference the claim takes is dropped by hand
// rather than through release, so the 4 KiB wipe stays out of this one.
bench_start("heap_next_string, empty pool", "call", 100.0);
for ( i = 0; i < count; i++ ) {
inner = akgl_heap_next_string(&str);
if ( inner != NULL ) {
break;
}
str->refcount = 0;
}
bench_stop(count);
PASS(errctx, inner);
// The same claim with 255 of the 256 slots taken: a megabyte walked, one
// reference count read per 4 KiB page.
for ( i = 0; i < (AKGL_MAX_HEAP_STRING - 1); i++ ) {
HEAP_STRING[i].refcount = 1;
}
bench_start("heap_next_string, one slot left", "call", 2500.0);
for ( i = 0; i < count; i++ ) {
inner = akgl_heap_next_string(&str);
if ( inner != NULL ) {
break;
}
str->refcount = 0;
}
bench_stop(count);
PASS(errctx, inner);
PASS(errctx, akgl_heap_init());
// Release on its own: one 4 KiB memset per call, whatever the string held.
PASS(errctx, akgl_heap_next_string(&str));
bench_start("heap_release_string, 4 KiB wipe", "call", 500.0);
for ( i = 0; i < count; i++ ) {
str->refcount = 1;
inner = akgl_heap_release_string(str);
if ( inner != NULL ) {
break;
}
}
bench_stop(count);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time a full reset of every pool.
*
* akgl_heap_init zeroes all five arrays -- better than a megabyte, dominated by
* the string pool. A game pays this once at startup and again on every call to
* akgl_game_init; a level transition that calls it per level pays it per level.
*/
static akerr_ErrorContext *bench_heap_init(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
int count = bench_iterations(2000);
int i = 0;
int rep = 0;
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("heap_init, all five pools", "call", 450000.0);
BENCH_LOOP(inner, i, count, akgl_heap_init());
bench_stop(count);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time an actor spawn, with and without the log line the library writes.
*
* A spawn is a pool claim, a `memset` of the whole actor, a registry insert, and
* an `SDL_Log`. The second run raises the log priority so SDL returns before it
* formats anything; the gap between the two numbers is what the log line costs
* every caller, including the ones that never read it.
*/
static akerr_ErrorContext *bench_actor_spawn(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_Actor *actor = NULL;
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
int count = bench_iterations(20000);
int i = 0;
int rep = 0;
memset(&name, 0x00, sizeof(name));
strncpy((char *)&name, "benchspawn", AKGL_ACTOR_MAX_NAME_LENGTH - 1);
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
PASS(errctx, akgl_heap_init_actor());
PASS(errctx, akgl_registry_init_actor());
bench_start("actor spawn + release, library logging on", "actor", 2100.0);
for ( i = 0; i < count; i++ ) {
inner = akgl_heap_next_actor(&actor);
if ( inner != NULL ) {
break;
}
inner = akgl_actor_initialize(actor, (char *)&name);
if ( inner != NULL ) {
break;
}
inner = akgl_heap_release_actor(actor);
if ( inner != NULL ) {
break;
}
}
bench_stop(count);
PASS(errctx, inner);
SDL_SetLogPriorities(SDL_LOG_PRIORITY_CRITICAL);
bench_start("actor spawn + release, logging suppressed", "actor", 1700.0);
for ( i = 0; i < count; i++ ) {
inner = akgl_heap_next_actor(&actor);
if ( inner != NULL ) {
break;
}
inner = akgl_actor_initialize(actor, (char *)&name);
if ( inner != NULL ) {
break;
}
inner = akgl_heap_release_actor(actor);
if ( inner != NULL ) {
break;
}
}
bench_stop(count);
SDL_SetLogPriorities(SDL_LOG_PRIORITY_INFO);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time a registry lookup with the actor registry full.
*
* akgl_actor_set_character is the library's own name-to-pointer lookup: an SDL
* property fetch against a registry holding #AKGL_MAX_HEAP_ACTOR entries, plus
* the four field copies that follow it. Every actor built from a tilemap object
* layer pays it once.
*/
static akerr_ErrorContext *bench_registry_lookup(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_Character *basechar = NULL;
akgl_Actor *actor = NULL;
int count = bench_iterations(200000);
int i = 0;
int rep = 0;
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_registry_init());
PASS(errctx, bench_make_character(&basechar, "benchlookupchar", 0));
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
actor = &HEAP_ACTOR[0];
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("actor_set_character, registry lookup", "call", 400.0);
BENCH_LOOP(inner, i, count, akgl_actor_set_character(actor, basechar->name));
bench_stop(count);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time the property store a game reads its configuration out of.
*
* akgl_get_property copies a fixed #AKGL_MAX_STRING_LENGTH bytes out of the
* property store regardless of how long the value is, so reading `"0.0"` moves
* 4 KiB. akgl_physics_init_arcade makes six of these calls.
*/
static akerr_ErrorContext *bench_properties(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_String *value = NULL;
int count = bench_iterations(200000);
int i = 0;
int rep = 0;
PASS(errctx, akgl_registry_init_properties());
PASS(errctx, akgl_set_property("bench.property", "a short value"));
PASS(errctx, akgl_heap_next_string(&value));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("set_property", "call", 1000.0);
BENCH_LOOP(inner, i, count, akgl_set_property("bench.property", "a short value"));
bench_stop(count);
PASS(errctx, inner);
bench_start("get_property, 4 KiB copy", "call", 900.0);
BENCH_LOOP(inner, i, count, akgl_get_property("bench.property", &value, "unset"));
bench_stop(count);
PASS(errctx, inner);
}
PASS(errctx, akgl_heap_release_string(value));
SUCCEED_RETURN(errctx);
}
/**
* @brief Time the state-to-sprite lookup that runs twice per actor per frame.
*
* akgl_character_sprite_get renders the state bitmask to decimal with `SDL_itoa`
* and looks the result up as a property string. akgl_actor_update calls it, and
* so does akgl_actor_render, so a 64-actor frame makes 128 of these calls before
* anything is drawn.
*/
static akerr_ErrorContext *bench_character_sprite_get(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_Character *basechar = NULL;
akgl_Sprite *sprite = NULL;
int count = bench_iterations(200000);
int i = 0;
int rep = 0;
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_registry_init());
PASS(errctx, bench_make_character(&basechar, "benchspritechar", 0));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("character_sprite_get, state to sprite", "call", 400.0);
BENCH_LOOP(inner, i, count, basechar->sprite_get(basechar, 0, &sprite));
bench_stop(count);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time one actor's logic update, with the animation advancing every call.
*
* The sprite's frame time is 0, so every update takes the branch that changes
* frame -- the expensive path, and the one a fast animation actually takes. An
* update is a face recalculation, a sprite lookup, a clock read, and the frame
* arithmetic.
*/
static akerr_ErrorContext *bench_actor_update(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_Character *basechar = NULL;
akgl_Actor *actor = NULL;
int count = bench_iterations(200000);
int i = 0;
int rep = 0;
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_registry_init());
PASS(errctx, bench_make_character(&basechar, "benchupdatechar", 0));
PASS(errctx, bench_fill_actor_pool(basechar, 1));
actor = &HEAP_ACTOR[0];
AKGL_BITMASK_CLEAR(actor->state);
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("actor_update, animation advancing", "actor", 700.0);
BENCH_LOOP(inner, i, count, actor->updatefunc(actor));
bench_stop(count);
PASS(errctx, inner);
// An actor in a state its character has no sprite for is not an error
// the library refuses -- akgl_actor_update handles AKERR_KEY and carries
// on, and akgl_actor_render logs it and draws nothing. It is a normal
// condition on a partly authored character, and it is worth knowing what
// one costs, because raising, formatting, and handling a context is far
// more work than the update it replaces.
AKGL_BITMASK_ADD(actor->state, AKGL_ACTOR_STATE_MOVING_UP);
bench_start("actor_update, no sprite for state", "actor", 6200.0);
BENCH_LOOP(inner, i, count, actor->updatefunc(actor));
bench_stop(count);
PASS(errctx, inner);
AKGL_BITMASK_CLEAR(actor->state);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time the physics sweep over a full actor pool, and over an empty one.
*
* akgl_physics_simulate walks all #AKGL_MAX_HEAP_ACTOR slots whether or not
* anything is in them, so the empty-pool number is the floor every frame pays.
* The full-pool number is one frame of movement for 64 actors: movement logic,
* gravity, drag, clamp, and integrate, per actor.
*/
static akerr_ErrorContext *bench_physics_simulate(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_Character *basechar = NULL;
akgl_PhysicsBackend backend;
int count = bench_iterations(20000);
int i = 0;
int rep = 0;
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_registry_init());
PASS(errctx, akgl_registry_init_properties());
PASS(errctx, bench_make_character(&basechar, "benchphysicschar", 0));
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
PASS(errctx, akgl_physics_init_arcade(&backend));
backend.gravity_y = 9.8;
backend.drag_x = 0.1;
backend.drag_y = 0.1;
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("physics_simulate, 64 live actors", "frame", 12500.0);
BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
bench_stop(count);
PASS(errctx, inner);
}
PASS(errctx, akgl_heap_init_actor());
PASS(errctx, akgl_registry_init_actor());
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("physics_simulate, empty pool", "frame", 650.0);
BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
bench_stop(count);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time the logic half of a frame: update every actor, then simulate.
*
* This is what a host's main loop does before it draws anything, and it is the
* number to subtract from a 16.6 ms frame to find out what is left for drawing.
* Note that it is *not* what akgl_game_update does: that one runs the update
* loop once per tilemap layer, which is measured in the render suite.
*/
static akerr_ErrorContext *bench_logic_frame(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_Character *basechar = NULL;
akgl_Actor *actor = NULL;
akgl_PhysicsBackend backend;
int count = bench_iterations(5000);
int i = 0;
int j = 0;
int rep = 0;
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_registry_init());
PASS(errctx, akgl_registry_init_properties());
PASS(errctx, bench_make_character(&basechar, "benchframechar", 0));
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
PASS(errctx, akgl_physics_init_arcade(&backend));
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("logic frame, 64 actors updated + simulated", "frame", 60000.0);
for ( i = 0; i < count; i++ ) {
for ( j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
actor = &HEAP_ACTOR[j];
if ( actor->refcount == 0 ) {
continue;
}
inner = actor->updatefunc(actor);
if ( inner != NULL ) {
break;
}
}
if ( inner != NULL ) {
break;
}
inner = backend.simulate(&backend, NULL);
if ( inner != NULL ) {
break;
}
}
bench_stop(count);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time the geometry helpers, including the all-pairs sweep a caller has to write.
*
* akgl_collide_rectangles tests eight corners and returns at the first hit, so
* an overlap is cheap and a miss is the full eight. The third benchmark is the
* broad phase the library does not provide: 64 actors is 2016 pairs, and a
* caller doing collision at all does that every frame.
*/
static akerr_ErrorContext *bench_geometry(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
RectanglePoints points;
SDL_FRect rects[BENCH_ACTOR_COUNT];
SDL_FRect overlapping = { .x = 8.0, .y = 8.0, .w = 32.0, .h = 32.0 };
SDL_FRect disjoint = { .x = 900.0, .y = 900.0, .w = 32.0, .h = 32.0 };
SDL_FRect subject = { .x = 0.0, .y = 0.0, .w = 32.0, .h = 32.0 };
bool collide = false;
int count = bench_iterations(500000);
int sweeps = bench_iterations(2000);
int pairs = 0;
int i = 0;
int j = 0;
int k = 0;
int rep = 0;
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
rects[i].x = (float32_t)(i * 24);
rects[i].y = (float32_t)(i * 18);
rects[i].w = 32.0;
rects[i].h = 32.0;
}
pairs = (BENCH_ACTOR_COUNT * (BENCH_ACTOR_COUNT - 1)) / 2;
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("rectangle_points", "call", 100.0);
BENCH_LOOP(inner, i, count, akgl_rectangle_points(&points, &subject));
bench_stop(count);
PASS(errctx, inner);
bench_start("collide_rectangles, overlapping", "call", 300.0);
BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &overlapping, &collide));
bench_stop(count);
PASS(errctx, inner);
bench_start("collide_rectangles, disjoint", "call", 600.0);
BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &disjoint, &collide));
bench_stop(count);
PASS(errctx, inner);
bench_start("all-pairs collision sweep, 64 actors", "sweep", 1200000.0);
for ( k = 0; k < sweeps; k++ ) {
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
for ( j = i + 1; j < BENCH_ACTOR_COUNT; j++ ) {
inner = akgl_collide_rectangles(&rects[i], &rects[j], &collide);
if ( inner != NULL ) {
break;
}
}
}
if ( inner != NULL ) {
break;
}
}
bench_stop(sweeps);
PASS(errctx, inner);
}
printf("all-pairs collision sweep covers %d pairs per sweep\n", pairs);
SUCCEED_RETURN(errctx);
}
/**
* @brief Time the pooled-string operations.
*
* Both of these move #AKGL_MAX_STRING_LENGTH bytes by default -- a copy with a
* count of 0 means "all of it" -- so the cost is the same for a filename and for
* a single character.
*/
static akerr_ErrorContext *bench_strings(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_String *src = NULL;
akgl_String *dst = NULL;
int count = bench_iterations(200000);
int i = 0;
int rep = 0;
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_heap_next_string(&src));
PASS(errctx, akgl_heap_next_string(&dst));
PASS(errctx, akgl_string_initialize(src, "assets/testcharacter.json"));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("string_initialize", "call", 350.0);
BENCH_LOOP(inner, i, count, akgl_string_initialize(dst, "assets/testcharacter.json"));
bench_stop(count);
PASS(errctx, inner);
bench_start("string_copy, full length", "call", 350.0);
BENCH_LOOP(inner, i, count, akgl_string_copy(src, dst, 0));
bench_stop(count);
PASS(errctx, inner);
}
PASS(errctx, akgl_heap_release_string(src));
PASS(errctx, akgl_heap_release_string(dst));
SUCCEED_RETURN(errctx);
}
/**
* @brief Time parsing a document and reading values back out of it.
*
* The parse is what an asset load pays per file. The accessors are what the
* tilemap and character loaders pay per field, and every one of them that
* returns a string claims and fills a 4 KiB pooled string.
*/
static akerr_ErrorContext *bench_json(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_String *path = NULL;
akgl_String *value = NULL;
json_t *doc = NULL;
json_error_t jsonerr;
int number = 0;
int loads = bench_iterations(2000);
int count = bench_iterations(200000);
int i = 0;
int rep = 0;
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_heap_next_string(&path));
PASS(errctx, akgl_heap_next_string(&value));
snprintf((char *)&path->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), BENCH_JSON_FIXTURE);
doc = json_load_file((char *)&path->data, 0, &jsonerr);
FAIL_ZERO_RETURN(errctx, doc, AKERR_IO, "Unable to load %s: %s", (char *)&path->data, jsonerr.text);
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("json_load_file, small document", "load", 120000.0);
for ( i = 0; i < loads; i++ ) {
json_t *scratch = json_load_file((char *)&path->data, 0, &jsonerr);
if ( scratch == NULL ) {
break;
}
json_decref(scratch);
}
bench_stop(loads);
bench_start("get_json_string_value", "call", 450.0);
BENCH_LOOP(inner, i, count, akgl_get_json_string_value(doc, "name", &value));
bench_stop(count);
PASS(errctx, inner);
bench_start("get_json_integer_value", "call", 150.0);
BENCH_LOOP(inner, i, count, akgl_get_json_integer_value(doc, "count", &number));
bench_stop(count);
PASS(errctx, inner);
}
json_decref(doc);
PASS(errctx, akgl_heap_release_string(path));
PASS(errctx, akgl_heap_release_string(value));
SUCCEED_RETURN(errctx);
}
/**
* @brief Time path resolution, which every asset reference goes through.
*
* akgl_path_relative calls `realpath(3)`, so this is a syscall and a walk of the
* filesystem, per reference, at load time. A tilemap naming twenty tilesets and
* sprites pays it twenty times.
*/
static akerr_ErrorContext *bench_path_relative(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_String *dst = NULL;
int count = bench_iterations(20000);
int i = 0;
int rep = 0;
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_heap_next_string(&dst));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("path_relative, realpath on an existing file", "call", 36000.0);
BENCH_LOOP(inner, i, count, akgl_path_relative(".", BENCH_PATH_FIXTURE, dst));
bench_stop(count);
PASS(errctx, inner);
}
PASS(errctx, akgl_heap_release_string(dst));
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
SDL_SetLogOutputFunction(bench_discard_log, NULL);
ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_registry_init_properties());
CATCH(errctx, bench_heap_actor_claim());
CATCH(errctx, bench_heap_string());
CATCH(errctx, bench_heap_init());
CATCH(errctx, bench_actor_spawn());
CATCH(errctx, bench_registry_lookup());
CATCH(errctx, bench_properties());
CATCH(errctx, bench_character_sprite_get());
CATCH(errctx, bench_actor_update());
CATCH(errctx, bench_physics_simulate());
CATCH(errctx, bench_logic_frame());
CATCH(errctx, bench_geometry());
CATCH(errctx, bench_strings());
CATCH(errctx, bench_json());
CATCH(errctx, bench_path_relative());
bench_report_footprint();
BENCH_REPORT_BREAK(errctx);
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -1,837 +0,0 @@
/**
* @file perf_render.c
* @brief Stress and timing benchmarks for the paths that need a renderer.
*
* The companion to `tests/perf.c`, which covers everything that does not draw.
* Everything here runs against a software renderer under the dummy video driver
* at #BENCH_SCREEN_W x #BENCH_SCREEN_H -- no display, no window shown, no GPU.
*
* A software renderer is the right instrument for this even though no shipped
* game uses one. What these benchmarks are measuring is the work libakgl does
* *per drawn thing* -- how many blits a screenful of tiles turns into, how many
* lookups an actor costs before its texture moves, what a line of HUD text
* allocates and throws away every frame. A software renderer keeps that work in
* the same process where it can be timed, and its per-blit cost is at least
* honest about how much pixel traffic was asked for. Read these numbers as
* *counts of work*, not as frame rates a real game would see.
*
* The tilemap benchmarks build their map by hand rather than loading the 2x2
* fixture. A 2x2 map measures nothing: the point of the exercise is a full
* screen of tiles -- #BENCH_MAP_W x #BENCH_MAP_H cells at #BENCH_TILE_SIZE
* pixels, of which the camera covers 40 x 30 -- and the second variant repeats
* it with eight tilesets, because akgl_tilemap_draw scans every tileset for
* every tile and never stops at the one that matched.
*/
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/sprite.h>
#include <akgl/physics.h>
#include <akgl/renderer.h>
#include <akgl/draw.h>
#include <akgl/text.h>
#include <akgl/tilemap.h>
#include <akgl/staticstring.h>
#include "benchutil.h"
#include "testutil.h"
/** @brief Width of the offscreen render target every benchmark draws into. */
#define BENCH_SCREEN_W 640
/** @brief Height of that target. */
#define BENCH_SCREEN_H 480
/** @brief Edge of one cell in the synthetic map, in pixels. */
#define BENCH_TILE_SIZE 16
/** @brief Width of the synthetic map, in tiles. Bigger than the camera on purpose. */
#define BENCH_MAP_W 128
/** @brief Height of the synthetic map, in tiles. */
#define BENCH_MAP_H 128
/** @brief Tilesets in the many-tileset variant of the draw benchmark. */
#define BENCH_MAP_TILESETS 8
/** @brief Actors the scene-wide benchmarks put on the heap. */
#define BENCH_ACTOR_COUNT AKGL_MAX_HEAP_ACTOR
/** @brief The tileset image the synthetic map cuts its tiles from: 768x576, 48x36 tiles. */
#define BENCH_TILESET_IMAGE "assets/World_A1.png"
/** @brief The font the text benchmarks rasterize with. */
#define BENCH_FONT_PATH "assets/akgl_test_mono.ttf"
/** @brief Point size for that font. */
#define BENCH_FONT_SIZE 16
/** @brief A representative line of HUD text: a score readout. */
#define BENCH_TEXT "SCORE 000123456"
/** @brief The font opened once in main and used by the text benchmarks. */
static TTF_Font *benchfont = NULL;
/** @brief The tileset texture the synthetic map borrows. Destroyed in main. */
static SDL_Texture *benchtiles = NULL;
/**
* @brief Stop the clock, but only after the renderer has done what it was asked.
*
* SDL batches. A `draw_*` call queues a command and returns, and the queue is
* not executed until something forces it: a present, a readback, or the
* destruction of a texture it refers to. Without a flush inside the timed
* region, every drawing benchmark here measures the cost of *queueing* work and
* none of the cost of doing it -- and the bill arrives at teardown, where
* SDL_DestroyTexture on the tileset spent nearly two minutes executing blits
* that six earlier benchmarks had queued and been given credit for not doing.
*
* So the flush goes inside the measurement: what these benchmarks report is N
* calls plus the drawing those N calls asked for, which is the number a caller
* actually pays over a frame.
*/
#define BENCH_FLUSH_STOP(e, ops) \
{ \
bool __bench_flushed = SDL_FlushRenderer(renderer->sdl_renderer); \
bench_stop(ops); \
FAIL_ZERO_RETURN(e, __bench_flushed, AKGL_ERR_SDL, "SDL_FlushRenderer: %s", SDL_GetError()); \
}
/**
* @brief Swallow SDL's log output. See the same function in tests/perf.c.
*/
static void bench_discard_log(void *userdata, int category, SDL_LogPriority priority, const char *message)
{
return;
}
/**
* @brief Build a map by hand: one tile layer, @p tilesets tilesets, every cell filled.
*
* The cells all draw from the *last* tileset, which is the worst case for
* akgl_tilemap_draw's inner scan and the reason the many-tileset variant exists.
* Global ids are laid out so that tileset k owns [1 + k*tilecount, ...], which
* keeps each cell matching exactly one tileset -- the draw loop has no `break`,
* so overlapping ranges would blit the same cell more than once and measure
* something that cannot happen with a map Tiled produced.
*
* @param map The tilemap to fill in. Required. Zeroed first.
* @param tilesets How many tilesets to declare, all sharing one texture.
*/
static akerr_ErrorContext *bench_build_map(akgl_Tilemap *map, int tilesets)
{
PREPARE_ERROR(errctx);
int columns = 0;
int rows = 0;
int tilecount = 0;
int lastgid = 0;
int i = 0;
int cell = 0;
FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "map");
FAIL_ZERO_RETURN(errctx, benchtiles, AKERR_NULLPOINTER, "benchtiles");
FAIL_NONZERO_RETURN(
errctx,
((tilesets < 1) || (tilesets > AKGL_TILEMAP_MAX_TILESETS)),
AKERR_OUTOFBOUNDS,
"%d tilesets is outside 1..%d",
tilesets,
AKGL_TILEMAP_MAX_TILESETS);
memset(map, 0x00, sizeof(akgl_Tilemap));
columns = benchtiles->w / BENCH_TILE_SIZE;
rows = benchtiles->h / BENCH_TILE_SIZE;
tilecount = columns * rows;
map->tilewidth = BENCH_TILE_SIZE;
map->tileheight = BENCH_TILE_SIZE;
map->width = BENCH_MAP_W;
map->height = BENCH_MAP_H;
map->numlayers = 1;
map->numtilesets = tilesets;
for ( i = 0; i < tilesets; i++ ) {
map->tilesets[i].firstgid = 1 + (i * tilecount);
map->tilesets[i].tilecount = tilecount;
map->tilesets[i].columns = columns;
map->tilesets[i].tilewidth = BENCH_TILE_SIZE;
map->tilesets[i].tileheight = BENCH_TILE_SIZE;
map->tilesets[i].imagewidth = benchtiles->w;
map->tilesets[i].imageheight = benchtiles->h;
map->tilesets[i].texture = benchtiles;
snprintf((char *)&map->tilesets[i].name, AKGL_TILEMAP_MAX_TILESET_NAME_SIZE, "benchtileset%d", i);
PASS(errctx, akgl_tilemap_compute_tileset_offsets(map, i));
}
lastgid = map->tilesets[tilesets - 1].firstgid;
map->layers[0].type = AKGL_TILEMAP_LAYER_TYPE_TILES;
map->layers[0].width = BENCH_MAP_W;
map->layers[0].height = BENCH_MAP_H;
map->layers[0].visible = true;
map->layers[0].opacity = 1.0;
for ( i = 0; i < (BENCH_MAP_W * BENCH_MAP_H); i++ ) {
cell = i % tilecount;
map->layers[0].data[i] = lastgid + cell;
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Load the test sprite and character, then fill the actor pool with actors wearing it.
*
* These are the real assets, loaded through the real loaders, so the actors have
* a texture-backed sprite and akgl_actor_render takes its full path rather than
* bailing out at the missing-sprite branch. Facing is pinned by turning
* `movement_controls_face` off: the automatic face logic clears every facing bit
* for an actor that is not moving, and the fixture character maps its sprite to
* `ALIVE | FACE_LEFT`.
*/
static akerr_ErrorContext *bench_populate_scene(void)
{
PREPARE_ERROR(errctx);
akgl_Actor *actor = NULL;
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
int i = 0;
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_registry_init());
PASS(errctx, akgl_sprite_load_json("assets/testsprite.json"));
PASS(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
PASS(errctx, akgl_character_load_json("assets/testcharacter.json"));
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
memset(&name, 0x00, sizeof(name));
snprintf((char *)&name, AKGL_ACTOR_MAX_NAME_LENGTH, "benchactor%d", i);
PASS(errctx, akgl_heap_next_actor(&actor));
PASS(errctx, akgl_actor_initialize(actor, (char *)&name));
PASS(errctx, akgl_actor_set_character(actor, "testcharacter"));
actor->visible = true;
actor->layer = 0;
actor->movement_controls_face = false;
actor->x = (float32_t)((i * 37) % BENCH_SCREEN_W);
actor->y = (float32_t)((i * 23) % BENCH_SCREEN_H);
actor->curSpriteFrameId = 0;
AKGL_BITMASK_ADD(actor->state, (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT));
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time the empty frame: clear the target and present it.
*
* Everything else a frame does is on top of this. It is also the only benchmark
* here whose cost is entirely SDL's rather than libakgl's, which makes it the
* baseline the rest are worth reading against.
*/
static akerr_ErrorContext *bench_frame(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
int count = bench_iterations(5000);
int i = 0;
int rep = 0;
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("frame_start + frame_end, 640x480", "frame", 230000.0);
for ( i = 0; i < count; i++ ) {
inner = renderer->frame_start(renderer);
if ( inner != NULL ) {
break;
}
inner = renderer->frame_end(renderer);
if ( inner != NULL ) {
break;
}
}
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time the immediate-mode primitives.
*
* Each of these saves the renderer's draw colour, sets its own, draws, and puts
* the colour back, so the fixed cost per call is three SDL round trips whatever
* the shape. The shapes are sized like something a game would actually draw: a
* full-diagonal line, a dialogue-box-sized rectangle, a 64 pixel radius circle.
*/
static akerr_ErrorContext *bench_primitives(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
SDL_Color red = { 0xff, 0x00, 0x00, 0xff };
SDL_FRect rect = { .x = 32.0, .y = 32.0, .w = 200.0, .h = 150.0 };
int count = bench_iterations(100000);
int filled = bench_iterations(2000);
int circles = bench_iterations(20000);
int i = 0;
int rep = 0;
PASS(errctx, renderer->frame_start(renderer));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("draw_point", "call", 1400.0);
BENCH_LOOP(inner, i, count, akgl_draw_point(renderer, 320.0, 240.0, red));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
bench_start("draw_line, screen diagonal", "call", 6200.0);
BENCH_LOOP(inner, i, count, akgl_draw_line(renderer, 0.0, 0.0, 639.0, 479.0, red));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
bench_start("draw_rect, 200x150 outline", "call", 5000.0);
BENCH_LOOP(inner, i, count, akgl_draw_rect(renderer, &rect, red));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
bench_start("draw_filled_rect, 200x150", "call", 440000.0);
BENCH_LOOP(inner, i, filled, akgl_draw_filled_rect(renderer, &rect, red));
BENCH_FLUSH_STOP(errctx, filled);
PASS(errctx, inner);
bench_start("draw_circle, radius 64", "call", 25000.0);
BENCH_LOOP(inner, i, circles, akgl_draw_circle(renderer, 320.0, 240.0, 64.0, red));
BENCH_FLUSH_STOP(errctx, circles);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time the framebuffer round trips: save a region, put it back, flood fill.
*
* These are the expensive ones by construction. A copy reads pixels back off the
* render target; a paste uploads a texture, draws it, and destroys it again; and
* a flood fill reads back the *entire* target, converts the whole surface to
* RGBA32, walks it, uploads a patch, and destroys everything. That is a
* megabyte and a half of traffic for one fill at 640x480, whatever the size of
* the region that actually changed.
*
* The fill alternates colours because a fill whose seed pixel already carries
* the requested colour returns immediately, and would otherwise measure the
* early exit rather than the fill.
*/
static akerr_ErrorContext *bench_framebuffer(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
SDL_Surface *saved = NULL;
SDL_Rect region = { .x = 64, .y = 64, .w = 64, .h = 64 };
SDL_Color red = { 0xff, 0x00, 0x00, 0xff };
SDL_Color blue = { 0x00, 0x00, 0xff, 0xff };
int count = bench_iterations(5000);
int fills = bench_iterations(200);
int i = 0;
int rep = 0;
PASS(errctx, renderer->frame_start(renderer));
PASS(errctx, akgl_draw_copy_region(renderer, &region, &saved));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("draw_copy_region, 64x64 readback", "call", 10000.0);
for ( i = 0; i < count; i++ ) {
SDL_Surface *scratch = NULL;
inner = akgl_draw_copy_region(renderer, &region, &scratch);
if ( inner != NULL ) {
break;
}
SDL_DestroySurface(scratch);
}
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
bench_start("draw_paste_region, 64x64 upload", "call", 55000.0);
BENCH_LOOP(inner, i, count, akgl_draw_paste_region(renderer, saved, 64.0, 64.0));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
bench_start("draw_flood_fill, full 640x480 target", "call", 20000000.0);
for ( i = 0; i < fills; i++ ) {
inner = akgl_draw_flood_fill(renderer, 320, 240, ( (i % 2) == 0 ) ? red : blue);
if ( inner != NULL ) {
break;
}
}
BENCH_FLUSH_STOP(errctx, fills);
PASS(errctx, inner);
PASS(errctx, renderer->frame_start(renderer));
}
SDL_DestroySurface(saved);
SUCCEED_RETURN(errctx);
}
/**
* @brief Time measuring and drawing one line of text.
*
* akgl_text_rendertextat rasterizes the string, uploads it as a texture, blits
* it, and destroys the texture again -- every call, with no cache anywhere. A
* HUD with six readouts pays this six times a frame for text that changes once a
* second.
*/
static akerr_ErrorContext *bench_text(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
SDL_Color white = { 0xff, 0xff, 0xff, 0xff };
int w = 0;
int h = 0;
int count = bench_iterations(20000);
int lines = bench_iterations(5000);
int i = 0;
int rep = 0;
PASS(errctx, renderer->frame_start(renderer));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("text_measure, 15 characters", "call", 400.0);
BENCH_LOOP(inner, i, count, akgl_text_measure(benchfont, BENCH_TEXT, &w, &h));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
bench_start("text_rendertextat, 15 characters", "call", 130000.0);
BENCH_LOOP(inner, i, lines, akgl_text_rendertextat(benchfont, BENCH_TEXT, white, 0, 8, 8));
BENCH_FLUSH_STOP(errctx, lines);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time loading assets off disk: a sprite, a character, and a whole map.
*
* This is startup and level-transition cost, not frame cost, and it is measured
* because it is the part of a game the player waits through. The sprite load
* after the first reuses the already registered spritesheet, so what is measured
* is the JSON path rather than the image decode.
*
* @note Each character load leaks the SDL property set holding its
* state-to-sprite map -- akgl_heap_release_character does not walk it.
* That is a known defect rather than something this benchmark introduces;
* it is repeated here often enough to be worth saying out loud.
*/
static akerr_ErrorContext *bench_asset_load(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_Sprite *sprite = NULL;
akgl_Character *basechar = NULL;
int count = bench_iterations(2000);
int maps = bench_iterations(50);
int i = 0;
int j = 0;
int rep = 0;
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
PASS(errctx, bench_populate_scene());
bench_start("sprite_load_json, sheet already loaded", "load", 180000.0);
for ( i = 0; i < count; i++ ) {
inner = akgl_sprite_load_json("assets/testsprite.json");
if ( inner != NULL ) {
break;
}
sprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
if ( sprite == NULL ) {
break;
}
inner = akgl_heap_release_sprite(sprite);
if ( inner != NULL ) {
break;
}
}
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
PASS(errctx, bench_populate_scene());
bench_start("character_load_json, two sprite mappings", "load", 150000.0);
for ( i = 0; i < count; i++ ) {
inner = akgl_character_load_json("assets/testcharacter.json");
if ( inner != NULL ) {
break;
}
basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL);
if ( basechar == NULL ) {
break;
}
inner = akgl_heap_release_character(basechar);
if ( inner != NULL ) {
break;
}
}
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
PASS(errctx, bench_populate_scene());
bench_start("tilemap_load + release, fixture map", "load", 120000000.0);
for ( i = 0; i < maps; i++ ) {
// The map's object layer spawns actors into the pool, and neither
// akgl_tilemap_release nor anything else gives them back, so a loop
// that only loaded and released would exhaust the actor pool on its
// 32nd iteration. Clearing the pool is part of what a host does
// between levels anyway, and it is a pair of memsets against a map
// load that decodes a 768x576 PNG.
inner = akgl_heap_init_actor();
if ( inner != NULL ) {
break;
}
inner = akgl_registry_init_actor();
if ( inner != NULL ) {
break;
}
// And the string pool has to be reclaimed by hand, because a map
// load leaks five pooled strings that release does not give back.
// The 52nd load would find the pool empty, and what happens then is
// not an AKGL_ERR_HEAP: akgl_get_json_string_value finishes with
// pass-up false, swallows the failed claim, and dereferences the
// pointer it never set. Both defects are filed in TODO.md; this loop
// works around the first so it never reaches the second.
for ( j = 0; j < AKGL_MAX_HEAP_STRING; j++ ) {
HEAP_STRING[j].refcount = 0;
}
inner = akgl_tilemap_load("assets/testmap.tmj", gamemap);
if ( inner != NULL ) {
break;
}
inner = akgl_tilemap_release(gamemap);
if ( inner != NULL ) {
break;
}
}
BENCH_FLUSH_STOP(errctx, maps);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time zeroing an akgl_Tilemap, which is the first thing a map load does.
*
* akgl_tilemap_load begins with `memset(dest, 0, sizeof(akgl_Tilemap))`, and an
* akgl_Tilemap is 16 layers of 512x512 cells plus 16 tilesets of 65536 offset
* pairs. The struct is that size whether the map is 2x2 or 512x512, so this is a
* fixed toll on every level transition, paid before a single byte of the map
* file has been read. Measuring it separately is the only way to tell how much
* of the load benchmark above is the map and how much is the container.
*/
static akerr_ErrorContext *bench_map_zero(void)
{
PREPARE_ERROR(errctx);
int count = bench_iterations(200);
int i = 0;
int rep = 0;
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("zeroing one akgl_Tilemap", "call", 16000000.0);
for ( i = 0; i < count; i++ ) {
memset(gamemap, 0x00, sizeof(akgl_Tilemap));
}
BENCH_FLUSH_STOP(errctx, count);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time building a tileset's offset table.
*
* akgl_tilemap_compute_tileset_offsets fills one entry per tile in the image --
* 1728 of them for the fixture tileset -- and a map pays it once per tileset at
* load time. It is also the function whose table costs 512 KB per tileset,
* which is the reason an akgl_Tilemap is as large as it is.
*/
static akerr_ErrorContext *bench_tileset_offsets(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
int count = bench_iterations(20000);
int i = 0;
int rep = 0;
PASS(errctx, bench_build_map(gamemap, 1));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("tilemap_compute_tileset_offsets, 1728 tiles", "call", 23000.0);
BENCH_LOOP(inner, i, count, akgl_tilemap_compute_tileset_offsets(gamemap, 0));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time drawing a screenful of tiles, with one tileset and with eight.
*
* The camera covers 40 x 30 cells, so each of these is 1200 blits plus whatever
* the tileset scan costs on top. The gap between the two numbers is that scan:
* akgl_tilemap_draw walks every tileset for every tile and does not stop at the
* one that matched, so the cost is linear in tileset count even though at most
* one can ever answer.
*/
static akerr_ErrorContext *bench_tilemap_draw(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
int count = bench_iterations(30);
int i = 0;
int rep = 0;
PASS(errctx, renderer->frame_start(renderer));
PASS(errctx, bench_build_map(gamemap, 1));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("tilemap_draw, 40x30 tiles, 1 tileset", "frame", 170000000.0);
BENCH_LOOP(inner, i, count, akgl_tilemap_draw(gamemap, camera, 0));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
}
PASS(errctx, bench_build_map(gamemap, BENCH_MAP_TILESETS));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("tilemap_draw, 40x30 tiles, 8 tilesets", "frame", 170000000.0);
BENCH_LOOP(inner, i, count, akgl_tilemap_draw(gamemap, camera, 0));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
}
printf("tilemap_draw covers %d x %d = %d tiles per frame\n",
(BENCH_SCREEN_W / BENCH_TILE_SIZE),
(BENCH_SCREEN_H / BENCH_TILE_SIZE),
((BENCH_SCREEN_W / BENCH_TILE_SIZE) * (BENCH_SCREEN_H / BENCH_TILE_SIZE)));
SUCCEED_RETURN(errctx);
}
/**
* @brief The control: the same 1200 blits, issued straight to SDL.
*
* Without this, every drawing number in this file is unreadable -- there is no
* way to tell how much of a 16 ms tilemap frame is libakgl deciding what to draw
* and how much is the software rasterizer moving pixels. This does the same
* amount of pixel work with none of the library in the way: same texture, same
* source rectangles, same destinations, no bounds logic, no tileset scan, no
* error contexts. Subtract it from the tilemap number and what is left is what
* libakgl costs.
*/
static akerr_ErrorContext *bench_raw_blit_control(void)
{
PREPARE_ERROR(errctx);
SDL_FRect src = { .x = 0.0, .y = 0.0, .w = BENCH_TILE_SIZE, .h = BENCH_TILE_SIZE };
SDL_FRect dest = { .x = 0.0, .y = 0.0, .w = BENCH_TILE_SIZE, .h = BENCH_TILE_SIZE };
int across = (BENCH_SCREEN_W / BENCH_TILE_SIZE);
int down = (BENCH_SCREEN_H / BENCH_TILE_SIZE);
int columns = 0;
int tilecount = 0;
int tile = 0;
int count = bench_iterations(30);
int i = 0;
int x = 0;
int y = 0;
int rep = 0;
FAIL_ZERO_RETURN(errctx, benchtiles, AKERR_NULLPOINTER, "benchtiles");
columns = benchtiles->w / BENCH_TILE_SIZE;
tilecount = columns * (benchtiles->h / BENCH_TILE_SIZE);
PASS(errctx, renderer->frame_start(renderer));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
// One source tile, blitted 1200 times. The floor: 1200 SDL calls and
// 1200 x 256 pixels of copying, with the source rectangle staying in
// cache the whole way.
bench_start("raw SDL blits, one source tile (control)", "frame", 5000000.0);
for ( i = 0; i < count; i++ ) {
src.x = 0.0;
src.y = 0.0;
for ( y = 0; y < down; y++ ) {
for ( x = 0; x < across; x++ ) {
dest.x = (float32_t)(x * BENCH_TILE_SIZE);
dest.y = (float32_t)(y * BENCH_TILE_SIZE);
SDL_RenderTexture(renderer->sdl_renderer, benchtiles, &src, &dest);
}
}
}
BENCH_FLUSH_STOP(errctx, count);
// The same 1200 blits reading the same scattered source tiles the map
// asks for: cell (x, y) of a BENCH_MAP_W-wide map, so consecutive screen
// rows read parts of the sheet BENCH_MAP_W tiles apart rather than
// walking it in order. This is the honest control for
// akgl_tilemap_draw -- same call count, same pixels, same access
// pattern, none of libakgl in the path -- and the gap between the two is
// what the library's own per-tile work costs.
bench_start("raw SDL blits, map order (control)", "frame", 170000000.0);
for ( i = 0; i < count; i++ ) {
for ( y = 0; y < down; y++ ) {
for ( x = 0; x < across; x++ ) {
tile = (x + (y * BENCH_MAP_W)) % tilecount;
src.x = (float32_t)((tile % columns) * BENCH_TILE_SIZE);
src.y = (float32_t)((tile / columns) * BENCH_TILE_SIZE);
dest.x = (float32_t)(x * BENCH_TILE_SIZE);
dest.y = (float32_t)(y * BENCH_TILE_SIZE);
SDL_RenderTexture(renderer->sdl_renderer, benchtiles, &src, &dest);
}
}
}
BENCH_FLUSH_STOP(errctx, count);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time one actor's render, and then the whole scene through the backend.
*
* A render is two state-to-sprite lookups -- one for the sprite, one inside the
* visibility test -- some frame arithmetic, and one blit. The scene benchmark
* puts that together with the tilemap: one layer of tiles plus 64 actors, which
* is what akgl_render_2d_draw_world does for a modest 2D game.
*/
static akerr_ErrorContext *bench_scene(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_Actor *actor = NULL;
int count = bench_iterations(20000);
int frames = bench_iterations(30);
int i = 0;
int rep = 0;
PASS(errctx, bench_populate_scene());
PASS(errctx, bench_build_map(gamemap, 1));
PASS(errctx, renderer->frame_start(renderer));
actor = &HEAP_ACTOR[0];
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("actor_render, on camera", "actor", 31000.0);
BENCH_LOOP(inner, i, count, actor->renderfunc(actor));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
bench_start("draw_world, 1200 tiles + 64 actors", "frame", 170000000.0);
BENCH_LOOP(inner, i, frames, renderer->draw_world(renderer, NULL));
BENCH_FLUSH_STOP(errctx, frames);
PASS(errctx, inner);
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Time a whole frame the way akgl_game_update runs one.
*
* This is the number a host actually gets, and it is not the sum of the parts.
* akgl_game_update's update sweep is nested inside a loop over
* #AKGL_TILEMAP_MAX_LAYERS and does not filter by layer, so every live actor is
* updated sixteen times per frame rather than once. The gap between this
* benchmark and the scene benchmark plus the logic frame in tests/perf.c is that
* multiplier, and TODO.md carries it as a defect.
*/
static akerr_ErrorContext *bench_game_update(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
int count = bench_iterations(30);
int i = 0;
int rep = 0;
PASS(errctx, bench_populate_scene());
PASS(errctx, bench_build_map(gamemap, 1));
PASS(errctx, akgl_physics_init_null(physics));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("game_update, full frame", "frame", 170000000.0);
BENCH_LOOP(inner, i, count, akgl_game_update(NULL));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
}
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");
SDL_SetLogOutputFunction(bench_discard_log, NULL);
ATTEMPT {
CATCH(errctx, akgl_error_init());
renderer = &_akgl_renderer;
physics = &_akgl_physics;
camera = &_akgl_camera;
gamemap = &_akgl_gamemap;
FAIL_ZERO_BREAK(
errctx,
SDL_Init(SDL_INIT_VIDEO),
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());
FAIL_ZERO_BREAK(
errctx,
SDL_CreateWindowAndRenderer(
"net/aklabs/libakgl/test_perf_render",
BENCH_SCREEN_W,
BENCH_SCREEN_H,
0,
&window,
&renderer->sdl_renderer),
AKGL_ERR_SDL,
"Couldn't create window/renderer: %s",
SDL_GetError());
CATCH(errctx, akgl_render_bind2d(renderer));
camera->x = 0.0;
camera->y = 0.0;
camera->w = BENCH_SCREEN_W;
camera->h = BENCH_SCREEN_H;
// akgl_game_update takes this before it does anything else. Normally
// akgl_game_init creates it; this suite brings the library up piece by
// piece so it can stay headless, so it creates the mutex itself.
game.statelock = SDL_CreateMutex();
FAIL_ZERO_BREAK(errctx, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
// And this one is not optional either. akgl_game_updateFPS calls
// game.lowfpsfunc through the pointer, unguarded, on every frame under
// 30 fps -- which includes the first frame, before there is a frame rate
// to compare. A host that brings the library up the way renderer.h
// documents for an embedder, akgl_render_bind2d over its own window,
// segfaults on its first akgl_game_update. Filed in TODO.md; installing
// the default by hand is the workaround.
game.lowfpsfunc = &akgl_game_lowfps;
benchfont = TTF_OpenFont(BENCH_FONT_PATH, BENCH_FONT_SIZE);
FAIL_ZERO_BREAK(errctx, benchfont, AKGL_ERR_SDL, "Couldn't open %s: %s", BENCH_FONT_PATH, SDL_GetError());
benchtiles = IMG_LoadTexture(renderer->sdl_renderer, BENCH_TILESET_IMAGE);
FAIL_ZERO_BREAK(errctx, benchtiles, AKGL_ERR_SDL, "Couldn't load %s: %s", BENCH_TILESET_IMAGE, SDL_GetError());
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_registry_init_properties());
CATCH(errctx, bench_frame());
CATCH(errctx, bench_primitives());
CATCH(errctx, bench_framebuffer());
CATCH(errctx, bench_text());
CATCH(errctx, bench_asset_load());
CATCH(errctx, bench_map_zero());
CATCH(errctx, bench_tileset_offsets());
CATCH(errctx, bench_tilemap_draw());
CATCH(errctx, bench_raw_blit_control());
CATCH(errctx, bench_scene());
CATCH(errctx, bench_game_update());
BENCH_REPORT_BREAK(errctx);
} CLEANUP {
if ( benchtiles != NULL ) {
SDL_DestroyTexture(benchtiles);
}
if ( benchfont != NULL ) {
TTF_CloseFont(benchfont);
}
TTF_Quit();
SDL_Quit();
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -1,748 +0,0 @@
/**
* @file physics.c
* @brief Unit tests for the physics backends and the simulation loop.
*
* None of these tests need a renderer or a window. The simulation loop walks
* HEAP_ACTOR directly, so each test rebuilds the actor heap rather than relying
* on state left behind by an earlier one.
*/
#include <SDL3/SDL.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/physics.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include "testutil.h"
/** @brief Records whether the stub movement logic ran, and with what dt. */
static int stub_movement_calls = 0;
static float32_t stub_movement_last_dt = 0;
/** @brief Movement logic stub that records its invocation and does nothing else. */
static akerr_ErrorContext *stub_movement_noop(akgl_Actor *actor, float32_t dt)
{
PREPARE_ERROR(e);
stub_movement_calls += 1;
stub_movement_last_dt = dt;
SUCCEED_RETURN(e);
}
/** @brief Movement logic stub that raises the interrupt the simulator must swallow. */
static akerr_ErrorContext *stub_movement_interrupt(akgl_Actor *actor, float32_t dt)
{
PREPARE_ERROR(e);
stub_movement_calls += 1;
FAIL_RETURN(e, AKGL_ERR_LOGICINTERRUPT, "deliberate interrupt from test stub");
}
/** @brief Movement logic stub that raises an error the simulator must propagate. */
static akerr_ErrorContext *stub_movement_error(akgl_Actor *actor, float32_t dt)
{
PREPARE_ERROR(e);
stub_movement_calls += 1;
FAIL_RETURN(e, AKERR_VALUE, "deliberate error from test stub");
}
/**
* @brief Build a character and an actor bound to it, ready for simulation.
*
* The actor is placed on the heap with a nonzero refcount and a movement logic
* stub, which is the minimum the simulation loop requires to process it.
*/
static akerr_ErrorContext *make_sim_actor(akgl_Actor **actor, akgl_Character **basechar, char *name)
{
PREPARE_ERROR(e);
ATTEMPT {
CATCH(e, akgl_heap_next_character(basechar));
CATCH(e, akgl_character_initialize(*basechar, name));
CATCH(e, akgl_heap_next_actor(actor));
CATCH(e, akgl_actor_initialize(*actor, name));
(*actor)->basechar = *basechar;
(*actor)->movementlogicfunc = &stub_movement_noop;
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/** @brief Reset the actor heap so each simulation test starts from a clean slate. */
static akerr_ErrorContext *reset_sim_heap(void)
{
PREPARE_ERROR(e);
ATTEMPT {
CATCH(e, akgl_registry_init_actor());
CATCH(e, akgl_registry_init_character());
CATCH(e, akgl_heap_init());
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
stub_movement_calls = 0;
stub_movement_last_dt = 0;
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_init_null(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
ATTEMPT {
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
TEST_EXPECT_OK(e, akgl_physics_init_null(&backend), "akgl_physics_init_null");
TEST_ASSERT(e, backend.gravity == &akgl_physics_null_gravity,
"init_null did not install the null gravity function");
TEST_ASSERT(e, backend.collide == &akgl_physics_null_collide,
"init_null did not install the null collide function");
TEST_ASSERT(e, backend.move == &akgl_physics_null_move,
"init_null did not install the null move function");
TEST_ASSERT(e, backend.simulate == &akgl_physics_simulate,
"init_null did not install the shared simulate function");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_init_null(NULL),
"akgl_physics_init_null(NULL)");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_null_backend_is_inert(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_Actor actor;
akgl_Actor reference;
ATTEMPT {
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
memset(&actor, 0x00, sizeof(akgl_Actor));
TEST_EXPECT_OK(e, akgl_physics_init_null(&backend), "akgl_physics_init_null");
actor.x = 3.0f; actor.y = 5.0f; actor.z = 7.0f;
actor.vx = 11.0f; actor.vy = 13.0f; actor.vz = 17.0f;
actor.ex = 19.0f; actor.ey = 23.0f; actor.ez = 29.0f;
memcpy(&reference, &actor, sizeof(akgl_Actor));
TEST_EXPECT_OK(e, backend.gravity(&backend, &actor, 1.0f), "null gravity");
TEST_EXPECT_OK(e, backend.move(&backend, &actor, 1.0f), "null move");
TEST_EXPECT_OK(e, backend.collide(&backend, &actor, &reference), "null collide");
TEST_ASSERT(e, memcmp(&actor, &reference, sizeof(akgl_Actor)) == 0,
"the null backend modified the actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_gravity(NULL, &actor, 1.0f),
"null gravity with NULL self");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_move(NULL, &actor, 1.0f),
"null move with NULL self");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_collide(NULL, &actor, &reference),
"null collide with NULL self");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_arcade_gravity(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_Actor actor;
ATTEMPT {
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
// X gravity subtracts, because the X origin is screen-left.
memset(&actor, 0x00, sizeof(akgl_Actor));
backend.gravity_x = 10.0;
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 0.5f), "arcade gravity x");
TEST_ASSERT_FEQ(e, actor.ex, -5.0f, "gravity_x 10 over dt 0.5 gave ex %f, expected -5", actor.ex);
TEST_ASSERT_FEQ(e, actor.ey, 0.0f, "gravity_x leaked into ey (%f)", actor.ey);
TEST_ASSERT_FEQ(e, actor.ez, 0.0f, "gravity_x leaked into ez (%f)", actor.ez);
// Y gravity adds, because the Y origin is down-screen.
memset(&actor, 0x00, sizeof(akgl_Actor));
backend.gravity_x = 0;
backend.gravity_y = 10.0;
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 0.5f), "arcade gravity y");
TEST_ASSERT_FEQ(e, actor.ey, 5.0f, "gravity_y 10 over dt 0.5 gave ey %f, expected 5", actor.ey);
TEST_ASSERT_FEQ(e, actor.ex, 0.0f, "gravity_y leaked into ex (%f)", actor.ex);
// Z gravity subtracts, because the Z origin is behind the camera.
memset(&actor, 0x00, sizeof(akgl_Actor));
backend.gravity_y = 0;
backend.gravity_z = 4.0;
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 0.25f), "arcade gravity z");
TEST_ASSERT_FEQ(e, actor.ez, -1.0f, "gravity_z 4 over dt 0.25 gave ez %f, expected -1", actor.ez);
// Negative gravity reverses each axis.
memset(&actor, 0x00, sizeof(akgl_Actor));
backend.gravity_x = -10.0;
backend.gravity_y = -10.0;
backend.gravity_z = -10.0;
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 1.0f), "arcade gravity negative");
TEST_ASSERT_FEQ(e, actor.ex, 10.0f, "negative gravity_x gave ex %f, expected 10", actor.ex);
TEST_ASSERT_FEQ(e, actor.ey, -10.0f, "negative gravity_y gave ey %f, expected -10", actor.ey);
TEST_ASSERT_FEQ(e, actor.ez, 10.0f, "negative gravity_z gave ez %f, expected 10", actor.ez);
// A zero dt applies no force even when gravity is set.
memset(&actor, 0x00, sizeof(akgl_Actor));
backend.gravity_x = 10.0;
backend.gravity_y = 10.0;
backend.gravity_z = 10.0;
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 0.0f), "arcade gravity dt 0");
TEST_ASSERT_FEQ(e, actor.ex, 0.0f, "dt 0 still moved ex (%f)", actor.ex);
TEST_ASSERT_FEQ(e, actor.ey, 0.0f, "dt 0 still moved ey (%f)", actor.ey);
TEST_ASSERT_FEQ(e, actor.ez, 0.0f, "dt 0 still moved ez (%f)", actor.ez);
// All-zero gravity leaves every axis alone.
memset(&actor, 0x00, sizeof(akgl_Actor));
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
actor.ex = 1.0f; actor.ey = 2.0f; actor.ez = 3.0f;
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 1.0f), "arcade gravity all zero");
TEST_ASSERT_FEQ(e, actor.ex, 1.0f, "zero gravity_x changed ex (%f)", actor.ex);
TEST_ASSERT_FEQ(e, actor.ey, 2.0f, "zero gravity_y changed ey (%f)", actor.ey);
TEST_ASSERT_FEQ(e, actor.ez, 3.0f, "zero gravity_z changed ez (%f)", actor.ez);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_arcade_gravity(NULL, &actor, 1.0f),
"arcade gravity with NULL self");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_arcade_gravity(&backend, NULL, 1.0f),
"arcade gravity with NULL actor");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_arcade_move(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_Actor actor;
ATTEMPT {
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.x = 100.0f; actor.y = 200.0f; actor.z = 300.0f;
actor.vx = 10.0f; actor.vy = -20.0f; actor.vz = 30.0f;
TEST_EXPECT_OK(e, akgl_physics_arcade_move(&backend, &actor, 0.5f), "arcade move");
TEST_ASSERT_FEQ(e, actor.x, 105.0f, "x moved to %f, expected 105", actor.x);
TEST_ASSERT_FEQ(e, actor.y, 190.0f, "y moved to %f, expected 190", actor.y);
TEST_ASSERT_FEQ(e, actor.z, 315.0f, "z moved to %f, expected 315", actor.z);
// dt of zero is a no-op regardless of velocity.
TEST_EXPECT_OK(e, akgl_physics_arcade_move(&backend, &actor, 0.0f), "arcade move dt 0");
TEST_ASSERT_FEQ(e, actor.x, 105.0f, "dt 0 moved x to %f", actor.x);
TEST_ASSERT_FEQ(e, actor.y, 190.0f, "dt 0 moved y to %f", actor.y);
TEST_ASSERT_FEQ(e, actor.z, 315.0f, "dt 0 moved z to %f", actor.z);
// Zero velocity is a no-op regardless of dt.
actor.vx = 0; actor.vy = 0; actor.vz = 0;
TEST_EXPECT_OK(e, akgl_physics_arcade_move(&backend, &actor, 10.0f), "arcade move zero velocity");
TEST_ASSERT_FEQ(e, actor.x, 105.0f, "zero velocity moved x to %f", actor.x);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_arcade_move(NULL, &actor, 1.0f),
"arcade move with NULL self");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_arcade_move(&backend, NULL, 1.0f),
"arcade move with NULL actor");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_arcade_collide_unimplemented(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_Actor a1;
akgl_Actor a2;
ATTEMPT {
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
memset(&a1, 0x00, sizeof(akgl_Actor));
memset(&a2, 0x00, sizeof(akgl_Actor));
// Pins the stub so that implementing collision has to update this test.
TEST_EXPECT_STATUS(e, AKERR_API, akgl_physics_arcade_collide(&backend, &a1, &a2),
"arcade collide is still a stub");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_arcade_collide(NULL, &a1, &a2),
"arcade collide with NULL self");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_factory(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_String typestr;
ATTEMPT {
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
CATCH(e, akgl_string_initialize(&typestr, "null"));
TEST_EXPECT_OK(e, akgl_physics_factory(&backend, &typestr), "factory(\"null\")");
TEST_ASSERT(e, backend.move == &akgl_physics_null_move,
"factory(\"null\") did not install the null backend");
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
CATCH(e, akgl_string_initialize(&typestr, "arcade"));
TEST_EXPECT_OK(e, akgl_physics_factory(&backend, &typestr), "factory(\"arcade\")");
TEST_ASSERT(e, backend.move == &akgl_physics_arcade_move,
"factory(\"arcade\") did not install the arcade backend");
CATCH(e, akgl_string_initialize(&typestr, "newtonian"));
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_physics_factory(&backend, &typestr),
"factory with an unknown backend name");
// An empty name must not match either prefix.
CATCH(e, akgl_string_initialize(&typestr, ""));
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_physics_factory(&backend, &typestr),
"factory with an empty backend name");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_factory(NULL, &typestr),
"factory with NULL self");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_factory(&backend, NULL),
"factory with NULL type");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_init_arcade_properties(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
ATTEMPT {
CATCH(e, akgl_registry_init_properties());
// With nothing configured, every environmental constant defaults to zero.
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
TEST_EXPECT_OK(e, akgl_physics_init_arcade(&backend), "init_arcade with no properties set");
TEST_ASSERT_FEQ(e, backend.gravity_y, 0.0f,
"unset physics.gravity.y defaulted to %f, expected 0", backend.gravity_y);
TEST_ASSERT_FEQ(e, backend.drag_x, 0.0f,
"unset physics.drag.x defaulted to %f, expected 0", backend.drag_x);
TEST_ASSERT(e, backend.gravity == &akgl_physics_arcade_gravity,
"init_arcade did not install the arcade gravity function");
TEST_ASSERT(e, backend.simulate == &akgl_physics_simulate,
"init_arcade did not install the shared simulate function");
// Configured values are read out of the property registry.
CATCH(e, akgl_set_property("physics.gravity.x", "1.5"));
CATCH(e, akgl_set_property("physics.gravity.y", "9.8"));
CATCH(e, akgl_set_property("physics.gravity.z", "2.25"));
CATCH(e, akgl_set_property("physics.drag.x", "0.5"));
CATCH(e, akgl_set_property("physics.drag.y", "0.25"));
CATCH(e, akgl_set_property("physics.drag.z", "0.125"));
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
TEST_EXPECT_OK(e, akgl_physics_init_arcade(&backend), "init_arcade with properties set");
TEST_ASSERT_FEQ(e, backend.gravity_x, 1.5f, "physics.gravity.x read as %f", backend.gravity_x);
TEST_ASSERT_FEQ(e, backend.gravity_y, 9.8f, "physics.gravity.y read as %f", backend.gravity_y);
TEST_ASSERT_FEQ(e, backend.gravity_z, 2.25f, "physics.gravity.z read as %f", backend.gravity_z);
TEST_ASSERT_FEQ(e, backend.drag_x, 0.5f, "physics.drag.x read as %f", backend.drag_x);
TEST_ASSERT_FEQ(e, backend.drag_y, 0.25f, "physics.drag.y read as %f", backend.drag_y);
TEST_ASSERT_FEQ(e, backend.drag_z, 0.125f, "physics.drag.z read as %f", backend.drag_z);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_init_arcade(NULL),
"init_arcade with NULL self");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_simulate_skips_inactive(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
ATTEMPT {
CATCH(e, reset_sim_heap());
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
CATCH(e, akgl_physics_init_null(&backend));
// A released actor (refcount 0) is skipped entirely.
CATCH(e, make_sim_actor(&actor, &basechar, "inactive"));
actor->refcount = 0;
stub_movement_calls = 0;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with a released actor");
TEST_ASSERT(e, stub_movement_calls == 0,
"simulate ran movement logic for an actor with refcount 0 (%d calls)",
stub_movement_calls);
// An actor with no base character is skipped.
CATCH(e, reset_sim_heap());
CATCH(e, make_sim_actor(&actor, &basechar, "nochar"));
actor->basechar = NULL;
stub_movement_calls = 0;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with a character-less actor");
TEST_ASSERT(e, stub_movement_calls == 0,
"simulate ran movement logic for an actor with no base character (%d calls)",
stub_movement_calls);
// A fully wired actor is processed.
CATCH(e, reset_sim_heap());
CATCH(e, make_sim_actor(&actor, &basechar, "active"));
stub_movement_calls = 0;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with an active actor");
TEST_ASSERT(e, stub_movement_calls == 1,
"simulate ran movement logic %d times for one active actor, expected 1",
stub_movement_calls);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_simulate_children_follow_parents(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_Actor *parent = NULL;
akgl_Actor *child = NULL;
akgl_Character *basechar = NULL;
akgl_Character *childchar = NULL;
ATTEMPT {
CATCH(e, reset_sim_heap());
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
CATCH(e, akgl_physics_init_null(&backend));
CATCH(e, make_sim_actor(&parent, &basechar, "parent"));
CATCH(e, make_sim_actor(&child, &childchar, "child"));
parent->x = 100.0f; parent->y = 200.0f; parent->z = 300.0f;
// For a child, vx/vy/vz are read as a fixed offset from the parent.
child->vx = 5.0f; child->vy = -5.0f; child->vz = 2.0f;
child->x = -999.0f; child->y = -999.0f; child->z = -999.0f;
CATCH(e, akgl_actor_add_child(parent, child));
stub_movement_calls = 0;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with a parented child");
TEST_ASSERT_FEQ(e, child->x, 105.0f, "child x resolved to %f, expected parent 100 + offset 5", child->x);
TEST_ASSERT_FEQ(e, child->y, 195.0f, "child y resolved to %f, expected parent 200 - offset 5", child->y);
TEST_ASSERT_FEQ(e, child->z, 302.0f, "child z resolved to %f, expected parent 300 + offset 2", child->z);
// Only the parent goes through the movement logic; the child is positional only.
TEST_ASSERT(e, stub_movement_calls == 1,
"simulate ran movement logic %d times, expected 1 (parent only)",
stub_movement_calls);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_simulate_layer_mask(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_Actor *layer0 = NULL;
akgl_Actor *layer3 = NULL;
akgl_Character *char0 = NULL;
akgl_Character *char3 = NULL;
akgl_Iterator opflags;
ATTEMPT {
CATCH(e, reset_sim_heap());
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
CATCH(e, akgl_physics_init_null(&backend));
CATCH(e, make_sim_actor(&layer0, &char0, "onlayer0"));
CATCH(e, make_sim_actor(&layer3, &char3, "onlayer3"));
layer0->layer = 0;
layer3->layer = 3;
// Masking to layer 3 processes only the layer 3 actor.
opflags.flags = AKGL_ITERATOR_OP_LAYERMASK;
opflags.layerid = 3;
stub_movement_calls = 0;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, &opflags), "simulate masked to layer 3");
TEST_ASSERT(e, stub_movement_calls == 1,
"layer mask 3 processed %d actors, expected 1", stub_movement_calls);
// Masking to a layer with no actors processes nothing.
opflags.layerid = 7;
stub_movement_calls = 0;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, &opflags), "simulate masked to an empty layer");
TEST_ASSERT(e, stub_movement_calls == 0,
"empty layer mask processed %d actors, expected 0", stub_movement_calls);
// Without the mask flag, layerid is ignored and both actors are processed.
opflags.flags = 0;
opflags.layerid = 7;
stub_movement_calls = 0;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, &opflags), "simulate with the layer mask off");
TEST_ASSERT(e, stub_movement_calls == 2,
"unmasked simulate processed %d actors, expected 2", stub_movement_calls);
// A NULL iterator selects the documented defaults, which mask nothing.
stub_movement_calls = 0;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with NULL opflags");
TEST_ASSERT(e, stub_movement_calls == 2,
"NULL opflags processed %d actors, expected 2", stub_movement_calls);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_simulate_thrust_and_clamp(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
ATTEMPT {
CATCH(e, reset_sim_heap());
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
CATCH(e, akgl_physics_init_null(&backend));
CATCH(e, make_sim_actor(&actor, &basechar, "thruster"));
// Thrust beyond the actor's max speed clamps to +sx, preserving sign.
actor->sx = 50.0f; actor->sy = 40.0f; actor->sz = 30.0f;
actor->tx = 500.0f; actor->ty = 400.0f; actor->tz = 300.0f;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with positive over-thrust");
TEST_ASSERT_FEQ(e, actor->tx, 50.0f, "tx clamped to %f, expected 50", actor->tx);
TEST_ASSERT_FEQ(e, actor->ty, 40.0f, "ty clamped to %f, expected 40", actor->ty);
TEST_ASSERT_FEQ(e, actor->tz, 30.0f, "tz clamped to %f, expected 30", actor->tz);
// Negative over-thrust clamps to -sx.
actor->tx = -500.0f; actor->ty = -400.0f; actor->tz = -300.0f;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with negative over-thrust");
TEST_ASSERT_FEQ(e, actor->tx, -50.0f, "tx clamped to %f, expected -50", actor->tx);
TEST_ASSERT_FEQ(e, actor->ty, -40.0f, "ty clamped to %f, expected -40", actor->ty);
TEST_ASSERT_FEQ(e, actor->tz, -30.0f, "tz clamped to %f, expected -30", actor->tz);
// Thrust inside the limit is left alone.
actor->tx = 10.0f; actor->ty = -10.0f; actor->tz = 5.0f;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with in-range thrust");
TEST_ASSERT_FEQ(e, actor->tx, 10.0f, "in-range tx changed to %f", actor->tx);
TEST_ASSERT_FEQ(e, actor->ty, -10.0f, "in-range ty changed to %f", actor->ty);
TEST_ASSERT_FEQ(e, actor->tz, 5.0f, "in-range tz changed to %f", actor->tz);
// Velocity is the sum of environmental force and thrust.
actor->ex = 3.0f; actor->ey = 4.0f; actor->ez = 5.0f;
actor->tx = 1.0f; actor->ty = 2.0f; actor->tz = 3.0f;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate velocity composition");
TEST_ASSERT_FEQ(e, actor->vx, 4.0f, "vx composed to %f, expected ex 3 + tx 1", actor->vx);
TEST_ASSERT_FEQ(e, actor->vy, 6.0f, "vy composed to %f, expected ey 4 + ty 2", actor->vy);
TEST_ASSERT_FEQ(e, actor->vz, 8.0f, "vz composed to %f, expected ez 5 + tz 3", actor->vz);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_simulate_drag(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
ATTEMPT {
CATCH(e, reset_sim_heap());
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
CATCH(e, akgl_physics_init_null(&backend));
CATCH(e, make_sim_actor(&actor, &basechar, "dragged"));
// Zero drag leaves environmental velocity untouched.
actor->ex = 100.0f; actor->ey = 100.0f; actor->ez = 100.0f;
actor->sx = 1000.0f; actor->sy = 1000.0f; actor->sz = 1000.0f;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with zero drag");
TEST_ASSERT_FEQ(e, actor->ex, 100.0f, "zero drag changed ex to %f", actor->ex);
TEST_ASSERT_FEQ(e, actor->ey, 100.0f, "zero drag changed ey to %f", actor->ey);
TEST_ASSERT_FEQ(e, actor->ez, 100.0f, "zero drag changed ez to %f", actor->ez);
// Nonzero drag bleeds off environmental velocity in proportion to dt.
// The simulator derives dt from the wall clock, so assert the direction
// and bounds of the change rather than an exact figure.
backend.drag_x = 0.5;
backend.drag_y = 0.5;
backend.drag_z = 0.5;
backend.gravity_time = SDL_GetTicksNS();
SDL_Delay(20);
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with drag applied");
TEST_ASSERT(e, actor->ex < 100.0f && actor->ex > 0.0f,
"drag left ex at %f, expected a value between 0 and 100", actor->ex);
TEST_ASSERT(e, actor->ey < 100.0f && actor->ey > 0.0f,
"drag left ey at %f, expected a value between 0 and 100", actor->ey);
TEST_ASSERT(e, actor->ez < 100.0f && actor->ez > 0.0f,
"drag left ez at %f, expected a value between 0 and 100", actor->ez);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_simulate_movement_states(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
ATTEMPT {
CATCH(e, reset_sim_heap());
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
CATCH(e, akgl_physics_init_null(&backend));
CATCH(e, make_sim_actor(&actor, &basechar, "mover"));
actor->sx = 1000.0f; actor->sy = 1000.0f; actor->sz = 1000.0f;
actor->ax = 10.0f; actor->ay = 20.0f;
// An idle actor accumulates no thrust on either axis.
actor->state = 0;
actor->tx = 0; actor->ty = 0;
backend.gravity_time = SDL_GetTicksNS();
SDL_Delay(10);
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate an idle actor");
TEST_ASSERT_FEQ(e, actor->tx, 0.0f, "idle actor accumulated tx %f", actor->tx);
TEST_ASSERT_FEQ(e, actor->ty, 0.0f, "idle actor accumulated ty %f", actor->ty);
// Moving horizontally accumulates thrust on X only.
actor->state = AKGL_ACTOR_STATE_MOVING_LEFT;
actor->tx = 0; actor->ty = 0;
backend.gravity_time = SDL_GetTicksNS();
SDL_Delay(10);
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate a horizontally moving actor");
TEST_ASSERT(e, actor->tx > 0.0f, "MOVING_LEFT accumulated tx %f, expected a positive value", actor->tx);
TEST_ASSERT_FEQ(e, actor->ty, 0.0f, "MOVING_LEFT leaked thrust into ty (%f)", actor->ty);
// Moving vertically accumulates thrust on Y only.
actor->state = AKGL_ACTOR_STATE_MOVING_DOWN;
actor->tx = 0; actor->ty = 0;
backend.gravity_time = SDL_GetTicksNS();
SDL_Delay(10);
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate a vertically moving actor");
TEST_ASSERT(e, actor->ty > 0.0f, "MOVING_DOWN accumulated ty %f, expected a positive value", actor->ty);
TEST_ASSERT_FEQ(e, actor->tx, 0.0f, "MOVING_DOWN leaked thrust into tx (%f)", actor->tx);
// The right and up states drive the same accumulators.
actor->state = (AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_MOVING_UP);
actor->tx = 0; actor->ty = 0;
backend.gravity_time = SDL_GetTicksNS();
SDL_Delay(10);
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate a diagonally moving actor");
TEST_ASSERT(e, actor->tx > 0.0f, "MOVING_RIGHT accumulated tx %f, expected a positive value", actor->tx);
TEST_ASSERT(e, actor->ty > 0.0f, "MOVING_UP accumulated ty %f, expected a positive value", actor->ty);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_simulate_logic_interrupt(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
akgl_Actor *first = NULL;
akgl_Actor *second = NULL;
akgl_Character *char1 = NULL;
akgl_Character *char2 = NULL;
ATTEMPT {
CATCH(e, reset_sim_heap());
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
CATCH(e, akgl_physics_init_null(&backend));
CATCH(e, make_sim_actor(&first, &char1, "interrupter"));
CATCH(e, make_sim_actor(&second, &char2, "follower"));
first->movementlogicfunc = &stub_movement_interrupt;
// AKGL_ERR_LOGICINTERRUPT means "skip me this frame", not "abort the frame",
// so the second actor must still be reached.
stub_movement_calls = 0;
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL),
"simulate must swallow AKGL_ERR_LOGICINTERRUPT");
TEST_ASSERT(e, stub_movement_calls == 2,
"an interrupting actor stopped the loop after %d of 2 actors",
stub_movement_calls);
// An unrelated error is not swallowed.
CATCH(e, reset_sim_heap());
CATCH(e, make_sim_actor(&first, &char1, "failer"));
first->movementlogicfunc = &stub_movement_error;
TEST_EXPECT_STATUS(e, AKERR_VALUE, akgl_physics_simulate(&backend, NULL),
"simulate must propagate a non-interrupt movement error");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_physics_simulate_nullpointers(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend backend;
ATTEMPT {
CATCH(e, reset_sim_heap());
// A backend with no move function cannot simulate.
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_simulate(&backend, NULL),
"simulate with no move function installed");
// A NULL backend must be reported, not dereferenced.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_simulate(NULL, NULL),
"simulate with NULL self");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
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());
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_registry_init_properties());
CATCH(errctx, test_physics_init_null());
CATCH(errctx, test_physics_null_backend_is_inert());
CATCH(errctx, test_physics_arcade_gravity());
CATCH(errctx, test_physics_arcade_move());
CATCH(errctx, test_physics_arcade_collide_unimplemented());
CATCH(errctx, test_physics_factory());
CATCH(errctx, test_physics_init_arcade_properties());
CATCH(errctx, test_physics_simulate_skips_inactive());
CATCH(errctx, test_physics_simulate_children_follow_parents());
CATCH(errctx, test_physics_simulate_layer_mask());
CATCH(errctx, test_physics_simulate_thrust_and_clamp());
CATCH(errctx, test_physics_simulate_drag());
CATCH(errctx, test_physics_simulate_movement_states());
CATCH(errctx, test_physics_simulate_logic_interrupt());
CATCH(errctx, test_physics_simulate_nullpointers());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -1,13 +1,7 @@
#include <SDL3/SDL.h>
#include <stdlib.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include "testutil.h"
typedef akerr_ErrorContext *(*RegistryFuncPtr)(void);
@@ -23,7 +17,7 @@ akerr_ErrorContext *test_akgl_registry_init(RegistryFuncPtr funcptr)
SDL_calloc_func calloc_func;
SDL_realloc_func realloc_func;
SDL_free_func free_func;
SDL_GetMemoryFunctions(
&malloc_func,
&calloc_func,
@@ -49,7 +43,7 @@ akerr_ErrorContext *test_akgl_registry_init(RegistryFuncPtr funcptr)
} PROCESS(errctx) {
} FINISH(errctx, true);
FAIL_RETURN(errctx, AKGL_ERR_BEHAVIOR, "SDL memory allocator fails but registry reports successful property creation");
FAIL_RETURN(errctx, AKERR_BEHAVIOR, "SDL memory allocator fails but registry reports successful property creation");
}
akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
@@ -60,7 +54,7 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n");
printf("Sucess\n");
} FINISH(errctx, true);
ATTEMPT {
@@ -68,7 +62,7 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n");
printf("Sucess\n");
} FINISH(errctx, true);
ATTEMPT {
@@ -76,7 +70,7 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n");
printf("Sucess\n");
} FINISH(errctx, true);
ATTEMPT {
@@ -84,76 +78,7 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n");
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief akgl_get_property must copy the value, and not a byte more.
*
* It used to copy a fixed AKGL_MAX_STRING_LENGTH bytes out of whatever SDL
* returned, and what SDL returns is its own strdup of the value -- four bytes
* for "0.0". That read up to 4 KiB past the end of somebody else's allocation on
* every call, which valgrind reported twelve times over in tests/physics.c
* alone.
*
* The destination is filled with a sentinel first, so the assertion is not
* "the value arrived" -- that would pass either way -- but "the bytes past the
* terminator were left alone", which is only true if the copy was bounded by
* the source.
*/
akerr_ErrorContext *test_akgl_get_property_copies_only_the_value(void)
{
PREPARE_ERROR(errctx);
akgl_String *value = NULL;
char oversized[AKGL_MAX_STRING_LENGTH + 8];
size_t valuelen = 0;
size_t i = 0;
bool untouched = true;
ATTEMPT {
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init_properties());
CATCH(errctx, akgl_heap_next_string(&value));
memset(&value->data, 0x5a, AKGL_MAX_STRING_LENGTH);
CATCH(errctx, akgl_set_property("registry.short", "0.0"));
CATCH(errctx, akgl_get_property("registry.short", &value, "unset"));
valuelen = strlen("0.0");
TEST_ASSERT(errctx, strcmp(value->data, "0.0") == 0,
"akgl_get_property returned \"%s\", expected \"0.0\"", value->data);
for ( i = (valuelen + 1); i < AKGL_MAX_STRING_LENGTH; i++ ) {
TEST_ASSERT_FLAG(untouched, (value->data[i] == 0x5a));
}
TEST_ASSERT(errctx, untouched,
"akgl_get_property wrote past the end of a %zu byte value", valuelen);
// The default takes the same path as a stored value.
CATCH(errctx, akgl_get_property("registry.absent", &value, "fallback"));
TEST_ASSERT(errctx, strcmp(value->data, "fallback") == 0,
"akgl_get_property returned \"%s\" for an unset property, expected the default",
value->data);
// A value too long for an akgl_String is refused rather than truncated
// into an unterminated buffer.
memset(&oversized, 'x', sizeof(oversized));
oversized[AKGL_MAX_STRING_LENGTH + 7] = '\0';
CATCH(errctx, akgl_set_property("registry.oversized", (char *)&oversized));
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_get_property("registry.oversized", &value, "unset"),
"reading a property longer than an akgl_String");
// And the documented refusal survives: unset, with no default.
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_get_property("registry.absent", &value, NULL),
"reading an unset property with no default");
} CLEANUP {
if ( value != NULL ) {
IGNORE(akgl_heap_release_string(value));
}
} PROCESS(errctx) {
printf("Sucess\n");
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
@@ -162,9 +87,7 @@ int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, test_akgl_registry_init_creation_failures());
CATCH(errctx, test_akgl_get_property_copies_only_the_value());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);

View File

@@ -1,222 +0,0 @@
/**
* @file renderer.c
* @brief Unit tests for the 2D render backend's vtable and its entry points.
*
* None of this needs a display. akgl_render_bind2d() installs the backend's
* methods and nothing else, so it can be checked against a backend that has no
* SDL_Renderer at all; the entry points that do draw are checked against a
* software renderer under the dummy video driver, the same way tests/draw.c
* does it.
*
* akgl_render_init2d() is not covered here: it creates a window from the
* property registry and writes the `camera` global, which is what the offscreen
* harness described in TODO.md exists to make testable.
*/
#include <SDL3/SDL.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/renderer.h>
#include "testutil.h"
/** @brief Width and height of the offscreen target the drawing tests use. */
#define TEST_TARGET_SIZE 32
/** @brief A backend bound to a live software renderer, built by main(). */
static akgl_RenderBackend bound;
akerr_ErrorContext *test_render_bind2d(void)
{
PREPARE_ERROR(errctx);
akgl_RenderBackend backend;
ATTEMPT {
// The state a host is in when it owns its own window: a zeroed backend
// with somebody else's SDL_Renderer in it. Binding must fill in the six
// methods and leave that renderer alone -- creating a second window is
// precisely what an embedded interpreter must not do.
memset(&backend, 0x00, sizeof(akgl_RenderBackend));
backend.sdl_renderer = (SDL_Renderer *)&bound;
TEST_EXPECT_OK(errctx, akgl_render_bind2d(&backend), "binding the 2D backend");
TEST_ASSERT(errctx, backend.sdl_renderer == (SDL_Renderer *)&bound,
"binding replaced the caller's SDL_Renderer");
TEST_ASSERT(errctx, backend.shutdown == &akgl_render_2d_shutdown,
"binding did not install shutdown");
TEST_ASSERT(errctx, backend.frame_start == &akgl_render_2d_frame_start,
"binding did not install frame_start");
TEST_ASSERT(errctx, backend.frame_end == &akgl_render_2d_frame_end,
"binding did not install frame_end");
TEST_ASSERT(errctx, backend.draw_texture == &akgl_render_2d_draw_texture,
"binding did not install draw_texture");
TEST_ASSERT(errctx, backend.draw_mesh == &akgl_render_2d_draw_mesh,
"binding did not install draw_mesh");
TEST_ASSERT(errctx, backend.draw_world == &akgl_render_2d_draw_world,
"binding did not install draw_world");
// Binding a backend that has no renderer yet is legal -- the two halves
// are separable in both directions -- and leaves it NULL rather than
// inventing one.
memset(&backend, 0x00, sizeof(akgl_RenderBackend));
TEST_EXPECT_OK(errctx, akgl_render_bind2d(&backend),
"binding a backend with no SDL renderer");
TEST_ASSERT(errctx, backend.sdl_renderer == NULL,
"binding invented an SDL_Renderer");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_bind2d(NULL),
"binding a NULL backend");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_render_backend_without_a_renderer(void)
{
PREPARE_ERROR(errctx);
akgl_RenderBackend empty;
ATTEMPT {
// Bound but never given an SDL_Renderer. Every entry point that draws
// has to report that rather than dereference it.
memset(&empty, 0x00, sizeof(akgl_RenderBackend));
CATCH(errctx, akgl_render_bind2d(&empty));
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_start(&empty),
"starting a frame on a backend with no renderer");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_end(&empty),
"ending a frame on a backend with no renderer");
// Shutdown is the one that has nothing to release yet, so it succeeds.
TEST_EXPECT_OK(errctx, empty.shutdown(&empty), "shutting down an unused backend");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_render_frames_and_textures(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *surface = NULL;
SDL_Texture *texture = NULL;
SDL_FRect dest;
SDL_FPoint center;
ATTEMPT {
dest.x = 0.0f;
dest.y = 0.0f;
dest.w = 8.0f;
dest.h = 8.0f;
center.x = 4.0f;
center.y = 4.0f;
surface = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA32);
FAIL_ZERO_BREAK(errctx, surface, AKGL_ERR_SDL, "%s", SDL_GetError());
texture = SDL_CreateTextureFromSurface(bound.sdl_renderer, surface);
FAIL_ZERO_BREAK(errctx, texture, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_EXPECT_OK(errctx, bound.frame_start(&bound), "starting a frame");
TEST_EXPECT_OK(errctx,
bound.draw_texture(&bound, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE),
"blitting a texture unrotated");
// A non-zero angle takes the rotated path, which is the one that
// consults the pivot and the flip mode.
TEST_EXPECT_OK(errctx,
bound.draw_texture(&bound, texture, NULL, &dest, 90.0, &center, SDL_FLIP_HORIZONTAL),
"blitting a texture rotated");
TEST_EXPECT_OK(errctx, bound.frame_end(&bound), "ending a frame");
// A rotation with no pivot is refused: SDL's "NULL means the centre of
// dest" convention is not offered here, so a NULL is a caller mistake
// rather than a default.
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
bound.draw_texture(&bound, texture, NULL, &dest, 90.0, NULL, SDL_FLIP_NONE),
"blitting rotated with no pivot");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_render_2d_draw_texture(NULL, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE),
"blitting through a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
bound.draw_texture(&bound, NULL, NULL, &dest, 0, NULL, SDL_FLIP_NONE),
"blitting a NULL texture");
} CLEANUP {
if ( texture != NULL ) {
SDL_DestroyTexture(texture);
}
if ( surface != NULL ) {
SDL_DestroySurface(surface);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_render_unimplemented_and_guards(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
// The 3D hook is not a stub that quietly does nothing. It refuses, so a
// caller that reaches it finds out on the first frame.
TEST_EXPECT_STATUS(errctx, AKERR_API, bound.draw_mesh(&bound),
"drawing a mesh through the 2D backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_draw_world(NULL, NULL),
"drawing the world through a NULL backend");
// Shutting the 2D backend down releases nothing today, and says so.
TEST_EXPECT_OK(errctx, bound.shutdown(&bound), "shutting the backend down");
} 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");
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
ATTEMPT {
CATCH(errctx, akgl_error_init());
memset(&bound, 0x00, sizeof(akgl_RenderBackend));
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_renderer",
TEST_TARGET_SIZE,
TEST_TARGET_SIZE,
0,
&window,
&bound.sdl_renderer),
AKGL_ERR_SDL,
"Couldn't create window/renderer: %s",
SDL_GetError());
// The host owns the window; libakgl only ever binds to it. This is the
// arrangement akgl_render_bind2d exists for.
CATCH(errctx, akgl_render_bind2d(&bound));
CATCH(errctx, test_render_bind2d());
CATCH(errctx, test_render_backend_without_a_renderer());
CATCH(errctx, test_render_frames_and_textures());
CATCH(errctx, test_render_unimplemented_and_guards());
} CLEANUP {
SDL_Quit();
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -10,59 +10,59 @@
#include <akgl/heap.h>
#include <akgl/util.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/renderer.h>
SDL_Window *window;
SDL_Renderer *renderer;
akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
{
PREPARE_ERROR(errctx);
akgl_SpriteSheet *sheet = NULL;
SDL_Texture *image = NULL;
akgl_String *tmpstr = NULL;
// Does the image file get loaded?
// Is the image file loaded correctly? (Surface comparison)
// Is the spritesheet in the registry?
ATTEMPT {
CATCH(errctx, akgl_heap_next_spritesheet(&sheet));
CATCH(errctx, akgl_heap_next_string(&tmpstr));
PREPARE_ERROR(errctx);
akgl_SpriteSheet *sheet = NULL;
SDL_Texture *image = NULL;
akgl_String *tmpstr = NULL;
// Does the image file get loaded?
// Is the image file loaded correctly? (Surface comparison)
// Is the spritesheet in the registry?
ATTEMPT {
CATCH(errctx, akgl_heap_next_spritesheet(&sheet));
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, (char *)&tmpstr->data);
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
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");
CATCH(
errctx,
akgl_render_and_compare(
sheet->texture,
image,
0, 0, 576, 384,
"test_spritesheet_loaded_image.png")
);
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(
errctx,
akgl_render_and_compare(
sheet->texture,
image,
0, 0, 576, 384,
"test_spritesheet_loaded_image.png")
);
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);
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)
@@ -71,28 +71,28 @@ akerr_ErrorContext *test_akgl_sprite_initialize(void)
akgl_SpriteSheet *testsheet = NULL;
akgl_Sprite *testsprite = NULL;
akgl_String *tmpstr = NULL;
// Does the sprite get loaded?
// Do all frames of the sprite get loaded?
// Are all the frames of the sprite what we expect? (Surface comparison)
// Is the sprite added to the registry?
ATTEMPT {
CATCH(errctx, akgl_heap_next_spritesheet(&testsheet));
CATCH(errctx, akgl_heap_next_sprite(&testsprite));
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(testsheet, 48, 48, "assets/spritesheet.png"));
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_heap_next_spritesheet(&testsheet));
CATCH(errctx, akgl_heap_next_sprite(&testsprite));
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(testsheet, 48, 48, "assets/spritesheet.png"));
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");
} CLEANUP {
IGNORE(akgl_heap_release_sprite(testsprite));
IGNORE(akgl_heap_release_string(tmpstr));
@@ -108,65 +108,65 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void)
akgl_Sprite *testsprite2 = NULL;
akgl_String *tmpstr = NULL;
SDL_Texture *image = NULL;
// Does the sprite get loaded?
// Do all frames of the sprite get loaded?
// Are all the frames of the sprite what we expect? (Surface comparison)
// Is the sprite added to the registry?
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");
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);
FAIL_ZERO_BREAK(errctx, (testsprite->height == 48), AKERR_VALUE, "height incorrect (48 : %d)", testsprite->height);
FAIL_ZERO_BREAK(errctx, (testsprite->speed == 100000000), AKERR_VALUE, "speed incorrect (100 : %d)", testsprite->speed);
FAIL_ZERO_BREAK(errctx, (testsprite->loop == true), AKERR_VALUE, "loop incorrect (1 : %d)", testsprite->loop);
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);
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);
FAIL_ZERO_BREAK(errctx, (testsprite->speed == 100000000), AKERR_VALUE, "speed incorrect (100 : %d)", testsprite->speed);
FAIL_ZERO_BREAK(errctx, (testsprite->loop == true), AKERR_VALUE, "loop incorrect (1 : %d)", testsprite->loop);
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);
// Is it using the right spritesheet?
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
image = IMG_LoadTexture(renderer, (char *)&tmpstr->data);
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
// Is it using the right spritesheet?
CATCH(
errctx,
akgl_render_and_compare(
testsprite->sheet->texture,
image,
0, 0, 576, 384,
"test_sprite_loaded_from_json_sheet.png"
)
);
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(
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/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");
// 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");
} CLEANUP {
if ( testsprite != NULL ) {
IGNORE(akgl_heap_release_sprite(testsprite));
@@ -185,21 +185,18 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void)
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
renderer = &_akgl_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
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());
}
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)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
}
renderer->draw_texture = &akgl_render_2d_draw_texture;
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init_sprite());

View File

@@ -1,6 +1,5 @@
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/heap.h>
#include <akgl/staticstring.h>
@@ -8,19 +7,19 @@ void reset_string_heap(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);
}
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;
return 0;
}
akerr_ErrorContext *test_string_heap_error_when_no_strings_left(void)
@@ -95,7 +94,7 @@ akerr_ErrorContext *test_strcpy_to_all_strings_no_segfault(void)
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
}
akerr_ErrorContext *test_akgl_string_initialize(void)
{
@@ -111,8 +110,8 @@ akerr_ErrorContext *test_akgl_string_initialize(void)
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);
CATCH(errctx, akgl_heap_release_string(NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Failure to properly handle NULL pointer");
CATCH(errctx, akgl_heap_release_string(NULL));
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Failure to properly handle NULL pointer");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
@@ -130,10 +129,9 @@ void reset_string_heap(void)
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
printf("test_fresh_heap_gives_string ....\n");
test_fresh_heap_gives_strings();
reset_string_heap();
@@ -151,6 +149,6 @@ int main(void)
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
return 0;
}

View File

@@ -1,99 +0,0 @@
/**
* @file testutil.h
* @brief Shared assertion helpers and headless bootstrap for the libakgl test suites.
*
* The akerror ATTEMPT/CATCH/PROCESS/FINISH macros are verbose when what a test
* wants to say is "this call must fail with exactly this status". These helpers
* wrap that pattern.
*
* All of the TEST_* assertion macros expand to a `break` on failure, so they
* must be used directly inside an ATTEMPT block. Inside a `for` or `while`
* nested in an ATTEMPT they would break the inner loop instead; use
* TEST_ASSERT_FLAG in that case and check the flag after the loop.
*/
#ifndef _AKGL_TESTUTIL_H_
#define _AKGL_TESTUTIL_H_
#include <math.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/error.h>
/** @brief Fail the enclosing ATTEMPT block unless @p cond holds. */
#define TEST_ASSERT(e, cond, ...) \
if ( ! (cond) ) { \
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__); \
}
/**
* @brief Run @p stmt and require that it reports exactly @p expected.
*
* Releases whatever context @p stmt returns, so a test can assert many failure
* paths in a row without draining AKERR_ARRAY_ERROR. Pass 0 for @p expected to
* require success.
*/
#define TEST_EXPECT_STATUS(e, expected, stmt, desc) \
{ \
akerr_ErrorContext *__tec = (stmt); \
int __tst = ( __tec == NULL ) ? 0 : __tec->status; \
if ( __tec != NULL ) { \
__tec->handled = true; \
__tec = akerr_release_error(__tec); \
} \
if ( __tst != (expected) ) { \
FAIL_BREAK( \
e, \
AKGL_ERR_BEHAVIOR, \
"%s: expected status %d (%s), got %d (%s)", \
desc, \
(int)(expected), \
akerr_name_for_status((int)(expected), NULL), \
__tst, \
akerr_name_for_status(__tst, NULL)); \
} \
}
/** @brief Require that @p stmt succeeds, reporting @p desc if it does not. */
#define TEST_EXPECT_OK(e, stmt, desc) \
TEST_EXPECT_STATUS(e, 0, stmt, desc)
/**
* @brief Require that @p stmt fails, without pinning which status it reports.
*
* For paths that are delegated to a dependency, where the exact status is that
* dependency's business and asserting it would make the test brittle.
*/
#define TEST_EXPECT_ANY_ERROR(e, stmt, desc) \
{ \
akerr_ErrorContext *__tec = (stmt); \
int __tst = ( __tec == NULL ) ? 0 : __tec->status; \
if ( __tec != NULL ) { \
__tec->handled = true; \
__tec = akerr_release_error(__tec); \
} \
if ( __tst == 0 ) { \
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, "%s: expected a failure, got success", desc); \
} \
}
/** @brief Require that two floats agree to within AKGL_TEST_EPSILON. */
#define AKGL_TEST_EPSILON 0.0001f
#define TEST_ASSERT_FEQ(e, actual, expected, ...) \
if ( fabsf((float)(actual) - (float)(expected)) > AKGL_TEST_EPSILON ) { \
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__); \
}
/**
* @brief Record a failure into a flag instead of breaking.
*
* For assertions inside a loop nested in an ATTEMPT block, where `break` would
* only leave the loop.
*/
#define TEST_ASSERT_FLAG(flag, cond) \
if ( ! (cond) ) { \
(flag) = false; \
}
#endif // _AKGL_TESTUTIL_H_

View File

@@ -1,387 +0,0 @@
/**
* @file text.c
* @brief Unit tests for font loading and text measurement.
*
* Measurement needs a font but no renderer at all. Drawing needs one, but not a
* display: a software renderer over a window under the dummy video driver takes
* akgl_text_rendertextat() all the way from rasterizing to blitting, so this
* suite covers it without waiting for the offscreen harness. What is still
* missing is any assertion about the *pixels* -- that is the harness's job.
*
* 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/game.h>
#include <akgl/registry.h>
#include <akgl/renderer.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 Width and height of the offscreen target the drawing tests draw into. */
#define TEST_TARGET_SIZE 128
/** @brief The fixture font, opened once by main() and shared by every test. */
static TTF_Font *testfont = NULL;
/** @brief A 2D backend bound to a software renderer, built by main(). */
static akgl_RenderBackend testbackend;
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");
// Loading over a live name closes the font it displaces. Nothing here
// can observe the close directly -- the proof is the memcheck run --
// but the replacement itself is observable, and it is the path that
// used to abandon ten kilobytes per call.
TEST_EXPECT_OK(
errctx,
akgl_text_loadfont("testfont", TEST_FONT_PATH, TEST_FONT_SIZE),
"loading a second font over a registered name");
TEST_ASSERT(errctx,
SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "testfont", NULL) != registered,
"reloading a font under a live name left the displaced font registered");
// And the font goes back. Unloading is what makes the suite's own
// bookkeeping honest, so this is not only an API test.
TEST_EXPECT_OK(errctx, akgl_text_unloadfont("testfont"), "unloading a registered font");
TEST_ASSERT(errctx,
SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "testfont", NULL) == NULL,
"akgl_text_unloadfont left the font in AKGL_REGISTRY_FONT");
TEST_EXPECT_STATUS(errctx, AKERR_KEY, akgl_text_unloadfont("testfont"),
"unloading a font that is already gone");
TEST_EXPECT_STATUS(errctx, AKERR_KEY, akgl_text_unloadfont("never_loaded"),
"unloading a name that was never registered");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_unloadfont(NULL),
"unloading a NULL name");
} 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);
}
akerr_ErrorContext *test_text_render_backend_guards(void)
{
PREPARE_ERROR(errctx);
akgl_RenderBackend backend;
SDL_Color white = { 0xff, 0xff, 0xff, 0xff };
ATTEMPT {
// Nothing initialized the renderer at all, which is where the global
// starts and where a host that has not called akgl_game_init leaves it.
renderer = NULL;
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
"drawing text with no renderer backend");
// A backend that exists but was never given an SDL_Renderer -- the
// state a host is in between allocating one and initializing it.
memset(&backend, 0x00, sizeof(akgl_RenderBackend));
renderer = &backend;
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
"drawing text through a backend with no SDL renderer");
// A live SDL_Renderer but no methods: a host that created its own
// renderer and has not called akgl_render_bind2d() yet. This is the one
// that used to be a segfault rather than an error -- with a real
// renderer behind it the call gets all the way to a NULL function
// pointer, which is why the check has to be here and not left to SDL.
backend.sdl_renderer = testbackend.sdl_renderer;
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
"drawing text through a backend that was never bound");
renderer = &testbackend;
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_rendertextat(NULL, "hello", white, 0, 0, 0),
"drawing text with a NULL font");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_rendertextat(testfont, NULL, white, 0, 0, 0),
"drawing a NULL string");
} CLEANUP {
renderer = &testbackend;
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_text_rendertextat(void)
{
PREPARE_ERROR(errctx);
SDL_Color white = { 0xff, 0xff, 0xff, 0xff };
ATTEMPT {
// A bound backend over a software renderer is enough to draw through:
// no display, no window shown, and the whole path from rasterizing to
// blitting runs.
renderer = &testbackend;
TEST_EXPECT_OK(errctx, renderer->frame_start(renderer), "starting a frame");
TEST_EXPECT_OK(errctx, akgl_text_rendertextat(testfont, "hello", white, 0, 4, 4),
"drawing a line of text");
// Wrapping takes the other rasterizing path.
TEST_EXPECT_OK(errctx,
akgl_text_rendertextat(testfont, "one two three", white, TEST_TARGET_SIZE / 2, 0, 0),
"drawing wrapped text");
// Not asserted here: the empty string, which SDL_ttf refuses with "Text
// has zero width" on both paths, so drawing an empty line reports a
// failure rather than drawing nothing. That is filed rather than pinned
// -- see TODO.md, "Known and still open".
TEST_EXPECT_OK(errctx, renderer->frame_end(renderer), "ending the frame");
} 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");
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
ATTEMPT {
CATCH(errctx, akgl_error_init());
memset(&testbackend, 0x00, sizeof(akgl_RenderBackend));
FAIL_ZERO_BREAK(
errctx,
SDL_Init(SDL_INIT_VIDEO),
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());
// The host owns the window and libakgl binds to it, which is the
// arrangement akgl_render_bind2d exists for and the one an embedded
// interpreter drawing text is in.
FAIL_ZERO_BREAK(
errctx,
SDL_CreateWindowAndRenderer(
"net/aklabs/libakgl/test_text",
TEST_TARGET_SIZE,
TEST_TARGET_SIZE,
0,
&window,
&testbackend.sdl_renderer),
AKGL_ERR_SDL,
"Couldn't create window/renderer: %s",
SDL_GetError());
CATCH(errctx, akgl_render_bind2d(&testbackend));
CATCH(errctx, test_text_loadfont());
CATCH(errctx, test_text_measure());
CATCH(errctx, test_text_measure_wrapped());
CATCH(errctx, test_text_render_backend_guards());
CATCH(errctx, test_text_rendertextat());
} CLEANUP {
if ( testfont != NULL ) {
TTF_CloseFont(testfont);
}
TTF_Quit();
SDL_Quit();
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -18,17 +18,15 @@ akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void)
json_error_t jsonerr;
akgl_String *tmpstr = NULL;
int propnum;
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_heap_next_string(&tmpstr));
snprintf(
(char *)&tmpstr->data,
AKGL_MAX_STRING_LENGTH,
"%s%s",
SDL_GetBasePath(),
"assets/snippets/test_tilemap_get_json_tilemap_property.json"
"assets/snippets/test_tilemap_akgl_get_json_tilemap_property.json"
);
jsondoc = json_load_file((char *)&tmpstr->data, 0, (json_error_t *)&jsonerr);
FAIL_ZERO_BREAK(errctx, jsondoc, AKERR_NULLPOINTER, "Failure loading json fixture: %s", (char *)jsonerr.text);
@@ -87,34 +85,32 @@ akerr_ErrorContext *test_akgl_tilemap_compute_tileset_offsets(void)
10 // Tile 2 Y
};
int *comparison_ptrs[8] = {
&gamemap->tilesets[0].tile_offsets[0][0], // Tile 0 X
&gamemap->tilesets[0].tile_offsets[0][1], // Tile 0 Y
&gamemap->tilesets[0].tile_offsets[1][0], // Tile 1 X
&gamemap->tilesets[0].tile_offsets[1][1], // Tile 0 Y
&gamemap->tilesets[0].tile_offsets[2][0], // Tile 2 X
&gamemap->tilesets[0].tile_offsets[2][1], // Tile 2 Y
&gamemap->tilesets[0].tile_offsets[3][0], // Tile 3 X
&gamemap->tilesets[0].tile_offsets[3][1], // Tile 3 Y
&gamemap.tilesets[0].tile_offsets[0][0], // Tile 0 X
&gamemap.tilesets[0].tile_offsets[0][1], // Tile 0 Y
&gamemap.tilesets[0].tile_offsets[1][0], // Tile 1 X
&gamemap.tilesets[0].tile_offsets[1][1], // Tile 0 Y
&gamemap.tilesets[0].tile_offsets[2][0], // Tile 2 X
&gamemap.tilesets[0].tile_offsets[2][1], // Tile 2 Y
&gamemap.tilesets[0].tile_offsets[3][0], // Tile 3 X
&gamemap.tilesets[0].tile_offsets[3][0], // Tile 3 Y
};
int i = 0;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
gamemap->tilesets[0].tilecount = 4;
gamemap->tilesets[0].columns = 2;
gamemap->tilesets[0].firstgid = 1;
gamemap->tilesets[0].imageheight = 20;
gamemap->tilesets[0].imagewidth = 20;
gamemap->tilesets[0].tilecount = 4;
gamemap->tilesets[0].tileheight = 10;
gamemap->tilesets[0].tilewidth = 10;
gamemap->tilesets[0].spacing = 0;
gamemap->tilesets[0].margin = 0;
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
gamemap.tilesets[0].tilecount = 4;
gamemap.tilesets[0].columns = 2;
gamemap.tilesets[0].firstgid = 1;
gamemap.tilesets[0].imageheight = 20;
gamemap.tilesets[0].imagewidth = 20;
gamemap.tilesets[0].tilecount = 4;
gamemap.tilesets[0].tileheight = 10;
gamemap.tilesets[0].tilewidth = 10;
gamemap.tilesets[0].spacing = 0;
gamemap.tilesets[0].margin = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(gamemap, 0));
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(&gamemap, 0));
// Tile 0 X offset
for ( i = 0; i < 8; i++ ) {
FAIL_NONZERO_BREAK(
@@ -143,18 +139,15 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_objects(void)
json_error_t errdata;
akgl_Actor *testactor;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
CATCH(errctx, akgl_get_json_array_index_object(layers, 1, &objectlayer));
CATCH(errctx, akgl_tilemap_load_layer_objects(gamemap, objectlayer, 1, pathstr));
CATCH(errctx, akgl_tilemap_load_layer_objects(&gamemap, objectlayer, 1));
testactor = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL);
FAIL_ZERO_BREAK(
@@ -193,26 +186,23 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
json_t *tiledata = NULL;
json_error_t errdata;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
CATCH(errctx, akgl_get_json_array_index_object(layers, 0, &tilelayer));
CATCH(errctx, akgl_get_json_array_value(tilelayer, "data", &tiledata));
CATCH(errctx, akgl_tilemap_load_layer_tile(gamemap, tilelayer, 0, pathstr));
if ( (gamemap->layers[0].data[0] != 1) ||
(gamemap->layers[0].data[1] != 2) ||
(gamemap->layers[0].data[2] != 3) ||
(gamemap->layers[0].data[3] != 4) ) {
CATCH(errctx, akgl_tilemap_load_layer_tile(&gamemap, tilelayer, 0));
if ( (gamemap.layers[0].data[0] != 1) ||
(gamemap.layers[0].data[1] != 2) ||
(gamemap.layers[0].data[2] != 3) ||
(gamemap.layers[0].data[3] != 4) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)");
}
}
} CLEANUP {
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
@@ -233,65 +223,62 @@ akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
json_error_t errdata;
int i = 0;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_tilemap_load_layers(gamemap, doc, pathstr));
CATCH(errctx, akgl_tilemap_load_layers(&gamemap, doc));
FAIL_NONZERO_BREAK(
errctx,
(gamemap->numlayers != 3),
(gamemap.numlayers != 3),
AKERR_VALUE,
"Map layer count incorrect"
);
for ( i = 0; i < gamemap->numlayers; i++ ) {
if ( (gamemap->layers[i].opacity != 1) ||
(gamemap->layers[i].visible != true) ||
(gamemap->layers[i].id != (i + 1)) ||
(gamemap->layers[i].x != 0) ||
(gamemap->layers[i].y != 0) ) {
for ( i = 0; i < gamemap.numlayers; i++ ) {
if ( (gamemap.layers[i].opacity != 1) ||
(gamemap.layers[i].visible != true) ||
(gamemap.layers[i].id != (i + 1)) ||
(gamemap.layers[i].x != 0) ||
(gamemap.layers[i].y != 0) ) {
FAIL(errctx, AKERR_VALUE, "Map layer data loaded incorrectly (see gdb)");
goto _test_akgl_tilemap_load_layers_cleanup;
}
}
// Layer 2 should have 1 object loaded
if ( (gamemap->layers[1].objects[0].actorptr != SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL)) ||
(gamemap->layers[1].objects[1].name[0] != '\0' ) ||
(gamemap->layers[1].objects[1].id != 0) ) {
if ( (gamemap.layers[1].objects[0].actorptr != SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL)) ||
(gamemap.layers[1].objects[1].name[0] != '\0' ) ||
(gamemap.layers[1].objects[1].id != 0) ) {
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
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
if ( gamemap->layers[0].objects[i].id != 0 ) {
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
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");
goto _test_akgl_tilemap_load_layers_cleanup;
}
}
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
if ( gamemap->layers[2].objects[i].id != 0 ) {
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
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");
goto _test_akgl_tilemap_load_layers_cleanup;
}
}
// Layers 1 and 3 should have tile data
if ( (gamemap->layers[0].data[0] != 1) ||
(gamemap->layers[0].data[1] != 2) ||
(gamemap->layers[0].data[2] != 3) ||
(gamemap->layers[0].data[3] != 4) ||
(gamemap->layers[2].data[0] != 0) ||
(gamemap->layers[2].data[1] != 5) ||
(gamemap->layers[2].data[2] != 0) ||
(gamemap->layers[2].data[3] != 6)
if ( (gamemap.layers[0].data[0] != 1) ||
(gamemap.layers[0].data[1] != 2) ||
(gamemap.layers[0].data[2] != 3) ||
(gamemap.layers[0].data[3] != 4) ||
(gamemap.layers[2].data[0] != 0) ||
(gamemap.layers[2].data[1] != 5) ||
(gamemap.layers[2].data[2] != 0) ||
(gamemap.layers[2].data[3] != 6)
) {
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 {
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
@@ -312,43 +299,40 @@ akerr_ErrorContext *test_akgl_tilemap_load_tilesets(void)
json_error_t errdata;
SDL_Texture *image = NULL;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_tilemap_load_tilesets(gamemap, doc, pathstr));
FAIL_NONZERO_BREAK(errctx, (gamemap->numtilesets != 1), AKERR_VALUE, "Incorrect number of tilesets loaded for map");
if ( (gamemap->tilesets[0].columns != 48 ) ||
(gamemap->tilesets[0].firstgid != 1) ||
(gamemap->tilesets[0].imageheight != 576) ||
(gamemap->tilesets[0].imagewidth != 768) ||
(gamemap->tilesets[0].margin != 0) ||
(gamemap->tilesets[0].spacing != 0) ||
(gamemap->tilesets[0].tilecount != 1728) ||
(gamemap->tilesets[0].tileheight != 16) ||
(gamemap->tilesets[0].tilewidth != 16) ) {
CATCH(errctx, akgl_tilemap_load_tilesets(&gamemap, doc));
FAIL_NONZERO_BREAK(errctx, (gamemap.numtilesets != 1), AKERR_VALUE, "Incorrect number of tilesets loaded for map");
if ( (gamemap.tilesets[0].columns != 48 ) ||
(gamemap.tilesets[0].firstgid != 1) ||
(gamemap.tilesets[0].imageheight != 576) ||
(gamemap.tilesets[0].imagewidth != 768) ||
(gamemap.tilesets[0].margin != 0) ||
(gamemap.tilesets[0].spacing != 0) ||
(gamemap.tilesets[0].tilecount != 1728) ||
(gamemap.tilesets[0].tileheight != 16) ||
(gamemap.tilesets[0].tilewidth != 16) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Tileset loaded with incorrect values");
}
FAIL_NONZERO_BREAK(
errctx,
strcmp((char *)&gamemap->tilesets[0].name, "World_A1"),
strcmp((char *)&gamemap.tilesets[0].name, "World_A1"),
AKERR_VALUE,
"Tileset loaded with incorrect name");
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/World_A1.png");
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&pathstr->data);
image = IMG_LoadTexture(renderer, (char *)&pathstr->data);
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
CATCH(
errctx,
akgl_render_and_compare(
gamemap->tilesets[0].texture,
gamemap.tilesets[0].texture,
image,
0, 0, 768, 576,
"test_akgl_tilemap_loaded_tileset.png")
@@ -370,12 +354,10 @@ akerr_ErrorContext *test_akgl_tilemap_load(void)
{
PREPARE_ERROR(errctx);
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_tilemap_load("assets/testmap.tmj", gamemap));
CATCH(errctx, akgl_tilemap_load("assets/testmap.tmj", &gamemap));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
@@ -385,10 +367,8 @@ akerr_ErrorContext *test_akgl_tilemap_load(void)
akerr_ErrorContext *test_akgl_tilemap_draw(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
@@ -399,10 +379,8 @@ akerr_ErrorContext *test_akgl_tilemap_draw(void)
akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
@@ -412,21 +390,17 @@ akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void)
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
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());
}
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)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
}
renderer->draw_texture = &akgl_render_2d_draw_texture;
CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_heap_init());
@@ -446,5 +420,6 @@ int main(void)
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -1,45 +1,16 @@
#include <SDL3/SDL.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/heap.h>
#include <akgl/staticstring.h>
#include <akgl/util.h>
/**
* @brief How many entries of AKERR_ARRAY_ERROR are currently held by somebody.
*
* The error contexts are a fixed pool exactly like the object pools: a context
* is in use while its reference count is non-zero, and a function that finishes
* without releasing one has leaked a slot out of AKERR_MAX_ARRAY_ERROR.
*/
static int live_error_contexts(void)
{
int live = 0;
int i = 0;
for ( i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
if ( AKERR_ARRAY_ERROR[i].refcount != 0 ) {
live += 1;
}
}
return live;
}
akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
{
RectanglePoints points;
// Zeroed for the same reason as the fixtures in
// test_akgl_collide_point_rectangle_nullpointers: the last case here is a
// real call, and feeding it stack garbage is noise under `memcheck`.
SDL_FRect testrect = {.x = 0, .y = 0, .w = 0, .h = 0};
SDL_FRect testrect;
PREPARE_ERROR(errctx);
memset((void *)&points, 0x00, sizeof(RectanglePoints));
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(NULL, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with all NULL pointers");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with all NULL pointers");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
@@ -48,16 +19,16 @@ akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(NULL, &testrect));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL SDL_FRect pointer");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL SDL_FRect pointer");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
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, AKERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL RectanglePoints pointer");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
@@ -78,7 +49,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_math(void)
RectanglePoints points;
SDL_FRect testrect = {.x = 0, .y = 0, .w = 32, .h = 32};
memset((void *)&points, 0x00, sizeof(RectanglePoints));
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&points, &testrect));
@@ -92,7 +63,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_math(void)
points.bottomright.y != 32 ) {
FAIL_BREAK(
errctx,
AKGL_ERR_BEHAVIOR,
AKERR_BEHAVIOR,
"akgl_rectangle_points incorrectly calculated points for {x=0, y=0, w=32, h=32} to {topleft={%d, %d}, topright={%d, %d}, bottomleft={%d, %d}, bottomright={%d, %d}}",
points.topleft.x, points.topleft.y,
points.topright.x, points.topright.y,
@@ -108,21 +79,15 @@ akerr_ErrorContext *test_akgl_rectangle_points_math(void)
akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
{
// Zeroed rather than left as whatever the stack held. The last case in this
// function is a real call with real arguments, and reading uninitialised
// floats out of it is sixteen findings under `memcheck` for a test that is
// not about coordinates at all.
point testpoint = { .x = 0, .y = 0 };
point testpoint;
RectanglePoints testrectpoints;
bool testcollide = false;
bool testcollide;
PREPARE_ERROR(errctx);
memset(&testrectpoints, 0x00, sizeof(RectanglePoints));
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, *, NULL) failed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(*, *, NULL) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
@@ -131,7 +96,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, NULL, &testcollide));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, NULL, *) failed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(*, NULL, *) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
@@ -140,7 +105,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(NULL, &testrectpoints, &testcollide));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, *, *) failed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, *, *) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
@@ -149,7 +114,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(NULL, NULL, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, NULL, NULL) failed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, NULL, NULL) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
@@ -172,19 +137,19 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void)
RectanglePoints testrectpoints;
bool testcollide = false;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&testrectpoints, &testrect));
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
}
testpoint.x = 48;
testpoint.y = 48;
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
if ( testcollide == true ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Invalid collision reported");
}
} CLEANUP {
} PROCESS(errctx) {
@@ -194,15 +159,15 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void)
akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
{
SDL_FRect testrect1 = {.x = 0, .y = 0, .w = 0, .h = 0};
SDL_FRect testrect2 = {.x = 0, .y = 0, .w = 0, .h = 0};
bool testcollide = false;
SDL_FRect testrect1;
SDL_FRect testrect2;
bool testcollide;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, *, NULL) failed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(*, *, NULL) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
@@ -211,7 +176,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
ATTEMPT {
CATCH(errctx, akgl_collide_rectangles(&testrect1, NULL, &testcollide));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, NULL, *) failed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(*, NULL, *) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
@@ -220,7 +185,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
ATTEMPT {
CATCH(errctx, akgl_collide_rectangles(NULL, &testrect2, &testcollide));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, *, *) failed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(NULL, *, *) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
@@ -229,7 +194,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
ATTEMPT {
CATCH(errctx, akgl_collide_rectangles(NULL, NULL, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, NULL, NULL) failed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(NULL, NULL, NULL) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
@@ -250,21 +215,21 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
SDL_FRect testrect1 = { .x = 0, .y = 0, .w = 32, .h = 32};
SDL_FRect testrect2 = { .x = 30, .y = 30, .w = 40, .h = 40};
bool testcollide = false;
PREPARE_ERROR(errctx);
ATTEMPT {
// Collision overlapping on the top left
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping on the top right
testrect1.x = 64;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping on the bottom left
@@ -272,7 +237,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
testrect1.y = 32;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping on the bottom right
@@ -280,7 +245,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
testrect1.y = 32;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping the top edge
@@ -290,7 +255,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
testrect1.h = 32;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping the left edge
@@ -300,7 +265,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
testrect1.h = 80;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping the right edge
@@ -310,7 +275,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
testrect1.h = 80;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping the bottom edge
@@ -320,7 +285,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
testrect1.h = 32;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
}
// Not colliding
@@ -330,9 +295,9 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
testrect1.h = 16;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == true ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Invalid collision reported");
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
@@ -340,64 +305,15 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
}
/**
* @brief Resolving a path through the root fallback must give its context back.
*
* akgl_path_relative tries the working directory first and falls back to
* resolving against @p root when that reports ENOENT. That fallback used to be
* taken by returning from inside the HANDLE block, which skips the
* RELEASE_ERROR that FINISH ends with -- so every call down that branch leaked
* one entry of AKERR_ARRAY_ERROR, and the 129th call aborted the whole process
* with "Unable to pull an error context from the array!". A single map load
* resolves several paths this way.
*
* The loop runs well past AKERR_MAX_ARRAY_ERROR on purpose: at the old
* behaviour this test does not fail, it terminates the suite.
*/
akerr_ErrorContext *test_akgl_path_relative_releases_contexts(void)
{
PREPARE_ERROR(errctx);
akgl_String *dst = NULL;
int before = 0;
int after = 0;
int i = 0;
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_heap_next_string(&dst));
before = live_error_contexts();
for ( i = 0; i < (AKERR_MAX_ARRAY_ERROR * 2); i++ ) {
PASS(errctx, akgl_path_relative("assets", "testcharacter.json", dst));
}
after = live_error_contexts();
ATTEMPT {
if ( after != before ) {
FAIL_BREAK(
errctx,
AKGL_ERR_BEHAVIOR,
"akgl_path_relative leaked %d error context(s) over %d root-fallback resolutions",
(after - before),
(AKERR_MAX_ARRAY_ERROR * 2));
}
} CLEANUP {
IGNORE(akgl_heap_release_string(dst));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, test_akgl_rectangle_points_nullpointers());
CATCH(errctx, test_akgl_rectangle_points_math());
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());
CATCH(errctx, test_akgl_collide_rectangles_nullpointers());
CATCH(errctx, test_akgl_collide_rectangles_logic());
CATCH(errctx, test_akgl_path_relative_releases_contexts());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);

View File

@@ -1,108 +0,0 @@
/**
* @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 pathbuf[4096];
char cwdbuf[1024];
if ( argc < 3 ) {
SDL_Log("charviewer [CHARACTER_FILE] [SPRITE ...]");
return SDL_APP_FAILURE;
@@ -39,20 +39,21 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
characterjson = argv[1];
memset((char *)&pathbuf, 0x00, 4096);
memset((char *)&cwdbuf, 0x00, 1024);
ATTEMPT {
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");
strcpy((char *)&game.name, "charviewer");
strcpy((char *)&game.version, "0.0.1");
strcpy((char *)&game.uri, "net.aklabs.libakgl.charviewer");
game.screenwidth = 640;
game.screenheight = 480;
CATCH(errctx, akgl_game_init());
CATCH(errctx, akgl_set_property("game.screenwidth", "640"));
CATCH(errctx, akgl_set_property("game.screenheight", "480"));
CATCH(errctx, akgl_render_init2d(renderer));
CATCH(errctx, akgl_physics_init_null(physics));
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
} CLEANUP {
} PROCESS(errctx) {
@@ -79,7 +80,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
memset((char *)&pathbuf, 0x00, 4096);
}
ATTEMPT {
ATTEMPT {
if ( characterjson[0] != '/' ) {
sprintf((char *)&pathbuf, "%s/%s", (char *)&cwdbuf, characterjson);
} else {
@@ -93,7 +94,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
AKGL_REGISTRY_CHARACTER,
"little guy",
NULL);
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKGL_ERR_REGISTRY, "Can't load character 'little guy' from the registry");
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKERR_REGISTRY, "Can't load character 'little guy' from the registry");
actorptr->movement_controls_face = false;
actorptr->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN);
actorptr->x = 320;
@@ -105,7 +106,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
SDL_Log("Opening gamepad %d", gamepadids[0]);
FAIL_ZERO_BREAK(errctx, SDL_OpenGamepad(gamepadids[0]), AKGL_ERR_SDL, "%s", SDL_GetError());
CATCH(errctx, akgl_controller_default(0, "player", 0, gamepadids[0]));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
@@ -123,7 +124,7 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
ATTEMPT {
FAIL_ZERO_BREAK(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate pointer");
FAIL_ZERO_BREAK(errctx, event, AKERR_NULLPOINTER, "NULL event pointer");
CATCH(errctx, akgl_controller_handle_event(appstate, event));
if (event->type == SDL_EVENT_QUIT) {
return SDL_APP_SUCCESS; /* end the program, reporting success to the OS. */
@@ -140,13 +141,13 @@ SDL_AppResult SDL_AppIterate(void *appstate)
akgl_Iterator opflags;
PREPARE_ERROR(errctx);
AKGL_BITMASK_CLEAR(opflags.flags);
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_UPDATE);
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_RENDER);
akgl_draw_background((int)camera->w, (int)camera->h);
akgl_draw_background(game.screenwidth, game.screenheight);
ATTEMPT {
CATCH(errctx, akgl_tilemap_draw(gamemap, camera, i));
CATCH(errctx, akgl_tilemap_draw(renderer, (akgl_Tilemap *)&gamemap, &camera, i));
SDL_EnumerateProperties(AKGL_REGISTRY_ACTOR, &akgl_registry_iterate_actor, (void *)&opflags);
} CLEANUP {
} PROCESS(errctx) {
@@ -154,7 +155,7 @@ SDL_AppResult SDL_AppIterate(void *appstate)
LOG_ERROR(errctx);
return SDL_APP_FAILURE;
} FINISH_NORETURN(errctx);
SDL_RenderPresent(renderer->sdl_renderer);
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}