Compare commits
33 Commits
bugfix/phy
...
42b60f725d
| Author | SHA1 | Date | |
|---|---|---|---|
|
42b60f725d
|
|||
|
4208d9d471
|
|||
|
f56f88710f
|
|||
|
2a3ca48d8f
|
|||
|
dba0f8db89
|
|||
|
1ddc64010a
|
|||
|
17e6e04c79
|
|||
|
5f03475e0f
|
|||
|
6dfe7487ae
|
|||
|
c2b16d3c18
|
|||
|
2be9831c0c
|
|||
|
c5a7b6053d
|
|||
|
9b124f2e27
|
|||
|
b014eb2360
|
|||
|
e423f9594e
|
|||
|
772f960865
|
|||
|
28bde4176d
|
|||
|
8d21d5c7dd
|
|||
|
bc782fbffe
|
|||
|
22162db2da
|
|||
|
6f6bd2d563
|
|||
|
ff88f48fa2
|
|||
|
549b27d3eb
|
|||
|
a2995e81df
|
|||
|
dc1bd0a798
|
|||
|
3854b33750
|
|||
|
cf9ebb206f
|
|||
|
74867ea82e
|
|||
|
4e510dd6d6
|
|||
|
dca03cb50d
|
|||
|
652ee4cdf3
|
|||
|
9fed59c4c8
|
|||
|
941eeb2493
|
16
.dir-locals.el
Normal file
16
.dir-locals.el
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
;;; Directory Local Variables -*- no-byte-compile: t -*-
|
||||||
|
;;; See AGENTS.md -> "Coding Style" for the rationale.
|
||||||
|
;;;
|
||||||
|
;;; The canonical style is cc-mode "stroustrup" with tabs enabled: 4 columns per
|
||||||
|
;;; level, tabs 8 columns wide, so depth 1 is four spaces, depth 2 is one tab,
|
||||||
|
;;; depth 3 is a tab plus four spaces. A correctly formatted file is a fixed
|
||||||
|
;;; point of `indent-region' under these settings.
|
||||||
|
;;;
|
||||||
|
;;; Reindent a whole file with C-x h C-M-\, or the tree with
|
||||||
|
;;; `scripts/reindent.sh'.
|
||||||
|
|
||||||
|
((c-mode . ((c-file-style . "stroustrup")
|
||||||
|
(indent-tabs-mode . t)
|
||||||
|
(tab-width . 8)
|
||||||
|
(fill-column . 100)
|
||||||
|
(require-final-newline . t))))
|
||||||
89
.gitea/workflows/ci.yaml
Normal file
89
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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
|
||||||
|
- name: Test (JUnit)
|
||||||
|
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
|
||||||
|
|
||||||
|
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
10
.gitignore
vendored
@@ -1,3 +1,11 @@
|
|||||||
./build/*
|
# A leading ./ is not a valid gitignore pattern, so "./build/*" matched nothing
|
||||||
|
# and every out-of-tree build tree showed up as untracked. This also covers the
|
||||||
|
# instrumented trees scripts/coverage.py and the AKGL_COVERAGE option create.
|
||||||
|
build*/
|
||||||
.aider*
|
.aider*
|
||||||
*~
|
*~
|
||||||
|
|
||||||
|
# Generated by configure_file into the build tree. include/ precedes the build
|
||||||
|
# tree on the include path, so a stray copy here would silently shadow the real
|
||||||
|
# one and pin every consumer to whatever version it was generated at.
|
||||||
|
include/akgl/version.h
|
||||||
|
|||||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -15,10 +15,10 @@
|
|||||||
url = git@github.com:libsdl-org/SDL_ttf.git
|
url = git@github.com:libsdl-org/SDL_ttf.git
|
||||||
[submodule "deps/libsdlerror"]
|
[submodule "deps/libsdlerror"]
|
||||||
path = deps/libakerror
|
path = deps/libakerror
|
||||||
url = https://source.home.aklabs.net/andrew/libsdlerror.git
|
url = https://source.starfort.tech/andrew/libakerror.git
|
||||||
[submodule "deps/libakstdlib"]
|
[submodule "deps/libakstdlib"]
|
||||||
path = deps/libakstdlib
|
path = deps/libakstdlib
|
||||||
url = https://source.home.aklabs.net/andrew/libakstdlib.git
|
url = https://source.starfort.tech/andrew/libakstdlib.git
|
||||||
[submodule "deps/jansson"]
|
[submodule "deps/jansson"]
|
||||||
path = deps/jansson
|
path = deps/jansson
|
||||||
url = git@github.com:akheron/jansson.git
|
url = git@github.com:akheron/jansson.git
|
||||||
|
|||||||
301
AGENTS.md
Normal file
301
AGENTS.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# 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 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
262
CMakeLists.txt
262
CMakeLists.txt
@@ -1,10 +1,39 @@
|
|||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
project(akgl LANGUAGES C)
|
# The single source of truth for the library version. It drives the generated
|
||||||
|
# include/akgl/version.h, the shared library's VERSION and SOVERSION, and the
|
||||||
|
# Version field in akgl.pc. Bump it here and nowhere else.
|
||||||
|
project(akgl VERSION 0.1.0 LANGUAGES C)
|
||||||
|
|
||||||
include(CTest)
|
include(CTest)
|
||||||
|
option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||||
|
|
||||||
|
# Vendored projects own their test suites. Suppress their CTest registration
|
||||||
|
# while embedded so the top-level suite contains only targets built here.
|
||||||
|
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()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
add_subdirectory(deps/jansson EXCLUDE_FROM_ALL)
|
add_subdirectory(deps/jansson EXCLUDE_FROM_ALL)
|
||||||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||||||
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
|
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
|
||||||
@@ -13,6 +42,11 @@ add_subdirectory(deps/SDL_image EXCLUDE_FROM_ALL)
|
|||||||
add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL)
|
add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL)
|
||||||
add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL)
|
add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
else()
|
else()
|
||||||
|
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
@@ -29,11 +63,19 @@ else()
|
|||||||
if(NOT TARGET SDL3_ttf::SDL3_ttf)
|
if(NOT TARGET SDL3_ttf::SDL3_ttf)
|
||||||
find_package(SDL3_ttf REQUIRED)
|
find_package(SDL3_ttf REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
# No version here: libakerror ships no akerrorConfigVersion.cmake, so asking
|
||||||
|
# for one makes find_package reject every install. The floor is enforced by
|
||||||
|
# the #error in include/akgl/error.h instead, which feature-tests
|
||||||
|
# AKERR_FIRST_CONSUMER_STATUS.
|
||||||
if(NOT TARGET akerror::akerror)
|
if(NOT TARGET akerror::akerror)
|
||||||
find_package(akerror REQUIRED)
|
find_package(akerror REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
# 0.1 rather than bare: libakstdlib 0.1.0 ships an akstdlibConfigVersion.cmake
|
||||||
|
# with SameMinorVersion compatibility, mirroring its soname, so this accepts
|
||||||
|
# any 0.1.x and refuses 0.2 and 1.0. Unversioned, this path would silently
|
||||||
|
# accept an ABI-incompatible libakstdlib.
|
||||||
if(NOT TARGET akstdlib::akstdlib)
|
if(NOT TARGET akstdlib::akstdlib)
|
||||||
find_package(akstdlib REQUIRED)
|
find_package(akstdlib 0.1 REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
if(NOT TARGET jansson::jansson)
|
if(NOT TARGET jansson::jansson)
|
||||||
find_package(jansson)
|
find_package(jansson)
|
||||||
@@ -47,6 +89,13 @@ set(exec_prefix "\${prefix}")
|
|||||||
set(libdir "\${exec_prefix}/lib")
|
set(libdir "\${exec_prefix}/lib")
|
||||||
set(includedir "\${prefix}/include")
|
set(includedir "\${prefix}/include")
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akgl.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc @ONLY)
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akgl.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc @ONLY)
|
||||||
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/include/akgl/version.h.in
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h @ONLY)
|
||||||
|
|
||||||
|
# Tests use both relative paths and SDL_GetBasePath(), so stage fixtures beside
|
||||||
|
# test executables in every out-of-tree build.
|
||||||
|
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/tests/assets"
|
||||||
|
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
|
||||||
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT ${GAMECONTROLLERDB_H}
|
OUTPUT ${GAMECONTROLLERDB_H}
|
||||||
@@ -60,50 +109,136 @@ add_library(akgl SHARED
|
|||||||
deps/semver/semver.c
|
deps/semver/semver.c
|
||||||
src/actor.c
|
src/actor.c
|
||||||
src/actor_state_string_names.c
|
src/actor_state_string_names.c
|
||||||
|
src/audio.c
|
||||||
src/text.c
|
src/text.c
|
||||||
src/assets.c
|
src/assets.c
|
||||||
src/character.c
|
src/character.c
|
||||||
src/draw.c
|
src/draw.c
|
||||||
|
src/error.c
|
||||||
src/game.c
|
src/game.c
|
||||||
src/controller.c
|
src/controller.c
|
||||||
src/heap.c
|
src/heap.c
|
||||||
src/json_helpers.c
|
src/json_helpers.c
|
||||||
src/registry.c
|
src/registry.c
|
||||||
|
src/renderer.c
|
||||||
|
src/physics.c
|
||||||
src/sprite.c
|
src/sprite.c
|
||||||
src/staticstring.c
|
src/staticstring.c
|
||||||
src/tilemap.c
|
src/tilemap.c
|
||||||
src/util.c
|
src/util.c
|
||||||
|
src/version.c
|
||||||
${GAMECONTROLLERDB_H}
|
${GAMECONTROLLERDB_H}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# While the major version is 0 the ABI is not stable across minor releases, so
|
||||||
|
# the soname carries major.minor -- libakgl.so.0.1. A plain SOVERSION 0 would
|
||||||
|
# claim 0.1.0 and 0.2.0 are interchangeable, which is exactly the silent
|
||||||
|
# mispairing the soname is here to prevent. At 1.0.0 this becomes the major
|
||||||
|
# alone, matching libakerror.
|
||||||
|
if(PROJECT_VERSION_MAJOR EQUAL 0)
|
||||||
|
set(AKGL_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||||
|
else()
|
||||||
|
set(AKGL_SOVERSION "${PROJECT_VERSION_MAJOR}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set_target_properties(akgl PROPERTIES
|
||||||
|
VERSION ${PROJECT_VERSION}
|
||||||
|
SOVERSION ${AKGL_SOVERSION}
|
||||||
|
)
|
||||||
|
|
||||||
add_library(akgl::akgl ALIAS akgl)
|
add_library(akgl::akgl ALIAS akgl)
|
||||||
|
|
||||||
add_executable(charviewer util/charviewer.c)
|
add_executable(charviewer util/charviewer.c)
|
||||||
add_executable(test_actor tests/actor.c)
|
add_executable(akgl_test_semver_unit deps/semver/semver_unit.c)
|
||||||
add_executable(test_bitmasks tests/bitmasks.c)
|
|
||||||
add_executable(test_character tests/character.c)
|
# Every suite here is a standalone C program named tests/<name>.c, built as
|
||||||
add_executable(test_registry tests/registry.c)
|
# akgl_test_<name> and registered with CTest under <name>.
|
||||||
add_executable(test_sprite tests/sprite.c)
|
set(AKGL_TEST_SUITES
|
||||||
add_executable(test_staticstring tests/staticstring.c)
|
actor
|
||||||
add_executable(test_tilemap tests/tilemap.c)
|
audio
|
||||||
add_executable(test_util tests/util.c)
|
bitmasks
|
||||||
add_executable(test_semver_unit deps/semver/semver_unit.c)
|
character
|
||||||
add_test(NAME actor COMMAND test_actor)
|
controller
|
||||||
add_test(NAME bitmasks COMMAND test_bitmasks)
|
draw
|
||||||
add_test(NAME character COMMAND test_character)
|
error
|
||||||
add_test(NAME registry COMMAND test_registry)
|
game
|
||||||
add_test(NAME sprite COMMAND test_sprite)
|
heap
|
||||||
add_test(NAME staticstring COMMAND test_staticstring)
|
json_helpers
|
||||||
add_test(NAME tilemap COMMAND test_tilemap)
|
physics
|
||||||
add_test(NAME util COMMAND test_util)
|
registry
|
||||||
add_test(NAME semver_unit COMMAND test_semver_unit)
|
sprite
|
||||||
|
staticstring
|
||||||
|
text
|
||||||
|
tilemap
|
||||||
|
util
|
||||||
|
version
|
||||||
|
)
|
||||||
|
|
||||||
|
# The executables carry an akgl_ prefix but the CTest names do not: a vendored
|
||||||
|
# dependency is free to ship its own tests/version.c, and libakstdlib now does.
|
||||||
|
# Its target is created by add_subdirectory even though EXCLUDE_FROM_ALL keeps
|
||||||
|
# it from being built, so an unprefixed test_version here is a configure error.
|
||||||
|
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||||
|
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)
|
||||||
|
|
||||||
|
set_tests_properties(
|
||||||
|
${AKGL_TEST_SUITES} semver_unit
|
||||||
|
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 30
|
||||||
|
)
|
||||||
|
|
||||||
# Specify include directories for the library's headers (if applicable)
|
# Specify include directories for the library's headers (if applicable)
|
||||||
target_include_directories(akgl PUBLIC
|
target_include_directories(akgl PUBLIC
|
||||||
include/
|
include/
|
||||||
deps/semver/
|
deps/semver/
|
||||||
|
# akgl/version.h is generated by configure_file, so it lives in the build tree
|
||||||
|
# rather than beside the headers it is included from.
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/include/
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(AKGL_COVERAGE)
|
||||||
|
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.
|
||||||
|
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
|
target_link_libraries(akgl
|
||||||
PUBLIC
|
PUBLIC
|
||||||
SDL3::SDL3
|
SDL3::SDL3
|
||||||
@@ -115,25 +250,92 @@ target_link_libraries(akgl
|
|||||||
jansson::jansson
|
jansson::jansson
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||||
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(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_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_include_directories(akgl_test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||||
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)
|
endforeach()
|
||||||
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)
|
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)
|
||||||
|
|
||||||
set(main_lib_dest "lib/akgl-${MY_LIBRARY_VERSION}")
|
# 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.
|
||||||
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||||
|
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)
|
||||||
|
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}
|
||||||
|
PROPERTIES ENVIRONMENT_MODIFICATION "${AKGL_TEST_ENV_MOD}"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
string(REPLACE ";" ":" AKGL_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
|
||||||
|
set_tests_properties(
|
||||||
|
${AKGL_TEST_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()
|
||||||
|
|
||||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/")
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/")
|
||||||
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h DESTINATION "include/akgl/")
|
||||||
install(TARGETS akgl DESTINATION "lib/")
|
install(TARGETS akgl DESTINATION "lib/")
|
||||||
install(FILES "deps/semver/semver.h" DESTINATION "include/")
|
install(FILES "deps/semver/semver.h" DESTINATION "include/")
|
||||||
install(FILES "include/akgl/actor.h" DESTINATION "include/akgl/")
|
install(FILES "include/akgl/actor.h" DESTINATION "include/akgl/")
|
||||||
install(FILES "include/akgl/types.h" DESTINATION "include/akgl/")
|
install(FILES "include/akgl/types.h" DESTINATION "include/akgl/")
|
||||||
install(FILES "include/akgl/text.h" DESTINATION "include/akgl/")
|
install(FILES "include/akgl/text.h" DESTINATION "include/akgl/")
|
||||||
install(FILES "include/akgl/assets.h" DESTINATION "include/akgl/")
|
install(FILES "include/akgl/assets.h" DESTINATION "include/akgl/")
|
||||||
|
install(FILES "include/akgl/audio.h" DESTINATION "include/akgl/")
|
||||||
install(FILES "include/akgl/character.h" DESTINATION "include/akgl/")
|
install(FILES "include/akgl/character.h" DESTINATION "include/akgl/")
|
||||||
install(FILES "include/akgl/error.h" DESTINATION "include/akgl/")
|
install(FILES "include/akgl/error.h" DESTINATION "include/akgl/")
|
||||||
install(FILES "include/akgl/draw.h" DESTINATION "include/akgl/")
|
install(FILES "include/akgl/draw.h" DESTINATION "include/akgl/")
|
||||||
@@ -142,6 +344,8 @@ install(FILES "include/akgl/controller.h" DESTINATION "include/akgl/")
|
|||||||
install(FILES "include/akgl/heap.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/iterator.h" DESTINATION "include/akgl/")
|
||||||
install(FILES "include/akgl/json_helpers.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/registry.h" DESTINATION "include/akgl/")
|
||||||
install(FILES "include/akgl/sprite.h" DESTINATION "include/akgl/")
|
install(FILES "include/akgl/sprite.h" DESTINATION "include/akgl/")
|
||||||
install(FILES "include/akgl/staticstring.h" DESTINATION "include/akgl/")
|
install(FILES "include/akgl/staticstring.h" DESTINATION "include/akgl/")
|
||||||
|
|||||||
18
Doxyfile
Normal file
18
Doxyfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
376
README.md
Normal file
376
README.md
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
## 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.
|
||||||
853
TODO.md
Normal file
853
TODO.md
Normal file
@@ -0,0 +1,853 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
## Internal consistency
|
||||||
|
|
||||||
|
Findings from a sweep of `src/` and `include/`. These are consistency and
|
||||||
|
convention problems, not new functional defects — where one has a functional
|
||||||
|
consequence it is called out. Ordered roughly by blast radius. Items that
|
||||||
|
overlap the existing **Defects** list are cross-referenced rather than repeated.
|
||||||
|
|
||||||
|
### 1. Public naming conventions
|
||||||
|
|
||||||
|
1. **Include guards use three different schemes.** `_AKGL_ACTOR_H_`,
|
||||||
|
`_AKGL_CHARACTER_H_`, `_AKGL_GAME_H_`, `_AKGL_HEAP_H_`, `_AKGL_ITERATOR_H_`,
|
||||||
|
`_AKGL_SPRITE_H_`, and `_AKGL_TYPES_H_` carry the project prefix; `_ASSETS_H_`,
|
||||||
|
`_CONTROLLER_H_`, `_DRAW_H_`, `_ERROR_H_`, `_JSON_HELPERS_H_`, `_PHYSICS_H_`,
|
||||||
|
`_REGISTRY_H_`, `_RENDERER_H_`, `_TEXT_H_`, `_TILEMAP_H_`, and `_UTIL_H_` do
|
||||||
|
not. Pick `_AKGL_<FILE>_H_` everywhere.
|
||||||
|
|
||||||
|
The worst case is `include/akgl/staticstring.h:6`, which guards with
|
||||||
|
`_STRING_H_` — a name several libc implementations use for their own
|
||||||
|
`<string.h>`. The same file then does `#include "string.h"` (line 9) with
|
||||||
|
quotes, which is a relative-first lookup that only reaches the system header
|
||||||
|
by accident. Rename the guard to `_AKGL_STATICSTRING_H_` and use
|
||||||
|
`#include <string.h>`.
|
||||||
|
|
||||||
|
2. **Function names contradict the stated convention.** `AGENTS.md` says public
|
||||||
|
symbols use `akgl_` and types use `akgl_TypeName`. Exceptions:
|
||||||
|
- `akgl_Actor_cmhf_left_on` and its seven siblings (`include/akgl/actor.h:196-252`)
|
||||||
|
embed the *type* name in a *function* name, unlike every other actor entry
|
||||||
|
point (`akgl_actor_*`). Rename to `akgl_actor_cmhf_*`.
|
||||||
|
- `akgl_game_updateFPS` (`include/akgl/game.h:104`) is camelCase; every other
|
||||||
|
function is snake_case.
|
||||||
|
- `akgl_render_init2d` (`include/akgl/renderer.h:83`) puts the `2d` at the end
|
||||||
|
while the six functions it installs are `akgl_render_2d_*`. Make it
|
||||||
|
`akgl_render_2d_init`.
|
||||||
|
- `akgl_sprite_sheet_coords_for_frame` (`include/akgl/sprite.h:86`) spells it
|
||||||
|
`sprite_sheet`; `akgl_spritesheet_initialize` and `akgl_heap_next_spritesheet`
|
||||||
|
spell it `spritesheet`.
|
||||||
|
|
||||||
|
3. **Two public types have no prefix at all.** `point` and `RectanglePoints`
|
||||||
|
(`include/akgl/util.h:13-25`) are unprefixed, and `RectanglePoints` is
|
||||||
|
PascalCase with no namespace. Both are dumped into every translation unit
|
||||||
|
that includes `util.h`. Rename to `akgl_Point` / `akgl_RectanglePoints`.
|
||||||
|
|
||||||
|
4. **Global variables use four different conventions.** `window`, `bgm`, `game`,
|
||||||
|
`gamemap`, `renderer`, `physics`, and `camera` (`src/game.c:26-43`) are
|
||||||
|
unprefixed single common words exported from a shared library — `renderer`
|
||||||
|
and `camera` in particular are very likely to collide with a consuming game.
|
||||||
|
Alongside them the same file exports `akgl_mixer` and `akgl_tracks` (prefixed),
|
||||||
|
`_akgl_renderer` / `_akgl_camera` / `_akgl_physics` / `_akgl_gamemap`
|
||||||
|
(underscore-prefixed, which is reserved at file scope), and elsewhere
|
||||||
|
`HEAP_ACTOR`…`HEAP_STRING` (`src/heap.c:17-21`) and `GAME_ControlMaps`
|
||||||
|
(`src/controller.c:13`) are SCREAMING_SNAKE, which the convention reserves for
|
||||||
|
constants and macros. Settle on `akgl_` for all exported objects.
|
||||||
|
|
||||||
|
5. **`AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH`** (`include/akgl/character.h:13`) is a
|
||||||
|
character constant carrying the `SPRITE` prefix, and lives in `character.h`.
|
||||||
|
Rename to `AKGL_CHARACTER_MAX_NAME_LENGTH`.
|
||||||
|
|
||||||
|
6. **`AKGL_TIME_ONESEC_MS` is misnamed and the error is live.**
|
||||||
|
`include/akgl/game.h:22` defines it as `1000000`. One second in milliseconds
|
||||||
|
is `1000`; `1000000` is the number of nanoseconds in a *millisecond*. The name
|
||||||
|
says "one second" but the value means "one millisecond", so:
|
||||||
|
- `src/character.c:209` and `src/sprite.c:141` use it correctly as a
|
||||||
|
milliseconds-to-nanoseconds scale factor.
|
||||||
|
- `src/game.c:136` uses it as documented — a one-second budget for the state
|
||||||
|
lock — in a loop that advances `totaltime += 100` alongside `SDL_Delay(100)`.
|
||||||
|
The loop therefore runs 10,000 iterations of 100 ms, so `akgl_game_state_lock`
|
||||||
|
blocks for roughly 16 minutes rather than 1 second before reporting failure.
|
||||||
|
|
||||||
|
Rename to `AKGL_TIME_ONEMS_NS` to match `AKGL_TIME_ONESEC_NS`, and give
|
||||||
|
`akgl_game_state_lock` a real one-second budget.
|
||||||
|
|
||||||
|
### 2. Header/implementation surface drift
|
||||||
|
|
||||||
|
7. **Nineteen non-static functions are defined in `src/` but declared in no
|
||||||
|
header.** They have external linkage and public-looking names, so they are
|
||||||
|
part of the ABI whether intended or not, and no consumer can call them:
|
||||||
|
|
||||||
|
`akgl_game_load_objectnamemap`, `akgl_game_load_versioncmp`,
|
||||||
|
`akgl_game_save_actors`, `akgl_game_save_actorname_iterator`,
|
||||||
|
`akgl_game_save_charactername_iterator`, `akgl_game_save_spritename_iterator`,
|
||||||
|
`akgl_game_save_spritesheetname_iterator` (`src/game.c`);
|
||||||
|
`akgl_get_json_properties_double`, `akgl_get_json_properties_float`,
|
||||||
|
`akgl_get_json_properties_number`, `akgl_tilemap_load_layer_image`,
|
||||||
|
`akgl_tilemap_load_layer_object_actor`, `akgl_tilemap_load_physics`
|
||||||
|
(`src/tilemap.c`); `akgl_path_relative_from`, `akgl_path_relative_root`
|
||||||
|
(`src/util.c`); `gamepad_handle_added`, `gamepad_handle_button_down`,
|
||||||
|
`gamepad_handle_button_up`, `gamepad_handle_removed` (`src/controller.c`).
|
||||||
|
|
||||||
|
Each should be either declared in its header or made `static`. Note that
|
||||||
|
`tilemap.h` already has a "part of the internal API, exposed here for unit
|
||||||
|
testing" block — the tilemap entries belong there.
|
||||||
|
|
||||||
|
8. **`akgl_game_init_screen` is declared but never defined**
|
||||||
|
(`include/akgl/game.h:100`). Same failure mode as **Defects → Known and still
|
||||||
|
open #10**, which covers the four `akgl_controller_handle_*` declarations;
|
||||||
|
fold this one into that item.
|
||||||
|
|
||||||
|
9. **Static helpers use three different naming styles.** `actor_visible`
|
||||||
|
(`src/actor.c:185`) is bare; `akgl_character_load_json_inner` and
|
||||||
|
`akgl_character_load_json_state_int_from_strings` (`src/character.c:101,132`)
|
||||||
|
and `akgl_sprite_load_json_spritesheet` (`src/sprite.c:51`) carry the full
|
||||||
|
public prefix; `gamepad_handle_*` (`src/controller.c:121+`) uses a third
|
||||||
|
subsystem word that appears nowhere else. Adopt one rule — the clearest is
|
||||||
|
that `static` helpers drop the `akgl_` prefix, since it exists to avoid
|
||||||
|
external collisions.
|
||||||
|
|
||||||
|
10. **Parameter names disagree between declaration and definition.** Doxygen
|
||||||
|
documents the header spelling, so the generated docs describe names the
|
||||||
|
implementation does not use:
|
||||||
|
|
||||||
|
| Header | Implementation |
|
||||||
|
|---|---|
|
||||||
|
| `character.h:41` `basechar` | `character.c:21` `obj` |
|
||||||
|
| `character.h:69` `props` | `character.c:75` `registry` |
|
||||||
|
| `heap.h:121` `ptr` | `heap.c:143` `basechar` |
|
||||||
|
| `registry.h:97` `value` | `registry.c:163` `src` |
|
||||||
|
| `json_helpers.h:134` `e` | `json_helpers.c:149` `err` |
|
||||||
|
| `tilemap.h:134,143` `dest` | `tilemap.c:646,767` `map` |
|
||||||
|
|
||||||
|
The `tilemap.h` pair is the most misleading: the parameter is the map being
|
||||||
|
*read* and drawn, but it is named `dest` and documented as "Output
|
||||||
|
destination populated by the function".
|
||||||
|
|
||||||
|
11. **Object-pool size macros are defined twice, and the override hook is
|
||||||
|
dead.** `heap.h:15-29` wraps `AKGL_MAX_HEAP_ACTOR`, `_SPRITE`, `_SPRITESHEET`,
|
||||||
|
`_CHARACTER`, and `_STRING` in `#ifndef` guards so a consumer can override
|
||||||
|
them, but `actor.h:65`, `sprite.h:19-20`, and `character.h:14` define the same
|
||||||
|
four unconditionally and are included from `heap.h:9-11`. Whichever header
|
||||||
|
lands first wins and the `#ifndef` never fires, so the override mechanism
|
||||||
|
cannot work. Define each pool size once — `heap.h` is the natural home.
|
||||||
|
|
||||||
|
12. **Headers rely on their includers for types.** `iterator.h` uses `uint32_t`
|
||||||
|
without `<stdint.h>`; `json_helpers.h` uses `json_t` without `<jansson.h>`;
|
||||||
|
`util.h` uses `SDL_FRect` and `bool` without any SDL include; `text.h` uses
|
||||||
|
`SDL_Color` and `akerr_ErrorContext` without including SDL or `akerror.h`;
|
||||||
|
`controller.h` uses `akgl_Actor` without including `actor.h`. Each compiles
|
||||||
|
only because of `.c`-file include ordering. Headers should be self-contained.
|
||||||
|
|
||||||
|
13. **Include spelling is split between quoted and angled forms for the same
|
||||||
|
directory.** `actor.h:10-11`, `character.h:10-11`, `controller.h:11`,
|
||||||
|
`game.h:11-14`, `json_helpers.h:10`, and `staticstring.h:9` use
|
||||||
|
`#include "sibling.h"`; `physics.h:11-13`, `registry.h:9-10`, `renderer.h:13`,
|
||||||
|
`tilemap.h:10-12`, and `util.h:10` use `#include <akgl/sibling.h>`. The
|
||||||
|
angled form is correct for an installed library.
|
||||||
|
|
||||||
|
14. **Empty parameter lists.** `akgl_heap_init()`, `akgl_heap_init_actor()`,
|
||||||
|
`akgl_registry_init*()`, `akgl_game_init()`, `akgl_game_init_screen()`, and
|
||||||
|
`akgl_game_updateFPS()` declare `()` rather than `(void)`, while
|
||||||
|
`akgl_controller_list_keyboards(void)`, `akgl_controller_open_gamepads(void)`,
|
||||||
|
`akgl_game_lowfps(void)`, and `akgl_game_state_lock(void)` use `(void)`.
|
||||||
|
Before C23 the two are not equivalent — `()` suppresses argument checking.
|
||||||
|
`akgl_heap_init_actor` is even declared `()` in `heap.h:57` and defined
|
||||||
|
`(void)` in `heap.c:48`.
|
||||||
|
|
||||||
|
15. **`AKERR_NOIGNORE` is applied inconsistently at definition sites.** Headers
|
||||||
|
use it uniformly (except `akgl_sprite_sheet_coords_for_frame`, `sprite.h:86`,
|
||||||
|
which omits it). Definitions are split even within one file: `registry.c:55,120,163,172`
|
||||||
|
repeat it, `registry.c:27,44,63,71,79,96,104,112` do not. Since the attribute
|
||||||
|
is already on the declaration, drop it from all definitions.
|
||||||
|
|
||||||
|
### 3. Error-handling pattern
|
||||||
|
|
||||||
|
16. **`*_RETURN` macros are used inside `ATTEMPT` blocks, which skips `CLEANUP`.**
|
||||||
|
The established pattern is `FAIL_*_BREAK` / `CATCH` inside `ATTEMPT` and
|
||||||
|
`*_RETURN` outside it. Violations:
|
||||||
|
- `src/tilemap.c:52` — `SUCCEED_RETURN` on the success path of
|
||||||
|
`akgl_get_json_tilemap_property` returns directly from inside `ATTEMPT`,
|
||||||
|
bypassing the `CLEANUP` at line 54 that releases `tmpstr` and `typestr`.
|
||||||
|
Every successful property lookup leaks two heap strings, and the string
|
||||||
|
pool is only 256 entries.
|
||||||
|
- `src/tilemap.c:620` — `FAIL_RETURN` inside `ATTEMPT`.
|
||||||
|
- `src/controller.c:286-289,306` — `FAIL_ZERO_RETURN` / `FAIL_NONZERO_RETURN`
|
||||||
|
inside `ATTEMPT`; `src/controller.c:383` — `SUCCEED_RETURN` inside `ATTEMPT`,
|
||||||
|
which also leaves `akgl_controller_default` with no return statement on the
|
||||||
|
path that falls out of `FINISH`.
|
||||||
|
- `src/game.c:457,462` — `FAIL_NONZERO_RETURN` inside `ATTEMPT`, skipping the
|
||||||
|
`fclose` in `CLEANUP` at line 482.
|
||||||
|
|
||||||
|
17. **NULL-check discipline varies by function.** In `src/json_helpers.c` only
|
||||||
|
`akgl_get_json_string_value` (line 71) and `akgl_get_json_array_index_string`
|
||||||
|
(line 128) validate `key`/`dest`; the other eight accessors validate only the
|
||||||
|
container and then dereference `dest` unconditionally. In `src/physics.c` the
|
||||||
|
arcade backends check `actor` but `akgl_physics_null_gravity`,
|
||||||
|
`_null_collide`, and `_null_move` (lines 15-34) check only `self`. In
|
||||||
|
`src/renderer.c:65,74`, `akgl_render_2d_frame_start` and `_frame_end`
|
||||||
|
dereference `self->sdl_renderer` with no check on `self`, while
|
||||||
|
`akgl_render_2d_draw_texture` (line 82) checks `self` first.
|
||||||
|
|
||||||
|
18. **Error-context variable naming is split between `errctx` and `e`,
|
||||||
|
sometimes within one file.** `src/util.c` uses `e` in the path helpers
|
||||||
|
(lines 32-116) and `errctx` in the geometry helpers (lines 118-259);
|
||||||
|
`src/heap.c` uses `errctx` everywhere except `akgl_heap_init_actor` (line 50).
|
||||||
|
`src/actor.c`, `character.c`, `json_helpers.c`, `registry.c`, `sprite.c`, and
|
||||||
|
`tilemap.c` favor `errctx`; `game.c`, `physics.c`, `renderer.c`, and
|
||||||
|
`controller.c` favor `e`. Pick one.
|
||||||
|
|
||||||
|
### 4. Types and macros
|
||||||
|
|
||||||
|
19. **`float`/`double` are used raw where `types.h` defines aliases.** `types.h`
|
||||||
|
exports `float32_t` and `float64_t`, and the actor and character structs use
|
||||||
|
`float32_t` throughout — but `akgl_get_json_number_value` takes `float *`
|
||||||
|
(`json_helpers.h:55`), `akgl_get_json_double_value` takes `double *` (line 66),
|
||||||
|
`akgl_PhysicsBackend`'s six drag/gravity fields are `double`
|
||||||
|
(`physics.h:22-27`), and `akgl_Tilemap`'s perspective fields are `float`
|
||||||
|
(`tilemap.h:105-108`). Either use the aliases consistently or delete them.
|
||||||
|
|
||||||
|
20. **`AKGL_COLLIDE_RECTANGLES` (`include/akgl/util.h:27`) has unbalanced
|
||||||
|
parentheses** — three opens, two closes — so any use is a syntax error. It has
|
||||||
|
no callers and duplicates `akgl_collide_rectangles`. Delete it.
|
||||||
|
|
||||||
|
21. **Bitmask macros are unparenthesized.** `AKGL_BITMASK_HAS(x, y)` expands to
|
||||||
|
`(x & y) == y` with no outer parens, so `!AKGL_BITMASK_HAS(a, b)` parses as
|
||||||
|
`!(a & b) == b`. No in-tree caller negates it today (`AKGL_BITMASK_HASNOT`
|
||||||
|
exists for that), so this is latent rather than live. `AKGL_BITMASK_CLEAR(x)`
|
||||||
|
(`game.h:86`) also carries a trailing semicolon inside the macro body, so
|
||||||
|
normal use produces an empty statement. Parenthesize all five and drop the
|
||||||
|
semicolon.
|
||||||
|
|
||||||
|
22. **The state and iterator bit macros mix forms.** `AKGL_ITERATOR_OP_UPDATE`
|
||||||
|
(`iterator.h:15`) is written `1` while its 31 siblings are `1 << n`, and none
|
||||||
|
of the `1 << n` values in `iterator.h` or `actor.h:15-49` are parenthesized.
|
||||||
|
The hand-maintained trailing bit-pattern comments in `actor.h:34-49` are also
|
||||||
|
wrong for the high word — they restart at `0000 0000 0000 0001` for bit 16
|
||||||
|
rather than showing the full 32-bit value.
|
||||||
|
|
||||||
|
23. **`akgl_Frame` (`game.h:27-31`) is defined and never used** anywhere in
|
||||||
|
`src/`, `include/`, `tests/`, or `util/`.
|
||||||
|
|
||||||
|
### 5. `AKGL_ACTOR_STATE_STRING_NAMES` disagrees with `actor.h`
|
||||||
|
|
||||||
|
24. **The array bound differs between declaration and definition.**
|
||||||
|
`include/akgl/actor.h:57` declares `[AKGL_ACTOR_MAX_STATES+1]` (33);
|
||||||
|
`src/actor_state_string_names.c:6` defines `[32]`. Any consumer that trusts
|
||||||
|
the declared bound and reads index 32 reads past the object.
|
||||||
|
|
||||||
|
25. **Two entries name the wrong bit.** `actor.h` assigns bit 11 to
|
||||||
|
`AKGL_ACTOR_STATE_MOVING_IN` and bit 12 to `AKGL_ACTOR_STATE_MOVING_OUT`, but
|
||||||
|
`actor_state_string_names.c:18-19` puts `"AKGL_ACTOR_STATE_UNDEFINED_11"` and
|
||||||
|
`"AKGL_ACTOR_STATE_UNDEFINED_12"` at those indices — names for macros that do
|
||||||
|
not exist. Since `akgl_registry_init_actor_state_strings`
|
||||||
|
(`src/registry.c:79-94`) builds `AKGL_REGISTRY_ACTOR_STATE_STRINGS` from this
|
||||||
|
array and `akgl_character_load_json_state_int_from_strings`
|
||||||
|
(`src/character.c:101`) resolves character-JSON state names through it, a
|
||||||
|
character JSON can never bind a sprite to `MOVING_IN` or `MOVING_OUT`.
|
||||||
|
|
||||||
|
26. **The generation comment is stale.** `actor.h:53-55` says the file "is built
|
||||||
|
by a utility script and not kept in git, see the Makefile for
|
||||||
|
lib_src/actor_state_string_names.c". There is no Makefile (the project is
|
||||||
|
CMake), no `lib_src/`, no such script under `scripts/` or `util/`, and the
|
||||||
|
file *is* tracked in git at `src/actor_state_string_names.c`. Either restore
|
||||||
|
the generator — which would fix items 24 and 25 by construction — or delete
|
||||||
|
the comment and maintain the file by hand.
|
||||||
|
|
||||||
|
### 6. Doxygen drift
|
||||||
|
|
||||||
|
27. **Three struct doc comments in `tilemap.h` are rotated by one.**
|
||||||
|
`akgl_TilemapObject` (line 31) is documented as "Stores tileset metadata,
|
||||||
|
texture, and frame offsets" (that is `akgl_Tileset`); `akgl_TilemapLayer`
|
||||||
|
(line 46) as "Represents an object embedded in a tilemap layer" (that is
|
||||||
|
`akgl_TilemapObject`); `akgl_Tileset` (line 61) as "Stores tile, image, or
|
||||||
|
object data for one map layer" (that is `akgl_TilemapLayer`).
|
||||||
|
|
||||||
|
28. **`point` is documented as "Represents a two-dimensional point"**
|
||||||
|
(`util.h:12`) but has `x`, `y`, and `z`.
|
||||||
|
|
||||||
|
29. **Doc comments live on the definition for public functions.** The convention
|
||||||
|
is header-side documentation, but `akgl_path_relative_root` (`util.c:23`),
|
||||||
|
`akgl_path_relative_from` (`util.c:97`), the four `gamepad_handle_*`
|
||||||
|
(`controller.c:114+`), the `akgl_game_save_*` iterators and
|
||||||
|
`akgl_game_load_*` helpers (`game.c:173+`), and the tilemap helpers
|
||||||
|
(`tilemap.c:90+`) are documented only in the `.c`. This is the same set as
|
||||||
|
item 7 — resolving that resolves this.
|
||||||
|
|
||||||
|
### 7. Formatting
|
||||||
|
|
||||||
|
30. **A minority of files use a 2-column offset instead of the canonical
|
||||||
|
4-column one.** The mix of tabs and spaces across most of `src/` is *not*
|
||||||
|
disorder: it is exactly what Emacs `cc-mode` emits for the `stroustrup` style
|
||||||
|
with `indent-tabs-mode t` and `tab-width 8` — depth 1 is four spaces, depth 2
|
||||||
|
is one tab, depth 3 is a tab plus four spaces, and so on. Twelve of the
|
||||||
|
seventeen `.c` files follow that ladder cleanly.
|
||||||
|
|
||||||
|
The genuine outliers indent at 2 columns: `src/json_helpers.c` (82 lines,
|
||||||
|
except `akgl_get_json_with_default` at line 149 which is 4), `src/util.c`
|
||||||
|
(37 lines, from `akgl_rectangle_points` at line 118 onward), `src/assets.c`
|
||||||
|
(9), `src/staticstring.c` (7), `src/actor.c` (8, in `akgl_actor_add_child`
|
||||||
|
at line 272), plus `include/akgl/util.h` (7) and
|
||||||
|
`include/akgl/staticstring.h` (2).
|
||||||
|
|
||||||
|
**Resolved.** `AGENTS.md` now specifies the canonical style and the exact
|
||||||
|
`cc-mode` settings; `.dir-locals.el` applies them in Emacs; and
|
||||||
|
`scripts/reindent.sh` applies them in batch. The whole tree (32 files across
|
||||||
|
`src/`, `include/`, `tests/`, and `util/`) has been reindented and is a fixed
|
||||||
|
point of `scripts/reindent.sh --check`. Verified whitespace-only: apart from
|
||||||
|
three trailing blank lines removed at EOF in `src/heap.c`, `src/registry.c`,
|
||||||
|
and `tests/tilemap.c`, `git diff -w` over the reindent is empty, and the test
|
||||||
|
results are unchanged (13/14, `character` still the intentional failure).
|
||||||
|
`scripts/hooks/pre-commit` keeps it that way — enable with
|
||||||
|
`git config core.hooksPath scripts/hooks`.
|
||||||
|
|
||||||
|
31. **Leftover debug code ships in the library.** `src/controller.c:91-97` logs
|
||||||
|
four lines whenever `event->type == 768 && event->key.which == 11 &&
|
||||||
|
event->key.key == 13` — hardcoded decimal values for a specific keyboard ID
|
||||||
|
on a specific developer's machine, inside the per-event inner loop.
|
||||||
|
|
||||||
|
32. **Large commented-out blocks.** `src/sprite.c:115-120,157,185-198,210`;
|
||||||
|
`src/character.c:192-199,215`; `src/assets.c:18-27,50`;
|
||||||
|
`src/tilemap.c:583,596-598,640`. All are the same abandoned
|
||||||
|
`SDL_GetBasePath()` path-prefixing approach, superseded by
|
||||||
|
`akgl_path_relative`. Delete them.
|
||||||
|
|
||||||
|
33. **Unused locals.** `screenwidth`/`screenheight` (`game.c:53-54`), `curTime`
|
||||||
|
(`game.c:499`), `curTime` and `j` (`renderer.c:114,116` — `j` is also shadowed
|
||||||
|
by the inner loop at line 128), `target` (`character.c:59`), `result`
|
||||||
|
(`util.c:37,73`), `opflags` (`heap.c:146`, declared and cleared but never
|
||||||
|
read).
|
||||||
|
|
||||||
|
34. **`akgl_game_update`'s default flags OR the same bit twice.**
|
||||||
|
`src/game.c:496` reads
|
||||||
|
`(AKGL_ITERATOR_OP_LAYERMASK | AKGL_ITERATOR_OP_LAYERMASK)`. Given the loop
|
||||||
|
that follows walks layers and calls `updatefunc`, the intent was almost
|
||||||
|
certainly `AKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_LAYERMASK`.
|
||||||
|
|
||||||
|
35. **`akgl_draw_background` is the only public function outside the error
|
||||||
|
protocol.** `include/akgl/draw.h:14` returns `void` and reports nothing;
|
||||||
|
every other public entry point returns `akerr_ErrorContext *`. It also calls
|
||||||
|
`SDL_SetRenderDrawColor` and `SDL_RenderFillRect` without checking `renderer`
|
||||||
|
or `renderer->sdl_renderer`.
|
||||||
|
|
||||||
|
36. **`akgl_registry_init_actor` is the only registry initializer that destroys
|
||||||
|
an existing registry** before recreating it (`src/registry.c:47-49`). The
|
||||||
|
other seven leak the old `SDL_PropertiesID` on a second call. Either all of
|
||||||
|
them should do it or none should.
|
||||||
|
|
||||||
|
37. **Redundant casts obscure the code.** `(json_t *)json` where `json` is
|
||||||
|
already `json_t *`, `(akgl_Tilemap *)dest`, `(akgl_Actor *)actorobj`,
|
||||||
|
`(char *)&obj->name` on an array that already decays — roughly 180 pointer
|
||||||
|
casts across `src/`, a large majority of them no-ops, densest in
|
||||||
|
`src/tilemap.c:150-624` and `src/character.c:144-172`.
|
||||||
|
They suppress exactly the conversion warnings that would catch a real
|
||||||
|
mismatch.
|
||||||
|
|
||||||
|
38. **`struct`-qualified parameters in two definitions.**
|
||||||
|
`akgl_physics_null_move` (`src/physics.c:29`) and `akgl_physics_arcade_move`
|
||||||
|
(`src/physics.c:80`) are defined with `struct akgl_PhysicsBackend *self`
|
||||||
|
while the header and the other eight physics functions use the typedef.
|
||||||
|
|
||||||
|
39. **`text.c:19` validates the wrong argument.**
|
||||||
|
`FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null filepath")` checks
|
||||||
|
`name` a second time; `filepath` is never checked and is passed straight to
|
||||||
|
`TTF_OpenFont`. Copy-paste of line 18.
|
||||||
|
|
||||||
|
**Resolved** alongside the text measurement work; `tests/text.c` asserts a
|
||||||
|
NULL filepath reports `AKERR_NULLPOINTER`.
|
||||||
|
|
||||||
|
40. **`akgl_path_relative` and `akgl_path_relative_from` disagree on the output
|
||||||
|
parameter.** `akgl_path_relative` and `akgl_path_relative_root` take
|
||||||
|
`akgl_String *dst`; `akgl_path_relative_from` takes `akgl_String **dst`
|
||||||
|
(`src/util.c:105`). The `**` form matches the rest of the library
|
||||||
|
(`akgl_get_json_string_value`, `akgl_get_property`, `akgl_heap_next_string`),
|
||||||
|
which allocate when `*dest` is NULL. Related: **Defects → Known and still
|
||||||
|
open #4**, which covers the fact that `akgl_path_relative_from` never writes
|
||||||
|
`*dst` at all.
|
||||||
|
|
||||||
|
41. **`dst` vs `dest` for output parameters.** `akgl_string_copy` and the
|
||||||
|
`akgl_path_relative*` family use `dst`; everything else uses `dest`.
|
||||||
|
|
||||||
|
## Coverage status
|
||||||
|
|
||||||
|
Generated with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
|
||||||
|
cmake --build build-coverage --parallel
|
||||||
|
ctest --test-dir build-coverage --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
Reports land in `build-coverage/coverage/` (`index.html`, `coverage.xml`).
|
||||||
|
|
||||||
|
**Line coverage 77.2%, function coverage 83.1%** (2035/2637 lines), up from a
|
||||||
|
39.6% / 44.3% baseline. All 19 suites pass. (An earlier revision of this file
|
||||||
|
recorded `character` as an intentionally failing suite; it passes now, and
|
||||||
|
nothing in `tests/character.c` or `src/character.c` has changed since — the fix
|
||||||
|
came from elsewhere in the tree and this note was never updated.)
|
||||||
|
|
||||||
|
| File | Lines | Functions |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/actor.c` | 205/258 (79%) | 16/18 |
|
||||||
|
| `src/assets.c` | 0/21 (0%) | 0/1 |
|
||||||
|
| `src/audio.c` | 189/207 (91%) | 19/20 |
|
||||||
|
| `src/character.c` | 104/118 (88%) | 6/7 |
|
||||||
|
| `src/controller.c` | 247/270 (91%) | 12/12 |
|
||||||
|
| `src/draw.c` | 253/267 (95%) | 11/12 |
|
||||||
|
| `src/error.c` | 9/9 (100%) | 1/1 |
|
||||||
|
| `src/game.c` | 124/231 (54%) | 10/15 |
|
||||||
|
| `src/heap.c` | 111/111 (100%) | 12/12 |
|
||||||
|
| `src/json_helpers.c` | 111/111 (100%) | 11/11 |
|
||||||
|
| `src/physics.c` | 140/140 (100%) | 10/10 |
|
||||||
|
| `src/registry.c` | 76/102 (75%) | 11/12 |
|
||||||
|
| `src/renderer.c` | 7/70 (10%) | 1/7 |
|
||||||
|
| `src/sprite.c` | 93/101 (92%) | 5/5 |
|
||||||
|
| `src/staticstring.c` | 16/17 (94%) | 2/2 |
|
||||||
|
| `src/text.c` | 26/45 (58%) | 3/4 |
|
||||||
|
| `src/tilemap.c` | 201/426 (47%) | 10/20 |
|
||||||
|
| `src/util.c` | 121/131 (92%) | 7/8 |
|
||||||
|
| `src/version.c` | 2/2 (100%) | 1/1 |
|
||||||
|
|
||||||
|
Branch coverage reads 21.2% and should not be used as a target. The akerror
|
||||||
|
control-flow macros (`ATTEMPT`/`CATCH`/`PROCESS`/`FINISH`, `FAIL_*_RETURN`)
|
||||||
|
expand into large branch trees per call site, most of them unreachable in normal
|
||||||
|
operation — `src/game.c` reports over 1700 branches across 230 lines. Track line
|
||||||
|
and function coverage; treat branch coverage as a relative signal within a file.
|
||||||
|
|
||||||
|
### Mutation testing
|
||||||
|
|
||||||
|
`scripts/mutation_test.py` was run over the three new files as a smoke check
|
||||||
|
(`--max-mutants 8` to 10 each, so these are samples rather than exhaustive
|
||||||
|
scores):
|
||||||
|
|
||||||
|
| File | Score | Surviving mutants |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/draw.c` | 90% | Deleting the `FAIL_ZERO_BREAK` on `SDL_CreateTextureFromSurface` in `akgl_draw_flood_fill` |
|
||||||
|
| `src/audio.c` | 75% | Deleting `SUCCEED_RETURN` from the static `check_voice`; deleting `spec.freq` before opening a device |
|
||||||
|
| `src/text.c` | 50% | Three in `akgl_text_rendertextat`, which has no test yet, plus one `SUCCEED_RETURN` deletion |
|
||||||
|
|
||||||
|
The first pass over `src/audio.c` scored 50% and named two real gaps, both of
|
||||||
|
which are now tested: nothing asserted that an *unconfigured* voice is audible
|
||||||
|
(the whole reason the table defaults to a square wave at full level rather than
|
||||||
|
a zeroed struct), and no envelope test used a non-zero attack *and* decay
|
||||||
|
together, so the decay measuring from the wrong origin survived. `src/draw.c`
|
||||||
|
scored 70% first and named one: the circle was only checked at its four axis
|
||||||
|
points, which a mis-signed octant reflection survives, so it now checks that
|
||||||
|
every plotted pixel has a mirror in the other three quadrants.
|
||||||
|
|
||||||
|
What is left is honestly untestable from here. Deleting a `SUCCEED_RETURN`
|
||||||
|
leaves a non-void function falling off its end, which is undefined rather than
|
||||||
|
observably wrong, and the surviving SDL branches are allocation failures the
|
||||||
|
suite has no way to provoke. The `src/text.c` survivors go away with the
|
||||||
|
offscreen harness.
|
||||||
|
|
||||||
|
### Suites
|
||||||
|
|
||||||
|
Every suite is registered through the `AKGL_TEST_SUITES` list in
|
||||||
|
`CMakeLists.txt`, which drives target creation, CTest registration, the
|
||||||
|
`WORKING_DIRECTORY`/`TIMEOUT` properties, the link line, and the
|
||||||
|
`FIXTURES_REQUIRED akgl_coverage` list together. Adding `tests/<name>.c` and the
|
||||||
|
name to that list is all a new suite needs; it can no longer be accidentally
|
||||||
|
left out of the coverage fixture.
|
||||||
|
|
||||||
|
Shared assertion helpers are in `tests/testutil.h`: `TEST_ASSERT`,
|
||||||
|
`TEST_ASSERT_FEQ`, `TEST_EXPECT_STATUS`, `TEST_EXPECT_OK`, `TEST_EXPECT_ANY_ERROR`,
|
||||||
|
and `TEST_ASSERT_FLAG`. All except the last expand to a `break` on failure, so
|
||||||
|
they belong directly inside an `ATTEMPT` block, not inside a loop nested in one.
|
||||||
|
|
||||||
|
Done:
|
||||||
|
|
||||||
|
- `tests/physics.c` — both backends, the factory, and the full simulation loop
|
||||||
|
including thrust clamping, drag, layer masking, parent/child positioning, and
|
||||||
|
logic-interrupt handling. 100%.
|
||||||
|
- `tests/heap.c` — pool exhaustion for all five pools, refcount clamping,
|
||||||
|
recursive child release, registry cleanup on release. 100%.
|
||||||
|
- `tests/json_helpers.c` — every typed accessor, both string accessors'
|
||||||
|
allocate-vs-reuse paths, array bounds, and `akgl_get_json_with_default`. 100%.
|
||||||
|
- `tests/controller.c` — control map push and capacity, the default binding set,
|
||||||
|
keyboard and gamepad dispatch including cross-device rejection, the dpad
|
||||||
|
handlers, and device add/remove against the dummy drivers. 90%.
|
||||||
|
- `tests/game.c` — version gating, the save/load roundtrip, foreign-save
|
||||||
|
rejection, truncated-table detection, the state lock, and FPS accounting. 54%.
|
||||||
|
- `tests/actor.c` — extended with the eight control-map handlers, automatic
|
||||||
|
facing, movement logic, the animation frame state machine, `akgl_actor_update`,
|
||||||
|
and character/sprite binding lookups. 80%.
|
||||||
|
- `tests/audio.c` — every waveform, the ADSR envelope stage by stage, gate
|
||||||
|
expiry and release, voice summing and clamping, the master level, and device
|
||||||
|
open/shutdown under the dummy driver. 91%.
|
||||||
|
- `tests/draw.c` — every primitive against a 64x64 software renderer with the
|
||||||
|
pixels read back, including flood-fill containment, save/paste roundtrip, and
|
||||||
|
that drawing restores the renderer's draw color. 95%.
|
||||||
|
- `tests/text.c` — font loading into the registry and both measurement entry
|
||||||
|
points against a monospaced fixture font. 58%; the rest of `src/text.c` is
|
||||||
|
`akgl_text_rendertextat`, which needs the harness.
|
||||||
|
|
||||||
|
## Remaining work
|
||||||
|
|
||||||
|
### Needs the offscreen renderer harness
|
||||||
|
|
||||||
|
`src/renderer.c` (63 lines), `akgl_text_rendertextat` in `src/text.c` (19),
|
||||||
|
`akgl_draw_background` in `src/draw.c` (13), `src/assets.c` (21),
|
||||||
|
`akgl_actor_render`/`actor_visible` in `src/actor.c` (53), and the drawing half
|
||||||
|
of `src/tilemap.c` all need a live `renderer` global.
|
||||||
|
|
||||||
|
Build `tests/harness.c` / `tests/harness.h` with `akgl_test_init_headless()` and
|
||||||
|
`akgl_test_shutdown_headless()`: set the dummy video and audio drivers,
|
||||||
|
`SDL_Init()`, `akgl_heap_init()`, `akgl_registry_init()`, create a software
|
||||||
|
`SDL_CreateWindowAndRenderer`, and point the global `renderer` at it. Four
|
||||||
|
existing tests hand-roll this today (`tests/sprite.c:194`, `tests/character.c:200`,
|
||||||
|
`tests/tilemap.c:421`, `tests/charviewer.c:42`); collapse them onto the shared
|
||||||
|
harness in the same change. The new suites set the driver hints inline and need
|
||||||
|
no window, so they are unaffected.
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
- **`tests/renderer.c`** — `akgl_render_init2d` populating all six function
|
||||||
|
pointers and the camera; frame start/end against a NULL `sdl_renderer`; the
|
||||||
|
rotated `draw_texture` path including `angle != 0` with a NULL center; the
|
||||||
|
`draw_mesh` "not implemented" stub; and `draw_world` layer ordering. Note
|
||||||
|
`defflags` at `src/renderer.c:113` is uninitialized until the `if` body runs.
|
||||||
|
- **`tests/assets.c`** — BGM loading into `AKGL_REGISTRY_MUSIC` under the dummy
|
||||||
|
audio driver.
|
||||||
|
- **`tests/text.c` extensions** — text rendering with wrap on and off.
|
||||||
|
`tests/text.c` exists and covers font loading and measurement; only
|
||||||
|
`akgl_text_rendertextat` is left.
|
||||||
|
- **`tests/draw.c` extensions** — `akgl_draw_background` at zero, negative and
|
||||||
|
oversized dimensions. `tests/draw.c` exists and covers every other primitive
|
||||||
|
against a software renderer; `akgl_draw_background` is the one function in the
|
||||||
|
file that still reads the global `renderer` rather than taking a backend.
|
||||||
|
- **`tests/tilemap.c` extensions** — `akgl_tilemap_draw`, `_draw_tileset`, and
|
||||||
|
`akgl_tilemap_load_layer_image`.
|
||||||
|
|
||||||
|
### Does not need a renderer
|
||||||
|
|
||||||
|
- **`src/tilemap.c`** — `akgl_tilemap_scale_actor` is pure math over three
|
||||||
|
branches (`src/tilemap.c:824-830`); `akgl_get_json_properties_number`,
|
||||||
|
`_float`, and `_double` need only a JSON snippet; `akgl_tilemap_load_physics`
|
||||||
|
needs a fixture with the physics property block present, absent, and
|
||||||
|
malformed. Together roughly 100 of the 225 uncovered lines.
|
||||||
|
- **`src/game.c`** — `akgl_game_init`, `akgl_game_update`, `akgl_game_lowfps`,
|
||||||
|
and `akgl_game_updateFPS`'s frame loop need a window; revisit after the
|
||||||
|
harness lands.
|
||||||
|
- **`src/registry.c`** — `akgl_registry_load_properties` needs a fixture with a
|
||||||
|
`properties` object, plus the missing-file, missing-key, and wrong-value-type
|
||||||
|
cases. Assert the loop at `src/registry.c:148-158` does not leak the string
|
||||||
|
heap.
|
||||||
|
|
||||||
|
## Defects
|
||||||
|
|
||||||
|
### Fixed while building the suites
|
||||||
|
|
||||||
|
Each was found by a test written to assert correct behavior.
|
||||||
|
|
||||||
|
1. **`akgl_physics_simulate` dereferenced `self` before its NULL check.**
|
||||||
|
`src/physics.c:132` read `self->gravity_time` at declaration time, three
|
||||||
|
lines above `FAIL_ZERO_RETURN(e, self, ...)`. A NULL backend segfaulted.
|
||||||
|
2. **`akgl_game_save` never flushed or closed its stream.** `CLEANUP` and
|
||||||
|
`PROCESS` were transposed, which put the `fclose` inside the `PROCESS`
|
||||||
|
switch, where it only ran if an error context existed and reported success.
|
||||||
|
An ordinary save produced an empty file.
|
||||||
|
3. **`akgl_game_save_actors` wrote name-table terminators from a single char.**
|
||||||
|
`aksl_fwrite((void *)&nullval, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp)` emitted
|
||||||
|
127 bytes of adjacent stack memory into the save file and produced a sentinel
|
||||||
|
the loader could not recognize.
|
||||||
|
4. **`akgl_game_load_objectnamemap` swallowed read failures.** `CATCH` used
|
||||||
|
directly inside `while (1)` breaks the loop, not the function, so a truncated
|
||||||
|
or corrupt name table loaded as a successful game.
|
||||||
|
5. **`akgl_Actor_cmhf_up_on` and `_down_on` dereferenced `basechar` unguarded**,
|
||||||
|
unlike their left and right counterparts.
|
||||||
|
6. **`akgl_actor_logic_movement` checked `actor` twice** instead of checking
|
||||||
|
`actor->basechar` before dereferencing it.
|
||||||
|
7. **The gamepad handlers checked `appstate` three times each**, so a NULL event
|
||||||
|
or a missing player actor was never caught and `player->state` was
|
||||||
|
dereferenced regardless.
|
||||||
|
|
||||||
|
### Known and still open
|
||||||
|
|
||||||
|
1. **`akgl_render_and_compare` compares a texture against itself.**
|
||||||
|
`src/util.c:228` and `src/util.c:245` both draw `t1`; `t2` is never rendered,
|
||||||
|
so the function always passes and the image assertions in `tests/sprite.c`
|
||||||
|
assert nothing.
|
||||||
|
2. **`akgl_tilemap_release` double-frees tileset textures.**
|
||||||
|
`src/tilemap.c:849` destroys `dest->tilesets[i].texture` inside the *layers*
|
||||||
|
loop; it should be `dest->layers[i].texture`. It also never NULLs the
|
||||||
|
pointers, so a second release is a use-after-free.
|
||||||
|
3. **`akgl_registry_init` never initializes the properties registry.**
|
||||||
|
`akgl_registry_init_properties()` is not called from `akgl_registry_init()`
|
||||||
|
(`src/registry.c:27`), so `AKGL_REGISTRY_PROPERTIES` stays 0 unless the
|
||||||
|
caller initializes it separately. `akgl_set_property` is then a silent no-op
|
||||||
|
and `akgl_get_property` always returns the caller's default, which means
|
||||||
|
`akgl_physics_init_arcade` and `akgl_render_init2d` silently ignore
|
||||||
|
configuration. `akgl_game_init` does call it; a caller that does not use
|
||||||
|
`akgl_game_init` does not get it.
|
||||||
|
4. **`akgl_path_relative_from` is a stub that leaks.** `src/util.c:105` claims a
|
||||||
|
heap string, never writes `*dst`, and never releases it. 256 calls exhaust
|
||||||
|
the string pool.
|
||||||
|
5. **`akgl_compare_sdl_surfaces` memcmps without checking geometry.**
|
||||||
|
`src/util.c:208` compares `s1->pitch * s1->h` bytes of `s2` without verifying
|
||||||
|
the surfaces share dimensions, pitch, or format.
|
||||||
|
6. **`akgl_string_initialize` overflows by four bytes when `init` is NULL.**
|
||||||
|
`src/staticstring.c:17` does `memset(&obj->data, 0x00, sizeof(akgl_String))`,
|
||||||
|
but `data` starts four bytes into the struct (after `refcount`), so the memset
|
||||||
|
runs four bytes past the end.
|
||||||
|
7. **Savegame name lengths disagree between writer and reader.**
|
||||||
|
`akgl_game_save_actors` writes spritesheet names at
|
||||||
|
`AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH` (512) and character names at
|
||||||
|
`AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH` (128), but `akgl_game_load` reads
|
||||||
|
every table at `AKGL_ACTOR_MAX_NAME_LENGTH` (128). A save with any registered
|
||||||
|
spritesheet cannot be read back. The current roundtrip test passes only
|
||||||
|
because empty registries write nothing but zeroed sentinels.
|
||||||
|
8. **Heap acquire functions are asymmetric.** `akgl_heap_next_string` increments
|
||||||
|
`refcount`; `next_actor`, `next_sprite`, `next_spritesheet`, and
|
||||||
|
`next_character` do not. `tests/heap.c` pins the current behavior and says so;
|
||||||
|
decide whether to make them symmetric or document the split.
|
||||||
|
9. **`tests/util.c` defines `test_akgl_collide_point_rectangle_logic` but
|
||||||
|
`main()` never calls it.**
|
||||||
|
10. **`controller.h` declares functions that do not exist.** It declares
|
||||||
|
`akgl_controller_handle_button_down`, `_button_up`, `_added`, and `_removed`,
|
||||||
|
but `src/controller.c` defines them as `gamepad_handle_*`. Anything compiled
|
||||||
|
against the header alone fails to link.
|
||||||
|
11. **`akgl_controller_pushmap` and `akgl_controller_default` accept negative map
|
||||||
|
ids.** Both check `controlmapid >= AKGL_MAX_CONTROL_MAPS` but not
|
||||||
|
`controlmapid < 0`, so a negative id indexes before `GAME_ControlMaps`.
|
||||||
|
12. **A failed controller-DB fetch silently destroys the tracked fallback.**
|
||||||
|
`include/akgl/SDL_GameControllerDB.h` is committed deliberately so the
|
||||||
|
library still builds if upstream disappears. But `mkcontrollermappings.sh`
|
||||||
|
has no `set -e` and never checks `curl`'s exit status: on a failed fetch it
|
||||||
|
writes `mappings.txt` empty, then overwrites the good tracked header with
|
||||||
|
`AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty initializer — and exits 0.
|
||||||
|
Verified by pointing the script at an unresolvable host.
|
||||||
|
|
||||||
|
Two compounding problems:
|
||||||
|
|
||||||
|
- `add_custom_command` at `CMakeLists.txt:89-93` declares
|
||||||
|
`OUTPUT include/akgl/SDL_GameControllerDB.h` as a *relative* path, which
|
||||||
|
CMake resolves against the binary directory, while the script writes to
|
||||||
|
the source directory. The declared output never appears, so the command is
|
||||||
|
permanently out of date and re-runs on every build — making every build
|
||||||
|
depend on network access and leaving the file dirty in the working tree
|
||||||
|
each time.
|
||||||
|
- `const char *SDL_GAMECONTROLLER_DB[] = {\n};` is an empty initializer,
|
||||||
|
which is a constraint violation in ISO C and compiles only as a GCC
|
||||||
|
extension.
|
||||||
|
|
||||||
|
Fix: have the script fetch to a temporary file, check `curl`'s status and a
|
||||||
|
plausible minimum line count, and leave the existing header untouched on
|
||||||
|
failure (exiting non-zero). Separately, make regeneration explicit — a
|
||||||
|
dedicated `controllerdb` target, or an `OUTPUT` that matches where the
|
||||||
|
script actually writes — so an ordinary build neither needs the network nor
|
||||||
|
dirties the tree.
|
||||||
|
13. **A stale build tree in the source directory breaks the coverage run.**
|
||||||
|
`CMakeLists.txt:208` and `CMakeLists.txt:223` pass
|
||||||
|
`--root "${CMAKE_CURRENT_SOURCE_DIR}"` to gcovr, and gcovr searches for
|
||||||
|
`.gcda`/`.gcno` files under the root. The `--object-directory` argument on
|
||||||
|
the line below each does *not* narrow that search: per `gcovr --help` it
|
||||||
|
only identifies "the path between gcda files and the directory where the
|
||||||
|
compiler was originally run". So every instrumented build tree left inside
|
||||||
|
the source directory is folded into the report alongside the one actually
|
||||||
|
being measured.
|
||||||
|
|
||||||
|
With a `build-coverage/` from an earlier session still present, a freshly
|
||||||
|
configured `-DAKGL_COVERAGE=ON` tree fails in `coverage_reset`, before any
|
||||||
|
test runs:
|
||||||
|
|
||||||
|
AssertionError: Got function akgl_game_lowfps on multiple lines: 45, 46.
|
||||||
|
|
||||||
|
45 and 46 are that function's line numbers before and after an unrelated
|
||||||
|
`#include` was added to `src/game.c`: gcovr found 18 stale `.gcno` files
|
||||||
|
describing the old layout, merged them with the current ones, and could not
|
||||||
|
reconcile the two. Moving `build-coverage/` aside makes the same tree pass
|
||||||
|
18/18. Verified with gcovr 7.0.
|
||||||
|
|
||||||
|
The `build*/` entry in `.gitignore` hides these trees from `git status`,
|
||||||
|
which makes the state easier to get into and no easier to notice.
|
||||||
|
|
||||||
|
A second, smaller version of the same thing: rebuilding an existing
|
||||||
|
coverage tree after editing a test leaves `.gcda` files describing the old
|
||||||
|
object layout, and `coverage_reset` -- whose whole job is to delete them --
|
||||||
|
fails with `GCOV returncode was 5` before it gets the chance. `find
|
||||||
|
build-coverage -name '*.gcda' -delete` clears it. Observed with gcovr 7.0.
|
||||||
|
|
||||||
|
Fix: pass the build tree to gcovr as an explicit search path instead of
|
||||||
|
letting it default to `--root`, so only the tree under measurement is
|
||||||
|
considered. Touches the two `add_test` blocks at `CMakeLists.txt:204-211`
|
||||||
|
and `CMakeLists.txt:220-229`; nothing outside the `AKGL_COVERAGE` branch.
|
||||||
|
|
||||||
|
## Build notes
|
||||||
|
|
||||||
|
The vendored SDL satellite libraries are built into per-project subdirectories
|
||||||
|
that are not on the loader's default search path, and `LD_LIBRARY_PATH` is
|
||||||
|
searched ahead of RPATH — so a developer who has previously run `rebuild.sh` had
|
||||||
|
their installed `libakgl.so` shadow the one under test, and a developer who had
|
||||||
|
not saw every test abort before `main()` with "cannot open shared object file".
|
||||||
|
`CMakeLists.txt` now sets `BUILD_RPATH` on the library, the utility, and every
|
||||||
|
test target, and prepends the build tree to `LD_LIBRARY_PATH` for the CTest run.
|
||||||
|
|
||||||
|
## API gaps blocking akbasic
|
||||||
|
|
||||||
|
`akbasic` (the C port of the BASIC interpreter, `source.starfort.tech/andrew/akbasic`) is
|
||||||
|
being built to link into `libakgl` as a scripting engine for game authors. Its interpreter
|
||||||
|
core is complete and passes its whole acceptance corpus against a stdio text sink; the
|
||||||
|
`libakgl`-backed sink and the graphics, sound and console verbs of Commodore BASIC 7.0 are
|
||||||
|
blocked on the four gaps below.
|
||||||
|
|
||||||
|
These are filed here rather than worked around in `akbasic` because growing `libakgl` to serve
|
||||||
|
a consumer is the wanted outcome. Each entry says what the BASIC verb needs, what the
|
||||||
|
`akgl_*` entry point should look like, and what would cover it.
|
||||||
|
|
||||||
|
1. **No way to measure rendered text.** `include/akgl/text.h` exposes `akgl_text_loadfont()`
|
||||||
|
and `akgl_text_rendertextat()`, and nothing that reports how large a string will be in a
|
||||||
|
given font. A terminal-style text surface cannot be built on that: a cursor needs the
|
||||||
|
advance width of one cell, and wrapping needs to know where a string crosses the right
|
||||||
|
margin. The reference interpreter got this from SDL2_ttf's `font.SizeUTF8("A")` and derived
|
||||||
|
its whole character grid from it.
|
||||||
|
|
||||||
|
Wants `akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h);`
|
||||||
|
over `TTF_GetStringSize`, and probably a companion
|
||||||
|
`akgl_text_measure_wrapped(font, text, wraplength, w, h)` matching the `wraplength`
|
||||||
|
argument `akgl_text_rendertextat` already takes. Tests: a known string in a known font at a
|
||||||
|
known size, the empty string, and a wrapped string wide enough to force two lines.
|
||||||
|
|
||||||
|
This is the only one of the four that blocks work already designed and waiting. **`akbasic`
|
||||||
|
cannot render any output through `libakgl` until it lands.**
|
||||||
|
|
||||||
|
**Resolved.** `akgl_text_measure(font, text, w, h)` and
|
||||||
|
`akgl_text_measure_wrapped(font, text, wraplength, w, h)` are in
|
||||||
|
`include/akgl/text.h`, over `TTF_GetStringSize` and `TTF_GetStringSizeWrapped`.
|
||||||
|
Neither needs a renderer. A negative `wraplength` is refused with
|
||||||
|
`AKERR_OUTOFBOUNDS` rather than passed through, because SDL_ttf reads it as a
|
||||||
|
very large unsigned width and silently stops wrapping. `tests/text.c` covers
|
||||||
|
both against `tests/assets/akgl_test_mono.ttf`, a 10 KB monospaced ASCII
|
||||||
|
subset added for the purpose — being monospaced, it lets the suite assert
|
||||||
|
`width("AAAA") == 4 * width("A")` instead of hardcoding glyph metrics that
|
||||||
|
FreeType is free to round differently. Same change fixed item 39 below:
|
||||||
|
`akgl_text_loadfont` checked `name` twice and never checked `filepath`.
|
||||||
|
|
||||||
|
2. **No immediate-mode drawing.** `include/akgl/draw.h` declares exactly one function,
|
||||||
|
`akgl_draw_background(int w, int h)`, and `src/draw.c` is at 0% coverage. BASIC 7.0's
|
||||||
|
graphics verbs are all immediate-mode plotting against the current screen: `DRAW` (line and
|
||||||
|
point), `BOX`, `CIRCLE`, `PAINT` (flood fill), `LOCATE` (set the pixel cursor), `COLOR`,
|
||||||
|
and `SSHAPE`/`GSHAPE` (save and restore a rectangle of pixels).
|
||||||
|
|
||||||
|
Wants an `akgl_draw_*` family taking the renderer the host already initialized --
|
||||||
|
`akgl_draw_line`, `_rect`, `_filled_rect`, `_circle`, `_point`, `_flood_fill`,
|
||||||
|
`_copy_region` -- in the shape of the existing `akgl_render_2d_draw_texture`. SDL3's
|
||||||
|
`SDL_RenderLine`/`SDL_RenderRect`/`SDL_RenderFillRect` cover most of it; the circle and the
|
||||||
|
flood fill do not exist in SDL3 and need writing. Tests belong with the offscreen renderer
|
||||||
|
harness described under "Remaining work": render a known shape, read the target back, and
|
||||||
|
compare against a reference surface with the existing `akgl_compare_sdl_surfaces`.
|
||||||
|
|
||||||
|
**Resolved.** `include/akgl/draw.h` now declares `akgl_draw_point`, `_line`,
|
||||||
|
`_rect`, `_filled_rect`, `_circle`, `_flood_fill`, `_copy_region` and
|
||||||
|
`_paste_region`, all taking the `akgl_RenderBackend *` the host initialized,
|
||||||
|
in the shape of `akgl_render_2d_draw_texture`. Decisions worth knowing:
|
||||||
|
|
||||||
|
- **Color is an argument, not state.** There is no current-color global to
|
||||||
|
get out of step with the caller's own. Each call saves and restores the
|
||||||
|
renderer's draw color, so drawing a line does not change what the host's
|
||||||
|
next `SDL_RenderClear` paints. `tests/draw.c` asserts that.
|
||||||
|
- **The circle is a midpoint circle**, integer arithmetic with eight-way
|
||||||
|
symmetry, plotted eight points per step through `SDL_RenderPoints`.
|
||||||
|
- **The flood fill reads the target back, fills on the CPU, and blits only
|
||||||
|
the bounding box of what changed.** It keeps a fixed
|
||||||
|
`AKGL_DRAW_MAX_FLOOD_SPANS` (4096) stack of horizontal runs at file scope
|
||||||
|
rather than recursing per pixel; running out reports `AKERR_OUTOFBOUNDS`
|
||||||
|
and leaves the region partially filled, which is stated in the header. It
|
||||||
|
is therefore not reentrant — neither is anything else that draws to a
|
||||||
|
single `SDL_Renderer`.
|
||||||
|
- `_copy_region` allocates when `*dest` is `NULL` and otherwise copies into
|
||||||
|
the caller's surface, matching `akgl_get_json_string_value` and friends. A
|
||||||
|
region that would be clipped by the target edge is refused rather than
|
||||||
|
silently returning a smaller surface.
|
||||||
|
|
||||||
|
`tests/draw.c` draws into a 64x64 software renderer under the dummy video
|
||||||
|
driver and reads pixels back, so it did not need the offscreen harness. That
|
||||||
|
harness is still wanted for `src/renderer.c`, `src/text.c` and `src/assets.c`.
|
||||||
|
`akgl_draw_background` is untouched and still outside the error protocol
|
||||||
|
(item 35).
|
||||||
|
|
||||||
|
3. **No audio API at all.** `SDL3_mixer` is a vendored dependency and `registry.h` declares
|
||||||
|
`AKGL_REGISTRY_MUSIC`, but there is no `src/audio.c`, no `include/akgl/audio.h`, and no
|
||||||
|
`akgl_*` symbol that opens a mixer, loads a chunk, or plays a note. BASIC 7.0's sound verbs
|
||||||
|
are `SOUND` (a tone on a voice, with a duration), `PLAY` (a string of notes in a
|
||||||
|
Commodore-specific notation), `ENVELOPE` (ADSR per voice), `FILTER`, `VOL` and `TEMPO`.
|
||||||
|
|
||||||
|
`PLAY` and `ENVELOPE` want a synthesised voice rather than a sample, which SDL3_mixer does
|
||||||
|
not provide directly -- the honest first step is a small tone generator feeding
|
||||||
|
`SDL_AudioStream`, with `akgl_audio_init`, `akgl_audio_tone(voice, hz, ms)`,
|
||||||
|
`akgl_audio_envelope(voice, a, d, s, r)` and `akgl_audio_volume(level)`. This is the largest
|
||||||
|
of the four and the one most worth designing before writing. Tests can run under the dummy
|
||||||
|
audio driver and assert state transitions rather than sound.
|
||||||
|
|
||||||
|
**Resolved.** `include/akgl/audio.h` and `src/audio.c` add a three-voice tone
|
||||||
|
generator over `SDL_AudioStream`: `akgl_audio_init`, `_shutdown`, `_tone`,
|
||||||
|
`_stop`, `_waveform`, `_envelope`, `_volume`, `_voice_active` and `_mix`.
|
||||||
|
It is deliberately separate from the SDL3_mixer side of the library, which
|
||||||
|
plays audio *assets*; nothing in `audio.c` reads a file. Decisions worth
|
||||||
|
knowing:
|
||||||
|
|
||||||
|
- **The voice table works with no device open.** `akgl_audio_init` connects
|
||||||
|
it to one; without that a host can still pull samples itself through
|
||||||
|
`akgl_audio_mix`. That is not only for embedding — it is what makes the
|
||||||
|
suite deterministic. A device pulls samples on SDL's audio thread whenever
|
||||||
|
it likes, so a test that opened one and then asserted on voice state would
|
||||||
|
be racing the callback. `tests/audio.c` mixes by hand and opens a device
|
||||||
|
only in its last test.
|
||||||
|
- **Phase is derived from the frame counter, not accumulated.** A float
|
||||||
|
increment of `hz / 44100` is not exact, and adding it 44100 times a second
|
||||||
|
walks a held note off pitch.
|
||||||
|
- **A voice that was never configured is audible.** A zeroed voice has a
|
||||||
|
sustain of 0.0, which is silence with no error to explain it, so the table
|
||||||
|
defaults to a square wave held at full level.
|
||||||
|
- **Three voices summing past full scale are clamped, not scaled**, so one
|
||||||
|
voice plays at the level it was asked for rather than a third of it.
|
||||||
|
- Everything that touches the table takes the stream lock when a device is
|
||||||
|
open, since the callback reads it on another thread.
|
||||||
|
|
||||||
|
Still missing for a complete BASIC sound vocabulary: `FILTER` (SDL3 has no
|
||||||
|
filter primitive; this would need writing), `TEMPO` and the `PLAY` note-string
|
||||||
|
parser, both of which belong in the interpreter rather than here.
|
||||||
|
|
||||||
|
4. **No non-blocking keystroke read.** `include/akgl/controller.h` is built around SDL event
|
||||||
|
handlers the host pumps (`akgl_controller_handle_event` and friends), which suits a game
|
||||||
|
loop and does not suit `GET` and `GETKEY` -- those ask "is there a keystroke waiting, yes or
|
||||||
|
no" and must not require the interpreter to own the event loop. Goal 3 of `akbasic` forbids
|
||||||
|
it owning one.
|
||||||
|
|
||||||
|
Wants `akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available);`
|
||||||
|
reading a small ring buffer that `akgl_controller_handle_event` already fills, so the host
|
||||||
|
keeps pumping events and the interpreter drains characters at its own pace. Tests: push
|
||||||
|
synthetic `SDL_EVENT_KEY_DOWN` events through the existing handler and drain them, plus the
|
||||||
|
empty-buffer and overflow cases.
|
||||||
|
|
||||||
|
**Resolved.** `akgl_controller_poll_key(int *keycode, bool *available)` and
|
||||||
|
`akgl_controller_flush_keys(void)` are in `include/akgl/controller.h`, over a
|
||||||
|
fixed `AKGL_CONTROLLER_KEY_BUFFER` (32) ring in `src/controller.c`.
|
||||||
|
`akgl_controller_handle_event` records every `SDL_EVENT_KEY_DOWN` *before* it
|
||||||
|
scans the control maps, so a key bound to an actor still reaches a polling
|
||||||
|
caller — the scan returns as soon as a binding claims the event, and doing it
|
||||||
|
afterwards would have lost exactly the keys a game also acts on. An empty
|
||||||
|
buffer is success with `available` false, not an error. A full buffer drops
|
||||||
|
the newest key rather than overwriting the oldest, matching the Commodore
|
||||||
|
keyboard buffer and keeping what was typed first. `tests/controller.c` covers
|
||||||
|
drain order, the release-is-not-a-keystroke case, a key shared with a control
|
||||||
|
map, flush, both NULL arguments, and overflow plus reuse afterwards.
|
||||||
|
|
||||||
|
## Carried over
|
||||||
|
|
||||||
|
1. **Make character-to-sprite state bindings release their references symmetrically.**
|
||||||
|
`akgl_character_sprite_add()` increments the sprite refcount for every state-map
|
||||||
|
binding, but no corresponding removal API exists. Replacing a state binding
|
||||||
|
also leaves the previous sprite's refcount incremented. When
|
||||||
|
`akgl_heap_release_character()` drops the character refcount to zero, it clears
|
||||||
|
the character registry entry and zeroes the structure without enumerating
|
||||||
|
`state_sprites`, decrementing the bound sprites, or destroying the SDL property
|
||||||
|
map. Implement binding removal/replacement so each removed binding releases
|
||||||
|
exactly one sprite reference. On final character release, enumerate every
|
||||||
|
remaining binding (the existing `akgl_character_state_sprites_iterate()` release
|
||||||
|
path may be reusable), release each reference, destroy `state_sprites`, and then
|
||||||
|
clear the character. Add tests for removal, replacement, duplicate sprite
|
||||||
|
bindings across multiple states, and final character release.
|
||||||
|
|
||||||
|
`akgl_character_state_sprites_iterate` is still uncovered and is the natural
|
||||||
|
place to start; `tests/actor.c` now covers `akgl_character_sprite_get` and the
|
||||||
|
composite-state binding behavior it depends on.
|
||||||
2
deps/libakerror
vendored
2
deps/libakerror
vendored
Submodule deps/libakerror updated: 4a09ca87fd...5ff87908e7
2
deps/libakstdlib
vendored
2
deps/libakstdlib
vendored
Submodule deps/libakstdlib updated: b9d703f48a...95e5002512
@@ -1,9 +1,9 @@
|
|||||||
#ifndef _SDL_GAMECONTROLLERDB_H_
|
#ifndef _SDL_GAMECONTROLLERDB_H_
|
||||||
#define _SDL_GAMECONTROLLERDB_H_
|
#define _SDL_GAMECONTROLLERDB_H_
|
||||||
|
|
||||||
// Taken from https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt on Tue May 26 10:27:58 AM EDT 2026
|
// Taken from https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt on Tue Jun 2 04:45:53 PM EDT 2026
|
||||||
|
|
||||||
#define AKGL_SDL_GAMECONTROLLER_DB_LEN 2228
|
#define AKGL_SDL_GAMECONTROLLER_DB_LEN 2229
|
||||||
|
|
||||||
const char *SDL_GAMECONTROLLER_DB[] = {
|
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",
|
"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",
|
||||||
@@ -708,7 +708,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
|||||||
"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",
|
"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",
|
"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",
|
"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 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",
|
"03000000632500002705000000000000,ShanWan Gamepad,a:b0,b:b1,x:b3,y:b4,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,-lefty:-a1,+lefty:+a2,leftshoulder:b6,lefttrigger:b8,rightshoulder:b7,righttrigger:b9,start:b11,leftstick:b13,rightstick:b14,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",
|
"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",
|
"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",
|
"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",
|
||||||
@@ -780,6 +780,7 @@ 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",
|
"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",
|
"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",
|
"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,x:b3,y:b4,back:b5,start:b2,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,lefttrigger:b8,righttrigger:b9,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",
|
"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",
|
"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",
|
"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",
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file actor.h
|
||||||
|
* @brief Declares the public actor API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _AKGL_ACTOR_H_
|
#ifndef _AKGL_ACTOR_H_
|
||||||
#define _AKGL_ACTOR_H_
|
#define _AKGL_ACTOR_H_
|
||||||
|
|
||||||
@@ -48,6 +53,7 @@
|
|||||||
// This is an array of strings equal to actor states from 1-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
|
// This is built by a utility script and not kept in git, see
|
||||||
// the Makefile for lib_src/actor_state_string_names.c
|
// the Makefile for lib_src/actor_state_string_names.c
|
||||||
|
/** @brief Maps actor-state bit positions to symbolic names. */
|
||||||
extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1];
|
extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1];
|
||||||
|
|
||||||
#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)
|
#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)
|
||||||
@@ -58,6 +64,7 @@ extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1];
|
|||||||
|
|
||||||
#define AKGL_MAX_HEAP_ACTOR 64
|
#define AKGL_MAX_HEAP_ACTOR 64
|
||||||
|
|
||||||
|
/** @brief Represents a live actor, including state, motion, hierarchy, and behavior callbacks. */
|
||||||
typedef struct akgl_Actor {
|
typedef struct akgl_Actor {
|
||||||
uint8_t refcount;
|
uint8_t refcount;
|
||||||
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||||||
@@ -71,9 +78,31 @@ typedef struct akgl_Actor {
|
|||||||
void *actorData;
|
void *actorData;
|
||||||
bool visible;
|
bool visible;
|
||||||
SDL_Time movetimer;
|
SDL_Time movetimer;
|
||||||
|
// Velocity. Combined effect of all forces acting on the actor resulting
|
||||||
|
// in energy along an axis.
|
||||||
float32_t vx;
|
float32_t vx;
|
||||||
float32_t vy;
|
float32_t vy;
|
||||||
float32_t vz;
|
float32_t vz;
|
||||||
|
// Environmental velocity. These are the forces acting on the actor by the
|
||||||
|
// environment (such as gravity and atmospheric drag)
|
||||||
|
float32_t ex;
|
||||||
|
float32_t ey;
|
||||||
|
float32_t ez;
|
||||||
|
// Thrust. Energy originating only from the actor's own acceleration on a
|
||||||
|
// given axis, before the effects of gravity and drag.
|
||||||
|
float32_t tx;
|
||||||
|
float32_t ty;
|
||||||
|
float32_t tz;
|
||||||
|
// 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;
|
||||||
|
float32_t ay;
|
||||||
|
float32_t az;
|
||||||
|
// Max speed. These are borrowed from the base character object.
|
||||||
|
float32_t sx;
|
||||||
|
float32_t sy;
|
||||||
|
float32_t sz;
|
||||||
|
// Position.
|
||||||
float32_t x;
|
float32_t x;
|
||||||
float32_t y;
|
float32_t y;
|
||||||
float32_t z;
|
float32_t z;
|
||||||
@@ -83,29 +112,151 @@ typedef struct akgl_Actor {
|
|||||||
akerr_ErrorContext AKERR_NOIGNORE *(*updatefunc)(struct akgl_Actor *obj);
|
akerr_ErrorContext AKERR_NOIGNORE *(*updatefunc)(struct akgl_Actor *obj);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *(*renderfunc)(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 *(*facefunc)(struct akgl_Actor *obj);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *(*movementlogicfunc)(struct akgl_Actor *obj, SDL_Time curtimems);
|
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 *(*changeframefunc)(struct akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *(*addchild)(struct akgl_Actor *obj, struct akgl_Actor *child);
|
akerr_ErrorContext AKERR_NOIGNORE *(*addchild)(struct akgl_Actor *obj, struct akgl_Actor *child);
|
||||||
} akgl_Actor;
|
} akgl_Actor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Actor initialize.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param name Registry key or human-readable object name.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_initialize(akgl_Actor *obj, char *name);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_initialize(akgl_Actor *obj, char *name);
|
||||||
|
/**
|
||||||
|
* @brief Actor set character.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param basecharname Registry name of the character to assign.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_set_character(akgl_Actor *obj, char *basecharname);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_set_character(akgl_Actor *obj, char *basecharname);
|
||||||
|
/**
|
||||||
|
* @brief Actor render.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_render(akgl_Actor *obj);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_render(akgl_Actor *obj);
|
||||||
|
/**
|
||||||
|
* @brief Actor update.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_update(akgl_Actor *obj);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_update(akgl_Actor *obj);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_movement(akgl_Actor *obj, SDL_Time curtimems);
|
/**
|
||||||
|
* @brief Actor logic movement.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param dt Elapsed simulation time in seconds.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_movement(akgl_Actor *obj, float32_t dt);
|
||||||
|
/**
|
||||||
|
* @brief Actor logic changeframe.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param curSprite Sprite currently selected for the actor.
|
||||||
|
* @param curtimems Current frame-clock value.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems);
|
||||||
|
/**
|
||||||
|
* @brief Actor automatic face.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_automatic_face(akgl_Actor *obj);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_automatic_face(akgl_Actor *obj);
|
||||||
|
/**
|
||||||
|
* @brief Actor add child.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param child Actor to attach as a child.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_RELATIONSHIP When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Actor cmhf left on.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
/**
|
||||||
|
* @brief Actor cmhf left off.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
/**
|
||||||
|
* @brief Actor cmhf right on.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
/**
|
||||||
|
* @brief Actor cmhf right off.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
/**
|
||||||
|
* @brief Actor cmhf up on.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
/**
|
||||||
|
* @brief Actor cmhf up off.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
/**
|
||||||
|
* @brief Actor cmhf down on.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
/**
|
||||||
|
* @brief Actor cmhf down off.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Registry iterate actor.
|
||||||
|
* @param userdata Caller data supplied to the SDL property iterator.
|
||||||
|
* @param registry SDL property registry being queried.
|
||||||
|
* @param name Registry key or human-readable object name.
|
||||||
|
*/
|
||||||
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name);
|
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name);
|
||||||
|
|
||||||
#endif // _AKGL_ACTOR_H_
|
#endif // _AKGL_ACTOR_H_
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* @file assets.h
|
||||||
|
* @brief Declares the public assets API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _ASSETS_H_
|
#ifndef _ASSETS_H_
|
||||||
#define _ASSETS_H_
|
#define _ASSETS_H_
|
||||||
|
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Load start bgm.
|
||||||
|
* @param fname Path to the input or output file.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_load_start_bgm(char *fname);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_load_start_bgm(char *fname);
|
||||||
|
|
||||||
#endif //_ASSETS_H_
|
#endif //_ASSETS_H_
|
||||||
|
|||||||
204
include/akgl/audio.h
Normal file
204
include/akgl/audio.h
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
/**
|
||||||
|
* @file audio.h
|
||||||
|
* @brief Declares the public audio API.
|
||||||
|
*
|
||||||
|
* A small tone generator: a fixed set of voices, each with a waveform, a
|
||||||
|
* frequency, a gate length and an ADSR envelope, mixed to one stream of float
|
||||||
|
* samples. This is what a synthesised-voice vocabulary needs -- SOUND, PLAY,
|
||||||
|
* ENVELOPE and VOL all describe a note rather than a recording -- and it is
|
||||||
|
* deliberately separate from the SDL3_mixer side of the library, which loads
|
||||||
|
* and plays audio *assets*. Nothing here reads a file.
|
||||||
|
*
|
||||||
|
* The voice table exists whether or not an audio device is open.
|
||||||
|
* akgl_audio_init() connects it to one; without that a caller can still set up
|
||||||
|
* voices and pull samples itself with akgl_audio_mix(), which is how the test
|
||||||
|
* suite exercises the synthesis without depending on a sound card's timing.
|
||||||
|
* What a caller cannot do is set up voices and expect to hear them with no
|
||||||
|
* device open.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AKGL_AUDIO_H_
|
||||||
|
#define _AKGL_AUDIO_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akgl/types.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Voices that can sound at once.
|
||||||
|
*
|
||||||
|
* Three, because the machine this vocabulary comes from had three and its
|
||||||
|
* music is written for three. A voice is addressed here by a zero-based index;
|
||||||
|
* a language whose own voices are numbered from one maps them itself.
|
||||||
|
*/
|
||||||
|
#define AKGL_AUDIO_MAX_VOICES 3
|
||||||
|
|
||||||
|
/** @brief Sample rate of the generated stream, in frames per second. */
|
||||||
|
#define AKGL_AUDIO_SAMPLE_RATE 44100
|
||||||
|
|
||||||
|
/** @brief Frames the device callback generates per pass through the mixer. */
|
||||||
|
#define AKGL_AUDIO_MIX_FRAMES 512
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Shape of one voice's oscillator.
|
||||||
|
*
|
||||||
|
* The trailing comment on each is the waveform number the C128 SOUND statement
|
||||||
|
* uses for the same shape, for a caller translating one to the other.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
AKGL_AUDIO_WAVE_TRIANGLE = 0, /** SOUND waveform 0. Soft, flute-like. */
|
||||||
|
AKGL_AUDIO_WAVE_SAWTOOTH = 1, /** SOUND waveform 1. Bright, brassy. */
|
||||||
|
AKGL_AUDIO_WAVE_SQUARE = 2, /** SOUND waveform 2. Hollow, reedy. The default. */
|
||||||
|
AKGL_AUDIO_WAVE_NOISE = 3, /** SOUND waveform 3. Unpitched; percussion. */
|
||||||
|
AKGL_AUDIO_WAVE_SINE = 4 /** No SOUND equivalent. A pure tone. */
|
||||||
|
} akgl_AudioWaveform;
|
||||||
|
|
||||||
|
/** @brief Holds one voice's oscillator, envelope, and how far through it is. */
|
||||||
|
typedef struct {
|
||||||
|
bool active;
|
||||||
|
akgl_AudioWaveform waveform;
|
||||||
|
float32_t hz;
|
||||||
|
/**
|
||||||
|
* @brief Position through one cycle, 0.0 to 1.0.
|
||||||
|
*
|
||||||
|
* Derived from `elapsed_frames` and `hz` each sample rather than
|
||||||
|
* accumulated, so a long note does not drift off pitch. Reading it is
|
||||||
|
* meaningful; writing it is not.
|
||||||
|
*/
|
||||||
|
float32_t phase;
|
||||||
|
/** @brief Frames the gate stays open, before the release begins. */
|
||||||
|
uint32_t duration_frames;
|
||||||
|
/** @brief Frames generated since the tone started, gate and release. */
|
||||||
|
uint32_t elapsed_frames;
|
||||||
|
uint32_t attack_frames;
|
||||||
|
uint32_t decay_frames;
|
||||||
|
uint32_t release_frames;
|
||||||
|
/** @brief Level the envelope decays to and holds, 0.0 to 1.0. */
|
||||||
|
float32_t sustain;
|
||||||
|
} akgl_AudioVoice;
|
||||||
|
|
||||||
|
/** @brief The process-wide voice table. */
|
||||||
|
extern akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Open an audio device and start pulling samples from the voice table.
|
||||||
|
*
|
||||||
|
* Requires SDL's audio subsystem to be initialized. Repeating the call is a
|
||||||
|
* no-op, so a program that cannot order its initialization precisely may call
|
||||||
|
* it more than once. The voice table is reset only on the first call, so this
|
||||||
|
* does not silence a voice that is already sounding.
|
||||||
|
*
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Close the audio device and silence every voice.
|
||||||
|
*
|
||||||
|
* Safe to call when no device is open.
|
||||||
|
*
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sound a note on one voice for a fixed time.
|
||||||
|
*
|
||||||
|
* The voice's envelope and waveform are whatever akgl_audio_envelope() and
|
||||||
|
* akgl_audio_waveform() last set them to. @p ms is the length of the gate: the
|
||||||
|
* voice's release runs *after* it, so a voice with a release stays audible
|
||||||
|
* slightly longer than @p ms. Sounding a voice that is already sounding
|
||||||
|
* restarts it from the beginning of its envelope.
|
||||||
|
*
|
||||||
|
* @param voice Zero-based voice index.
|
||||||
|
* @param hz Frequency in hertz.
|
||||||
|
* @param ms Gate length in milliseconds.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Silence one voice immediately, skipping its release.
|
||||||
|
* @param voice Zero-based voice index.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Choose the oscillator shape one voice will use.
|
||||||
|
*
|
||||||
|
* Takes effect on the next akgl_audio_tone(); it does not reshape a note that
|
||||||
|
* is already sounding.
|
||||||
|
*
|
||||||
|
* @param voice Zero-based voice index.
|
||||||
|
* @param waveform Oscillator shape to use.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set one voice's ADSR envelope.
|
||||||
|
*
|
||||||
|
* @p attack, @p decay and @p release are milliseconds; @p sustain is the level
|
||||||
|
* the envelope decays to and holds while the gate is open, from 0.0 to 1.0. A
|
||||||
|
* zero-length stage is skipped rather than divided by.
|
||||||
|
*
|
||||||
|
* Takes effect on the next akgl_audio_tone().
|
||||||
|
*
|
||||||
|
* @param voice Zero-based voice index.
|
||||||
|
* @param attack Milliseconds to rise from silence to full level.
|
||||||
|
* @param decay Milliseconds to fall from full level to @p sustain.
|
||||||
|
* @param sustain Held level while the gate is open, 0.0 to 1.0.
|
||||||
|
* @param release Milliseconds to fall to silence once the gate closes.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the level every voice is scaled by.
|
||||||
|
* @param level Master level, 0.0 to 1.0.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Report whether a voice is still sounding.
|
||||||
|
*
|
||||||
|
* A voice goes quiet on its own when its gate and release have both elapsed, so
|
||||||
|
* this is how a caller waits out a note without keeping its own clock.
|
||||||
|
*
|
||||||
|
* @param voice Zero-based voice index.
|
||||||
|
* @param active Output destination set to `true` while the voice is sounding.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Generate the next @p frames mono samples from the voice table.
|
||||||
|
*
|
||||||
|
* The device callback installed by akgl_audio_init() is a loop around this. A
|
||||||
|
* host that owns its own audio pipeline can call it directly instead and never
|
||||||
|
* open a device here at all. Samples are single-precision, one channel, in the
|
||||||
|
* range -1.0 to 1.0, and every active voice is advanced by @p frames.
|
||||||
|
*
|
||||||
|
* @param dest Output destination populated with @p frames samples.
|
||||||
|
* @param frames Number of samples to generate.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames);
|
||||||
|
|
||||||
|
#endif // _AKGL_AUDIO_H_
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file character.h
|
||||||
|
* @brief Declares the public character API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _AKGL_CHARACTER_H_
|
#ifndef _AKGL_CHARACTER_H_
|
||||||
#define _AKGL_CHARACTER_H_
|
#define _AKGL_CHARACTER_H_
|
||||||
|
|
||||||
@@ -8,6 +13,7 @@
|
|||||||
#define AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH 128
|
#define AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH 128
|
||||||
#define AKGL_MAX_HEAP_CHARACTER 256
|
#define AKGL_MAX_HEAP_CHARACTER 256
|
||||||
|
|
||||||
|
/** @brief Defines reusable movement parameters and actor-state sprite bindings. */
|
||||||
typedef struct akgl_Character {
|
typedef struct akgl_Character {
|
||||||
uint8_t refcount;
|
uint8_t refcount;
|
||||||
char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH];
|
char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH];
|
||||||
@@ -24,13 +30,50 @@ typedef struct akgl_Character {
|
|||||||
} akgl_Character;
|
} akgl_Character;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Character initialize.
|
||||||
|
* @param basechar Character whose state-to-sprite map is accessed.
|
||||||
|
* @param name Registry key or human-readable object name.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *basechar, char *name);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *basechar, char *name);
|
||||||
|
/**
|
||||||
|
* @brief Character sprite add.
|
||||||
|
* @param basechar Character whose state-to-sprite map is accessed.
|
||||||
|
* @param ref Sprite reference to associate with the character.
|
||||||
|
* @param state Actor-state bit mask used as a sprite-map key.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state);
|
||||||
|
/**
|
||||||
|
* @brief Character sprite get.
|
||||||
|
* @param basechar Character whose state-to-sprite map is accessed.
|
||||||
|
* @param state Actor-state bit mask used as a sprite-map key.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest);
|
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.
|
// This is an SDL iterator so we can't return our error state from it.
|
||||||
|
/**
|
||||||
|
* @brief Character state sprites iterate.
|
||||||
|
* @param userdata Caller data supplied to the SDL property iterator.
|
||||||
|
* @param props SDL property collection being iterated.
|
||||||
|
* @param name Registry key or human-readable object name.
|
||||||
|
*/
|
||||||
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name);
|
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Character load json.
|
||||||
|
* @param filename Path to the source asset or JSON document.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_load_json(char *filename);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_load_json(char *filename);
|
||||||
|
|
||||||
#endif // _AKGL_CHARACTER_H_
|
#endif // _AKGL_CHARACTER_H_
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file controller.h
|
||||||
|
* @brief Declares the public controller API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _CONTROLLER_H_
|
#ifndef _CONTROLLER_H_
|
||||||
#define _CONTROLLER_H_
|
#define _CONTROLLER_H_
|
||||||
|
|
||||||
@@ -8,6 +13,17 @@
|
|||||||
#define AKGL_MAX_CONTROL_MAPS 8
|
#define AKGL_MAX_CONTROL_MAPS 8
|
||||||
#define AKGL_MAX_CONTROLS 32
|
#define AKGL_MAX_CONTROLS 32
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Keystrokes akgl_controller_handle_event() will hold for a poller.
|
||||||
|
*
|
||||||
|
* The Commodore keyboard buffer this serves held ten. Thirty-two is enough that
|
||||||
|
* a program which polls once per frame never loses a key to a fast typist, and
|
||||||
|
* small enough that the buffer stays a fixed-size object in the library's data
|
||||||
|
* segment.
|
||||||
|
*/
|
||||||
|
#define AKGL_CONTROLLER_KEY_BUFFER 32
|
||||||
|
|
||||||
|
/** @brief Maps one SDL input to pressed and released callbacks. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t event_on;
|
uint32_t event_on;
|
||||||
uint32_t event_off;
|
uint32_t event_off;
|
||||||
@@ -20,6 +36,7 @@ typedef struct {
|
|||||||
akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event);
|
||||||
} akgl_Control;
|
} akgl_Control;
|
||||||
|
|
||||||
|
/** @brief Groups input bindings for one actor and its input devices. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
akgl_Actor *target;
|
akgl_Actor *target;
|
||||||
uint16_t nextMap;
|
uint16_t nextMap;
|
||||||
@@ -30,20 +47,119 @@ typedef struct {
|
|||||||
SDL_PenID penid;
|
SDL_PenID penid;
|
||||||
} akgl_ControlMap;
|
} akgl_ControlMap;
|
||||||
|
|
||||||
|
/** @brief Stores all process-wide actor input maps. */
|
||||||
extern akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
|
extern akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Controller list keyboards.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Controller handle event.
|
||||||
|
* @param appstate Application state supplied by SDL.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_event(void *appstate, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_event(void *appstate, SDL_Event *event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Controller handle button down.
|
||||||
|
* @param appstate Application state supplied by SDL.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_down(void *appstate, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_down(void *appstate, SDL_Event *event);
|
||||||
|
/**
|
||||||
|
* @brief Controller handle button up.
|
||||||
|
* @param appstate Application state supplied by SDL.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_up(void *appstate, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_up(void *appstate, SDL_Event *event);
|
||||||
|
/**
|
||||||
|
* @brief Controller handle added.
|
||||||
|
* @param appstate Application state supplied by SDL.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_added(void *appstate, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_added(void *appstate, SDL_Event *event);
|
||||||
|
/**
|
||||||
|
* @brief Controller handle removed.
|
||||||
|
* @param appstate Application state supplied by SDL.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate, SDL_Event *event);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate, SDL_Event *event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Controller pushmap.
|
||||||
|
* @param controlmapid Index of the control map to update.
|
||||||
|
* @param control Control binding to append to the selected map.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Controller default.
|
||||||
|
* @param controlmapid Index of the control map to update.
|
||||||
|
* @param actorname Registry name of the controlled actor.
|
||||||
|
* @param kbid SDL keyboard identifier assigned to the map.
|
||||||
|
* @param jsid SDL joystick or gamepad identifier assigned to the map.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_REGISTRY When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Controller open gamepads.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Take the oldest waiting keystroke, if there is one.
|
||||||
|
*
|
||||||
|
* The rest of this header is built around the host pumping SDL events into
|
||||||
|
* akgl_controller_handle_event(), which suits a game loop and does not suit an
|
||||||
|
* embedded interpreter asking "is there a key waiting, yes or no" without
|
||||||
|
* owning the event loop itself. Every key press that reaches
|
||||||
|
* akgl_controller_handle_event() is recorded in a fixed ring buffer first,
|
||||||
|
* whether or not a control map claims it, and this drains that buffer one
|
||||||
|
* keystroke per call.
|
||||||
|
*
|
||||||
|
* When no key is waiting the call still succeeds: @p available is set to
|
||||||
|
* `false` and @p keycode to 0. The caller polls, it does not block.
|
||||||
|
*
|
||||||
|
* A full buffer drops the *newest* keystroke rather than the oldest, so what
|
||||||
|
* was typed first is what is read first. This runs on whichever thread pumps
|
||||||
|
* events; it is not synchronized.
|
||||||
|
*
|
||||||
|
* @param keycode Output destination populated with the SDL keycode, or 0.
|
||||||
|
* @param available Output destination set to `true` when a keystroke was taken.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Discard every keystroke waiting in the buffer.
|
||||||
|
*
|
||||||
|
* For a caller that has been ignoring input and does not want a backlog acted
|
||||||
|
* on the moment it starts polling again.
|
||||||
|
*
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void);
|
||||||
#endif // _CONTROLLER_H_
|
#endif // _CONTROLLER_H_
|
||||||
|
|||||||
@@ -1,6 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* @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_
|
#ifndef _DRAW_H_
|
||||||
#define _DRAW_H_
|
#define _DRAW_H_
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akgl/renderer.h>
|
||||||
|
#include <akgl/types.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Spans akgl_draw_flood_fill() will hold while walking a region.
|
||||||
|
*
|
||||||
|
* The fill keeps a fixed stack of horizontal runs still to be examined rather
|
||||||
|
* than recursing per pixel. A region complicated enough to need more than this
|
||||||
|
* many pending runs at once reports AKERR_OUTOFBOUNDS instead of overflowing;
|
||||||
|
* an ordinary convex or moderately concave shape needs a few dozen.
|
||||||
|
*/
|
||||||
|
#define AKGL_DRAW_MAX_FLOOD_SPANS 4096
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Draw background.
|
||||||
|
* @param w Destination width.
|
||||||
|
* @param h Destination height.
|
||||||
|
*/
|
||||||
void akgl_draw_background(int w, int h);
|
void akgl_draw_background(int w, int h);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Plot a single pixel.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param x Horizontal destination coordinate.
|
||||||
|
* @param y Vertical destination coordinate.
|
||||||
|
* @param color Color to draw with.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Draw a line between two points.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param x1 Horizontal coordinate of the first endpoint.
|
||||||
|
* @param y1 Vertical coordinate of the first endpoint.
|
||||||
|
* @param x2 Horizontal coordinate of the second endpoint.
|
||||||
|
* @param y2 Vertical coordinate of the second endpoint.
|
||||||
|
* @param color Color to draw with.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Draw the outline of a rectangle.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param rect Rectangle to outline.
|
||||||
|
* @param color Color to draw with.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fill a rectangle.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param rect Rectangle to fill.
|
||||||
|
* @param color Color to draw with.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Draw the outline of a circle.
|
||||||
|
*
|
||||||
|
* SDL3 has no circle primitive, so this plots one with the midpoint circle
|
||||||
|
* algorithm -- integer arithmetic, eight-way symmetry, one pass per octant. A
|
||||||
|
* radius of zero draws the center pixel and nothing else.
|
||||||
|
*
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param x Horizontal coordinate of the center.
|
||||||
|
* @param y Vertical coordinate of the center.
|
||||||
|
* @param radius Circle radius in pixels.
|
||||||
|
* @param color Color to draw with.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Flood the connected region containing one pixel with a color.
|
||||||
|
*
|
||||||
|
* SDL3 has no flood fill either, and unlike the shape primitives it cannot be
|
||||||
|
* done on the GPU side: the region is defined by what is already on the screen.
|
||||||
|
* This reads the render target back, walks the region on the CPU with a
|
||||||
|
* bounded span stack, and blits the result over the area it touched.
|
||||||
|
*
|
||||||
|
* Filling a region that already holds @p color is a no-op rather than an error.
|
||||||
|
* A seed outside the render target reports AKERR_OUTOFBOUNDS.
|
||||||
|
*
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param x Horizontal coordinate of the seed pixel.
|
||||||
|
* @param y Vertical coordinate of the seed pixel.
|
||||||
|
* @param color Color to fill with.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read a rectangle of the render target into a surface.
|
||||||
|
*
|
||||||
|
* The save half of SSHAPE/GSHAPE. When `*dest` is `NULL` the function allocates
|
||||||
|
* the surface and the caller owns it from then on -- release it with
|
||||||
|
* SDL_DestroySurface(). When `*dest` already points at a surface of exactly
|
||||||
|
* @p src's dimensions the pixels are copied into it instead, so a caller
|
||||||
|
* saving the same region repeatedly does not churn allocations.
|
||||||
|
*
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param src Rectangle of the render target to read.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Draw a saved surface back onto the render target.
|
||||||
|
*
|
||||||
|
* The restore half of SSHAPE/GSHAPE, taking what akgl_draw_copy_region()
|
||||||
|
* produced. The surface is not consumed and may be pasted as many times as the
|
||||||
|
* caller likes.
|
||||||
|
*
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param src Surface to draw.
|
||||||
|
* @param x Horizontal destination coordinate.
|
||||||
|
* @param y Vertical destination coordinate.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y);
|
||||||
|
|
||||||
#endif //_DRAW_H_
|
#endif //_DRAW_H_
|
||||||
|
|||||||
@@ -1,6 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* @file error.h
|
||||||
|
* @brief Declares the public error API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _ERROR_H_
|
#ifndef _ERROR_H_
|
||||||
#define _ERROR_H_
|
#define _ERROR_H_
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* libakerror 1.0.0 is the floor. That release moved the status-name table into a
|
||||||
|
* private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the
|
||||||
|
* registry entry points raise akerr_ErrorContext * instead of returning int, and
|
||||||
|
* the library gained an soname -- so a translation unit that pairs this header
|
||||||
|
* with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem.
|
||||||
|
*
|
||||||
|
* libakerror publishes no version macro, so this feature-tests on
|
||||||
|
* AKERR_FIRST_CONSUMER_STATUS, which that release introduced, rather than on a
|
||||||
|
* version number that does not exist. Without the guard an embedded build is
|
||||||
|
* fine but a stale installed header fails much further in, on the AKGL_ERR_*
|
||||||
|
* codes below and again inside src/heap.c.
|
||||||
|
*
|
||||||
|
* See deps/libakerror/UPGRADING.md.
|
||||||
|
*/
|
||||||
|
#ifndef AKERR_FIRST_CONSUMER_STATUS
|
||||||
|
#error "libakgl requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
|
||||||
|
#endif
|
||||||
|
|
||||||
// This macro is used to silence warnings on string concatenation operations that may fail.
|
// This macro is used to silence warnings on string concatenation operations that may fail.
|
||||||
// e.g., combining two element of PATH_MAX into a string buffer of AKGL_STRING_MAX_LENGTH.
|
// e.g., combining two element of PATH_MAX into a string buffer of AKGL_STRING_MAX_LENGTH.
|
||||||
// We have to draw a line in the sand somewhere or we will just let our buffers grow forever
|
// We have to draw a line in the sand somewhere or we will just let our buffers grow forever
|
||||||
@@ -12,7 +38,41 @@
|
|||||||
#define RESTORE_GCC_WARNINGS \
|
#define RESTORE_GCC_WARNINGS \
|
||||||
_Pragma("GCC diagnostic pop")
|
_Pragma("GCC diagnostic pop")
|
||||||
|
|
||||||
#define AKGL_ERR_SDL AKERR_LAST_ERRNO_VALUE + 1
|
// libakerror reserves statuses 0-255 for the host's errno values and its own
|
||||||
#define AKGL_ERR_LOGICINTERRUPT AKERR_LAST_ERRNO_VALUE + 2
|
// 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);
|
||||||
|
|
||||||
#endif // _ERROR_H_
|
#endif // _ERROR_H_
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file game.h
|
||||||
|
* @brief Declares the public game API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _AKGL_GAME_H_
|
#ifndef _AKGL_GAME_H_
|
||||||
#define _AKGL_GAME_H_
|
#define _AKGL_GAME_H_
|
||||||
|
|
||||||
@@ -7,8 +12,9 @@
|
|||||||
#include "tilemap.h"
|
#include "tilemap.h"
|
||||||
#include "renderer.h"
|
#include "renderer.h"
|
||||||
#include "physics.h"
|
#include "physics.h"
|
||||||
|
// AKGL_VERSION used to be defined here by hand, which is how akgl.pc came to
|
||||||
#define AKGL_VERSION "0.1.0"
|
// ship an empty Version field: nothing tied the two together.
|
||||||
|
#include <akgl/version.h>
|
||||||
|
|
||||||
#define AKGL_GAME_AUDIO_TRACK_BGM 1
|
#define AKGL_GAME_AUDIO_TRACK_BGM 1
|
||||||
#define AKGL_GAME_AUDIO_MAX_TRACKS 64
|
#define AKGL_GAME_AUDIO_MAX_TRACKS 64
|
||||||
@@ -18,16 +24,19 @@
|
|||||||
|
|
||||||
/* ==================== GAME STATE VARIABLES =================== */
|
/* ==================== GAME STATE VARIABLES =================== */
|
||||||
|
|
||||||
|
/** @brief Describes a renderable frame. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
float32_t w;
|
float32_t w;
|
||||||
float32_t h;
|
float32_t h;
|
||||||
SDL_Texture *texture;
|
SDL_Texture *texture;
|
||||||
} akgl_Frame;
|
} akgl_Frame;
|
||||||
|
|
||||||
|
/** @brief Stores application-defined game-state flags. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int32_t flags;
|
int32_t flags;
|
||||||
} akgl_GameState;
|
} akgl_GameState;
|
||||||
|
|
||||||
|
/** @brief Stores game metadata, timing, synchronization, and FPS accounting. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char libversion[32];
|
char libversion[32];
|
||||||
char version[32];
|
char version[32];
|
||||||
@@ -43,15 +52,33 @@ typedef struct {
|
|||||||
void (*lowfpsfunc)(void);
|
void (*lowfpsfunc)(void);
|
||||||
} akgl_Game;
|
} akgl_Game;
|
||||||
|
|
||||||
|
/** @brief SDL window used by the active renderer. */
|
||||||
extern SDL_Window *window;
|
extern SDL_Window *window;
|
||||||
extern akgl_Tilemap gamemap;
|
/** @brief Loaded background-music resource. */
|
||||||
extern MIX_Audio *bgm;
|
extern MIX_Audio *bgm;
|
||||||
|
/** @brief Mixer used for game audio playback. */
|
||||||
extern MIX_Mixer *akgl_mixer;
|
extern MIX_Mixer *akgl_mixer;
|
||||||
|
/** @brief Process-wide audio-track table. */
|
||||||
extern MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
|
extern MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
|
||||||
extern SDL_FRect camera;
|
/** @brief Storage for the default camera. */
|
||||||
|
extern SDL_FRect _akgl_camera;
|
||||||
|
/** @brief Process-wide game metadata and timing state. */
|
||||||
extern akgl_Game game;
|
extern akgl_Game game;
|
||||||
extern akgl_RenderBackend renderer;
|
/** @brief Storage for the default renderer. */
|
||||||
extern akgl_PhysicsBackend physics;
|
extern akgl_RenderBackend _akgl_renderer;
|
||||||
|
/** @brief Storage for the default physics backend. */
|
||||||
|
extern akgl_PhysicsBackend _akgl_physics;
|
||||||
|
/** @brief Storage for the default tilemap. */
|
||||||
|
extern akgl_Tilemap _akgl_gamemap;
|
||||||
|
|
||||||
|
/** @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;
|
||||||
|
|
||||||
#define AKGL_BITMASK_HAS(x, y) (x & y) == y
|
#define AKGL_BITMASK_HAS(x, y) (x & y) == y
|
||||||
#define AKGL_BITMASK_HASNOT(x, y) (x & y) != y
|
#define AKGL_BITMASK_HASNOT(x, y) (x & y) != y
|
||||||
@@ -59,14 +86,60 @@ extern akgl_PhysicsBackend physics;
|
|||||||
#define AKGL_BITMASK_DEL(x, y) x &= ~(y)
|
#define AKGL_BITMASK_DEL(x, y) x &= ~(y)
|
||||||
#define AKGL_BITMASK_CLEAR(x) x = 0;
|
#define AKGL_BITMASK_CLEAR(x) x = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Game init.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init();
|
||||||
|
/**
|
||||||
|
* @brief Game init screen.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init_screen();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init_screen();
|
||||||
|
/**
|
||||||
|
* @brief Game updatefps.
|
||||||
|
*/
|
||||||
void akgl_game_updateFPS();
|
void akgl_game_updateFPS();
|
||||||
|
/**
|
||||||
|
* @brief Game save.
|
||||||
|
* @param fpath Path to the input or output file.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath);
|
||||||
|
/**
|
||||||
|
* @brief Game load.
|
||||||
|
* @param fpath Path to the input or output file.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath);
|
||||||
|
/**
|
||||||
|
* @brief Game lowfps.
|
||||||
|
*/
|
||||||
void akgl_game_lowfps(void);
|
void akgl_game_lowfps(void);
|
||||||
|
/**
|
||||||
|
* @brief Game state lock.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void);
|
||||||
|
/**
|
||||||
|
* @brief Game state unlock.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
|
||||||
|
/**
|
||||||
|
* @brief Game update.
|
||||||
|
* @param opflags Optional iterator operation flags; `NULL` selects defaults.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags);
|
||||||
|
|
||||||
#endif //_AKGL_GAME_H_
|
#endif //_AKGL_GAME_H_
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file heap.h
|
||||||
|
* @brief Declares the public heap API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _AKGL_HEAP_H_
|
#ifndef _AKGL_HEAP_H_
|
||||||
#define _AKGL_HEAP_H_
|
#define _AKGL_HEAP_H_
|
||||||
|
|
||||||
@@ -23,24 +28,103 @@
|
|||||||
#define AKGL_MAX_HEAP_STRING 256
|
#define AKGL_MAX_HEAP_STRING 256
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/** @brief Fixed-capacity actor object pool. */
|
||||||
extern akgl_Actor HEAP_ACTOR[AKGL_MAX_HEAP_ACTOR];
|
extern akgl_Actor HEAP_ACTOR[AKGL_MAX_HEAP_ACTOR];
|
||||||
|
/** @brief Fixed-capacity sprite object pool. */
|
||||||
extern akgl_Sprite HEAP_SPRITE[AKGL_MAX_HEAP_SPRITE];
|
extern akgl_Sprite HEAP_SPRITE[AKGL_MAX_HEAP_SPRITE];
|
||||||
|
/** @brief Fixed-capacity spritesheet object pool. */
|
||||||
extern akgl_SpriteSheet HEAP_SPRITESHEET[AKGL_MAX_HEAP_SPRITESHEET];
|
extern akgl_SpriteSheet HEAP_SPRITESHEET[AKGL_MAX_HEAP_SPRITESHEET];
|
||||||
|
/** @brief Fixed-capacity character object pool. */
|
||||||
extern akgl_Character HEAP_CHARACTER[AKGL_MAX_HEAP_CHARACTER];
|
extern akgl_Character HEAP_CHARACTER[AKGL_MAX_HEAP_CHARACTER];
|
||||||
|
/** @brief Fixed-capacity static-string object pool. */
|
||||||
extern akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING];
|
extern akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Heap init.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_BEHAVIOR When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_LOGICINTERRUPT When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_REGISTRY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init();
|
||||||
|
/**
|
||||||
|
* @brief Heap init actor.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor();
|
||||||
|
/**
|
||||||
|
* @brief Heap next actor.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_actor(akgl_Actor **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_actor(akgl_Actor **dest);
|
||||||
|
/**
|
||||||
|
* @brief Heap next sprite.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_sprite(akgl_Sprite **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_sprite(akgl_Sprite **dest);
|
||||||
|
/**
|
||||||
|
* @brief Heap next spritesheet.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest);
|
||||||
|
/**
|
||||||
|
* @brief Heap next character.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_character(akgl_Character **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_character(akgl_Character **dest);
|
||||||
|
/**
|
||||||
|
* @brief Heap next string.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_string(akgl_String **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_string(akgl_String **dest);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Heap release actor.
|
||||||
|
* @param ptr Heap object whose reference count is decremented.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_actor(akgl_Actor *ptr);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_actor(akgl_Actor *ptr);
|
||||||
|
/**
|
||||||
|
* @brief Heap release sprite.
|
||||||
|
* @param ptr Heap object whose reference count is decremented.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_sprite(akgl_Sprite *ptr);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_sprite(akgl_Sprite *ptr);
|
||||||
|
/**
|
||||||
|
* @brief Heap release spritesheet.
|
||||||
|
* @param ptr Heap object whose reference count is decremented.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_spritesheet(akgl_SpriteSheet *ptr);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_spritesheet(akgl_SpriteSheet *ptr);
|
||||||
|
/**
|
||||||
|
* @brief Heap release character.
|
||||||
|
* @param ptr Heap object whose reference count is decremented.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_character(akgl_Character *ptr);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_character(akgl_Character *ptr);
|
||||||
|
/**
|
||||||
|
* @brief Heap release string.
|
||||||
|
* @param ptr Heap object whose reference count is decremented.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_string(akgl_String *ptr);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_string(akgl_String *ptr);
|
||||||
|
|
||||||
#endif //_AKGL_HEAP_H_
|
#endif //_AKGL_HEAP_H_
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* @file iterator.h
|
||||||
|
* @brief Declares the public iterator API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _AKGL_ITERATOR_H_
|
#ifndef _AKGL_ITERATOR_H_
|
||||||
#define _AKGL_ITERATOR_H_
|
#define _AKGL_ITERATOR_H_
|
||||||
|
|
||||||
|
/** @brief Selects operations and an optional layer for actor traversal. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t flags;
|
uint32_t flags;
|
||||||
uint8_t layerid;
|
uint8_t layerid;
|
||||||
|
|||||||
@@ -1,17 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* @file json_helpers.h
|
||||||
|
* @brief Declares the public json helpers API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _JSON_HELPERS_H_
|
#ifndef _JSON_HELPERS_H_
|
||||||
#define _JSON_HELPERS_H_
|
#define _JSON_HELPERS_H_
|
||||||
|
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
#include "staticstring.h"
|
#include "staticstring.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get json object value.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest);
|
||||||
|
/**
|
||||||
|
* @brief Get json boolean value.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest);
|
||||||
|
/**
|
||||||
|
* @brief Get json integer value.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_integer_value(json_t *obj, char *key, int *dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_integer_value(json_t *obj, char *key, int *dest);
|
||||||
|
/**
|
||||||
|
* @brief Get json number value.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char *key, float *dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char *key, float *dest);
|
||||||
|
/**
|
||||||
|
* @brief Get json double value.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest);
|
||||||
|
/**
|
||||||
|
* @brief Get json string value.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest);
|
||||||
|
/**
|
||||||
|
* @brief Get json array value.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest);
|
||||||
|
/**
|
||||||
|
* @brief Get json array index object.
|
||||||
|
* @param array JSON array to read.
|
||||||
|
* @param index Zero-based element index.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest);
|
||||||
|
/**
|
||||||
|
* @brief Get json array index integer.
|
||||||
|
* @param array JSON array to read.
|
||||||
|
* @param index Zero-based element index.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_integer(json_t *array, int index, int *dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_integer(json_t *array, int index, int *dest);
|
||||||
|
/**
|
||||||
|
* @brief Get json array index string.
|
||||||
|
* @param array JSON array to read.
|
||||||
|
* @param index Zero-based element index.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get json with default.
|
||||||
|
* @param e Error context to inspect and optionally consume.
|
||||||
|
* @param defval Fallback value copied when the supplied error is eligible.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param defsize Size in bytes of the fallback value.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_INDEX When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize);
|
||||||
|
|
||||||
#endif // _JSON_HELPERS_H_
|
#endif // _JSON_HELPERS_H_
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file physics.h
|
||||||
|
* @brief Declares the public physics API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _PHYSICS_H_
|
#ifndef _PHYSICS_H_
|
||||||
#define _PHYSICS_H_
|
#define _PHYSICS_H_
|
||||||
|
|
||||||
@@ -5,12 +10,14 @@
|
|||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
#include <akgl/actor.h>
|
#include <akgl/actor.h>
|
||||||
#include <akgl/iterator.h>
|
#include <akgl/iterator.h>
|
||||||
|
#include <akgl/staticstring.h>
|
||||||
|
|
||||||
|
/** @brief Defines a pluggable physics backend and its environmental parameters. */
|
||||||
typedef struct akgl_PhysicsBackend {
|
typedef struct akgl_PhysicsBackend {
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *(*simulate)(struct akgl_PhysicsBackend *self, akgl_Iterator *opflags);
|
akerr_ErrorContext AKERR_NOIGNORE *(*simulate)(struct akgl_PhysicsBackend *self, akgl_Iterator *opflags);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *(*gravity)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, SDL_Time curtime);
|
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 *(*collide)(struct akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, SDL_Time curtime);
|
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||||
|
|
||||||
double drag_x;
|
double drag_x;
|
||||||
double drag_y;
|
double drag_y;
|
||||||
@@ -20,21 +27,98 @@ typedef struct akgl_PhysicsBackend {
|
|||||||
double gravity_z;
|
double gravity_z;
|
||||||
SDL_Time gravity_time;
|
SDL_Time gravity_time;
|
||||||
SDL_Time timer_gravity;
|
SDL_Time timer_gravity;
|
||||||
|
|
||||||
} akgl_PhysicsBackend;
|
} akgl_PhysicsBackend;
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, SDL_Time curtime);
|
/**
|
||||||
|
* @brief Physics null gravity.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param actor Actor to inspect or modify.
|
||||||
|
* @param dt Elapsed simulation time in seconds.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||||
|
/**
|
||||||
|
* @brief Physics null collide.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param a1 First actor supplied to collision handling.
|
||||||
|
* @param a2 Second actor supplied to collision handling.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, SDL_Time curtime);
|
/**
|
||||||
|
* @brief Physics null move.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param actor Actor to inspect or modify.
|
||||||
|
* @param dt Elapsed simulation time in seconds.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||||
|
/**
|
||||||
|
* @brief Physics init null.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *self);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *self);
|
||||||
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, SDL_Time curtime);
|
/**
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
* @brief Physics arcade gravity.
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_move(akgl_PhysicsBackend *self, akgl_Actor *actor, SDL_Time curtime);
|
* @param self Backend or object instance to operate on.
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_sidescroller(akgl_PhysicsBackend *self);
|
* @param actor Actor to inspect or modify.
|
||||||
|
* @param dt Elapsed simulation time in seconds.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||||
|
/**
|
||||||
|
* @brief Physics arcade collide.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param a1 First actor supplied to collision handling.
|
||||||
|
* @param a2 Second actor supplied to collision handling.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
||||||
|
/**
|
||||||
|
* @brief Physics arcade move.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param actor Actor to inspect or modify.
|
||||||
|
* @param dt Elapsed simulation time in seconds.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||||
|
/**
|
||||||
|
* @brief Physics init arcade.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Physics factory.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param type Expected JSON property type or backend type name.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Physics simulate.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param opflags Optional iterator operation flags; `NULL` selects defaults.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_LOGICINTERRUPT When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags);
|
||||||
|
|
||||||
#endif // _PHYSICS_H_
|
#endif // _PHYSICS_H_
|
||||||
|
|||||||
@@ -1,29 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* @file registry.h
|
||||||
|
* @brief Declares the public registry API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _REGISTRY_H_
|
#ifndef _REGISTRY_H_
|
||||||
#define _REGISTRY_H_
|
#define _REGISTRY_H_
|
||||||
|
|
||||||
#include <akgl/error.h>
|
#include <akgl/error.h>
|
||||||
#include <akgl/staticstring.h>
|
#include <akgl/staticstring.h>
|
||||||
|
|
||||||
|
/** @brief Registry of actors keyed by name. */
|
||||||
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR;
|
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR;
|
||||||
|
/** @brief Registry of sprites keyed by name. */
|
||||||
extern SDL_PropertiesID AKGL_REGISTRY_SPRITE;
|
extern SDL_PropertiesID AKGL_REGISTRY_SPRITE;
|
||||||
|
/** @brief Registry of spritesheets keyed by asset path. */
|
||||||
extern SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET;
|
extern SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET;
|
||||||
|
/** @brief Registry of characters keyed by name. */
|
||||||
extern SDL_PropertiesID AKGL_REGISTRY_CHARACTER;
|
extern SDL_PropertiesID AKGL_REGISTRY_CHARACTER;
|
||||||
|
/** @brief Registry mapping actor-state names to bit values. */
|
||||||
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR_STATE_STRINGS;
|
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR_STATE_STRINGS;
|
||||||
|
/** @brief Registry of fonts keyed by name. */
|
||||||
extern SDL_PropertiesID AKGL_REGISTRY_FONT;
|
extern SDL_PropertiesID AKGL_REGISTRY_FONT;
|
||||||
|
/** @brief Registry of music assets keyed by name. */
|
||||||
extern SDL_PropertiesID AKGL_REGISTRY_MUSIC;
|
extern SDL_PropertiesID AKGL_REGISTRY_MUSIC;
|
||||||
|
/** @brief Registry of string configuration properties. */
|
||||||
extern SDL_PropertiesID AKGL_REGISTRY_PROPERTIES;
|
extern SDL_PropertiesID AKGL_REGISTRY_PROPERTIES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Registry init.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init();
|
||||||
|
/**
|
||||||
|
* @brief Registry init music.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music();
|
||||||
|
/**
|
||||||
|
* @brief Registry init font.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font();
|
||||||
|
/**
|
||||||
|
* @brief Registry init actor.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor();
|
||||||
|
/**
|
||||||
|
* @brief Registry init sprite.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite();
|
||||||
|
/**
|
||||||
|
* @brief Registry init spritesheet.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet();
|
||||||
|
/**
|
||||||
|
* @brief Registry init character.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character();
|
||||||
|
/**
|
||||||
|
* @brief Registry init properties.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_properties();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_properties();
|
||||||
|
/**
|
||||||
|
* @brief Registry load properties.
|
||||||
|
* @param fname Path to the input or output file.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname);
|
||||||
|
/**
|
||||||
|
* @brief Registry init actor state strings.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor_state_strings();
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor_state_strings();
|
||||||
|
/**
|
||||||
|
* @brief Set property.
|
||||||
|
* @param name Registry key or human-readable object name.
|
||||||
|
* @param value Property value to store.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *value);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *value);
|
||||||
|
/**
|
||||||
|
* @brief Get property.
|
||||||
|
* @param name Registry key or human-readable object name.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param def Fallback string returned when the property is absent.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def);
|
||||||
|
|
||||||
#endif //_REGISTRY_H_
|
#endif //_REGISTRY_H_
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file renderer.h
|
||||||
|
* @brief Declares the public renderer API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _RENDERER_H_
|
#ifndef _RENDERER_H_
|
||||||
#define _RENDERER_H_
|
#define _RENDERER_H_
|
||||||
|
|
||||||
@@ -7,6 +12,7 @@
|
|||||||
|
|
||||||
#include <akgl/iterator.h>
|
#include <akgl/iterator.h>
|
||||||
|
|
||||||
|
/** @brief Defines a pluggable renderer backend and drawing callbacks. */
|
||||||
typedef struct akgl_RenderBackend {
|
typedef struct akgl_RenderBackend {
|
||||||
SDL_Renderer *sdl_renderer;
|
SDL_Renderer *sdl_renderer;
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self);
|
akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self);
|
||||||
@@ -17,13 +23,63 @@ typedef struct akgl_RenderBackend {
|
|||||||
akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags);
|
akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags);
|
||||||
} akgl_RenderBackend;
|
} akgl_RenderBackend;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Render 2d shutdown.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self);
|
||||||
|
/**
|
||||||
|
* @brief Render 2d frame start.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self);
|
||||||
|
/**
|
||||||
|
* @brief Render 2d frame end.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self);
|
||||||
|
/**
|
||||||
|
* @brief Render 2d draw texture.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param texture SDL texture to draw.
|
||||||
|
* @param src Source value to copy or inspect.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param angle Clockwise rotation angle in degrees.
|
||||||
|
* @param center Optional rotation center.
|
||||||
|
* @param flip SDL texture flip mode.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_texture(akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip);
|
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 Render 2d draw mesh.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self);
|
||||||
|
/**
|
||||||
|
* @brief Render 2d draw world.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param opflags Optional iterator operation flags; `NULL` selects defaults.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Render init2d.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self);
|
||||||
|
|
||||||
#endif // _RENDERER_H_
|
#endif // _RENDERER_H_
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file sprite.h
|
||||||
|
* @brief Declares the public sprite API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _AKGL_SPRITE_H_
|
#ifndef _AKGL_SPRITE_H_
|
||||||
#define _AKGL_SPRITE_H_
|
#define _AKGL_SPRITE_H_
|
||||||
|
|
||||||
@@ -14,6 +19,7 @@
|
|||||||
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
|
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
|
||||||
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
|
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
|
||||||
|
|
||||||
|
/** @brief Stores a loaded spritesheet texture and frame geometry. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t refcount;
|
uint8_t refcount;
|
||||||
SDL_Texture *texture;
|
SDL_Texture *texture;
|
||||||
@@ -22,6 +28,7 @@ typedef struct {
|
|||||||
uint16_t sprite_h;
|
uint16_t sprite_h;
|
||||||
} akgl_SpriteSheet;
|
} akgl_SpriteSheet;
|
||||||
|
|
||||||
|
/** @brief Describes an animated sprite and its spritesheet reference. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t refcount;
|
uint8_t refcount;
|
||||||
uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; // which IDs on the spritesheet belong to our frames
|
uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; // which IDs on the spritesheet belong to our frames
|
||||||
@@ -36,11 +43,46 @@ typedef struct {
|
|||||||
} akgl_Sprite;
|
} akgl_Sprite;
|
||||||
|
|
||||||
// initializes a new sprite to use the given sheet and otherwise sets to zero
|
// initializes a new sprite to use the given sheet and otherwise sets to zero
|
||||||
|
/**
|
||||||
|
* @brief Sprite initialize.
|
||||||
|
* @param spr Sprite object to initialize.
|
||||||
|
* @param name Registry key or human-readable object name.
|
||||||
|
* @param sheet Spritesheet used by the sprite.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet);
|
||||||
// loads a given image file into a new spritesheet
|
// loads a given image file into a new spritesheet
|
||||||
|
/**
|
||||||
|
* @brief Spritesheet initialize.
|
||||||
|
* @param sheet Spritesheet used by the sprite.
|
||||||
|
* @param sprite_w Width of one sprite frame in pixels.
|
||||||
|
* @param sprite_h Height of one sprite frame in pixels.
|
||||||
|
* @param filename Path to the source asset or JSON document.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename);
|
||||||
|
/**
|
||||||
|
* @brief Sprite load json.
|
||||||
|
* @param filename Path to the source asset or JSON document.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_load_json(char *filename);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_load_json(char *filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sprite sheet coords for frame.
|
||||||
|
* @param self Backend or object instance to operate on.
|
||||||
|
* @param srccoords Output source rectangle for the selected frame.
|
||||||
|
* @param frameid Sprite frame index to resolve.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid);
|
akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid);
|
||||||
|
|
||||||
#endif //_AKGL_SPRITE_H_
|
#endif //_AKGL_SPRITE_H_
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file staticstring.h
|
||||||
|
* @brief Declares the public staticstring API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _STRING_H_
|
#ifndef _STRING_H_
|
||||||
#define _STRING_H_
|
#define _STRING_H_
|
||||||
|
|
||||||
@@ -7,12 +12,28 @@
|
|||||||
|
|
||||||
#define AKGL_MAX_STRING_LENGTH PATH_MAX
|
#define AKGL_MAX_STRING_LENGTH PATH_MAX
|
||||||
|
|
||||||
|
/** @brief Provides a fixed-capacity, heap-managed string buffer. */
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
int refcount;
|
int refcount;
|
||||||
char data[AKGL_MAX_STRING_LENGTH];
|
char data[AKGL_MAX_STRING_LENGTH];
|
||||||
} akgl_String;
|
} akgl_String;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief String initialize.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param init Optional initial string contents; `NULL` clears the buffer.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_initialize(akgl_String *obj, char *init);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_initialize(akgl_String *obj, char *init);
|
||||||
|
/**
|
||||||
|
* @brief String copy.
|
||||||
|
* @param src Source value to copy or inspect.
|
||||||
|
* @param dst Output destination populated by the function.
|
||||||
|
* @param count Maximum number of elements or bytes to process; zero selects the default.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_copy(akgl_String *src, akgl_String *dst, int count);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_copy(akgl_String *src, akgl_String *dst, int count);
|
||||||
#endif //_STRING_H_
|
#endif //_STRING_H_
|
||||||
|
|||||||
@@ -1,9 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* @file text.h
|
||||||
|
* @brief Declares the public text API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _TEXT_H_
|
#ifndef _TEXT_H_
|
||||||
#define _TEXT_H_
|
#define _TEXT_H_
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
#include <SDL3_ttf/SDL_ttf.h>
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Text loadfont.
|
||||||
|
* @param name Registry key or human-readable object name.
|
||||||
|
* @param filepath Path to the requested file.
|
||||||
|
* @param size Requested font size.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size);
|
||||||
|
/**
|
||||||
|
* @brief Text rendertextat.
|
||||||
|
* @param font Font used to render the text.
|
||||||
|
* @param text UTF-8 text to render.
|
||||||
|
* @param color Text color.
|
||||||
|
* @param wraplength Maximum rendered line width; zero disables wrapping.
|
||||||
|
* @param x Horizontal destination coordinate.
|
||||||
|
* @param y Vertical destination coordinate.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y);
|
||||||
|
/**
|
||||||
|
* @brief Report the size, in pixels, that @p text would occupy on one line.
|
||||||
|
*
|
||||||
|
* Nothing is drawn and no renderer is required. A caller building a character
|
||||||
|
* grid measures one cell with this -- the advance width of a single glyph in a
|
||||||
|
* monospaced font -- and derives the rest of the grid from it.
|
||||||
|
*
|
||||||
|
* @param font Font used to render the text.
|
||||||
|
* @param text UTF-8 text to measure.
|
||||||
|
* @param w Output destination populated with the rendered width in pixels.
|
||||||
|
* @param h Output destination populated with the rendered height in pixels.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h);
|
||||||
|
/**
|
||||||
|
* @brief Report the size, in pixels, that @p text would occupy when wrapped.
|
||||||
|
*
|
||||||
|
* The companion to akgl_text_measure() for the wrapping case, matching the
|
||||||
|
* @p wraplength argument akgl_text_rendertextat() already takes: a string
|
||||||
|
* longer than @p wraplength reports the height of every line it breaks onto.
|
||||||
|
* A @p wraplength of zero wraps only on newlines in @p text.
|
||||||
|
*
|
||||||
|
* @param font Font used to render the text.
|
||||||
|
* @param text UTF-8 text to measure.
|
||||||
|
* @param wraplength Maximum rendered line width; zero wraps on newlines only.
|
||||||
|
* @param w Output destination populated with the rendered width in pixels.
|
||||||
|
* @param h Output destination populated with the rendered height in pixels.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h);
|
||||||
|
|
||||||
#endif // _TEXT_H_
|
#endif // _TEXT_H_
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* @file tilemap.h
|
||||||
|
* @brief Declares the public tilemap API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _TILEMAP_H_
|
#ifndef _TILEMAP_H_
|
||||||
#define _TILEMAP_H_
|
#define _TILEMAP_H_
|
||||||
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include "actor.h"
|
#include <akgl/actor.h>
|
||||||
#include "staticstring.h"
|
#include <akgl/staticstring.h>
|
||||||
|
#include <akgl/physics.h>
|
||||||
#include <jansson.h>
|
#include <jansson.h>
|
||||||
|
|
||||||
#define AKGL_TILEMAP_MAX_WIDTH 512
|
#define AKGL_TILEMAP_MAX_WIDTH 512
|
||||||
@@ -22,6 +28,7 @@
|
|||||||
#define AKGL_TILEMAP_LAYER_TYPE_OBJECTS 2
|
#define AKGL_TILEMAP_LAYER_TYPE_OBJECTS 2
|
||||||
#define AKGL_TILEMAP_LAYER_TYPE_IMAGE 3
|
#define AKGL_TILEMAP_LAYER_TYPE_IMAGE 3
|
||||||
|
|
||||||
|
/** @brief Stores tileset metadata, texture, and frame offsets. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
float x;
|
float x;
|
||||||
float y;
|
float y;
|
||||||
@@ -36,6 +43,7 @@ typedef struct {
|
|||||||
char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE];
|
char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE];
|
||||||
} akgl_TilemapObject;
|
} akgl_TilemapObject;
|
||||||
|
|
||||||
|
/** @brief Represents an object embedded in a tilemap layer. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
short type;
|
short type;
|
||||||
float opacity;
|
float opacity;
|
||||||
@@ -50,6 +58,7 @@ typedef struct {
|
|||||||
akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER];
|
akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER];
|
||||||
} akgl_TilemapLayer;
|
} akgl_TilemapLayer;
|
||||||
|
|
||||||
|
/** @brief Stores tile, image, or object data for one map layer. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int columns;
|
int columns;
|
||||||
int firstgid;
|
int firstgid;
|
||||||
@@ -80,6 +89,7 @@ typedef struct {
|
|||||||
int margin;
|
int margin;
|
||||||
} akgl_Tileset;
|
} akgl_Tileset;
|
||||||
|
|
||||||
|
/** @brief Represents a complete tilemap and its optional physics backend. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int tilewidth;
|
int tilewidth;
|
||||||
int tileheight;
|
int tileheight;
|
||||||
@@ -98,10 +108,38 @@ typedef struct {
|
|||||||
float p_rate;
|
float p_rate;
|
||||||
akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS];
|
akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS];
|
||||||
akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS];
|
akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS];
|
||||||
|
|
||||||
|
// Different levels may have different physics.
|
||||||
|
bool use_own_physics;
|
||||||
|
akgl_PhysicsBackend physics;
|
||||||
} akgl_Tilemap;
|
} akgl_Tilemap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Tilemap load.
|
||||||
|
* @param fname Path to the input or output file.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load(char *fname, akgl_Tilemap *dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load(char *fname, akgl_Tilemap *dest);
|
||||||
|
/**
|
||||||
|
* @brief Tilemap draw.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param viewport World-space rectangle visible to the renderer.
|
||||||
|
* @param layeridx Zero-based tilemap layer index.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRect *viewport, int layeridx);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRect *viewport, int layeridx);
|
||||||
|
/**
|
||||||
|
* @brief Tilemap draw tileset.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param tilesetidx Zero-based tileset index.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest, int tilesetidx);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest, int tilesetidx);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -109,16 +147,108 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest,
|
|||||||
* They are only exposed here for unit testing.
|
* They are only exposed here for unit testing.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get json tilemap property.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param type Expected JSON property type or backend type name.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest);
|
||||||
|
/**
|
||||||
|
* @brief Get json properties string.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest);
|
||||||
|
/**
|
||||||
|
* @brief Get json properties integer.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest);
|
||||||
|
/**
|
||||||
|
* @brief Tilemap compute tileset offsets.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param tilesetidx Zero-based tileset index.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx);
|
||||||
|
/**
|
||||||
|
* @brief Tilemap load layer objects.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param root Base directory used to resolve the path.
|
||||||
|
* @param layerid Zero-based tilemap layer index.
|
||||||
|
* @param dirname Directory containing paths referenced by the document.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
|
||||||
|
/**
|
||||||
|
* @brief Tilemap load layer tile.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param root Base directory used to resolve the path.
|
||||||
|
* @param layerid Zero-based tilemap layer index.
|
||||||
|
* @param dirname Directory containing paths referenced by the document.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
|
||||||
|
/**
|
||||||
|
* @brief Tilemap load layers.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param root Base directory used to resolve the path.
|
||||||
|
* @param dirname Directory containing paths referenced by the document.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
|
||||||
|
/**
|
||||||
|
* @brief Tilemap load tilesets each.
|
||||||
|
* @param tileset JSON tileset object to load.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param tsidx Zero-based destination tileset index.
|
||||||
|
* @param dirname Directory containing paths referenced by the document.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname);
|
||||||
|
/**
|
||||||
|
* @brief Tilemap load tilesets.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param root Base directory used to resolve the path.
|
||||||
|
* @param dirname Directory containing paths referenced by the document.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
|
||||||
|
/**
|
||||||
|
* @brief Tilemap release.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest);
|
||||||
|
/**
|
||||||
|
* @brief Tilemap scale actor.
|
||||||
|
* @param map Tilemap to inspect, draw, or modify.
|
||||||
|
* @param actor Actor to inspect or modify.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor);
|
||||||
|
|
||||||
#endif //_TILEMAP_H_
|
#endif //_TILEMAP_H_
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file types.h
|
||||||
|
* @brief Declares the public types API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _AKGL_TYPES_H_
|
#ifndef _AKGL_TYPES_H_
|
||||||
#define _AKGL_TYPES_H_
|
#define _AKGL_TYPES_H_
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* @file util.h
|
||||||
|
* @brief Declares the public util API.
|
||||||
|
*/
|
||||||
|
|
||||||
#ifndef _UTIL_H_
|
#ifndef _UTIL_H_
|
||||||
#define _UTIL_H_
|
#define _UTIL_H_
|
||||||
|
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
#include <akgl/staticstring.h>
|
#include <akgl/staticstring.h>
|
||||||
|
|
||||||
|
/** @brief Represents a two-dimensional point. */
|
||||||
typedef struct point {
|
typedef struct point {
|
||||||
int x;
|
int x;
|
||||||
int y;
|
int y;
|
||||||
int z;
|
int z;
|
||||||
} point;
|
} point;
|
||||||
|
|
||||||
|
/** @brief Stores the corners of an axis-aligned rectangle. */
|
||||||
typedef struct RectanglePoints {
|
typedef struct RectanglePoints {
|
||||||
point topleft;
|
point topleft;
|
||||||
point topright;
|
point topright;
|
||||||
@@ -19,14 +26,67 @@ typedef struct RectanglePoints {
|
|||||||
|
|
||||||
#define AKGL_COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x)
|
#define AKGL_COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Rectangle points.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param rect Rectangle whose corner points are calculated.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect);
|
||||||
|
/**
|
||||||
|
* @brief Collide point rectangle.
|
||||||
|
* @param p Point to test for containment.
|
||||||
|
* @param r Rectangle corner data used for collision testing.
|
||||||
|
* @param collide Output set to whether the tested geometry intersects.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(point *p, RectanglePoints *r, bool *collide);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(point *p, RectanglePoints *r, bool *collide);
|
||||||
|
/**
|
||||||
|
* @brief Collide rectangles.
|
||||||
|
* @param r1 First rectangle to compare.
|
||||||
|
* @param r2 Second rectangle to compare.
|
||||||
|
* @param collide Output set to whether the tested geometry intersects.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Path relative.
|
||||||
|
* @param root Base directory used to resolve the path.
|
||||||
|
* @param path Path to resolve or transform.
|
||||||
|
* @param dst Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, akgl_String *dst);
|
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
|
// These are REALLY slow routines that are only useful in testing harnesses
|
||||||
|
/**
|
||||||
|
* @brief Compare sdl surfaces.
|
||||||
|
* @param s1 First SDL surface to compare.
|
||||||
|
* @param s2 Second SDL surface to compare.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_VALUE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2);
|
||||||
|
/**
|
||||||
|
* @brief Render and compare.
|
||||||
|
* @param t1 First texture to render for comparison.
|
||||||
|
* @param t2 Second texture to render for comparison.
|
||||||
|
* @param x Horizontal destination coordinate.
|
||||||
|
* @param y Vertical destination coordinate.
|
||||||
|
* @param w Destination width.
|
||||||
|
* @param h Destination height.
|
||||||
|
* @param writeout Optional filename for the rendered diagnostic image.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_IO When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout);
|
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_
|
#endif // _UTIL_H_
|
||||||
|
|||||||
56
include/akgl/version.h.in
Normal file
56
include/akgl/version.h.in
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* @file version.h
|
||||||
|
* @brief Declares the libakgl version, both as compiled against and as linked.
|
||||||
|
*
|
||||||
|
* GENERATED FILE -- edit include/akgl/version.h.in, never the copy in the build
|
||||||
|
* tree. Every value here comes from the project() call in CMakeLists.txt, which
|
||||||
|
* is also what sets the shared library's VERSION and SOVERSION and the Version
|
||||||
|
* field in akgl.pc. One number, one place, so the header, the soname and
|
||||||
|
* pkg-config cannot drift apart.
|
||||||
|
*
|
||||||
|
* AKGL_VERSION is the version you *compiled against*. akgl_version() reports the
|
||||||
|
* version of the libakgl you actually *linked*. They disagree when a stale
|
||||||
|
* shared library is ahead of the new one on the loader path -- the failure the
|
||||||
|
* soname exists to prevent and this pair exists to diagnose.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AKGL_VERSION_H_
|
||||||
|
#define _AKGL_VERSION_H_
|
||||||
|
|
||||||
|
#define AKGL_VERSION "@PROJECT_VERSION@"
|
||||||
|
#define AKGL_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
|
||||||
|
#define AKGL_VERSION_MINOR @PROJECT_VERSION_MINOR@
|
||||||
|
#define AKGL_VERSION_PATCH @PROJECT_VERSION_PATCH@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief True when the headers on the include path are at least the given version.
|
||||||
|
*
|
||||||
|
* For consumers that must build against more than one libakgl release. This is
|
||||||
|
* the test libakstdlib could not write against libakerror, which published no
|
||||||
|
* version macro and had to feature-test on AKERR_FIRST_CONSUMER_STATUS instead.
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* #if AKGL_VERSION_AT_LEAST(0, 2, 0)
|
||||||
|
* akgl_something_new();
|
||||||
|
* #endif
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
#define AKGL_VERSION_AT_LEAST(major, minor, patch) \
|
||||||
|
((AKGL_VERSION_MAJOR > (major)) || \
|
||||||
|
(AKGL_VERSION_MAJOR == (major) && AKGL_VERSION_MINOR > (minor)) || \
|
||||||
|
(AKGL_VERSION_MAJOR == (major) && AKGL_VERSION_MINOR == (minor) && \
|
||||||
|
AKGL_VERSION_PATCH >= (patch)))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Report the version of the libakgl that is actually linked.
|
||||||
|
*
|
||||||
|
* Returns "major.minor.patch". Compare it against AKGL_VERSION to detect a
|
||||||
|
* stale shared library. Never returns NULL, and the storage is static -- the
|
||||||
|
* caller must not free or modify it.
|
||||||
|
*
|
||||||
|
* This returns a string rather than an akerr_ErrorContext * because it cannot
|
||||||
|
* fail, the same reason akerr_name_for_status() returns a name directly.
|
||||||
|
*/
|
||||||
|
const char *akgl_version(void);
|
||||||
|
|
||||||
|
#endif // _AKGL_VERSION_H_
|
||||||
98
scripts/hooks/pre-commit
Executable file
98
scripts/hooks/pre-commit
Executable file
@@ -0,0 +1,98 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Reindent staged C sources to the canonical style before the commit is made.
|
||||||
|
# See AGENTS.md -> "Coding Style".
|
||||||
|
#
|
||||||
|
# Enable with:
|
||||||
|
# git config core.hooksPath scripts/hooks
|
||||||
|
# Bypass a single commit with `git commit --no-verify`.
|
||||||
|
#
|
||||||
|
# The hook checks the *staged* content, not the working tree, so what gets
|
||||||
|
# committed is what was verified. When a file needs reindenting it is fixed in
|
||||||
|
# the working tree and re-staged -- but only when the working tree and the index
|
||||||
|
# agree for that file. If they disagree (a partial `git add -p`), re-staging
|
||||||
|
# would sweep unstaged work into the commit, so the hook stops and asks you to
|
||||||
|
# do it yourself.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
root=$(git rev-parse --show-toplevel)
|
||||||
|
reindent="$root/scripts/reindent.sh"
|
||||||
|
|
||||||
|
# Leave conflict resolution alone.
|
||||||
|
if [ -e "$root/.git/MERGE_HEAD" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$reindent" ]; then
|
||||||
|
echo "pre-commit: $reindent missing or not executable; skipping style check" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Without Emacs the canonical style cannot be applied or even checked. Warn
|
||||||
|
# rather than block: a hook that fails closed on a missing optional tool just
|
||||||
|
# teaches everyone to pass --no-verify.
|
||||||
|
if ! command -v emacs >/dev/null 2>&1; then
|
||||||
|
echo "pre-commit: emacs not found; skipping reindent (see AGENTS.md)" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
staged=$(git diff --cached --name-only --diff-filter=ACMR \
|
||||||
|
| grep -E '^(src|include|tests|util)/.*\.[ch]$' \
|
||||||
|
| grep -v -x -F 'include/akgl/SDL_GameControllerDB.h' || true)
|
||||||
|
|
||||||
|
if [ -z "$staged" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||||
|
|
||||||
|
# Reindent a copy of each file's staged content and see whether it moves.
|
||||||
|
needs=''
|
||||||
|
for f in $staged; do
|
||||||
|
copy="$tmp/$(printf '%s' "$f" | tr '/' '_')"
|
||||||
|
git show ":$f" >"$copy"
|
||||||
|
cp "$copy" "$copy.orig"
|
||||||
|
if ! "$reindent" "$copy" >/dev/null 2>&1; then
|
||||||
|
echo "pre-commit: reindent failed on $f; commit aborted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! cmp -s "$copy" "$copy.orig"; then
|
||||||
|
needs="$needs $f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$needs" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Refuse to touch anything if even one file is partially staged.
|
||||||
|
blocked=''
|
||||||
|
for f in $needs; do
|
||||||
|
if ! git diff --quiet -- "$f"; then
|
||||||
|
blocked="$blocked $f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$blocked" ]; then
|
||||||
|
echo "pre-commit: these files need reindenting but have unstaged changes," >&2
|
||||||
|
echo "so re-staging them would pull unstaged work into the commit:" >&2
|
||||||
|
for f in $blocked; do echo " $f" >&2; done
|
||||||
|
echo >&2
|
||||||
|
echo "Reindent and stage them yourself, then commit again:" >&2
|
||||||
|
echo " scripts/reindent.sh$(for f in $blocked; do printf ' %s' "$f"; done)" >&2
|
||||||
|
echo " git add$(for f in $blocked; do printf ' %s' "$f"; done)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Safe: for every file needing work, the index and working tree agree.
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
"$reindent" $needs
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
git add $needs
|
||||||
|
|
||||||
|
echo "pre-commit: reindented and re-staged:" >&2
|
||||||
|
for f in $needs; do echo " $f" >&2; done
|
||||||
|
|
||||||
|
exit 0
|
||||||
466
scripts/mutation_test.py
Executable file
466
scripts/mutation_test.py
Executable file
@@ -0,0 +1,466 @@
|
|||||||
|
#!/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("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
.replace('"', """))
|
||||||
|
|
||||||
|
|
||||||
|
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())
|
||||||
87
scripts/reindent.el
Normal file
87
scripts/reindent.el
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
;;; reindent.el --- batch reindent to cc-mode "stroustrup" -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Reindents each file named on the command line, in place, to the project's
|
||||||
|
;; canonical style: cc-mode "stroustrup", 4 columns per level, tabs 8 columns
|
||||||
|
;; wide. See AGENTS.md -> "Coding Style".
|
||||||
|
;;
|
||||||
|
;; The style is set explicitly here rather than read from .dir-locals.el so that
|
||||||
|
;; the result does not depend on where the file lives -- the pre-commit hook
|
||||||
|
;; runs this over temporary copies outside the project tree.
|
||||||
|
;;
|
||||||
|
;; Usage: emacs --batch -Q -l scripts/reindent.el -- FILE...
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cc-mode)
|
||||||
|
|
||||||
|
(setq make-backup-files nil
|
||||||
|
create-lockfiles nil
|
||||||
|
auto-save-default nil
|
||||||
|
vc-handled-backends nil
|
||||||
|
inhibit-message t)
|
||||||
|
|
||||||
|
(defun akgl-retab-leading-whitespace ()
|
||||||
|
"Rewrite every line's leading whitespace as tabs-then-spaces at `tab-width'.
|
||||||
|
|
||||||
|
`indent-region' fixes the column a line starts at, but `indent-line-to' leaves
|
||||||
|
a line alone when it is already at the right column even if the bytes are
|
||||||
|
wrong -- eight spaces where the canonical form is one tab. This pass closes
|
||||||
|
that gap.
|
||||||
|
|
||||||
|
Deliberately not `tabify': that function's `tabify-regexp' is \" [ \\t]+\",
|
||||||
|
which is NOT anchored to the line start, so it rewrites runs of spaces
|
||||||
|
anywhere on the line and destroys the hand-aligned columns in the bit-flag
|
||||||
|
tables in actor.h and iterator.h. Only leading whitespace is touched here, and
|
||||||
|
lines beginning inside a string literal are skipped outright."
|
||||||
|
(goto-char (point-min))
|
||||||
|
(while (not (eobp))
|
||||||
|
(let ((bol (point)))
|
||||||
|
(unless (nth 3 (syntax-ppss bol))
|
||||||
|
(skip-chars-forward " \t" (line-end-position))
|
||||||
|
(let ((col (current-column)))
|
||||||
|
(unless (or (zerop col) (eolp))
|
||||||
|
(let ((want (concat (make-string (/ col tab-width) ?\t)
|
||||||
|
(make-string (% col tab-width) ?\s))))
|
||||||
|
(unless (string= want (buffer-substring bol (point)))
|
||||||
|
(delete-region bol (point))
|
||||||
|
(insert want)))))))
|
||||||
|
(forward-line 1)))
|
||||||
|
|
||||||
|
(defun akgl-reindent-file (path)
|
||||||
|
"Reindent PATH in place. Returns t if the file changed on disk."
|
||||||
|
(let ((before (with-temp-buffer
|
||||||
|
(insert-file-contents path)
|
||||||
|
(buffer-string))))
|
||||||
|
(with-current-buffer (find-file-noselect path t)
|
||||||
|
(c-mode)
|
||||||
|
(c-set-style "stroustrup")
|
||||||
|
(setq indent-tabs-mode t
|
||||||
|
tab-width 8
|
||||||
|
c-basic-offset 4
|
||||||
|
require-final-newline t)
|
||||||
|
;; 1. Put every line at its correct column.
|
||||||
|
(indent-region (point-min) (point-max))
|
||||||
|
;; 2. Convert leading whitespace to the canonical tab/space mix.
|
||||||
|
(akgl-retab-leading-whitespace)
|
||||||
|
;; 3. Trailing whitespace and a single final newline.
|
||||||
|
(delete-trailing-whitespace)
|
||||||
|
(goto-char (point-max))
|
||||||
|
(unless (bolp) (insert "\n"))
|
||||||
|
(let ((changed (not (string= before (buffer-string)))))
|
||||||
|
(when changed (save-buffer))
|
||||||
|
(kill-buffer)
|
||||||
|
changed))))
|
||||||
|
|
||||||
|
;; Emacs leaves the "--" separator in `command-line-args-left'; drop it, along
|
||||||
|
;; with any empty argument, so the remainder is exactly the file list.
|
||||||
|
(dolist (path (seq-remove (lambda (a) (or (string= a "--") (string= a "")))
|
||||||
|
command-line-args-left))
|
||||||
|
(when (akgl-reindent-file path)
|
||||||
|
(princ (format "reindented %s\n" path))))
|
||||||
|
|
||||||
|
;; Exit 0 on success whether or not anything changed, so that any non-zero
|
||||||
|
;; status from this script means a real failure. Callers detect "something
|
||||||
|
;; changed" from stdout, or by comparing files themselves.
|
||||||
|
(kill-emacs 0)
|
||||||
|
|
||||||
|
;;; reindent.el ends here
|
||||||
101
scripts/reindent.sh
Executable file
101
scripts/reindent.sh
Executable file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Reindent C sources to the project's canonical style (cc-mode "stroustrup",
|
||||||
|
# 4-column offset, 8-column tabs). See AGENTS.md -> "Coding Style".
|
||||||
|
#
|
||||||
|
# scripts/reindent.sh reindent every tracked C source in place
|
||||||
|
# scripts/reindent.sh FILE... reindent only the named files
|
||||||
|
# scripts/reindent.sh --check ... report non-conforming files, change nothing
|
||||||
|
#
|
||||||
|
# Exit status: 0 if everything already conforms (or was reindented), 1 if
|
||||||
|
# --check found a file that needs reindenting, 2 on a usage or environment
|
||||||
|
# error.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
root=$(git rev-parse --show-toplevel)
|
||||||
|
elisp="$root/scripts/reindent.el"
|
||||||
|
|
||||||
|
# Directories whose C sources are hand-maintained. Anything outside these is
|
||||||
|
# vendored (deps/) or generated (include/akgl/SDL_GameControllerDB.h) and is
|
||||||
|
# left alone.
|
||||||
|
SCOPE='src include tests util'
|
||||||
|
GENERATED='include/akgl/SDL_GameControllerDB.h'
|
||||||
|
|
||||||
|
check=0
|
||||||
|
if [ "${1:-}" = "--check" ]; then
|
||||||
|
check=1
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v emacs >/dev/null 2>&1; then
|
||||||
|
echo "reindent: emacs not found; cannot verify or apply the canonical style" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [ ! -f "$elisp" ]; then
|
||||||
|
echo "reindent: missing $elisp" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the file list: explicit arguments, or every tracked C source in scope.
|
||||||
|
if [ "$#" -gt 0 ]; then
|
||||||
|
files=$(for f in "$@"; do printf '%s\n' "$f"; done)
|
||||||
|
else
|
||||||
|
files=$(cd "$root" && git ls-files $SCOPE | grep -E '\.[ch]$' || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Drop generated files and anything that no longer exists on disk.
|
||||||
|
files=$(printf '%s\n' "$files" | grep -v -x -F "$GENERATED" || true)
|
||||||
|
files=$(cd "$root" && for f in $files; do [ -f "$f" ] && printf '%s\n' "$f"; done)
|
||||||
|
|
||||||
|
if [ -z "$files" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$check" -eq 0 ]; then
|
||||||
|
# Reindent in place. A non-zero status here is a real failure -- never
|
||||||
|
# swallow it, or a broken Emacs would look like "everything already conforms".
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
(cd "$root" && emacs --batch -Q -l "$elisp" -- $files) || {
|
||||||
|
echo "reindent: emacs failed; no files were reindented" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --check: reindent throwaway copies and report which originals differ. The
|
||||||
|
# copies keep their extension so cc-mode still selects the right major mode.
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||||
|
|
||||||
|
# Accept both repo-relative and absolute paths: the default file list is
|
||||||
|
# relative, but callers (including the pre-commit hook) pass absolute ones.
|
||||||
|
abspath() {
|
||||||
|
case "$1" in
|
||||||
|
/*) printf '%s' "$1" ;;
|
||||||
|
*) printf '%s/%s' "$root" "$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
copies=''
|
||||||
|
for f in $files; do
|
||||||
|
dest="$tmp/$(printf '%s' "$f" | tr '/' '_')"
|
||||||
|
cp "$(abspath "$f")" "$dest"
|
||||||
|
copies="$copies $dest"
|
||||||
|
done
|
||||||
|
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
emacs --batch -Q -l "$elisp" -- $copies >/dev/null || {
|
||||||
|
echo "reindent: emacs failed; cannot determine whether sources conform" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
status=0
|
||||||
|
for f in $files; do
|
||||||
|
dest="$tmp/$(printf '%s' "$f" | tr '/' '_')"
|
||||||
|
if ! cmp -s "$(abspath "$f")" "$dest"; then
|
||||||
|
echo "$f"
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
exit $status
|
||||||
109
src/actor.c
109
src/actor.c
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file actor.c
|
||||||
|
* @brief Implements the actor subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include <SDL3_image/SDL_image.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -51,6 +56,10 @@ akerr_ErrorContext *akgl_actor_set_character(akgl_Actor *obj, char *basecharname
|
|||||||
obj->basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, basecharname, NULL);
|
obj->basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, basecharname, NULL);
|
||||||
|
|
||||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Character not found in the registry");
|
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);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,20 +122,25 @@ akerr_ErrorContext *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *c
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// raises AKGL_ERR_LOGICINTERRUPT if we don't want the physics object to process us
|
// raises AKGL_ERR_LOGICINTERRUPT if we don't want the physics simulator to process us
|
||||||
akerr_ErrorContext *akgl_actor_logic_movement(akgl_Actor *obj, SDL_Time curtime)
|
akerr_ErrorContext *akgl_actor_logic_movement(akgl_Actor *actor, float32_t dt)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
||||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj->basechar");
|
FAIL_ZERO_RETURN(errctx, actor->basechar, AKERR_NULLPOINTER, "actor->basechar");
|
||||||
if ( obj->vx > obj->basechar->sx ) {
|
actor->sx = actor->basechar->sx;
|
||||||
obj->vx = obj->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 ( obj->vy > obj->basechar->sy ) {
|
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
|
||||||
obj->vy = obj->basechar->sy;
|
actor->ay = -actor->basechar->ay;
|
||||||
|
} else if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
|
||||||
|
actor->ay = actor->basechar->ay;
|
||||||
}
|
}
|
||||||
// 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);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +173,15 @@ akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Actor visible.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param camera Camera rectangle used for visibility testing.
|
||||||
|
* @param visible Output set to whether the actor intersects the camera.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, bool *visible)
|
static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, bool *visible)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -201,7 +224,7 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
|
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
|
||||||
CATCH(errctx, actor_visible(obj, &camera, &visible));
|
CATCH(errctx, actor_visible(obj, camera, &visible));
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_KEY) {
|
} HANDLE(errctx, AKERR_KEY) {
|
||||||
@@ -233,16 +256,16 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
|
|||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
|
|
||||||
if ( obj->parent != NULL ) {
|
if ( obj->parent != NULL ) {
|
||||||
dest.x = (obj->parent->x + obj->x - camera.x);
|
dest.x = (obj->parent->x + obj->x - camera->x);
|
||||||
dest.y = (obj->parent->y + obj->y - camera.y);
|
dest.y = (obj->parent->y + obj->y - camera->y);
|
||||||
} else {
|
} else {
|
||||||
dest.x = (obj->x - camera.x);
|
dest.x = (obj->x - camera->x);
|
||||||
dest.y = (obj->y - camera.y);
|
dest.y = (obj->y - camera->y);
|
||||||
}
|
}
|
||||||
dest.w = curSprite->width * obj->scale;
|
dest.w = curSprite->width * obj->scale;
|
||||||
dest.h = 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);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,13 +288,45 @@ akerr_ErrorContext *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child)
|
|||||||
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
|
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "actor");
|
||||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
|
||||||
|
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
|
||||||
//SDL_Log("event %d (button %d / key %d) moves actor left", event->type, event->gbutton.which, event->key.key);
|
//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));
|
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));
|
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT));
|
||||||
//SDL_Log("new target actor state: %b", obj->state);
|
//SDL_Log("new target actor state: %b", obj->state);
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
@@ -283,6 +338,9 @@ 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, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
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);
|
//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;
|
obj->vx = 0;
|
||||||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT);
|
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT);
|
||||||
//SDL_Log("new target actor state: %b", obj->state);
|
//SDL_Log("new target actor state: %b", obj->state);
|
||||||
@@ -294,7 +352,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
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);
|
//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_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));
|
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT));
|
||||||
//SDL_Log("new target actor state: %b", obj->state);
|
//SDL_Log("new target actor state: %b", obj->state);
|
||||||
@@ -307,6 +367,9 @@ 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, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
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);
|
//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;
|
obj->vx = 0;
|
||||||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
|
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
|
||||||
//SDL_Log("new target actor state: %b", obj->state);
|
//SDL_Log("new target actor state: %b", obj->state);
|
||||||
@@ -318,7 +381,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Ev
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
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);
|
//SDL_Log("event %d (button %d / key %d) moves actor up", 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_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));
|
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_UP | AKGL_ACTOR_STATE_MOVING_UP));
|
||||||
//SDL_Log("new target actor state: %b", obj->state);
|
//SDL_Log("new target actor state: %b", obj->state);
|
||||||
@@ -331,6 +396,9 @@ 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, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
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);
|
//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;
|
obj->vy = 0;
|
||||||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_UP);
|
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_UP);
|
||||||
//SDL_Log("new target actor state: %b", obj->state);
|
//SDL_Log("new target actor state: %b", obj->state);
|
||||||
@@ -342,7 +410,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
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);
|
//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_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));
|
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN));
|
||||||
//SDL_Log("new target actor state: %b", obj->state);
|
//SDL_Log("new target actor state: %b", obj->state);
|
||||||
@@ -355,6 +425,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL
|
|||||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
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);
|
//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;
|
obj->vy = 0;
|
||||||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_DOWN);
|
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_DOWN);
|
||||||
//SDL_Log("new target actor state: %b", obj->state);
|
//SDL_Log("new target actor state: %b", obj->state);
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file actor_state_string_names.c
|
||||||
|
* @brief Implements the actor state string names subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
char *AKGL_ACTOR_STATE_STRING_NAMES[32] = {
|
char *AKGL_ACTOR_STATE_STRING_NAMES[32] = {
|
||||||
"AKGL_ACTOR_STATE_FACE_DOWN",
|
"AKGL_ACTOR_STATE_FACE_DOWN",
|
||||||
"AKGL_ACTOR_STATE_FACE_LEFT",
|
"AKGL_ACTOR_STATE_FACE_LEFT",
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file assets.c
|
||||||
|
* @brief Implements the assets subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include <SDL3_image/SDL_image.h>
|
||||||
#include <SDL3_mixer/SDL_mixer.h>
|
#include <SDL3_mixer/SDL_mixer.h>
|
||||||
|
|||||||
431
src/audio.c
Normal file
431
src/audio.c
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
/**
|
||||||
|
* @file audio.c
|
||||||
|
* @brief Implements the audio subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akgl/audio.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
|
|
||||||
|
akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The device akgl_audio_init() opened, or NULL when the voice table is not
|
||||||
|
* connected to one. Everything that mutates a voice locks this stream when it
|
||||||
|
* is open, because the device callback reads the same table on SDL's audio
|
||||||
|
* thread. When it is NULL there is no other thread to race with.
|
||||||
|
*/
|
||||||
|
static SDL_AudioStream *audiostream = NULL;
|
||||||
|
|
||||||
|
/** @brief Level every voice is scaled by before the mix is clamped. */
|
||||||
|
static float32_t mastervolume = 1.0f;
|
||||||
|
|
||||||
|
/** @brief Scratch the device callback mixes into before handing it to SDL. */
|
||||||
|
static float32_t mixbuffer[AKGL_AUDIO_MIX_FRAMES];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* State for the noise waveform. A 32-bit xorshift rather than rand(): it needs
|
||||||
|
* no allocation, no locking beyond what already guards the voice table, and it
|
||||||
|
* produces the same sequence every run, so a test can assert on noise output.
|
||||||
|
*/
|
||||||
|
static uint32_t noisestate = 0x13579bdfu;
|
||||||
|
|
||||||
|
/** @brief Whether the voice table has been given its defaults yet. */
|
||||||
|
static bool voicesready = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Put every voice back to a flat, audible default.
|
||||||
|
*
|
||||||
|
* A zeroed voice has a sustain of 0.0, which is silence -- so a caller who
|
||||||
|
* sounded a note without first describing an envelope would get nothing and no
|
||||||
|
* error saying why. The default is instead the simplest thing that makes noise:
|
||||||
|
* a square wave with no attack, no decay and no release, held at full level for
|
||||||
|
* as long as the gate is open.
|
||||||
|
*/
|
||||||
|
static void reset_voices(void)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
|
||||||
|
akgl_audio_voices[i].active = false;
|
||||||
|
akgl_audio_voices[i].waveform = AKGL_AUDIO_WAVE_SQUARE;
|
||||||
|
akgl_audio_voices[i].hz = 0.0f;
|
||||||
|
akgl_audio_voices[i].phase = 0.0f;
|
||||||
|
akgl_audio_voices[i].duration_frames = 0;
|
||||||
|
akgl_audio_voices[i].elapsed_frames = 0;
|
||||||
|
akgl_audio_voices[i].attack_frames = 0;
|
||||||
|
akgl_audio_voices[i].decay_frames = 0;
|
||||||
|
akgl_audio_voices[i].release_frames = 0;
|
||||||
|
akgl_audio_voices[i].sustain = 1.0f;
|
||||||
|
}
|
||||||
|
mastervolume = 1.0f;
|
||||||
|
voicesready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Give the voice table its defaults the first time anything touches it.
|
||||||
|
*
|
||||||
|
* The table is process-wide static storage, so it is reachable before
|
||||||
|
* akgl_audio_init() has run. Rather than make every entry point document an
|
||||||
|
* ordering requirement, the first one to arrive sets the defaults.
|
||||||
|
*/
|
||||||
|
static void ensure_voices(void)
|
||||||
|
{
|
||||||
|
if ( voicesready == false ) {
|
||||||
|
reset_voices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Lock the voice table against the device callback, if one is running. */
|
||||||
|
static void lock_voices(void)
|
||||||
|
{
|
||||||
|
if ( audiostream != NULL ) {
|
||||||
|
SDL_LockAudioStream(audiostream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Release the lock taken by lock_voices(). */
|
||||||
|
static void unlock_voices(void)
|
||||||
|
{
|
||||||
|
if ( audiostream != NULL ) {
|
||||||
|
SDL_UnlockAudioStream(audiostream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Convert a duration in milliseconds to a whole number of frames. */
|
||||||
|
static uint32_t frames_for_ms(uint32_t ms)
|
||||||
|
{
|
||||||
|
return (uint32_t)(((uint64_t)ms * AKGL_AUDIO_SAMPLE_RATE) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Next value of the noise oscillator, in the range -1.0 to 1.0. */
|
||||||
|
static float32_t noise_sample(void)
|
||||||
|
{
|
||||||
|
noisestate ^= noisestate << 13;
|
||||||
|
noisestate ^= noisestate >> 17;
|
||||||
|
noisestate ^= noisestate << 5;
|
||||||
|
// The top 24 bits are the well-mixed ones; scale them to -1..1.
|
||||||
|
return (((float32_t)(noisestate >> 8) / 8388607.5f) - 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief One sample of @p voice's oscillator at its current phase. */
|
||||||
|
static float32_t voice_oscillator(akgl_AudioVoice *voice)
|
||||||
|
{
|
||||||
|
float32_t value = 0.0f;
|
||||||
|
|
||||||
|
switch ( voice->waveform ) {
|
||||||
|
case AKGL_AUDIO_WAVE_TRIANGLE:
|
||||||
|
if ( voice->phase < 0.5f ) {
|
||||||
|
value = (4.0f * voice->phase) - 1.0f;
|
||||||
|
} else {
|
||||||
|
value = 3.0f - (4.0f * voice->phase);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AKGL_AUDIO_WAVE_SAWTOOTH:
|
||||||
|
value = (2.0f * voice->phase) - 1.0f;
|
||||||
|
break;
|
||||||
|
case AKGL_AUDIO_WAVE_SQUARE:
|
||||||
|
value = ( voice->phase < 0.5f ) ? 1.0f : -1.0f;
|
||||||
|
break;
|
||||||
|
case AKGL_AUDIO_WAVE_NOISE:
|
||||||
|
value = noise_sample();
|
||||||
|
break;
|
||||||
|
case AKGL_AUDIO_WAVE_SINE:
|
||||||
|
value = SDL_sinf(voice->phase * 2.0f * SDL_PI_F);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Envelope level @p frame frames into the gate, before the release.
|
||||||
|
*
|
||||||
|
* Split out because the release has to start from wherever the gate left off,
|
||||||
|
* which for a gate shorter than attack plus decay is partway up or down a
|
||||||
|
* ramp rather than at the sustain level.
|
||||||
|
*/
|
||||||
|
static float32_t voice_gate_level(akgl_AudioVoice *voice, uint32_t frame)
|
||||||
|
{
|
||||||
|
uint32_t elapsed = frame;
|
||||||
|
|
||||||
|
if ( elapsed < voice->attack_frames ) {
|
||||||
|
return (float32_t)elapsed / (float32_t)voice->attack_frames;
|
||||||
|
}
|
||||||
|
elapsed -= voice->attack_frames;
|
||||||
|
if ( elapsed < voice->decay_frames ) {
|
||||||
|
return 1.0f - ((1.0f - voice->sustain) * ((float32_t)elapsed / (float32_t)voice->decay_frames));
|
||||||
|
}
|
||||||
|
return voice->sustain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Envelope level for @p voice where it currently stands. */
|
||||||
|
static float32_t voice_envelope(akgl_AudioVoice *voice)
|
||||||
|
{
|
||||||
|
uint32_t released = 0;
|
||||||
|
float32_t gatelevel = 0.0f;
|
||||||
|
|
||||||
|
if ( voice->elapsed_frames < voice->duration_frames ) {
|
||||||
|
return voice_gate_level(voice, voice->elapsed_frames);
|
||||||
|
}
|
||||||
|
released = voice->elapsed_frames - voice->duration_frames;
|
||||||
|
if ( released >= voice->release_frames ) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
gatelevel = voice_gate_level(voice, voice->duration_frames);
|
||||||
|
return gatelevel * (1.0f - ((float32_t)released / (float32_t)voice->release_frames));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Refuse a voice index that is not in the table. */
|
||||||
|
static akerr_ErrorContext *check_voice(int voice)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_NONZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
((voice < 0) || (voice >= AKGL_AUDIO_MAX_VOICES)),
|
||||||
|
AKERR_OUTOFBOUNDS,
|
||||||
|
"Voice %d is not in the range 0 to %d",
|
||||||
|
voice,
|
||||||
|
AKGL_AUDIO_MAX_VOICES - 1);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fill SDL's request from the voice table.
|
||||||
|
*
|
||||||
|
* SDL holds the stream lock for the duration of this callback, which is the
|
||||||
|
* same lock lock_voices() takes, so the voice table cannot change underneath a
|
||||||
|
* mix in progress.
|
||||||
|
*/
|
||||||
|
static void SDLCALL audio_stream_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount)
|
||||||
|
{
|
||||||
|
akerr_ErrorContext *errctx = NULL;
|
||||||
|
int frames = 0;
|
||||||
|
|
||||||
|
while ( additional_amount > 0 ) {
|
||||||
|
frames = additional_amount / (int)sizeof(float32_t);
|
||||||
|
if ( frames > AKGL_AUDIO_MIX_FRAMES ) {
|
||||||
|
frames = AKGL_AUDIO_MIX_FRAMES;
|
||||||
|
}
|
||||||
|
if ( frames <= 0 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
errctx = akgl_audio_mix(mixbuffer, frames);
|
||||||
|
if ( errctx != NULL ) {
|
||||||
|
// There is nobody to return an error to on the audio thread, and
|
||||||
|
// refusing to write leaves SDL underrunning. Report and go quiet.
|
||||||
|
LOG_ERROR_WITH_MESSAGE(errctx, "** AUDIO CALLBACK **");
|
||||||
|
errctx->handled = true;
|
||||||
|
errctx = akerr_release_error(errctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SDL_PutAudioStreamData(stream, mixbuffer, frames * (int)sizeof(float32_t));
|
||||||
|
additional_amount -= frames * (int)sizeof(float32_t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void)
|
||||||
|
{
|
||||||
|
SDL_AudioSpec spec;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
if ( audiostream != NULL ) {
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_voices();
|
||||||
|
|
||||||
|
spec.format = SDL_AUDIO_F32;
|
||||||
|
spec.channels = 1;
|
||||||
|
spec.freq = AKGL_AUDIO_SAMPLE_RATE;
|
||||||
|
|
||||||
|
audiostream = SDL_OpenAudioDeviceStream(
|
||||||
|
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||||
|
&spec,
|
||||||
|
&audio_stream_callback,
|
||||||
|
NULL);
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
audiostream,
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"Couldn't open an audio device: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Devices open paused so a caller can set a stream up before it is heard.
|
||||||
|
// Nothing here needs that, and a caller who expected akgl_audio_tone() to
|
||||||
|
// make a sound would otherwise get silence with no error to explain it.
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
SDL_ResumeAudioStreamDevice(audiostream),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"Couldn't start the audio device: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void)
|
||||||
|
{
|
||||||
|
SDL_AudioStream *closing = audiostream;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
// Clear the pointer before destroying the stream: lock_voices() checks it,
|
||||||
|
// and SDL_DestroyAudioStream can run the callback one last time.
|
||||||
|
audiostream = NULL;
|
||||||
|
if ( closing != NULL ) {
|
||||||
|
SDL_DestroyAudioStream(closing);
|
||||||
|
}
|
||||||
|
reset_voices();
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
PASS(errctx, check_voice(voice));
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (hz <= 0.0f), AKERR_OUTOFBOUNDS, "Frequency %f is not positive", hz);
|
||||||
|
FAIL_ZERO_RETURN(errctx, ms, AKERR_OUTOFBOUNDS, "A tone needs a duration; use akgl_audio_stop to silence a voice");
|
||||||
|
|
||||||
|
ensure_voices();
|
||||||
|
lock_voices();
|
||||||
|
akgl_audio_voices[voice].hz = hz;
|
||||||
|
akgl_audio_voices[voice].phase = 0.0f;
|
||||||
|
akgl_audio_voices[voice].duration_frames = frames_for_ms(ms);
|
||||||
|
akgl_audio_voices[voice].elapsed_frames = 0;
|
||||||
|
akgl_audio_voices[voice].active = true;
|
||||||
|
unlock_voices();
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
PASS(errctx, check_voice(voice));
|
||||||
|
|
||||||
|
ensure_voices();
|
||||||
|
lock_voices();
|
||||||
|
akgl_audio_voices[voice].active = false;
|
||||||
|
akgl_audio_voices[voice].elapsed_frames = 0;
|
||||||
|
akgl_audio_voices[voice].phase = 0.0f;
|
||||||
|
unlock_voices();
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
PASS(errctx, check_voice(voice));
|
||||||
|
FAIL_NONZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
((waveform < AKGL_AUDIO_WAVE_TRIANGLE) || (waveform > AKGL_AUDIO_WAVE_SINE)),
|
||||||
|
AKERR_OUTOFBOUNDS,
|
||||||
|
"Waveform %d is not one of the %d shapes",
|
||||||
|
(int)waveform,
|
||||||
|
(int)AKGL_AUDIO_WAVE_SINE + 1);
|
||||||
|
|
||||||
|
ensure_voices();
|
||||||
|
lock_voices();
|
||||||
|
akgl_audio_voices[voice].waveform = waveform;
|
||||||
|
unlock_voices();
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
PASS(errctx, check_voice(voice));
|
||||||
|
FAIL_NONZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
((sustain < 0.0f) || (sustain > 1.0f)),
|
||||||
|
AKERR_OUTOFBOUNDS,
|
||||||
|
"Sustain level %f is not between 0.0 and 1.0",
|
||||||
|
sustain);
|
||||||
|
|
||||||
|
ensure_voices();
|
||||||
|
lock_voices();
|
||||||
|
akgl_audio_voices[voice].attack_frames = frames_for_ms(attack);
|
||||||
|
akgl_audio_voices[voice].decay_frames = frames_for_ms(decay);
|
||||||
|
akgl_audio_voices[voice].release_frames = frames_for_ms(release);
|
||||||
|
akgl_audio_voices[voice].sustain = sustain;
|
||||||
|
unlock_voices();
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_NONZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
((level < 0.0f) || (level > 1.0f)),
|
||||||
|
AKERR_OUTOFBOUNDS,
|
||||||
|
"Volume level %f is not between 0.0 and 1.0",
|
||||||
|
level);
|
||||||
|
|
||||||
|
ensure_voices();
|
||||||
|
lock_voices();
|
||||||
|
mastervolume = level;
|
||||||
|
unlock_voices();
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
PASS(errctx, check_voice(voice));
|
||||||
|
FAIL_ZERO_RETURN(errctx, active, AKERR_NULLPOINTER, "NULL activity destination");
|
||||||
|
|
||||||
|
ensure_voices();
|
||||||
|
lock_voices();
|
||||||
|
*active = akgl_audio_voices[voice].active;
|
||||||
|
unlock_voices();
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames)
|
||||||
|
{
|
||||||
|
akgl_AudioVoice *voice = NULL;
|
||||||
|
float32_t sum = 0.0f;
|
||||||
|
int i = 0;
|
||||||
|
int v = 0;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL sample destination");
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (frames < 0), AKERR_OUTOFBOUNDS, "Frame count %d is negative", frames);
|
||||||
|
|
||||||
|
for ( i = 0; i < frames; i++ ) {
|
||||||
|
sum = 0.0f;
|
||||||
|
for ( v = 0; v < AKGL_AUDIO_MAX_VOICES; v++ ) {
|
||||||
|
voice = &akgl_audio_voices[v];
|
||||||
|
if ( voice->active == false ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ( voice->elapsed_frames >= (voice->duration_frames + voice->release_frames) ) {
|
||||||
|
// Gate and release are both spent. The voice goes quiet on its
|
||||||
|
// own so a caller does not have to keep a clock to stop it.
|
||||||
|
voice->active = false;
|
||||||
|
voice->elapsed_frames = 0;
|
||||||
|
voice->phase = 0.0f;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Derived from the frame counter rather than accumulated one
|
||||||
|
// increment at a time. A float increment of hz/rate is not exact,
|
||||||
|
// and adding it 44100 times a second walks the pitch off over the
|
||||||
|
// length of a held note.
|
||||||
|
voice->phase = (float32_t)SDL_fmod(
|
||||||
|
((double)voice->elapsed_frames * (double)voice->hz) / (double)AKGL_AUDIO_SAMPLE_RATE,
|
||||||
|
1.0);
|
||||||
|
sum += voice_oscillator(voice) * voice_envelope(voice);
|
||||||
|
voice->elapsed_frames += 1;
|
||||||
|
}
|
||||||
|
sum = sum * mastervolume;
|
||||||
|
// Three voices at full level can sum past full scale. Clamping rather
|
||||||
|
// than scaling by the voice count keeps a single voice at the level it
|
||||||
|
// was asked for instead of a third of it.
|
||||||
|
if ( sum > 1.0f ) {
|
||||||
|
sum = 1.0f;
|
||||||
|
} else if ( sum < -1.0f ) {
|
||||||
|
sum = -1.0f;
|
||||||
|
}
|
||||||
|
dest[i] = sum;
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file character.c
|
||||||
|
* @brief Implements the character subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include <SDL3_image/SDL_image.h>
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
@@ -85,6 +90,14 @@ void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID regis
|
|||||||
} FINISH_NORETURN(errctx);
|
} FINISH_NORETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Character load json state int from strings.
|
||||||
|
* @param states JSON array of actor-state names.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_t *states, int *dest)
|
static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_t *states, int *dest)
|
||||||
{
|
{
|
||||||
int i = 0;
|
int i = 0;
|
||||||
@@ -109,6 +122,13 @@ static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Character load json inner.
|
||||||
|
* @param json JSON object to parse.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Character *obj)
|
static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Character *obj)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|||||||
116
src/controller.c
116
src/controller.c
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file controller.c
|
||||||
|
* @brief Implements the controller subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
#include <akgl/heap.h>
|
#include <akgl/heap.h>
|
||||||
@@ -7,6 +12,39 @@
|
|||||||
|
|
||||||
akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
|
akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Keystrokes waiting for akgl_controller_poll_key(). Filled by
|
||||||
|
* akgl_controller_handle_event() before it consults the control maps, so a key
|
||||||
|
* that also drives an actor is still delivered to a polling caller.
|
||||||
|
*
|
||||||
|
* head is the next slot to read, count is how many are waiting. Both index a
|
||||||
|
* fixed array rather than a queue object, which is the whole point: a host that
|
||||||
|
* never polls cannot make this grow.
|
||||||
|
*/
|
||||||
|
static SDL_Keycode keybuffer[AKGL_CONTROLLER_KEY_BUFFER];
|
||||||
|
static int keybuffer_head = 0;
|
||||||
|
static int keybuffer_count = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Record one keystroke, discarding it if the buffer is already full.
|
||||||
|
*
|
||||||
|
* Dropping the newest rather than overwriting the oldest is deliberate. A
|
||||||
|
* caller reading a line of input wants the characters that were typed first;
|
||||||
|
* overwriting would hand it the tail of what the user typed and silently lose
|
||||||
|
* the head.
|
||||||
|
*/
|
||||||
|
static void keybuffer_push(SDL_Keycode key)
|
||||||
|
{
|
||||||
|
int tail = 0;
|
||||||
|
|
||||||
|
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tail = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
|
||||||
|
keybuffer[tail] = key;
|
||||||
|
keybuffer_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
|
||||||
{
|
{
|
||||||
int count;
|
int count;
|
||||||
@@ -60,6 +98,13 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
|
|||||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||||
|
|
||||||
|
// Before the control maps, not after: a key bound to an actor is still a
|
||||||
|
// key an interpreter polling with akgl_controller_poll_key() wants to see,
|
||||||
|
// and the scan below returns as soon as a binding claims the event.
|
||||||
|
if ( event->type == SDL_EVENT_KEY_DOWN ) {
|
||||||
|
keybuffer_push(event->key.key);
|
||||||
|
}
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
||||||
curmap = &GAME_ControlMaps[i];
|
curmap = &GAME_ControlMaps[i];
|
||||||
@@ -106,6 +151,13 @@ _akgl_controller_handle_event_success:
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gamepad handle button down.
|
||||||
|
* @param appstate Application state supplied by SDL.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
||||||
{
|
{
|
||||||
akgl_Actor *player = NULL;
|
akgl_Actor *player = NULL;
|
||||||
@@ -113,9 +165,9 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
|
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||||
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
|
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
|
||||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "Player actor does not exist");
|
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
|
||||||
|
|
||||||
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
|
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
|
||||||
event->key.key == SDLK_DOWN ) {
|
event->key.key == SDLK_DOWN ) {
|
||||||
@@ -157,6 +209,13 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gamepad handle button up.
|
||||||
|
* @param appstate Application state supplied by SDL.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
||||||
{
|
{
|
||||||
akgl_Actor *player = NULL;
|
akgl_Actor *player = NULL;
|
||||||
@@ -164,9 +223,9 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
|
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||||
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
|
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
|
||||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "Player actor does not exist");
|
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
|
||||||
|
|
||||||
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
|
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
|
||||||
event->key.key == SDLK_DOWN ) {
|
event->key.key == SDLK_DOWN ) {
|
||||||
@@ -196,6 +255,13 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gamepad handle added.
|
||||||
|
* @param appstate Application state supplied by SDL.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
||||||
{
|
{
|
||||||
SDL_JoystickID which;
|
SDL_JoystickID which;
|
||||||
@@ -204,7 +270,7 @@ akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
|||||||
|
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
|
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||||
|
|
||||||
which = event->gbutton.which;
|
which = event->gbutton.which;
|
||||||
gamepad = SDL_GetGamepadFromID(which);
|
gamepad = SDL_GetGamepadFromID(which);
|
||||||
@@ -226,6 +292,13 @@ akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gamepad handle removed.
|
||||||
|
* @param appstate Application state supplied by SDL.
|
||||||
|
* @param event SDL input event to process.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
|
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
|
||||||
{
|
{
|
||||||
SDL_JoystickID which;
|
SDL_JoystickID which;
|
||||||
@@ -233,7 +306,7 @@ akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
|
|||||||
|
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
|
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||||
|
|
||||||
which = event->gbutton.which;
|
which = event->gbutton.which;
|
||||||
gamepad = SDL_GetGamepadFromID(which);
|
gamepad = SDL_GetGamepadFromID(which);
|
||||||
@@ -277,7 +350,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
|
|||||||
controlmap->jsid = jsid;
|
controlmap->jsid = jsid;
|
||||||
|
|
||||||
controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL);
|
controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL);
|
||||||
FAIL_ZERO_BREAK(errctx, controlmap->target, AKERR_REGISTRY, "Actor %s not found in registry", actorname);
|
FAIL_ZERO_BREAK(errctx, controlmap->target, AKGL_ERR_REGISTRY, "Actor %s not found in registry", actorname);
|
||||||
|
|
||||||
// ---- KEYBOARD CONTROLS ----
|
// ---- KEYBOARD CONTROLS ----
|
||||||
|
|
||||||
@@ -353,3 +426,32 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
|
|||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, keycode, AKERR_NULLPOINTER, "NULL keycode destination");
|
||||||
|
FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");
|
||||||
|
|
||||||
|
if ( keybuffer_count == 0 ) {
|
||||||
|
// An empty buffer is the ordinary case, not a failure: the caller is
|
||||||
|
// asking whether a key is waiting, and the answer is no.
|
||||||
|
*keycode = 0;
|
||||||
|
*available = false;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
*keycode = (int)keybuffer[keybuffer_head];
|
||||||
|
*available = true;
|
||||||
|
keybuffer_head = (keybuffer_head + 1) % AKGL_CONTROLLER_KEY_BUFFER;
|
||||||
|
keybuffer_count -= 1;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
keybuffer_head = 0;
|
||||||
|
keybuffer_count = 0;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|||||||
525
src/draw.c
525
src/draw.c
@@ -1,8 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* @file draw.c
|
||||||
|
* @brief Implements the draw subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include <SDL3_image/SDL_image.h>
|
||||||
#include <SDL3_mixer/SDL_mixer.h>
|
#include <SDL3_mixer/SDL_mixer.h>
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akgl/draw.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
#include <akgl/game.h>
|
#include <akgl/game.h>
|
||||||
|
|
||||||
|
/** @brief One horizontal run of pixels the flood fill has still to examine. */
|
||||||
|
typedef struct {
|
||||||
|
int x1;
|
||||||
|
int x2;
|
||||||
|
int y;
|
||||||
|
} FloodSpan;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The flood fill's working stack. File scope and fixed size rather than a local
|
||||||
|
* array because AKGL_DRAW_MAX_FLOOD_SPANS spans is 48 KB, which does not belong
|
||||||
|
* on the stack of a function a game may call every frame. The consequence is
|
||||||
|
* that akgl_draw_flood_fill is not reentrant -- it is a single-threaded
|
||||||
|
* immediate-mode operation on a single render target, and so is everything else
|
||||||
|
* that touches an SDL_Renderer.
|
||||||
|
*/
|
||||||
|
static FloodSpan floodspans[AKGL_DRAW_MAX_FLOOD_SPANS];
|
||||||
|
|
||||||
/* Draw a Gimpish background pattern to show transparency in the image */
|
/* Draw a Gimpish background pattern to show transparency in the image */
|
||||||
void akgl_draw_background(int w, int h)
|
void akgl_draw_background(int w, int h)
|
||||||
{
|
{
|
||||||
@@ -20,11 +45,507 @@ void akgl_draw_background(int w, int h)
|
|||||||
for (x = 0; x < w; x += dx) {
|
for (x = 0; x < w; x += dx) {
|
||||||
/* use an 8x8 checkerboard pattern */
|
/* use an 8x8 checkerboard pattern */
|
||||||
i = (((x ^ y) >> 3) & 1);
|
i = (((x ^ y) >> 3) & 1);
|
||||||
SDL_SetRenderDrawColor(renderer.sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a);
|
SDL_SetRenderDrawColor(renderer->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a);
|
||||||
|
|
||||||
rect.x = (float)x;
|
rect.x = (float)x;
|
||||||
rect.y = (float)y;
|
rect.y = (float)y;
|
||||||
SDL_RenderFillRect(renderer.sdl_renderer, &rect);
|
SDL_RenderFillRect(renderer->sdl_renderer, &rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remember the renderer's draw color and replace it with @p color.
|
||||||
|
*
|
||||||
|
* @p previous is written before anything that can fail, so a caller may restore
|
||||||
|
* it unconditionally from a CLEANUP block.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *push_draw_color(akgl_RenderBackend *self, SDL_Color color, SDL_Color *previous)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
previous->r = 0x00;
|
||||||
|
previous->g = 0x00;
|
||||||
|
previous->b = 0x00;
|
||||||
|
previous->a = SDL_ALPHA_OPAQUE;
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
SDL_GetRenderDrawColor(self->sdl_renderer, &previous->r, &previous->g, &previous->b, &previous->a),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
SDL_SetRenderDrawColor(self->sdl_renderer, color.r, color.g, color.b, color.a),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Put back the draw color push_draw_color() recorded. */
|
||||||
|
static akerr_ErrorContext *pop_draw_color(akgl_RenderBackend *self, SDL_Color *previous)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
SDL_SetRenderDrawColor(self->sdl_renderer, previous->r, previous->g, previous->b, previous->a),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fill the four-connected region of @p oldpixel around a seed pixel.
|
||||||
|
*
|
||||||
|
* A scanline fill: each entry on the stack is a run of pixels on one row that
|
||||||
|
* still has to be examined. Finding a matching pixel expands it to the whole
|
||||||
|
* run it belongs to, fills that run, and pushes the rows above and below.
|
||||||
|
* Filled pixels no longer match @p oldpixel, which is what terminates it.
|
||||||
|
*
|
||||||
|
* @p surface must be SDL_PIXELFORMAT_RGBA32; the fill compares and writes whole
|
||||||
|
* 32-bit words rather than going through SDL_ReadSurfacePixel per pixel.
|
||||||
|
*
|
||||||
|
* @p dirty is set to the bounding box of everything written, so the caller can
|
||||||
|
* put back only the pixels that changed.
|
||||||
|
*
|
||||||
|
* Running out of stack leaves the region partially filled and reports
|
||||||
|
* AKERR_OUTOFBOUNDS. There is no way to unwind a partial fill short of keeping
|
||||||
|
* a copy of the whole surface, and the caller asked for a bounded operation.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *flood_region(SDL_Surface *surface, int x, int y, uint32_t oldpixel, uint32_t newpixel, SDL_Rect *dirty)
|
||||||
|
{
|
||||||
|
uint32_t *pixels = (uint32_t *)surface->pixels;
|
||||||
|
int pitch = surface->pitch / (int)sizeof(uint32_t);
|
||||||
|
int count = 0;
|
||||||
|
int col = 0;
|
||||||
|
int left = 0;
|
||||||
|
int right = 0;
|
||||||
|
int i = 0;
|
||||||
|
int minx = surface->w;
|
||||||
|
int miny = surface->h;
|
||||||
|
int maxx = -1;
|
||||||
|
int maxy = -1;
|
||||||
|
FloodSpan span;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
floodspans[0].x1 = x;
|
||||||
|
floodspans[0].x2 = x;
|
||||||
|
floodspans[0].y = y;
|
||||||
|
count = 1;
|
||||||
|
|
||||||
|
while ( count > 0 ) {
|
||||||
|
count -= 1;
|
||||||
|
span = floodspans[count];
|
||||||
|
col = span.x1;
|
||||||
|
while ( col <= span.x2 ) {
|
||||||
|
if ( pixels[(span.y * pitch) + col] != oldpixel ) {
|
||||||
|
col += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
left = col;
|
||||||
|
while ( left > 0 && pixels[(span.y * pitch) + (left - 1)] == oldpixel ) {
|
||||||
|
left -= 1;
|
||||||
|
}
|
||||||
|
right = col;
|
||||||
|
while ( right < (surface->w - 1) && pixels[(span.y * pitch) + (right + 1)] == oldpixel ) {
|
||||||
|
right += 1;
|
||||||
|
}
|
||||||
|
for ( i = left; i <= right; i++ ) {
|
||||||
|
pixels[(span.y * pitch) + i] = newpixel;
|
||||||
|
}
|
||||||
|
if ( left < minx ) {
|
||||||
|
minx = left;
|
||||||
|
}
|
||||||
|
if ( right > maxx ) {
|
||||||
|
maxx = right;
|
||||||
|
}
|
||||||
|
if ( span.y < miny ) {
|
||||||
|
miny = span.y;
|
||||||
|
}
|
||||||
|
if ( span.y > maxy ) {
|
||||||
|
maxy = span.y;
|
||||||
|
}
|
||||||
|
// Two pushes per run, so the check is for room for both.
|
||||||
|
FAIL_NONZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
((count + 2) > AKGL_DRAW_MAX_FLOOD_SPANS),
|
||||||
|
AKERR_OUTOFBOUNDS,
|
||||||
|
"Region needs more than %d pending spans; it is partially filled",
|
||||||
|
AKGL_DRAW_MAX_FLOOD_SPANS);
|
||||||
|
if ( span.y > 0 ) {
|
||||||
|
floodspans[count].x1 = left;
|
||||||
|
floodspans[count].x2 = right;
|
||||||
|
floodspans[count].y = span.y - 1;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
if ( span.y < (surface->h - 1) ) {
|
||||||
|
floodspans[count].x1 = left;
|
||||||
|
floodspans[count].x2 = right;
|
||||||
|
floodspans[count].y = span.y + 1;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
col = right + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dirty->x = minx;
|
||||||
|
dirty->y = miny;
|
||||||
|
dirty->w = (maxx - minx) + 1;
|
||||||
|
dirty->h = (maxy - miny) + 1;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color)
|
||||||
|
{
|
||||||
|
SDL_Color previous;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||||
|
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_RenderPoint(self->sdl_renderer, x, y),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(pop_draw_color(self, &previous));
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color)
|
||||||
|
{
|
||||||
|
SDL_Color previous;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||||
|
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_RenderLine(self->sdl_renderer, x1, y1, x2, y2),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(pop_draw_color(self, &previous));
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
|
||||||
|
{
|
||||||
|
SDL_Color previous;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||||
|
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||||
|
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_RenderRect(self->sdl_renderer, rect),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(pop_draw_color(self, &previous));
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
|
||||||
|
{
|
||||||
|
SDL_Color previous;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||||
|
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||||
|
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_RenderFillRect(self->sdl_renderer, rect),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(pop_draw_color(self, &previous));
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color)
|
||||||
|
{
|
||||||
|
SDL_Color previous;
|
||||||
|
SDL_FPoint octants[8];
|
||||||
|
int centerx = 0;
|
||||||
|
int centery = 0;
|
||||||
|
int r = 0;
|
||||||
|
int offsetx = 0;
|
||||||
|
int offsety = 0;
|
||||||
|
int decision = 0;
|
||||||
|
bool plotted = true;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||||
|
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (radius < 0), AKERR_OUTOFBOUNDS, "Negative radius %f", radius);
|
||||||
|
|
||||||
|
centerx = (int)SDL_lroundf(x);
|
||||||
|
centery = (int)SDL_lroundf(y);
|
||||||
|
r = (int)SDL_lroundf(radius);
|
||||||
|
offsety = r;
|
||||||
|
// The midpoint decision variable, started so the first step chooses between
|
||||||
|
// (0, r) and (1, r-1) correctly.
|
||||||
|
decision = 1 - r;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||||
|
while ( offsety >= offsetx ) {
|
||||||
|
// Eight-way symmetry: one computed point in the second octant gives
|
||||||
|
// the seven others by reflection.
|
||||||
|
octants[0].x = (float)(centerx + offsetx);
|
||||||
|
octants[0].y = (float)(centery + offsety);
|
||||||
|
octants[1].x = (float)(centerx - offsetx);
|
||||||
|
octants[1].y = (float)(centery + offsety);
|
||||||
|
octants[2].x = (float)(centerx + offsetx);
|
||||||
|
octants[2].y = (float)(centery - offsety);
|
||||||
|
octants[3].x = (float)(centerx - offsetx);
|
||||||
|
octants[3].y = (float)(centery - offsety);
|
||||||
|
octants[4].x = (float)(centerx + offsety);
|
||||||
|
octants[4].y = (float)(centery + offsetx);
|
||||||
|
octants[5].x = (float)(centerx - offsety);
|
||||||
|
octants[5].y = (float)(centery + offsetx);
|
||||||
|
octants[6].x = (float)(centerx + offsety);
|
||||||
|
octants[6].y = (float)(centery - offsetx);
|
||||||
|
octants[7].x = (float)(centerx - offsety);
|
||||||
|
octants[7].y = (float)(centery - offsetx);
|
||||||
|
// A CATCH here would break this loop rather than leave the function,
|
||||||
|
// so failure is recorded and reported once the loop is done.
|
||||||
|
if ( !SDL_RenderPoints(self->sdl_renderer, octants, 8) ) {
|
||||||
|
plotted = false;
|
||||||
|
}
|
||||||
|
offsetx += 1;
|
||||||
|
if ( decision < 0 ) {
|
||||||
|
decision += (2 * offsetx) + 1;
|
||||||
|
} else {
|
||||||
|
offsety -= 1;
|
||||||
|
decision += 2 * (offsetx - offsety) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FAIL_ZERO_BREAK(errctx, plotted, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(pop_draw_color(self, &previous));
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color)
|
||||||
|
{
|
||||||
|
SDL_Surface *target = NULL;
|
||||||
|
SDL_Surface *rgba = NULL;
|
||||||
|
SDL_Texture *patch = NULL;
|
||||||
|
// Only written by a successful flood_region(), and only read after one, but
|
||||||
|
// the paths in between are far enough apart that the compiler cannot see it.
|
||||||
|
SDL_Rect dirty = { 0, 0, 0, 0 };
|
||||||
|
SDL_FRect src;
|
||||||
|
SDL_FRect dest;
|
||||||
|
uint32_t *pixels = NULL;
|
||||||
|
uint32_t oldpixel = 0;
|
||||||
|
uint32_t newpixel = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||||
|
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||||
|
ATTEMPT {
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
FAIL_NONZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
((x < 0) || (y < 0) || (x >= width) || (y >= height)),
|
||||||
|
AKERR_OUTOFBOUNDS,
|
||||||
|
"Seed pixel %d,%d is outside the %dx%d render target",
|
||||||
|
x, y, width, height);
|
||||||
|
|
||||||
|
target = SDL_RenderReadPixels(self->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, target, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
// The fill works on 32-bit words, so the layout has to be known rather
|
||||||
|
// than whatever the render target happens to use.
|
||||||
|
rgba = SDL_ConvertSurface(target, SDL_PIXELFORMAT_RGBA32);
|
||||||
|
FAIL_ZERO_BREAK(errctx, rgba, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
|
||||||
|
pixels = (uint32_t *)rgba->pixels;
|
||||||
|
oldpixel = pixels[(y * (rgba->pitch / (int)sizeof(uint32_t))) + x];
|
||||||
|
newpixel = SDL_MapSurfaceRGBA(rgba, color.r, color.g, color.b, color.a);
|
||||||
|
if ( oldpixel == newpixel ) {
|
||||||
|
// Already the requested color. Walking it would compare filled
|
||||||
|
// pixels against themselves and find nothing, so say so up front.
|
||||||
|
SUCCEED_BREAK(errctx);
|
||||||
|
}
|
||||||
|
CATCH(errctx, flood_region(rgba, x, y, oldpixel, newpixel, &dirty));
|
||||||
|
|
||||||
|
patch = SDL_CreateTextureFromSurface(self->sdl_renderer, rgba);
|
||||||
|
FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
// Replace rather than blend: this is a framebuffer operation, and the
|
||||||
|
// pixels being put back are the ones that were just read out of it.
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Only the bounding box of what changed goes back to the target.
|
||||||
|
src.x = (float)dirty.x;
|
||||||
|
src.y = (float)dirty.y;
|
||||||
|
src.w = (float)dirty.w;
|
||||||
|
src.h = (float)dirty.h;
|
||||||
|
dest = src;
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_RenderTexture(self->sdl_renderer, patch, &src, &dest),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
} CLEANUP {
|
||||||
|
if ( patch != NULL ) {
|
||||||
|
SDL_DestroyTexture(patch);
|
||||||
|
}
|
||||||
|
if ( rgba != NULL ) {
|
||||||
|
SDL_DestroySurface(rgba);
|
||||||
|
}
|
||||||
|
if ( target != NULL ) {
|
||||||
|
SDL_DestroySurface(target);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest)
|
||||||
|
{
|
||||||
|
SDL_Surface *saved = NULL;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||||
|
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||||
|
FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
|
||||||
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||||||
|
ATTEMPT {
|
||||||
|
FAIL_NONZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
((src->w <= 0) || (src->h <= 0)),
|
||||||
|
AKERR_OUTOFBOUNDS,
|
||||||
|
"Region %dx%d has no area",
|
||||||
|
src->w, src->h);
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
// SDL clips a read to the target and hands back a smaller surface than
|
||||||
|
// was asked for, which a caller pasting it back would not notice.
|
||||||
|
FAIL_NONZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
((src->x < 0) || (src->y < 0) ||
|
||||||
|
((src->x + src->w) > width) || ((src->y + src->h) > height)),
|
||||||
|
AKERR_OUTOFBOUNDS,
|
||||||
|
"Region %d,%d %dx%d does not fit inside the %dx%d render target",
|
||||||
|
src->x, src->y, src->w, src->h, width, height);
|
||||||
|
|
||||||
|
saved = SDL_RenderReadPixels(self->sdl_renderer, src);
|
||||||
|
FAIL_ZERO_BREAK(errctx, saved, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
|
||||||
|
if ( *dest == NULL ) {
|
||||||
|
*dest = saved;
|
||||||
|
// Ownership has moved to the caller; CLEANUP must not free it.
|
||||||
|
saved = NULL;
|
||||||
|
} else {
|
||||||
|
FAIL_NONZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
(((*dest)->w != src->w) || ((*dest)->h != src->h)),
|
||||||
|
AKERR_OUTOFBOUNDS,
|
||||||
|
"Destination surface is %dx%d, region is %dx%d",
|
||||||
|
(*dest)->w, (*dest)->h, src->w, src->h);
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_SetSurfaceBlendMode(saved, SDL_BLENDMODE_NONE),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_BlitSurface(saved, NULL, *dest, NULL),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
} CLEANUP {
|
||||||
|
if ( saved != NULL ) {
|
||||||
|
SDL_DestroySurface(saved);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y)
|
||||||
|
{
|
||||||
|
SDL_Texture *patch = NULL;
|
||||||
|
SDL_FRect dest;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||||
|
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||||
|
FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
|
||||||
|
ATTEMPT {
|
||||||
|
patch = SDL_CreateTextureFromSurface(self->sdl_renderer, src);
|
||||||
|
FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
// Replace what is on the target, the way GSHAPE does by default.
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
dest.x = x;
|
||||||
|
dest.y = y;
|
||||||
|
dest.w = (float)src->w;
|
||||||
|
dest.h = (float)src->h;
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_RenderTexture(self->sdl_renderer, patch, NULL, &dest),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
} CLEANUP {
|
||||||
|
if ( patch != NULL ) {
|
||||||
|
SDL_DestroyTexture(patch);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|||||||
24
src/error.c
Normal file
24
src/error.c
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* @file error.c
|
||||||
|
* @brief Implements the error subsystem: claims and names the libakgl status band.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akgl/error.h>
|
||||||
|
|
||||||
|
akerr_ErrorContext *akgl_error_init(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
// Claim the whole band before naming anything in it: libakerror refuses a
|
||||||
|
// name for a status we do not own. Any collision propagates to the caller
|
||||||
|
// -- another component owning part of our range is an initialization
|
||||||
|
// failure, not a warning.
|
||||||
|
PASS(errctx, akerr_reserve_status_range(AKGL_ERR_BASE, AKGL_ERR_COUNT, AKGL_ERR_OWNER));
|
||||||
|
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_SDL, "SDL Error"));
|
||||||
|
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_REGISTRY, "Registry Error"));
|
||||||
|
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_HEAP, "Heap Error"));
|
||||||
|
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_BEHAVIOR, "Behavior Error"));
|
||||||
|
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt"));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
170
src/game.c
170
src/game.c
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file game.c
|
||||||
|
* @brief Implements the game subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include <SDL3_image/SDL_image.h>
|
||||||
#include <SDL3_mixer/SDL_mixer.h>
|
#include <SDL3_mixer/SDL_mixer.h>
|
||||||
@@ -16,20 +21,26 @@
|
|||||||
#include <akgl/staticstring.h>
|
#include <akgl/staticstring.h>
|
||||||
#include <akgl/iterator.h>
|
#include <akgl/iterator.h>
|
||||||
#include <akgl/physics.h>
|
#include <akgl/physics.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
#include <akgl/SDL_GameControllerDB.h>
|
#include <akgl/SDL_GameControllerDB.h>
|
||||||
|
|
||||||
SDL_Window *window = NULL;
|
SDL_Window *window = NULL;
|
||||||
akgl_RenderBackend renderer;
|
|
||||||
akgl_PhysicsBackend physics;
|
// Currently active objects
|
||||||
akgl_Frame ball;
|
akgl_RenderBackend *renderer;
|
||||||
akgl_Frame paddle1;
|
akgl_PhysicsBackend *physics;
|
||||||
akgl_Frame paddle2;
|
SDL_FRect *camera;
|
||||||
akgl_Frame table;
|
akgl_Tilemap *gamemap;
|
||||||
akgl_Tilemap gamemap;
|
|
||||||
|
// Default objects
|
||||||
|
akgl_RenderBackend _akgl_renderer;
|
||||||
|
akgl_PhysicsBackend _akgl_physics;
|
||||||
|
SDL_FRect _akgl_camera;
|
||||||
|
akgl_Tilemap _akgl_gamemap;
|
||||||
|
|
||||||
MIX_Audio *bgm = NULL;
|
MIX_Audio *bgm = NULL;
|
||||||
MIX_Mixer *akgl_mixer = NULL;
|
MIX_Mixer *akgl_mixer = NULL;
|
||||||
MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
|
MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
|
||||||
SDL_FRect camera;
|
|
||||||
akgl_Game game;
|
akgl_Game game;
|
||||||
|
|
||||||
void akgl_game_lowfps(void)
|
void akgl_game_lowfps(void)
|
||||||
@@ -45,7 +56,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
|
|||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
ATTEMPT {
|
// First, before anything that can raise: everything below reports through
|
||||||
|
// AKGL_ERR_* codes, and a code raised before its name is registered prints
|
||||||
|
// as "Unknown Error" in the stack trace the caller is left holding.
|
||||||
|
PASS(e, akgl_error_init());
|
||||||
strncpy((char *)&game.libversion, AKGL_VERSION, 32);
|
strncpy((char *)&game.libversion, AKGL_VERSION, 32);
|
||||||
game.gameStartTime = SDL_GetTicksNS();
|
game.gameStartTime = SDL_GetTicksNS();
|
||||||
game.lastIterTime = game.gameStartTime;
|
game.lastIterTime = game.gameStartTime;
|
||||||
@@ -53,23 +67,19 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
|
|||||||
game.lowfpsfunc = &akgl_game_lowfps;
|
game.lowfpsfunc = &akgl_game_lowfps;
|
||||||
game.statelock = SDL_CreateMutex();
|
game.statelock = SDL_CreateMutex();
|
||||||
FAIL_ZERO_RETURN(e, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
|
FAIL_ZERO_RETURN(e, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
CATCH(e, akgl_game_state_lock());
|
PASS(e, akgl_game_state_lock());
|
||||||
FAIL_ZERO_BREAK(e, strlen((char *)&game.name), AKERR_NULLPOINTER, "Must provide game name");
|
FAIL_ZERO_RETURN(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_RETURN(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");
|
FAIL_ZERO_RETURN(e, strlen((char *)&game.uri), AKERR_NULLPOINTER, "Must provide game uri");
|
||||||
CATCH(e, akgl_heap_init());
|
PASS(e, akgl_heap_init());
|
||||||
CATCH(e, akgl_registry_init_actor());
|
PASS(e, akgl_registry_init_actor());
|
||||||
CATCH(e, akgl_registry_init_sprite());
|
PASS(e, akgl_registry_init_sprite());
|
||||||
CATCH(e, akgl_registry_init_spritesheet());
|
PASS(e, akgl_registry_init_spritesheet());
|
||||||
CATCH(e, akgl_registry_init_character());
|
PASS(e, akgl_registry_init_character());
|
||||||
CATCH(e, akgl_registry_init_font());
|
PASS(e, akgl_registry_init_font());
|
||||||
CATCH(e, akgl_registry_init_music());
|
PASS(e, akgl_registry_init_music());
|
||||||
CATCH(e, akgl_registry_init_properties());
|
PASS(e, akgl_registry_init_properties());
|
||||||
CATCH(e, akgl_registry_init_actor_state_strings());
|
PASS(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);
|
SDL_SetAppMetadata(game.name, game.version, game.uri);
|
||||||
|
|
||||||
@@ -113,6 +123,12 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
|
|||||||
AKGL_ERR_SDL,
|
AKGL_ERR_SDL,
|
||||||
"Couldn't initialize front engine: %s",
|
"Couldn't initialize front engine: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
|
|
||||||
|
camera = &_akgl_camera;
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
|
physics = &_akgl_physics;
|
||||||
|
gamemap = &_akgl_gamemap;
|
||||||
|
|
||||||
PASS(e, akgl_game_state_unlock());
|
PASS(e, akgl_game_state_unlock());
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
@@ -159,6 +175,12 @@ void akgl_game_updateFPS()
|
|||||||
* entity name -> pointer map tables
|
* entity name -> pointer map tables
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Serializes an actor name-to-pointer entry.
|
||||||
|
* @param userdata Open output stream supplied by the caller.
|
||||||
|
* @param props Actor registry being iterated.
|
||||||
|
* @param name Actor registry key to serialize.
|
||||||
|
*/
|
||||||
void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||||
{
|
{
|
||||||
FILE *fp = (FILE *)userdata;
|
FILE *fp = (FILE *)userdata;
|
||||||
@@ -174,6 +196,12 @@ void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, c
|
|||||||
} FINISH_NORETURN(e);
|
} FINISH_NORETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Game save spritename iterator.
|
||||||
|
* @param userdata Caller data supplied to the SDL property iterator.
|
||||||
|
* @param props SDL property collection being iterated.
|
||||||
|
* @param name Registry key or human-readable object name.
|
||||||
|
*/
|
||||||
void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||||
{
|
{
|
||||||
FILE *fp = (FILE *)userdata;
|
FILE *fp = (FILE *)userdata;
|
||||||
@@ -189,6 +217,12 @@ void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props,
|
|||||||
} FINISH_NORETURN(e);
|
} FINISH_NORETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Game save spritesheetname iterator.
|
||||||
|
* @param userdata Caller data supplied to the SDL property iterator.
|
||||||
|
* @param props SDL property collection being iterated.
|
||||||
|
* @param name Registry key or human-readable object name.
|
||||||
|
*/
|
||||||
void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||||
{
|
{
|
||||||
FILE *fp = (FILE *)userdata;
|
FILE *fp = (FILE *)userdata;
|
||||||
@@ -204,6 +238,12 @@ void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID pr
|
|||||||
} FINISH_NORETURN(e);
|
} FINISH_NORETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Game save charactername iterator.
|
||||||
|
* @param userdata Caller data supplied to the SDL property iterator.
|
||||||
|
* @param props SDL property collection being iterated.
|
||||||
|
* @param name Registry key or human-readable object name.
|
||||||
|
*/
|
||||||
void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||||
{
|
{
|
||||||
FILE *fp = (FILE *)userdata;
|
FILE *fp = (FILE *)userdata;
|
||||||
@@ -219,10 +259,25 @@ void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID prop
|
|||||||
} FINISH_NORETURN(e);
|
} FINISH_NORETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Game save actors.
|
||||||
|
* @param fp Open save-game stream.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
char nullval = 0x00;
|
// 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));
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||||
@@ -231,29 +286,29 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
|
|||||||
AKGL_REGISTRY_ACTOR,
|
AKGL_REGISTRY_ACTOR,
|
||||||
&akgl_game_save_actorname_iterator,
|
&akgl_game_save_actorname_iterator,
|
||||||
(void *)fp);
|
(void *)fp);
|
||||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
|
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
|
||||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_Actor *), fp));
|
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_Actor *), fp));
|
||||||
// write the sprite name pointer table
|
// write the sprite name pointer table
|
||||||
SDL_EnumerateProperties(
|
SDL_EnumerateProperties(
|
||||||
AKGL_REGISTRY_SPRITE,
|
AKGL_REGISTRY_SPRITE,
|
||||||
&akgl_game_save_spritename_iterator,
|
&akgl_game_save_spritename_iterator,
|
||||||
(void *)fp);
|
(void *)fp);
|
||||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
|
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
|
||||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_Sprite *), fp));
|
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_Sprite *), fp));
|
||||||
// write the spritesheet name pointer table
|
// write the spritesheet name pointer table
|
||||||
SDL_EnumerateProperties(
|
SDL_EnumerateProperties(
|
||||||
AKGL_REGISTRY_SPRITESHEET,
|
AKGL_REGISTRY_SPRITESHEET,
|
||||||
&akgl_game_save_spritesheetname_iterator,
|
&akgl_game_save_spritesheetname_iterator,
|
||||||
(void *)fp);
|
(void *)fp);
|
||||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
|
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
|
||||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_SpriteSheet *), fp));
|
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_SpriteSheet *), fp));
|
||||||
// write the character name pointer table
|
// write the character name pointer table
|
||||||
SDL_EnumerateProperties(
|
SDL_EnumerateProperties(
|
||||||
AKGL_REGISTRY_CHARACTER,
|
AKGL_REGISTRY_CHARACTER,
|
||||||
&akgl_game_save_charactername_iterator,
|
&akgl_game_save_charactername_iterator,
|
||||||
(void *)fp);
|
(void *)fp);
|
||||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
|
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
|
||||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_Character *), fp));
|
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_Character *), fp));
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(e) {
|
} PROCESS(e) {
|
||||||
} FINISH(e, true);
|
} FINISH(e, true);
|
||||||
@@ -270,27 +325,49 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
|
|||||||
CATCH(e, aksl_fopen(fpath, "wb", &fp));
|
CATCH(e, aksl_fopen(fpath, "wb", &fp));
|
||||||
CATCH(e, aksl_fwrite(&game, 1, sizeof(akgl_Game), fp));
|
CATCH(e, aksl_fwrite(&game, 1, sizeof(akgl_Game), fp));
|
||||||
CATCH(e, akgl_game_save_actors(fp));
|
CATCH(e, akgl_game_save_actors(fp));
|
||||||
} PROCESS(e) {
|
|
||||||
} CLEANUP {
|
} 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 )
|
if ( fp != NULL )
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
|
} PROCESS(e) {
|
||||||
} FINISH(e, true);
|
} FINISH(e, true);
|
||||||
SUCCEED_RETURN(e); // SUCCEED_NORETURN if in main().
|
SUCCEED_RETURN(e); // SUCCEED_NORETURN if in main().
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Game load objectnamemap.
|
||||||
|
* @param fp Open save-game stream.
|
||||||
|
* @param map Tilemap to inspect, draw, or modify.
|
||||||
|
* @param namelength Fixed serialized name-field length.
|
||||||
|
* @param ptrlength Serialized pointer-field length.
|
||||||
|
* @param registry SDL property registry being queried.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry)
|
||||||
{
|
{
|
||||||
void *ptr = NULL;
|
void *ptr = NULL;
|
||||||
char ptrstring[32];
|
char ptrstring[32];
|
||||||
char objname[namelength];
|
char objname[namelength];
|
||||||
int retval = 0;
|
int retval = 0;
|
||||||
|
bool done = false;
|
||||||
|
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
while ( 1 ) {
|
// 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, aksl_fread((void *)&objname, 1, namelength, fp));
|
CATCH(e, aksl_fread((void *)&objname, 1, namelength, fp));
|
||||||
CATCH(e, aksl_fread((void *)&ptr, 1, ptrlength, fp));
|
CATCH(e, aksl_fread((void *)&ptr, 1, ptrlength, fp));
|
||||||
// End of the map
|
// End of the map
|
||||||
if ( ptr == 0x00 && objname[0] == 0x00 ) {
|
if ( ptr == 0x00 && objname[0] == 0x00 ) {
|
||||||
|
done = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// The map allows us to say "Object X has a reference to object Y at
|
// The map allows us to say "Object X has a reference to object Y at
|
||||||
@@ -307,10 +384,23 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_Pr
|
|||||||
map,
|
map,
|
||||||
ptrstring,
|
ptrstring,
|
||||||
SDL_GetPointerProperty(registry, objname, NULL));
|
SDL_GetPointerProperty(registry, objname, NULL));
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
} FINISH(e, true);
|
||||||
};
|
};
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Game load versioncmp.
|
||||||
|
* @param versiontype Description of the version being compared.
|
||||||
|
* @param newversion Version read from the save data.
|
||||||
|
* @param curversion Version supported by the running library.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_VALUE When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion)
|
||||||
{
|
{
|
||||||
semver_t current_version = {};
|
semver_t current_version = {};
|
||||||
@@ -432,15 +522,15 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE) ) {
|
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 {
|
} else {
|
||||||
actor->scale = 1.0;
|
actor->scale = 1.0;
|
||||||
}
|
}
|
||||||
PASS(e, actor->updatefunc(actor));
|
PASS(e, actor->updatefunc(actor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PASS(e, physics.simulate(&physics, NULL));
|
PASS(e, physics->simulate(physics, NULL));
|
||||||
PASS(e, renderer.draw_world(&renderer, NULL));
|
PASS(e, renderer->draw_world(renderer, NULL));
|
||||||
PASS(e, akgl_game_state_unlock());
|
PASS(e, akgl_game_state_unlock());
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/heap.c
17
src/heap.c
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file heap.c
|
||||||
|
* @brief Implements the heap subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
|
|
||||||
@@ -19,7 +24,6 @@ akerr_ErrorContext *akgl_heap_init()
|
|||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
int i = 0;
|
int i = 0;
|
||||||
akerr_name_for_status(AKGL_ERR_SDL, "SDL Error");
|
|
||||||
PASS(errctx, akgl_heap_init_actor());
|
PASS(errctx, akgl_heap_init_actor());
|
||||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) {
|
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) {
|
||||||
memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite));
|
memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite));
|
||||||
@@ -55,7 +59,7 @@ akerr_ErrorContext *akgl_heap_next_actor(akgl_Actor **dest)
|
|||||||
*dest = &HEAP_ACTOR[i];
|
*dest = &HEAP_ACTOR[i];
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused actor on the heap");
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused actor on the heap");
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akgl_heap_next_sprite(akgl_Sprite **dest)
|
akerr_ErrorContext *akgl_heap_next_sprite(akgl_Sprite **dest)
|
||||||
@@ -68,7 +72,7 @@ akerr_ErrorContext *akgl_heap_next_sprite(akgl_Sprite **dest)
|
|||||||
*dest = &HEAP_SPRITE[i];
|
*dest = &HEAP_SPRITE[i];
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused sprite on the heap");
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused sprite on the heap");
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest)
|
akerr_ErrorContext *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest)
|
||||||
@@ -81,7 +85,7 @@ akerr_ErrorContext *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest)
|
|||||||
*dest = &HEAP_SPRITESHEET[i];
|
*dest = &HEAP_SPRITESHEET[i];
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused spritesheet on the heap");
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused spritesheet on the heap");
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akgl_heap_next_character(akgl_Character **dest)
|
akerr_ErrorContext *akgl_heap_next_character(akgl_Character **dest)
|
||||||
@@ -94,7 +98,7 @@ akerr_ErrorContext *akgl_heap_next_character(akgl_Character **dest)
|
|||||||
*dest = &HEAP_CHARACTER[i];
|
*dest = &HEAP_CHARACTER[i];
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused character on the heap");
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused character on the heap");
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akgl_heap_next_string(akgl_String **dest)
|
akerr_ErrorContext *akgl_heap_next_string(akgl_String **dest)
|
||||||
@@ -108,7 +112,7 @@ akerr_ErrorContext *akgl_heap_next_string(akgl_String **dest)
|
|||||||
HEAP_STRING[i].refcount += 1;
|
HEAP_STRING[i].refcount += 1;
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused string on the heap");
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused string on the heap");
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akgl_heap_release_actor(akgl_Actor *ptr)
|
akerr_ErrorContext *akgl_heap_release_actor(akgl_Actor *ptr)
|
||||||
@@ -192,4 +196,3 @@ akerr_ErrorContext *akgl_heap_release_string(akgl_String *ptr)
|
|||||||
}
|
}
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file json_helpers.c
|
||||||
|
* @brief Implements the json helpers subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <jansson.h>
|
#include <jansson.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
@@ -52,6 +57,17 @@ akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float *de
|
|||||||
SUCCEED_RETURN(errctx);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest)
|
akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest)
|
||||||
{
|
{
|
||||||
json_t *value = NULL;
|
json_t *value = NULL;
|
||||||
@@ -129,3 +145,26 @@ akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, a
|
|||||||
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
|
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *err, void *defval, void *dest, uint32_t defsize)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
if ( err == NULL ) {
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|||||||
178
src/physics.c
178
src/physics.c
@@ -1,3 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* @file physics.c
|
||||||
|
* @brief Implements the physics subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
#include <akstdlib.h>
|
#include <akstdlib.h>
|
||||||
#include <akgl/physics.h>
|
#include <akgl/physics.h>
|
||||||
#include <akgl/actor.h>
|
#include <akgl/actor.h>
|
||||||
@@ -6,7 +12,7 @@
|
|||||||
#include <akgl/heap.h>
|
#include <akgl/heap.h>
|
||||||
#include <akgl/registry.h>
|
#include <akgl/registry.h>
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, SDL_Time curtime)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||||
@@ -20,7 +26,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend
|
|||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, SDL_Time curtime)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||||
@@ -41,29 +47,29 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *s
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, SDL_Time curtime)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||||
// Do nothing
|
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor");
|
||||||
// Calculate the drop of all relevant actors which is a function of their mass and the
|
|
||||||
// world's gravity
|
|
||||||
|
|
||||||
//Gravity is applied in units per second. So we apply
|
if ( self->gravity_x != 0 ) {
|
||||||
// (unit / milliseconds per second) * (milliseconds since last update)
|
// Assume the X origin is - (screen left)
|
||||||
actor->vx += ((self->gravity_x / AKGL_TIME_ONESEC_NS) * (curtime - self->gravity_time));
|
actor->ex -= (self->gravity_x * dt);
|
||||||
actor->vy += ((self->gravity_y / AKGL_TIME_ONESEC_NS) * (curtime - self->gravity_time));
|
}
|
||||||
actor->vz += ((self->gravity_z / AKGL_TIME_ONESEC_NS) * (curtime - self->gravity_time));
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply atmospheric drag
|
|
||||||
actor->vx -= actor->vx * self->drag_x * (curtime - self->gravity_time);
|
|
||||||
actor->vy -= actor->vy * self->drag_x * (curtime - self->gravity_time);
|
|
||||||
actor->vz -= actor->vz * self->drag_x * (curtime - self->gravity_time);
|
|
||||||
// Need a euler function
|
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||||
@@ -71,55 +77,27 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_collide(akgl_PhysicsBackend *
|
|||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_ss_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, SDL_Time curtime)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||||
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor");
|
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor");
|
||||||
|
actor->x += actor->vx * dt;
|
||||||
if ( actor->parent != NULL ) {
|
actor->y += actor->vy * dt;
|
||||||
// Children don't move independently of their parents, they just have an offset
|
actor->z += actor->vz * dt;
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
} else if ( actor->basechar == NULL ) {
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
} else {
|
|
||||||
if ( (curtime - actor->movetimer) >= actor->basechar->speedtime ) {
|
|
||||||
actor->movetimer = curtime;
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, actor->movementlogicfunc(actor,curtime));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKGL_ERR_LOGICINTERRUPT) {
|
|
||||||
// The actor told us NOT to process them, they handled their own update
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
} FINISH(e, true);
|
|
||||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ) {
|
|
||||||
actor->x += -actor->vx;
|
|
||||||
}
|
|
||||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
|
|
||||||
actor->x += actor->vx;
|
|
||||||
}
|
|
||||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
|
|
||||||
actor->y += -actor->vy;
|
|
||||||
}
|
|
||||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
|
|
||||||
actor->y += actor->vy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_sidescroller(akgl_PhysicsBackend *self)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self)
|
||||||
{
|
{
|
||||||
akgl_String *tmp;
|
akgl_String *tmp;
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||||
PASS(e, akgl_heap_next_string(&tmp));
|
PASS(e, akgl_heap_next_string(&tmp));
|
||||||
|
|
||||||
self->gravity = akgl_physics_ss_gravity;
|
self->gravity = akgl_physics_arcade_gravity;
|
||||||
self->collide = akgl_physics_ss_collide;
|
self->collide = akgl_physics_arcade_collide;
|
||||||
self->move = akgl_physics_ss_move;
|
self->move = akgl_physics_arcade_move;
|
||||||
self->simulate = akgl_physics_simulate;
|
self->simulate = akgl_physics_simulate;
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
@@ -150,37 +128,119 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
|
|||||||
.flags = 0,
|
.flags = 0,
|
||||||
.layerid = 0
|
.layerid = 0
|
||||||
};
|
};
|
||||||
SDL_Time curtime = SDL_GetTicksNS();
|
SDL_Time curtime = 0;
|
||||||
|
float32_t dt = 0;
|
||||||
akgl_Actor *actor = NULL;
|
akgl_Actor *actor = NULL;
|
||||||
|
|
||||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||||
FAIL_ZERO_RETURN(e, self->move, AKERR_NULLPOINTER, "self->move");
|
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 ) {
|
if ( opflags == NULL ) {
|
||||||
opflags = &defflags;
|
opflags = &defflags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||||
actor = &HEAP_ACTOR[i];
|
actor = &HEAP_ACTOR[i];
|
||||||
if ( actor->refcount == 0 ) {
|
if ( actor->refcount == 0 ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) ) {
|
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) ) {
|
||||||
if ( actor->layer != opflags->layerid ) {
|
if ( actor->layer != opflags->layerid ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// thrust is a function of acceleration on a given axis
|
||||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ||
|
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ||
|
||||||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
|
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
|
||||||
actor->vx += actor->basechar->ax;
|
actor->tx += actor->ax * dt;
|
||||||
}
|
}
|
||||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ||
|
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ||
|
||||||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
|
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
|
||||||
actor->vy += actor->basechar->ay;
|
actor->ty += actor->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 ( 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
} HANDLE(e, AKGL_ERR_LOGICINTERRUPT) {
|
||||||
|
// noop
|
||||||
|
} FINISH(e, true);
|
||||||
}
|
}
|
||||||
PASS(e, self->gravity(self, actor, curtime));
|
|
||||||
PASS(e, self->move(self, actor, curtime));
|
|
||||||
self->gravity_time = curtime;
|
self->gravity_time = curtime;
|
||||||
}
|
|
||||||
SUCCEED_RETURN(e);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file registry.c
|
||||||
|
* @brief Implements the registry subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
#include <jansson.h>
|
#include <jansson.h>
|
||||||
@@ -184,4 +189,3 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **d
|
|||||||
} FINISH(e, true);
|
} FINISH(e, true);
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file renderer.c
|
||||||
|
* @brief Implements the renderer subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <akgl/renderer.h>
|
#include <akgl/renderer.h>
|
||||||
@@ -33,10 +38,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self)
|
|||||||
"Couldn't create window/renderer: %s",
|
"Couldn't create window/renderer: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
|
|
||||||
camera.x = 0;
|
camera->x = 0;
|
||||||
camera.y = 0;
|
camera->y = 0;
|
||||||
camera.w = screenwidth;
|
camera->w = screenwidth;
|
||||||
camera.h = screenheight;
|
camera->h = screenheight;
|
||||||
|
|
||||||
self->shutdown = &akgl_render_2d_shutdown;
|
self->shutdown = &akgl_render_2d_shutdown;
|
||||||
self->frame_start = &akgl_render_2d_frame_start;
|
self->frame_start = &akgl_render_2d_frame_start;
|
||||||
@@ -117,8 +122,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend
|
|||||||
PASS(e, aksl_memset((void *)opflags, 0x00, sizeof(akgl_Iterator)));
|
PASS(e, aksl_memset((void *)opflags, 0x00, sizeof(akgl_Iterator)));
|
||||||
}
|
}
|
||||||
for ( int i = 0; i < AKGL_TILEMAP_MAX_LAYERS ; i++ ) {
|
for ( int i = 0; i < AKGL_TILEMAP_MAX_LAYERS ; i++ ) {
|
||||||
if ( i < gamemap.numlayers ) {
|
if ( i < gamemap->numlayers ) {
|
||||||
PASS(e, akgl_tilemap_draw((akgl_Tilemap *)&gamemap, &camera, i));
|
PASS(e, akgl_tilemap_draw(gamemap, camera, i));
|
||||||
}
|
}
|
||||||
for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR ; j++ ) {
|
for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR ; j++ ) {
|
||||||
actor = &HEAP_ACTOR[j];
|
actor = &HEAP_ACTOR[j];
|
||||||
|
|||||||
30
src/sprite.c
30
src/sprite.c
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file sprite.c
|
||||||
|
* @brief Implements the sprite subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include <SDL3_image/SDL_image.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -35,6 +40,14 @@ akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FR
|
|||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sprite load json spritesheet.
|
||||||
|
* @param json JSON object to parse.
|
||||||
|
* @param sheet Spritesheet used by the sprite.
|
||||||
|
* @param relative_path Base directory for relative asset references.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
|
static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -58,7 +71,7 @@ static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_
|
|||||||
if ( *sheet == NULL ) {
|
if ( *sheet == NULL ) {
|
||||||
CATCH(errctx, akgl_heap_next_spritesheet(sheet));
|
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_width", &ss_frame_width));
|
||||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_width));
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_height));
|
||||||
CATCH(errctx,
|
CATCH(errctx,
|
||||||
akgl_spritesheet_initialize(
|
akgl_spritesheet_initialize(
|
||||||
(akgl_SpriteSheet *)*sheet,
|
(akgl_SpriteSheet *)*sheet,
|
||||||
@@ -85,12 +98,20 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
|||||||
akgl_Sprite *obj = NULL;
|
akgl_Sprite *obj = NULL;
|
||||||
akgl_SpriteSheet *sheet = NULL;
|
akgl_SpriteSheet *sheet = NULL;
|
||||||
akgl_String *spritename = NULL;
|
akgl_String *spritename = NULL;
|
||||||
//string *tmpstr = NULL;
|
akgl_String *filename_copy = NULL;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
|
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_heap_next_sprite(&obj));
|
CATCH(errctx, akgl_heap_next_sprite(&obj));
|
||||||
|
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_heap_next_string(&tmpstr));
|
||||||
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
||||||
CATCH(errctx, akgl_heap_next_string(&spritename));
|
CATCH(errctx, akgl_heap_next_string(&spritename));
|
||||||
@@ -105,7 +126,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
|||||||
"Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
|
"Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
|
||||||
);
|
);
|
||||||
|
|
||||||
CATCH(errctx, akgl_sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename)));
|
CATCH(errctx, akgl_sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data)));
|
||||||
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
|
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
|
||||||
CATCH(errctx,
|
CATCH(errctx,
|
||||||
akgl_sprite_initialize(
|
akgl_sprite_initialize(
|
||||||
@@ -132,6 +153,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
|||||||
IGNORE(akgl_heap_release_spritesheet(sheet));
|
IGNORE(akgl_heap_release_spritesheet(sheet));
|
||||||
}
|
}
|
||||||
IGNORE(akgl_heap_release_string(spritename));
|
IGNORE(akgl_heap_release_string(spritename));
|
||||||
|
IGNORE(akgl_heap_release_string(filename_copy));
|
||||||
//IGNORE(akgl_heap_release_string(tmpstr));
|
//IGNORE(akgl_heap_release_string(tmpstr));
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
@@ -174,7 +196,7 @@ akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int spr
|
|||||||
strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH);
|
strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH);
|
||||||
|
|
||||||
//snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
|
//snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
|
||||||
sheet->texture = IMG_LoadTexture(renderer.sdl_renderer, filename);
|
sheet->texture = IMG_LoadTexture(renderer->sdl_renderer, filename);
|
||||||
FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());
|
FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());
|
||||||
|
|
||||||
FAIL_ZERO_BREAK(
|
FAIL_ZERO_BREAK(
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file staticstring.c
|
||||||
|
* @brief Implements the staticstring subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
#include <akgl/staticstring.h>
|
#include <akgl/staticstring.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|||||||
54
src/text.c
54
src/text.c
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file text.c
|
||||||
|
* @brief Implements the text subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <SDL3_ttf/SDL_ttf.h>
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
@@ -11,7 +16,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath
|
|||||||
|
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
|
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
|
||||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null filepath");
|
FAIL_ZERO_RETURN(errctx, filepath, AKERR_NULLPOINTER, "Null filepath");
|
||||||
font = TTF_OpenFont(filepath, size);
|
font = TTF_OpenFont(filepath, size);
|
||||||
FAIL_ZERO_RETURN(errctx, font, AKGL_ERR_SDL, "%s", SDL_GetError());
|
FAIL_ZERO_RETURN(errctx, font, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
FAIL_ZERO_RETURN(
|
FAIL_ZERO_RETURN(
|
||||||
@@ -49,13 +54,56 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *
|
|||||||
color);
|
color);
|
||||||
}
|
}
|
||||||
FAIL_ZERO_RETURN(errctx, textsurf, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
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());
|
FAIL_ZERO_RETURN(errctx, texture, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||||
dest.x = x;
|
dest.x = x;
|
||||||
dest.y = y;
|
dest.y = y;
|
||||||
SDL_GetTextureSize(texture, &dest.w, &dest.h);
|
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_DestroyTexture(texture);
|
||||||
SDL_DestroySurface(textsurf);
|
SDL_DestroySurface(textsurf);
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
|
||||||
|
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
|
||||||
|
FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
|
||||||
|
FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
|
||||||
|
// A zero length means "the string is null terminated", not "the empty
|
||||||
|
// string" -- an empty text measures 0 wide and one line high.
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
TTF_GetStringSize(font, text, 0, w, h),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
|
||||||
|
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
|
||||||
|
FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
|
||||||
|
FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
|
||||||
|
// SDL_ttf takes the wrap width as an int and reads a negative one as a
|
||||||
|
// very large unsigned width, which silently disables wrapping instead of
|
||||||
|
// reporting anything. Refuse it here rather than return a wrong measurement.
|
||||||
|
FAIL_NONZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
(wraplength < 0),
|
||||||
|
AKERR_OUTOFBOUNDS,
|
||||||
|
"Wrap length %d is negative",
|
||||||
|
wraplength);
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
TTF_GetStringSizeWrapped(font, text, 0, wraplength, w, h),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|||||||
226
src/tilemap.c
226
src/tilemap.c
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file tilemap.c
|
||||||
|
* @brief Implements the tilemap subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <libgen.h>
|
#include <libgen.h>
|
||||||
|
|
||||||
@@ -64,14 +69,10 @@ akerr_ErrorContext *akgl_get_json_properties_string(json_t *obj, char *key, akgl
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
json_t *property;
|
json_t *property;
|
||||||
|
|
||||||
ATTEMPT {
|
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "string", &property));
|
||||||
CATCH(errctx, akgl_get_json_tilemap_property(obj, key, "string", &property));
|
PASS(errctx, akgl_heap_next_string(dest));
|
||||||
CATCH(errctx, akgl_heap_next_string(dest));
|
PASS(errctx, akgl_string_initialize(*dest, NULL));
|
||||||
CATCH(errctx, akgl_string_initialize(*dest, NULL));
|
PASS(errctx, akgl_get_json_string_value(property, "value", dest));
|
||||||
CATCH(errctx, akgl_get_json_string_value(property, "value", dest));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
|
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
@@ -80,40 +81,62 @@ akerr_ErrorContext *akgl_get_json_properties_integer(json_t *obj, char *key, int
|
|||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
json_t *property = NULL;
|
json_t *property = NULL;
|
||||||
ATTEMPT {
|
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "int", &property));
|
||||||
CATCH(errctx, akgl_get_json_tilemap_property(obj, key, "int", &property));
|
PASS(errctx, akgl_get_json_integer_value(property, "value", dest));
|
||||||
CATCH(errctx, akgl_get_json_integer_value(property, "value", dest));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
|
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get json properties number.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, float *dest)
|
akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, float *dest)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
json_t *property = NULL;
|
json_t *property = NULL;
|
||||||
ATTEMPT {
|
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "number", &property));
|
||||||
CATCH(errctx, akgl_get_json_tilemap_property(obj, key, "number", &property));
|
PASS(errctx, akgl_get_json_number_value(property, "value", dest));
|
||||||
CATCH(errctx, akgl_get_json_number_value(property, "value", dest));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
|
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get json properties float.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float *dest)
|
akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float *dest)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
json_t *property = NULL;
|
json_t *property = NULL;
|
||||||
ATTEMPT {
|
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
|
||||||
CATCH(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
|
PASS(errctx, akgl_get_json_number_value(property, "value", dest));
|
||||||
CATCH(errctx, akgl_get_json_number_value(property, "value", dest));
|
|
||||||
} CLEANUP {
|
SUCCEED_RETURN(errctx);
|
||||||
} PROCESS(errctx) {
|
}
|
||||||
} FINISH(errctx, true);
|
|
||||||
|
/**
|
||||||
|
* @brief Get json properties double.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param key JSON object key or property name to locate.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||||
|
*/
|
||||||
|
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));
|
||||||
|
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
@@ -135,7 +158,6 @@ 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_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_get_json_string_value((json_t *)tileset, "name", &tmpstr));
|
||||||
PASS(e, akgl_heap_next_string(&tmpstr));
|
|
||||||
PASS(e, akgl_heap_next_string(&tmppath));
|
PASS(e, akgl_heap_next_string(&tmppath));
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
strncpy((char *)&dest->tilesets[tsidx].name,
|
strncpy((char *)&dest->tilesets[tsidx].name,
|
||||||
@@ -152,7 +174,7 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
|
|||||||
} PROCESS(e) {
|
} PROCESS(e) {
|
||||||
} FINISH(e, true);
|
} FINISH(e, true);
|
||||||
|
|
||||||
dest->tilesets[tsidx].texture = IMG_LoadTexture(renderer.sdl_renderer, (char *)&dest->tilesets[tsidx].imagefilename);
|
dest->tilesets[tsidx].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)&dest->tilesets[tsidx].imagefilename);
|
||||||
FAIL_ZERO_RETURN(e, dest->tilesets[tsidx].texture, AKERR_NULLPOINTER, "Failed loading tileset image : %s", SDL_GetError());
|
FAIL_ZERO_RETURN(e, dest->tilesets[tsidx].texture, AKERR_NULLPOINTER, "Failed loading tileset image : %s", SDL_GetError());
|
||||||
|
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
@@ -239,6 +261,15 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Tilemap load layer object actor.
|
||||||
|
* @param curobj Tilemap object being populated.
|
||||||
|
* @param layerdatavalue JSON object describing the layer entry.
|
||||||
|
* @param layerid Zero-based tilemap layer index.
|
||||||
|
* @param dirname Directory containing paths referenced by the document.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname)
|
akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -297,41 +328,36 @@ 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, dest, AKERR_NULLPOINTER, "NULL destination tilemap reference");
|
||||||
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");
|
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");
|
||||||
|
|
||||||
ATTEMPT {
|
PASS(errctx, akgl_get_json_array_value(root, "objects", &layerdata));
|
||||||
CATCH(errctx, akgl_get_json_array_value(root, "objects", &layerdata));
|
|
||||||
len = json_array_size((json_t *)layerdata);
|
len = json_array_size((json_t *)layerdata);
|
||||||
curlayer = &dest->layers[layerid];
|
curlayer = &dest->layers[layerid];
|
||||||
for ( j = 0; j < len; j++ ) {
|
for ( j = 0; j < len; j++ ) {
|
||||||
CATCH(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
|
PASS(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
|
||||||
curobj = &curlayer->objects[j];
|
curobj = &curlayer->objects[j];
|
||||||
CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
|
PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
|
||||||
strncpy((char *)curobj->name, tmpstr->data, AKGL_ACTOR_MAX_NAME_LENGTH);
|
strncpy((char *)curobj->name, tmpstr->data, AKGL_ACTOR_MAX_NAME_LENGTH);
|
||||||
CATCH(errctx, akgl_heap_release_string(tmpstr));
|
PASS(errctx, akgl_heap_release_string(tmpstr));
|
||||||
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
|
PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
|
||||||
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
|
PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
|
||||||
CATCH(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
|
PASS(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
|
||||||
|
|
||||||
CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
|
PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
|
||||||
if ( strcmp(tmpstr->data, "actor") == 0 ) {
|
if ( strcmp(tmpstr->data, "actor") == 0 ) {
|
||||||
CATCH(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
|
PASS(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
|
||||||
} else if ( strcmp(tmpstr->data, "perspective") == 0 ) {
|
} else if ( strcmp(tmpstr->data, "perspective") == 0 ) {
|
||||||
curobj->visible = false;
|
curobj->visible = false;
|
||||||
if ( strcmp((char *)curobj->name, "p_foreground") == 0 ) {
|
if ( strcmp((char *)curobj->name, "p_foreground") == 0 ) {
|
||||||
dest->p_foreground_y = curobj->y;
|
dest->p_foreground_y = curobj->y;
|
||||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_foreground_h));
|
PASS(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));
|
PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_foreground_scale));
|
||||||
} else if ( strcmp((char *)curobj->name, "p_vanishing") == 0 ) {
|
} else if ( strcmp((char *)curobj->name, "p_vanishing") == 0 ) {
|
||||||
dest->p_vanishing_y = curobj->y;
|
dest->p_vanishing_y = curobj->y;
|
||||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
|
PASS(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
|
||||||
CATCH(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
|
PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
layerdatavalue = NULL;
|
layerdatavalue = NULL;
|
||||||
}
|
}
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
|
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
@@ -365,6 +391,16 @@ akerr_ErrorContext *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *roo
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Tilemap load layer image.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param root Base directory used to resolve the path.
|
||||||
|
* @param layerid Zero-based tilemap layer index.
|
||||||
|
* @param dirname Directory containing paths referenced by the document.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
|
akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -389,7 +425,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *ro
|
|||||||
);
|
);
|
||||||
RESTORE_GCC_WARNINGS
|
RESTORE_GCC_WARNINGS
|
||||||
|
|
||||||
dest->layers[layerid].texture = IMG_LoadTexture(renderer.sdl_renderer, (char *)fpath->data);
|
dest->layers[layerid].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)fpath->data);
|
||||||
FAIL_ZERO_BREAK(errctx, dest->layers[layerid].texture, AKGL_ERR_SDL, "%s", SDL_GetError());
|
FAIL_ZERO_BREAK(errctx, dest->layers[layerid].texture, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
dest->layers[layerid].width = dest->layers[layerid].texture->w;
|
dest->layers[layerid].width = dest->layers[layerid].texture->w;
|
||||||
dest->layers[layerid].height = dest->layers[layerid].texture->h;
|
dest->layers[layerid].height = dest->layers[layerid].texture->h;
|
||||||
@@ -451,6 +487,95 @@ akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, a
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Tilemap load physics.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @param root Base directory used to resolve the path.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
|
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)
|
akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -484,6 +609,7 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
|
|||||||
error.line,
|
error.line,
|
||||||
error.text
|
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, "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, "tilewidth", &dest->tilewidth));
|
||||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &dest->height));
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &dest->height));
|
||||||
@@ -573,7 +699,7 @@ akerr_ErrorContext *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, in
|
|||||||
src.h = map->layers[layeridx].height;
|
src.h = map->layers[layeridx].height;
|
||||||
dest.w = map->layers[layeridx].width;
|
dest.w = map->layers[layeridx].width;
|
||||||
dest.h = map->layers[layeridx].height;
|
dest.h = map->layers[layeridx].height;
|
||||||
PASS(errctx, renderer.draw_texture(&renderer, map->layers[layeridx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
PASS(errctx, renderer->draw_texture(renderer, map->layers[layeridx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,7 +754,7 @@ akerr_ErrorContext *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, in
|
|||||||
dest.y,
|
dest.y,
|
||||||
dest.w,
|
dest.w,
|
||||||
dest.h);*/
|
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;
|
dest.x += map->tilewidth;
|
||||||
@@ -683,7 +809,7 @@ akerr_ErrorContext *akgl_tilemap_draw_tileset(akgl_Tilemap *map, int tilesetidx)
|
|||||||
dest.y,
|
dest.y,
|
||||||
dest.w,
|
dest.w,
|
||||||
dest.h);*/
|
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);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/util.c
34
src/util.c
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file util.c
|
||||||
|
* @brief Implements the util subsystem.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
@@ -15,6 +20,15 @@
|
|||||||
|
|
||||||
#include <akstdlib.h>
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Path relative root.
|
||||||
|
* @param root Base directory used to resolve the path.
|
||||||
|
* @param path Path to resolve or transform.
|
||||||
|
* @param dst Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String *dst)
|
akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String *dst)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
@@ -80,6 +94,14 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
|
|||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Path relative from.
|
||||||
|
* @param path Path to resolve or transform.
|
||||||
|
* @param from Base path from which the result is made relative.
|
||||||
|
* @param dst Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
|
*/
|
||||||
akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String **dst)
|
akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String **dst)
|
||||||
{
|
{
|
||||||
akgl_String *dirnamestr;
|
akgl_String *dirnamestr;
|
||||||
@@ -202,9 +224,9 @@ akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, in
|
|||||||
FAIL_ZERO_BREAK(errctx, t2, AKERR_NULLPOINTER, "NULL texture");
|
FAIL_ZERO_BREAK(errctx, t2, AKERR_NULLPOINTER, "NULL texture");
|
||||||
|
|
||||||
CATCH(errctx, akgl_heap_next_string(&tmpstring));
|
CATCH(errctx, akgl_heap_next_string(&tmpstring));
|
||||||
SDL_RenderClear(renderer.sdl_renderer);
|
SDL_RenderClear(renderer->sdl_renderer);
|
||||||
CATCH(errctx, renderer.draw_texture(&renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||||
s1 = SDL_RenderReadPixels(renderer.sdl_renderer, &read);
|
s1 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
|
||||||
FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer");
|
FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer");
|
||||||
|
|
||||||
if ( writeout != NULL ) {
|
if ( writeout != NULL ) {
|
||||||
@@ -218,10 +240,10 @@ akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, in
|
|||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_RenderClear(renderer.sdl_renderer);
|
SDL_RenderClear(renderer->sdl_renderer);
|
||||||
|
|
||||||
CATCH(errctx, renderer.draw_texture(&renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||||
s2 = SDL_RenderReadPixels(renderer.sdl_renderer, &read);
|
s2 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
|
||||||
FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer");
|
FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer");
|
||||||
|
|
||||||
CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2));
|
CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2));
|
||||||
|
|||||||
15
src/version.c
Normal file
15
src/version.c
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* @file version.c
|
||||||
|
* @brief Implements the runtime half of the version API.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <akgl/version.h>
|
||||||
|
|
||||||
|
const char *akgl_version(void)
|
||||||
|
{
|
||||||
|
// AKGL_VERSION is baked in when this translation unit is compiled, so what
|
||||||
|
// comes back is the version of the shared library the caller linked -- not
|
||||||
|
// the version of the header the caller built against. That asymmetry is the
|
||||||
|
// whole point: comparing the two detects a stale libakgl on the loader path.
|
||||||
|
return AKGL_VERSION;
|
||||||
|
}
|
||||||
610
tests/actor.c
610
tests/actor.c
@@ -2,6 +2,7 @@
|
|||||||
handle_unhandled_error(errctx);
|
handle_unhandled_error(errctx);
|
||||||
|
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
|
|
||||||
#define UNHANDLED_ERROR_EXIT 0
|
#define UNHANDLED_ERROR_EXIT 0
|
||||||
#define UNHANDLED_ERROR_SET 1
|
#define UNHANDLED_ERROR_SET 1
|
||||||
@@ -12,8 +13,13 @@
|
|||||||
#include <akgl/iterator.h>
|
#include <akgl/iterator.h>
|
||||||
#include <akgl/registry.h>
|
#include <akgl/registry.h>
|
||||||
#include <akgl/actor.h>
|
#include <akgl/actor.h>
|
||||||
|
#include <akgl/character.h>
|
||||||
|
#include <akgl/sprite.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
#include <akgl/heap.h>
|
#include <akgl/heap.h>
|
||||||
|
|
||||||
|
#include "testutil.h"
|
||||||
|
|
||||||
int UNHANDLED_ERROR_BEHAVIOR;
|
int UNHANDLED_ERROR_BEHAVIOR;
|
||||||
akerr_ErrorContext *unhandled_error_context;
|
akerr_ErrorContext *unhandled_error_context;
|
||||||
|
|
||||||
@@ -42,7 +48,7 @@ akerr_ErrorContext *akgl_actor_update_noop(akgl_Actor *obj)
|
|||||||
|
|
||||||
// Currently the renderer assumes there is a global variable named `renderer`
|
// Currently the renderer assumes there is a global variable named `renderer`
|
||||||
int akgl_actor_rendered;
|
int akgl_actor_rendered;
|
||||||
akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj, SDL_Renderer *r)
|
akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
akgl_actor_rendered = 1;
|
akgl_actor_rendered = 1;
|
||||||
@@ -129,13 +135,13 @@ akerr_ErrorContext *test_registry_actor_iterator_updaterender(void)
|
|||||||
FAIL_ZERO_BREAK(
|
FAIL_ZERO_BREAK(
|
||||||
unhandled_error_context,
|
unhandled_error_context,
|
||||||
akgl_actor_updated,
|
akgl_actor_updated,
|
||||||
AKERR_BEHAVIOR,
|
AKGL_ERR_BEHAVIOR,
|
||||||
"actor->updatefunc not called by the iterator"
|
"actor->updatefunc not called by the iterator"
|
||||||
);
|
);
|
||||||
FAIL_ZERO_BREAK(
|
FAIL_ZERO_BREAK(
|
||||||
unhandled_error_context,
|
unhandled_error_context,
|
||||||
akgl_actor_rendered,
|
akgl_actor_rendered,
|
||||||
AKERR_BEHAVIOR,
|
AKGL_ERR_BEHAVIOR,
|
||||||
"actor->renderfunc not called by the iterator"
|
"actor->renderfunc not called by the iterator"
|
||||||
);
|
);
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
@@ -235,7 +241,7 @@ akerr_ErrorContext *test_actor_manage_children(void)
|
|||||||
CATCH(errctx, parent->addchild(parent, child));
|
CATCH(errctx, parent->addchild(parent, child));
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
if ( errctx == NULL ) {
|
if ( errctx == NULL ) {
|
||||||
FAIL(errctx, AKERR_BEHAVIOR, "addchild does not throw AKERR_RELATIONSHIP when child already has a parent");
|
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_RELATIONSHIP when child already has a parent");
|
||||||
}
|
}
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_RELATIONSHIP) {
|
} HANDLE(errctx, AKERR_RELATIONSHIP) {
|
||||||
@@ -248,7 +254,7 @@ akerr_ErrorContext *test_actor_manage_children(void)
|
|||||||
CATCH(errctx, parent->addchild(parent, child));
|
CATCH(errctx, parent->addchild(parent, child));
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
if ( errctx == NULL ) {
|
if ( errctx == NULL ) {
|
||||||
FAIL(errctx, AKERR_BEHAVIOR, "addchild does not throw AKERR_OUTOFBOUNDS when all children already set");
|
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_OUTOFBOUNDS when all children already set");
|
||||||
}
|
}
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_OUTOFBOUNDS) {
|
} HANDLE(errctx, AKERR_OUTOFBOUNDS) {
|
||||||
@@ -307,13 +313,597 @@ _test_actor_addchild_heaprelease_cleanup:
|
|||||||
SUCCEED_RETURN(errctx);
|
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)
|
int main(void)
|
||||||
{
|
{
|
||||||
akgl_actor_updated = 0;
|
akgl_actor_updated = 0;
|
||||||
akgl_actor_rendered = 0;
|
akgl_actor_rendered = 0;
|
||||||
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
|
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||||
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_error_init());
|
||||||
CATCH(errctx, akgl_registry_init_actor());
|
CATCH(errctx, akgl_registry_init_actor());
|
||||||
CATCH(errctx, akgl_registry_init_sprite());
|
CATCH(errctx, akgl_registry_init_sprite());
|
||||||
CATCH(errctx, akgl_registry_init_spritesheet());
|
CATCH(errctx, akgl_registry_init_spritesheet());
|
||||||
@@ -324,6 +914,16 @@ int main(void)
|
|||||||
CATCH(errctx, test_registry_actor_iterator_updaterender());
|
CATCH(errctx, test_registry_actor_iterator_updaterender());
|
||||||
CATCH(errctx, test_akgl_actor_set_character());
|
CATCH(errctx, test_akgl_actor_set_character());
|
||||||
CATCH(errctx, test_actor_manage_children());
|
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 {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH_NORETURN(errctx);
|
} FINISH_NORETURN(errctx);
|
||||||
|
|||||||
121
tests/assets/akgl_test_mono.LICENSE.txt
Normal file
121
tests/assets/akgl_test_mono.LICENSE.txt
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
tests/assets/akgl_test_mono.ttf
|
||||||
|
================================
|
||||||
|
|
||||||
|
A subset of Liberation Mono Regular, cut down to printable ASCII (U+0020 to
|
||||||
|
U+007E) so the text suite has a font fixture that is 10 KB rather than 320 KB.
|
||||||
|
It is monospaced, which is what the measurement tests rely on: the width of an
|
||||||
|
N-character string is exactly N times the width of one character, in any font
|
||||||
|
size, so the assertions do not have to hardcode glyph metrics.
|
||||||
|
|
||||||
|
Generated with:
|
||||||
|
|
||||||
|
pyftsubset /usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf \
|
||||||
|
--unicodes=U+0020-007E --layout-features='' --no-hinting \
|
||||||
|
--desubroutinize --name-IDs='*' --output-file=akgl_test_mono.ttf
|
||||||
|
|
||||||
|
then renamed to "AKGL Test Mono" through the fontTools name table. The rename is
|
||||||
|
required, not cosmetic: "Liberation" is a Reserved Font Name under the license
|
||||||
|
below, and a modified copy may not carry it.
|
||||||
|
|
||||||
|
Copyright (c) 2012 Red Hat, Inc. with Reserved Font Name Liberation.
|
||||||
|
Digitized data copyright (c) 2010 Google Corporation with Reserved Font Arimo,
|
||||||
|
Tinos and Cousine.
|
||||||
|
|
||||||
|
Licensed under the SIL Open Font License, Version 1.1, reproduced in full below.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License,
|
||||||
|
Version 1.1.
|
||||||
|
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
|
||||||
|
PREAMBLE The goals of the Open Font License (OFL) are to stimulate
|
||||||
|
worldwide development of collaborative font projects, to support the font
|
||||||
|
creation efforts of academic and linguistic communities, and to provide
|
||||||
|
a free and open framework in which fonts may be shared and improved in
|
||||||
|
partnership with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves.
|
||||||
|
The fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply to
|
||||||
|
any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such.
|
||||||
|
This may include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components
|
||||||
|
as distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting ? in part or in whole ?
|
||||||
|
any of the components of the Original Version, by changing formats or
|
||||||
|
by porting the Font Software to a new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical writer
|
||||||
|
or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,in
|
||||||
|
Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the
|
||||||
|
corresponding Copyright Holder. This restriction only applies to the
|
||||||
|
primary font name as presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole, must
|
||||||
|
be distributed entirely under this license, and must not be distributed
|
||||||
|
under any other license. The requirement for fonts to remain under
|
||||||
|
this license does not apply to any document created using the Font
|
||||||
|
Software.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are not met.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||||
|
DEALINGS IN THE FONT SOFTWARE.
|
||||||
BIN
tests/assets/akgl_test_mono.ttf
Normal file
BIN
tests/assets/akgl_test_mono.ttf
Normal file
Binary file not shown.
19
tests/assets/snippets/test_json_helpers.json
Normal file
19
tests/assets/snippets/test_json_helpers.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"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 }]
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "testcharacter",
|
"name": "testcharacter",
|
||||||
"velocity_x": 0.20,
|
"movementspeed": 1,
|
||||||
"velocity_y": 0.20,
|
"speedtime": 1,
|
||||||
|
"speed_x": 0.20,
|
||||||
|
"speed_y": 0.20,
|
||||||
|
"acceleration_x": 0.20,
|
||||||
|
"acceleration_y": 0.20,
|
||||||
"sprite_mappings": [
|
"sprite_mappings": [
|
||||||
{
|
{
|
||||||
"state": [
|
"state": [
|
||||||
@@ -12,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": [
|
"state": [
|
||||||
"ACTOR_STATE_DEAD"
|
"AKGL_ACTOR_STATE_DEAD"
|
||||||
],
|
],
|
||||||
"sprite": "testsprite2"
|
"sprite": "testsprite2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"spritesheet": {
|
"spritesheet": {
|
||||||
"filename": "assets/spritesheet.png",
|
"filename": "spritesheet.png",
|
||||||
"frame_width": 48,
|
"frame_width": 48,
|
||||||
"frame_height": 48
|
"frame_height": 48
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"spritesheet": {
|
"spritesheet": {
|
||||||
"filename": "assets/spritesheet.png",
|
"filename": "spritesheet.png",
|
||||||
"frame_width": 48,
|
"frame_width": 48,
|
||||||
"frame_height": 48
|
"frame_height": 48
|
||||||
},
|
},
|
||||||
|
|||||||
491
tests/audio.c
Normal file
491
tests/audio.c
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
/**
|
||||||
|
* @file audio.c
|
||||||
|
* @brief Unit tests for the tone generator.
|
||||||
|
*
|
||||||
|
* Almost everything here drives akgl_audio_mix() by hand rather than opening a
|
||||||
|
* device. That is not a workaround: a device pulls samples on SDL's audio
|
||||||
|
* thread at whatever rate it likes, so a test that opened one and then asserted
|
||||||
|
* on voice state would be racing the callback. Pulling the samples ourselves
|
||||||
|
* makes the synthesis deterministic -- the same input produces the same
|
||||||
|
* waveform, sample for sample, every run.
|
||||||
|
*
|
||||||
|
* The device is opened once, at the end, to prove akgl_audio_init() and
|
||||||
|
* akgl_audio_shutdown() work against the dummy driver.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akgl/audio.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
|
|
||||||
|
#include "testutil.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Frames of output most tests generate at a time.
|
||||||
|
*
|
||||||
|
* Enough to hold a 10 ms attack and a 10 ms decay back to back at 44100 frames
|
||||||
|
* per second, which is the longest single stretch any test here inspects.
|
||||||
|
*/
|
||||||
|
#define TEST_MIX_FRAMES 1024
|
||||||
|
|
||||||
|
/** @brief 441 Hz at 44100 frames per second is exactly 100 frames per cycle. */
|
||||||
|
#define TEST_TONE_HZ 441.0f
|
||||||
|
/** @brief Frames in one cycle of TEST_TONE_HZ. */
|
||||||
|
#define TEST_TONE_PERIOD 100
|
||||||
|
|
||||||
|
/** @brief Somewhere to mix into. */
|
||||||
|
static float32_t samples[TEST_MIX_FRAMES];
|
||||||
|
|
||||||
|
/** @brief Silence every voice and put the master level back. */
|
||||||
|
static akerr_ErrorContext *reset_audio(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
|
||||||
|
CATCH(errctx, akgl_audio_stop(i));
|
||||||
|
CATCH(errctx, akgl_audio_waveform(i, AKGL_AUDIO_WAVE_SQUARE));
|
||||||
|
CATCH(errctx, akgl_audio_envelope(i, 0, 0, 1.0f, 0));
|
||||||
|
}
|
||||||
|
CATCH(errctx, akgl_audio_volume(1.0f));
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Report the largest absolute sample in the first @p frames. */
|
||||||
|
static float32_t peak_of(int frames)
|
||||||
|
{
|
||||||
|
float32_t peak = 0.0f;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < frames; i++ ) {
|
||||||
|
if ( fabsf(samples[i]) > peak ) {
|
||||||
|
peak = fabsf(samples[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return peak;
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_audio_defaults(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
// Deliberately the first thing this file does, and deliberately without
|
||||||
|
// a call to reset_audio(): a voice nobody has configured has to make a
|
||||||
|
// sound. A zeroed voice would have a sustain of 0.0 and be silent, with
|
||||||
|
// no error to say why, which is exactly the trap this defends against.
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
||||||
|
"an unconfigured voice mixed to %f, expected a full-level square wave",
|
||||||
|
samples[0]);
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[50], -1.0f,
|
||||||
|
"an unconfigured voice is not a square wave (%f half a cycle in)",
|
||||||
|
samples[50]);
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(reset_audio());
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_audio_silence(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
bool silent = true;
|
||||||
|
bool active = true;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
|
||||||
|
// With nothing sounding, the mixer produces silence rather than
|
||||||
|
// whatever was left in the buffer.
|
||||||
|
SDL_memset((void *)&samples, 0xff, sizeof(samples));
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES), "mixing an idle voice table");
|
||||||
|
for ( i = 0; i < TEST_MIX_FRAMES; i++ ) {
|
||||||
|
TEST_ASSERT_FLAG(silent, samples[i] == 0.0f);
|
||||||
|
}
|
||||||
|
TEST_ASSERT(errctx, silent == true, "an idle voice table did not mix to silence");
|
||||||
|
|
||||||
|
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
|
||||||
|
CATCH(errctx, akgl_audio_voice_active(i, &active));
|
||||||
|
TEST_ASSERT(errctx, active == false, "voice %d reports active with nothing playing", i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero frames is a legal request that writes nothing.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, 0), "mixing zero frames");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_audio_mix(NULL, 16),
|
||||||
|
"mixing into a NULL destination");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_mix(samples, -1),
|
||||||
|
"mixing a negative number of frames");
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_audio_square_tone(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
bool firsthalf = true;
|
||||||
|
bool secondhalf = true;
|
||||||
|
bool active = false;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
|
||||||
|
// A square wave with a flat envelope is the one waveform whose every
|
||||||
|
// sample is known exactly: +1 for the first half of each cycle, -1 for
|
||||||
|
// the second.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000), "sounding a tone");
|
||||||
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||||
|
TEST_ASSERT(errctx, active == true, "a sounded voice does not report itself active");
|
||||||
|
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES), "mixing a square wave");
|
||||||
|
for ( i = 0; i < (TEST_TONE_PERIOD / 2); i++ ) {
|
||||||
|
TEST_ASSERT_FLAG(firsthalf, samples[i] == 1.0f);
|
||||||
|
}
|
||||||
|
for ( i = (TEST_TONE_PERIOD / 2); i < TEST_TONE_PERIOD; i++ ) {
|
||||||
|
TEST_ASSERT_FLAG(secondhalf, samples[i] == -1.0f);
|
||||||
|
}
|
||||||
|
TEST_ASSERT(errctx, firsthalf == true,
|
||||||
|
"the first half cycle of a square wave is not at full positive level");
|
||||||
|
TEST_ASSERT(errctx, secondhalf == true,
|
||||||
|
"the second half cycle of a square wave is not at full negative level");
|
||||||
|
|
||||||
|
// The wave repeats: the second cycle matches the first.
|
||||||
|
TEST_ASSERT(errctx, samples[TEST_TONE_PERIOD] == samples[0],
|
||||||
|
"the wave did not repeat after one period");
|
||||||
|
TEST_ASSERT(errctx, samples[TEST_TONE_PERIOD + 60] == samples[60],
|
||||||
|
"the wave did not repeat after one period");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, 0.0f, 100),
|
||||||
|
"sounding a tone at zero hertz");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, -100.0f, 100),
|
||||||
|
"sounding a tone at a negative frequency");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, TEST_TONE_HZ, 0),
|
||||||
|
"sounding a tone with no duration");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(-1, TEST_TONE_HZ, 100),
|
||||||
|
"sounding a tone on a negative voice");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||||
|
akgl_audio_tone(AKGL_AUDIO_MAX_VOICES, TEST_TONE_HZ, 100),
|
||||||
|
"sounding a tone on a voice past the last one");
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(reset_audio());
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_audio_waveforms(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
bool inrange = true;
|
||||||
|
int i = 0;
|
||||||
|
int w = 0;
|
||||||
|
akgl_AudioWaveform shapes[4] = {
|
||||||
|
AKGL_AUDIO_WAVE_TRIANGLE,
|
||||||
|
AKGL_AUDIO_WAVE_SAWTOOTH,
|
||||||
|
AKGL_AUDIO_WAVE_NOISE,
|
||||||
|
AKGL_AUDIO_WAVE_SINE,
|
||||||
|
};
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
// Every shape has to stay inside full scale and actually move. The
|
||||||
|
// exact sample values differ per shape; these two properties do not.
|
||||||
|
for ( w = 0; w < 4; w++ ) {
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
CATCH(errctx, akgl_audio_waveform(0, shapes[w]));
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||||
|
for ( i = 0; i < TEST_MIX_FRAMES; i++ ) {
|
||||||
|
TEST_ASSERT_FLAG(inrange, (samples[i] >= -1.0f) && (samples[i] <= 1.0f));
|
||||||
|
}
|
||||||
|
TEST_ASSERT_FLAG(inrange, peak_of(TEST_MIX_FRAMES) > 0.5f);
|
||||||
|
}
|
||||||
|
TEST_ASSERT(errctx, inrange == true,
|
||||||
|
"a waveform either left full scale or produced nothing");
|
||||||
|
|
||||||
|
// The triangle is symmetric about the middle of its cycle, which the
|
||||||
|
// square and sawtooth are not -- enough to tell it was really selected.
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
CATCH(errctx, akgl_audio_waveform(0, AKGL_AUDIO_WAVE_TRIANGLE));
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[25], 0.0f,
|
||||||
|
"a triangle wave is at %f a quarter of the way up, expected 0",
|
||||||
|
samples[25]);
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[50], 1.0f,
|
||||||
|
"a triangle wave is at %f at its peak, expected 1", samples[50]);
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[75], 0.0f,
|
||||||
|
"a triangle wave is at %f three quarters through, expected 0",
|
||||||
|
samples[75]);
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||||
|
akgl_audio_waveform(0, (akgl_AudioWaveform)(AKGL_AUDIO_WAVE_SINE + 1)),
|
||||||
|
"selecting a waveform past the last one");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||||
|
akgl_audio_waveform(AKGL_AUDIO_MAX_VOICES, AKGL_AUDIO_WAVE_SINE),
|
||||||
|
"selecting a waveform on a voice past the last one");
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(reset_audio());
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_audio_envelope(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
bool rising = true;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
|
||||||
|
// A 10 ms attack at 44100 is 441 frames, so the level should climb from
|
||||||
|
// nothing to full across the first 441 samples of a square wave and
|
||||||
|
// then hold at the sustain level.
|
||||||
|
CATCH(errctx, akgl_audio_envelope(0, 10, 0, 1.0f, 0));
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||||
|
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[0], 0.0f,
|
||||||
|
"an attack does not start from silence (first sample %f)", samples[0]);
|
||||||
|
TEST_ASSERT(errctx, fabsf(samples[100]) > fabsf(samples[10]),
|
||||||
|
"the attack is not climbing (%f at frame 10, %f at frame 100)",
|
||||||
|
samples[10], samples[100]);
|
||||||
|
// Frames 0..49 are the positive half of the square, so their level is
|
||||||
|
// the envelope value directly.
|
||||||
|
for ( i = 1; i < 50; i++ ) {
|
||||||
|
TEST_ASSERT_FLAG(rising, samples[i] > samples[i - 1]);
|
||||||
|
}
|
||||||
|
TEST_ASSERT(errctx, rising == true, "the attack ramp is not monotonic");
|
||||||
|
|
||||||
|
// Half a millisecond of decay to a half sustain level, no attack: the
|
||||||
|
// very first sample is at full level and it settles at the sustain.
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
CATCH(errctx, akgl_audio_envelope(0, 0, 10, 0.5f, 0));
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
||||||
|
"with no attack the first sample is %f, expected full level", samples[0]);
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[450], -0.5f,
|
||||||
|
"after the decay the level is %f, expected the 0.5 sustain", samples[450]);
|
||||||
|
|
||||||
|
// Attack and decay together, which is the case where the decay has to
|
||||||
|
// measure from the end of the attack rather than from the start of the
|
||||||
|
// note. 10 ms of each is 441 frames of each at 44100.
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
CATCH(errctx, akgl_audio_envelope(0, 10, 10, 0.5f, 0));
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[441], 1.0f,
|
||||||
|
"at the end of the attack the level is %f, expected full", samples[441]);
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[661], -(1.0f - (0.5f * (220.0f / 441.0f))),
|
||||||
|
"halfway through the decay the level is %f, expected the halfway ramp",
|
||||||
|
samples[661]);
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[900], 0.5f,
|
||||||
|
"after the decay the level is %f, expected the 0.5 sustain", samples[900]);
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_envelope(0, 1, 1, -0.1f, 1),
|
||||||
|
"setting a negative sustain level");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_envelope(0, 1, 1, 1.5f, 1),
|
||||||
|
"setting a sustain level past full");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||||
|
akgl_audio_envelope(AKGL_AUDIO_MAX_VOICES, 1, 1, 1.0f, 1),
|
||||||
|
"setting an envelope on a voice past the last one");
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(reset_audio());
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_audio_duration_and_release(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
bool active = false;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
|
||||||
|
// A 10 ms gate is 441 frames. The voice is still sounding while they
|
||||||
|
// are being generated and goes quiet on its own afterwards, without the
|
||||||
|
// caller having to stop it.
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, 440));
|
||||||
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||||
|
TEST_ASSERT(errctx, active == true, "the voice went quiet before its gate closed");
|
||||||
|
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, 8));
|
||||||
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||||
|
TEST_ASSERT(errctx, active == false, "the voice is still sounding past its gate");
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, 64));
|
||||||
|
TEST_ASSERT_FEQ(errctx, peak_of(64), 0.0f,
|
||||||
|
"a finished voice is still producing sound (peak %f)", peak_of(64));
|
||||||
|
|
||||||
|
// With a release, the voice outlives its gate and fades rather than
|
||||||
|
// stopping dead.
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
CATCH(errctx, akgl_audio_envelope(0, 0, 0, 1.0f, 10));
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, 441));
|
||||||
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||||
|
TEST_ASSERT(errctx, active == true, "a voice with a release stopped when its gate closed");
|
||||||
|
// The release starts from the level the gate left off at and falls from
|
||||||
|
// there, so the end of this window has to be quieter than its start.
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, 220));
|
||||||
|
TEST_ASSERT(errctx, fabsf(samples[219]) < fabsf(samples[0]),
|
||||||
|
"the release is not attenuating (%f at its start, %f 220 frames later)",
|
||||||
|
samples[0], samples[219]);
|
||||||
|
TEST_ASSERT(errctx, peak_of(220) > 0.0f, "the release went silent immediately");
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, 250));
|
||||||
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||||
|
TEST_ASSERT(errctx, active == false, "the voice outlived its gate and its release");
|
||||||
|
|
||||||
|
// Stopping cuts a voice off where it stands, release and all.
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
CATCH(errctx, akgl_audio_envelope(0, 0, 0, 1.0f, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, 64));
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_audio_stop(0), "stopping a sounding voice");
|
||||||
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||||
|
TEST_ASSERT(errctx, active == false, "a stopped voice still reports itself active");
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, 64));
|
||||||
|
TEST_ASSERT_FEQ(errctx, peak_of(64), 0.0f,
|
||||||
|
"a stopped voice is still producing sound (peak %f)", peak_of(64));
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_stop(AKGL_AUDIO_MAX_VOICES),
|
||||||
|
"stopping a voice past the last one");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||||
|
akgl_audio_voice_active(AKGL_AUDIO_MAX_VOICES, &active),
|
||||||
|
"asking about a voice past the last one");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_audio_voice_active(0, NULL),
|
||||||
|
"asking about a voice with nowhere to put the answer");
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(reset_audio());
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_audio_volume_and_mixing(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
|
||||||
|
// The master level scales everything.
|
||||||
|
CATCH(errctx, akgl_audio_volume(0.25f));
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[0], 0.25f,
|
||||||
|
"at a quarter volume the first sample is %f, expected 0.25", samples[0]);
|
||||||
|
|
||||||
|
CATCH(errctx, akgl_audio_volume(0.0f));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||||
|
TEST_ASSERT_FEQ(errctx, peak_of(TEST_MIX_FRAMES), 0.0f,
|
||||||
|
"at zero volume the peak is %f, expected silence",
|
||||||
|
peak_of(TEST_MIX_FRAMES));
|
||||||
|
|
||||||
|
// Voices sum, and the sum is clamped rather than wrapped: three square
|
||||||
|
// waves in phase at full level would be 3.0 without the clamp.
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_tone(1, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_tone(2, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
||||||
|
"three voices in phase mixed to %f, expected a clamp at 1.0", samples[0]);
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[50], -1.0f,
|
||||||
|
"three voices in phase mixed to %f, expected a clamp at -1.0", samples[50]);
|
||||||
|
|
||||||
|
// Two voices at different levels sum to their total rather than to
|
||||||
|
// either one of them, which is only visible below the clamp.
|
||||||
|
CATCH(errctx, reset_audio());
|
||||||
|
CATCH(errctx, akgl_audio_volume(0.5f));
|
||||||
|
CATCH(errctx, akgl_audio_envelope(1, 0, 0, 0.5f, 0));
|
||||||
|
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_tone(1, TEST_TONE_HZ, 1000));
|
||||||
|
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||||
|
TEST_ASSERT_FEQ(errctx, samples[0], 0.75f,
|
||||||
|
"a full voice and a half voice at half volume mixed to %f, expected 0.75",
|
||||||
|
samples[0]);
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_volume(-0.1f),
|
||||||
|
"setting a negative master volume");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_volume(1.1f),
|
||||||
|
"setting a master volume past full");
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(reset_audio());
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_audio_device(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
bool active = true;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
// Everything above ran without a device. This is the one test that
|
||||||
|
// opens one, so nothing that asserts on voice state runs after it.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_audio_init(), "opening an audio device");
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_audio_init(), "opening an audio device a second time");
|
||||||
|
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10), "sounding a tone on a device");
|
||||||
|
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_audio_shutdown(), "closing the audio device");
|
||||||
|
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||||
|
TEST_ASSERT(errctx, active == false, "shutting down left a voice sounding");
|
||||||
|
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_audio_shutdown(), "closing an audio device that is not open");
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(akgl_audio_shutdown());
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||||
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_error_init());
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_Init(SDL_INIT_AUDIO),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"Couldn't initialize SDL: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
CATCH(errctx, test_audio_defaults());
|
||||||
|
CATCH(errctx, test_audio_silence());
|
||||||
|
CATCH(errctx, test_audio_square_tone());
|
||||||
|
CATCH(errctx, test_audio_waveforms());
|
||||||
|
CATCH(errctx, test_audio_envelope());
|
||||||
|
CATCH(errctx, test_audio_duration_and_release());
|
||||||
|
CATCH(errctx, test_audio_volume_and_mixing());
|
||||||
|
CATCH(errctx, test_audio_device());
|
||||||
|
} CLEANUP {
|
||||||
|
SDL_Quit();
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ akerr_ErrorContext *test_character_sprite_mgmt()
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_heap_next_character(&testchar));
|
CATCH(errctx, akgl_heap_next_character(&testchar));
|
||||||
|
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
|
||||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
||||||
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
||||||
FAIL_ZERO_BREAK(
|
FAIL_ZERO_BREAK(
|
||||||
@@ -89,6 +90,7 @@ akerr_ErrorContext *test_character_iterate_state_sprites()
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_heap_next_character(&testchar));
|
CATCH(errctx, akgl_heap_next_character(&testchar));
|
||||||
|
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
|
||||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
||||||
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
||||||
FAIL_ZERO_BREAK(
|
FAIL_ZERO_BREAK(
|
||||||
@@ -169,8 +171,8 @@ akerr_ErrorContext *test_akgl_character_load_json()
|
|||||||
CATCH(errctx, testcharacter->sprite_get(testcharacter, AKGL_ACTOR_STATE_DEAD, &comparesprite));
|
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, (comparesprite == testsprite2), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_DEAD");
|
||||||
|
|
||||||
FAIL_ZERO_BREAK(errctx, (testcharacter->vx != 0.200000003), AKERR_VALUE, "Wrong X velocity for test character");
|
FAIL_ZERO_BREAK(errctx, (testcharacter->ax != 0.200000003), AKERR_VALUE, "Wrong X acceleration for test character");
|
||||||
FAIL_ZERO_BREAK(errctx, (testcharacter->vy != 0.200000003), AKERR_VALUE, "Wrong Y velocity for test character");
|
FAIL_ZERO_BREAK(errctx, (testcharacter->ay != 0.200000003), AKERR_VALUE, "Wrong Y acceleration for test character");
|
||||||
|
|
||||||
// Release our handles on the sprites so the character's heap_release can reduce them to 0
|
// Release our handles on the sprites so the character's heap_release can reduce them to 0
|
||||||
CATCH(errctx, akgl_heap_release_sprite(testsprite));
|
CATCH(errctx, akgl_heap_release_sprite(testsprite));
|
||||||
@@ -193,6 +195,7 @@ int main(void)
|
|||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_error_init());
|
||||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||||
|
|
||||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ int main(void)
|
|||||||
AKGL_REGISTRY_CHARACTER,
|
AKGL_REGISTRY_CHARACTER,
|
||||||
"little guy",
|
"little guy",
|
||||||
NULL);
|
NULL);
|
||||||
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKERR_REGISTRY, "Can't load character 'little guy' from the registry");
|
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKGL_ERR_REGISTRY, "Can't load character 'little guy' from the registry");
|
||||||
actorptr->movement_controls_face = false;
|
actorptr->movement_controls_face = false;
|
||||||
actorptr->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT);
|
actorptr->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT);
|
||||||
actorptr->x = 320;
|
actorptr->x = 320;
|
||||||
|
|||||||
718
tests/controller.c
Normal file
718
tests/controller.c
Normal file
@@ -0,0 +1,718 @@
|
|||||||
|
/**
|
||||||
|
* @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>
|
||||||
|
// akgl/controller.h uses akgl_Actor without declaring it, so actor.h has to
|
||||||
|
// come first, the same way src/controller.c reaches it through akgl/game.h.
|
||||||
|
#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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
} CLEANUP {
|
||||||
|
SDL_Quit();
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
}
|
||||||
600
tests/draw.c
Normal file
600
tests/draw.c
Normal file
@@ -0,0 +1,600 @@
|
|||||||
|
/**
|
||||||
|
* @file draw.c
|
||||||
|
* @brief Unit tests for the immediate-mode drawing primitives.
|
||||||
|
*
|
||||||
|
* Everything here draws into a small software renderer under the dummy video
|
||||||
|
* driver and then reads the target back with SDL_RenderReadPixels, so the
|
||||||
|
* assertions are about pixels that actually changed rather than about SDL
|
||||||
|
* having been called. No window is shown and no display is required.
|
||||||
|
*
|
||||||
|
* The target is deliberately tiny: at 64x64 a full readback is 16 KB, which
|
||||||
|
* makes it cheap to read the whole thing back after every operation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akgl/error.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
|
#include <akgl/draw.h>
|
||||||
|
#include <akgl/renderer.h>
|
||||||
|
|
||||||
|
#include "testutil.h"
|
||||||
|
|
||||||
|
/** @brief Width and height of the offscreen target every test draws into. */
|
||||||
|
#define TEST_TARGET_SIZE 64
|
||||||
|
|
||||||
|
/** @brief Opaque black, what each test clears the target to. */
|
||||||
|
static const SDL_Color testblack = { 0x00, 0x00, 0x00, 0xff };
|
||||||
|
/** @brief The color most tests draw with. */
|
||||||
|
static const SDL_Color testred = { 0xff, 0x00, 0x00, 0xff };
|
||||||
|
/** @brief A second color, for tests that need to tell two marks apart. */
|
||||||
|
static const SDL_Color testgreen = { 0x00, 0xff, 0x00, 0xff };
|
||||||
|
|
||||||
|
/** @brief Clear the whole target to opaque black. */
|
||||||
|
static akerr_ErrorContext *clear_target(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
SDL_SetRenderDrawColor(renderer->sdl_renderer, 0x00, 0x00, 0x00, 0xff),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
SDL_RenderClear(renderer->sdl_renderer),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Report whether one pixel of @p shot carries @p color.
|
||||||
|
*
|
||||||
|
* Alpha is not compared: the render target's own alpha depends on the format
|
||||||
|
* SDL picked for it, and none of these tests draw translucently.
|
||||||
|
*/
|
||||||
|
static bool pixel_is(SDL_Surface *shot, int x, int y, SDL_Color color)
|
||||||
|
{
|
||||||
|
uint8_t r = 0;
|
||||||
|
uint8_t g = 0;
|
||||||
|
uint8_t b = 0;
|
||||||
|
uint8_t a = 0;
|
||||||
|
|
||||||
|
if ( shot == NULL ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ( !SDL_ReadSurfacePixel(shot, x, y, &r, &g, &b, &a) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ((r == color.r) && (g == color.g) && (b == color.b));
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_draw_point(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Surface *shot = NULL;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_point(renderer, 10.0f, 20.0f, testred),
|
||||||
|
"plotting one pixel");
|
||||||
|
|
||||||
|
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 10, 20, testred),
|
||||||
|
"the plotted pixel at 10,20 is not the color it was drawn with");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 11, 20, testblack),
|
||||||
|
"plotting one pixel also changed its neighbour at 11,20");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 10, 21, testblack),
|
||||||
|
"plotting one pixel also changed the pixel below it");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_draw_point(NULL, 0.0f, 0.0f, testred),
|
||||||
|
"plotting through a NULL backend");
|
||||||
|
} CLEANUP {
|
||||||
|
if ( shot != NULL ) {
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_draw_line(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Surface *shot = NULL;
|
||||||
|
bool onthe_line = true;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
// A vertical line, so every pixel of it is known without reasoning
|
||||||
|
// about how SDL rasterises a diagonal.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_line(renderer, 5.0f, 4.0f, 5.0f, 12.0f, testred),
|
||||||
|
"drawing a vertical line");
|
||||||
|
|
||||||
|
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
for ( i = 4; i <= 12; i++ ) {
|
||||||
|
TEST_ASSERT_FLAG(onthe_line, pixel_is(shot, 5, i, testred));
|
||||||
|
}
|
||||||
|
TEST_ASSERT(errctx, onthe_line == true,
|
||||||
|
"the vertical line from 5,4 to 5,12 has a gap in it");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 5, 3, testblack),
|
||||||
|
"the line ran past its first endpoint");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 5, 13, testblack),
|
||||||
|
"the line ran past its second endpoint");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 6, 8, testblack),
|
||||||
|
"the line is wider than one pixel");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_draw_line(NULL, 0.0f, 0.0f, 1.0f, 1.0f, testred),
|
||||||
|
"drawing a line through a NULL backend");
|
||||||
|
} CLEANUP {
|
||||||
|
if ( shot != NULL ) {
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_draw_rects(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Surface *shot = NULL;
|
||||||
|
SDL_FRect box;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
box.x = 8.0f;
|
||||||
|
box.y = 8.0f;
|
||||||
|
box.w = 16.0f;
|
||||||
|
box.h = 16.0f;
|
||||||
|
|
||||||
|
// The outline touches the border and leaves the middle alone.
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_rect(renderer, &box, testred), "outlining a rectangle");
|
||||||
|
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the outline is missing its top left corner");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 23, 23, testred),
|
||||||
|
"the outline is missing its bottom right corner");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 16, 8, testred), "the outline is missing its top edge");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testblack), "the outline filled its interior");
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
shot = NULL;
|
||||||
|
|
||||||
|
// The filled form covers the interior as well.
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(renderer, &box, testred), "filling a rectangle");
|
||||||
|
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testred), "the fill left its interior empty");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the fill missed its top left corner");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 24, 24, testblack),
|
||||||
|
"the fill ran one pixel past its bottom right corner");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(NULL, &box, testred),
|
||||||
|
"outlining through a NULL backend");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(renderer, NULL, testred),
|
||||||
|
"outlining a NULL rectangle");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(NULL, &box, testred),
|
||||||
|
"filling through a NULL backend");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(renderer, NULL, testred),
|
||||||
|
"filling a NULL rectangle");
|
||||||
|
} CLEANUP {
|
||||||
|
if ( shot != NULL ) {
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_draw_circle(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Surface *shot = NULL;
|
||||||
|
bool symmetric = true;
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_circle(renderer, 32.0f, 32.0f, 10.0f, testred),
|
||||||
|
"drawing a circle");
|
||||||
|
|
||||||
|
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
// The four axis points are exact for any correct midpoint circle.
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 42, 32, testred), "the circle is missing its rightmost pixel");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 22, 32, testred), "the circle is missing its leftmost pixel");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 42, testred), "the circle is missing its bottom pixel");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 22, testred), "the circle is missing its top pixel");
|
||||||
|
// It is an outline, not a disc.
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack), "the circle filled its center");
|
||||||
|
// ...and nothing lands outside the radius.
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 43, 32, testred) == false,
|
||||||
|
"the circle drew a pixel one past its radius");
|
||||||
|
|
||||||
|
// Every plotted pixel has a mirror in the other three quadrants. The
|
||||||
|
// circle is drawn one octant at a time and reflected seven ways, so a
|
||||||
|
// sign error in any single reflection breaks this and nothing else --
|
||||||
|
// the four axis points above stay put either way.
|
||||||
|
for ( y = 22; y <= 42; y++ ) {
|
||||||
|
for ( x = 22; x <= 42; x++ ) {
|
||||||
|
if ( !pixel_is(shot, x, y, testred) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, y, testred));
|
||||||
|
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, x, 64 - y, testred));
|
||||||
|
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, 64 - y, testred));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TEST_ASSERT(errctx, symmetric == true,
|
||||||
|
"the circle is not symmetric about its center; an octant is reflected wrong");
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
shot = NULL;
|
||||||
|
|
||||||
|
// A zero radius is the degenerate case, not an error: one pixel.
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_circle(renderer, 5.0f, 5.0f, 0.0f, testred),
|
||||||
|
"drawing a circle of radius zero");
|
||||||
|
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 5, 5, testred),
|
||||||
|
"a circle of radius zero did not plot its center");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||||
|
akgl_draw_circle(renderer, 5.0f, 5.0f, -1.0f, testred),
|
||||||
|
"drawing a circle of negative radius");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_draw_circle(NULL, 5.0f, 5.0f, 4.0f, testred),
|
||||||
|
"drawing a circle through a NULL backend");
|
||||||
|
} CLEANUP {
|
||||||
|
if ( shot != NULL ) {
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_draw_flood_fill(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Surface *shot = NULL;
|
||||||
|
SDL_FRect box;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
// A red box outline on black. Filling inside it must stay inside it,
|
||||||
|
// which is the whole contract of PAINT.
|
||||||
|
box.x = 10.0f;
|
||||||
|
box.y = 10.0f;
|
||||||
|
box.w = 20.0f;
|
||||||
|
box.h = 20.0f;
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
CATCH(errctx, akgl_draw_rect(renderer, &box, testred));
|
||||||
|
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 20, 20, testgreen),
|
||||||
|
"flooding the inside of a box");
|
||||||
|
|
||||||
|
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen), "the seed pixel was not filled");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 11, 11, testgreen),
|
||||||
|
"the fill did not reach the top left of the interior");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 28, 28, testgreen),
|
||||||
|
"the fill did not reach the bottom right of the interior");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred), "the fill overwrote the boundary");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 20, 10, testred), "the fill overwrote the top edge");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 20, 5, testblack), "the fill leaked outside the box");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 40, 40, testblack),
|
||||||
|
"the fill leaked into the rest of the target");
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
shot = NULL;
|
||||||
|
|
||||||
|
// Filling a region that is already the requested color changes nothing
|
||||||
|
// and is not an error.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 20, 20, testgreen),
|
||||||
|
"flooding a region that is already that color");
|
||||||
|
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen),
|
||||||
|
"refilling a region disturbed it");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred),
|
||||||
|
"refilling a region disturbed its boundary");
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
shot = NULL;
|
||||||
|
|
||||||
|
// Flooding the outside reaches every pixel that is not the box.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 0, 0, testgreen),
|
||||||
|
"flooding the area around a box");
|
||||||
|
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testgreen), "the seed pixel was not filled");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, TEST_TARGET_SIZE - 1, TEST_TARGET_SIZE - 1, testgreen),
|
||||||
|
"the fill did not reach the far corner of the target");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred),
|
||||||
|
"the fill from outside overwrote the boundary");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||||
|
akgl_draw_flood_fill(renderer, -1, 0, testred),
|
||||||
|
"flooding from a seed left of the target");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||||
|
akgl_draw_flood_fill(renderer, 0, TEST_TARGET_SIZE, testred),
|
||||||
|
"flooding from a seed below the target");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_draw_flood_fill(NULL, 0, 0, testred),
|
||||||
|
"flooding through a NULL backend");
|
||||||
|
} CLEANUP {
|
||||||
|
if ( shot != NULL ) {
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_draw_copy_and_paste_region(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Surface *shot = NULL;
|
||||||
|
SDL_Surface *saved = NULL;
|
||||||
|
SDL_Surface *reused = NULL;
|
||||||
|
SDL_Rect region;
|
||||||
|
SDL_FRect box;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
// Put something recognisable in the top left corner and save it.
|
||||||
|
box.x = 0.0f;
|
||||||
|
box.y = 0.0f;
|
||||||
|
box.w = 8.0f;
|
||||||
|
box.h = 8.0f;
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
CATCH(errctx, akgl_draw_filled_rect(renderer, &box, testred));
|
||||||
|
|
||||||
|
region.x = 0;
|
||||||
|
region.y = 0;
|
||||||
|
region.w = 8;
|
||||||
|
region.h = 8;
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_copy_region(renderer, ®ion, &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, ®ion, &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, ®ion, &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, ®ion, &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, ®ion, &reused),
|
||||||
|
"saving a region with no area");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_draw_copy_region(NULL, ®ion, &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, ®ion, 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, ®ion, &saved),
|
||||||
|
"saving through an uninitialized backend");
|
||||||
|
TEST_ASSERT(errctx, saved == NULL,
|
||||||
|
"a refused save still wrote something to the destination");
|
||||||
|
|
||||||
|
scratch = SDL_CreateSurface(4, 4, SDL_PIXELFORMAT_RGBA32);
|
||||||
|
FAIL_ZERO_BREAK(errctx, scratch, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_draw_paste_region(&empty, scratch, 0.0f, 0.0f),
|
||||||
|
"pasting through an uninitialized backend");
|
||||||
|
} CLEANUP {
|
||||||
|
if ( saved != NULL ) {
|
||||||
|
SDL_DestroySurface(saved);
|
||||||
|
}
|
||||||
|
if ( scratch != NULL ) {
|
||||||
|
SDL_DestroySurface(scratch);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||||
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||||
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_error_init());
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
|
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_Init(SDL_INIT_VIDEO),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"Couldn't initialize SDL: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_CreateWindowAndRenderer(
|
||||||
|
"net/aklabs/libakgl/test_draw",
|
||||||
|
TEST_TARGET_SIZE,
|
||||||
|
TEST_TARGET_SIZE,
|
||||||
|
0,
|
||||||
|
&window,
|
||||||
|
&renderer->sdl_renderer),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"Couldn't create window/renderer: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
CATCH(errctx, test_draw_point());
|
||||||
|
CATCH(errctx, test_draw_line());
|
||||||
|
CATCH(errctx, test_draw_rects());
|
||||||
|
CATCH(errctx, test_draw_circle());
|
||||||
|
CATCH(errctx, test_draw_flood_fill());
|
||||||
|
CATCH(errctx, test_draw_copy_and_paste_region());
|
||||||
|
CATCH(errctx, test_draw_preserves_render_draw_color());
|
||||||
|
CATCH(errctx, test_draw_backend_without_a_renderer());
|
||||||
|
} CLEANUP {
|
||||||
|
SDL_Quit();
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
}
|
||||||
102
tests/error.c
Normal file
102
tests/error.c
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* @file error.c
|
||||||
|
* @brief Unit tests for the libakgl status band: reservation, ownership and names.
|
||||||
|
*
|
||||||
|
* The libakerror registry is process-global, so these tests assert against
|
||||||
|
* whatever akgl_error_init() left behind rather than building their own state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akgl/error.h>
|
||||||
|
|
||||||
|
#include "testutil.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief akgl_error_init() must own the libakgl status band and name every code in it.
|
||||||
|
*
|
||||||
|
* A code whose name never registered degrades to "Unknown Error" in every stack
|
||||||
|
* trace that carries it, and a band we never reserved is one another component
|
||||||
|
* can name out from under us. Both stay silent until something has already gone
|
||||||
|
* wrong, so assert them directly rather than waiting to read a useless trace.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *test_error_init_owns_the_status_band(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
static const struct {
|
||||||
|
int status;
|
||||||
|
const char *name;
|
||||||
|
} expected[] = {
|
||||||
|
{ AKGL_ERR_SDL, "SDL Error" },
|
||||||
|
{ AKGL_ERR_REGISTRY, "Registry Error" },
|
||||||
|
{ AKGL_ERR_HEAP, "Heap Error" },
|
||||||
|
{ AKGL_ERR_BEHAVIOR, "Behavior Error" },
|
||||||
|
{ AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt" }
|
||||||
|
};
|
||||||
|
bool named = true;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(e, akgl_error_init());
|
||||||
|
|
||||||
|
TEST_ASSERT(e, (int)(sizeof(expected) / sizeof(expected[0])) == AKGL_ERR_COUNT,
|
||||||
|
"the libakgl status band holds %d codes but %d are named here",
|
||||||
|
AKGL_ERR_COUNT, (int)(sizeof(expected) / sizeof(expected[0])));
|
||||||
|
|
||||||
|
for ( i = 0; i < (int)(sizeof(expected) / sizeof(expected[0])); i++ ) {
|
||||||
|
TEST_ASSERT_FLAG(named,
|
||||||
|
strcmp(akerr_name_for_status(expected[i].status, NULL),
|
||||||
|
expected[i].name) == 0);
|
||||||
|
}
|
||||||
|
TEST_ASSERT(e, named,
|
||||||
|
"akgl_error_init did not register the expected name for every AKGL_ERR_* code");
|
||||||
|
|
||||||
|
// The reservation is what makes those names ours. Without it the
|
||||||
|
// registrations above would still succeed for anyone who asked.
|
||||||
|
TEST_EXPECT_STATUS(e, AKERR_STATUS_NAME_FOREIGN,
|
||||||
|
akerr_register_status_name("not-libakgl", AKGL_ERR_HEAP, "Squatter"),
|
||||||
|
"a foreign owner was allowed to rename a libakgl status");
|
||||||
|
TEST_EXPECT_STATUS(e, AKERR_STATUS_RANGE_OVERLAP,
|
||||||
|
akerr_reserve_status_range(AKGL_ERR_BASE, AKGL_ERR_COUNT, "not-libakgl"),
|
||||||
|
"a foreign owner was allowed to reserve the libakgl status band");
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
} FINISH(e, true);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Calling akgl_error_init() twice must be a no-op, not a self-collision.
|
||||||
|
*
|
||||||
|
* Nothing in libakgl orders initialization for an embedding program, so a second
|
||||||
|
* call has to be harmless. libakerror only treats an *identical* reservation as
|
||||||
|
* a repeat -- a subset or superset raises -- which makes this a real constraint
|
||||||
|
* on AKGL_ERR_BASE and AKGL_ERR_COUNT, not a triviality.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *test_error_init_is_idempotent(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
TEST_EXPECT_OK(e, akgl_error_init(), "the second akgl_error_init failed");
|
||||||
|
TEST_EXPECT_OK(e, akgl_error_init(), "the third akgl_error_init failed");
|
||||||
|
TEST_ASSERT(e, strcmp(akerr_name_for_status(AKGL_ERR_SDL, NULL), "SDL Error") == 0,
|
||||||
|
"re-running akgl_error_init lost the name for AKGL_ERR_SDL");
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
} FINISH(e, true);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, test_error_init_owns_the_status_band());
|
||||||
|
CATCH(errctx, test_error_init_is_idempotent());
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
}
|
||||||
422
tests/game.c
Normal file
422
tests/game.c
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
379
tests/heap.c
Normal file
379
tests/heap.c
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
367
tests/json_helpers.c
Normal file
367
tests/json_helpers.c
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
748
tests/physics.c
Normal file
748
tests/physics.c
Normal file
@@ -0,0 +1,748 @@
|
|||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ akerr_ErrorContext *test_akgl_registry_init(RegistryFuncPtr funcptr)
|
|||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
|
|
||||||
FAIL_RETURN(errctx, AKERR_BEHAVIOR, "SDL memory allocator fails but registry reports successful property creation");
|
FAIL_RETURN(errctx, AKGL_ERR_BEHAVIOR, "SDL memory allocator fails but registry reports successful property creation");
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
|
akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
|
||||||
@@ -87,6 +87,7 @@ int main(void)
|
|||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_error_init());
|
||||||
CATCH(errctx, test_akgl_registry_init_creation_failures());
|
CATCH(errctx, test_akgl_registry_init_creation_failures());
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
#include <akgl/heap.h>
|
#include <akgl/heap.h>
|
||||||
#include <akgl/util.h>
|
#include <akgl/util.h>
|
||||||
#include <akgl/error.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)
|
akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
|
||||||
{
|
{
|
||||||
@@ -37,7 +37,7 @@ akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
|
|||||||
"Loaded texture was not the correct size");
|
"Loaded texture was not the correct size");
|
||||||
|
|
||||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||||
image = IMG_LoadTexture(renderer, (char *)&tmpstr->data);
|
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
|
||||||
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
||||||
|
|
||||||
CATCH(
|
CATCH(
|
||||||
@@ -138,7 +138,7 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void)
|
|||||||
// Is it using the right spritesheet?
|
// Is it using the right spritesheet?
|
||||||
|
|
||||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||||
image = IMG_LoadTexture(renderer, (char *)&tmpstr->data);
|
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
|
||||||
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
||||||
|
|
||||||
CATCH(
|
CATCH(
|
||||||
@@ -187,6 +187,8 @@ int main(void)
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_error_init());
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
|
|
||||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||||
|
|
||||||
@@ -194,9 +196,10 @@ int main(void)
|
|||||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer)) {
|
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer->sdl_renderer)) {
|
||||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||||
}
|
}
|
||||||
|
renderer->draw_texture = &akgl_render_2d_draw_texture;
|
||||||
|
|
||||||
CATCH(errctx, akgl_heap_init());
|
CATCH(errctx, akgl_heap_init());
|
||||||
CATCH(errctx, akgl_registry_init_sprite());
|
CATCH(errctx, akgl_registry_init_sprite());
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
#include <akgl/heap.h>
|
#include <akgl/heap.h>
|
||||||
#include <akgl/staticstring.h>
|
#include <akgl/staticstring.h>
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ akerr_ErrorContext *test_akgl_string_initialize(void)
|
|||||||
FAIL_NONZERO_BREAK(errctx, strcmp((char *)&ptr->data, "Test value"), AKERR_VALUE, "Expected 'Test value', got %s", (char *)&ptr->data);
|
FAIL_NONZERO_BREAK(errctx, strcmp((char *)&ptr->data, "Test value"), AKERR_VALUE, "Expected 'Test value', got %s", (char *)&ptr->data);
|
||||||
|
|
||||||
CATCH(errctx, akgl_heap_release_string(NULL));
|
CATCH(errctx, akgl_heap_release_string(NULL));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Failure to properly handle NULL pointer");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Failure to properly handle NULL pointer");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
@@ -132,6 +133,7 @@ int main(void)
|
|||||||
|
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_error_init());
|
||||||
printf("test_fresh_heap_gives_string ....\n");
|
printf("test_fresh_heap_gives_string ....\n");
|
||||||
test_fresh_heap_gives_strings();
|
test_fresh_heap_gives_strings();
|
||||||
reset_string_heap();
|
reset_string_heap();
|
||||||
|
|||||||
99
tests/testutil.h
Normal file
99
tests/testutil.h
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* @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_
|
||||||
259
tests/text.c
Normal file
259
tests/text.c
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
/**
|
||||||
|
* @file text.c
|
||||||
|
* @brief Unit tests for font loading and text measurement.
|
||||||
|
*
|
||||||
|
* Measurement needs a font but no renderer, so this suite runs without a window
|
||||||
|
* and without the offscreen harness the drawing half of src/text.c is waiting
|
||||||
|
* on. akgl_text_rendertextat() is therefore not covered here.
|
||||||
|
*
|
||||||
|
* The fixture font is monospaced on purpose: the width of an N-character string
|
||||||
|
* is exactly N times the width of one character, so every assertion below is a
|
||||||
|
* relationship between measurements rather than a hardcoded pixel count that
|
||||||
|
* would break when FreeType changes its rounding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akgl/error.h>
|
||||||
|
#include <akgl/registry.h>
|
||||||
|
#include <akgl/text.h>
|
||||||
|
|
||||||
|
#include "testutil.h"
|
||||||
|
|
||||||
|
/** @brief The monospaced ASCII subset described in assets/akgl_test_mono.LICENSE.txt. */
|
||||||
|
#define TEST_FONT_PATH "assets/akgl_test_mono.ttf"
|
||||||
|
/** @brief Point size every test in this file opens the fixture font at. */
|
||||||
|
#define TEST_FONT_SIZE 16
|
||||||
|
|
||||||
|
/** @brief The fixture font, opened once by main() and shared by every test. */
|
||||||
|
static TTF_Font *testfont = NULL;
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_text_loadfont(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
TTF_Font *registered = NULL;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_registry_init_font());
|
||||||
|
|
||||||
|
TEST_EXPECT_OK(
|
||||||
|
errctx,
|
||||||
|
akgl_text_loadfont("testfont", TEST_FONT_PATH, TEST_FONT_SIZE),
|
||||||
|
"loading the fixture font");
|
||||||
|
|
||||||
|
registered = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "testfont", NULL);
|
||||||
|
TEST_ASSERT(errctx, registered != NULL,
|
||||||
|
"akgl_text_loadfont did not place the font in AKGL_REGISTRY_FONT");
|
||||||
|
TEST_ASSERT(errctx, TTF_GetFontHeight(registered) > 0,
|
||||||
|
"the registered font reports height %d, expected a positive height",
|
||||||
|
TTF_GetFontHeight(registered));
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_text_loadfont(NULL, TEST_FONT_PATH, TEST_FONT_SIZE),
|
||||||
|
"loading a font under a NULL name");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_text_loadfont("nullpath", NULL, TEST_FONT_SIZE),
|
||||||
|
"loading a font from a NULL path");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKGL_ERR_SDL,
|
||||||
|
akgl_text_loadfont("missing", "assets/no_such_font.ttf", TEST_FONT_SIZE),
|
||||||
|
"loading a font that does not exist");
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_text_measure(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
int w = 0;
|
||||||
|
int h = 0;
|
||||||
|
int onecharw = 0;
|
||||||
|
int onecharh = 0;
|
||||||
|
int emptyw = 0;
|
||||||
|
int emptyh = 0;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
// One cell. This is the measurement a character grid is built from.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "A", &onecharw, &onecharh),
|
||||||
|
"measuring a single character");
|
||||||
|
TEST_ASSERT(errctx, onecharw > 0,
|
||||||
|
"one character measured %d wide, expected a positive width", onecharw);
|
||||||
|
TEST_ASSERT(errctx, onecharh == TTF_GetFontHeight(testfont),
|
||||||
|
"one character measured %d high, expected the font height %d",
|
||||||
|
onecharh, TTF_GetFontHeight(testfont));
|
||||||
|
|
||||||
|
// The font is monospaced, so four cells are exactly four times one.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "AAAA", &w, &h),
|
||||||
|
"measuring four characters");
|
||||||
|
TEST_ASSERT(errctx, w == (onecharw * 4),
|
||||||
|
"four characters measured %d wide, expected %d", w, onecharw * 4);
|
||||||
|
TEST_ASSERT(errctx, h == onecharh,
|
||||||
|
"four characters on one line measured %d high, expected %d", h, onecharh);
|
||||||
|
|
||||||
|
// ...and every character advances by the same amount, which is what
|
||||||
|
// makes a fixed grid legitimate in the first place.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "W", &w, &h), "measuring a wide glyph");
|
||||||
|
TEST_ASSERT(errctx, w == onecharw,
|
||||||
|
"'W' measured %d wide but 'A' measured %d in a monospaced font", w, onecharw);
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "i", &w, &h), "measuring a narrow glyph");
|
||||||
|
TEST_ASSERT(errctx, w == onecharw,
|
||||||
|
"'i' measured %d wide but 'A' measured %d in a monospaced font", w, onecharw);
|
||||||
|
|
||||||
|
// The empty string is zero wide and still one line high, so a cursor
|
||||||
|
// sitting on an empty line has somewhere to be.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "", &emptyw, &emptyh),
|
||||||
|
"measuring the empty string");
|
||||||
|
TEST_ASSERT(errctx, emptyw == 0,
|
||||||
|
"the empty string measured %d wide, expected 0", emptyw);
|
||||||
|
TEST_ASSERT(errctx, emptyh == onecharh,
|
||||||
|
"the empty string measured %d high, expected the font height %d",
|
||||||
|
emptyh, onecharh);
|
||||||
|
|
||||||
|
// Measuring does not disturb the destinations it was not asked about.
|
||||||
|
w = -1;
|
||||||
|
h = -1;
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "hello", &w, &h),
|
||||||
|
"measuring a word");
|
||||||
|
TEST_ASSERT(errctx, w == (onecharw * 5),
|
||||||
|
"\"hello\" measured %d wide, expected %d", w, onecharw * 5);
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(NULL, "A", &w, &h),
|
||||||
|
"measuring with a NULL font");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, NULL, &w, &h),
|
||||||
|
"measuring a NULL string");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, "A", NULL, &h),
|
||||||
|
"measuring into a NULL width");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, "A", &w, NULL),
|
||||||
|
"measuring into a NULL height");
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_text_measure_wrapped(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
int w = 0;
|
||||||
|
int h = 0;
|
||||||
|
int onecharw = 0;
|
||||||
|
int flatw = 0;
|
||||||
|
int flath = 0;
|
||||||
|
int lineskip = 0;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_text_measure(testfont, "A", &onecharw, &flath));
|
||||||
|
lineskip = TTF_GetFontLineSkip(testfont);
|
||||||
|
TEST_ASSERT(errctx, lineskip > 0, "the font reports a line skip of %d", lineskip);
|
||||||
|
|
||||||
|
// A wrap length wide enough for the whole string measures the same as
|
||||||
|
// the unwrapped call.
|
||||||
|
CATCH(errctx, akgl_text_measure(testfont, "one two", &flatw, &flath));
|
||||||
|
TEST_EXPECT_OK(errctx,
|
||||||
|
akgl_text_measure_wrapped(testfont, "one two", flatw + onecharw, &w, &h),
|
||||||
|
"measuring a string that fits inside the wrap length");
|
||||||
|
TEST_ASSERT(errctx, w == flatw,
|
||||||
|
"an unwrapped measurement gave %d wide, expected %d", w, flatw);
|
||||||
|
TEST_ASSERT(errctx, h == flath,
|
||||||
|
"an unwrapped measurement gave %d high, expected %d", h, flath);
|
||||||
|
|
||||||
|
// Narrow enough to force a break at the space: two lines, and nothing
|
||||||
|
// wider than the wrap length.
|
||||||
|
TEST_EXPECT_OK(errctx,
|
||||||
|
akgl_text_measure_wrapped(testfont, "one two", onecharw * 4, &w, &h),
|
||||||
|
"measuring a string that has to wrap");
|
||||||
|
TEST_ASSERT(errctx, w <= (onecharw * 4),
|
||||||
|
"a wrapped measurement gave %d wide, past the %d wrap length",
|
||||||
|
w, onecharw * 4);
|
||||||
|
TEST_ASSERT(errctx, h >= (lineskip * 2),
|
||||||
|
"a wrapped measurement gave %d high, expected at least two lines (%d)",
|
||||||
|
h, lineskip * 2);
|
||||||
|
|
||||||
|
// Zero wraps on newlines only, so an embedded newline still costs a line
|
||||||
|
// and a long unbroken string does not.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_text_measure_wrapped(testfont, "one\ntwo", 0, &w, &h),
|
||||||
|
"measuring a string with an embedded newline");
|
||||||
|
TEST_ASSERT(errctx, h >= (lineskip * 2),
|
||||||
|
"an embedded newline measured %d high, expected at least two lines (%d)",
|
||||||
|
h, lineskip * 2);
|
||||||
|
TEST_ASSERT(errctx, w == (onecharw * 3),
|
||||||
|
"the longer of two three-character lines measured %d wide, expected %d",
|
||||||
|
w, onecharw * 3);
|
||||||
|
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_text_measure_wrapped(testfont, "one two", 0, &w, &h),
|
||||||
|
"measuring a string with no newline at wrap length zero");
|
||||||
|
TEST_ASSERT(errctx, h == flath,
|
||||||
|
"a string with no newline measured %d high at wrap length 0, expected %d",
|
||||||
|
h, flath);
|
||||||
|
|
||||||
|
// A negative wrap length is refused rather than silently treated as
|
||||||
|
// "never wrap", which is what SDL_ttf does with it.
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||||
|
akgl_text_measure_wrapped(testfont, "one two", -1, &w, &h),
|
||||||
|
"measuring at a negative wrap length");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_text_measure_wrapped(NULL, "A", 100, &w, &h),
|
||||||
|
"measuring wrapped with a NULL font");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_text_measure_wrapped(testfont, NULL, 100, &w, &h),
|
||||||
|
"measuring a NULL string wrapped");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_text_measure_wrapped(testfont, "A", 100, NULL, &h),
|
||||||
|
"measuring wrapped into a NULL width");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_text_measure_wrapped(testfont, "A", 100, &w, NULL),
|
||||||
|
"measuring wrapped into a NULL height");
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||||
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_error_init());
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
SDL_Init(0),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"Couldn't initialize SDL: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
TTF_Init(),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"Couldn't initialize the font engine: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
testfont = TTF_OpenFont(TEST_FONT_PATH, TEST_FONT_SIZE);
|
||||||
|
FAIL_ZERO_BREAK(
|
||||||
|
errctx,
|
||||||
|
testfont,
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"Couldn't open %s: %s",
|
||||||
|
TEST_FONT_PATH,
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
CATCH(errctx, test_text_loadfont());
|
||||||
|
CATCH(errctx, test_text_measure());
|
||||||
|
CATCH(errctx, test_text_measure_wrapped());
|
||||||
|
} CLEANUP {
|
||||||
|
if ( testfont != NULL ) {
|
||||||
|
TTF_CloseFont(testfont);
|
||||||
|
}
|
||||||
|
TTF_Quit();
|
||||||
|
SDL_Quit();
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
}
|
||||||
165
tests/tilemap.c
165
tests/tilemap.c
@@ -20,13 +20,15 @@ akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void)
|
|||||||
int propnum;
|
int propnum;
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
gamemap = &_akgl_gamemap;
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||||
snprintf(
|
snprintf(
|
||||||
(char *)&tmpstr->data,
|
(char *)&tmpstr->data,
|
||||||
AKGL_MAX_STRING_LENGTH,
|
AKGL_MAX_STRING_LENGTH,
|
||||||
"%s%s",
|
"%s%s",
|
||||||
SDL_GetBasePath(),
|
SDL_GetBasePath(),
|
||||||
"assets/snippets/test_tilemap_akgl_get_json_tilemap_property.json"
|
"assets/snippets/test_tilemap_get_json_tilemap_property.json"
|
||||||
);
|
);
|
||||||
jsondoc = json_load_file((char *)&tmpstr->data, 0, (json_error_t *)&jsonerr);
|
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);
|
FAIL_ZERO_BREAK(errctx, jsondoc, AKERR_NULLPOINTER, "Failure loading json fixture: %s", (char *)jsonerr.text);
|
||||||
@@ -85,32 +87,34 @@ akerr_ErrorContext *test_akgl_tilemap_compute_tileset_offsets(void)
|
|||||||
10 // Tile 2 Y
|
10 // Tile 2 Y
|
||||||
};
|
};
|
||||||
int *comparison_ptrs[8] = {
|
int *comparison_ptrs[8] = {
|
||||||
&gamemap.tilesets[0].tile_offsets[0][0], // Tile 0 X
|
&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[0][1], // Tile 0 Y
|
||||||
&gamemap.tilesets[0].tile_offsets[1][0], // Tile 1 X
|
&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[1][1], // Tile 0 Y
|
||||||
&gamemap.tilesets[0].tile_offsets[2][0], // Tile 2 X
|
&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[2][1], // Tile 2 Y
|
||||||
&gamemap.tilesets[0].tile_offsets[3][0], // Tile 3 X
|
&gamemap->tilesets[0].tile_offsets[3][0], // Tile 3 X
|
||||||
&gamemap.tilesets[0].tile_offsets[3][0], // Tile 3 Y
|
&gamemap->tilesets[0].tile_offsets[3][1], // Tile 3 Y
|
||||||
};
|
};
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||||
gamemap.tilesets[0].tilecount = 4;
|
gamemap->tilesets[0].tilecount = 4;
|
||||||
gamemap.tilesets[0].columns = 2;
|
gamemap->tilesets[0].columns = 2;
|
||||||
gamemap.tilesets[0].firstgid = 1;
|
gamemap->tilesets[0].firstgid = 1;
|
||||||
gamemap.tilesets[0].imageheight = 20;
|
gamemap->tilesets[0].imageheight = 20;
|
||||||
gamemap.tilesets[0].imagewidth = 20;
|
gamemap->tilesets[0].imagewidth = 20;
|
||||||
gamemap.tilesets[0].tilecount = 4;
|
gamemap->tilesets[0].tilecount = 4;
|
||||||
gamemap.tilesets[0].tileheight = 10;
|
gamemap->tilesets[0].tileheight = 10;
|
||||||
gamemap.tilesets[0].tilewidth = 10;
|
gamemap->tilesets[0].tilewidth = 10;
|
||||||
gamemap.tilesets[0].spacing = 0;
|
gamemap->tilesets[0].spacing = 0;
|
||||||
gamemap.tilesets[0].margin = 0;
|
gamemap->tilesets[0].margin = 0;
|
||||||
|
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(&gamemap, 0));
|
gamemap = &_akgl_gamemap;
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
|
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(gamemap, 0));
|
||||||
// Tile 0 X offset
|
// Tile 0 X offset
|
||||||
for ( i = 0; i < 8; i++ ) {
|
for ( i = 0; i < 8; i++ ) {
|
||||||
FAIL_NONZERO_BREAK(
|
FAIL_NONZERO_BREAK(
|
||||||
@@ -139,15 +143,18 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_objects(void)
|
|||||||
json_error_t errdata;
|
json_error_t errdata;
|
||||||
akgl_Actor *testactor;
|
akgl_Actor *testactor;
|
||||||
|
|
||||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
gamemap = &_akgl_gamemap;
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
||||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
||||||
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
|
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);
|
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_value(doc, "layers", &layers));
|
||||||
CATCH(errctx, akgl_get_json_array_index_object(layers, 1, &objectlayer));
|
CATCH(errctx, akgl_get_json_array_index_object(layers, 1, &objectlayer));
|
||||||
CATCH(errctx, akgl_tilemap_load_layer_objects(&gamemap, objectlayer, 1));
|
CATCH(errctx, akgl_tilemap_load_layer_objects(gamemap, objectlayer, 1, pathstr));
|
||||||
|
|
||||||
testactor = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL);
|
testactor = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL);
|
||||||
FAIL_ZERO_BREAK(
|
FAIL_ZERO_BREAK(
|
||||||
@@ -186,21 +193,24 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
|
|||||||
json_t *tiledata = NULL;
|
json_t *tiledata = NULL;
|
||||||
json_error_t errdata;
|
json_error_t errdata;
|
||||||
|
|
||||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
gamemap = &_akgl_gamemap;
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
||||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
||||||
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
|
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);
|
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_value(doc, "layers", &layers));
|
||||||
CATCH(errctx, akgl_get_json_array_index_object(layers, 0, &tilelayer));
|
CATCH(errctx, akgl_get_json_array_index_object(layers, 0, &tilelayer));
|
||||||
CATCH(errctx, akgl_get_json_array_value(tilelayer, "data", &tiledata));
|
CATCH(errctx, akgl_get_json_array_value(tilelayer, "data", &tiledata));
|
||||||
CATCH(errctx, akgl_tilemap_load_layer_tile(&gamemap, tilelayer, 0));
|
CATCH(errctx, akgl_tilemap_load_layer_tile(gamemap, tilelayer, 0, pathstr));
|
||||||
if ( (gamemap.layers[0].data[0] != 1) ||
|
if ( (gamemap->layers[0].data[0] != 1) ||
|
||||||
(gamemap.layers[0].data[1] != 2) ||
|
(gamemap->layers[0].data[1] != 2) ||
|
||||||
(gamemap.layers[0].data[2] != 3) ||
|
(gamemap->layers[0].data[2] != 3) ||
|
||||||
(gamemap.layers[0].data[3] != 4) ) {
|
(gamemap->layers[0].data[3] != 4) ) {
|
||||||
FAIL_BREAK(errctx, AKERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)");
|
FAIL_BREAK(errctx, AKERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)");
|
||||||
}
|
}
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
@@ -223,58 +233,61 @@ akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
|
|||||||
json_error_t errdata;
|
json_error_t errdata;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
gamemap = &_akgl_gamemap;
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
||||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
||||||
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
|
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);
|
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
|
||||||
CATCH(errctx, akgl_tilemap_load_layers(&gamemap, doc));
|
CATCH(errctx, akgl_tilemap_load_layers(gamemap, doc, pathstr));
|
||||||
FAIL_NONZERO_BREAK(
|
FAIL_NONZERO_BREAK(
|
||||||
errctx,
|
errctx,
|
||||||
(gamemap.numlayers != 3),
|
(gamemap->numlayers != 3),
|
||||||
AKERR_VALUE,
|
AKERR_VALUE,
|
||||||
"Map layer count incorrect"
|
"Map layer count incorrect"
|
||||||
);
|
);
|
||||||
for ( i = 0; i < gamemap.numlayers; i++ ) {
|
for ( i = 0; i < gamemap->numlayers; i++ ) {
|
||||||
if ( (gamemap.layers[i].opacity != 1) ||
|
if ( (gamemap->layers[i].opacity != 1) ||
|
||||||
(gamemap.layers[i].visible != true) ||
|
(gamemap->layers[i].visible != true) ||
|
||||||
(gamemap.layers[i].id != (i + 1)) ||
|
(gamemap->layers[i].id != (i + 1)) ||
|
||||||
(gamemap.layers[i].x != 0) ||
|
(gamemap->layers[i].x != 0) ||
|
||||||
(gamemap.layers[i].y != 0) ) {
|
(gamemap->layers[i].y != 0) ) {
|
||||||
FAIL(errctx, AKERR_VALUE, "Map layer data loaded incorrectly (see gdb)");
|
FAIL(errctx, AKERR_VALUE, "Map layer data loaded incorrectly (see gdb)");
|
||||||
goto _test_akgl_tilemap_load_layers_cleanup;
|
goto _test_akgl_tilemap_load_layers_cleanup;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Layer 2 should have 1 object loaded
|
// Layer 2 should have 1 object loaded
|
||||||
if ( (gamemap.layers[1].objects[0].actorptr != SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL)) ||
|
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].name[0] != '\0' ) ||
|
||||||
(gamemap.layers[1].objects[1].id != 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)");
|
FAIL_BREAK(errctx, AKERR_VALUE, "Map layer 2 should have 1 loaded object (testactor) and nothing else (see gdb)");
|
||||||
}
|
}
|
||||||
// Layer 1 and 3 should have no objects
|
// Layer 1 and 3 should have no objects
|
||||||
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
|
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
|
||||||
if ( gamemap.layers[0].objects[i].id != 0 ) {
|
if ( gamemap->layers[0].objects[i].id != 0 ) {
|
||||||
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
|
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
|
||||||
goto _test_akgl_tilemap_load_layers_cleanup;
|
goto _test_akgl_tilemap_load_layers_cleanup;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
|
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
|
||||||
if ( gamemap.layers[2].objects[i].id != 0 ) {
|
if ( gamemap->layers[2].objects[i].id != 0 ) {
|
||||||
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
|
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
|
||||||
goto _test_akgl_tilemap_load_layers_cleanup;
|
goto _test_akgl_tilemap_load_layers_cleanup;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Layers 1 and 3 should have tile data
|
// Layers 1 and 3 should have tile data
|
||||||
if ( (gamemap.layers[0].data[0] != 1) ||
|
if ( (gamemap->layers[0].data[0] != 1) ||
|
||||||
(gamemap.layers[0].data[1] != 2) ||
|
(gamemap->layers[0].data[1] != 2) ||
|
||||||
(gamemap.layers[0].data[2] != 3) ||
|
(gamemap->layers[0].data[2] != 3) ||
|
||||||
(gamemap.layers[0].data[3] != 4) ||
|
(gamemap->layers[0].data[3] != 4) ||
|
||||||
(gamemap.layers[2].data[0] != 0) ||
|
(gamemap->layers[2].data[0] != 0) ||
|
||||||
(gamemap.layers[2].data[1] != 5) ||
|
(gamemap->layers[2].data[1] != 5) ||
|
||||||
(gamemap.layers[2].data[2] != 0) ||
|
(gamemap->layers[2].data[2] != 0) ||
|
||||||
(gamemap.layers[2].data[3] != 6)
|
(gamemap->layers[2].data[3] != 6)
|
||||||
) {
|
) {
|
||||||
FAIL_BREAK(errctx, AKERR_VALUE, "Map layers 1 and 3 should have tile data but it is incorrect");
|
FAIL_BREAK(errctx, AKERR_VALUE, "Map layers 1 and 3 should have tile data but it is incorrect");
|
||||||
}
|
}
|
||||||
@@ -299,40 +312,43 @@ akerr_ErrorContext *test_akgl_tilemap_load_tilesets(void)
|
|||||||
json_error_t errdata;
|
json_error_t errdata;
|
||||||
SDL_Texture *image = NULL;
|
SDL_Texture *image = NULL;
|
||||||
|
|
||||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
gamemap = &_akgl_gamemap;
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
||||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
||||||
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
|
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);
|
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
|
||||||
CATCH(errctx, akgl_tilemap_load_tilesets(&gamemap, doc));
|
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");
|
FAIL_NONZERO_BREAK(errctx, (gamemap->numtilesets != 1), AKERR_VALUE, "Incorrect number of tilesets loaded for map");
|
||||||
if ( (gamemap.tilesets[0].columns != 48 ) ||
|
if ( (gamemap->tilesets[0].columns != 48 ) ||
|
||||||
(gamemap.tilesets[0].firstgid != 1) ||
|
(gamemap->tilesets[0].firstgid != 1) ||
|
||||||
(gamemap.tilesets[0].imageheight != 576) ||
|
(gamemap->tilesets[0].imageheight != 576) ||
|
||||||
(gamemap.tilesets[0].imagewidth != 768) ||
|
(gamemap->tilesets[0].imagewidth != 768) ||
|
||||||
(gamemap.tilesets[0].margin != 0) ||
|
(gamemap->tilesets[0].margin != 0) ||
|
||||||
(gamemap.tilesets[0].spacing != 0) ||
|
(gamemap->tilesets[0].spacing != 0) ||
|
||||||
(gamemap.tilesets[0].tilecount != 1728) ||
|
(gamemap->tilesets[0].tilecount != 1728) ||
|
||||||
(gamemap.tilesets[0].tileheight != 16) ||
|
(gamemap->tilesets[0].tileheight != 16) ||
|
||||||
(gamemap.tilesets[0].tilewidth != 16) ) {
|
(gamemap->tilesets[0].tilewidth != 16) ) {
|
||||||
FAIL_BREAK(errctx, AKERR_VALUE, "Tileset loaded with incorrect values");
|
FAIL_BREAK(errctx, AKERR_VALUE, "Tileset loaded with incorrect values");
|
||||||
}
|
}
|
||||||
FAIL_NONZERO_BREAK(
|
FAIL_NONZERO_BREAK(
|
||||||
errctx,
|
errctx,
|
||||||
strcmp((char *)&gamemap.tilesets[0].name, "World_A1"),
|
strcmp((char *)&gamemap->tilesets[0].name, "World_A1"),
|
||||||
AKERR_VALUE,
|
AKERR_VALUE,
|
||||||
"Tileset loaded with incorrect name");
|
"Tileset loaded with incorrect name");
|
||||||
|
|
||||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/World_A1.png");
|
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/World_A1.png");
|
||||||
image = IMG_LoadTexture(renderer, (char *)&pathstr->data);
|
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&pathstr->data);
|
||||||
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
||||||
|
|
||||||
CATCH(
|
CATCH(
|
||||||
errctx,
|
errctx,
|
||||||
akgl_render_and_compare(
|
akgl_render_and_compare(
|
||||||
gamemap.tilesets[0].texture,
|
gamemap->tilesets[0].texture,
|
||||||
image,
|
image,
|
||||||
0, 0, 768, 576,
|
0, 0, 768, 576,
|
||||||
"test_akgl_tilemap_loaded_tileset.png")
|
"test_akgl_tilemap_loaded_tileset.png")
|
||||||
@@ -354,10 +370,12 @@ akerr_ErrorContext *test_akgl_tilemap_load(void)
|
|||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_tilemap_load("assets/testmap.tmj", &gamemap));
|
gamemap = &_akgl_gamemap;
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
|
CATCH(errctx, akgl_tilemap_load("assets/testmap.tmj", gamemap));
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
@@ -369,6 +387,8 @@ akerr_ErrorContext *test_akgl_tilemap_draw(void)
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
gamemap = &_akgl_gamemap;
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
@@ -381,6 +401,8 @@ akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void)
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
gamemap = &_akgl_gamemap;
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
@@ -392,15 +414,19 @@ int main(void)
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_error_init());
|
||||||
|
gamemap = &_akgl_gamemap;
|
||||||
|
renderer = &_akgl_renderer;
|
||||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||||
|
|
||||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
||||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 768, 576, 0, &window, &renderer)) {
|
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 768, 576, 0, &window, &renderer->sdl_renderer)) {
|
||||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||||
}
|
}
|
||||||
|
renderer->draw_texture = &akgl_render_2d_draw_texture;
|
||||||
|
|
||||||
CATCH(errctx, akgl_registry_init());
|
CATCH(errctx, akgl_registry_init());
|
||||||
CATCH(errctx, akgl_heap_init());
|
CATCH(errctx, akgl_heap_init());
|
||||||
@@ -422,4 +448,3 @@ int main(void)
|
|||||||
} FINISH_NORETURN(errctx);
|
} FINISH_NORETURN(errctx);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
tests/util.c
48
tests/util.c
@@ -1,5 +1,6 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
#include <akgl/util.h>
|
#include <akgl/util.h>
|
||||||
|
|
||||||
akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
||||||
@@ -10,7 +11,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_rectangle_points(NULL, NULL));
|
CATCH(errctx, akgl_rectangle_points(NULL, NULL));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with all NULL pointers");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with all NULL pointers");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
@@ -19,7 +20,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_rectangle_points(NULL, &testrect));
|
CATCH(errctx, akgl_rectangle_points(NULL, &testrect));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL SDL_FRect pointer");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL SDL_FRect pointer");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
@@ -28,7 +29,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_rectangle_points(&points, NULL));
|
CATCH(errctx, akgl_rectangle_points(&points, NULL));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL RectanglePoints pointer");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL RectanglePoints pointer");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
@@ -63,7 +64,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_math(void)
|
|||||||
points.bottomright.y != 32 ) {
|
points.bottomright.y != 32 ) {
|
||||||
FAIL_BREAK(
|
FAIL_BREAK(
|
||||||
errctx,
|
errctx,
|
||||||
AKERR_BEHAVIOR,
|
AKGL_ERR_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}}",
|
"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.topleft.x, points.topleft.y,
|
||||||
points.topright.x, points.topright.y,
|
points.topright.x, points.topright.y,
|
||||||
@@ -87,7 +88,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, NULL));
|
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, NULL));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(*, *, NULL) failed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, *, NULL) failed");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
@@ -96,7 +97,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, NULL, &testcollide));
|
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, NULL, &testcollide));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(*, NULL, *) failed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, NULL, *) failed");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
@@ -105,7 +106,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_collide_point_rectangle(NULL, &testrectpoints, &testcollide));
|
CATCH(errctx, akgl_collide_point_rectangle(NULL, &testrectpoints, &testcollide));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, *, *) failed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, *, *) failed");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
@@ -114,7 +115,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_collide_point_rectangle(NULL, NULL, NULL));
|
CATCH(errctx, akgl_collide_point_rectangle(NULL, NULL, NULL));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, NULL, NULL) failed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, NULL, NULL) failed");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
@@ -142,14 +143,14 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void)
|
|||||||
CATCH(errctx, akgl_rectangle_points(&testrectpoints, &testrect));
|
CATCH(errctx, akgl_rectangle_points(&testrectpoints, &testrect));
|
||||||
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
|
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
|
||||||
if ( testcollide == false ) {
|
if ( testcollide == false ) {
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||||
}
|
}
|
||||||
|
|
||||||
testpoint.x = 48;
|
testpoint.x = 48;
|
||||||
testpoint.y = 48;
|
testpoint.y = 48;
|
||||||
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
|
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
|
||||||
if ( testcollide == true ) {
|
if ( testcollide == true ) {
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Invalid collision reported");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
|
||||||
}
|
}
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
@@ -167,7 +168,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, NULL));
|
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, NULL));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(*, *, NULL) failed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, *, NULL) failed");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
@@ -176,7 +177,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, NULL, &testcollide));
|
CATCH(errctx, akgl_collide_rectangles(&testrect1, NULL, &testcollide));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(*, NULL, *) failed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, NULL, *) failed");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
@@ -185,7 +186,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_collide_rectangles(NULL, &testrect2, &testcollide));
|
CATCH(errctx, akgl_collide_rectangles(NULL, &testrect2, &testcollide));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(NULL, *, *) failed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, *, *) failed");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
@@ -194,7 +195,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
|
|||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_collide_rectangles(NULL, NULL, NULL));
|
CATCH(errctx, akgl_collide_rectangles(NULL, NULL, NULL));
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(NULL, NULL, NULL) failed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, NULL, NULL) failed");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
@@ -222,14 +223,14 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
|||||||
// Collision overlapping on the top left
|
// Collision overlapping on the top left
|
||||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||||
if ( testcollide == false ) {
|
if ( testcollide == false ) {
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collision overlapping on the top right
|
// Collision overlapping on the top right
|
||||||
testrect1.x = 64;
|
testrect1.x = 64;
|
||||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||||
if ( testcollide == false ) {
|
if ( testcollide == false ) {
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collision overlapping on the bottom left
|
// Collision overlapping on the bottom left
|
||||||
@@ -237,7 +238,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
|||||||
testrect1.y = 32;
|
testrect1.y = 32;
|
||||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||||
if ( testcollide == false ) {
|
if ( testcollide == false ) {
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collision overlapping on the bottom right
|
// Collision overlapping on the bottom right
|
||||||
@@ -245,7 +246,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
|||||||
testrect1.y = 32;
|
testrect1.y = 32;
|
||||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||||
if ( testcollide == false ) {
|
if ( testcollide == false ) {
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collision overlapping the top edge
|
// Collision overlapping the top edge
|
||||||
@@ -255,7 +256,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
|||||||
testrect1.h = 32;
|
testrect1.h = 32;
|
||||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||||
if ( testcollide == false ) {
|
if ( testcollide == false ) {
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collision overlapping the left edge
|
// Collision overlapping the left edge
|
||||||
@@ -265,7 +266,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
|||||||
testrect1.h = 80;
|
testrect1.h = 80;
|
||||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||||
if ( testcollide == false ) {
|
if ( testcollide == false ) {
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collision overlapping the right edge
|
// Collision overlapping the right edge
|
||||||
@@ -275,7 +276,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
|||||||
testrect1.h = 80;
|
testrect1.h = 80;
|
||||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||||
if ( testcollide == false ) {
|
if ( testcollide == false ) {
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collision overlapping the bottom edge
|
// Collision overlapping the bottom edge
|
||||||
@@ -285,7 +286,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
|||||||
testrect1.h = 32;
|
testrect1.h = 32;
|
||||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||||
if ( testcollide == false ) {
|
if ( testcollide == false ) {
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not colliding
|
// Not colliding
|
||||||
@@ -295,7 +296,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
|||||||
testrect1.h = 16;
|
testrect1.h = 16;
|
||||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||||
if ( testcollide == true ) {
|
if ( testcollide == true ) {
|
||||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Invalid collision reported");
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
|
||||||
}
|
}
|
||||||
|
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
@@ -309,6 +310,7 @@ int main(void)
|
|||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_error_init());
|
||||||
CATCH(errctx, test_akgl_rectangle_points_nullpointers());
|
CATCH(errctx, test_akgl_rectangle_points_nullpointers());
|
||||||
CATCH(errctx, test_akgl_rectangle_points_math());
|
CATCH(errctx, test_akgl_rectangle_points_math());
|
||||||
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());
|
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());
|
||||||
|
|||||||
108
tests/version.c
Normal file
108
tests/version.c
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* @file version.c
|
||||||
|
* @brief Unit tests for the version macros and the linked-library accessor.
|
||||||
|
*
|
||||||
|
* These assert that the several places the version appears cannot drift: the
|
||||||
|
* string, the numeric components, and what the shared library reports at
|
||||||
|
* runtime all come from one project() call, and this is what proves it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akgl/error.h>
|
||||||
|
#include <akgl/version.h>
|
||||||
|
|
||||||
|
#include "testutil.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The version string and the numeric components must describe one version.
|
||||||
|
*
|
||||||
|
* They are separate substitutions in version.h.in, so a mangled template can
|
||||||
|
* leave them disagreeing and nothing else would notice.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *test_version_string_matches_components(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
char assembled[64];
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
snprintf(assembled, sizeof(assembled), "%d.%d.%d",
|
||||||
|
AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH);
|
||||||
|
TEST_ASSERT(e, strcmp(assembled, AKGL_VERSION) == 0,
|
||||||
|
"AKGL_VERSION is \"%s\" but the components assemble to \"%s\"",
|
||||||
|
AKGL_VERSION, assembled);
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
} FINISH(e, true);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The linked library must report the version its headers were generated from.
|
||||||
|
*
|
||||||
|
* In this build tree they are the same tree, so this can only fail if the test
|
||||||
|
* picked up an installed libakgl off LD_LIBRARY_PATH instead of the one just
|
||||||
|
* built -- which is precisely the mispairing the accessor exists to catch.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *test_version_linked_matches_compiled(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
TEST_ASSERT(e, akgl_version() != NULL, "akgl_version returned NULL");
|
||||||
|
TEST_ASSERT(e, strcmp(akgl_version(), AKGL_VERSION) == 0,
|
||||||
|
"linked libakgl reports \"%s\" but the headers say \"%s\"",
|
||||||
|
akgl_version(), AKGL_VERSION);
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
} FINISH(e, true);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief AKGL_VERSION_AT_LEAST must order versions correctly at the boundaries.
|
||||||
|
*
|
||||||
|
* Checked against the current version rather than fixed literals, so the test
|
||||||
|
* does not have to be rewritten every time the version is bumped.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *test_version_at_least_boundaries(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
TEST_ASSERT(e,
|
||||||
|
AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH),
|
||||||
|
"AKGL_VERSION_AT_LEAST rejected the current version");
|
||||||
|
TEST_ASSERT(e,
|
||||||
|
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH + 1),
|
||||||
|
"AKGL_VERSION_AT_LEAST accepted a later patch");
|
||||||
|
TEST_ASSERT(e,
|
||||||
|
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR + 1, 0),
|
||||||
|
"AKGL_VERSION_AT_LEAST accepted a later minor");
|
||||||
|
TEST_ASSERT(e,
|
||||||
|
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR + 1, 0, 0),
|
||||||
|
"AKGL_VERSION_AT_LEAST accepted a later major");
|
||||||
|
TEST_ASSERT(e,
|
||||||
|
AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, 0),
|
||||||
|
"AKGL_VERSION_AT_LEAST rejected an earlier patch");
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
} FINISH(e, true);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_error_init());
|
||||||
|
CATCH(errctx, test_version_string_matches_components());
|
||||||
|
CATCH(errctx, test_version_linked_matches_compiled());
|
||||||
|
CATCH(errctx, test_version_at_least_boundaries());
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
}
|
||||||
@@ -48,12 +48,11 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
|||||||
strcpy((char *)&game.name, "charviewer");
|
strcpy((char *)&game.name, "charviewer");
|
||||||
strcpy((char *)&game.version, "0.0.1");
|
strcpy((char *)&game.version, "0.0.1");
|
||||||
strcpy((char *)&game.uri, "net.aklabs.libakgl.charviewer");
|
strcpy((char *)&game.uri, "net.aklabs.libakgl.charviewer");
|
||||||
game.screenwidth = 640;
|
|
||||||
game.screenheight = 480;
|
|
||||||
|
|
||||||
CATCH(errctx, akgl_game_init());
|
CATCH(errctx, akgl_game_init());
|
||||||
CATCH(errctx, akgl_heap_init());
|
CATCH(errctx, akgl_set_property("game.screenwidth", "640"));
|
||||||
CATCH(errctx, akgl_registry_init());
|
CATCH(errctx, akgl_set_property("game.screenheight", "480"));
|
||||||
|
CATCH(errctx, akgl_render_init2d(renderer));
|
||||||
|
CATCH(errctx, akgl_physics_init_null(physics));
|
||||||
|
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
@@ -94,7 +93,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
|||||||
AKGL_REGISTRY_CHARACTER,
|
AKGL_REGISTRY_CHARACTER,
|
||||||
"little guy",
|
"little guy",
|
||||||
NULL);
|
NULL);
|
||||||
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKERR_REGISTRY, "Can't load character 'little guy' from the registry");
|
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKGL_ERR_REGISTRY, "Can't load character 'little guy' from the registry");
|
||||||
actorptr->movement_controls_face = false;
|
actorptr->movement_controls_face = false;
|
||||||
actorptr->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN);
|
actorptr->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN);
|
||||||
actorptr->x = 320;
|
actorptr->x = 320;
|
||||||
@@ -145,9 +144,9 @@ SDL_AppResult SDL_AppIterate(void *appstate)
|
|||||||
AKGL_BITMASK_CLEAR(opflags.flags);
|
AKGL_BITMASK_CLEAR(opflags.flags);
|
||||||
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_UPDATE);
|
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_UPDATE);
|
||||||
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_RENDER);
|
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_RENDER);
|
||||||
akgl_draw_background(game.screenwidth, game.screenheight);
|
akgl_draw_background((int)camera->w, (int)camera->h);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_tilemap_draw(renderer, (akgl_Tilemap *)&gamemap, &camera, i));
|
CATCH(errctx, akgl_tilemap_draw(gamemap, camera, i));
|
||||||
SDL_EnumerateProperties(AKGL_REGISTRY_ACTOR, &akgl_registry_iterate_actor, (void *)&opflags);
|
SDL_EnumerateProperties(AKGL_REGISTRY_ACTOR, &akgl_registry_iterate_actor, (void *)&opflags);
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
@@ -155,7 +154,7 @@ SDL_AppResult SDL_AppIterate(void *appstate)
|
|||||||
LOG_ERROR(errctx);
|
LOG_ERROR(errctx);
|
||||||
return SDL_APP_FAILURE;
|
return SDL_APP_FAILURE;
|
||||||
} FINISH_NORETURN(errctx);
|
} FINISH_NORETURN(errctx);
|
||||||
SDL_RenderPresent(renderer);
|
SDL_RenderPresent(renderer->sdl_renderer);
|
||||||
return SDL_APP_CONTINUE;
|
return SDL_APP_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user