Compare commits
99 Commits
feature/re
...
93e8e4afa4
| Author | SHA1 | Date | |
|---|---|---|---|
|
93e8e4afa4
|
|||
|
5dfa49482c
|
|||
|
76c6240280
|
|||
|
fb58bb01b0
|
|||
|
80fdf2e098
|
|||
|
af304dc2f9
|
|||
|
8a920860c5
|
|||
|
bcd49fc5b1
|
|||
|
c6227545a6
|
|||
|
f35443e84d
|
|||
|
57bf1c7649
|
|||
|
a3eada1b3f
|
|||
|
34a076b851
|
|||
|
42b53dcb20
|
|||
|
ede3452c49
|
|||
|
18399f2726
|
|||
|
54df954ed6
|
|||
|
55675cc9de
|
|||
|
f0858b0d38
|
|||
|
582008a411
|
|||
|
1066ac716e
|
|||
|
996cacb10c
|
|||
|
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
|
|||
|
314ce5e10d
|
|||
|
d87c5d2c20
|
|||
|
6314ad7f26
|
|||
|
8f613397d6
|
|||
|
980bbc56fb
|
|||
|
e3edd5b855
|
|||
|
f695a035c8
|
|||
|
73b1a4cab0
|
|||
|
aad196d84e
|
|||
|
9443fa8532
|
|||
|
dc2e88b72f
|
|||
|
53e4f5c14f
|
|||
|
b8dee456ca
|
|||
|
36dfd47a06
|
|||
|
23dbc7d985
|
|||
|
6f62e674d5
|
|||
|
8ae99120b5
|
|||
|
c79d93dd58
|
|||
|
ccd26494d9
|
|||
|
5439b8004b
|
|||
|
0f01126bad
|
|||
|
d928a8af0f
|
|||
|
4a02f0364f
|
|||
|
cc0916cd1f
|
|||
|
4c771227f5
|
|||
|
ff6b282112
|
|||
|
0bd1ae1df8
|
|||
|
e0a59e2447
|
|||
|
5dda86d887
|
|||
|
a0b2dda4cf
|
|||
|
359ae23414
|
|||
|
f416cb5dee
|
|||
|
ef24d7b843
|
|||
|
87a5b1da21
|
|||
|
c3847160da
|
|||
|
284ffd7b4a
|
|||
|
90cbf41d75
|
|||
|
6763b5629f
|
|||
|
f475dfb6ee
|
|||
|
7cff27f035
|
|||
| 0601a8d767 | |||
| 4b901886a2 | |||
| 62ba23a1a0 | |||
| 93bc3addfe |
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))))
|
||||
210
.gitea/workflows/ci.yaml
Normal file
210
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,210 @@
|
||||
name: libakgl CI Build
|
||||
run-name: ${{ gitea.actor }} libakgl test
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
cmake_build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y \
|
||||
cmake doxygen gcc gcovr pkg-config \
|
||||
libasound2-dev libfreetype-dev libharfbuzz-dev \
|
||||
libpng-dev libtiff-dev libwebp-dev \
|
||||
libudev-dev libx11-dev libxcursor-dev libxext-dev \
|
||||
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
|
||||
libxss-dev libxtst-dev
|
||||
- name: Configure and build
|
||||
run: |
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DAKGL_COVERAGE=ON
|
||||
cmake --build build --parallel
|
||||
- name: Build API documentation
|
||||
run: doxygen Doxyfile
|
||||
# The perf suites run here too -- they are ordinary CTest tests -- but this
|
||||
# is a Debug build with coverage instrumentation, where a timing is a
|
||||
# measurement of gcov. AKGL_BENCH_SCALE cuts their iteration counts so
|
||||
# they contribute path coverage without spending ten minutes proving
|
||||
# nothing; benchutil.h already refuses to enforce budgets in an
|
||||
# unoptimized build. The performance job below is where the timings are
|
||||
# taken and the budgets are checked.
|
||||
- name: Test (JUnit)
|
||||
env:
|
||||
AKGL_BENCH_SCALE: '0.02'
|
||||
run: |
|
||||
export LD_LIBRARY_PATH="$PWD/build:$PWD/build/deps/SDL:$PWD/build/deps/SDL_image:$PWD/build/deps/SDL_mixer:$PWD/build/deps/SDL_ttf"
|
||||
ctest \
|
||||
--test-dir build \
|
||||
-E '^character$' \
|
||||
--output-on-failure \
|
||||
--output-junit "$(pwd)/ctest-junit.xml"
|
||||
- name: Publish test results
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
with:
|
||||
report_paths: 'ctest-junit.xml'
|
||||
annotate_only: true
|
||||
detailed_summary: true
|
||||
include_passed: true
|
||||
fail_on_failure: 'true'
|
||||
- name: Upload coverage reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: code-coverage
|
||||
path: build/coverage/
|
||||
if-no-files-found: warn
|
||||
|
||||
# The benchmarks, in the only build where their numbers mean anything.
|
||||
# tests/benchutil.h enforces a budget per measurement only when it is compiled
|
||||
# optimized and running at full scale, which is exactly this job and no other:
|
||||
# the coverage job above is Debug, and the memory job below is under valgrind.
|
||||
# A budget is roughly ten times the baseline recorded in PERFORMANCE.md, which
|
||||
# is what makes a shared, slower runner viable -- it catches an algorithmic
|
||||
# regression and ignores a busy machine. If a budget still proves flaky here,
|
||||
# raise that one budget with a measurement behind it and re-record the
|
||||
# baseline; do not drop the gate.
|
||||
performance:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y \
|
||||
cmake gcc pkg-config \
|
||||
libasound2-dev libfreetype-dev libharfbuzz-dev \
|
||||
libpng-dev libtiff-dev libwebp-dev \
|
||||
libudev-dev libx11-dev libxcursor-dev libxext-dev \
|
||||
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
|
||||
libxss-dev libxtst-dev
|
||||
- name: Configure and build
|
||||
run: |
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cmake --build build --parallel
|
||||
# --verbose rather than --output-on-failure: a passing benchmark's output
|
||||
# is the entire point of running it, and --output-on-failure prints
|
||||
# nothing at all for a green run. pipefail because the step ends in a
|
||||
# pipe, whose status would otherwise be tee's -- always 0 -- and a blown
|
||||
# budget would be reported as a passing step.
|
||||
- name: Benchmarks (JUnit)
|
||||
run: |
|
||||
set -o pipefail
|
||||
export LD_LIBRARY_PATH="$PWD/build:$PWD/build/deps/SDL:$PWD/build/deps/SDL_image:$PWD/build/deps/SDL_mixer:$PWD/build/deps/SDL_ttf"
|
||||
ctest \
|
||||
--test-dir build \
|
||||
-L perf \
|
||||
--verbose \
|
||||
--output-junit "$(pwd)/perf-junit.xml" \
|
||||
2>&1 | tee "$(pwd)/perf-baseline.txt"
|
||||
- name: Publish benchmark results
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
with:
|
||||
report_paths: 'perf-junit.xml'
|
||||
annotate_only: true
|
||||
detailed_summary: true
|
||||
include_passed: true
|
||||
fail_on_failure: 'true'
|
||||
# Kept whether the job passed or failed: the tables are the point. A run
|
||||
# that went red says which measurement moved, and a run that went green is
|
||||
# the next baseline if somebody re-records PERFORMANCE.md.
|
||||
- name: Upload benchmark tables
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: performance-baseline
|
||||
path: perf-baseline.txt
|
||||
if-no-files-found: warn
|
||||
|
||||
# Every suite under valgrind. No memory-check programs of their own: the perf
|
||||
# suites scale themselves down when they detect valgrind, which turns them
|
||||
# into the broadest path coverage in the tree at a cost this job can afford.
|
||||
#
|
||||
# This job gates. A definite leak, a read past the end of an allocation, or a
|
||||
# branch on uninitialised memory fails the build on the push that introduced
|
||||
# it -- not on the day somebody gets around to caring. The six findings this
|
||||
# job had on its first run were fixed to make that possible rather than
|
||||
# excused into a warning.
|
||||
memory_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install memory-check dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y \
|
||||
cmake gcc pkg-config valgrind \
|
||||
libasound2-dev libfreetype-dev libharfbuzz-dev \
|
||||
libpng-dev libtiff-dev libwebp-dev \
|
||||
libudev-dev libx11-dev libxcursor-dev libxext-dev \
|
||||
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
|
||||
libxss-dev libxtst-dev
|
||||
# RelWithDebInfo rather than Debug: valgrind needs the symbols, and -O0
|
||||
# would leave every inlined frame in the stacks it prints.
|
||||
- name: Configure and build
|
||||
run: |
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cmake --build build --parallel
|
||||
# The character suite is excluded here for the same reason as in the
|
||||
# coverage job: it fails deliberately, and a failing suite would be
|
||||
# reported as "valgrind found nothing but ctest exited non-zero".
|
||||
- name: Memory check
|
||||
run: scripts/memcheck.sh -E '^character$'
|
||||
- name: Upload valgrind logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: valgrind-logs
|
||||
path: build/Testing/Temporary/MemoryChecker.*.log
|
||||
if-no-files-found: warn
|
||||
|
||||
mutation_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install mutation-test dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y \
|
||||
cmake gcc pkg-config python3 \
|
||||
libasound2-dev libfreetype-dev libharfbuzz-dev \
|
||||
libpng-dev libtiff-dev libwebp-dev \
|
||||
libudev-dev libx11-dev libxcursor-dev libxext-dev \
|
||||
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
|
||||
libxss-dev libxtst-dev
|
||||
# Keep CI bounded to a focused source file. Run the default target locally
|
||||
# for the complete libakgl-owned src/ tree.
|
||||
- name: Mutation testing
|
||||
run: |
|
||||
python3 scripts/mutation_test.py \
|
||||
--target src/staticstring.c \
|
||||
--junit mutation-junit.xml \
|
||||
--threshold 50
|
||||
- name: Publish mutation results
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
with:
|
||||
report_paths: 'mutation-junit.xml'
|
||||
annotate_only: true
|
||||
detailed_summary: true
|
||||
include_passed: true
|
||||
fail_on_failure: 'false'
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1 +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*
|
||||
*~
|
||||
|
||||
# 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
|
||||
|
||||
24
.gitmodules
vendored
Normal file
24
.gitmodules
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
[submodule "deps/semver"]
|
||||
path = deps/semver
|
||||
url = git@github.com:h2non/semver.c.git
|
||||
[submodule "deps/SDL"]
|
||||
path = deps/SDL
|
||||
url = git@github.com:libsdl-org/SDL.git
|
||||
[submodule "deps/SDL_image"]
|
||||
path = deps/SDL_image
|
||||
url = git@github.com:libsdl-org/SDL_image.git
|
||||
[submodule "deps/SDL_mixer"]
|
||||
path = deps/SDL_mixer
|
||||
url = git@github.com:libsdl-org/SDL_mixer.git
|
||||
[submodule "deps/SDL_ttf"]
|
||||
path = deps/SDL_ttf
|
||||
url = git@github.com:libsdl-org/SDL_ttf.git
|
||||
[submodule "deps/libsdlerror"]
|
||||
path = deps/libakerror
|
||||
url = https://source.starfort.tech/andrew/libakerror.git
|
||||
[submodule "deps/libakstdlib"]
|
||||
path = deps/libakstdlib
|
||||
url = https://source.starfort.tech/andrew/libakstdlib.git
|
||||
[submodule "deps/jansson"]
|
||||
path = deps/jansson
|
||||
url = git@github.com:akheron/jansson.git
|
||||
371
AGENTS.md
Normal file
371
AGENTS.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
This is a C library intended to support the development of video games. Public C headers live in `include/akgl/`; keep declarations there aligned with their implementations in `src/`. Tests are standalone C programs under `tests/`, with JSON, image, and map fixtures in `tests/assets/`. The `util/` directory contains the `charviewer` utility and its sample assets. Third-party and companion libraries are vendored in `deps/`. Treat `build/` and generated `akgl.pc` files as build outputs rather than hand-maintained source; `include/akgl/SDL_GameControllerDB.h` is generated too, but is tracked on purpose — see below.
|
||||
|
||||
## Generated and Vendored Sources
|
||||
|
||||
### `include/akgl/SDL_GameControllerDB.h` is generated, and is tracked deliberately
|
||||
|
||||
`mkcontrollermappings.sh` regenerates this header by fetching the community
|
||||
controller database from `raw.githubusercontent.com`. **It is committed to the
|
||||
repository on purpose**: it is the offline fallback that keeps the library
|
||||
buildable if upstream is renamed, rate-limited, taken down, or simply
|
||||
unreachable from the build machine. Do not delete it, do not add it to
|
||||
`.gitignore`, and do not "clean up" the fact that a generated file is tracked.
|
||||
|
||||
Working rules:
|
||||
|
||||
- **Never hand-edit it.** It is machine-written; changes belong in
|
||||
`mkcontrollermappings.sh`.
|
||||
- **Do not commit incidental regeneration churn.** The build regenerates the
|
||||
header on every run, and the script stamps `$(date)` into a comment, so an
|
||||
ordinary build leaves the file modified with nothing of substance changed.
|
||||
Revert that before committing: `git checkout -- include/akgl/SDL_GameControllerDB.h`.
|
||||
- **Update it as a deliberate, standalone commit** when you actually want newer
|
||||
mappings, so the diff is reviewable and bisectable.
|
||||
- **Never commit a copy with `AKGL_SDL_GAMECONTROLLER_DB_LEN 0`.** That is the
|
||||
signature of a failed fetch, not an empty upstream — see the defect note
|
||||
below. Check the constant before staging this file.
|
||||
- Formatting tooling ignores it: `scripts/reindent.sh` and the pre-commit hook
|
||||
both skip it.
|
||||
|
||||
> **Known defect (tracked in `TODO.md`).** The safety net is currently
|
||||
> self-defeating. `mkcontrollermappings.sh` has no `set -e` and does not check
|
||||
> `curl`'s exit status, so when the fetch fails it writes a header with
|
||||
> `AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty array **over the good tracked
|
||||
> copy, and exits 0**. Because CMake re-runs the generator on every build, an
|
||||
> offline build silently destroys the very fallback the file exists to provide.
|
||||
> Until that is fixed, treat a dirty `SDL_GameControllerDB.h` after a build as
|
||||
> suspect and check the length constant before committing it.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
Configure an out-of-tree development build:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
```
|
||||
|
||||
Build all library, utility, and test targets:
|
||||
|
||||
```sh
|
||||
cmake --build build --parallel
|
||||
```
|
||||
|
||||
Run the complete test suite with failure details:
|
||||
|
||||
```sh
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
Run one test while iterating, for example `ctest --test-dir build -R sprite --output-on-failure`. The `rebuild.sh` script also installs into a developer-specific `/home/andrew/local` prefix and removes existing outputs; prefer the portable commands above unless that exact workflow is intended.
|
||||
|
||||
Run only the performance suites with `ctest --test-dir build -L perf --output-on-failure`, or leave them out of an ordinary run with `-LE perf`. They take about 30 seconds together, print a table of nanoseconds per operation, and fail only when a measurement exceeds a budget set at roughly ten times the recorded baseline. `AKGL_BENCH_SCALE` scales every iteration count — `AKGL_BENCH_SCALE=0.1 ctest --test-dir build -L perf` for a quick look — and below 1.0 the budgets are reported but not enforced. The recorded baseline and what it means are in `PERFORMANCE.md`.
|
||||
|
||||
Check for memory defects with `cmake --build build --target memcheck`, or
|
||||
`scripts/memcheck.sh` directly — it takes ctest's selection flags, so
|
||||
`scripts/memcheck.sh -R tilemap` and `scripts/memcheck.sh -LE perf` both work.
|
||||
It runs the suites that already exist under valgrind (`ctest -T memcheck`) with
|
||||
the headless drivers forced, and exits non-zero when valgrind finds a definite
|
||||
leak, an invalid access, or a read of uninitialised memory — which `ctest -T
|
||||
memcheck` on its own will not do. The whole run takes about thirty seconds
|
||||
because the perf suites scale themselves down under valgrind. Suppressions for
|
||||
third-party findings live in `scripts/valgrind.supp`; add one only when the
|
||||
finding genuinely cannot be fixed from this repository.
|
||||
|
||||
Run mutation testing with `cmake --build build --target mutation`. For a quick smoke run, use `scripts/mutation_test.py --target src/tilemap.c --max-mutants 10`; the harness mutates only a scratch copy and excludes the intentionally failing character test.
|
||||
|
||||
Generate HTML and Cobertura coverage reports with `cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug`, then build and run CTest. Reports are written to `build-coverage/coverage/`; this mode requires `gcovr` and GCC or Clang.
|
||||
|
||||
## Continuous integration
|
||||
|
||||
`.gitea/workflows/ci.yaml` runs four jobs on every push, and each one exists
|
||||
because the others cannot do its work:
|
||||
|
||||
| Job | Build | What it is for |
|
||||
|---|---|---|
|
||||
| `cmake_build` | Debug + `AKGL_COVERAGE=ON` | The unit suites and the coverage report. The perf suites run here too, at `AKGL_BENCH_SCALE=0.02`, for path coverage only — a timing taken under gcov is a measurement of gcov. |
|
||||
| `performance` | RelWithDebInfo | `ctest -L perf`. The only job where budgets are enforced, because `tests/benchutil.h` enforces them only when compiled optimized at full scale. Keeps the tables as an artifact. |
|
||||
| `memory_check` | RelWithDebInfo | `scripts/memcheck.sh`, every suite under valgrind. Gates: a definite leak, an invalid access or a branch on uninitialised memory fails the build. Keeps the valgrind logs as an artifact. |
|
||||
| `mutation_test` | Debug | One focused source file, so CI stays bounded. |
|
||||
|
||||
The `character` suite is excluded wherever a whole run is selected: it fails
|
||||
deliberately, and pinning current-but-wrong behavior is not what it is for.
|
||||
|
||||
## Coding Style
|
||||
|
||||
The canonical style is Emacs `cc-mode` **`stroustrup`**, with tabs enabled. This
|
||||
is not advisory — new and edited code must match what `cc-mode` produces, so that
|
||||
reindenting a region never shows up as a diff.
|
||||
|
||||
### Emacs setup
|
||||
|
||||
`.dir-locals.el` in the repository root already applies the style to every
|
||||
`c-mode` buffer, so nothing needs configuring by hand:
|
||||
|
||||
```elisp
|
||||
((c-mode . ((c-file-style . "stroustrup")
|
||||
(indent-tabs-mode . t)
|
||||
(tab-width . 8)
|
||||
(fill-column . 100)
|
||||
(require-final-newline . t))))
|
||||
```
|
||||
|
||||
Interactively: `C-x h C-M-\` (`mark-whole-buffer` + `indent-region`) reindents
|
||||
the buffer. A correctly formatted file is a fixed point of that operation —
|
||||
reindenting must produce no change.
|
||||
|
||||
### Reindenting from the command line
|
||||
|
||||
```sh
|
||||
scripts/reindent.sh # reindent every tracked C source in place
|
||||
scripts/reindent.sh src/tilemap.c # reindent only the named files
|
||||
scripts/reindent.sh --check # list non-conforming files, change nothing
|
||||
```
|
||||
|
||||
`--check` exits 1 when something needs reindenting and 2 if Emacs is missing, so
|
||||
it is usable from CI. The script drives Emacs in batch mode through
|
||||
`scripts/reindent.el`, which reindents, normalises leading whitespace to the
|
||||
canonical tab/space mix, strips trailing whitespace, and ensures a final
|
||||
newline. `include/akgl/SDL_GameControllerDB.h` is generated and always skipped.
|
||||
|
||||
Note that `reindent.el` deliberately does **not** use Emacs' `tabify`: that
|
||||
function's `tabify-regexp` is `" [ \t]+"`, which is not anchored to the start of
|
||||
a line, so it rewrites runs of spaces anywhere and destroys the hand-aligned
|
||||
value columns in the bit-flag tables in `actor.h` and `iterator.h`. Only leading
|
||||
whitespace is ever rewritten.
|
||||
|
||||
### The pre-commit hook
|
||||
|
||||
`scripts/hooks/pre-commit` reindents staged C sources before the commit is
|
||||
written. Enable it once per clone:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath scripts/hooks
|
||||
```
|
||||
|
||||
It inspects the *staged* content rather than the working tree, so what lands in
|
||||
the commit is what was checked. When a file needs reindenting it is fixed in the
|
||||
working tree and re-staged — but only if the index and working tree agree for
|
||||
that file. If they differ, re-staging would sweep unstaged work into the commit,
|
||||
so the hook stops and tells you to reindent and stage it yourself. It steps
|
||||
aside during a merge, and warns rather than blocking if Emacs is unavailable.
|
||||
`git commit --no-verify` bypasses it.
|
||||
|
||||
For reference, `cc-mode` defines the style as:
|
||||
|
||||
```elisp
|
||||
("stroustrup"
|
||||
(c-basic-offset . 4)
|
||||
(c-comment-only-line-offset . 0)
|
||||
(c-offsets-alist . ((statement-block-intro . +)
|
||||
(substatement-open . 0)
|
||||
(substatement-label . 0)
|
||||
(label . 0)
|
||||
(statement-cont . +))))
|
||||
```
|
||||
|
||||
### Indentation and whitespace
|
||||
|
||||
- **4 columns per level** (`c-basic-offset` 4).
|
||||
- **Tabs are 8 columns wide and are used for indentation** (`indent-tabs-mode`
|
||||
`t`, `tab-width` 8). Emacs emits the largest possible run of tabs and pads the
|
||||
remainder with spaces, which produces this ladder. Editors that expand tabs, or
|
||||
that assume a 4-column tab, will silently corrupt it:
|
||||
|
||||
| Depth | Columns | Bytes |
|
||||
|---|---|---|
|
||||
| 1 | 4 | 4 spaces |
|
||||
| 2 | 8 | `TAB` |
|
||||
| 3 | 12 | `TAB` + 4 spaces |
|
||||
| 4 | 16 | `TAB` `TAB` |
|
||||
| 5 | 20 | `TAB` `TAB` + 4 spaces |
|
||||
|
||||
- No trailing whitespace. Files end with a single newline.
|
||||
- `case` labels sit at the same column as their `switch` (`label . 0`).
|
||||
- Continuation lines of a wrapped expression indent one level (`statement-cont . +`).
|
||||
|
||||
Most of `src/` already conforms. The known non-conforming files are
|
||||
`src/json_helpers.c`, `src/util.c` (from `akgl_rectangle_points` onward),
|
||||
`src/assets.c`, `src/staticstring.c`, parts of `src/actor.c`, and the headers
|
||||
`include/akgl/util.h` and `include/akgl/staticstring.h` — all of which use a
|
||||
2-column offset. Convert a file to the canonical style in its own commit, not
|
||||
mixed into a behavioral change.
|
||||
|
||||
### Braces
|
||||
|
||||
- **Function bodies open on their own line, in column 0.**
|
||||
- **Control statements keep the brace on the same line**, one space before it.
|
||||
- **`else`, `else if`, and `while` of a `do`/`while` stay on the closing-brace
|
||||
line**: `} else {`, not a bare `else` on the next line. Note that this is a
|
||||
house convention rather than something `cc-mode` enforces — the `stroustrup`
|
||||
style governs indentation only and will not move a brace or an `else` for you.
|
||||
- **Always brace, even single-statement bodies.**
|
||||
|
||||
```c
|
||||
akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
|
||||
{
|
||||
if ( obj->curSpriteFrameId == 0 ) {
|
||||
obj->curSpriteReversing = false;
|
||||
} else if ( obj->parent != NULL ) {
|
||||
obj->curSpriteFrameId -= 1;
|
||||
} else {
|
||||
obj->curSpriteFrameId += 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Spacing
|
||||
|
||||
- **Spaces inside control-flow parentheses**: `if ( x == y ) {`, `while ( done == false ) {`,
|
||||
`for ( i = 0; i < len; i++ ) {`. This is the dominant existing convention
|
||||
(140 sites to 7) and `cc-mode` will not add or remove it.
|
||||
- No space between a function name and its argument list, at both call and
|
||||
definition sites, and no padding inside call parentheses: `SDL_Log("x %d", n)`.
|
||||
- Binary operators and assignment are surrounded by single spaces; unary
|
||||
operators are not separated from their operand.
|
||||
- **The pointer `*` binds to the identifier**, not the type: `char *name`,
|
||||
`akgl_Actor **dest`. Never `char* name`.
|
||||
- When a call is too long for one line, put each argument on its own line
|
||||
indented one level past the callee, with the closing `)` on its own line. This
|
||||
is the established shape for the `FAIL_*` and `CATCH` macros:
|
||||
|
||||
```c
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj),
|
||||
AKERR_KEY,
|
||||
"Unable to add actor to registry"
|
||||
);
|
||||
```
|
||||
|
||||
### No repository-wide formatter
|
||||
|
||||
There is no `clang-format` or linter wired into the build, and `cc-mode`'s
|
||||
indentation engine is not exactly reproducible by `clang-format`. Do not
|
||||
introduce one without discussion, and do not reformat code you are not otherwise
|
||||
changing — unrelated whitespace churn makes review harder and is the reason the
|
||||
style drifted in the first place.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- **Public functions**: `akgl_<subsystem>_<verb>`, all lower snake_case —
|
||||
`akgl_actor_set_character`, `akgl_heap_next_string`. No camelCase, and never
|
||||
embed a type name (`akgl_Actor_cmhf_left_on` is wrong; it should be
|
||||
`akgl_actor_cmhf_left_on`).
|
||||
- **Public types**: `akgl_TypeName` — `akgl_Actor`, `akgl_SpriteSheet`,
|
||||
`akgl_PhysicsBackend`. Every type exported from a header takes the prefix;
|
||||
bare names like `point` and `RectanglePoints` are defects, not precedent.
|
||||
- **Constants and macros**: `AKGL_UPPER_SNAKE_CASE`. A constant belongs to the
|
||||
subsystem it describes: a character limit is `AKGL_CHARACTER_MAX_*`, not
|
||||
`AKGL_SPRITE_MAX_CHARACTER_*`. Name a constant for what its value *is* — a
|
||||
nanoseconds-per-millisecond scale factor is `AKGL_TIME_ONEMS_NS`.
|
||||
- **Exported globals** take the `akgl_` prefix like any other public symbol. Do
|
||||
not add bare names (`renderer`, `camera`, `window`), leading-underscore names
|
||||
(reserved at file scope), or SCREAMING_SNAKE names for mutable objects —
|
||||
`AKGL_UPPER_SNAKE_CASE` is for constants.
|
||||
- **`static` helpers drop the `akgl_` prefix**, which exists only to avoid
|
||||
external collisions.
|
||||
- **Include guards**: `_AKGL_<FILE>_H_`, matching the file name. Guard names
|
||||
without the project prefix risk colliding with system headers.
|
||||
- **Parameter names must match between the declaration and the definition** —
|
||||
Doxygen publishes the header spelling. Conventional names: `dest` for an
|
||||
output parameter (not `dst`), `self` for a backend receiver, `obj` for the
|
||||
instance being initialized or inspected, `e` for an incoming error context to
|
||||
be inspected.
|
||||
- **The local error context is named `errctx`** (92 sites to 45). New code uses
|
||||
`errctx`; convert `e` when you touch a function for other reasons.
|
||||
- **Header/source pairs are named by feature**: `include/akgl/sprite.h` and
|
||||
`src/sprite.c`.
|
||||
|
||||
## Error-Handling Protocol
|
||||
|
||||
The `akerror` control-flow macros have a shape that must be followed exactly,
|
||||
because the failure mode is silent.
|
||||
|
||||
- Inside an `ATTEMPT` block use the **`_BREAK`** variants (`FAIL_ZERO_BREAK`,
|
||||
`FAIL_NONZERO_BREAK`, `FAIL_BREAK`) and `CATCH`. Outside it use the
|
||||
**`_RETURN`** variants.
|
||||
- **Never use a `*_RETURN` macro inside an `ATTEMPT` block.** It returns past the
|
||||
`CLEANUP` block, so every release, `fclose`, and free in `CLEANUP` is skipped.
|
||||
This has already caused a heap-string leak on the success path of
|
||||
`akgl_get_json_tilemap_property`.
|
||||
- `CLEANUP` must precede `PROCESS`. Transposing them moves the cleanup body into
|
||||
the `PROCESS` switch, where it runs only when an error context exists.
|
||||
- `CATCH` reports failure by `break`ing, which binds to the innermost enclosing
|
||||
loop or `switch`. A `CATCH` written directly inside a `while` exits the loop
|
||||
rather than the function — put the `ATTEMPT` block inside the loop.
|
||||
- **Never `return` from inside a `HANDLE` block either.** `FINISH` ends with
|
||||
`RELEASE_ERROR`, so leaving before it means the handled context is never given
|
||||
back to `AKERR_ARRAY_ERROR` — one leaked slot per call, and the 129th call
|
||||
aborts the process with "Unable to pull an error context from the array!".
|
||||
`SUCCEED_RETURN` is safe because it releases first; a bare `return`, or a
|
||||
`return f(...)` that tail-calls the fallback, is not. Set a flag in the
|
||||
`HANDLE` block and act on it after `FINISH`. This has already cost a
|
||||
process-killing leak in `akgl_path_relative`; see TODO.md, "Performance".
|
||||
- Validate every pointer parameter before dereferencing it, including the ones a
|
||||
sibling function happens not to check.
|
||||
|
||||
## API Surface
|
||||
|
||||
Every function with external linkage must be declared in a header, and every
|
||||
declaration must have a definition. A non-`static` function that appears in no
|
||||
header is still in the ABI but is unreachable by callers; a declaration with no
|
||||
definition is a link error for anyone compiling against the header alone. If a
|
||||
function is exposed only so tests can reach it, declare it under the existing
|
||||
"part of the internal API" comment block in the relevant header.
|
||||
|
||||
Headers must be self-contained: include what you use, so that a translation unit
|
||||
including a single `akgl` header compiles without a particular include order.
|
||||
Within the project use the angled form, `#include <akgl/sibling.h>`.
|
||||
|
||||
Declare no-argument functions as `(void)`, not `()`.
|
||||
|
||||
## Rules
|
||||
|
||||
- Add yourself (agent program name, model name and version) as a co-author on every commit message
|
||||
- Avoid dynamic memory allocation. Use the `akgl_heap_*` methods to retrieve necessary objects at runtime, and to release them when done. If the akgl heap facilities don't provide the kind of object needed, create a new heap layer to support that type of object.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Tests use simple executable return codes and are registered through CTest in `CMakeLists.txt`; there is no declared coverage threshold. Add focused tests as `tests/<feature>.c`, create a matching `test_<feature>` target, and register it with `add_test`. Put reusable fixtures in `tests/assets/` and keep paths compatible with tests launched from the build tree. Coverage mode wraps the suite in a CTest fixture so counters are reset before tests and reports are generated afterward.
|
||||
|
||||
### Performance suites
|
||||
|
||||
`tests/perf.c` (nothing that draws) and `tests/perf_render.c` (everything that
|
||||
does) are benchmarks, built and registered like any other suite but listed in
|
||||
`AKGL_PERF_SUITES` so they carry the `perf` label and a longer timeout. The
|
||||
harness is `tests/benchutil.h`. Three rules keep the numbers honest, and all
|
||||
three were learned by getting them wrong first:
|
||||
|
||||
- **Nothing that checks an error goes inside the clock.** `PASS` and `CATCH`
|
||||
call `akerr_valid_error_address`, which walks `AKERR_ARRAY_ERROR` — more work
|
||||
than several of the calls being measured. Use `BENCH_LOOP`, which stashes the
|
||||
context and stops at the first failure, and hand it to `PASS` afterwards.
|
||||
- **Flush the renderer before stopping the clock.** SDL batches: a `draw_*` call
|
||||
queues a command and returns. `BENCH_FLUSH_STOP` in `tests/perf_render.c` is
|
||||
there because the first version of that suite measured queueing, reported a
|
||||
tilemap frame 250 times faster than it is, and paid the real cost at teardown
|
||||
inside `SDL_DestroyTexture`.
|
||||
- **A drawing benchmark needs a raw-SDL control that does the same pixel work.**
|
||||
Without one there is no way to separate what libakgl costs from what the
|
||||
rasterizer costs, and the answer is not the one you would guess — see
|
||||
`PERFORMANCE.md`.
|
||||
|
||||
Budgets are per-operation ceilings at roughly ten times the recorded baseline;
|
||||
they are enforced only in an optimized build at full scale. When a change moves
|
||||
a number for a good reason, re-record the baseline in `PERFORMANCE.md` in the
|
||||
same commit and say why it moved.
|
||||
|
||||
These two suites are also the memory-check vehicle. `tests/benchutil.h` detects
|
||||
valgrind from `LD_PRELOAD` and drops the scale to
|
||||
`AKGL_BENCH_VALGRIND_SCALE`, so the same binaries walk every path once instead
|
||||
of a hundred thousand times, and the timings a checked run prints are labelled
|
||||
as meaningless. **Do not add memory-check suites**: a new path worth checking
|
||||
belongs in a benchmark, where it gets both.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Recent commits use concise, imperative summaries such as `Fix a scale bug...` and often explain related changes in one sentence. Keep each commit scoped and describe user-visible behavior. Pull requests should summarize the change, identify affected modules, list the CMake/CTest commands run, and link relevant issues. Include screenshots only for rendering, map, or `charviewer` changes.
|
||||
506
CMakeLists.txt
506
CMakeLists.txt
@@ -1,90 +1,478 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(sdl3game 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.4.0 LANGUAGES C)
|
||||
|
||||
# Memory checking reuses the suites that already exist -- `ctest -T memcheck`
|
||||
# runs every registered test under valgrind -- rather than adding programs of its
|
||||
# own. The perf suites carry most of the weight there: they are the only things
|
||||
# in the tree that load assets, draw a scene, and run a frame loop in one
|
||||
# process, and tests/benchutil.h drops their iteration counts by three orders of
|
||||
# magnitude when it finds itself under valgrind, which turns a benchmark into
|
||||
# exactly the broad, once-through path coverage a leak check wants.
|
||||
#
|
||||
# These have to be set before include(CTest): that is what writes them into
|
||||
# DartConfiguration.tcl, and a memcheck run reads them from there.
|
||||
find_program(MEMORYCHECK_COMMAND valgrind)
|
||||
# Set with FORCE, but only when empty. include(CTest) declares both of these as
|
||||
# empty cache entries, so a build tree configured before this block existed has
|
||||
# them already and a plain set(... CACHE ...) would be ignored -- the values
|
||||
# would silently never reach DartConfiguration.tcl. Guarding on emptiness still
|
||||
# leaves a deliberate -DMEMORYCHECK_SUPPRESSIONS_FILE=... alone.
|
||||
if(NOT MEMORYCHECK_SUPPRESSIONS_FILE)
|
||||
set(MEMORYCHECK_SUPPRESSIONS_FILE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/scripts/valgrind.supp"
|
||||
CACHE FILEPATH "Suppressions for third-party findings the memcheck run cannot fix" FORCE)
|
||||
endif()
|
||||
# Definite losses only. "Still reachable" is every global SDL and FreeType keeps
|
||||
# for the process lifetime and says nothing about libakgl; "possibly lost" is
|
||||
# dominated by interior pointers into pools and thread stacks. Neither is worth
|
||||
# the false positives.
|
||||
if(NOT MEMORYCHECK_COMMAND_OPTIONS)
|
||||
set(MEMORYCHECK_COMMAND_OPTIONS
|
||||
"--leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite --track-origins=yes --num-callers=25"
|
||||
CACHE STRING "Options passed to the memory checker" FORCE)
|
||||
endif()
|
||||
|
||||
include(CTest)
|
||||
option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(SDL3 REQUIRED)
|
||||
find_package(SDL3_image REQUIRED)
|
||||
find_package(SDL3_mixer REQUIRED)
|
||||
find_package(sdlerror REQUIRED)
|
||||
find_package(jansson REQUIRED)
|
||||
find_package(box2d REQUIRED)
|
||||
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()
|
||||
|
||||
# Check for SDL3 using pkg-config
|
||||
pkg_check_modules(SDL3 REQUIRED sdl3)
|
||||
pkg_check_modules(SDL3_image REQUIRED sdl3-image)
|
||||
pkg_check_modules(SDL3_mixer REQUIRED sdl3-mixer)
|
||||
pkg_check_modules(jansson REQUIRED jansson)
|
||||
pkg_check_modules(sdlerror REQUIRED sdlerror)
|
||||
# Vendored projects own their test suites. Suppress their CTest registration so
|
||||
# the suite that runs contains only the targets built here. The override is
|
||||
# lifted again below, before this project registers its own tests.
|
||||
#
|
||||
# **Only when this project is top-level, and that guard is load-bearing.** CMake
|
||||
# exposes an overridden command as `_name` and chains exactly one level deep: a
|
||||
# second override rebinds `_add_test` to the *first* override and the builtin
|
||||
# becomes unreachable to everybody, forever. So two projects in one tree cannot
|
||||
# both shadow add_test() -- whoever goes second breaks, and an embedding
|
||||
# consumer that shadows it first sees its own registrations recurse until CMake
|
||||
# stops at depth 1000.
|
||||
#
|
||||
# An embedded libakgl therefore leaves the override alone and lets its consumer
|
||||
# suppress what it does not want; the consumer has to have that machinery
|
||||
# anyway, for libakerror and libakstdlib. libakstdlib guards its own shadow the
|
||||
# same way and for the same reason.
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(AKGL_SUPPRESS_DEPENDENCY_TESTS TRUE)
|
||||
function(add_test)
|
||||
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
|
||||
_add_test(${ARGV})
|
||||
endif()
|
||||
endfunction()
|
||||
function(set_tests_properties)
|
||||
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
|
||||
_set_tests_properties(${ARGV})
|
||||
endif()
|
||||
endfunction()
|
||||
endif()
|
||||
|
||||
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 one vendored dependency, if nobody has already declared it and the
|
||||
# submodule is actually checked out.
|
||||
#
|
||||
# A macro rather than a function on purpose: add_subdirectory() inside a
|
||||
# function runs with that function's variable scope, so every cache-ish variable
|
||||
# these projects set for their own subdirectories would be discarded on return.
|
||||
macro(akgl_add_vendored_dependency target dir)
|
||||
if(NOT TARGET ${target})
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/CMakeLists.txt")
|
||||
add_subdirectory(${dir} EXCLUDE_FROM_ALL)
|
||||
set(AKGL_VENDORED_DEPENDENCIES TRUE)
|
||||
endif()
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# Added on both paths, not just when this is the top-level project. Embedded
|
||||
# with add_subdirectory(), the submodules under deps/ are sitting right there --
|
||||
# the recursive clone the consumer just did put them there -- and refusing to
|
||||
# configure until SDL3 is installed system-wide is a failure with its own answer
|
||||
# three directories away. if(NOT TARGET ...) means a consumer that has already
|
||||
# declared one of these wins; the EXISTS check means a checkout without
|
||||
# submodules falls through to find_package below.
|
||||
akgl_add_vendored_dependency(jansson::jansson deps/jansson)
|
||||
akgl_add_vendored_dependency(akerror::akerror deps/libakerror)
|
||||
akgl_add_vendored_dependency(akstdlib::akstdlib deps/libakstdlib)
|
||||
akgl_add_vendored_dependency(SDL3::SDL3 deps/SDL)
|
||||
akgl_add_vendored_dependency(SDL3_image::SDL3_image deps/SDL_image)
|
||||
akgl_add_vendored_dependency(SDL3_mixer::SDL3_mixer deps/SDL_mixer)
|
||||
akgl_add_vendored_dependency(SDL3_ttf::SDL3_ttf deps/SDL_ttf)
|
||||
|
||||
# libakerror 1.0.0 sizes its own status-name registry; consumers no longer do.
|
||||
# libakgl claims its status codes at runtime in akgl_heap_init() instead.
|
||||
|
||||
set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE)
|
||||
|
||||
# Anything the vendored block did not supply has to come from the system.
|
||||
if(NOT (TARGET SDL3::SDL3 AND TARGET SDL3_image::SDL3_image AND
|
||||
TARGET SDL3_mixer::SDL3_mixer AND TARGET SDL3_ttf::SDL3_ttf AND
|
||||
TARGET akerror::akerror AND TARGET akstdlib::akstdlib AND
|
||||
TARGET jansson::jansson))
|
||||
# Only needed to locate installed copies; a fully vendored build does not
|
||||
# require pkg-config to be present at all.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
endif()
|
||||
|
||||
if(NOT TARGET SDL3::SDL3)
|
||||
find_package(SDL3 REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET SDL3_image::SDL3_image)
|
||||
find_package(SDL3_image REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET SDL3_mixer::SDL3_mixer)
|
||||
find_package(SDL3_mixer REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET SDL3_ttf::SDL3_ttf)
|
||||
find_package(SDL3_ttf REQUIRED)
|
||||
endif()
|
||||
# No version here: libakerror ships no akerrorConfigVersion.cmake, so asking
|
||||
# for one makes find_package reject every install. The floor is enforced by
|
||||
# the #error in include/akgl/error.h instead, which feature-tests
|
||||
# AKERR_FIRST_CONSUMER_STATUS.
|
||||
if(NOT TARGET akerror::akerror)
|
||||
find_package(akerror REQUIRED)
|
||||
endif()
|
||||
# 0.2 rather than bare: libakstdlib 0.2.0 ships an akstdlibConfigVersion.cmake
|
||||
# with SameMinorVersion compatibility, mirroring its soname, so this accepts
|
||||
# any 0.2.x and refuses 0.3 and 1.0. Unversioned, this path would silently
|
||||
# accept an ABI-incompatible libakstdlib.
|
||||
if(NOT TARGET akstdlib::akstdlib)
|
||||
find_package(akstdlib 0.2 REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET jansson::jansson)
|
||||
find_package(jansson)
|
||||
endif()
|
||||
|
||||
set(GAMECONTROLLERDB_H "include/akgl/SDL_GameControllerDB.h")
|
||||
set(prefix ${CMAKE_INSTALL_PREFIX})
|
||||
set(exec_prefix "\${prefix}")
|
||||
set(libdir "\${exec_prefix}/lib")
|
||||
set(includedir "\${prefix}/include")
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akgl.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc @ONLY)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/include/akgl/version.h.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h @ONLY)
|
||||
|
||||
# Tests use both relative paths and SDL_GetBasePath(), so stage fixtures beside
|
||||
# test executables in every out-of-tree build.
|
||||
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/tests/assets"
|
||||
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${GAMECONTROLLERDB_H}
|
||||
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/mkcontrollermappings.sh ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
COMMENT "Generating controller mappings ..."
|
||||
)
|
||||
|
||||
# Add include directories
|
||||
include_directories(${SDL3_INCLUDE_DIRS})
|
||||
add_library(sdl3game SHARED
|
||||
add_library(akgl SHARED
|
||||
deps/semver/semver.c
|
||||
src/actor.c
|
||||
src/actor_state_string_names.c
|
||||
src/audio.c
|
||||
src/text.c
|
||||
src/assets.c
|
||||
src/character.c
|
||||
src/draw.c
|
||||
src/error.c
|
||||
src/game.c
|
||||
src/controller.c
|
||||
src/heap.c
|
||||
src/json_helpers.c
|
||||
src/registry.c
|
||||
src/renderer.c
|
||||
src/physics.c
|
||||
src/sprite.c
|
||||
src/staticstring.c
|
||||
src/tilemap.c
|
||||
src/util.c
|
||||
src/version.c
|
||||
${GAMECONTROLLERDB_H}
|
||||
)
|
||||
|
||||
add_executable(charviewer util/charviewer.c)
|
||||
# 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()
|
||||
|
||||
add_executable(test_actor tests/actor.c)
|
||||
add_executable(test_bitmasks tests/bitmasks.c)
|
||||
add_executable(test_character tests/character.c)
|
||||
add_executable(test_registry tests/registry.c)
|
||||
add_executable(test_sprite tests/sprite.c)
|
||||
add_executable(test_staticstring tests/staticstring.c)
|
||||
add_executable(test_tilemap tests/tilemap.c)
|
||||
add_executable(test_util tests/util.c)
|
||||
add_test(NAME actor COMMAND test_actor)
|
||||
add_test(NAME bitmasks COMMAND test_bitmasks)
|
||||
add_test(NAME character COMMAND test_character)
|
||||
add_test(NAME registry COMMAND test_registry)
|
||||
add_test(NAME sprite COMMAND test_sprite)
|
||||
add_test(NAME staticstring COMMAND test_staticstring)
|
||||
add_test(NAME tilemap COMMAND test_tilemap)
|
||||
add_test(NAME util COMMAND test_util)
|
||||
set_target_properties(akgl PROPERTIES
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${AKGL_SOVERSION}
|
||||
)
|
||||
|
||||
add_library(akgl::akgl ALIAS akgl)
|
||||
|
||||
add_executable(charviewer util/charviewer.c)
|
||||
add_executable(akgl_test_semver_unit deps/semver/semver_unit.c)
|
||||
|
||||
# Every suite here is a standalone C program named tests/<name>.c, built as
|
||||
# akgl_test_<name> and registered with CTest under <name>.
|
||||
set(AKGL_TEST_SUITES
|
||||
actor
|
||||
audio
|
||||
bitmasks
|
||||
character
|
||||
controller
|
||||
draw
|
||||
error
|
||||
game
|
||||
headers
|
||||
heap
|
||||
json_helpers
|
||||
physics
|
||||
registry
|
||||
renderer
|
||||
sprite
|
||||
staticstring
|
||||
text
|
||||
tilemap
|
||||
util
|
||||
version
|
||||
)
|
||||
|
||||
# The performance suites are built and registered exactly like the unit suites,
|
||||
# but they are benchmarks: they drive hot paths for millions of iterations, print
|
||||
# a table of nanoseconds per operation, and fail only when a measurement exceeds
|
||||
# a budget set at roughly ten times the recorded baseline. They carry the `perf`
|
||||
# label, so `ctest -L perf` runs only them and `ctest -LE perf` leaves them out
|
||||
# of an ordinary run, and they get a much longer timeout for obvious reasons.
|
||||
#
|
||||
# Set AKGL_BENCH_SCALE in the environment to change how long they run --
|
||||
# `AKGL_BENCH_SCALE=0.1 ctest -L perf` for a tenth of the iterations. Below 1.0
|
||||
# the budgets are measured and reported but not enforced, because a short run is
|
||||
# a noisy one.
|
||||
set(AKGL_PERF_SUITES
|
||||
perf
|
||||
perf_render
|
||||
)
|
||||
|
||||
# The executables carry an akgl_ prefix but the CTest names do not: a vendored
|
||||
# dependency is free to ship its own tests/version.c, and libakstdlib now does.
|
||||
# Its target is created by add_subdirectory even though EXCLUDE_FROM_ALL keeps
|
||||
# it from being built, so an unprefixed test_version here is a configure error.
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
||||
add_executable(akgl_test_${suite} tests/${suite}.c)
|
||||
add_test(NAME ${suite} COMMAND akgl_test_${suite})
|
||||
endforeach()
|
||||
|
||||
add_test(NAME semver_unit COMMAND akgl_test_semver_unit)
|
||||
|
||||
# TIMEOUT is generous because CTest applies the same property to `ctest -T
|
||||
# memcheck`, where every suite runs under valgrind at something like twenty
|
||||
# times its normal cost. The unit suites finish in well under a second each
|
||||
# without it; the ceiling is there for the checked run, not the ordinary one.
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES} semver_unit
|
||||
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 300
|
||||
)
|
||||
|
||||
set_tests_properties(
|
||||
${AKGL_PERF_SUITES}
|
||||
PROPERTIES
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
|
||||
TIMEOUT 900
|
||||
LABELS perf
|
||||
)
|
||||
|
||||
# Specify include directories for the library's headers (if applicable)
|
||||
target_include_directories(sdl3game PUBLIC
|
||||
include/
|
||||
target_include_directories(akgl PUBLIC
|
||||
include/
|
||||
deps/semver/
|
||||
# akgl/version.h is generated by configure_file, so it lives in the build tree
|
||||
# rather than beside the headers it is included from.
|
||||
${CMAKE_CURRENT_BINARY_DIR}/include/
|
||||
)
|
||||
|
||||
target_link_libraries(test_actor PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
|
||||
target_link_libraries(test_bitmasks PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
|
||||
target_link_libraries(test_character PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
|
||||
target_link_libraries(test_registry PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
|
||||
target_link_libraries(test_sprite PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
|
||||
target_link_libraries(test_staticstring PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
|
||||
target_link_libraries(test_tilemap PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
|
||||
target_link_libraries(test_util PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
|
||||
if(AKGL_COVERAGE)
|
||||
target_compile_options(akgl PRIVATE --coverage -O0 -g)
|
||||
target_link_options(akgl PRIVATE --coverage)
|
||||
|
||||
target_link_libraries(charviewer PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
|
||||
set(AKGL_COVERAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/coverage")
|
||||
file(MAKE_DIRECTORY "${AKGL_COVERAGE_DIR}")
|
||||
|
||||
set(main_lib_dest "lib/sdl3game-${MY_LIBRARY_VERSION}")
|
||||
install(TARGETS sdl3game DESTINATION "lib/")
|
||||
install(FILES "include/sdl3game/actor.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/assets.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/character.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/draw.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/game.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/controller.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/heap.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/iterator.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/json_helpers.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/registry.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/sprite.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/staticstring.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/tilemap.h" DESTINATION "include/sdl3game/")
|
||||
install(FILES "include/sdl3game/util.h" DESTINATION "include/sdl3game/")
|
||||
add_test(
|
||||
NAME coverage_reset
|
||||
COMMAND ${GCOVR_EXECUTABLE}
|
||||
--root "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
--delete
|
||||
)
|
||||
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP akgl_coverage)
|
||||
# Any suite missing from this list runs outside the fixture and has its
|
||||
# counters discarded by coverage_reset. The perf suites are deliberately
|
||||
# outside it: an instrumented -O0 build measures gcov, not libakgl, and the
|
||||
# lines they cover are covered by the unit suites anyway.
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES} semver_unit
|
||||
PROPERTIES FIXTURES_REQUIRED akgl_coverage
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME coverage_report
|
||||
COMMAND ${GCOVR_EXECUTABLE}
|
||||
--root "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
--filter "${CMAKE_CURRENT_SOURCE_DIR}/src/"
|
||||
--xml-pretty
|
||||
--xml "${AKGL_COVERAGE_DIR}/coverage.xml"
|
||||
--html-details "${AKGL_COVERAGE_DIR}/index.html"
|
||||
)
|
||||
set_tests_properties(
|
||||
coverage_report
|
||||
PROPERTIES
|
||||
FIXTURES_CLEANUP akgl_coverage
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(akgl
|
||||
PUBLIC
|
||||
SDL3::SDL3
|
||||
SDL3_image::SDL3_image
|
||||
SDL3_mixer::SDL3_mixer
|
||||
SDL3_ttf::SDL3_ttf
|
||||
akstdlib::akstdlib
|
||||
akerror::akerror
|
||||
jansson::jansson
|
||||
)
|
||||
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
||||
target_link_libraries(akgl_test_${suite} PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_include_directories(akgl_test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||
endforeach()
|
||||
|
||||
target_link_libraries(charviewer PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
|
||||
# When the vendored SDL satellite libraries are built in-tree they land in per-
|
||||
# project subdirectories that are not on the loader's default search path, so a
|
||||
# freshly built test aborts before main() with "cannot open shared object file".
|
||||
# Bake those directories into the build-tree RPATH. Installed builds resolve the
|
||||
# same libraries through find_package and need no help, so this keys on whether
|
||||
# anything was actually vendored rather than on being the top-level project.
|
||||
if(AKGL_VENDORED_DEPENDENCIES)
|
||||
set(AKGL_VENDORED_RPATH
|
||||
"$<TARGET_FILE_DIR:SDL3::SDL3>"
|
||||
"$<TARGET_FILE_DIR:SDL3_image::SDL3_image>"
|
||||
"$<TARGET_FILE_DIR:SDL3_ttf::SDL3_ttf>"
|
||||
"$<TARGET_FILE_DIR:SDL3_mixer::SDL3_mixer>"
|
||||
"$<TARGET_FILE_DIR:akerror::akerror>"
|
||||
"$<TARGET_FILE_DIR:akstdlib::akstdlib>"
|
||||
)
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
||||
set_target_properties(akgl_test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
endforeach()
|
||||
set_target_properties(charviewer akgl PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
|
||||
# RPATH alone is not enough: LD_LIBRARY_PATH is searched first, so a developer
|
||||
# who has previously run rebuild.sh has an installed libakgl.so ahead of the
|
||||
# one under test. Prepend the build tree for the CTest run so the suite always
|
||||
# exercises what was just compiled.
|
||||
set(AKGL_TEST_LIBPATH
|
||||
"${CMAKE_CURRENT_BINARY_DIR}"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_image"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_ttf"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_mixer"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakerror"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakstdlib"
|
||||
)
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
|
||||
set(AKGL_TEST_ENV_MOD "")
|
||||
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
|
||||
list(APPEND AKGL_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
|
||||
endforeach()
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
||||
PROPERTIES ENVIRONMENT_MODIFICATION "${AKGL_TEST_ENV_MOD}"
|
||||
)
|
||||
else()
|
||||
string(REPLACE ";" ":" AKGL_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
||||
PROPERTIES ENVIRONMENT "LD_LIBRARY_PATH=${AKGL_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Mutation testing copies the repository to scratch space, applies one small
|
||||
# source change at a time, and verifies that the passing tests detect it. The
|
||||
# intentionally failing character test is excluded by the harness.
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
if(Python3_FOUND)
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(AKGL_MUTATION_TARGET mutation)
|
||||
else()
|
||||
set(AKGL_MUTATION_TARGET akgl_mutation)
|
||||
endif()
|
||||
add_custom_target(${AKGL_MUTATION_TARGET}
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
||||
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
USES_TERMINAL
|
||||
COMMENT "Running mutation tests (breaks a scratch copy, expects tests to fail)"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Memory checking runs the whole registered suite under valgrind. The wrapper
|
||||
# script exists because `ctest -T memcheck` records defects and still exits 0,
|
||||
# which cannot gate anything; it also forces the headless drivers, so the vendor
|
||||
# GPU stack is never loaded and never has to be suppressed.
|
||||
if(MEMORYCHECK_COMMAND)
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(AKGL_MEMCHECK_TARGET memcheck)
|
||||
else()
|
||||
set(AKGL_MEMCHECK_TARGET akgl_memcheck)
|
||||
endif()
|
||||
add_custom_target(${AKGL_MEMCHECK_TARGET}
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
AKGL_BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/memcheck.sh
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
USES_TERMINAL
|
||||
COMMENT "Running every test suite under valgrind (slow; the perf suites scale themselves down)"
|
||||
)
|
||||
endif()
|
||||
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/")
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h DESTINATION "include/akgl/")
|
||||
install(TARGETS akgl DESTINATION "lib/")
|
||||
install(FILES "deps/semver/semver.h" DESTINATION "include/")
|
||||
install(FILES "include/akgl/actor.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/types.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/text.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/assets.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/audio.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/character.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/error.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/draw.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/game.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/controller.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/heap.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/iterator.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/json_helpers.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/renderer.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/physics.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/registry.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/sprite.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/staticstring.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/tilemap.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/util.h" DESTINATION "include/akgl/")
|
||||
install(FILES ${GAMECONTROLLERDB_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
|
||||
344
PERFORMANCE.md
Normal file
344
PERFORMANCE.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# libakgl performance baseline
|
||||
|
||||
This is where the library actually spends its time, measured rather than guessed.
|
||||
The numbers below are the first recorded baseline: libakgl 0.3.0, at commit
|
||||
`f35443e` plus the perf suites themselves. Everything here is reproducible with
|
||||
two commands, and the suites that produced it are checked in as
|
||||
`tests/perf.c` and `tests/perf_render.c`.
|
||||
|
||||
Read it in this order if you only want the short version: **the frame budget** is
|
||||
the part that matters and **what the numbers say** is the argument. The six
|
||||
defects the suites turned up on the way — two of them process-killing — are in
|
||||
`TODO.md` under **Performance**, along with the targets these numbers are
|
||||
measured against.
|
||||
|
||||
## Reproducing it
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cmake --build build --parallel
|
||||
ctest --test-dir build -L perf --output-on-failure
|
||||
```
|
||||
|
||||
Both suites print a table and exit non-zero if any measurement blew its budget.
|
||||
They are ordinary CTest tests, so `ctest --test-dir build` runs them along with
|
||||
everything else; `ctest --test-dir build -LE perf` leaves them out when you only
|
||||
want the unit suites. `AKGL_BENCH_SCALE` scales every iteration count —
|
||||
`AKGL_BENCH_SCALE=0.1` for a quick look, `10` for a long one. Below 1.0 the
|
||||
budgets are reported but not enforced, because a short run is a noisy one.
|
||||
|
||||
The whole thing takes about 30 seconds: 4 s for `perf`, 25 s for `perf_render`.
|
||||
|
||||
## The machine
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| CPU | AMD Ryzen 5 7535HS, 6 cores / 12 threads, 4.6 GHz max |
|
||||
| Memory | 62 GiB |
|
||||
| OS | Linux 6.8.0-136-generic |
|
||||
| Compiler | gcc 13.3.0, `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (`-O2 -g`) |
|
||||
| SDL | vendored SDL 3.4.8, `dummy` video driver, `software` renderer |
|
||||
| Target | 640x480, offscreen |
|
||||
|
||||
One machine, one build type, one afternoon. Treat the absolute numbers as this
|
||||
laptop's and the *ratios* as the library's.
|
||||
|
||||
## How it is measured
|
||||
|
||||
- **Best of five.** Each benchmark runs five times and the harness keeps the
|
||||
fastest. The mean is the wrong statistic on a shared machine: every source of
|
||||
noise makes a run slower and none makes it faster.
|
||||
- **No error checking inside the clock.** `PASS` and `CATCH` call
|
||||
`akerr_valid_error_address`, which walks `AKERR_ARRAY_ERROR` — more work than
|
||||
several of the calls being measured. The timed loop stashes the context and
|
||||
checks it after the clock stops.
|
||||
- **The renderer is flushed inside the measurement.** SDL batches: a `draw_*`
|
||||
call queues a command and returns. The first version of this suite measured
|
||||
*queueing* and reported a tilemap frame at 65 µs; the deferred work then took
|
||||
114 seconds to come out at teardown, inside `SDL_DestroyTexture`. Every
|
||||
drawing benchmark now flushes before it stops the clock, and the numbers below
|
||||
are 250x larger and true.
|
||||
- **SDL's log output goes to a sink that discards it.** The library logs on
|
||||
paths this suite calls hundreds of thousands of times; timing a write to a
|
||||
terminal measures the terminal. What the *formatting* costs is measured
|
||||
deliberately, by the pair of actor-spawn benchmarks.
|
||||
- **Budgets are set at roughly 10x the measured baseline.** Loose enough that a
|
||||
busy machine does not turn CI red, tight enough that a linear scan becoming
|
||||
quadratic cannot hide. They are enforced only in an optimized build at full
|
||||
scale — a coverage build measures gcov, not libakgl.
|
||||
|
||||
**The renderer caveat, stated once and loudly.** These draw benchmarks run
|
||||
against SDL's *software* renderer. No shipped game does that. What a software
|
||||
renderer buys is that all the work stays in this process where it can be timed,
|
||||
and that the per-blit cost is at least honest about how much pixel traffic was
|
||||
asked for. Read every drawing number as *a count of work libakgl asked for*, not
|
||||
as a frame rate anyone would ship. That is exactly why the raw-SDL control rows
|
||||
exist: they do the same pixel work with none of the library in the path, so the
|
||||
difference between the two is libakgl's share, and that part *does* carry over
|
||||
to a GPU backend.
|
||||
|
||||
## Static footprint
|
||||
|
||||
libakgl does not call `malloc`. Every one of these arrays exists from process
|
||||
start whether the game uses one slot or all of them.
|
||||
|
||||
| Pool | Slots | Bytes each | Total |
|
||||
|---|---:|---:|---:|
|
||||
| `HEAP_ACTOR` | 64 | 400 | 25,600 |
|
||||
| `HEAP_SPRITE` | 1024 | 176 | 180,224 |
|
||||
| `HEAP_SPRITESHEET` | 1024 | 536 | 548,864 |
|
||||
| `HEAP_CHARACTER` | 256 | 184 | 47,104 |
|
||||
| `HEAP_STRING` | 256 | 4,100 | 1,049,600 |
|
||||
| **pools, total** | | | **1,851,392** |
|
||||
| `akgl_Tilemap` (one, as `_akgl_gamemap`) | 1 | 26,388,008 | 26,388,008 |
|
||||
| — of which layers | 16 | 1,120,296 | 17,924,736 |
|
||||
| — of which tilesets | 16 | 528,944 | 8,463,104 |
|
||||
|
||||
**28 MB of BSS before `main` runs, and 94% of it is one tilemap.** A layer is
|
||||
512x512 `int` cells whether the map is 512x512 or 2x2, and a tileset carries a
|
||||
65,536-entry offset table whether the image holds 65,536 tiles or 1,728. This is
|
||||
not a hypothetical cost: zeroing that struct is 1.37 ms, and `akgl_tilemap_load`
|
||||
pays it before it has read a byte of the map file.
|
||||
|
||||
## Results: everything that does not draw
|
||||
|
||||
`tests/perf.c`, full scale. `ns/op` is the best of five runs; `ops/sec` is its
|
||||
reciprocal.
|
||||
|
||||
| Benchmark | Unit | ns/op | ops/sec |
|
||||
|---|---|---:|---:|
|
||||
| `heap_next_actor`, empty pool | call | 4.0 | 248,392,158 |
|
||||
| `heap_next_actor`, one slot left | call | 36.9 | 27,077,696 |
|
||||
| heap string claim + release cycle | cycle | 49.8 | 20,084,967 |
|
||||
| `heap_next_string`, empty pool | call | 3.9 | 255,059,748 |
|
||||
| `heap_next_string`, one slot left | call | 250.9 | 3,986,007 |
|
||||
| `heap_release_string`, 4 KiB wipe | call | 47.2 | 21,191,918 |
|
||||
| `heap_init`, all five pools | call | 45,141 | 22,153 |
|
||||
| actor spawn + release, library logging on | actor | 202.3 | 4,942,862 |
|
||||
| actor spawn + release, logging suppressed | actor | 162.5 | 6,153,324 |
|
||||
| `akgl_actor_set_character`, registry lookup | call | 39.3 | 25,438,092 |
|
||||
| `akgl_set_property` | call | 96.9 | 10,316,516 |
|
||||
| `akgl_get_property`, 4 KiB copy | call | 85.3 | 11,722,134 |
|
||||
| `akgl_character_sprite_get`, state to sprite | call | 37.4 | 26,734,605 |
|
||||
| `akgl_actor_update`, animation advancing | actor | 68.4 | 14,609,892 |
|
||||
| `akgl_actor_update`, no sprite for state | actor | 616.5 | 1,621,937 |
|
||||
| `akgl_physics_simulate`, 64 live actors | frame | 1,216.8 | 821,841 |
|
||||
| `akgl_physics_simulate`, empty pool | frame | 63.9 | 15,650,101 |
|
||||
| logic frame: 64 updates + simulate | frame | 5,763.1 | 173,517 |
|
||||
| `akgl_rectangle_points` | call | 4.0 | 248,412,026 |
|
||||
| `akgl_collide_rectangles`, overlapping | call | 24.9 | 40,181,108 |
|
||||
| `akgl_collide_rectangles`, disjoint | call | 57.9 | 17,261,906 |
|
||||
| all-pairs collision sweep, 64 actors (2016 pairs) | sweep | 115,023.6 | 8,694 |
|
||||
| `akgl_string_initialize` | call | 32.1 | 31,197,432 |
|
||||
| `akgl_string_copy`, full length | call | 32.2 | 31,075,004 |
|
||||
| `json_load_file`, small document | load | 11,338.8 | 88,193 |
|
||||
| `akgl_get_json_string_value` | call | 40.7 | 24,552,958 |
|
||||
| `akgl_get_json_integer_value` | call | 13.8 | 72,704,562 |
|
||||
| `akgl_path_relative`, `realpath` on an existing file | call | 3,481.5 | 287,232 |
|
||||
|
||||
## Results: everything that draws
|
||||
|
||||
`tests/perf_render.c`, full scale, 640x480 software renderer. The two **control**
|
||||
rows are raw `SDL_RenderTexture` loops with no libakgl in the path.
|
||||
|
||||
| Benchmark | Unit | ns/op | ops/sec |
|
||||
|---|---|---:|---:|
|
||||
| `frame_start` + `frame_end` (clear + present) | frame | 23,161.7 | 43,175 |
|
||||
| `akgl_draw_point` | call | 132.5 | 7,547,141 |
|
||||
| `akgl_draw_line`, screen diagonal | call | 605.9 | 1,650,375 |
|
||||
| `akgl_draw_rect`, 200x150 outline | call | 520.8 | 1,920,055 |
|
||||
| `akgl_draw_filled_rect`, 200x150 | call | 43,695.1 | 22,886 |
|
||||
| `akgl_draw_circle`, radius 64 | call | 2,516.2 | 397,423 |
|
||||
| `akgl_draw_copy_region`, 64x64 readback | call | 954.7 | 1,047,471 |
|
||||
| `akgl_draw_paste_region`, 64x64 upload | call | 5,352.1 | 186,841 |
|
||||
| `akgl_draw_flood_fill`, full 640x480 target | call | 1,967,295.6 | 508 |
|
||||
| `akgl_text_measure`, 15 characters | call | 37.3 | 26,804,657 |
|
||||
| `akgl_text_rendertextat`, 15 characters | call | 12,601.7 | 79,354 |
|
||||
| `akgl_sprite_load_json`, sheet already loaded | load | 17,011.3 | 58,784 |
|
||||
| `akgl_character_load_json`, two mappings | load | 14,506.8 | 68,933 |
|
||||
| `akgl_tilemap_load` + release, fixture map | load | 11,881,190.8 | 84 |
|
||||
| zeroing one `akgl_Tilemap` | call | 1,372,396.7 | 729 |
|
||||
| `akgl_tilemap_compute_tileset_offsets`, 1728 tiles | call | 2,240.8 | 446,264 |
|
||||
| `akgl_tilemap_draw`, 40x30 tiles, 1 tileset | frame | 16,260,566.6 | 61 |
|
||||
| `akgl_tilemap_draw`, 40x30 tiles, 8 tilesets | frame | 16,395,182.4 | 61 |
|
||||
| **control**: raw SDL blits, one source tile | frame | 471,676.6 | 2,120 |
|
||||
| **control**: raw SDL blits, map order | frame | 16,229,501.0 | 62 |
|
||||
| `akgl_actor_render`, on camera | actor | 2,992.0 | 334,220 |
|
||||
| `draw_world`, 1200 tiles + 64 actors | frame | 16,484,493.6 | 61 |
|
||||
| `akgl_game_update`, full frame | frame | 16,576,902.2 | 60 |
|
||||
|
||||
## The frame budget
|
||||
|
||||
At 60 fps a frame is 16.67 ms. Here is where it goes for a 640x480 game with a
|
||||
full screen of 16-pixel tiles and 64 actors:
|
||||
|
||||
| Part of the frame | Cost | Share of 16.67 ms |
|
||||
|---|---:|---:|
|
||||
| Logic: 64 actor updates + one physics sweep | 0.006 ms | 0.03% |
|
||||
| All-pairs collision over 64 actors, if you do it | 0.115 ms | 0.7% |
|
||||
| Clear + present | 0.023 ms | 0.1% |
|
||||
| 64 actor renders (48x48 blits) | 0.191 ms | 1.1% |
|
||||
| 1200 tile blits | 16.26 ms | 97.6% |
|
||||
| Six lines of HUD text | 0.076 ms | 0.5% |
|
||||
|
||||
**Everything libakgl decides is free. The pixels are the whole frame.** Every
|
||||
piece of bookkeeping this library does — pool scans, registry lookups, state-to-
|
||||
sprite mapping, physics, the error-context machinery — adds up to well under 1%
|
||||
of a frame that is 97% software rasterization. On a GPU backend those blits get
|
||||
cheap and libakgl's own share rises, which is exactly why the per-operation
|
||||
numbers above matter more than the frame totals.
|
||||
|
||||
## What the numbers say
|
||||
|
||||
### The tilemap draw is SDL, not libakgl — and I can prove it
|
||||
|
||||
`akgl_tilemap_draw` takes 16.26 ms for a 1200-tile screen. A raw
|
||||
`SDL_RenderTexture` loop issuing the *same 1200 blits from the same scattered
|
||||
source tiles* takes 16.23 ms. The library's own per-tile work — the bounds
|
||||
arithmetic, the tileset scan, the offset-table lookup, the backend indirection,
|
||||
the error macros — is **0.03 ms per frame, under 0.2%**.
|
||||
|
||||
That took three attempts to measure honestly. A control that walked the sheet
|
||||
sequentially said libakgl cost 67%; a control that blitted one source tile over
|
||||
and over said it cost 3400%. Both were wrong, and both were wrong the same way:
|
||||
they changed the *memory access pattern of the source texture* rather than
|
||||
isolating the library. A 16x16 tile read from a random place in a 768x576 sheet
|
||||
costs about 13 µs on this software rasterizer; the same tile read from cache
|
||||
costs 0.4 µs. That factor of thirty is the whole story, and none of it is
|
||||
libakgl's.
|
||||
|
||||
Related: the `FIXME` in `src/tilemap.c` worrying that the per-tile tileset scan
|
||||
"is probably not very efficient" is, at eight tilesets, worth **0.8% of the
|
||||
frame** (16.40 ms vs 16.26 ms). It is a real O(tiles x tilesets) loop and it
|
||||
should still be fixed, but it is not where the time is, and nobody should
|
||||
reorganise the loader for it.
|
||||
|
||||
### The pools are linear scans, and only the string pool cares
|
||||
|
||||
Claiming an actor from an empty pool is 4.0 ns; claiming the last free slot is
|
||||
36.9 ns — nine times the cost, and still nothing.
|
||||
|
||||
The string pool is the exception, and it is instructive. Claiming from an empty
|
||||
string pool is 3.9 ns; claiming the *last free slot* is 250.9 ns, **64 times**
|
||||
slower. Same algorithm, same 256-ish entries. The difference is that each
|
||||
`akgl_String` is `PATH_MAX` + 4 bytes, so the scan touches one reference count
|
||||
every 4 KiB and takes a cache miss on every candidate. A pool of 256 strings is
|
||||
a megabyte, and walking it is walking a megabyte.
|
||||
|
||||
`akgl_heap_release_string` costs 47.2 ns because it `memset`s all 4,100 bytes
|
||||
whether the string held a path or one character. Same for `akgl_string_copy`
|
||||
(32.2 ns) and `akgl_get_property` (85.3 ns), which move `AKGL_MAX_STRING_LENGTH`
|
||||
bytes unconditionally. None of these is expensive in isolation; all of them are
|
||||
the same avoidable habit of paying for `PATH_MAX` when you used eleven bytes.
|
||||
|
||||
### Errors cost about ten times what success costs
|
||||
|
||||
`akgl_actor_update` on an actor whose character has a sprite for its state:
|
||||
68.4 ns. The same call on an actor whose character does *not*: **616.5 ns**.
|
||||
|
||||
That path is not an error in any meaningful sense — the library handles
|
||||
`AKERR_KEY` and carries on, and `akgl_actor_render` logs it and draws nothing.
|
||||
It is a normal condition on a partly authored character. But raising it means
|
||||
claiming a context out of `AKERR_ARRAY_ERROR`, formatting a message with
|
||||
`vsnprintf`, appending a stack-trace frame, walking the `PROCESS` switch, and
|
||||
releasing it again. Nine times the cost of the update it replaced.
|
||||
|
||||
The design conclusion is not "make errors cheaper". It is that a *routine*
|
||||
condition should not be reported as an error. A character that has no sprite for
|
||||
a state should answer that question with a boolean.
|
||||
|
||||
### `akgl_game_update` updates every actor sixteen times
|
||||
|
||||
`src/game.c:617` loops over `AKGL_TILEMAP_MAX_LAYERS`, and the actor sweep
|
||||
nested inside it does not filter by layer. Every live actor's `updatefunc` runs
|
||||
**16 times per frame**. At 68.4 ns per update and 64 actors that is 70 µs of
|
||||
work to do 4.4 µs of work.
|
||||
|
||||
It is invisible in the frame total here because the tilemap blits are three
|
||||
orders of magnitude larger. On a GPU backend, where the frame might be 2 ms, it
|
||||
is 3.5% of the frame doing nothing. Filed in `TODO.md`.
|
||||
|
||||
### Text has no cache at all
|
||||
|
||||
`akgl_text_rendertextat` is 12.6 µs for fifteen characters: it rasterizes the
|
||||
string, uploads it as a texture, blits it, and destroys the texture — every
|
||||
call, every frame, for a score that changes once a second. Measuring the string
|
||||
first with `akgl_text_measure` is 37.3 ns, i.e. free, which tells you the whole
|
||||
cost is the rasterize-and-upload.
|
||||
|
||||
Six HUD readouts is 76 µs a frame. That is fine at 60 fps on this machine and it
|
||||
is 4% of a 2 ms GPU frame. A one-line cache keyed on (font, string, colour)
|
||||
would take it to nothing, and it is the single clearest optimisation in the
|
||||
library.
|
||||
|
||||
### Loading is dominated by things that are not the file
|
||||
|
||||
A 2x2 fixture map with one tileset takes **11.9 ms** to load and release. Of
|
||||
that, 1.37 ms — 11.5% — is `memset`ing the 26 MB `akgl_Tilemap` before anything
|
||||
is read. Most of the rest is decoding the tileset PNG. The JSON is noise:
|
||||
parsing a small document is 11.3 µs, and the accessors are 14-41 ns each.
|
||||
|
||||
`akgl_path_relative` is 3.5 µs, because it is a `realpath(3)` syscall. A map
|
||||
naming twenty assets pays 70 µs. Also noise, but worth knowing it is a syscall
|
||||
and not a string operation.
|
||||
|
||||
### The collision helpers are fine; the missing broad phase is the problem
|
||||
|
||||
`akgl_collide_rectangles` is 24.9 ns when the rectangles overlap (it returns at
|
||||
the first corner that hits) and 57.9 ns when they do not (all eight corner tests
|
||||
run). Both are fine.
|
||||
|
||||
What the library does not provide is a broad phase, so a caller that wants
|
||||
collision writes the all-pairs loop: 2016 pairs for 64 actors, 115 µs a frame,
|
||||
0.7% of a 60 fps budget. That is affordable. It is also O(n²): raise
|
||||
`AKGL_MAX_HEAP_ACTOR` to 256 and the same loop is 32,640 pairs and 1.9 ms — over
|
||||
10% of the frame, for a game that has done nothing yet.
|
||||
|
||||
### Spawning is cheap, and a third of it is a log line
|
||||
|
||||
An actor spawn — pool claim, `memset`, registry insert, release — is 202.3 ns
|
||||
with the library's logging on and 162.5 ns with SDL's log priority raised so the
|
||||
message is never formatted. **20% of a spawn is formatting a log line nobody
|
||||
reads**, and that is with output going to a sink that throws it away; write it to
|
||||
a terminal and it is far worse. `akgl_actor_initialize` and
|
||||
`akgl_character_sprite_add` both log unconditionally at `INFO`.
|
||||
|
||||
## Defects these tests found
|
||||
|
||||
Six, and they are filed where defects live: `TODO.md`, under **Performance ->
|
||||
Defects the perf suites found**, items 28-33, each with its file, line,
|
||||
functional consequence, and what fixing it would touch. They are not repeated
|
||||
here.
|
||||
|
||||
Worth saying in this document, because it is the argument for having written the
|
||||
suites at all: stress testing breaks things unit tests do not reach. Two of the
|
||||
six are process-killing — an error-context leak that aborted on the 129th path
|
||||
resolution (fixed, with a regression test), and a pooled-string leak that turns
|
||||
into a segfault rather than an `AKGL_ERR_HEAP` around the 52nd map load. Neither
|
||||
is reachable by a suite that calls each function a handful of times, and neither
|
||||
had anything to do with speed. They came out of loops that ran the same call ten
|
||||
thousand times in one process, which is what a running game does and what
|
||||
nothing else in this tree did.
|
||||
|
||||
## What this report does not cover
|
||||
|
||||
- **One machine.** No ARM, no Raspberry Pi, no Windows, no macOS. The ratios
|
||||
should travel; the absolute numbers will not.
|
||||
- **No GPU backend.** Everything drawing-related is a software rasterizer under
|
||||
the dummy video driver. The share of a frame that belongs to libakgl is a
|
||||
floor, not an estimate.
|
||||
- **No audio.** `src/audio.c` is not benchmarked at all; the mixer under the
|
||||
dummy driver does not do the work a real device would.
|
||||
- **No controller input.** The event path is driven by SDL and has no gamepad to
|
||||
drive it under the dummy driver.
|
||||
- **No heap profiling beyond the static footprint.** Nothing here measures peak
|
||||
or steady-state heap usage. Correctness of the heap *is* covered now, by
|
||||
`cmake --build build --target memcheck`, which runs these same binaries under
|
||||
valgrind — `tests/benchutil.h` cuts the iteration counts by three orders of
|
||||
magnitude when it finds itself there, so a benchmark becomes a path-coverage
|
||||
program. That run found six more defects, including a leak of the parsed JSON
|
||||
document in every asset loader; they are in `TODO.md` under **Memory
|
||||
checking**. What is still missing is a profile: how much a running game
|
||||
actually holds at once, and how that grows over an hour.
|
||||
- **Single-threaded throughout.** The library is not thread-safe by design
|
||||
(`akgl_game_state_lock` guards one field), and nothing here tests contention.
|
||||
404
README.md
Normal file
404
README.md
Normal file
@@ -0,0 +1,404 @@
|
||||
## How do I initialize a game
|
||||
|
||||
Initialize the global game object with info about your game
|
||||
|
||||
```c
|
||||
strncpy((char *)&game.name, "sdl3-gametest", 256);
|
||||
strncpy((char *)&game.version, "0.0.1", 32);
|
||||
strncpy((char *)&game.uri, "net.aklabs.games.sdl3-gametest", 256);
|
||||
```
|
||||
|
||||
Call the game initialization routines and lock the game state for further initialization
|
||||
|
||||
```c
|
||||
PASS(e, akgl_game_init());
|
||||
PASS(e, akgl_game_state_lock());
|
||||
```
|
||||
|
||||
If you have a registry properties file, load it. If you don't have a properties file, use `akgl_set_property("prop_name", "prop_value")` to populate the required game properties.
|
||||
|
||||
```c
|
||||
PASS(e, akgl_registry_load_properties(YOUR_REGISTR_FILEPATH));
|
||||
```
|
||||
|
||||
Initialize your physics engine and renderer of choice
|
||||
|
||||
```c
|
||||
PASS(e, akgl_render_init2d(renderer));
|
||||
PASS(e, akgl_physics_init_arcade(physics));
|
||||
```
|
||||
|
||||
Unlock the game state
|
||||
|
||||
```c
|
||||
PASS(e, akgl_game_state_unlock());
|
||||
```
|
||||
|
||||
## What is in a properties file (or, What properties must I set if I don't have one?)
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"game.screenwidth": "640",
|
||||
"game.screenheight": "480",
|
||||
"physics.gravity.y": "1024.0",
|
||||
"physics.drag.y": "1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Physics properties (gravity and drag along X, Y and Z) are optional and default to `0`.
|
||||
|
||||
## How do I update and render the game world in my main loop
|
||||
|
||||
In your game loop (or in your `SDL_AppIterate` method), lock the game state, call the game update function, and then unlock the game state
|
||||
|
||||
```c
|
||||
PASS(e, akgl_game_state_lock());
|
||||
PASS(e, renderer->frame_start(renderer));
|
||||
SDL_RenderClear(renderer->sdl_renderer);
|
||||
PASS(e, akgl_game_update(NULL));
|
||||
PASS(e, renderer->frame_end(renderer));
|
||||
PASS(e, akgl_game_state_unlock());
|
||||
```
|
||||
|
||||
## How do I get an actor on screen
|
||||
|
||||
Load a sprite for a character. Sprites are JSON documents describing 2D sprites, frames, and looping. Sprites are named, and sprite names must be unique.
|
||||
|
||||
```c
|
||||
PASS(e, akgl_sprite_load_json(SOME_FILENAME))
|
||||
```
|
||||
|
||||
Load a character from a JSON file. Characters map sprites to actor states and define physics characteristics like movement speed. Each character is named, and character names must be unique.
|
||||
|
||||
```c
|
||||
PASS(e, akgl_character_load_json(SOME_FILENAME))
|
||||
```
|
||||
|
||||
You don't strictly have to load sprites and characters from json files, you can initialize them yourself, it's just tedious work. Here's an example of initializing a 32x32 sprite from a spritesheet that uses the first 4 frames in a looping animation.
|
||||
|
||||
```c
|
||||
akgl_SpriteSheet *sheet;
|
||||
akgl_Sprite *sprite;
|
||||
akgl_Character *character;
|
||||
PASS(e, akgl_heap_next_spritesheet(&sheet);
|
||||
PASS(e, akgl_spritesheet_initialize(sheet, 32, 32, IMAGE_FILENAME));
|
||||
PASS(e, akgl_heap_next_sprite(&sprite));
|
||||
PASS(e, akgl_sprite_initialize(sprite, SPRITE_NAME, &sheet);
|
||||
sprite->frames = 4;
|
||||
sprite->frameids = [0, 1, 2, 3];
|
||||
sprite->width = 32;
|
||||
sprite->height = 32;
|
||||
sprite->speed = 1000;
|
||||
sprite->loop = true;
|
||||
strncpy((char *)&sprite->name, "SPRITE NAME", AKGL_SPRITE_MAX_NAME_LENGTH);
|
||||
PASS(e, akgl_heap_next_character(&character));
|
||||
PASS(e, akgl_character_initialize(&character, "CHAR NAME"));
|
||||
PASS(e, akgl_character_sprite_add(&character, &sprite, STATE_MASK));
|
||||
// Set the character acceleration and scale if desired
|
||||
character->ax = 64.0;
|
||||
character->ay = 64.0;
|
||||
character->sx = 2.0;
|
||||
character->sy = 2.0;
|
||||
```
|
||||
|
||||
Initialize an actor. Actors are named ("player", "Quest NPC", whatever) and names must be unique.
|
||||
|
||||
```c
|
||||
akgl_Actor *myactor = NULL;
|
||||
PASS(e, akgl_heap_next_actor(&myactor);
|
||||
PASS(e, akgl_actor_initialize(&myactor, "ACTOR_NAME"));
|
||||
```
|
||||
|
||||
Assign a character to the actor by looking up the `akgl_Character` from the AKGL registry and assign it.
|
||||
|
||||
```c
|
||||
myactor->basechar = SDL_GetPointerProperty(
|
||||
AKGL_REGISTRY_CHARACTER,
|
||||
"CHARACTER_NAME",
|
||||
NULL);
|
||||
FAIL_ZERO_BREAK(e, myactor->basechar, AKERR_REGISTRY, "Character missing");
|
||||
```
|
||||
|
||||
Give the actor a position and a state, and turn it visible.
|
||||
|
||||
```c
|
||||
myactor->state = 9AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
myactor->x = 320;
|
||||
myactor->y = 240;
|
||||
myactor->visible = true;
|
||||
```
|
||||
|
||||
## What are in Sprite and Character files
|
||||
|
||||
Sprite files:
|
||||
|
||||
```json
|
||||
{
|
||||
"spritesheet": {
|
||||
"filename": "RELATIVE_IMAGE_FILE_REFERENCE",
|
||||
"frame_width": int,
|
||||
"frame_height": int
|
||||
},
|
||||
"name": "UNIQUE_SPRITE_NAME",
|
||||
"width": int,
|
||||
"height": int,
|
||||
"speed": int,
|
||||
"loop": boolean,
|
||||
"loopReverse": boolean,
|
||||
"frames": [
|
||||
int
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
* `frames` references the frame indexes in the spritesheet that should be used for this animation. Spritesheets are counted from the top left corner going to the right according to the spritesheet `frame_width` and `frame_height`.
|
||||
* `loop` says whether or not we should loop the animation
|
||||
* `loopReverse` says whether or not we should "bounce" the animation (when we reach the end of the frames, start counting back to the beginning, then count to the end, etc). Otherwise the frames are displayed from 0..n and then cycles back to 0.
|
||||
* `speed` is the number of milliseconds each frame in the animation should appear on the screen
|
||||
|
||||
Character files:
|
||||
|
||||
```c
|
||||
{
|
||||
"name": "UNIQUE_CHARACTER_NAME",
|
||||
"speedtime": 8,
|
||||
"speed_x": 0,
|
||||
"speed_y": 0,
|
||||
"acceleration_x": 0,
|
||||
"acceleration_y": 0,
|
||||
"sprite_mappings": [
|
||||
{
|
||||
"state": [
|
||||
"AKGL_ACTOR_STATE_ALIVE",
|
||||
"AKGL_ACTOR_STATE_FACE_UP",
|
||||
"AKGL_ACTOR_STATE_MOVING_UP"
|
||||
],
|
||||
"sprite": "menupointer"
|
||||
}[, ...]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
* `speedtime` appears to be legacy and unused.
|
||||
* `speed_[xy]` and `acceleration_[xy]` are physics parameters that specify the top speed and acceleration rate (in pixels per nanosecond) of the character in physics simulations. The effect of acceleration depends on the physics simulation being used at the time (which may or may not account for gravity, drag, etc).
|
||||
* `sprite_mappings` map a set of actor state flag bitmasks (assume everything in `state` is `OR`ed together) to a sprite name. The game engine uses this to automatically pick the correct sprite (by name) for a given set of state flags. You need one of these for every possible state the character may be used in.
|
||||
|
||||
## How do I load a tilemap from the filesystem and display it on screen with my actors
|
||||
|
||||
The engine ONLY supports TilED TMJ tilemaps with tileset external references. Load a tilemap into the global `akgl_Tilemap *gamemap` object.
|
||||
|
||||
```c
|
||||
PASS(e, akgl_tilemap_load(PATHSTRING, gamemap));
|
||||
```
|
||||
|
||||
Actors will be automatically populated from objects in the tilemap object layers. Actor state flags here must be expressed as an integer, you can't (yet) use the same array of strings that is used in character json files.
|
||||
|
||||
```json
|
||||
"objects":[
|
||||
{
|
||||
"gid":147,
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"player",
|
||||
"properties":[
|
||||
{
|
||||
"name":"character",
|
||||
"type":"string",
|
||||
"value":"little guy"
|
||||
},
|
||||
{
|
||||
"name":"state",
|
||||
"type":"int",
|
||||
"value":24
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"actor",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":440.510088317656,
|
||||
"y":140.347239175702
|
||||
}[, ... ]
|
||||
```
|
||||
|
||||
Check if the tilemap wants to use its own physics, and if you want to allow that, override the global physics simulation
|
||||
|
||||
```c
|
||||
if ( gamemap->use_own_physics == true ) {
|
||||
physics = &gamemap->physics;
|
||||
}
|
||||
```
|
||||
|
||||
Tilemap physics specification follows. A map can specify its own physics properties (drag, gravity) without specifying a custom model.
|
||||
|
||||
```json
|
||||
"properties":[
|
||||
{
|
||||
"name":"physics.drag.y",
|
||||
"type":"float",
|
||||
"value":0
|
||||
},
|
||||
{
|
||||
"name":"physics.gravity.y",
|
||||
"type":"float",
|
||||
"value":0
|
||||
},
|
||||
{
|
||||
"name":"physics.model",
|
||||
"type":"string",
|
||||
"value":"arcade"
|
||||
}],
|
||||
```
|
||||
|
||||
The global `gamemap` object is automatically displayed if it is populated. Actors are drawn at the appropriate map layer depending on the actor's `layer` property (warning: this may be replaced with a `z` property soon.)
|
||||
|
||||
## How do I get the screen width and height
|
||||
|
||||
The most direct is to call `SDL_GetCurrentDisplayMode` to get the parameters from the returned `SDL_DisplayMode` structure (`->w` and `->h`).
|
||||
|
||||
The simplest way is to check the global `camera` object's `camera->w` and `camera->h` object. You may have more than one camera on a scene, and it's theoretically possible that the global camera object has been overriden and no longer represents the full screen.
|
||||
|
||||
The most reliable engine-centric way is to use `akgl_get_property` to get the property from the engine. Properties are read and stored as strings, so if you need to do these kinds of things a lot, cache the integer value somewhere.
|
||||
|
||||
```c
|
||||
akgl_String *width = NULL;
|
||||
int screenwidth = NULL;
|
||||
PASS(e, akgl_get_property("game.screenwidth", &width, "0"));
|
||||
PASS(e, aksl_atoi(width->data, &screenwidth));
|
||||
PASS(e, akgl_heap_release_string(width));
|
||||
```
|
||||
|
||||
|
||||
## Git hooks
|
||||
|
||||
The repository ships a `pre-commit` hook that keeps committed C sources in the
|
||||
project's canonical format (Emacs `cc-mode` "stroustrup"; see `AGENTS.md` for the
|
||||
full style guide). The hook lives in `scripts/hooks/` and is version controlled,
|
||||
but **Git configuration is not cloned**, so every clone has to be pointed at it
|
||||
once:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath scripts/hooks
|
||||
```
|
||||
|
||||
Confirm it took effect:
|
||||
|
||||
```sh
|
||||
git rev-parse --git-path hooks # should print: scripts/hooks
|
||||
```
|
||||
|
||||
That is the whole installation. The hook is already committed with its
|
||||
executable bit set, so nothing needs `chmod`.
|
||||
|
||||
### What the hook does
|
||||
|
||||
On each commit it looks at the **staged** content of any added, copied, modified,
|
||||
or renamed `.c`/`.h` file under `src/`, `include/`, `tests/`, or `util/`, and
|
||||
reindents it if it does not already match the canonical style. Checking the
|
||||
staged content rather than the working tree means what lands in the commit is
|
||||
what was actually verified.
|
||||
|
||||
When a file needs reindenting, the hook fixes it in the working tree and
|
||||
re-stages it — but only when the index and working tree agree for that file. If
|
||||
they differ, you have staged part of a file with `git add -p`, and re-staging
|
||||
would sweep your unstaged work into the commit. Rather than do that silently the
|
||||
hook stops and prints the commands to run yourself:
|
||||
|
||||
```sh
|
||||
scripts/reindent.sh path/to/file.c
|
||||
git add path/to/file.c
|
||||
```
|
||||
|
||||
`include/akgl/SDL_GameControllerDB.h` is generated and always skipped.
|
||||
|
||||
### Requirements
|
||||
|
||||
The hook drives Emacs in batch mode, because `cc-mode`'s indentation engine is
|
||||
the definition of the style and `clang-format` cannot reproduce it exactly. If
|
||||
`emacs` is not on `PATH` the hook prints a warning and allows the commit — a
|
||||
hook that refuses to run without an optional tool only teaches people to reach
|
||||
for `--no-verify`. Install Emacs to get the check; without it, formatting is on
|
||||
you.
|
||||
|
||||
### Bypassing and uninstalling
|
||||
|
||||
Skip the hook for a single commit:
|
||||
|
||||
```sh
|
||||
git commit --no-verify
|
||||
```
|
||||
|
||||
Remove it entirely:
|
||||
|
||||
```sh
|
||||
git config --unset core.hooksPath
|
||||
```
|
||||
|
||||
### If you already have local hooks
|
||||
|
||||
`core.hooksPath` **replaces** the hooks directory outright — once it is set, Git
|
||||
stops reading `.git/hooks/` altogether, so any hooks you keep there will silently
|
||||
stop firing. If that matters, leave `core.hooksPath` unset and symlink just this
|
||||
one hook instead:
|
||||
|
||||
```sh
|
||||
ln -s ../../scripts/hooks/pre-commit .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
### Formatting without the hook
|
||||
|
||||
The hook is a convenience, not the source of truth. The same check is available
|
||||
directly, and is what you would run in CI:
|
||||
|
||||
```sh
|
||||
scripts/reindent.sh --check # list non-conforming files; exit 1 if any
|
||||
scripts/reindent.sh # reindent every tracked C source in place
|
||||
scripts/reindent.sh src/game.c # reindent specific files
|
||||
```
|
||||
|
||||
`--check` exits `0` when everything conforms, `1` when a file needs reindenting,
|
||||
and `2` if Emacs is missing or the script cannot run — so a CI job that treats
|
||||
any non-zero status as failure will not mistake a broken toolchain for a clean
|
||||
tree.
|
||||
|
||||
## Mutation testing
|
||||
|
||||
The mutation harness makes one deliberate source-code change at a time in a scratch copy, then rebuilds and runs the passing CTest suite to measure whether tests detect the change. The known-failing `character` test is excluded by default.
|
||||
|
||||
```sh
|
||||
cmake --build build --target mutation
|
||||
scripts/mutation_test.py --target src/tilemap.c --list
|
||||
scripts/mutation_test.py --target src/tilemap.c --max-mutants 10
|
||||
scripts/mutation_test.py --threshold 40 --junit mutation-junit.xml
|
||||
```
|
||||
|
||||
The default run covers all libakgl-owned files under `src/`. Use repeated `--target` options to narrow the scope. A surviving mutant identifies behavior that the current tests do not verify; the script prints its file, line, operator, and exact edit. The real working tree is never mutated.
|
||||
|
||||
## Performance testing
|
||||
|
||||
`tests/perf.c` and `tests/perf_render.c` are benchmarks rather than unit tests: they drive the hot paths — pool acquire and release, the registry, the physics sweep, the per-actor update and render, the tilemap draw, text, and asset loading — and print what each one costs per operation.
|
||||
|
||||
```sh
|
||||
ctest --test-dir build -L perf --output-on-failure # benchmarks only, about 30 seconds
|
||||
ctest --test-dir build -LE perf # everything except the benchmarks
|
||||
AKGL_BENCH_SCALE=0.1 ctest --test-dir build -L perf # a tenth of the iterations
|
||||
```
|
||||
|
||||
Each measurement is the best of five runs and is held to a budget set at roughly ten times the recorded baseline, so a suite that turns red means an algorithmic regression rather than a busy machine. Budgets are enforced only in an optimized build at full scale; below `AKGL_BENCH_SCALE=1.0`, and in a coverage build, they are reported without failing.
|
||||
|
||||
`PERFORMANCE.md` carries the recorded baseline, the frame budget it adds up to, and what the numbers say — including the raw-SDL control rows that separate what libakgl costs from what the rasterizer costs. The six defects the stress tests turned up, and the targets the numbers are measured against, are in `TODO.md` under **Performance**.
|
||||
|
||||
## Memory checking
|
||||
|
||||
`memcheck` runs the suites that already exist under valgrind rather than adding suites of its own. The perf binaries carry most of it: they are the only programs in the tree that load assets, draw a scene, and run a frame loop in one process, and `tests/benchutil.h` cuts their iteration counts by three orders of magnitude when it detects valgrind — a benchmark walks one path a hundred thousand times, a leak check wants every path walked once.
|
||||
|
||||
```sh
|
||||
cmake --build build --target memcheck # everything, about 30 seconds
|
||||
scripts/memcheck.sh -R tilemap # one suite; ctest selection flags pass through
|
||||
scripts/memcheck.sh -LE perf # skip the benchmarks
|
||||
```
|
||||
|
||||
The run forces `SDL_VIDEO_DRIVER=dummy`, `SDL_RENDER_DRIVER=software` and `SDL_AUDIO_DRIVER=dummy` so the vendor GPU stack is never loaded — that removes thousands of unfixable findings inside the driver without suppressing anything. Definite leaks, invalid accesses and uninitialised reads are counted and set the exit status; "still reachable" is not, because that is what SDL and FreeType keep for the process lifetime. Suppressions for genuine third-party findings are in `scripts/valgrind.supp`.
|
||||
|
||||
What it found the first time it ran is in `TODO.md` under **Memory checking**.
|
||||
2
TODO.txt
2
TODO.txt
@@ -1,5 +1,3 @@
|
||||
Character definitions should allow for specifying multiple states per sprite mapping, rather than only one state per map. Use an array instead.
|
||||
|
||||
Rendering should move to the SDL GPU renderer so i can do lighting and particles etc
|
||||
- Example suitable for my most primitive use case: https://github.com/TheSpydog/SDL_gpu_examples/blob/main/Examples/Blit2DArray.c
|
||||
- Try vulkan and D3D tutorials to come up to speed on the moving pieces, then figure ou the details from the examples and API docs
|
||||
|
||||
10
akgl.pc.in
Normal file
10
akgl.pc.in
Normal file
@@ -0,0 +1,10 @@
|
||||
prefix=@CMAKE_INSTALL_PREFIX@
|
||||
exec_prefix=${prefix}
|
||||
libdir=${exec_prefix}/lib
|
||||
includedir=${exec_prefix}/include
|
||||
|
||||
Name: akgl
|
||||
Description: AKLabs Game library
|
||||
Version: @PROJECT_VERSION@
|
||||
Cflags: -I${includedir}/
|
||||
Libs: -L${libdir} -lakgl
|
||||
1
deps/SDL
vendored
Submodule
1
deps/SDL
vendored
Submodule
Submodule deps/SDL added at d9d5536704
1
deps/SDL_image
vendored
Submodule
1
deps/SDL_image
vendored
Submodule
Submodule deps/SDL_image added at bec9134a26
1
deps/SDL_mixer
vendored
Submodule
1
deps/SDL_mixer
vendored
Submodule
Submodule deps/SDL_mixer added at a5e1890afc
1
deps/SDL_ttf
vendored
Submodule
1
deps/SDL_ttf
vendored
Submodule
Submodule deps/SDL_ttf added at a1ce3670ae
1
deps/jansson
vendored
Submodule
1
deps/jansson
vendored
Submodule
Submodule deps/jansson added at 1eb7a81297
1
deps/libakerror
vendored
Submodule
1
deps/libakerror
vendored
Submodule
Submodule deps/libakerror added at 5ff87908e7
1
deps/libakstdlib
vendored
Submodule
1
deps/libakstdlib
vendored
Submodule
Submodule deps/libakstdlib added at f8425b8729
1
deps/semver
vendored
Submodule
1
deps/semver
vendored
Submodule
Submodule deps/semver added at bd1db234a6
2263
include/akgl/SDL_GameControllerDB.h
Normal file
2263
include/akgl/SDL_GameControllerDB.h
Normal file
File diff suppressed because it is too large
Load Diff
459
include/akgl/actor.h
Normal file
459
include/akgl/actor.h
Normal file
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* @file actor.h
|
||||
* @brief A live thing in the world: state bits, motion, hierarchy, and behaviour hooks.
|
||||
*
|
||||
* An actor is an instance; the akgl_Character it points at is its template. The
|
||||
* actor carries only what is unique to it -- where it is, which way it is
|
||||
* moving, which animation frame it is on -- and borrows speed, acceleration and
|
||||
* the state-to-sprite map from the character. That is what makes a hundred of
|
||||
* one kind of thing cheap.
|
||||
*
|
||||
* **State is a bitmask**, not an enum. An actor is facing left *and* moving left
|
||||
* *and* alive at the same time, and the whole combination is the key that
|
||||
* selects a sprite (see akgl_character_sprite_get). Adding a state means adding
|
||||
* a bit here *and* a name in `src/actor_state_string_names.c`, or character JSON
|
||||
* cannot refer to it.
|
||||
*
|
||||
* **Behaviour attaches as function pointers**, not by inheritance. Every actor
|
||||
* gets the library's default `updatefunc`, `renderfunc`, `facefunc`,
|
||||
* `movementlogicfunc`, `changeframefunc` and `addchild` from
|
||||
* akgl_actor_initialize; a game that wants a different flavour of anything
|
||||
* replaces the pointer on that one actor.
|
||||
*
|
||||
* **Motion is split four ways** -- thrust, environmental, acceleration, and the
|
||||
* velocity that is their sum -- so that a character's own walking and the
|
||||
* world's gravity and drag can be reasoned about separately. See physics.h for
|
||||
* the model and which fields mean what.
|
||||
*
|
||||
* Actors are pool objects (akgl_heap_next_actor) published in
|
||||
* #AKGL_REGISTRY_ACTOR under their name.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_ACTOR_H_
|
||||
#define _AKGL_ACTOR_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include "types.h"
|
||||
#include "character.h"
|
||||
|
||||
// ---- LOW WORD STATUSES ----
|
||||
|
||||
#define AKGL_ACTOR_STATE_FACE_DOWN 1 << 0 // 1 0000 0000 0000 0001
|
||||
#define AKGL_ACTOR_STATE_FACE_LEFT 1 << 1 // 2 0000 0000 0000 0010
|
||||
#define AKGL_ACTOR_STATE_FACE_RIGHT 1 << 2 // 4 0000 0000 0000 0100
|
||||
#define AKGL_ACTOR_STATE_FACE_UP 1 << 3 // 8 0000 0000 0000 1000
|
||||
#define AKGL_ACTOR_STATE_ALIVE 1 << 4 // 16 0000 0000 0001 0000
|
||||
#define AKGL_ACTOR_STATE_DYING 1 << 5 // 32 0000 0000 0010 0000
|
||||
#define AKGL_ACTOR_STATE_DEAD 1 << 6 // 64 0000 0000 0100 0000
|
||||
#define AKGL_ACTOR_STATE_MOVING_LEFT 1 << 7 // 128 0000 0000 1000 0000
|
||||
#define AKGL_ACTOR_STATE_MOVING_RIGHT 1 << 8 // 256 0000 0001 0000 0000
|
||||
#define AKGL_ACTOR_STATE_MOVING_UP 1 << 9 // 512 0000 0010 0000 0000
|
||||
#define AKGL_ACTOR_STATE_MOVING_DOWN 1 << 10 // 1024 0000 0100 0000 0000
|
||||
#define AKGL_ACTOR_STATE_MOVING_IN 1 << 11 // 2048 0000 1000 0000 0000
|
||||
#define AKGL_ACTOR_STATE_MOVING_OUT 1 << 12 // 4096 0001 0000 0000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_13 1 << 13 // 8192 0010 0000 0000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_14 1 << 14 // 16384 0100 0000 0000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_15 1 << 15 // 32768 1000 0000 0000 0000
|
||||
|
||||
// ----- HIGH WORD STATUSES -----
|
||||
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_16 1 << 16 // 65536 0000 0000 0000 0001
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_17 1 << 17 // 131072 0000 0000 0000 0010
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_18 1 << 18 // 262144 0000 0000 0000 0100
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_19 1 << 19 // 524288 0000 0000 0000 1000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_20 1 << 20 // 1048576 0000 0000 0001 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_21 1 << 21 // 2097152 0000 0000 0010 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_22 1 << 22 // 4194304 0000 0000 0100 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_23 1 << 23 // 8388608 0000 0000 1000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_24 1 << 24 // 16777216 0000 0001 0000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_25 1 << 25 // 33554432 0000 0010 0000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_26 1 << 26 // 67108864 0000 0100 0000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_27 1 << 27 // 134217728 0000 1000 0000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_28 1 << 28 // 268435456 0001 0000 0000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_29 1 << 29 // 536870912 0010 0000 0000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_30 1 << 30 // 1073741824 0100 0000 0000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_31 1 << 31 // 2147483648 1000 0000 0000 0000
|
||||
|
||||
/** @brief Bits in an actor's state word. Fixed by the width of `int32_t state`. */
|
||||
#define AKGL_ACTOR_MAX_STATES 32
|
||||
|
||||
// This is an array of strings equal to actor states from 1-32.
|
||||
// This is built by a utility script and not kept in git, see
|
||||
// the Makefile for lib_src/actor_state_string_names.c
|
||||
/**
|
||||
* @brief Bit position -> state name, as text. Index `i` names the bit `1 << i`.
|
||||
*
|
||||
* akgl_registry_init_actor_state_strings builds
|
||||
* #AKGL_REGISTRY_ACTOR_STATE_STRINGS out of this, which is what lets character
|
||||
* JSON write `"AKGL_ACTOR_STATE_FACE_LEFT"` instead of `2`. A bit whose name
|
||||
* here does not match its `#define` above cannot be referred to from JSON at
|
||||
* all.
|
||||
*
|
||||
* @warning Three known defects, all tracked in TODO.md items 24-26: the
|
||||
* definition in `src/actor_state_string_names.c` is 32 entries while
|
||||
* this declares 33, so reading index 32 reads past the object; bits 11
|
||||
* and 12 are named `UNDEFINED_11`/`UNDEFINED_12` rather than
|
||||
* `MOVING_IN`/`MOVING_OUT`; and the comment above about a generator
|
||||
* script is stale -- there is no such script, and the file is tracked
|
||||
* in git and maintained by hand.
|
||||
*/
|
||||
extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1];
|
||||
|
||||
/** @brief Every facing bit. Clear this before setting one, so an actor faces exactly one way. */
|
||||
#define AKGL_ACTOR_STATE_FACE_ALL (AKGL_ACTOR_STATE_FACE_DOWN | AKGL_ACTOR_STATE_FACE_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_FACE_UP)
|
||||
/** @brief Every planar movement bit. The two depth bits, MOVING_IN and MOVING_OUT, are not in it. */
|
||||
#define AKGL_ACTOR_STATE_MOVING_ALL (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_MOVING_UP | AKGL_ACTOR_STATE_MOVING_DOWN)
|
||||
|
||||
/** @brief Longest actor name, including the terminator. Names are truncated, not rejected. */
|
||||
#define AKGL_ACTOR_MAX_NAME_LENGTH 128
|
||||
/** @brief Children one actor can carry. A child moves with its parent rather than simulating. */
|
||||
#define AKGL_ACTOR_MAX_CHILDREN 8
|
||||
|
||||
/** @brief Actors in the pool. Override before including heap.h to change it; see heap.h. */
|
||||
#define AKGL_MAX_HEAP_ACTOR 64
|
||||
|
||||
/** @brief Represents a live actor, including state, motion, hierarchy, and behavior callbacks. */
|
||||
typedef struct akgl_Actor {
|
||||
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. One reference per parent, plus one for the registry. */
|
||||
char name[AKGL_ACTOR_MAX_NAME_LENGTH]; /**< Registry key. Truncated, not rejected, if the source name is longer. */
|
||||
akgl_Character *basechar; /**< The template this actor instantiates. Borrowed; no reference is taken. Required before the actor can update, render, or simulate. */
|
||||
uint8_t curSpriteFrameId; /**< Index into the current sprite's `frameids`, not a frame number on the sheet. */
|
||||
SDL_Time curSpriteFrameTimer; /**< When the current frame was shown. The next frame is due once the sprite's `speed` has elapsed. */
|
||||
bool curSpriteReversing; /**< Walking the animation backwards, for a sprite with `loopReverse` set. */
|
||||
uint32_t layer; /**< Which tilemap layer the actor is drawn and simulated on. Set from the object layer it was placed in. */
|
||||
int32_t state; /**< The `AKGL_ACTOR_STATE_*` bitmask. The whole value is the key that selects a sprite. */
|
||||
bool movement_controls_face; /**< When set, the default `facefunc` turns the actor to face whichever way it is moving. Clear it for an actor that aims independently. */
|
||||
void *actorData; /**< The game's own per-actor data. Never read or freed by the library. */
|
||||
bool visible; /**< Whether to draw at all. An actor off-camera is skipped separately, so this is for deliberate hiding. */
|
||||
SDL_Time movetimer; /**< Unused. Nothing in the library reads or writes it. */
|
||||
// Velocity. Combined effect of all forces acting on the actor resulting
|
||||
// in energy along an axis.
|
||||
float32_t vx; /**< Velocity along x, world units per second. Recomputed each step as `ex + tx`; writing it directly is overwritten. */
|
||||
float32_t vy; /**< Velocity along y. On a child actor this is read as an offset from the parent instead. */
|
||||
float32_t vz; /**< Velocity along z. */
|
||||
// Environmental velocity. These are the forces acting on the actor by the
|
||||
// environment (such as gravity and atmospheric drag)
|
||||
float32_t ex; /**< Environmental velocity along x: what gravity and drag have done. Accumulates across steps. */
|
||||
float32_t ey; /**< Environmental velocity along y. This is what carries a falling actor. */
|
||||
float32_t ez; /**< Environmental velocity along z. */
|
||||
// Thrust. Energy originating only from the actor's own acceleration on a
|
||||
// given axis, before the effects of gravity and drag.
|
||||
float32_t tx; /**< Thrust along x: the actor's own effort. Capped at `sx`, which is why gravity can outrun top speed and walking cannot. */
|
||||
float32_t ty; /**< Thrust along y, capped at `sy`. */
|
||||
float32_t tz; /**< Thrust along z, capped at `sz`. */
|
||||
// Acceleration. These are borrowed from the base character object.
|
||||
// A given axis resets to 0 when the actor stops moving in a given axis.
|
||||
float32_t ax; /**< Acceleration along x, signed by the direction of travel. Copied from the character each step by the default movement logic. */
|
||||
float32_t ay; /**< Acceleration along y, signed the same way. */
|
||||
float32_t az; /**< Acceleration along z. Not set by the default movement logic. */
|
||||
// Max speed. These are borrowed from the base character object.
|
||||
float32_t sx; /**< Maximum thrust along x, copied from the character. */
|
||||
float32_t sy; /**< Maximum thrust along y. */
|
||||
float32_t sz; /**< Maximum thrust along z. */
|
||||
// Position.
|
||||
float32_t x; /**< Position in map pixels. For a child, an offset from the parent. */
|
||||
float32_t y; /**< Position in map pixels. Also what the tilemap's perspective band scales against. */
|
||||
float32_t z; /**< Depth. Carried through the simulation but not used at draw time. */
|
||||
float32_t scale; /**< Draw scale, 1.0 for natural size. Written by akgl_tilemap_scale_actor, or forced to 1.0 when tilemap scaling is off. */
|
||||
struct akgl_Actor *children[AKGL_ACTOR_MAX_CHILDREN]; /**< Attached actors, `NULL` in unused slots. Each holds a reference and is released with the parent. */
|
||||
struct akgl_Actor *parent; /**< The actor this one is attached to, or `NULL`. A child is positioned relative to it and does not simulate on its own. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*updatefunc)(struct akgl_Actor *obj); /**< Per-frame logic: facing, then animation frame. Defaults to akgl_actor_update. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*renderfunc)(struct akgl_Actor *obj); /**< Per-frame draw. Defaults to akgl_actor_render. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*facefunc)(struct akgl_Actor *obj); /**< Chooses the facing bits. Defaults to akgl_actor_automatic_face. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*movementlogicfunc)(struct akgl_Actor *obj, float32_t dt); /**< Called by the physics step before gravity. Defaults to akgl_actor_logic_movement. May raise AKGL_ERR_LOGICINTERRUPT to skip the rest of the step. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*changeframefunc)(struct akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems); /**< Advances the animation. Defaults to akgl_actor_logic_changeframe. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*addchild)(struct akgl_Actor *obj, struct akgl_Actor *child); /**< Attaches a child. Defaults to akgl_actor_add_child. */
|
||||
} akgl_Actor;
|
||||
|
||||
/**
|
||||
* @brief Zero a pooled actor, name it, wire up its default behaviour, and register it.
|
||||
*
|
||||
* Sets `scale` to 1.0 and `movement_controls_face` to true, installs all six
|
||||
* default function pointers, publishes the actor in #AKGL_REGISTRY_ACTOR, and
|
||||
* takes the first reference. It does *not* set a character: the actor cannot
|
||||
* update, render, or simulate until akgl_actor_set_character has run.
|
||||
*
|
||||
* @param obj Pooled actor to initialize, normally from akgl_heap_next_actor.
|
||||
* Required. Any previous contents are discarded -- including its
|
||||
* child list, so release an actor rather than reinitializing it.
|
||||
* @param name Registry key, NUL-terminated. Required. Truncated at
|
||||
* #AKGL_ACTOR_MAX_NAME_LENGTH. An existing entry with the same name
|
||||
* is silently replaced, and the actor it displaced becomes
|
||||
* unreachable rather than being released.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p name is `NULL`.
|
||||
* @throws AKERR_KEY If the actor cannot be written into #AKGL_REGISTRY_ACTOR --
|
||||
* in practice, because akgl_registry_init has not run.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_initialize(akgl_Actor *obj, char *name);
|
||||
/**
|
||||
* @brief Bind an actor to a registered character, and copy its top speeds.
|
||||
*
|
||||
* Looks the character up by name and takes its `sx`/`sy` as this actor's speed
|
||||
* limits, zeroing `ax` and `ay` so the actor starts from rest. The character is
|
||||
* borrowed, not referenced: releasing it out from under a live actor leaves a
|
||||
* dangling pointer.
|
||||
*
|
||||
* @param obj The actor to bind. Required.
|
||||
* @param basecharname Registry name of the character. Required. Rebinding an
|
||||
* actor mid-life is allowed, and is how a character swaps
|
||||
* its whole sprite set at once.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p basecharname is `NULL`, or if no
|
||||
* character is registered under that name. The last case is a lookup
|
||||
* miss reported with a pointer status rather than AKERR_KEY.
|
||||
*
|
||||
* @note `sz` is not copied and `az` is not zeroed, so an actor's depth speed
|
||||
* keeps whatever it had. The default movement logic re-copies all three
|
||||
* each step, which papers over it for anything that simulates.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_set_character(akgl_Actor *obj, char *basecharname);
|
||||
/**
|
||||
* @brief Draw an actor's current animation frame, if it is on camera.
|
||||
*
|
||||
* Selects the sprite for the actor's current state, checks it against the
|
||||
* camera, and blits the frame at the actor's position translated into screen
|
||||
* space. A child actor is drawn at its parent's position plus its own, which is
|
||||
* what makes an offset mean an offset.
|
||||
*
|
||||
* Several things are skipped rather than reported, because a frame is not the
|
||||
* place to fail: an actor with no sprite for its state, one off camera, one with
|
||||
* `visible` clear, and one whose frame index has run past its current sprite
|
||||
* are all simply not drawn.
|
||||
*
|
||||
* @param obj The actor to draw. Required, along with its `basechar`.
|
||||
* @return `NULL` on success -- including every skipped case above -- otherwise
|
||||
* an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
|
||||
* @throws AKERR_* Whatever akgl_sprite_sheet_coords_for_frame or the renderer's
|
||||
* `draw_texture` raises.
|
||||
*
|
||||
* @note The destination height is computed from the sprite's *width*, so a
|
||||
* non-square sprite is drawn square.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_render(akgl_Actor *obj);
|
||||
/**
|
||||
* @brief Per-frame logic: turn the actor to face its direction, then advance its animation.
|
||||
*
|
||||
* Calls the actor's `facefunc`, then selects the sprite for the resulting state
|
||||
* and calls `changeframefunc` if that sprite's frame time has elapsed. An actor
|
||||
* with no sprite registered for its current state is left alone and reported as
|
||||
* success -- states change faster than art gets drawn, and a missing sprite
|
||||
* should not stop the frame.
|
||||
*
|
||||
* @param obj The actor to update. Required, along with its `basechar`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
|
||||
* @throws AKERR_* Whatever `facefunc` or `changeframefunc` raises. Note that an
|
||||
* AKERR_KEY from either is swallowed along with the missing-sprite case.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_update(akgl_Actor *obj);
|
||||
/**
|
||||
* @brief The default `movementlogicfunc`: turn movement bits into signed acceleration.
|
||||
*
|
||||
* Re-copies the character's speed limits onto the actor -- so a change to the
|
||||
* character takes effect next step -- and sets `ax`/`ay` to plus or minus the
|
||||
* character's acceleration according to which movement bits are set. It does not
|
||||
* integrate anything; the physics backend does that.
|
||||
*
|
||||
* Opposing bits do not cancel: `MOVING_LEFT` wins over `MOVING_RIGHT` because it
|
||||
* is tested first. An actor with no movement bits keeps its previous
|
||||
* acceleration rather than being zeroed, which is why the input handlers clear
|
||||
* it themselves on key release.
|
||||
*
|
||||
* @param obj The actor to compute acceleration for. Required, along with its
|
||||
* `basechar`.
|
||||
* @param dt Seconds since the previous step. Accepted for the hook's signature;
|
||||
* this implementation does not use it, since it sets an acceleration
|
||||
* rather than integrating one.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
|
||||
*
|
||||
* @note A replacement for this hook may raise AKGL_ERR_LOGICINTERRUPT to tell
|
||||
* the physics step to skip the rest of this actor's tick. This
|
||||
* implementation never does.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_movement(akgl_Actor *obj, float32_t dt);
|
||||
/**
|
||||
* @brief The default `changeframefunc`: step to the next animation frame.
|
||||
*
|
||||
* Walks forward through the sprite's frames; at the last one it either wraps to
|
||||
* 0, or -- for a sprite with both `loop` and `loopReverse` -- turns round and
|
||||
* walks back down, turning again at frame 0. A sprite with `loop` clear also
|
||||
* wraps to 0 rather than holding the final frame.
|
||||
*
|
||||
* @param obj The actor whose frame index to advance. Required.
|
||||
* @param curSprite The sprite currently selected for the actor, whose frame
|
||||
* count and loop flags decide what happens at the end.
|
||||
* Required in practice, and **not** checked -- a `NULL` here is
|
||||
* a crash, not an error.
|
||||
* @param curtimems The current clock reading. Accepted for the hook's signature;
|
||||
* this implementation does not use it, because the caller has
|
||||
* already decided the frame is due.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems);
|
||||
/**
|
||||
* @brief The default `facefunc`: turn the actor to face whichever way it is moving.
|
||||
*
|
||||
* Clears every facing bit and sets the one matching the first movement bit it
|
||||
* finds, in the order left, right, up, down -- so an actor moving diagonally
|
||||
* faces the horizontal component. An actor with `movement_controls_face` clear
|
||||
* is left alone entirely, which is the hook for something that aims
|
||||
* independently of the way it walks.
|
||||
*
|
||||
* @param obj The actor to turn. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
*
|
||||
* @note An actor that has stopped moving is left with no facing bit at all,
|
||||
* rather than keeping the way it was last facing -- so its state no longer
|
||||
* matches any "standing still facing left" sprite. The implementation
|
||||
* carries a TODO saying as much.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_automatic_face(akgl_Actor *obj);
|
||||
/**
|
||||
* @brief Attach one actor to another so it moves with it.
|
||||
*
|
||||
* A child is positioned relative to its parent rather than simulated: the
|
||||
* physics step snaps it to the parent's position plus its own velocity fields,
|
||||
* used here as a fixed offset. That is what a carried lantern or a turret on a
|
||||
* tank wants.
|
||||
*
|
||||
* The parent takes a reference on the child, and releasing the parent releases
|
||||
* every child with it.
|
||||
*
|
||||
* @param obj The parent. Required.
|
||||
* @param child The actor to attach. Required. Must not already have a parent --
|
||||
* this builds a tree, not a graph. Its `x`/`y` become an offset
|
||||
* from the parent's position rather than a world position.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p child is `NULL`.
|
||||
* @throws AKERR_RELATIONSHIP If @p child already has a parent. Detach it first --
|
||||
* though note there is no detach function, so in practice this means
|
||||
* releasing and rebuilding it.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p obj already has #AKGL_ACTOR_MAX_CHILDREN
|
||||
* children.
|
||||
*
|
||||
* @warning Nothing checks for a cycle. Making an actor its own ancestor makes
|
||||
* akgl_heap_release_actor recurse until the stack runs out.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child);
|
||||
|
||||
/*
|
||||
* The control-map handler functions ("cmhf"). These are what
|
||||
* akgl_controller_default binds the arrow keys and the D-pad to, and what a
|
||||
* game's own akgl_Control bindings can point at. They are called with the
|
||||
* control map's target actor, not with a global "player", so they work for any
|
||||
* number of locally controlled actors.
|
||||
*
|
||||
* Each pair is symmetric: the `_on` handler starts movement in a direction and
|
||||
* turns the actor to face it, and the `_off` handler stops it dead -- zeroing
|
||||
* acceleration, thrust, environmental velocity and velocity on that axis, so
|
||||
* there is no coasting. That is an arcade feel rather than a physical one, and
|
||||
* it is deliberate; a game wanting momentum binds its own handlers instead.
|
||||
*
|
||||
* An `_on` handler clears *every* facing and movement bit before setting its
|
||||
* own, so holding two directions at once moves in whichever was pressed last
|
||||
* rather than diagonally.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Start the actor moving left, and turn it to face left.
|
||||
* @param obj The actor to move, supplied by the control map. Required, along
|
||||
* with its `basechar`, whose acceleration is negated onto `ax`.
|
||||
* @param event The event that triggered this. Required, but not read -- the
|
||||
* binding has already decided what the event means.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Stop the actor moving left, zeroing everything on the x axis.
|
||||
* @param obj The actor to stop. Required. `basechar` is not needed here.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
|
||||
* @note It clears the x axis whichever way the actor was going, so releasing
|
||||
* left while holding right also stops the rightward movement.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Start the actor moving right, and turn it to face right.
|
||||
* @param obj The actor to move. Required, along with its `basechar`.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Stop the actor moving right, zeroing everything on the x axis.
|
||||
* @param obj The actor to stop. Required.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Start the actor moving up the screen, and turn it to face up.
|
||||
* @param obj The actor to move. Required, along with its `basechar`, whose y
|
||||
* acceleration is negated -- y grows downward.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Stop the actor moving up, zeroing everything on the y axis.
|
||||
* @param obj The actor to stop. Required.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
|
||||
* @note This also zeroes `ey`, so an actor under gravity has its accumulated
|
||||
* fall cancelled by releasing a movement key.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Start the actor moving down the screen, and turn it to face down.
|
||||
* @param obj The actor to move. Required, along with its `basechar`.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Stop the actor moving down, zeroing everything on the y axis.
|
||||
* @param obj The actor to stop. Required.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
|
||||
* @note Zeroes `ey` as well; see akgl_Actor_cmhf_up_off.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event);
|
||||
|
||||
/**
|
||||
* @brief `SDL_EnumerateProperties` callback that applies an akgl_Iterator to one registered actor.
|
||||
*
|
||||
* Runs the operations the iterator asks for, in a fixed order regardless of the
|
||||
* order the bits were set: layer filter, update, tilemap scaling, render. With
|
||||
* #AKGL_ITERATOR_OP_LAYERMASK set, an actor on any other layer is skipped
|
||||
* entirely; with #AKGL_ITERATOR_OP_TILEMAPSCALE clear, `scale` is forced to 1.0
|
||||
* rather than left alone.
|
||||
*
|
||||
* @param userdata The akgl_Iterator carrying the operation flags, passed through
|
||||
* by `SDL_EnumerateProperties`. Required despite the `void *`.
|
||||
* @param registry #AKGL_REGISTRY_ACTOR, supplied by SDL.
|
||||
* @param name The actor's registry key. Required.
|
||||
*
|
||||
* @warning This is an SDL callback, so it returns `void` and has nowhere to
|
||||
* propagate an error to. It ends in `FINISH_NORETURN`, which logs the
|
||||
* stack trace and then calls libakerror's unhandled-error handler --
|
||||
* whose default implementation **exits the process** with the error
|
||||
* status. An actor whose `updatefunc` fails therefore terminates the
|
||||
* game rather than skipping a frame. Install your own
|
||||
* `akerr_handler_unhandled_error` if that is not what you want.
|
||||
*/
|
||||
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name);
|
||||
|
||||
#endif // _AKGL_ACTOR_H_
|
||||
44
include/akgl/assets.h
Normal file
44
include/akgl/assets.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @file assets.h
|
||||
* @brief Loads the game's startup assets into the global mixer and track table.
|
||||
*/
|
||||
|
||||
#ifndef _ASSETS_H_
|
||||
#define _ASSETS_H_
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
/**
|
||||
* @brief Load a music file, bind it to the background-music track, and start it playing.
|
||||
*
|
||||
* Loads @p fname through `akgl_mixer`, creates a track for it, stores that track
|
||||
* in `akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM]`, and starts playback. The loaded
|
||||
* audio is also published as the global `bgm`. This is a *startup* helper: it
|
||||
* assumes the mixer already exists, so `akgl_game_init` (or a bare
|
||||
* `akgl_audio_init`) has to have run first, and it overwrites whatever the BGM
|
||||
* track slot held without releasing it.
|
||||
*
|
||||
* Loading is deferred inside SDL_mixer (`MIX_LoadAudio(..., true)` predecodes),
|
||||
* so a large file costs its decode time here rather than at first play.
|
||||
*
|
||||
* On any failure after the audio loads, the audio is destroyed again before the
|
||||
* error is returned; the track, if one was created, is not.
|
||||
*
|
||||
* @param fname Path to a music file in any format SDL_mixer can open. Required.
|
||||
* Used verbatim -- it is not resolved against `SDL_GetBasePath()`,
|
||||
* so a relative path is relative to the process working directory.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p fname is `NULL`, if SDL_mixer cannot load the
|
||||
* file (missing, unreadable, or an unsupported format), or if it cannot
|
||||
* allocate a track for it. The message carries `SDL_GetError()`.
|
||||
* @throws AKGL_ERR_SDL If the loaded audio cannot be bound to the new track, or
|
||||
* if playback fails to start.
|
||||
*
|
||||
* @note The infinite-loop request does not currently take effect:
|
||||
* `MIX_PROP_PLAY_LOOPS_NUMBER` is set on property set 0, which is the
|
||||
* "no properties" sentinel rather than a set this function owns, so the
|
||||
* call is rejected and the music plays once.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_load_start_bgm(char *fname);
|
||||
|
||||
#endif //_ASSETS_H_
|
||||
321
include/akgl/audio.h
Normal file
321
include/akgl/audio.h
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* @file audio.h
|
||||
* @brief Declares the public audio API.
|
||||
*
|
||||
* A small tone generator: a fixed set of voices, each with a waveform, a
|
||||
* frequency -- held, or walking toward another one -- a gate length and an ADSR
|
||||
* envelope, mixed to one stream of float samples. This is what a synthesised-
|
||||
* voice vocabulary needs -- SOUND, PLAY, ENVELOPE and VOL all describe a note
|
||||
* rather than a recording -- and it is deliberately separate from the SDL3_mixer
|
||||
* side of the library, which loads and plays audio *assets*. Nothing here reads
|
||||
* a file.
|
||||
*
|
||||
* The voice table exists whether or not an audio device is open.
|
||||
* akgl_audio_init() connects it to one; without that a caller can still set up
|
||||
* voices and pull samples itself with akgl_audio_mix(), which is how the test
|
||||
* suite exercises the synthesis without depending on a sound card's timing.
|
||||
* What a caller cannot do is set up voices and expect to hear them with no
|
||||
* device open.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_AUDIO_H_
|
||||
#define _AKGL_AUDIO_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/types.h>
|
||||
|
||||
/**
|
||||
* @brief Voices that can sound at once.
|
||||
*
|
||||
* Three, because the machine this vocabulary comes from had three and its
|
||||
* music is written for three. A voice is addressed here by a zero-based index;
|
||||
* a language whose own voices are numbered from one maps them itself.
|
||||
*/
|
||||
#define AKGL_AUDIO_MAX_VOICES 3
|
||||
|
||||
/** @brief Sample rate of the generated stream, in frames per second. */
|
||||
#define AKGL_AUDIO_SAMPLE_RATE 44100
|
||||
|
||||
/** @brief Frames the device callback generates per pass through the mixer. */
|
||||
#define AKGL_AUDIO_MIX_FRAMES 512
|
||||
|
||||
/**
|
||||
* @brief How often a frequency sweep takes one step, in steps per second.
|
||||
*
|
||||
* Sixty, because the machine whose SOUND statement this serves advanced its
|
||||
* sweep on a 60 Hz interrupt and its tunes are written for that rate. It
|
||||
* divides #AKGL_AUDIO_SAMPLE_RATE exactly, so a step boundary always lands on a
|
||||
* whole frame.
|
||||
*/
|
||||
#define AKGL_AUDIO_SWEEP_TICK_HZ 60
|
||||
|
||||
/** @brief Frames between two steps of a sweep. #AKGL_AUDIO_SAMPLE_RATE / #AKGL_AUDIO_SWEEP_TICK_HZ. */
|
||||
#define AKGL_AUDIO_SWEEP_TICK_FRAMES (AKGL_AUDIO_SAMPLE_RATE / AKGL_AUDIO_SWEEP_TICK_HZ)
|
||||
|
||||
/**
|
||||
* @brief Shape of one voice's oscillator.
|
||||
*
|
||||
* The trailing comment on each is the waveform number the C128 SOUND statement
|
||||
* uses for the same shape, for a caller translating one to the other.
|
||||
*/
|
||||
typedef enum {
|
||||
AKGL_AUDIO_WAVE_TRIANGLE = 0, /**< SOUND waveform 0. Soft, flute-like. */
|
||||
AKGL_AUDIO_WAVE_SAWTOOTH = 1, /**< SOUND waveform 1. Bright, brassy. */
|
||||
AKGL_AUDIO_WAVE_SQUARE = 2, /**< SOUND waveform 2. Hollow, reedy. The default. */
|
||||
AKGL_AUDIO_WAVE_NOISE = 3, /**< SOUND waveform 3. Unpitched; percussion. */
|
||||
AKGL_AUDIO_WAVE_SINE = 4 /**< No SOUND equivalent. A pure tone. */
|
||||
} akgl_AudioWaveform;
|
||||
|
||||
/** @brief Holds one voice's oscillator, envelope, and how far through it is. */
|
||||
typedef struct {
|
||||
bool active; /**< Whether this voice is sounding. Cleared by the mixer once gate and release are both spent, so it goes quiet without being told to. */
|
||||
akgl_AudioWaveform waveform; /**< Oscillator shape. Persists across notes; set once with akgl_audio_waveform(). */
|
||||
float32_t hz; /**< Frequency of the current note. 0.0 when the voice has never sounded. */
|
||||
/**
|
||||
* @brief Position through one cycle, 0.0 to 1.0.
|
||||
*
|
||||
* Derived from `elapsed_frames` and `hz` each sample rather than
|
||||
* accumulated, so a long note does not drift off pitch. Reading it is
|
||||
* meaningful; writing it is not.
|
||||
*/
|
||||
float32_t phase;
|
||||
/** @brief Frames the gate stays open, before the release begins. */
|
||||
uint32_t duration_frames;
|
||||
/** @brief Frames generated since the tone started, gate and release. */
|
||||
uint32_t elapsed_frames;
|
||||
/** @brief Frames to rise from silence to full level. 0 starts at full level. */
|
||||
uint32_t attack_frames;
|
||||
/** @brief Frames to fall from full level to `sustain`. 0 drops to it at once. */
|
||||
uint32_t decay_frames;
|
||||
/** @brief Frames to fall to silence after the gate closes. 0 cuts off at once. */
|
||||
uint32_t release_frames;
|
||||
/** @brief Level the envelope decays to and holds, 0.0 to 1.0. */
|
||||
float32_t sustain;
|
||||
/** @brief Pitch the current sweep started from. Equal to `sweep_to_hz` when the voice is not sweeping. */
|
||||
float32_t sweep_from_hz;
|
||||
/** @brief Pitch the sweep is heading for. The pitch holds there once it arrives. */
|
||||
float32_t sweep_to_hz;
|
||||
/** @brief Hertz added or subtracted per sweep step. 0.0 means this voice holds one pitch. */
|
||||
float32_t sweep_step_hz;
|
||||
} akgl_AudioVoice;
|
||||
|
||||
/** @brief The process-wide voice table. */
|
||||
extern akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES];
|
||||
|
||||
/**
|
||||
* @brief Open an audio device and start pulling samples from the voice table.
|
||||
*
|
||||
* Requires SDL's audio subsystem to be initialized. Repeating the call is a
|
||||
* no-op, so a program that cannot order its initialization precisely may call
|
||||
* it more than once. The voice table is reset only on the first call, so this
|
||||
* does not silence a voice that is already sounding.
|
||||
*
|
||||
* The device is resumed immediately rather than left paused, which is SDL's
|
||||
* default -- a caller who set up a voice and heard nothing would have no error
|
||||
* to explain it.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_SDL If no playback device can be opened -- none present, none
|
||||
* permitted, or SDL's audio subsystem never initialized -- or if the
|
||||
* device cannot be resumed. The message carries `SDL_GetError()`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void);
|
||||
|
||||
/**
|
||||
* @brief Close the audio device and silence every voice.
|
||||
*
|
||||
* Safe to call when no device is open, and safe to call twice. It also puts the
|
||||
* voice table back to its defaults, so a subsequent akgl_audio_init() starts
|
||||
* from a known state rather than from whatever was left sounding.
|
||||
*
|
||||
* @return `NULL`. There is no failure path -- SDL's stream teardown reports
|
||||
* nothing.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void);
|
||||
|
||||
/**
|
||||
* @brief Sound a note on one voice for a fixed time.
|
||||
*
|
||||
* The voice's envelope and waveform are whatever akgl_audio_envelope() and
|
||||
* akgl_audio_waveform() last set them to. @p ms is the length of the gate: the
|
||||
* voice's release runs *after* it, so a voice with a release stays audible
|
||||
* slightly longer than @p ms. Sounding a voice that is already sounding
|
||||
* restarts it from the beginning of its envelope, and clears any sweep
|
||||
* akgl_audio_sweep() left on it -- this is one pitch, held.
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @param hz Frequency in hertz. Must be greater than 0. There is no upper
|
||||
* bound check, so a frequency above half the sample rate aliases
|
||||
* rather than being refused.
|
||||
* @param ms Gate length in milliseconds. Must be non-zero -- a zero-length
|
||||
* tone is refused rather than treated as "stop", because
|
||||
* akgl_audio_stop() already means that. Rounded down to a whole
|
||||
* number of frames, so a duration under ~0.023 ms rounds to
|
||||
* nothing.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, if @p hz is not
|
||||
* positive, or if @p ms is 0. Each message says which.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms);
|
||||
|
||||
/**
|
||||
* @brief Sound a note whose pitch walks from one frequency toward another.
|
||||
*
|
||||
* A siren, a laser, a falling bomb: akgl_audio_tone() with the pitch moving.
|
||||
* The note starts at @p from_hz and steps by @p step_hz every
|
||||
* 1/#AKGL_AUDIO_SWEEP_TICK_HZ of a second toward @p to_hz, stopping when it
|
||||
* arrives and holding there for whatever is left of @p ms. Direction comes from
|
||||
* the two frequencies, not from the sign of @p step_hz -- a sweep to a lower
|
||||
* pitch is @p to_hz below @p from_hz, and @p step_hz stays positive.
|
||||
*
|
||||
* The step is taken on the mixer's own frame counter, which is the whole reason
|
||||
* this is here rather than in a caller's step loop: a caller re-issuing tones
|
||||
* ties audible pitch to how often it happens to run, so the same siren changes
|
||||
* shape with the frame rate. Nothing outside this library can see that counter.
|
||||
*
|
||||
* Everything else matches akgl_audio_tone(): the envelope, the waveform, the
|
||||
* gate, and the release that follows it. Equal frequencies are legal and are
|
||||
* simply a held tone, so a caller translating a statement that computes its own
|
||||
* limits does not have to special-case them.
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @param from_hz Pitch the note starts at. Must be greater than 0.
|
||||
* @param to_hz Pitch the sweep walks toward and then holds. Must be greater
|
||||
* than 0. Below @p from_hz sweeps down, above it sweeps up,
|
||||
* equal to it holds.
|
||||
* @param step_hz Hertz per step. Must be greater than 0; the last step is
|
||||
* clamped to @p to_hz rather than overshooting it. A step larger
|
||||
* than the whole interval arrives in one tick.
|
||||
* @param ms Gate length in milliseconds. Must be non-zero. A gate shorter
|
||||
* than the sweep needs is not an error -- the note ends partway
|
||||
* through, which is what a short siren sounds like.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, if @p from_hz,
|
||||
* @p to_hz or @p step_hz is not positive, or if @p ms is 0. Each
|
||||
* message says which.
|
||||
*
|
||||
* @note A swept voice accumulates its phase one frame at a time instead of
|
||||
* deriving it from the frame counter the way a held note does. Deriving
|
||||
* it assumes a constant frequency, and using it here would jump the
|
||||
* waveform -- an audible click -- at every step. The cost is the small
|
||||
* drift the derived form exists to avoid, which a note that is changing
|
||||
* pitch anyway cannot be said to suffer from.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms);
|
||||
|
||||
/**
|
||||
* @brief Silence one voice immediately, skipping its release.
|
||||
*
|
||||
* A hard cut, not a note-off: the release stage is not run, so the sound stops
|
||||
* on the next sample. Stopping a voice that is already silent is a no-op.
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice);
|
||||
|
||||
/**
|
||||
* @brief Choose the oscillator shape one voice will use.
|
||||
*
|
||||
* Takes effect on the next akgl_audio_tone(); it does not reshape a note that
|
||||
* is already sounding.
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @param waveform One of the ::akgl_AudioWaveform shapes. The setting persists
|
||||
* across notes until changed.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, or @p waveform is
|
||||
* not one of the five defined shapes.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform);
|
||||
|
||||
/**
|
||||
* @brief Set one voice's ADSR envelope.
|
||||
*
|
||||
* @p attack, @p decay and @p release are milliseconds; @p sustain is the level
|
||||
* the envelope decays to and holds while the gate is open, from 0.0 to 1.0. A
|
||||
* zero-length stage is skipped rather than divided by.
|
||||
*
|
||||
* Takes effect on the next akgl_audio_tone().
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @param attack Milliseconds to rise from silence to full level. 0 starts at
|
||||
* full level.
|
||||
* @param decay Milliseconds to fall from full level to @p sustain. 0 drops to
|
||||
* it at once.
|
||||
* @param sustain Held level while the gate is open, 0.0 to 1.0 inclusive. 0.0 is
|
||||
* legal and means the note is audible only through its attack and
|
||||
* decay -- a plucked sound.
|
||||
* @param release Milliseconds to fall to silence once the gate closes. 0 cuts
|
||||
* off at once. An attack plus decay longer than the note's gate
|
||||
* is not an error: the release simply starts partway up the ramp.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, or @p sustain is
|
||||
* outside 0.0 to 1.0. The three durations are unbounded.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release);
|
||||
|
||||
/**
|
||||
* @brief Set the level every voice is scaled by.
|
||||
*
|
||||
* Applied after the voices are summed and before the mix is clamped, so it
|
||||
* takes effect on notes already sounding. akgl_audio_shutdown() puts it back
|
||||
* to 1.0.
|
||||
*
|
||||
* @param level Master level, 0.0 to 1.0 inclusive. 0.0 is silence.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p level is outside 0.0 to 1.0. The message
|
||||
* reports it.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level);
|
||||
|
||||
/**
|
||||
* @brief Report whether a voice is still sounding.
|
||||
*
|
||||
* A voice goes quiet on its own when its gate and release have both elapsed, so
|
||||
* this is how a caller waits out a note without keeping its own clock.
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @param active Receives `true` while the voice is sounding, including during
|
||||
* its release. Required -- the return value is the error context.
|
||||
* The flag is only cleared by the mixer, so a voice whose time is
|
||||
* up still reads active until samples are next generated; with no
|
||||
* device open and nobody calling akgl_audio_mix(), it never
|
||||
* changes.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table.
|
||||
* @throws AKERR_NULLPOINTER If @p active is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active);
|
||||
|
||||
/**
|
||||
* @brief Generate the next @p frames mono samples from the voice table.
|
||||
*
|
||||
* The device callback installed by akgl_audio_init() is a loop around this. A
|
||||
* host that owns its own audio pipeline can call it directly instead and never
|
||||
* open a device here at all. Samples are single-precision, one channel, in the
|
||||
* range -1.0 to 1.0, and every active voice is advanced by @p frames.
|
||||
*
|
||||
* @param dest Receives @p frames samples. Required, and must have room for all
|
||||
* of them -- the count is trusted, not checked against anything.
|
||||
* Overwritten, not accumulated into.
|
||||
* @param frames How many samples to generate. 0 is a no-op, not an error.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p frames is negative.
|
||||
*
|
||||
* @warning This mutates the voice table -- it advances each voice's frame
|
||||
* counter and clears `active` on voices that have finished. Calling it
|
||||
* while a device opened by akgl_audio_init() is running means two
|
||||
* threads advancing the same voices, and this path does not take the
|
||||
* stream lock. Use one or the other, not both.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames);
|
||||
|
||||
#endif // _AKGL_AUDIO_H_
|
||||
164
include/akgl/character.h
Normal file
164
include/akgl/character.h
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @file character.h
|
||||
* @brief The reusable half of an actor: movement constants and a state-to-sprite map.
|
||||
*
|
||||
* A character is the template; an akgl_Actor is the instance. Everything that is
|
||||
* the same for every goblin on the map -- top speed, acceleration, frame dwell
|
||||
* time, and which sprite to draw for which combination of actor-state bits --
|
||||
* lives here and is shared by pointer, so a hundred actors cost one character.
|
||||
*
|
||||
* Characters are pool objects (akgl_heap_next_character) and are published in
|
||||
* the #AKGL_REGISTRY_CHARACTER property registry under their name, which is how
|
||||
* akgl_actor_set_character finds them. akgl_registry_init must have run first.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_CHARACTER_H_
|
||||
#define _AKGL_CHARACTER_H_
|
||||
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include "types.h"
|
||||
#include "sprite.h"
|
||||
|
||||
#define AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH 128
|
||||
#define AKGL_MAX_HEAP_CHARACTER 256
|
||||
|
||||
/** @brief Defines reusable movement parameters and actor-state sprite bindings. */
|
||||
typedef struct akgl_Character {
|
||||
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. */
|
||||
char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH]; /**< Registry key. Truncated, not rejected, if the source name is longer. */
|
||||
SDL_PropertiesID state_sprites; /**< State bitmask (decimal, as a string) -> akgl_Sprite *. */
|
||||
uint64_t speedtime; /**< Nanoseconds one sprite frame is held before advancing. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONESEC_MS, which despite its name is nanoseconds-per-millisecond. TODO.md item 6. */
|
||||
float32_t ax; /**< Acceleration along x, world units per second squared. Copied into an actor by akgl_actor_set_character. */
|
||||
float32_t ay; /**< Acceleration along y. */
|
||||
float32_t az; /**< Acceleration along z. Not read from JSON; stays 0 unless set by hand. */
|
||||
float32_t sx; /**< Maximum speed along x, world units per second. */
|
||||
float32_t sy; /**< Maximum speed along y. */
|
||||
float32_t sz; /**< Maximum speed along z. Not read from JSON; stays 0 unless set by hand. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_add)(struct akgl_Character *, akgl_Sprite *, int); /**< Bound to akgl_character_sprite_add by akgl_character_initialize. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_get)(struct akgl_Character *, int, akgl_Sprite **); /**< Bound to akgl_character_sprite_get by akgl_character_initialize. */
|
||||
} akgl_Character;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Zero a pooled character, name it, and publish it in the character registry.
|
||||
*
|
||||
* Wipes the struct, copies @p name into it, creates the empty state-to-sprite
|
||||
* property set, binds the `sprite_add`/`sprite_get` function pointers, inserts
|
||||
* the character into #AKGL_REGISTRY_CHARACTER under @p name, and takes the first
|
||||
* reference. Everything numeric (speeds, accelerations, `speedtime`) is left at
|
||||
* zero for the caller -- or akgl_character_load_json -- to fill in.
|
||||
*
|
||||
* @param basechar Pooled character to initialize, normally straight from
|
||||
* akgl_heap_next_character. Required. Any previous contents are
|
||||
* discarded without releasing the sprites they referenced.
|
||||
* @param name Registry key, NUL-terminated. Required. Truncated at
|
||||
* #AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH. A name already in the
|
||||
* registry is silently replaced, and the character it displaced
|
||||
* becomes unreachable rather than being released.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p basechar or @p name is `NULL`, or if SDL
|
||||
* cannot create the state-to-sprite property set (out of memory).
|
||||
* @throws AKERR_KEY If the character cannot be written into
|
||||
* #AKGL_REGISTRY_CHARACTER -- in practice, because akgl_registry_init
|
||||
* has not run and the registry id is still 0.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *basechar, char *name);
|
||||
/**
|
||||
* @brief Bind a sprite to one exact combination of actor-state bits.
|
||||
*
|
||||
* The map key is the decimal spelling of @p state, so lookups match the *whole*
|
||||
* value: a sprite added for `FACE_LEFT|MOVING_LEFT` is not found by a lookup for
|
||||
* `FACE_LEFT` alone. Callers are expected to register every combination they
|
||||
* intend to draw. Adding a sprite takes a reference on it.
|
||||
*
|
||||
* @param basechar Character to add the mapping to. Required. Must already have
|
||||
* been through akgl_character_initialize.
|
||||
* @param ref Sprite to draw for @p state. Required. Its `refcount` is
|
||||
* incremented, so the character keeps it alive.
|
||||
* @param state The exact actor-state bitmask (`AKGL_ACTOR_STATE_*`) this
|
||||
* sprite is for. 0 is accepted and is a usable key.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p basechar or @p ref is `NULL`.
|
||||
*
|
||||
* @note Re-adding a different sprite for a @p state that is already mapped
|
||||
* replaces the entry without releasing the sprite it displaced, so the
|
||||
* displaced sprite's reference is never given back.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state);
|
||||
/**
|
||||
* @brief Look up the sprite bound to one exact combination of actor-state bits.
|
||||
*
|
||||
* The counterpart to akgl_character_sprite_add, and matched the same way: an
|
||||
* exact match on the whole @p state value, with no fallback to a subset of the
|
||||
* bits and no default sprite. Unlike most lookups in this codebase, *not finding
|
||||
* one is an error* rather than a successful "nothing here" -- an actor with no
|
||||
* sprite for its current state cannot be drawn.
|
||||
*
|
||||
* @param basechar Character to search. Required.
|
||||
* @param state The exact actor-state bitmask to look up.
|
||||
* @param dest Receives the mapped sprite. Required -- the return value is
|
||||
* spoken for by the error context. Set to `NULL` when there is
|
||||
* no mapping, alongside the AKERR_KEY error.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p basechar or @p dest is `NULL`.
|
||||
* @throws AKERR_KEY If no sprite is bound to exactly that @p state. The message
|
||||
* carries the state both as a decimal and as a bit pattern.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest);
|
||||
|
||||
// This is an SDL iterator so we can't return our error state from it.
|
||||
/**
|
||||
* @brief `SDL_EnumerateProperties` callback that applies an akgl_Iterator to one state-to-sprite entry.
|
||||
*
|
||||
* Currently implements exactly one operation: with #AKGL_ITERATOR_OP_RELEASE
|
||||
* set, it hands the mapped sprite back to its heap layer. Every other flag is
|
||||
* ignored here. Used to tear a character's sprite map down entry by entry.
|
||||
*
|
||||
* @param userdata The akgl_Iterator carrying the operation flags, passed through
|
||||
* by `SDL_EnumerateProperties`. Required despite the `void *` --
|
||||
* a `NULL` here is an error, not "no operations".
|
||||
* @param props The character's `state_sprites` set, supplied by SDL.
|
||||
* @param name The property key: one state bitmask in decimal. Required.
|
||||
*
|
||||
* @warning This is an SDL callback, so it returns `void` and has nowhere to
|
||||
* propagate an error to. It ends in `FINISH_NORETURN`, which logs the
|
||||
* stack trace and then calls libakerror's unhandled-error handler --
|
||||
* whose default implementation **exits the process** with the error
|
||||
* status. A missing sprite or a `NULL` @p userdata therefore terminates
|
||||
* the game rather than skipping an entry. Install your own
|
||||
* `akerr_handler_unhandled_error` if that is not what you want.
|
||||
*/
|
||||
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name);
|
||||
|
||||
/**
|
||||
* @brief Build a character from a JSON definition file and register it.
|
||||
*
|
||||
* Claims a character from the pool, then reads: `name` (the registry key),
|
||||
* `sprite_mappings` (an array of `{ "sprite": <name>, "state": [<state names>] }`
|
||||
* objects), `speedtime` in seconds, `speed_x`, `speed_y`, `acceleration_x`, and
|
||||
* `acceleration_y`. Each mapping's `state` array is OR-ed together into one
|
||||
* bitmask by looking every name up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS, so
|
||||
* `["FACE_LEFT", "MOVING_LEFT"]` becomes a single key.
|
||||
*
|
||||
* Every referenced sprite must already be in #AKGL_REGISTRY_SPRITE: this loads
|
||||
* characters, not sprites, so akgl_sprite_load_json runs first. On any failure
|
||||
* the pooled character is released again.
|
||||
*
|
||||
* @param filename Path to the JSON document. Required. Used verbatim -- it is
|
||||
* not resolved against `SDL_GetBasePath()`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p filename is `NULL`; if the file cannot be
|
||||
* opened or does not parse (the message carries jansson's line number
|
||||
* and text); or if a mapping names a sprite that is not in the sprite
|
||||
* registry.
|
||||
* @throws AKERR_KEY If a `state` array contains a name that is not a known
|
||||
* actor state, if a required top-level key is absent, or if the
|
||||
* character cannot be added to the registry.
|
||||
* @throws AKERR_TYPE If a key is present but holds the wrong JSON type -- for
|
||||
* example `speedtime` as a string. The message names the key.
|
||||
* @throws AKERR_OUTOFBOUNDS If a `state` array is indexed past its end.
|
||||
* @throws AKGL_ERR_HEAP If the character pool or the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_load_json(char *filename);
|
||||
|
||||
#endif // _AKGL_CHARACTER_H_
|
||||
337
include/akgl/controller.h
Normal file
337
include/akgl/controller.h
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* @file controller.h
|
||||
* @brief Input binding: SDL events in, actor state changes out.
|
||||
*
|
||||
* A control map ties one actor to one keyboard and one gamepad, and holds up to
|
||||
* #AKGL_MAX_CONTROLS bindings. A binding says "when event X arrives from device
|
||||
* Y carrying button or key Z, call this handler". Eight maps means up to eight
|
||||
* locally controlled players, each on its own device.
|
||||
*
|
||||
* The host pumps SDL events into akgl_controller_handle_event(), which scans the
|
||||
* maps in order and stops at the first binding that claims the event -- so a key
|
||||
* bound in two maps only fires once, in the lower-numbered one.
|
||||
*
|
||||
* Alongside that, and independent of it, every key press is pushed into a small
|
||||
* ring buffer that akgl_controller_poll_key() and akgl_controller_poll_keystroke()
|
||||
* drain. That serves a caller that wants "is there a key waiting" without owning
|
||||
* an event loop, and it sees keys whether or not a control map also claimed them.
|
||||
*
|
||||
* The two pollers read the same buffer and differ in what they hand back. A game
|
||||
* asking "was the up arrow pressed" wants a keycode and nothing else, which is
|
||||
* akgl_controller_poll_key(). A line editor asking "what did the user type" needs
|
||||
* the modifier state and the character SDL composed -- there is no `"` keycode,
|
||||
* and no keycode at all for a dead key or an IME commit -- which is
|
||||
* akgl_controller_poll_keystroke().
|
||||
*/
|
||||
|
||||
#ifndef _CONTROLLER_H_
|
||||
#define _CONTROLLER_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include "types.h"
|
||||
// The binding handlers below take an akgl_Actor *, so this header cannot be
|
||||
// included before akgl/actor.h without one. akgl_Actor is a typedef of a named
|
||||
// struct, and repeating a typedef is not C99, so it is included rather than
|
||||
// forward-declared. actor.h reaches only types.h and character.h, so this does
|
||||
// not close a cycle.
|
||||
#include <akgl/actor.h>
|
||||
|
||||
/** @brief How many control maps exist -- effectively the local player limit. */
|
||||
#define AKGL_MAX_CONTROL_MAPS 8
|
||||
/** @brief Bindings per control map. The default map installed by akgl_controller_default uses 8 of them. */
|
||||
#define AKGL_MAX_CONTROLS 32
|
||||
|
||||
/**
|
||||
* @brief Keystrokes akgl_controller_handle_event() will hold for a poller.
|
||||
*
|
||||
* The Commodore keyboard buffer this serves held ten. Thirty-two is enough that
|
||||
* a program which polls once per frame never loses a key to a fast typist, and
|
||||
* small enough that the buffer stays a fixed-size object in the library's data
|
||||
* segment.
|
||||
*/
|
||||
#define AKGL_CONTROLLER_KEY_BUFFER 32
|
||||
|
||||
/**
|
||||
* @brief Bytes of composed text one buffered keystroke can carry, NUL included.
|
||||
*
|
||||
* A UTF-8 code point is at most four bytes, so this holds one character and its
|
||||
* terminator with room to spare. An input event carrying more than fits -- an
|
||||
* IME committing a whole word at once -- is truncated on a code point boundary
|
||||
* rather than split through the middle of one.
|
||||
*/
|
||||
#define AKGL_CONTROLLER_KEYSTROKE_TEXT 8
|
||||
|
||||
/** @brief One buffered keystroke: which key, which modifiers, and what it typed. */
|
||||
typedef struct {
|
||||
SDL_Keycode key; /**< The keycode, or 0 for an entry that carries only composed text -- an IME commit, or a character finished by a dead key. */
|
||||
SDL_Keymod mod; /**< Modifier state when the key went down. 0 on a text-only entry, whose text already reflects them. */
|
||||
char text[AKGL_CONTROLLER_KEYSTROKE_TEXT]; /**< What the keystroke composed to, UTF-8. The empty string for a key that produces no character, such as an arrow or a bare modifier. */
|
||||
} akgl_Keystroke;
|
||||
|
||||
/** @brief Maps one SDL input to pressed and released callbacks. */
|
||||
typedef struct {
|
||||
uint32_t event_on; /**< SDL event type that fires `handler_on`, e.g. `SDL_EVENT_KEY_DOWN`. */
|
||||
uint32_t event_off; /**< SDL event type that fires `handler_off`, e.g. `SDL_EVENT_KEY_UP`. */
|
||||
uint8_t button; /**< Gamepad button (`SDL_GamepadButton`) this binding is for. Only consulted for gamepad events. */
|
||||
SDL_Keycode key; /**< Keycode this binding is for. Only consulted for keyboard events. */
|
||||
uint8_t axis; /**< Analogue axis. Declared but not yet consulted by akgl_controller_handle_event. */
|
||||
uint8_t axis_range_min; /**< Low end of the axis range that counts as "on". Not yet consulted. */
|
||||
uint8_t axis_range_max; /**< High end of that range. Not yet consulted. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*handler_on)(akgl_Actor *obj, SDL_Event *event); /**< Called with the map's target on `event_on`. Required if `event_on` can fire. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event); /**< Called with the map's target on `event_off`. */
|
||||
} akgl_Control;
|
||||
|
||||
/** @brief Groups input bindings for one actor and its input devices. */
|
||||
typedef struct {
|
||||
akgl_Actor *target; /**< The actor these bindings drive. A `NULL` target makes the whole map inert, which is how an unused slot is spelled. */
|
||||
uint16_t nextMap; /**< Number of bindings in use; the index akgl_controller_pushmap writes to next. */
|
||||
akgl_Control controls[AKGL_MAX_CONTROLS]; /**< The bindings, scanned in order. */
|
||||
SDL_KeyboardID kbid; /**< Keyboard this map listens to. A keyboard event from any other id is ignored, which is what keeps two players on two keyboards apart. */
|
||||
SDL_JoystickID jsid; /**< Gamepad this map listens to, matched the same way. */
|
||||
SDL_MouseID mouseid; /**< Mouse this map listens to. Declared but not yet consulted. */
|
||||
SDL_PenID penid; /**< Pen this map listens to. Declared but not yet consulted. */
|
||||
} akgl_ControlMap;
|
||||
|
||||
/** @brief Every control map. Zeroed by akgl_game_init; index it by the same id the functions below take. */
|
||||
extern akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
|
||||
|
||||
/**
|
||||
* @brief Log every attached keyboard and its SDL id.
|
||||
*
|
||||
* A diagnostic, not a query: it writes to the SDL log rather than returning
|
||||
* anything. Its use is finding the `kbid` to hand akgl_controller_default when
|
||||
* more than one keyboard is attached.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot enumerate keyboards. The message
|
||||
* carries `SDL_GetError()`; note that "no keyboards attached" is
|
||||
* reported by SDL as an empty list, not as a failure.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void);
|
||||
|
||||
/**
|
||||
* @brief Dispatch one SDL event to whichever control map binds it.
|
||||
*
|
||||
* The entry point for the whole subsystem -- call it for every event the host
|
||||
* pumps. A key press is recorded in the poll buffer first, whether or not any
|
||||
* map wants it, and an `SDL_EVENT_TEXT_INPUT` is attached to the press it
|
||||
* belongs to; then the maps are scanned in index order and, within a map,
|
||||
* bindings in the order they were pushed. The **first** binding whose event type
|
||||
* and device id and button/key all match wins, and the scan stops there.
|
||||
*
|
||||
* An event nothing binds is not an error: it returns success having done
|
||||
* nothing, which is what lets a host pass every event through unconditionally.
|
||||
*
|
||||
* @param appstate Passed through from SDL's callback. Required -- but only as a
|
||||
* non-`NULL` token: nothing here reads it. Pass any non-`NULL`
|
||||
* pointer if your program has no app state.
|
||||
* @param event The event to dispatch. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
|
||||
* @throws AKERR_* Whatever the matched binding's handler raises.
|
||||
*
|
||||
* @warning A matched binding's handler pointer is not checked, so a control
|
||||
* pushed with a `NULL` `handler_on` or `handler_off` crashes when its
|
||||
* event arrives.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_event(void *appstate, SDL_Event *event);
|
||||
|
||||
/**
|
||||
* @brief Declared but not defined under this name. Do not call.
|
||||
*
|
||||
* The implementation exists as `gamepad_handle_button_down` in `src/controller.c`
|
||||
* and is `static`-in-spirit -- it is not declared anywhere -- so a caller that
|
||||
* uses this declaration compiles and then fails to link. TODO.md, "Known and
|
||||
* still open" item 10.
|
||||
*
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return Nothing; it cannot be called.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_down(void *appstate, SDL_Event *event);
|
||||
/**
|
||||
* @brief Declared but not defined under this name. Do not call.
|
||||
*
|
||||
* See akgl_controller_handle_button_down. The implementation is
|
||||
* `gamepad_handle_button_up`.
|
||||
*
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return Nothing; it cannot be called.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_up(void *appstate, SDL_Event *event);
|
||||
/**
|
||||
* @brief Declared but not defined under this name. Do not call.
|
||||
*
|
||||
* See akgl_controller_handle_button_down. The implementation is
|
||||
* `gamepad_handle_added`, which opens a newly plugged-in gamepad.
|
||||
*
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return Nothing; it cannot be called.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_added(void *appstate, SDL_Event *event);
|
||||
/**
|
||||
* @brief Declared but not defined under this name. Do not call.
|
||||
*
|
||||
* See akgl_controller_handle_button_down. The implementation is
|
||||
* `gamepad_handle_removed`, which closes an unplugged gamepad.
|
||||
*
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return Nothing; it cannot be called.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate, SDL_Event *event);
|
||||
|
||||
/**
|
||||
* @brief Append a binding to a control map.
|
||||
*
|
||||
* The binding is copied, so the caller's `akgl_Control` can be a stack local
|
||||
* reused across pushes -- which is exactly what akgl_controller_default does.
|
||||
* Bindings can only be appended; there is no remove, and no way to reset a map
|
||||
* short of zeroing it in ::GAME_ControlMaps directly.
|
||||
*
|
||||
* @param controlmapid Which map to append to, 0 through
|
||||
* #AKGL_MAX_CONTROL_MAPS - 1.
|
||||
* @param control The binding to copy in. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p control is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p controlmapid is at or above
|
||||
* #AKGL_MAX_CONTROL_MAPS, or if the map already holds
|
||||
* #AKGL_MAX_CONTROLS bindings.
|
||||
*
|
||||
* @warning A **negative** @p controlmapid is not rejected -- only the upper
|
||||
* bound is checked -- and indexes before the start of
|
||||
* ::GAME_ControlMaps. TODO.md, "Known and still open" item 11.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control);
|
||||
|
||||
/**
|
||||
* @brief Bind an actor to the arrow keys and the D-pad, in one call.
|
||||
*
|
||||
* Points the map at the named actor and pushes eight bindings: the four arrow
|
||||
* keys and the four D-pad directions, each wired to the matching
|
||||
* `akgl_Actor_cmhf_*_on`/`_off` pair. It is the "just give me something that
|
||||
* works" path -- a game wanting different keys builds its own bindings with
|
||||
* akgl_controller_pushmap.
|
||||
*
|
||||
* @param controlmapid Which map to configure, 0 through
|
||||
* #AKGL_MAX_CONTROL_MAPS - 1.
|
||||
* @param actorname Registry name of the actor to drive. Required in practice,
|
||||
* though a `NULL` is reported as "not found" rather than as
|
||||
* a null pointer.
|
||||
* @param kbid SDL keyboard id to listen to. Only events from this
|
||||
* keyboard match; see akgl_controller_list_keyboards for
|
||||
* how to find it.
|
||||
* @param jsid SDL gamepad id to listen to, matched the same way.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p controlmapid is at or above
|
||||
* #AKGL_MAX_CONTROL_MAPS, or if the map cannot hold eight more bindings.
|
||||
* @throws AKGL_ERR_REGISTRY If @p actorname is not in #AKGL_REGISTRY_ACTOR --
|
||||
* usually because the actor has not been created yet.
|
||||
*
|
||||
* @warning A negative @p controlmapid is not rejected. See
|
||||
* akgl_controller_pushmap.
|
||||
* @note It appends rather than replaces, so calling it twice on the same map
|
||||
* leaves sixteen bindings and the first eight are the ones that fire.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid);
|
||||
|
||||
/**
|
||||
* @brief Open every gamepad currently attached.
|
||||
*
|
||||
* SDL will not deliver button events from a gamepad nobody has opened, so this
|
||||
* runs once at startup -- akgl_game_init calls it. Devices plugged in later are
|
||||
* SDL_EVENT_GAMEPAD_ADDED events, handled elsewhere.
|
||||
*
|
||||
* No gamepads attached is success, not an error.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If SDL reports gamepads present but cannot
|
||||
* enumerate them, or if one of them cannot be opened. The message
|
||||
* carries `SDL_GetError()`.
|
||||
*
|
||||
* @note The enumeration array is only freed on the success path, so a failure
|
||||
* part-way through leaks it.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void);
|
||||
|
||||
/**
|
||||
* @brief Take the oldest waiting keystroke, if there is one.
|
||||
*
|
||||
* The rest of this header is built around the host pumping SDL events into
|
||||
* akgl_controller_handle_event(), which suits a game loop and does not suit an
|
||||
* embedded interpreter asking "is there a key waiting, yes or no" without
|
||||
* owning the event loop itself. Every key press that reaches
|
||||
* akgl_controller_handle_event() is recorded in a fixed ring buffer first,
|
||||
* whether or not a control map claims it, and this drains that buffer one
|
||||
* keystroke per call.
|
||||
*
|
||||
* When no key is waiting the call still succeeds: @p available is set to
|
||||
* `false` and @p keycode to 0. The caller polls, it does not block.
|
||||
*
|
||||
* A full buffer drops the *newest* keystroke rather than the oldest, so what
|
||||
* was typed first is what is read first. This runs on whichever thread pumps
|
||||
* events; it is not synchronized.
|
||||
*
|
||||
* Entries that carry only composed text and no keycode are discarded on the way
|
||||
* past rather than reported as keycode 0: this form is for a caller that acts on
|
||||
* keys, and there is no key to report. A caller that wants those characters uses
|
||||
* akgl_controller_poll_keystroke() instead.
|
||||
*
|
||||
* @param keycode Receives the SDL keycode, or 0 when nothing was waiting.
|
||||
* Required -- the return value is the error context.
|
||||
* @param available Receives `true` when a keystroke was taken, `false` when the
|
||||
* buffer was empty. Required. Check this rather than testing
|
||||
* @p keycode against 0.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p keycode or @p available is `NULL`. Both are
|
||||
* required; there is no "I only want to know whether one is waiting"
|
||||
* form.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available);
|
||||
|
||||
/**
|
||||
* @brief Take the oldest waiting keystroke whole: key, modifiers and text.
|
||||
*
|
||||
* akgl_controller_poll_key() with nothing thrown away. A line editor needs all
|
||||
* three: the keycode to recognise Backspace and Return, the modifier state to
|
||||
* tell Ctrl-C from a `c`, and the composed text because a keycode cannot express
|
||||
* what a shifted key produces on the user's own layout. `"`, `!`, `(`, `)`, `:`
|
||||
* and `;` are all unreachable from a keycode alone, and so is every lower-case
|
||||
* letter.
|
||||
*
|
||||
* The text is what SDL composed, taken from the `SDL_EVENT_TEXT_INPUT` that
|
||||
* follows the key press -- the only correct way to get a character out of SDL,
|
||||
* and what makes a keyboard layout, a compose key and a dead key work. **SDL
|
||||
* only sends those events while text input is started**, so a host that wants
|
||||
* the `text` field populated calls `SDL_StartTextInput()` on its window first.
|
||||
* Without it, `key` and `mod` still arrive and `text` is always empty.
|
||||
*
|
||||
* The two pollers drain the same buffer, so a keystroke taken by one is not
|
||||
* waiting for the other.
|
||||
*
|
||||
* @param dest Receives the keystroke. Required. Written only when a
|
||||
* keystroke was waiting; zeroed otherwise, so `key` is 0 and
|
||||
* `text` is the empty string.
|
||||
* @param available Receives `true` when a keystroke was taken, `false` when the
|
||||
* buffer was empty. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest or @p available is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available);
|
||||
|
||||
/**
|
||||
* @brief Discard every keystroke waiting in the buffer.
|
||||
*
|
||||
* For a caller that has been ignoring input and does not want a backlog acted
|
||||
* on the moment it starts polling again.
|
||||
*
|
||||
* This empties only the polling buffer. Keys already dispatched to control maps
|
||||
* have had their effect and cannot be taken back.
|
||||
*
|
||||
* @return `NULL`. There is no failure path -- it resets two counters.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void);
|
||||
#endif // _CONTROLLER_H_
|
||||
236
include/akgl/draw.h
Normal file
236
include/akgl/draw.h
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* @file draw.h
|
||||
* @brief Declares the public draw API.
|
||||
*
|
||||
* Immediate-mode plotting against whichever renderer the caller hands in. This
|
||||
* is the shape a BASIC-style graphics vocabulary needs -- DRAW, BOX, CIRCLE,
|
||||
* PAINT, SSHAPE and GSHAPE all say "put this on the screen now" rather than
|
||||
* "add this to the scene" -- and it sits alongside the actor and tilemap
|
||||
* rendering rather than replacing it.
|
||||
*
|
||||
* Every entry point takes its color as an argument instead of reading a
|
||||
* current-color global. A caller that has a notion of a current color (a BASIC
|
||||
* COLOR statement, say) already owns that state and does not need the library
|
||||
* to keep a second copy that can disagree with it. The renderer's own draw
|
||||
* color is saved and restored around each call, so drawing a line never changes
|
||||
* what the next SDL_RenderClear() paints.
|
||||
*/
|
||||
|
||||
#ifndef _DRAW_H_
|
||||
#define _DRAW_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/types.h>
|
||||
|
||||
/**
|
||||
* @brief Spans akgl_draw_flood_fill() will hold while walking a region.
|
||||
*
|
||||
* The fill keeps a fixed stack of horizontal runs still to be examined rather
|
||||
* than recursing per pixel. A region complicated enough to need more than this
|
||||
* many pending runs at once reports AKERR_OUTOFBOUNDS instead of overflowing;
|
||||
* an ordinary convex or moderately concave shape needs a few dozen.
|
||||
*/
|
||||
#define AKGL_DRAW_MAX_FLOOD_SPANS 4096
|
||||
|
||||
/**
|
||||
* @brief Paint an 8x8 grey checkerboard over a region, the way an image editor shows transparency.
|
||||
*
|
||||
* A diagnostic backdrop, not a general primitive -- `charviewer` uses it so a
|
||||
* sprite's transparent pixels are visible rather than blending into black. It
|
||||
* is the one function in this file that does not follow the file's conventions:
|
||||
* it draws through the *global* `renderer` rather than a backend the caller
|
||||
* passes in, it leaves the renderer's draw colour changed, and it reports
|
||||
* nothing.
|
||||
*
|
||||
* @param w Width of the region to cover, in pixels, starting at x = 0.
|
||||
* @param h Height of the region, starting at y = 0. Both round up to whole 8px
|
||||
* cells, so a 12-pixel height paints 16.
|
||||
*/
|
||||
void akgl_draw_background(int w, int h);
|
||||
|
||||
/**
|
||||
* @brief Plot a single pixel.
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param x Horizontal position in render-target pixels. Outside the target
|
||||
* is clipped by SDL, not reported.
|
||||
* @param y Vertical position.
|
||||
* @param color Colour to plot in, including alpha. Blending follows the
|
||||
* renderer's current blend mode, which this does not change.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the plot
|
||||
* itself fails. The message carries `SDL_GetError()`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Draw a line between two points, endpoints included.
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param x1 Horizontal position of the first endpoint.
|
||||
* @param y1 Vertical position of the first endpoint.
|
||||
* @param x2 Horizontal position of the second endpoint.
|
||||
* @param y2 Vertical position of the second endpoint. Coincident endpoints
|
||||
* draw a single pixel rather than nothing.
|
||||
* @param color Colour to draw in, including alpha.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the line
|
||||
* cannot be drawn.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Draw the outline of a rectangle.
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param rect The rectangle, in render-target pixels. Required. A zero or
|
||||
* negative width or height draws nothing and is not reported.
|
||||
* @param color Colour to draw in, including alpha.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p rect is
|
||||
* `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the
|
||||
* outline cannot be drawn.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Fill a rectangle, outline included.
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param rect The rectangle, in render-target pixels. Required. A zero or
|
||||
* negative width or height fills nothing and is not reported.
|
||||
* @param color Colour to fill with, including alpha.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p rect is
|
||||
* `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the fill
|
||||
* cannot be drawn.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Draw the outline of a circle.
|
||||
*
|
||||
* SDL3 has no circle primitive, so this plots one with the midpoint circle
|
||||
* algorithm -- integer arithmetic, eight-way symmetry, one pass per octant. A
|
||||
* radius of zero draws the center pixel and nothing else.
|
||||
*
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param x Horizontal position of the centre. Rounded to the nearest whole
|
||||
* pixel -- the algorithm is integer-only.
|
||||
* @param y Vertical position of the centre, rounded the same way.
|
||||
* @param radius Radius in pixels, rounded to the nearest whole pixel. Must not
|
||||
* be negative; 0 draws the centre pixel alone.
|
||||
* @param color Colour to draw in, including alpha. The outline is one pixel
|
||||
* wide and is not anti-aliased.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p radius is negative. The message reports it.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if any of
|
||||
* the plotting passes fails. Failures inside the loop are recorded and
|
||||
* reported once at the end rather than aborting mid-circle, so a partial
|
||||
* arc may already be on the target.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Flood the connected region containing one pixel with a color.
|
||||
*
|
||||
* SDL3 has no flood fill either, and unlike the shape primitives it cannot be
|
||||
* done on the GPU side: the region is defined by what is already on the screen.
|
||||
* This reads the render target back, walks the region on the CPU with a
|
||||
* bounded span stack, and blits the result over the area it touched.
|
||||
*
|
||||
* Filling a region that already holds @p color is a no-op rather than an error.
|
||||
* A seed outside the render target reports AKERR_OUTOFBOUNDS.
|
||||
*
|
||||
* The region is four-connected -- it spreads up, down, left and right, not
|
||||
* diagonally -- and its boundary is any pixel whose colour differs from the
|
||||
* seed's, exactly. There is no tolerance, so an anti-aliased edge stops the fill
|
||||
* at its first blended pixel and leaves a fringe.
|
||||
*
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param x Horizontal position of the seed pixel, in render-target pixels.
|
||||
* Must be inside the target.
|
||||
* @param y Vertical position of the seed pixel.
|
||||
* @param color Colour to fill with. Written over the region rather than blended,
|
||||
* since the pixels going back are the ones just read out.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If the seed is outside the render target -- the
|
||||
* message reports both the seed and the target size -- or if the region
|
||||
* needs more than #AKGL_DRAW_MAX_FLOOD_SPANS pending spans, in which
|
||||
* case **it is left partially filled**.
|
||||
* @throws AKGL_ERR_SDL If the render target cannot be read back, converted,
|
||||
* uploaded, or blitted.
|
||||
*
|
||||
* @warning Not reentrant, and not safe from two threads: the span stack is a
|
||||
* single file-scope array. Nothing that touches an `SDL_Renderer` is
|
||||
* thread-safe either way.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Read a rectangle of the render target into a surface.
|
||||
*
|
||||
* The save half of SSHAPE/GSHAPE. When `*dest` is `NULL` the function allocates
|
||||
* the surface and the caller owns it from then on -- release it with
|
||||
* SDL_DestroySurface(). When `*dest` already points at a surface of exactly
|
||||
* @p src's dimensions the pixels are copied into it instead, so a caller
|
||||
* saving the same region repeatedly does not churn allocations.
|
||||
*
|
||||
* @param self The backend to read from. Required, along with its `sdl_renderer`.
|
||||
* @param src Rectangle of the render target to read, in pixels. Required. It
|
||||
* must fit *entirely* inside the target: SDL would otherwise clip
|
||||
* the read and hand back a smaller surface than was asked for, which
|
||||
* a caller pasting it back would not notice.
|
||||
* @param dest Address of the destination surface. Required, and `*dest` must be
|
||||
* initialized -- `NULL` to have one allocated (the caller then owns
|
||||
* it and frees it with `SDL_DestroySurface`), or an existing surface
|
||||
* of exactly @p src's dimensions to reuse.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, @p src, or @p dest
|
||||
* is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p src has no area, if it does not fit inside the
|
||||
* render target, or if a supplied `*dest` is a different size from
|
||||
* @p src. Each message reports both sets of dimensions.
|
||||
* @throws AKGL_ERR_SDL If the target size cannot be queried, the pixels cannot
|
||||
* be read, or the copy into a supplied `*dest` fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest);
|
||||
|
||||
/**
|
||||
* @brief Draw a saved surface back onto the render target.
|
||||
*
|
||||
* The restore half of SSHAPE/GSHAPE, taking what akgl_draw_copy_region()
|
||||
* produced. The surface is not consumed and may be pasted as many times as the
|
||||
* caller likes.
|
||||
*
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param src The surface to paste. Required. Drawn at its own size -- there is
|
||||
* no scaling -- and *replacing* what is on the target rather than
|
||||
* blending with it, which is GSHAPE's default behaviour and means a
|
||||
* saved region's transparent pixels come back as transparent rather
|
||||
* than letting the background show through.
|
||||
* @param x Horizontal position of the paste's left edge.
|
||||
* @param y Vertical position of its top edge. Parts falling outside the
|
||||
* target are clipped by SDL, not reported.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p src is
|
||||
* `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the surface cannot be uploaded as a texture, its blend
|
||||
* mode cannot be set, or the draw fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y);
|
||||
|
||||
#endif //_DRAW_H_
|
||||
78
include/akgl/error.h
Normal file
78
include/akgl/error.h
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @file error.h
|
||||
* @brief Declares the public error API.
|
||||
*/
|
||||
|
||||
#ifndef _ERROR_H_
|
||||
#define _ERROR_H_
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
/*
|
||||
* libakerror 1.0.0 is the floor. That release moved the status-name table into a
|
||||
* private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the
|
||||
* registry entry points raise akerr_ErrorContext * instead of returning int, and
|
||||
* the library gained an soname -- so a translation unit that pairs this header
|
||||
* with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem.
|
||||
*
|
||||
* libakerror publishes no version macro, so this feature-tests on
|
||||
* AKERR_FIRST_CONSUMER_STATUS, which that release introduced, rather than on a
|
||||
* version number that does not exist. Without the guard an embedded build is
|
||||
* fine but a stale installed header fails much further in, on the AKGL_ERR_*
|
||||
* codes below and again inside src/heap.c.
|
||||
*
|
||||
* See deps/libakerror/UPGRADING.md.
|
||||
*/
|
||||
#ifndef AKERR_FIRST_CONSUMER_STATUS
|
||||
#error "libakgl requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
|
||||
#endif
|
||||
|
||||
// This macro is used to silence warnings on string concatenation operations that may fail.
|
||||
// e.g., combining two element of PATH_MAX into a string buffer of AKGL_STRING_MAX_LENGTH.
|
||||
// We have to draw a line in the sand somewhere or we will just let our buffers grow forever
|
||||
// to keep the compiler happy.
|
||||
#define DISABLE_GCC_WARNING_FORMAT_TRUNCATION \
|
||||
_Pragma("GCC diagnostic push") \
|
||||
_Pragma("GCC diagnostic ignored \"-Wformat-truncation\"")
|
||||
|
||||
#define RESTORE_GCC_WARNINGS \
|
||||
_Pragma("GCC diagnostic pop")
|
||||
|
||||
// libakerror reserves statuses 0-255 for the host's errno values and its own
|
||||
// AKERR_* codes; consumers allocate from AKERR_FIRST_CONSUMER_STATUS upward.
|
||||
// These are fixed offsets from that base rather than from AKERR_LAST_ERRNO_VALUE
|
||||
// so that a libc which grows an errno cannot move them out from under us.
|
||||
//
|
||||
// akgl_error_init() reserves this whole band in one call and registers a name
|
||||
// for every code below. Add a code here and you must name it there, or it
|
||||
// prints as "Unknown Error" in every stack trace that carries it.
|
||||
#define AKGL_ERR_OWNER "libakgl"
|
||||
#define AKGL_ERR_BASE AKERR_FIRST_CONSUMER_STATUS
|
||||
|
||||
#define AKGL_ERR_SDL (AKGL_ERR_BASE + 0) /**< An SDL call failed; the message carries SDL_GetError() */
|
||||
#define AKGL_ERR_REGISTRY (AKGL_ERR_BASE + 1) /**< A registry property or lookup operation failed */
|
||||
#define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /**< A heap pool has no free object left to hand out */
|
||||
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */
|
||||
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */
|
||||
|
||||
// One past the last libakgl status. The reservation is all-or-nothing -- a
|
||||
// subset or superset of an existing one is refused -- so this must stay one
|
||||
// past the highest code above.
|
||||
#define AKGL_ERR_LIMIT (AKGL_ERR_BASE + 5)
|
||||
#define AKGL_ERR_COUNT (AKGL_ERR_LIMIT - AKGL_ERR_BASE)
|
||||
|
||||
/**
|
||||
* @brief Claim the libakgl status band and register a name for every code in it.
|
||||
*
|
||||
* Call this before anything else in libakgl. Every other subsystem raises
|
||||
* AKGL_ERR_* codes, and a code raised before this runs carries no name into its
|
||||
* stack trace. Repeating the call is a no-op, so a program that cannot order its
|
||||
* initialization precisely may call it more than once.
|
||||
*
|
||||
* @throws AKERR_STATUS_RANGE_OVERLAP When another component already owns part of the band.
|
||||
* @throws AKERR_STATUS_RANGE_FULL When libakerror has no reservation slots left.
|
||||
* @throws AKERR_STATUS_NAME_FULL When libakerror's name registry is full.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_error_init(void);
|
||||
|
||||
#endif // _ERROR_H_
|
||||
299
include/akgl/game.h
Normal file
299
include/akgl/game.h
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* @file game.h
|
||||
* @brief Process-wide game state, the startup sequence, and the per-frame tick.
|
||||
*
|
||||
* This is the top of the library. `akgl_game_init` brings up SDL, the pools, the
|
||||
* registries and the audio and font engines; `akgl_game_update` is one frame --
|
||||
* update every actor, step the physics, draw the world.
|
||||
*
|
||||
* There is exactly one of everything. `renderer`, `physics`, `camera`, and
|
||||
* `gamemap` are globals pointing at the `_akgl_*` storage below them, so a
|
||||
* program can swap in its own instance by reassigning the pointer without the
|
||||
* rest of the library knowing. That is the whole extent of the indirection:
|
||||
* there is no notion of two worlds at once.
|
||||
*
|
||||
* The startup order that actually works:
|
||||
*
|
||||
* 1. fill in `game.name`, `game.version`, and `game.uri` -- akgl_game_init
|
||||
* refuses to run without them;
|
||||
* 2. akgl_game_init();
|
||||
* 3. akgl_registry_load_properties() or akgl_set_property(), to configure
|
||||
* screen size, physics constants and so on;
|
||||
* 4. akgl_render_init2d(renderer) and akgl_physics_factory(physics, ...), both
|
||||
* of which read that configuration;
|
||||
* 5. load assets, then loop on akgl_game_update().
|
||||
*
|
||||
* @warning None of this is thread-safe beyond the `game.state` mutex, and that
|
||||
* mutex protects the state flags, not the pools or the registries.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_GAME_H_
|
||||
#define _AKGL_GAME_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
#include "types.h"
|
||||
#include "tilemap.h"
|
||||
#include "renderer.h"
|
||||
#include "physics.h"
|
||||
// AKGL_VERSION used to be defined here by hand, which is how akgl.pc came to
|
||||
// ship an empty Version field: nothing tied the two together.
|
||||
#include <akgl/version.h>
|
||||
|
||||
/** @brief Slot in ::akgl_tracks reserved for background music. Note that slot 0 is unused. */
|
||||
#define AKGL_GAME_AUDIO_TRACK_BGM 1
|
||||
/** @brief Size of the ::akgl_tracks table. Every simultaneous sound needs its own slot. */
|
||||
#define AKGL_GAME_AUDIO_MAX_TRACKS 64
|
||||
|
||||
/** @brief Nanoseconds in one second. The unit `SDL_GetTicksNS` reports in. */
|
||||
#define AKGL_TIME_ONESEC_NS 1000000000
|
||||
/**
|
||||
* @brief Misnamed: this is nanoseconds per **millisecond**, not milliseconds per second.
|
||||
*
|
||||
* One second in milliseconds is 1000. 1000000 is one millisecond in
|
||||
* nanoseconds, which is how sprite and character load actually use it -- as a
|
||||
* milliseconds-to-nanoseconds scale factor -- and is not how
|
||||
* akgl_game_state_lock uses it. TODO.md item 6 proposes renaming it
|
||||
* `AKGL_TIME_ONEMS_NS`.
|
||||
*/
|
||||
#define AKGL_TIME_ONESEC_MS 1000000
|
||||
|
||||
/* ==================== GAME STATE VARIABLES =================== */
|
||||
|
||||
/** @brief Describes a renderable frame. Declared but not used by anything in the library. */
|
||||
typedef struct {
|
||||
float32_t w; /**< Width in pixels. */
|
||||
float32_t h; /**< Height in pixels. */
|
||||
SDL_Texture *texture; /**< The frame's texture. */
|
||||
} akgl_Frame;
|
||||
|
||||
/** @brief Stores application-defined game-state flags. */
|
||||
typedef struct {
|
||||
int32_t flags; /**< Meaning is entirely the application's; the library never reads it. Guard changes with akgl_game_state_lock. */
|
||||
} akgl_GameState;
|
||||
|
||||
/** @brief Stores game metadata, timing, synchronization, and FPS accounting. */
|
||||
typedef struct {
|
||||
char libversion[32]; /**< libakgl's version, stamped by akgl_game_init. Compared on load to refuse a save from another build. */
|
||||
char version[32]; /**< The *application's* version. Caller-supplied, required, and must be a semver string. */
|
||||
char name[256]; /**< Application name. Caller-supplied and required; also becomes SDL's app metadata. */
|
||||
char uri[256]; /**< Application URI, e.g. a reverse-DNS identifier. Caller-supplied, required, and used as the window title. */
|
||||
akgl_GameState state; /**< The application's own state flags. */
|
||||
SDL_Mutex *statelock; /**< Guards `state`. Created by akgl_game_init. */
|
||||
int16_t fps; /**< Frames drawn during the last completed second. Recomputed once per second, not per frame. */
|
||||
SDL_Time gameStartTime; /**< `SDL_GetTicksNS()` at akgl_game_init. */
|
||||
SDL_Time lastIterTime; /**< Timestamp of the most recent akgl_game_updateFPS call. */
|
||||
SDL_Time lastFPSTime; /**< When `fps` was last recomputed. */
|
||||
int16_t framesSinceUpdate; /**< Frames counted so far in the current second. */
|
||||
void (*lowfpsfunc)(void); /**< Called every frame while `fps` is under 30. Defaults to akgl_game_lowfps; replace it to do something more useful than log. */
|
||||
} akgl_Game;
|
||||
|
||||
/** @brief The SDL window, created by akgl_render_init2d. `NULL` until then. */
|
||||
extern SDL_Window *window;
|
||||
/** @brief The background music, loaded by akgl_load_start_bgm. `NULL` until then. */
|
||||
extern MIX_Audio *bgm;
|
||||
/** @brief The mixer device, created by akgl_game_init. Everything audio goes through it. */
|
||||
extern MIX_Mixer *akgl_mixer;
|
||||
/** @brief Playback tracks by slot. #AKGL_GAME_AUDIO_TRACK_BGM is the music track; the rest are the application's to assign. */
|
||||
extern MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
|
||||
/** @brief Storage behind the default `camera`. Point `camera` elsewhere rather than reaching for this. */
|
||||
extern SDL_FRect _akgl_camera;
|
||||
/** @brief The one game object: metadata, timing, and FPS accounting. */
|
||||
extern akgl_Game game;
|
||||
/** @brief Storage behind the default `renderer`. */
|
||||
extern akgl_RenderBackend _akgl_renderer;
|
||||
/** @brief Storage behind the default `physics`. */
|
||||
extern akgl_PhysicsBackend _akgl_physics;
|
||||
/** @brief Storage behind the default `gamemap`. */
|
||||
extern akgl_Tilemap _akgl_gamemap;
|
||||
|
||||
/** @brief Currently active tilemap. */
|
||||
extern akgl_Tilemap *gamemap;
|
||||
/** @brief Currently active renderer. */
|
||||
extern akgl_RenderBackend *renderer;
|
||||
/** @brief Currently active physics backend. */
|
||||
extern akgl_PhysicsBackend *physics;
|
||||
/** @brief Currently active camera. */
|
||||
extern SDL_FRect *camera;
|
||||
|
||||
/**
|
||||
* @brief True when every bit of `y` is set in `x`. Not "any of them" -- all of them.
|
||||
* @warning Unparenthesized: it expands to `(x & y) == y`, so `!AKGL_BITMASK_HAS(a, b)`
|
||||
* parses as `!(a & b) == b`. Use #AKGL_BITMASK_HASNOT rather than
|
||||
* negating this. TODO.md item 21.
|
||||
*/
|
||||
#define AKGL_BITMASK_HAS(x, y) (x & y) == y
|
||||
/** @brief True when at least one bit of `y` is missing from `x`. Same parenthesization caveat as #AKGL_BITMASK_HAS. */
|
||||
#define AKGL_BITMASK_HASNOT(x, y) (x & y) != y
|
||||
/** @brief Set every bit of `y` in `x`. Modifies `x`. */
|
||||
#define AKGL_BITMASK_ADD(x, y) x |= y
|
||||
/** @brief Clear every bit of `y` in `x`. Modifies `x`. */
|
||||
#define AKGL_BITMASK_DEL(x, y) x &= ~(y)
|
||||
/** @brief Clear every bit of `x`. Carries its own trailing semicolon, so do not add another. */
|
||||
#define AKGL_BITMASK_CLEAR(x) x = 0;
|
||||
|
||||
/**
|
||||
* @brief Bring the whole library up: error codes, pools, registries, SDL, audio, fonts, gamepads.
|
||||
*
|
||||
* In order: claim the libakgl status band (so every later error has a name),
|
||||
* stamp the library version, start the frame clock, create the state mutex,
|
||||
* check that the caller filled in the three required `game` fields, zero the
|
||||
* pools, create the registries, hand SDL the app metadata, clear the control
|
||||
* maps, `SDL_Init` video/gamepad/audio, load the bundled controller database,
|
||||
* open any attached gamepads, start SDL_mixer and SDL_ttf, and finally point
|
||||
* `renderer`, `physics`, `camera`, and `gamemap` at their default storage.
|
||||
*
|
||||
* What it does *not* do: create the window, choose a physics backend, or load
|
||||
* any configuration. Those read properties, so they come after the caller has
|
||||
* set them. See the sequence at the top of this file.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If `game.name`, `game.version`, or `game.uri` is
|
||||
* empty. All three are required and there are no defaults -- the
|
||||
* window title, SDL's app metadata, and the savegame compatibility
|
||||
* check are all built from them.
|
||||
* @throws AKGL_ERR_SDL If the state mutex cannot be created, if `SDL_Init`
|
||||
* fails, if a controller-database entry is rejected, if SDL_mixer
|
||||
* cannot start or open the default playback device, or if SDL_ttf
|
||||
* cannot start. Each message carries `SDL_GetError()`.
|
||||
* @throws AKERR_STATUS_RANGE_OVERLAP If another component already owns part of
|
||||
* the libakgl status band. See akgl_error_init.
|
||||
* @throws AKERR_* Whatever the heap, registry, and controller initializers raise.
|
||||
*
|
||||
* @note It takes the state lock on the way in and releases it on the way out, so
|
||||
* a failure part-way through leaves the mutex held.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init();
|
||||
/**
|
||||
* @brief Declared but never defined. Do not call.
|
||||
*
|
||||
* There is no definition anywhere in the library, so a translation unit that
|
||||
* calls this compiles and then fails to link. Screen setup is akgl_render_init2d.
|
||||
* Tracked in TODO.md under header/implementation surface drift.
|
||||
*
|
||||
* @return Nothing; it cannot be called.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init_screen();
|
||||
/**
|
||||
* @brief Count this frame, and recompute the frame rate once a second has passed.
|
||||
*
|
||||
* Called at the top of akgl_game_update, so a caller running its own loop needs
|
||||
* to call it itself. `game.fps` is only refreshed when a full second has
|
||||
* elapsed, so it is a completed-second average rather than an instantaneous
|
||||
* figure -- and it reads 0 for the first second of the process, which is under
|
||||
* the low-FPS threshold and so fires `lowfpsfunc` on every frame until the first
|
||||
* second is up.
|
||||
*/
|
||||
void akgl_game_updateFPS();
|
||||
/**
|
||||
* @brief Write the game state and the name-to-pointer tables to a save file.
|
||||
*
|
||||
* Writes the `akgl_Game` struct verbatim, then four name tables -- actors,
|
||||
* sprites, spritesheets, characters -- each mapping a registered name to the
|
||||
* address the object had at save time, and each terminated by a zeroed name and
|
||||
* a zeroed pointer. The tables are what let akgl_game_load reconnect pointers
|
||||
* between objects that will be at different addresses next run.
|
||||
*
|
||||
* @param fpath Path to write. Required. Opened with `"wb"`; an existing file is
|
||||
* truncated.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p fpath is `NULL`.
|
||||
* @throws ENOENT, EACCES Or whatever else `fopen(3)` reports, with the path in
|
||||
* the message.
|
||||
* @throws AKERR_IO On a stream error or a short write.
|
||||
*
|
||||
* @note This is a partial implementation: the name tables are written but the
|
||||
* objects themselves are not, so the file is not yet enough to restore a
|
||||
* session. See also TODO.md, "Known and still open" item 7 -- the writer
|
||||
* and the reader disagree on the name-field widths, so a save with any
|
||||
* registered spritesheet cannot be read back.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath);
|
||||
/**
|
||||
* @brief Read a save file back, refusing one that does not match this build.
|
||||
*
|
||||
* Reads the saved `akgl_Game`, then rejects the file unless the library version,
|
||||
* the game version, the game name, and the game URI all match the running
|
||||
* program -- versions by exact semver equality, name and URI by string compare.
|
||||
* Only then does it copy the saved state over `game` and rebuild the four
|
||||
* old-address-to-current-object maps.
|
||||
*
|
||||
* @param fpath Path to read. Required. Opened with `"rb"`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p fpath is `NULL`.
|
||||
* @throws ENOENT, EACCES Or whatever else `fopen(3)` reports.
|
||||
* @throws AKERR_EOF If a read runs off the end of the file -- which is what a
|
||||
* truncated or corrupt name table looks like.
|
||||
* @throws AKERR_IO On a stream error.
|
||||
* @throws AKERR_VALUE If either version string in the save file, or either in
|
||||
* the running game, is not valid semver.
|
||||
* @throws AKERR_API If the save file is from a different library version, game
|
||||
* version, game name, or game URI.
|
||||
*
|
||||
* @note Like akgl_game_save, this is partial: it rebuilds the pointer maps but
|
||||
* does not yet read back any objects. The four `SDL_CreateProperties`
|
||||
* sets it builds are never destroyed, so each call leaks them.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath);
|
||||
/**
|
||||
* @brief The default `game.lowfpsfunc`: log the current frame rate.
|
||||
*
|
||||
* Called from akgl_game_updateFPS on every frame where `game.fps` is under 30.
|
||||
* It is a placeholder -- the point of the hook is that a game can replace it
|
||||
* with something that actually sheds work.
|
||||
*/
|
||||
void akgl_game_lowfps(void);
|
||||
/**
|
||||
* @brief Take the game-state mutex, retrying rather than blocking.
|
||||
*
|
||||
* Polls with `SDL_TryLockMutex` on a 100 ms cadence instead of blocking
|
||||
* outright, so a deadlock reports an error rather than hanging the process.
|
||||
*
|
||||
* @return `NULL` once the lock is held, otherwise an error context owned by the
|
||||
* caller.
|
||||
* @throws AKGL_ERR_SDL If the budget runs out with the lock still held
|
||||
* elsewhere. The message carries `SDL_GetError()`, which after a failed
|
||||
* `SDL_TryLockMutex` is usually stale or empty -- the status is the
|
||||
* signal, not the text.
|
||||
*
|
||||
* @note The budget is meant to be one second but is not: the loop counts against
|
||||
* #AKGL_TIME_ONESEC_MS, which is nanoseconds-per-millisecond (1000000)
|
||||
* rather than milliseconds-per-second, so it retries 10,000 times at 100 ms
|
||||
* and gives up after roughly 16 minutes. TODO.md item 6.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void);
|
||||
/**
|
||||
* @brief Release the game-state mutex.
|
||||
*
|
||||
* @return `NULL`. `SDL_UnlockMutex` reports nothing, so there is no failure path
|
||||
* -- including for the case that matters, unlocking a mutex this thread
|
||||
* does not hold, which is undefined behaviour in SDL rather than an
|
||||
* error here.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
|
||||
/**
|
||||
* @brief One frame: update every actor layer by layer, step the physics, draw the world.
|
||||
*
|
||||
* Takes the state lock, counts the frame, then walks layers 0 through
|
||||
* #AKGL_TILEMAP_MAX_LAYERS calling each live actor's `updatefunc`, optionally
|
||||
* rescaling it to the tilemap first. Then it steps `physics` and draws through
|
||||
* `renderer`, and releases the lock.
|
||||
*
|
||||
* @param opflags Iterator flags. Optional -- `NULL` selects a default set that
|
||||
* sweeps one layer at a time, in which case `layerid` is advanced
|
||||
* by the loop. Only #AKGL_ITERATOR_OP_TILEMAPSCALE is read here;
|
||||
* with it clear every actor's `scale` is forced to 1.0. Note that
|
||||
* the flags are *not* forwarded to the physics or render calls,
|
||||
* both of which are passed `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_SDL If the state lock cannot be taken.
|
||||
* @throws AKERR_* Whatever an actor's `updatefunc`, akgl_tilemap_scale_actor,
|
||||
* the physics backend, or the renderer raises.
|
||||
*
|
||||
* @warning Every failure path returns with the state lock still held, and a live
|
||||
* actor's `updatefunc` is called without a `NULL` check -- a hand-built
|
||||
* actor that never went through akgl_actor_initialize crashes here.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags);
|
||||
|
||||
#endif //_AKGL_GAME_H_
|
||||
220
include/akgl/heap.h
Normal file
220
include/akgl/heap.h
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* @file heap.h
|
||||
* @brief The object pools. This library does not call `malloc`, it claims slots.
|
||||
*
|
||||
* Every runtime object -- actors, sprites, spritesheets, characters, strings --
|
||||
* comes out of a fixed, statically allocated array declared here. A "heap layer"
|
||||
* is one such array plus its `next`/`release` pair. Allocation is a linear scan
|
||||
* for a slot whose `refcount` is 0; release decrements, and the slot is zeroed
|
||||
* and unregistered when the count reaches 0.
|
||||
*
|
||||
* The consequence to design around is that exhaustion is a *normal* error, not
|
||||
* an out-of-memory catastrophe: AKGL_ERR_HEAP means the pool is full, and the
|
||||
* fix is usually a missing release rather than a bigger pool. Every ceiling
|
||||
* below is overridable at compile time, so a game that needs 256 actors defines
|
||||
* `AKGL_MAX_HEAP_ACTOR` before including this -- but the arrays are sized at
|
||||
* compile time, so the library and everything linking it must agree.
|
||||
*
|
||||
* If you need a new kind of runtime object, add a layer here. Do not reach for
|
||||
* the allocator.
|
||||
*
|
||||
* @warning The acquire functions are asymmetric: akgl_heap_next_string takes the
|
||||
* reference for you, and the other four do not -- their caller is
|
||||
* expected to take it, which in practice the `*_initialize` function
|
||||
* does. Until one is taken the slot is still free and the next
|
||||
* allocation hands out the same pointer. TODO.md, "Known and still
|
||||
* open" item 8.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_HEAP_H_
|
||||
#define _AKGL_HEAP_H_
|
||||
|
||||
#include "sprite.h"
|
||||
#include "actor.h"
|
||||
#include "character.h"
|
||||
#include "staticstring.h"
|
||||
#include <akerror.h>
|
||||
|
||||
#ifndef AKGL_MAX_HEAP_ACTOR
|
||||
#define AKGL_MAX_HEAP_ACTOR 64
|
||||
#endif
|
||||
#ifndef AKGL_MAX_HEAP_SPRITE
|
||||
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
|
||||
#endif
|
||||
#ifndef AKGL_MAX_HEAP_SPRITESHEET
|
||||
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
|
||||
#endif
|
||||
#ifndef AKGL_MAX_HEAP_CHARACTER
|
||||
#define AKGL_MAX_HEAP_CHARACTER 256
|
||||
#endif
|
||||
#ifndef AKGL_MAX_HEAP_STRING
|
||||
#define AKGL_MAX_HEAP_STRING 256
|
||||
#endif
|
||||
|
||||
/** @brief The actor pool. Public so the render and physics sweeps can walk it directly instead of going through the registry. */
|
||||
extern akgl_Actor HEAP_ACTOR[AKGL_MAX_HEAP_ACTOR];
|
||||
/** @brief The sprite pool. 16 per actor, on the assumption of one sprite per state combination. */
|
||||
extern akgl_Sprite HEAP_SPRITE[AKGL_MAX_HEAP_SPRITE];
|
||||
/** @brief The spritesheet pool. Sized like the sprite pool, though sharing means far fewer are used in practice. */
|
||||
extern akgl_SpriteSheet HEAP_SPRITESHEET[AKGL_MAX_HEAP_SPRITESHEET];
|
||||
/** @brief The character pool. */
|
||||
extern akgl_Character HEAP_CHARACTER[AKGL_MAX_HEAP_CHARACTER];
|
||||
/** @brief The string pool. Every entry is PATH_MAX bytes, so this is the largest of the five by a wide margin. */
|
||||
extern akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING];
|
||||
|
||||
/**
|
||||
* @brief Zero every pool, marking every slot free.
|
||||
*
|
||||
* Call once at startup, before anything allocates. Calling it again is a reset,
|
||||
* not a refresh: it does not release textures, clear registries, or consult
|
||||
* reference counts, so every live object becomes a dangling pointer and every
|
||||
* registry entry points at a zeroed slot. akgl_game_init calls it for you.
|
||||
*
|
||||
* @return `NULL`. There is no failure path today -- it is a series of `memset`s
|
||||
* -- but check it anyway; the signature exists so a layer that needs
|
||||
* real setup has somewhere to report from.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init();
|
||||
/**
|
||||
* @brief Zero the actor pool only.
|
||||
*
|
||||
* Split out from akgl_heap_init so a caller can reset the actors between levels
|
||||
* while keeping the loaded sprites, sheets, and characters -- those are the
|
||||
* expensive ones, since they own textures. Pair it with
|
||||
* akgl_registry_init_actor, which clears the matching registry.
|
||||
*
|
||||
* @return `NULL`. No failure path today; see akgl_heap_init.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor();
|
||||
/**
|
||||
* @brief Claim a free actor slot.
|
||||
* @param dest Receives a pointer to the free slot. Required, and **not**
|
||||
* checked -- a `NULL` here is a crash, not an error context. The
|
||||
* slot is *not* zeroed and its `refcount` is not incremented; it
|
||||
* stays free until akgl_actor_initialize takes the reference.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_ACTOR slots is in use.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_actor(akgl_Actor **dest);
|
||||
/**
|
||||
* @brief Claim a free sprite slot.
|
||||
* @param dest Receives a pointer to the free slot. Required and unchecked; not
|
||||
* zeroed, and no reference taken. See akgl_heap_next_actor.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_SPRITE slots is in use.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_sprite(akgl_Sprite **dest);
|
||||
/**
|
||||
* @brief Claim a free spritesheet slot.
|
||||
* @param dest Receives a pointer to the free slot. Required and unchecked; not
|
||||
* zeroed, and no reference taken. See akgl_heap_next_actor.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_SPRITESHEET slots is
|
||||
* in use.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest);
|
||||
/**
|
||||
* @brief Claim a free character slot.
|
||||
* @param dest Receives a pointer to the free slot. Required and unchecked; not
|
||||
* zeroed, and no reference taken. See akgl_heap_next_actor.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_CHARACTER slots is in
|
||||
* use.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_character(akgl_Character **dest);
|
||||
/**
|
||||
* @brief Claim a free string slot, and take the reference on it.
|
||||
*
|
||||
* The odd one out: this *does* increment `refcount`, so the slot is yours the
|
||||
* moment it returns and stays yours until you call akgl_heap_release_string.
|
||||
* That is what makes the scratch-buffer idiom -- claim, use, release in
|
||||
* `CLEANUP` -- safe. The contents are whatever the last holder left; call
|
||||
* akgl_string_initialize if you need it clean.
|
||||
*
|
||||
* @param dest Receives a pointer to the claimed slot. Required and **not**
|
||||
* checked -- a `NULL` here is a crash, not an error context.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_STRING slots is in
|
||||
* use. In practice this means a missing release somewhere, not a pool
|
||||
* that is genuinely too small.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_string(akgl_String **dest);
|
||||
|
||||
/**
|
||||
* @brief Drop a reference to an actor, tearing it down when the last one goes.
|
||||
*
|
||||
* At zero it releases every child recursively, clears the actor's entry from
|
||||
* #AKGL_REGISTRY_ACTOR, and zeroes the slot.
|
||||
*
|
||||
* @param ptr The actor to release. Required. A slot whose `refcount` is already
|
||||
* 0 is re-torn-down rather than rejected, which is harmless on a
|
||||
* zeroed slot and destructive on a live one that was never
|
||||
* registered.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
|
||||
*
|
||||
* @warning Children are released unconditionally, and the recursion has no cycle
|
||||
* check: an actor reachable from its own child list recurses until the
|
||||
* stack runs out.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_actor(akgl_Actor *ptr);
|
||||
/**
|
||||
* @brief Drop a reference to a sprite, tearing it down when the last one goes.
|
||||
*
|
||||
* At zero it clears the sprite's entry from #AKGL_REGISTRY_SPRITE and zeroes the
|
||||
* slot. The spritesheet it pointed at is *not* released -- the sprite only
|
||||
* borrowed it.
|
||||
*
|
||||
* @param ptr The sprite to release. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_sprite(akgl_Sprite *ptr);
|
||||
/**
|
||||
* @brief Drop a reference to a spritesheet, destroying its texture when the last one goes.
|
||||
*
|
||||
* The only release that frees a resource outside the pool: at zero it clears the
|
||||
* entry from #AKGL_REGISTRY_SPRITESHEET, destroys the `SDL_Texture`, and zeroes
|
||||
* the slot. Any sprite still pointing at the sheet is left with a dangling
|
||||
* pointer, since nothing takes a reference on a sheet on a sprite's behalf.
|
||||
*
|
||||
* @param ptr The spritesheet to release. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
|
||||
*
|
||||
* @note Destroying a texture is a main-thread operation in SDL, so this must be
|
||||
* called from the thread that owns the renderer.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_spritesheet(akgl_SpriteSheet *ptr);
|
||||
/**
|
||||
* @brief Drop a reference to a character, tearing it down when the last one goes.
|
||||
*
|
||||
* At zero it clears the entry from #AKGL_REGISTRY_CHARACTER and zeroes the slot.
|
||||
*
|
||||
* @param ptr The character to release. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
|
||||
*
|
||||
* @note It does not walk the state-to-sprite map, so the references the
|
||||
* character took in akgl_character_sprite_add are never given back, and
|
||||
* the SDL property set holding the map is leaked. Release the sprites
|
||||
* first with akgl_character_state_sprites_iterate and
|
||||
* #AKGL_ITERATOR_OP_RELEASE if you need them back.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_character(akgl_Character *ptr);
|
||||
/**
|
||||
* @brief Drop a reference to a pooled string, zeroing it when the last one goes.
|
||||
*
|
||||
* Strings are not registered anywhere, so this is just the reference count and a
|
||||
* wipe. It is safe to call on a string a function may or may not have claimed,
|
||||
* which is why the `CLEANUP` blocks in this library call it unconditionally.
|
||||
*
|
||||
* @param ptr The string to release. Required -- unlike the pattern elsewhere, a
|
||||
* `NULL` here is an error rather than a no-op, so `CLEANUP` blocks
|
||||
* wrap it in `IGNORE()`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_string(akgl_String *ptr);
|
||||
|
||||
#endif //_AKGL_HEAP_H_
|
||||
63
include/akgl/iterator.h
Normal file
63
include/akgl/iterator.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @file iterator.h
|
||||
* @brief The work order handed to a registry sweep.
|
||||
*
|
||||
* There is no iterator object with a `next()` on it. Traversal is SDL's --
|
||||
* `SDL_EnumerateProperties` over a registry -- and this struct is the `userdata`
|
||||
* carried into each callback, telling it *which* entries to touch and *what* to
|
||||
* do to each one. akgl_registry_iterate_actor and
|
||||
* akgl_registry_iterate_character are the callbacks that read it;
|
||||
* akgl_game_update, akgl_physics_simulate, and akgl_render_2d_draw_world are the
|
||||
* entry points that take one.
|
||||
*
|
||||
* The operations are independent bits, not an enum: a single sweep can update,
|
||||
* scale, and render, and they run in that fixed order regardless of how the bits
|
||||
* were set. Passing `NULL` where an `akgl_Iterator *` is expected is not an
|
||||
* error at the top-level entry points -- each substitutes its own default set --
|
||||
* but it *is* an error once inside a callback.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_ITERATOR_H_
|
||||
#define _AKGL_ITERATOR_H_
|
||||
|
||||
/** @brief Selects operations and an optional layer for actor traversal. */
|
||||
typedef struct {
|
||||
uint32_t flags; /**< Bitwise OR of the `AKGL_ITERATOR_OP_*` values below. */
|
||||
uint8_t layerid; /**< Layer to restrict the sweep to. Read only when #AKGL_ITERATOR_OP_LAYERMASK is set. */
|
||||
} akgl_Iterator;
|
||||
|
||||
#define AKGL_ITERATOR_OP_UPDATE 1 // 1 Call the actor's updatefunc
|
||||
#define AKGL_ITERATOR_OP_RENDER 1 << 1 // 2 Call the actor's renderfunc
|
||||
#define AKGL_ITERATOR_OP_RELEASE 1 << 2 // 4 Release the object back to its heap layer
|
||||
#define AKGL_ITERATOR_OP_LAYERMASK 1 << 3 // 8 Skip anything whose layer != layerid
|
||||
#define AKGL_ITERATOR_OP_TILEMAPSCALE 1 << 4 // 16 Scale actors to the tilemap; otherwise force scale 1.0
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_5 1 << 5 // 32
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_6 1 << 6 // 64
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_7 1 << 7 // 128
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_8 1 << 8 // 256
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_9 1 << 9 // 512
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_10 1 << 10 // 1024
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_11 1 << 11 // 2048
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_12 1 << 12 // 4096
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_13 1 << 13 // 8192
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_14 1 << 14 // 16384
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_15 1 << 15 // 32768
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_16 1 << 16 // 65536
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_17 1 << 17 // 131072
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_18 1 << 18 // 262144
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_19 1 << 19 // 524288
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_20 1 << 20 // 1048576
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_21 1 << 21 // 2097152
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_22 1 << 22 // 4194304
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_23 1 << 23 // 8388608
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_24 1 << 24 // 16777216
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_25 1 << 25 // 33554432
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_26 1 << 26 // 67108864
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_27 1 << 27 // 134217728
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_28 1 << 28 // 268435456
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_29 1 << 29 // 536870912
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_30 1 << 30 // 1073741824
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_31 1 << 31 // 2147483648
|
||||
|
||||
|
||||
#endif // _AKGL_ITERATOR_H_
|
||||
236
include/akgl/json_helpers.h
Normal file
236
include/akgl/json_helpers.h
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* @file json_helpers.h
|
||||
* @brief Typed jansson accessors that report "missing" and "wrong type" as errors.
|
||||
*
|
||||
* Every asset in this library is described by a JSON document, and jansson's own
|
||||
* accessors answer "missing key", "wrong type", and "index past the end" all
|
||||
* with the same `NULL`. These wrappers split those apart -- AKERR_KEY,
|
||||
* AKERR_TYPE, AKERR_OUTOFBOUNDS -- so a malformed asset file produces a message
|
||||
* naming the key and what was wrong with it, instead of a `NULL` dereference
|
||||
* three frames later.
|
||||
*
|
||||
* Conventions that run through the whole set, so they need not be repeated per
|
||||
* function:
|
||||
*
|
||||
* - **Absence is an error here.** Unlike the search functions in libakstdlib,
|
||||
* these treat a missing key as AKERR_KEY. An *optional* key is expressed by
|
||||
* passing the resulting error to akgl_get_json_with_default() rather than by
|
||||
* the accessor staying quiet.
|
||||
* - **The result comes back through @p dest**, because the return value is the
|
||||
* error context.
|
||||
* - **`json_t *` results are borrowed, not owned.** Objects and arrays are
|
||||
* returned as pointers into the document; they are valid until the document is
|
||||
* freed and must not be `json_decref`'d.
|
||||
* - **@p dest is not `NULL`-checked** except where noted, so a `NULL` there is a
|
||||
* crash rather than an error context.
|
||||
* - **The document itself is never modified.**
|
||||
*/
|
||||
|
||||
#ifndef _JSON_HELPERS_H_
|
||||
#define _JSON_HELPERS_H_
|
||||
|
||||
#include <akerror.h>
|
||||
#include "staticstring.h"
|
||||
|
||||
/**
|
||||
* @brief Read a nested object out of a JSON object.
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up. A `NULL` key is reported as a missing
|
||||
* key rather than as a `NULL` pointer.
|
||||
* @param dest Receives a borrowed pointer to the nested object; not written on
|
||||
* any failure path. Not checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent. The message names it.
|
||||
* @throws AKERR_TYPE If @p key is present but is not an object.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest);
|
||||
/**
|
||||
* @brief Read a boolean out of a JSON object.
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up.
|
||||
* @param dest Receives the value; not written on any failure path. Not checked
|
||||
* for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not `true` or `false`. A `0`
|
||||
* or `1` is a number in JSON, and is refused here.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest);
|
||||
/**
|
||||
* @brief Read an integer out of a JSON object.
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up.
|
||||
* @param dest Receives the value, narrowed to `int`. Not written on any failure
|
||||
* path, and not checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not an integer. This is strict:
|
||||
* `3.0` is a real in JSON and is refused, not truncated. Use
|
||||
* akgl_get_json_number_value() where either spelling should be accepted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_integer_value(json_t *obj, char *key, int *dest);
|
||||
/**
|
||||
* @brief Read a number out of a JSON object as a `float`.
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up.
|
||||
* @param dest Receives the value, narrowed to `float`. Not written on any
|
||||
* failure path, and not checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not a number. Integers and
|
||||
* reals are both accepted, so `1` and `1.0` behave alike.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char *key, float *dest);
|
||||
/**
|
||||
* @brief Read a number out of a JSON object as a `double`.
|
||||
*
|
||||
* The full-precision form of akgl_get_json_number_value(), for the physics
|
||||
* constants, which are `double`.
|
||||
*
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up.
|
||||
* @param dest Receives the value. Not written on any failure path, and not
|
||||
* checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not a number.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest);
|
||||
/**
|
||||
* @brief Read a string out of a JSON object into a pooled akgl_String.
|
||||
*
|
||||
* Unlike the other accessors this copies, because the caller wants a buffer it
|
||||
* can keep rather than a pointer into the document. `*dest` doubles as an input:
|
||||
* `NULL` means "claim one for me", non-`NULL` means "write into this one".
|
||||
*
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up. Required -- checked here, unlike the
|
||||
* other accessors in this file.
|
||||
* @param dest Address of the destination string. Required, and it must be
|
||||
* *initialized*: set `*dest` to `NULL` to have a pool string claimed
|
||||
* for you, or to a claimed string to write in place. An
|
||||
* indeterminate `*dest` is dereferenced. Either way the caller
|
||||
* releases it with akgl_heap_release_string().
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj, @p key, or @p dest is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not a string.
|
||||
* @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
|
||||
*
|
||||
* @note A value longer than #AKGL_MAX_STRING_LENGTH is truncated silently, and
|
||||
* is left without a terminator -- this is `strncpy`, not `strlcpy`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest);
|
||||
/**
|
||||
* @brief Read an array out of a JSON object.
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up.
|
||||
* @param dest Receives a borrowed pointer to the array; not written on any
|
||||
* failure path. Not checked for `NULL`. Use `json_array_size()` on
|
||||
* it and the `akgl_get_json_array_index_*` family to walk it.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not an array.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest);
|
||||
/**
|
||||
* @brief Read one element of a JSON array as an object.
|
||||
* @param array The array to read from. Required.
|
||||
* @param index Zero-based element index. A negative index is reported the same
|
||||
* way as one past the end.
|
||||
* @param dest Receives a borrowed pointer to the element; not written on any
|
||||
* failure path. Not checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p array is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array. The message
|
||||
* reports the index.
|
||||
* @throws AKERR_TYPE If the element is not an object.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest);
|
||||
/**
|
||||
* @brief Read one element of a JSON array as an integer.
|
||||
* @param array The array to read from. Required.
|
||||
* @param index Zero-based element index.
|
||||
* @param dest Receives the value, narrowed to `int`; not written on any failure
|
||||
* path. Not checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p array is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array.
|
||||
* @throws AKERR_TYPE If the element is not an integer. Strict, as in
|
||||
* akgl_get_json_integer_value().
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_integer(json_t *array, int index, int *dest);
|
||||
/**
|
||||
* @brief Read one element of a JSON array into a pooled akgl_String.
|
||||
* @param array The array to read from. Required.
|
||||
* @param index Zero-based element index.
|
||||
* @param dest Address of the destination string, with the same claim-or-reuse
|
||||
* contract as akgl_get_json_string_value(): required, must be
|
||||
* initialized, and released by the caller.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p array or @p dest is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array.
|
||||
* @throws AKERR_TYPE If the element is not a string.
|
||||
* @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
|
||||
*
|
||||
* @note Truncates silently at #AKGL_MAX_STRING_LENGTH, as
|
||||
* akgl_get_json_string_value() does.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest);
|
||||
|
||||
/**
|
||||
* @brief Turn a "key not found" error from one of the accessors above into a default value.
|
||||
*
|
||||
* This is how an optional key is spelled. Run the accessor, hand its error
|
||||
* context here along with the fallback, and a missing key becomes @p dest
|
||||
* holding @p defval and a `NULL` return; anything else propagates untouched:
|
||||
*
|
||||
* ```c
|
||||
* int width = 0;
|
||||
* int width_default = 32;
|
||||
* PASS(errctx,
|
||||
* akgl_get_json_with_default(
|
||||
* akgl_get_json_integer_value(json, "width", &width),
|
||||
* &width_default,
|
||||
* &width,
|
||||
* sizeof(int)
|
||||
* ));
|
||||
* ```
|
||||
*
|
||||
* A `NULL` @p e -- the accessor succeeded -- is the ordinary case and returns at
|
||||
* once without touching @p dest. When the error *is* consumed it is also
|
||||
* released, so the caller must not release it again.
|
||||
*
|
||||
* @param e The error context to inspect, straight from an accessor.
|
||||
* `NULL` means "no error", which is not itself an error.
|
||||
* @param defval The fallback value to copy. Required when @p e is non-`NULL`.
|
||||
* @param dest Where to copy it. Required when @p e is non-`NULL`. Must be the
|
||||
* same destination the accessor was given, and at least
|
||||
* @p defsize bytes.
|
||||
* @param defsize Bytes to copy out of @p defval. Trusted, not derived -- it must
|
||||
* match the type both sides actually are, since this is a
|
||||
* `memcpy` through `void *` with no type information.
|
||||
* @return `NULL` when the error was consumed or there was none, otherwise an
|
||||
* error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p e is non-`NULL` and @p defval or @p dest is
|
||||
* `NULL`.
|
||||
* @throws AKERR_* Whatever @p e carried, if it is not one of the statuses this
|
||||
* defaults on -- an AKERR_TYPE from a key that exists but is the wrong
|
||||
* type propagates, which is right: that is a malformed document, not an
|
||||
* omitted setting.
|
||||
*
|
||||
* @note It defaults on AKERR_KEY and AKERR_INDEX, but *not* on
|
||||
* AKERR_OUTOFBOUNDS -- which is the status the
|
||||
* `akgl_get_json_array_index_*` family actually raises for a short array.
|
||||
* So this pairs with the object accessors and does not currently give an
|
||||
* array index a default.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize);
|
||||
|
||||
#endif // _JSON_HELPERS_H_
|
||||
238
include/akgl/physics.h
Normal file
238
include/akgl/physics.h
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* @file physics.h
|
||||
* @brief The pluggable physics backend, and the two implementations that ship.
|
||||
*
|
||||
* Same shape as the renderer: a record of function pointers plus an initializer
|
||||
* that fills it in. `null` accepts every call and changes nothing, which is what
|
||||
* a menu screen or a test harness wants; `arcade` applies gravity, drag, thrust
|
||||
* and a speed cap. Selecting between them is akgl_physics_factory, driven by the
|
||||
* `physics.engine` configuration property -- not a branch in the simulation.
|
||||
*
|
||||
* The model an actor is simulated under, and the fields on akgl_Actor that carry
|
||||
* it:
|
||||
*
|
||||
* | Term | Fields | Comes from |
|
||||
* |-----------------------|--------------|-----------------------------------------------|
|
||||
* | thrust | `tx, ty, tz` | the actor's own acceleration while it is moving |
|
||||
* | environmental | `ex, ey, ez` | gravity, less drag |
|
||||
* | velocity | `vx, vy, vz` | thrust + environmental |
|
||||
* | max speed | `sx, sy, sz` | the character; caps thrust, not velocity |
|
||||
* | acceleration | `ax, ay, az` | the character |
|
||||
*
|
||||
* Each step: movement logic, then gravity, then drag, then velocity, then move.
|
||||
* Because the cap is applied to thrust rather than to velocity, gravity can
|
||||
* carry an actor faster than its stated top speed -- which is the point, for a
|
||||
* falling one.
|
||||
*/
|
||||
|
||||
#ifndef _PHYSICS_H_
|
||||
#define _PHYSICS_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/iterator.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
/** @brief Defines a pluggable physics backend and its environmental parameters. */
|
||||
typedef struct akgl_PhysicsBackend {
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*simulate)(struct akgl_PhysicsBackend *self, akgl_Iterator *opflags); /**< Step the whole world. Both backends point this at akgl_physics_simulate. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*gravity)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /**< Apply environmental acceleration to one actor. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*collide)(struct akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2); /**< Resolve a collision between two actors. Not called by the simulation loop yet. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /**< Commit one actor's velocity to its position. */
|
||||
|
||||
double drag_x; /**< Fraction of environmental velocity shed per second along x. 0 disables. From `physics.drag.x`. */
|
||||
double drag_y; /**< Drag along y. From `physics.drag.y`. */
|
||||
double drag_z; /**< Drag along z. From `physics.drag.z`. */
|
||||
double gravity_x; /**< Acceleration along x, world units per second squared. Applied toward screen left. From `physics.gravity.x`. */
|
||||
double gravity_y; /**< Acceleration along y. Applied down the screen, since y grows downward. From `physics.gravity.y`. */
|
||||
double gravity_z; /**< Acceleration along z. Applied away from the camera. From `physics.gravity.z`. */
|
||||
SDL_Time gravity_time; /**< Timestamp of the previous step, in nanoseconds. The simulation's `dt` is measured from this. */
|
||||
SDL_Time timer_gravity; /**< Unused. Nothing in the library reads or writes it. */
|
||||
} akgl_PhysicsBackend;
|
||||
|
||||
/**
|
||||
* @brief Gravity for the null backend: accept the call and change nothing.
|
||||
* @param self The backend. Required -- the one thing this does check.
|
||||
* @param actor The actor that would have been accelerated. Ignored; `NULL` is
|
||||
* accepted.
|
||||
* @param dt Seconds since the previous step. Ignored.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Collision for the null backend: accept the call and change nothing.
|
||||
*
|
||||
* Note that this *succeeds* where the arcade backend's collide refuses -- the
|
||||
* null backend's contract is "nothing collides", which is an answer, not a gap.
|
||||
*
|
||||
* @param self The backend. Required.
|
||||
* @param a1 First actor. Ignored; `NULL` is accepted.
|
||||
* @param a2 Second actor. Ignored; `NULL` is accepted.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
||||
/**
|
||||
* @brief Movement for the null backend: accept the call and change nothing.
|
||||
*
|
||||
* Actors under the null backend do not move on their own. Anything that sets an
|
||||
* actor's `x`/`y` directly still works -- this only declines to integrate
|
||||
* velocity.
|
||||
*
|
||||
* @param self The backend. Required.
|
||||
* @param actor The actor that would have moved. Ignored; `NULL` is accepted.
|
||||
* @param dt Seconds since the previous step. Ignored.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Install the null backend's methods.
|
||||
*
|
||||
* Reads no configuration and touches no gravity or drag fields, so whatever was
|
||||
* in them stays -- harmlessly, since none of the null methods look.
|
||||
*
|
||||
* @param self The backend to initialize. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *self);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Accelerate one actor's environmental velocity by gravity.
|
||||
*
|
||||
* Adds `gravity * dt` to the actor's `ex`/`ey`/`ez` on each axis whose gravity
|
||||
* is non-zero, with the sign chosen for screen space: x pulls left, y pulls
|
||||
* down, z pulls away from the camera. It touches only the environmental term, so
|
||||
* an actor's own thrust is unaffected and the speed cap does not apply -- which
|
||||
* is why a falling actor keeps accelerating past its character's top speed.
|
||||
*
|
||||
* @param self The backend supplying the gravity constants. Required.
|
||||
* @param actor The actor to accelerate. Required.
|
||||
* @param dt Seconds since the previous step. A `dt` of 0 is a no-op; a
|
||||
* negative one accelerates backwards.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or @p actor is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Collision for the arcade backend. Not implemented.
|
||||
*
|
||||
* Deliberately loud rather than silently permissive: unlike
|
||||
* akgl_physics_null_collide, which means "nothing collides", this means "nobody
|
||||
* has written this yet", and a caller that reaches it should find out.
|
||||
*
|
||||
* @param self The backend. Required, and checked before the refusal.
|
||||
* @param a1 First actor. Never examined.
|
||||
* @param a2 Second actor. Never examined.
|
||||
* @return Never `NULL`.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
* @throws AKERR_API Otherwise, always, with the message "Not implemented".
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
||||
/**
|
||||
* @brief Commit an actor's velocity to its position.
|
||||
*
|
||||
* The last step of a simulation tick: `position += velocity * dt` on all three
|
||||
* axes. It does not clamp to the map, consult the tilemap, or test for
|
||||
* collisions -- an actor will walk straight through a wall and off the edge of
|
||||
* the world.
|
||||
*
|
||||
* @param self The backend. Required, though nothing on it is read.
|
||||
* @param actor The actor to move. Required. Its `vx`/`vy`/`vz` must already have
|
||||
* been computed; akgl_physics_simulate does that immediately
|
||||
* before calling this.
|
||||
* @param dt Seconds since the previous step.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or @p actor is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Install the arcade backend's methods and read its constants from configuration.
|
||||
*
|
||||
* Reads `physics.gravity.x`, `.y`, `.z` and `physics.drag.x`, `.y`, `.z` from
|
||||
* the property registry, all defaulting to `"0.0"` -- so an arcade backend with
|
||||
* no configuration behaves like the null one until something is set.
|
||||
*
|
||||
* @param self The backend to initialize. Required. Its method pointers are
|
||||
* installed before the properties are read, so a failure part-way
|
||||
* leaves a usable backend with some constants still at their
|
||||
* previous values.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
* @throws AKERR_VALUE If one of the six properties is set to something that is
|
||||
* not a number.
|
||||
* @throws ERANGE If one of them does not fit a `double`.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @note With #AKGL_REGISTRY_PROPERTIES uninitialized every property reads back
|
||||
* as its default, so this silently configures zero gravity and zero drag
|
||||
* rather than reporting anything. See the warning in registry.h.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self);
|
||||
|
||||
/**
|
||||
* @brief Select and initialize a physics backend by name.
|
||||
*
|
||||
* The dispatch point for the whole subsystem: `"null"` and `"arcade"` are the
|
||||
* two names, and akgl_game_init passes whatever the `physics.engine` property
|
||||
* holds. Adding a backend means adding a name here, not a branch in the
|
||||
* simulation.
|
||||
*
|
||||
* @param self The backend to initialize. Required.
|
||||
* @param type The backend name. Required. Matched on its leading characters, so
|
||||
* `"nullify"` selects `null` and `"arcadia"` selects `arcade`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or @p type is `NULL`.
|
||||
* @throws AKERR_KEY If @p type matches neither name. The message quotes what was
|
||||
* asked for.
|
||||
* @throws AKERR_* Whatever the selected initializer raises.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type);
|
||||
|
||||
/**
|
||||
* @brief Step every live actor forward by the time elapsed since the previous call.
|
||||
*
|
||||
* Shared by both backends -- what differs is the `gravity` and `move` they point
|
||||
* at. For each live actor, in pool order:
|
||||
*
|
||||
* - a child actor is snapped to its parent's position plus its own `v` as an
|
||||
* offset, and skipped entirely -- children do not simulate;
|
||||
* - an actor with no character is skipped, since the speed and acceleration
|
||||
* constants live there;
|
||||
* - thrust accumulates along an axis the actor is moving on, then is clamped to
|
||||
* the character's maximum speed;
|
||||
* - the backend's `gravity` runs, drag is applied to the environmental term,
|
||||
* velocity becomes environmental plus thrust, and the backend's `move`
|
||||
* commits it.
|
||||
*
|
||||
* `dt` is measured from `self->gravity_time`, which is stamped at the end. The
|
||||
* first call after initialization therefore measures from 0 and produces an
|
||||
* enormous `dt` -- initialize `gravity_time` from `SDL_GetTicksNS()` before the
|
||||
* first step if that matters.
|
||||
*
|
||||
* @param self The backend. Required, along with its `move` pointer.
|
||||
* @param opflags Iterator flags. Optional -- `NULL` means "simulate everything".
|
||||
* Only #AKGL_ITERATOR_OP_LAYERMASK is honoured, restricting the
|
||||
* step to actors on `layerid`; the update, render, and release
|
||||
* bits are ignored here.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or `self->move` is `NULL`.
|
||||
* @throws AKERR_* Whatever an actor's `movementlogicfunc`, or the backend's
|
||||
* `gravity` or `move`, raises. The first failure aborts the whole step,
|
||||
* leaving the actors already processed advanced and the rest not.
|
||||
*
|
||||
* @note An actor's `movementlogicfunc` can raise AKGL_ERR_LOGICINTERRUPT to say
|
||||
* "skip the rest of the simulation for me this tick". It is handled here
|
||||
* and does not propagate -- it is a control signal wearing an error's
|
||||
* clothes, not a failure. Only `movementlogicfunc` gets that treatment:
|
||||
* the `gravity` and `move` calls return straight out of the loop on any
|
||||
* status, LOGICINTERRUPT included, so a backend must not use it to opt an
|
||||
* actor out from there.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags);
|
||||
|
||||
#endif // _PHYSICS_H_
|
||||
218
include/akgl/registry.h
Normal file
218
include/akgl/registry.h
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* @file registry.h
|
||||
* @brief The name-to-object lookup tables, and the string configuration store.
|
||||
*
|
||||
* Nothing in this library is passed around by pointer where a name will do. An
|
||||
* actor names the character it instantiates, a character names the sprites it
|
||||
* draws, a sprite names the sheet it cuts frames from -- all resolved at load
|
||||
* time through these eight registries. That is what lets the whole asset graph
|
||||
* be described in JSON files that reference each other by name.
|
||||
*
|
||||
* They are SDL property sets, not akgl types, so a caller can enumerate one with
|
||||
* `SDL_EnumerateProperties` -- which is exactly what akgl_registry_iterate_actor
|
||||
* and akgl_character_state_sprites_iterate are for.
|
||||
*
|
||||
* Every id starts at 0, which SDL treats as "no such property set": reads return
|
||||
* the default and writes are silently dropped. So an uninitialized registry does
|
||||
* not fail loudly, it fails quietly, and akgl_registry_init has to run first.
|
||||
*
|
||||
* @warning akgl_registry_init does *not* initialize
|
||||
* #AKGL_REGISTRY_PROPERTIES -- akgl_registry_init_properties is a
|
||||
* separate call, made by akgl_game_init but by nothing else. A program
|
||||
* that builds its own startup path and skips it gets a silently
|
||||
* no-op akgl_set_property and an akgl_get_property that always hands
|
||||
* back the caller's default, which in turn means akgl_render_init2d
|
||||
* and akgl_physics_init_arcade quietly ignore their configuration.
|
||||
* TODO.md, "Known and still open" item 3.
|
||||
*/
|
||||
|
||||
#ifndef _REGISTRY_H_
|
||||
#define _REGISTRY_H_
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
/** @brief Actor name -> `akgl_Actor *`. Written by akgl_actor_initialize, cleared when an actor's last reference goes. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR;
|
||||
/** @brief Sprite name -> `akgl_Sprite *`. The `name` field from a sprite JSON. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_SPRITE;
|
||||
/** @brief Resolved image path -> `akgl_SpriteSheet *`. Keyed by path so two sprites sharing an image share the texture. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET;
|
||||
/** @brief Character name -> `akgl_Character *`. What akgl_actor_set_character resolves against. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_CHARACTER;
|
||||
/** @brief Actor-state name -> its bit value, as a number. Lets character JSON say "AKGL_ACTOR_STATE_FACE_LEFT" instead of 2. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR_STATE_STRINGS;
|
||||
/** @brief Font name -> `TTF_Font *`. Names are chosen by the caller of akgl_text_loadfont, not derived from the file. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_FONT;
|
||||
/** @brief Music name -> audio handle. Created but not yet populated by anything in the library. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_MUSIC;
|
||||
/** @brief Configuration key -> string value. Read through akgl_get_property; everything is a string, including numbers. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_PROPERTIES;
|
||||
|
||||
/**
|
||||
* @brief Create the seven asset registries, in dependency order.
|
||||
*
|
||||
* Spritesheet, sprite, character, actor, actor-state-strings, font, music. Call
|
||||
* it once at startup, before loading anything.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate any one of the property sets;
|
||||
* the message names which registry failed. The registries created before
|
||||
* the failure are left in place rather than torn down.
|
||||
*
|
||||
* @warning This does not create #AKGL_REGISTRY_PROPERTIES -- see the warning on
|
||||
* this file.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init();
|
||||
/**
|
||||
* @brief Create the music registry.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set, so calling it
|
||||
* twice leaks the first one along with everything registered in it.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music();
|
||||
/**
|
||||
* @brief Create the font registry.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set. See
|
||||
* akgl_registry_init_music.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font();
|
||||
/**
|
||||
* @brief Create the actor registry, destroying any previous one first.
|
||||
*
|
||||
* The one initializer here that cleans up after itself, which is what makes it
|
||||
* safe to call between levels: it destroys the old property set before creating
|
||||
* the replacement, so the actors from the previous map are unregistered in one
|
||||
* step. Note that it destroys the *registry*, not the actors -- releasing those
|
||||
* is akgl_heap_release_actor's job.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set. The old one
|
||||
* has already been destroyed at that point, so the registry is left at 0.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor();
|
||||
/**
|
||||
* @brief Create the sprite registry.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set. See
|
||||
* akgl_registry_init_music.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite();
|
||||
/**
|
||||
* @brief Create the spritesheet registry.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set. See
|
||||
* akgl_registry_init_music.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet();
|
||||
/**
|
||||
* @brief Create the character registry.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set. See
|
||||
* akgl_registry_init_music.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character();
|
||||
/**
|
||||
* @brief Create the configuration-property registry.
|
||||
*
|
||||
* Deliberately separate from akgl_registry_init, because configuration has to be
|
||||
* in place before the subsystems that read it start up. akgl_game_init creates
|
||||
* it; filling it in -- with akgl_registry_load_properties or akgl_set_property --
|
||||
* is the caller's job, and has to happen before akgl_render_init2d or
|
||||
* akgl_physics_init_arcade run.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set, so every
|
||||
* property set before a second call is lost.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_properties();
|
||||
/**
|
||||
* @brief Load a JSON configuration file into the property registry.
|
||||
*
|
||||
* Expects a document with a top-level `properties` object whose members are all
|
||||
* strings; each becomes one entry. Numbers are configured as strings here and
|
||||
* parsed by whoever reads them -- `game.screenwidth` is `"800"`, not `800`.
|
||||
*
|
||||
* @param fname Path to the JSON document. Required. Used verbatim.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p fname is `NULL`, or if the file cannot be
|
||||
* opened or does not parse. The message carries jansson's line number
|
||||
* and text.
|
||||
* @throws AKERR_KEY If the document has no top-level `properties` member.
|
||||
* @throws AKERR_TYPE If `properties` is not an object, or if one of its members
|
||||
* is not a string.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @note Writes into #AKGL_REGISTRY_PROPERTIES, so akgl_registry_init_properties
|
||||
* has to have run -- otherwise every entry is silently dropped and this
|
||||
* still returns success.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname);
|
||||
/**
|
||||
* @brief Create the actor-state-name registry and fill it from the name table.
|
||||
*
|
||||
* Walks `AKGL_ACTOR_STATE_STRING_NAMES` and maps entry `i` to the value `1 << i`,
|
||||
* which is what lets a character JSON write `"AKGL_ACTOR_STATE_FACE_LEFT"` and
|
||||
* have akgl_character_load_json turn it into a bit.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set. The
|
||||
* individual name registrations are not checked.
|
||||
*
|
||||
* @note The name table it reads disagrees with actor.h in two places: bits 11
|
||||
* and 12 are named `UNDEFINED_11`/`UNDEFINED_12` rather than `MOVING_IN`
|
||||
* and `MOVING_OUT`, so those two states cannot be named from JSON at all.
|
||||
* TODO.md items 24-26.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor_state_strings();
|
||||
/**
|
||||
* @brief Set one configuration property.
|
||||
*
|
||||
* @param name Property key. Required.
|
||||
* @param value Property value. Required. SDL copies it, so the caller's buffer
|
||||
* can go away afterwards.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p name or @p value is `NULL`.
|
||||
*
|
||||
* @note The write itself is unchecked. If #AKGL_REGISTRY_PROPERTIES is still 0 --
|
||||
* akgl_registry_init_properties never having run -- this discards the
|
||||
* value and returns success.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *value);
|
||||
/**
|
||||
* @brief Read one configuration property into a pooled string, with a fallback.
|
||||
*
|
||||
* Absence is not an error: an unset property yields @p def, which is how every
|
||||
* caller in the library gets a working default without checking first.
|
||||
*
|
||||
* @param name Property key. Required.
|
||||
* @param dest Receives the value. Required. If `*dest` is `NULL` a string is
|
||||
* claimed from the pool for you, so initialize it to `NULL` on the
|
||||
* first call; either way the caller releases it with
|
||||
* akgl_heap_release_string. If `*dest` is non-`NULL` it is written
|
||||
* in place.
|
||||
* @param def Value to use when @p name is not set. Effectively required --
|
||||
* a `NULL` here on an unset property is an AKERR_NULLPOINTER rather
|
||||
* than an empty result.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p name or @p dest is `NULL`, or if the property
|
||||
* is unset and @p def is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If the stored value is #AKGL_MAX_STRING_LENGTH bytes
|
||||
* or longer, which will not fit an akgl_String with its terminator.
|
||||
* @throws AKERR_VALUE If the value and the destination overlap in memory.
|
||||
* @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
|
||||
*
|
||||
* @note Only the value and its terminator are copied. The rest of @p dest keeps
|
||||
* whatever the previous holder left there, so read the result as a C
|
||||
* string rather than as #AKGL_MAX_STRING_LENGTH bytes.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def);
|
||||
|
||||
#endif //_REGISTRY_H_
|
||||
200
include/akgl/renderer.h
Normal file
200
include/akgl/renderer.h
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* @file renderer.h
|
||||
* @brief The pluggable rendering backend: a record of function pointers plus an initializer.
|
||||
*
|
||||
* There is one backend shipped, the 2D SDL one, and `akgl_render_init2d` is its
|
||||
* initializer -- it creates the window and the `SDL_Renderer` and then fills in
|
||||
* the six `akgl_render_2d_*` entry points. A different renderer is a different
|
||||
* initializer populating the same struct, not a branch inside these functions.
|
||||
*
|
||||
* Those are two separable jobs, and a host that already owns an `SDL_Renderer`
|
||||
* -- an embedded interpreter, which must not create the window -- wants only the
|
||||
* second. akgl_render_bind2d() is that half on its own; akgl_render_init2d()
|
||||
* makes the window and then calls it.
|
||||
*
|
||||
* Callers do not normally name the `akgl_render_2d_*` functions directly; they
|
||||
* go through the pointers on the backend (`renderer->frame_start(renderer)`),
|
||||
* which is what makes the swap possible. The global `renderer` in game.h is the
|
||||
* instance the rest of the library draws through.
|
||||
*
|
||||
* A frame is `frame_start` (clear), any number of `draw_*` calls, then
|
||||
* `frame_end` (present).
|
||||
*/
|
||||
|
||||
#ifndef _RENDERER_H_
|
||||
#define _RENDERER_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/iterator.h>
|
||||
|
||||
/** @brief Defines a pluggable renderer backend and drawing callbacks. */
|
||||
typedef struct akgl_RenderBackend {
|
||||
SDL_Renderer *sdl_renderer; /**< The SDL renderer, created by the initializer. Owned by SDL, not freed here. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self); /**< Tear the backend down. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*frame_start)(struct akgl_RenderBackend *self); /**< Begin a frame: clear the target. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*frame_end)(struct akgl_RenderBackend *self); /**< End a frame: present it. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*draw_texture)(struct akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip); /**< Blit one texture. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*draw_mesh)(struct akgl_RenderBackend *self); /**< Reserved for a 3D backend; the 2D one refuses. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags); /**< Draw the whole scene, layer by layer. */
|
||||
} akgl_RenderBackend;
|
||||
|
||||
/**
|
||||
* @brief Tear the 2D backend down.
|
||||
*
|
||||
* A no-op placeholder that always succeeds. The window and `SDL_Renderer` this
|
||||
* backend created are still owned by SDL and are reclaimed by `SDL_Quit`, so
|
||||
* there is nothing here to release yet; the hook exists so a backend that *does*
|
||||
* own resources has somewhere to free them.
|
||||
*
|
||||
* @param self The backend to shut down. Ignored -- not even dereferenced, so
|
||||
* `NULL` is currently harmless.
|
||||
* @return `NULL` always. It has no failure path today; check it anyway, because
|
||||
* a backend that acquires anything will grow one.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief Begin a frame: clear the render target to opaque black.
|
||||
* @param self The backend to draw through. Required, and dereferenced *before*
|
||||
* it is checked -- a `NULL` @p self is a crash, not an error.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If `self->sdl_renderer` is `NULL`, which means the
|
||||
* backend was never run through akgl_render_init2d.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief End a frame: present everything drawn since akgl_render_2d_frame_start.
|
||||
* @param self The backend to draw through. Required, and dereferenced *before*
|
||||
* it is checked -- a `NULL` @p self is a crash, not an error.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If `self->sdl_renderer` is `NULL`, which means the
|
||||
* backend was never run through akgl_render_init2d.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief Blit a texture, rotating it only when asked to.
|
||||
*
|
||||
* A rotation of exactly 0 takes the plain `SDL_RenderTexture` path, which is why
|
||||
* @p center and @p flip are only consulted when @p angle is non-zero.
|
||||
*
|
||||
* @param self The backend to draw through. Required.
|
||||
* @param texture The texture to blit. Required.
|
||||
* @param src The rectangle of @p texture to take, in pixels. Optional --
|
||||
* `NULL` means the whole texture.
|
||||
* @param dest Where to put it on the render target, in pixels. Optional --
|
||||
* `NULL` means stretch to fill the whole target. Not an output
|
||||
* parameter despite the name.
|
||||
* @param angle Clockwise rotation in degrees. Exactly 0 skips rotation
|
||||
* entirely, along with @p center and @p flip.
|
||||
* @param center Pivot for the rotation, relative to @p dest. Required *when*
|
||||
* @p angle is non-zero, ignored otherwise. SDL's own "NULL means
|
||||
* the centre of dest" convention is not available here.
|
||||
* @param flip Horizontal/vertical mirroring. Applied only on the rotated
|
||||
* path; ignored when @p angle is 0.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or @p texture is `NULL`; if @p angle is
|
||||
* non-zero and @p center is `NULL`; or if the SDL draw call itself
|
||||
* fails, in which case the message carries `SDL_GetError()`. That last
|
||||
* case is a reused status rather than a pointer problem.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_texture(akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip);
|
||||
/**
|
||||
* @brief Draw geometry. Not implemented by the 2D backend.
|
||||
*
|
||||
* The hook exists so a 3D backend has a slot to fill; this one refuses every
|
||||
* call. It is not a stub that quietly does nothing -- it fails loudly, so a
|
||||
* caller that reaches it finds out at once.
|
||||
*
|
||||
* @param self The backend. Ignored.
|
||||
* @return Never `NULL`.
|
||||
* @throws AKERR_API Always, with the message "Not implemented".
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief Draw the whole scene: every tilemap layer, and the actors standing on it.
|
||||
*
|
||||
* Walks layers from 0 to #AKGL_TILEMAP_MAX_LAYERS. For each, it draws that layer
|
||||
* of the global `gamemap` through `camera` (if the map has that many layers),
|
||||
* then sweeps the actor pool and calls `renderfunc` on every live actor whose
|
||||
* `layer` matches. Sweeping in layer order is what puts actors in front of the
|
||||
* scenery they stand on and behind the scenery they walk under.
|
||||
*
|
||||
* It reads the globals `gamemap`, `camera`, and `HEAP_ACTOR` directly rather
|
||||
* than taking them as arguments, so there is exactly one world to draw.
|
||||
*
|
||||
* @param self The backend to draw through. Required.
|
||||
* @param opflags Iterator flags. Optional -- `NULL` substitutes a zeroed set.
|
||||
* Currently ignored either way: this function sweeps the actor
|
||||
* pool itself instead of going through
|
||||
* akgl_registry_iterate_actor, so no `AKGL_ITERATOR_OP_*` bit
|
||||
* reaches anything. The parameter is here for the backend
|
||||
* signature and for the layer-mask support that has yet to be
|
||||
* wired up.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
* @throws AKERR_* Whatever akgl_tilemap_draw or an actor's own `renderfunc`
|
||||
* raises; the first failure aborts the frame and propagates unchanged.
|
||||
*
|
||||
* @warning Neither the global `gamemap` nor a live actor's `renderfunc` is
|
||||
* checked before use, so drawing before akgl_tilemap_load, or with a
|
||||
* hand-built actor that was never run through akgl_actor_initialize,
|
||||
* is a crash rather than an error context.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags);
|
||||
|
||||
/**
|
||||
* @brief Install the 2D backend's methods on a backend the caller owns.
|
||||
*
|
||||
* The vtable half of akgl_render_init2d(), with no window, no renderer, and no
|
||||
* property registry involved: it points @p self's six function pointers at the
|
||||
* `akgl_render_2d_*` entry points and returns. `sdl_renderer` is not touched,
|
||||
* so a caller who has already put its own `SDL_Renderer` there keeps it, and a
|
||||
* caller who has not gets a backend whose entry points all report
|
||||
* `AKERR_NULLPOINTER` rather than crash.
|
||||
*
|
||||
* This is the entry point for a host that owns its own window -- it can drive
|
||||
* akgl_actor_render(), akgl_tilemap_draw() and the rest through a backend
|
||||
* libakgl never created. akgl_render_init2d() is the same thing with a window
|
||||
* in front of it.
|
||||
*
|
||||
* @param self The backend to bind. Required. Only the method pointers are
|
||||
* written.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_bind2d(akgl_RenderBackend *self);
|
||||
|
||||
/**
|
||||
* @brief Create the window and SDL renderer, and bind the 2D backend's methods.
|
||||
*
|
||||
* Reads `game.screenwidth` and `game.screenheight` from the property registry
|
||||
* (both defaulting to the string "0", which asks SDL for a zero-sized window),
|
||||
* creates the window and renderer with `game.uri` as the title, points `camera`
|
||||
* at the full screen rectangle, and then calls akgl_render_bind2d() to install
|
||||
* the six `akgl_render_2d_*` function pointers on @p self.
|
||||
*
|
||||
* Because the dimensions come from the registry, akgl_registry_init_properties
|
||||
* and the property writes have to happen first -- see the note on
|
||||
* akgl_registry_init about the properties registry not being initialized for
|
||||
* callers that skip akgl_game_init.
|
||||
*
|
||||
* @param self The backend to initialize. Required. Its `sdl_renderer` and method
|
||||
* pointers are overwritten; anything it held before is not released.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
* @throws AKERR_VALUE If `game.screenwidth` or `game.screenheight` is set to
|
||||
* something that is not a base-10 integer.
|
||||
* @throws ERANGE If either dimension does not fit in an `int`.
|
||||
* @throws AKGL_ERR_SDL If the window and renderer cannot be created. The message
|
||||
* carries `SDL_GetError()`.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted while reading the two
|
||||
* properties.
|
||||
*
|
||||
* @note The two pooled strings holding the dimensions are only released on the
|
||||
* success path, so each failed initialization leaks two string slots.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self);
|
||||
|
||||
#endif // _RENDERER_H_
|
||||
171
include/akgl/sprite.h
Normal file
171
include/akgl/sprite.h
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* @file sprite.h
|
||||
* @brief Spritesheets (one texture, many frames) and the animations cut out of them.
|
||||
*
|
||||
* The split is deliberate: an akgl_SpriteSheet owns the `SDL_Texture` and the
|
||||
* frame grid, and any number of akgl_Sprite animations point at the same sheet
|
||||
* and name the frame indices they use. Loading two sprites from the same image
|
||||
* loads the image once -- akgl_sprite_load_json looks the sheet up in
|
||||
* #AKGL_REGISTRY_SPRITESHEET by resolved path before creating one.
|
||||
*
|
||||
* Both are pool objects (akgl_heap_next_sprite, akgl_heap_next_spritesheet) and
|
||||
* both publish themselves in a registry under their name, which is how
|
||||
* akgl_character_sprite_add and the tilemap loader find them. Because loading a
|
||||
* sheet uploads a texture, the renderer has to exist first -- so
|
||||
* akgl_render_init2d, or akgl_game_init, before any of this.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_SPRITE_H_
|
||||
#define _AKGL_SPRITE_H_
|
||||
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
|
||||
|
||||
#define AKGL_SPRITE_MAX_FRAMES 16
|
||||
#define AKGL_SPRITE_MAX_NAME_LENGTH 128
|
||||
#define AKGL_SPRITE_MAX_REGISTRY_SIZE 1024
|
||||
#define AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH 512
|
||||
|
||||
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
|
||||
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
|
||||
|
||||
/** @brief Stores a loaded spritesheet texture and frame geometry. */
|
||||
typedef struct {
|
||||
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. Destroys the texture when it reaches 0. */
|
||||
SDL_Texture *texture; /**< The whole sheet as one GPU texture. Owned by this struct. */
|
||||
char name[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH]; /**< Registry key: the resolved path the image was loaded from. */
|
||||
uint16_t sprite_w; /**< Frame width. Vestigial: nothing in the library writes or reads it; the grid comes from akgl_Sprite::width. */
|
||||
uint16_t sprite_h; /**< Frame height. Vestigial, same as sprite_w. */
|
||||
} akgl_SpriteSheet;
|
||||
|
||||
/** @brief Describes an animated sprite and its spritesheet reference. */
|
||||
typedef struct {
|
||||
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. One reference per character that maps it. */
|
||||
uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; /**< Frame numbers on the sheet, in playback order. Counted left to right, then wrapping to the next row. */
|
||||
uint32_t frames; /**< How many entries of frameids are in use. */
|
||||
uint32_t width; /**< Frame width in pixels; also the horizontal stride used to find a frame on the sheet. */
|
||||
uint32_t height; /**< Frame height in pixels; the vertical stride once frames wrap to the next row. */
|
||||
uint32_t speed; /**< Nanoseconds one frame is held. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONESEC_MS, which despite its name is nanoseconds-per-millisecond. TODO.md item 6. */
|
||||
bool loop; /**< Restart from frame 0 when the last frame is reached, instead of holding it. */
|
||||
bool loopReverse; /**< Play back down to frame 0 instead of jumping to it -- a ping-pong loop. */
|
||||
char name[AKGL_SPRITE_MAX_NAME_LENGTH]; /**< Registry key, from the JSON `name` field. */
|
||||
akgl_SpriteSheet *sheet; /**< The sheet these frames are cut from. Borrowed, not owned. */
|
||||
} akgl_Sprite;
|
||||
|
||||
// initializes a new sprite to use the given sheet and otherwise sets to zero
|
||||
/**
|
||||
* @brief Zero a pooled sprite, bind it to a sheet, and publish it in the sprite registry.
|
||||
*
|
||||
* Sets the name and the sheet and takes the first reference; everything else --
|
||||
* frame list, geometry, speed, loop flags -- is left at zero for the caller, or
|
||||
* akgl_sprite_load_json, to fill in. The sheet is borrowed, so this does *not*
|
||||
* take a reference on it: releasing the sheet out from under a live sprite
|
||||
* leaves a dangling pointer.
|
||||
*
|
||||
* @param spr Pooled sprite to initialize, normally from akgl_heap_next_sprite.
|
||||
* Required. Any previous contents are discarded.
|
||||
* @param name Registry key, NUL-terminated. Required. Copied at a fixed
|
||||
* #AKGL_SPRITE_MAX_NAME_LENGTH bytes, so a shorter string reads
|
||||
* past its end -- pass a name from an akgl_String or another
|
||||
* buffer of at least that size. An existing entry with the same
|
||||
* name is silently replaced.
|
||||
* @param sheet The spritesheet this sprite's frames come from. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p spr, @p name, or @p sheet is `NULL`.
|
||||
* @throws AKERR_KEY If the sprite cannot be written into #AKGL_REGISTRY_SPRITE
|
||||
* -- in practice, because akgl_registry_init has not run.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet);
|
||||
// loads a given image file into a new spritesheet
|
||||
/**
|
||||
* @brief Load an image file as a texture and publish it as a spritesheet.
|
||||
*
|
||||
* Uploads @p filename through SDL_image using the global `renderer`, so the
|
||||
* renderer must already be initialized. The sheet is registered in
|
||||
* #AKGL_REGISTRY_SPRITESHEET under @p filename, which is the key
|
||||
* akgl_sprite_load_json looks it up by to avoid loading the same image twice.
|
||||
*
|
||||
* @param sheet Pooled spritesheet to initialize, normally from
|
||||
* akgl_heap_next_spritesheet. Required. Zeroed first, so a
|
||||
* texture it already held is leaked rather than destroyed.
|
||||
* @param sprite_w Frame width in pixels. Accepted and currently discarded --
|
||||
* the frame grid actually used at draw time comes from the
|
||||
* akgl_Sprite's `width`/`height`, not from here.
|
||||
* @param sprite_h Frame height in pixels. Same caveat as @p sprite_w.
|
||||
* @param filename Path to the image, in any format SDL_image can decode.
|
||||
* Required. Doubles as the registry key, so callers should pass
|
||||
* an already-resolved path; two spellings of the same file are
|
||||
* two sheets. Truncated at
|
||||
* #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p sheet or @p filename is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the image cannot be loaded -- missing, unreadable, or
|
||||
* a format SDL_image was not built with. The message carries
|
||||
* `SDL_GetError()`.
|
||||
* @throws AKERR_KEY If the sheet cannot be written into
|
||||
* #AKGL_REGISTRY_SPRITESHEET.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename);
|
||||
/**
|
||||
* @brief Build a sprite -- and, if needed, its spritesheet -- from a JSON definition file.
|
||||
*
|
||||
* Reads `name`, `width`, `height`, `speed` (seconds, scaled to milliseconds),
|
||||
* `loop`, `loopReverse`, a `frames` array of frame indices, and a `spritesheet`
|
||||
* object holding `filename`, `frame_width`, and `frame_height`. The sheet's
|
||||
* `filename` is resolved *relative to the directory of @p filename*, so a sprite
|
||||
* definition can sit next to its image and move with it.
|
||||
*
|
||||
* If a spritesheet with that resolved path is already registered it is reused;
|
||||
* otherwise one is claimed from the pool and loaded. On failure both the sprite
|
||||
* and any sheet loaded for it are released again.
|
||||
*
|
||||
* @param filename Path to the JSON document. Required. Must be shorter than
|
||||
* #AKGL_MAX_STRING_LENGTH -- it is copied into a pooled string
|
||||
* so `dirname` can be taken without modifying the caller's
|
||||
* buffer.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p filename is `NULL`, or if the file cannot be
|
||||
* opened or does not parse. The message carries jansson's line number
|
||||
* and text.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p filename is at least #AKGL_MAX_STRING_LENGTH
|
||||
* bytes long, or if the `frames` array is indexed past its end.
|
||||
* @throws AKERR_KEY If a required key is absent, or if the sprite or sheet
|
||||
* cannot be added to its registry. The message names the key.
|
||||
* @throws AKERR_TYPE If a key is present with the wrong JSON type -- `speed` as
|
||||
* a string, `frames` as an object.
|
||||
* @throws AKGL_ERR_SDL If the spritesheet image fails to load.
|
||||
* @throws AKGL_ERR_HEAP If the sprite, spritesheet, or string pool is exhausted.
|
||||
*
|
||||
* @note The `frames` array is not bounded against #AKGL_SPRITE_MAX_FRAMES. A
|
||||
* definition with more than 16 frames writes past `frameids` into the
|
||||
* rest of the struct.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_load_json(char *filename);
|
||||
|
||||
/**
|
||||
* @brief Work out where one animation frame sits on its spritesheet.
|
||||
*
|
||||
* Frames are numbered in reading order across the sheet: multiply the frame
|
||||
* number by the sprite width, then wrap whole rows off the right-hand edge of
|
||||
* the texture and step down by the sprite height for each. Callers use the
|
||||
* result as the source rectangle for `SDL_RenderTexture`.
|
||||
*
|
||||
* @param self The sprite whose sheet and frame geometry to use. Required,
|
||||
* along with its `sheet`.
|
||||
* @param srccoords Receives the frame's rectangle on the sheet, in pixels.
|
||||
* Required -- the return value is the error context.
|
||||
* @param frameid Index into the sprite's `frameids` array, not a frame number
|
||||
* on the sheet. Not bounds-checked against `frames` or
|
||||
* #AKGL_SPRITE_MAX_FRAMES; an out-of-range index reads a
|
||||
* neighbouring struct member and yields a nonsense rectangle
|
||||
* rather than an error.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self, @p srccoords, or `self->sheet` is
|
||||
* `NULL`. Note that `sheet->texture` is dereferenced without a check,
|
||||
* so a registered-but-unloaded sheet is a crash rather than an error.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid);
|
||||
|
||||
#endif //_AKGL_SPRITE_H_
|
||||
71
include/akgl/staticstring.h
Normal file
71
include/akgl/staticstring.h
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @file staticstring.h
|
||||
* @brief A fixed-capacity string object handed out by the akgl string heap layer.
|
||||
*
|
||||
* The library allocates nothing at runtime, so a "string" here is a
|
||||
* PATH_MAX-sized buffer claimed from the pool with akgl_heap_next_string and
|
||||
* given back with akgl_heap_release_string. Capacity is fixed at compile time:
|
||||
* these functions truncate rather than grow, and truncation is silent.
|
||||
*/
|
||||
|
||||
#ifndef _STRING_H_
|
||||
#define _STRING_H_
|
||||
|
||||
#include "string.h"
|
||||
#include <akerror.h>
|
||||
#include <limits.h>
|
||||
|
||||
#define AKGL_MAX_STRING_LENGTH PATH_MAX
|
||||
|
||||
/** @brief Provides a fixed-capacity, heap-managed string buffer. */
|
||||
typedef struct
|
||||
{
|
||||
int refcount; /**< Pool bookkeeping; 0 means the slot is free. Owned by the heap layer. */
|
||||
char data[AKGL_MAX_STRING_LENGTH]; /**< The characters. Not guaranteed NUL-terminated when filled to capacity. */
|
||||
} akgl_String;
|
||||
|
||||
/**
|
||||
* @brief Set a pooled string's contents and mark the slot in use.
|
||||
*
|
||||
* Copies at most #AKGL_MAX_STRING_LENGTH bytes out of @p init, or zeroes the
|
||||
* buffer when @p init is `NULL`, then sets `refcount` to 1. Callers normally
|
||||
* reach this through akgl_heap_next_string rather than calling it directly.
|
||||
*
|
||||
* @param obj The pooled string to (re)initialize. Required. Its previous
|
||||
* contents are discarded without inspection.
|
||||
* @param init Initial contents, NUL-terminated. Optional -- `NULL` zero-fills
|
||||
* the buffer instead. An @p init longer than
|
||||
* #AKGL_MAX_STRING_LENGTH is truncated *and left unterminated*,
|
||||
* because this is `strncpy` semantics, not `strlcpy`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
*
|
||||
* @note Known defect: the `NULL` @p init path zeroes `sizeof(akgl_String)`
|
||||
* bytes starting at `data`, which is four bytes past the end of the
|
||||
* buffer -- `refcount` sits in front of it. TODO.md, "Known and still
|
||||
* open" item 6.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_initialize(akgl_String *obj, char *init);
|
||||
/**
|
||||
* @brief Copy the contents of one pooled string into another.
|
||||
*
|
||||
* A bounded `strncpy` between two already-claimed pool slots. It copies bytes
|
||||
* only: `refcount` is left alone, so @p dst keeps whatever pool state it had.
|
||||
*
|
||||
* @param src Source string. Required. Read up to @p count bytes.
|
||||
* @param dst Destination string. Required. Overwritten in place; the pool
|
||||
* slot must already have been claimed.
|
||||
* @param count Maximum bytes to copy. 0 selects #AKGL_MAX_STRING_LENGTH, the
|
||||
* whole buffer. A @p count shorter than the source truncates
|
||||
* without writing a terminator; a @p count longer than the source
|
||||
* zero-pads the remainder, per `strncpy`. Values above
|
||||
* #AKGL_MAX_STRING_LENGTH overrun both buffers and are not
|
||||
* rejected.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p src or @p dst is `NULL`.
|
||||
* @throws errno Whatever `errno` holds if `strncpy` returns something other
|
||||
* than @p dst. In practice `strncpy` always returns its destination, so
|
||||
* this path is unreachable rather than merely rare.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_copy(akgl_String *src, akgl_String *dst, int count);
|
||||
#endif //_STRING_H_
|
||||
158
include/akgl/text.h
Normal file
158
include/akgl/text.h
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @file text.h
|
||||
* @brief Loading fonts and drawing or measuring strings with them.
|
||||
*
|
||||
* Fonts are `TTF_Font *` handles kept in the #AKGL_REGISTRY_FONT property
|
||||
* registry under a caller-chosen name; there is no akgl font type wrapping them.
|
||||
* SDL_ttf must be initialized (akgl_game_init does it) before any of this.
|
||||
*
|
||||
* The two measure functions do not touch the renderer, so they are usable
|
||||
* before -- or entirely without -- a window. Drawing is immediate mode: each
|
||||
* akgl_text_rendertextat() call rasterizes, uploads, blits, and throws the
|
||||
* texture away, which is fine for a HUD line and wrong for a large body of
|
||||
* static text redrawn every frame.
|
||||
*/
|
||||
|
||||
#ifndef _TEXT_H_
|
||||
#define _TEXT_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <akerror.h>
|
||||
|
||||
/**
|
||||
* @brief Open a TrueType font at one size and publish it in the font registry.
|
||||
*
|
||||
* A size is baked into the handle, so the same file at two sizes is two calls
|
||||
* under two names. Nothing releases these: the handles live until the process
|
||||
* ends.
|
||||
*
|
||||
* @param name Registry key to publish the font under. Required. An existing
|
||||
* entry with the same name is replaced, and the font it
|
||||
* displaced is closed through akgl_text_unloadfont -- but only
|
||||
* after the new one has opened, so a failed load leaves the
|
||||
* caller with the font they already had.
|
||||
* @param filepath Path to a `.ttf`/`.otf` file. Required. Used verbatim -- not
|
||||
* resolved against `SDL_GetBasePath()`.
|
||||
* @param size Point size to rasterize at. Passed straight to SDL_ttf, which
|
||||
* rejects anything that is not positive.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p name or @p filepath is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the font cannot be opened -- missing, unreadable, not
|
||||
* a font, or a @p size SDL_ttf refuses. The message carries
|
||||
* `SDL_GetError()`.
|
||||
* @throws AKERR_KEY If the font cannot be written into #AKGL_REGISTRY_FONT --
|
||||
* in practice, because akgl_registry_init has not run.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size);
|
||||
/**
|
||||
* @brief Close a loaded font and take it out of the font registry.
|
||||
*
|
||||
* The other half of akgl_text_loadfont, and for a while the half that did not
|
||||
* exist: a font could be opened and published but never handed back, so a game
|
||||
* that changed fonts between scenes had no way to reclaim the one it had
|
||||
* finished with. A `TTF_Font` is about ten kilobytes once FreeType's own
|
||||
* structures are counted.
|
||||
*
|
||||
* The registry entry is cleared before the font is closed, so a font is never
|
||||
* reachable through #AKGL_REGISTRY_FONT after it has gone back to SDL_ttf.
|
||||
*
|
||||
* @param name Registry key the font was published under. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p name is `NULL`.
|
||||
* @throws AKERR_KEY If no font is registered under @p name -- including the
|
||||
* case where it was already unloaded, which makes a double unload an
|
||||
* error rather than a double close.
|
||||
*
|
||||
* @warning Anything still holding the `TTF_Font *` -- a caller that fetched it
|
||||
* from the registry earlier, a pending akgl_text_rendertextat -- is
|
||||
* left with a dangling pointer. Fonts are not reference counted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name);
|
||||
/**
|
||||
* @brief Rasterize a string and blit it at a screen position, in one call.
|
||||
*
|
||||
* Renders blended (anti-aliased, alpha-blended) through SDL_ttf, uploads the
|
||||
* result to a texture, draws it through the global `renderer`, and destroys both
|
||||
* the texture and the surface before returning. The text is drawn at its natural
|
||||
* size -- @p x and @p y are the top-left corner, not a centre.
|
||||
*
|
||||
* Coordinates are screen coordinates, not world ones: this does not go through
|
||||
* the camera, so a HUD stays put while the world scrolls under it.
|
||||
*
|
||||
* @param font Font to render with, from akgl_text_loadfont. Required.
|
||||
* @param text UTF-8 text. Required. May contain newlines, which break
|
||||
* lines on either path.
|
||||
* @param color Text colour, including alpha.
|
||||
* @param wraplength Wrap width in pixels. Greater than 0 wraps on word
|
||||
* boundaries at that width; 0 or less draws a single line and
|
||||
* breaks only on newlines in @p text.
|
||||
* @param x Left edge of the text, in screen pixels.
|
||||
* @param y Top edge of the text, in screen pixels.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p font or @p text is `NULL`; if the global
|
||||
* `renderer`, its `sdl_renderer`, or its `draw_texture` is `NULL` --
|
||||
* that last one is the state a backend is in between being allocated
|
||||
* and being run through akgl_render_bind2d(); if SDL_ttf cannot
|
||||
* rasterize the string; or if the surface cannot be uploaded as a
|
||||
* texture. The last two carry `SDL_GetError()` and are a reused status
|
||||
* rather than a pointer problem.
|
||||
* @throws AKERR_* Whatever the backend's `draw_texture` raises.
|
||||
*
|
||||
* @note On a failure after rasterizing -- the texture upload, or the draw -- the
|
||||
* surface and texture are not destroyed, because the error returns before
|
||||
* the cleanup. Repeated failures leak.
|
||||
* @note The empty string is **refused**, not drawn as nothing: SDL_ttf reports
|
||||
* "Text has zero width" and this passes that on as `AKERR_NULLPOINTER`.
|
||||
* akgl_text_measure() accepts it, so the two disagree. A caller drawing a
|
||||
* line of text that may be empty has to check for it. TODO.md, "Known and
|
||||
* still open".
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y);
|
||||
/**
|
||||
* @brief Report the size, in pixels, that @p text would occupy on one line.
|
||||
*
|
||||
* Nothing is drawn and no renderer is required. A caller building a character
|
||||
* grid measures one cell with this -- the advance width of a single glyph in a
|
||||
* monospaced font -- and derives the rest of the grid from it.
|
||||
*
|
||||
* @param font Font to measure with, from akgl_text_loadfont. Required.
|
||||
* @param text UTF-8 text to measure. Required. The empty string is legal and
|
||||
* measures 0 wide by one line high.
|
||||
* @param w Receives the width in pixels. Required.
|
||||
* @param h Receives the height in pixels -- one line, whatever @p text
|
||||
* contains, since this form does not wrap. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p font, @p text, @p w, or @p h is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font,
|
||||
* or text that is not valid UTF-8. The message carries `SDL_GetError()`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h);
|
||||
/**
|
||||
* @brief Report the size, in pixels, that @p text would occupy when wrapped.
|
||||
*
|
||||
* The companion to akgl_text_measure() for the wrapping case, matching the
|
||||
* @p wraplength argument akgl_text_rendertextat() already takes: a string
|
||||
* longer than @p wraplength reports the height of every line it breaks onto.
|
||||
* A @p wraplength of zero wraps only on newlines in @p text.
|
||||
*
|
||||
* @param font Font to measure with, from akgl_text_loadfont. Required.
|
||||
* @param text UTF-8 text to measure. Required.
|
||||
* @param wraplength Wrap width in pixels. 0 wraps on newlines only. Negative is
|
||||
* refused rather than passed through: SDL_ttf reads a negative
|
||||
* width as a very large unsigned one and silently stops
|
||||
* wrapping, which would return a measurement that is wrong
|
||||
* rather than an error.
|
||||
* @param w Receives the width in pixels: the longest line, not
|
||||
* @p wraplength. Required.
|
||||
* @param h Receives the height in pixels, covering every line the text
|
||||
* wraps onto. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p font, @p text, @p w, or @p h is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p wraplength is negative.
|
||||
* @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font,
|
||||
* or text that is not valid UTF-8. The message carries `SDL_GetError()`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h);
|
||||
|
||||
#endif // _TEXT_H_
|
||||
493
include/akgl/tilemap.h
Normal file
493
include/akgl/tilemap.h
Normal file
@@ -0,0 +1,493 @@
|
||||
/**
|
||||
* @file tilemap.h
|
||||
* @brief Loading and drawing Tiled maps: tilesets, layers, embedded actors, per-map physics.
|
||||
*
|
||||
* The map format is the JSON export from the Tiled editor, so the level design
|
||||
* tool is somebody else's and the loader is the only thing that has to know
|
||||
* about it. A map is a stack of layers -- tile grids, images, and object groups
|
||||
* -- drawn over a set of tilesets, each tileset being one image cut into a grid.
|
||||
*
|
||||
* Three things beyond ordinary Tiled semantics are worth knowing:
|
||||
*
|
||||
* - **Object layers can spawn actors.** An object of type `actor` names a
|
||||
* character in its custom properties, and loading the map creates and
|
||||
* registers the actor rather than just recording a rectangle.
|
||||
* - **A map can carry its own physics.** A `physics.model` custom property on
|
||||
* the map selects a backend, and `physics.gravity.*` / `physics.drag.*` set
|
||||
* its constants, so a swimming level and a walking level differ by data
|
||||
* rather than by code.
|
||||
* - **A map can carry a pseudo-3D perspective.** Two objects named
|
||||
* `p_foreground` and `p_vanishing` in an object layer, each with a `scale`
|
||||
* property, define a band down the screen over which actors are scaled --
|
||||
* which is what makes a character look further away as they walk up the
|
||||
* screen. akgl_tilemap_scale_actor applies it.
|
||||
*
|
||||
* All paths inside a map -- tileset images, layer images -- are resolved
|
||||
* relative to the map file, so a map and its art move together.
|
||||
*
|
||||
* Note the size of akgl_Tilemap: a layer's tile grid alone is 512x512 ints, and
|
||||
* a tileset's offset table is 65536 pairs. It is a megabytes-large object and
|
||||
* belongs in static storage, which is where `_akgl_gamemap` puts it. Do not put
|
||||
* one on the stack.
|
||||
*/
|
||||
|
||||
#ifndef _TILEMAP_H_
|
||||
#define _TILEMAP_H_
|
||||
|
||||
#include <limits.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <jansson.h>
|
||||
|
||||
/** @brief Widest map, in tiles. Width times height is what is actually bounded. */
|
||||
#define AKGL_TILEMAP_MAX_WIDTH 512
|
||||
/** @brief Tallest map, in tiles. */
|
||||
#define AKGL_TILEMAP_MAX_HEIGHT 512
|
||||
/** @brief Layers per map. Also the number of draw passes akgl_render_2d_draw_world makes. */
|
||||
#define AKGL_TILEMAP_MAX_LAYERS 16
|
||||
/** @brief Tilesets per map. */
|
||||
#define AKGL_TILEMAP_MAX_TILESETS 16
|
||||
/** @brief Entries in a tileset's offset table. Indexed by *local* tile id, so a tileset with a high `firstgid` still starts at 0. */
|
||||
#define AKGL_TILEMAP_MAX_TILES_PER_IMAGE 65536
|
||||
/** @brief Longest tileset name. */
|
||||
#define AKGL_TILEMAP_MAX_TILESET_NAME_SIZE 512
|
||||
/** @brief Longest resolved tileset image path. */
|
||||
#define AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE PATH_MAX
|
||||
/** @brief Longest object name. Note that an object naming an actor is truncated at #AKGL_ACTOR_MAX_NAME_LENGTH (128) instead. */
|
||||
#define AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE 512
|
||||
/** @brief Objects in one object layer. Not enforced by the loader -- a longer group writes past the array. */
|
||||
#define AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER 128
|
||||
|
||||
/** @brief Object `type` for a Tiled object whose `type` field is the string "actor". */
|
||||
#define AKGL_TILEMAP_OBJECT_TYPE_ACTOR 1
|
||||
|
||||
/** @brief Layer `type` for a Tiled `tilelayer`: a grid of tile ids. */
|
||||
#define AKGL_TILEMAP_LAYER_TYPE_TILES 1
|
||||
/** @brief Layer `type` for a Tiled `objectgroup`: actors and perspective markers. */
|
||||
#define AKGL_TILEMAP_LAYER_TYPE_OBJECTS 2
|
||||
/** @brief Layer `type` for a Tiled `imagelayer`: one image drawn at the origin. */
|
||||
#define AKGL_TILEMAP_LAYER_TYPE_IMAGE 3
|
||||
|
||||
/** @brief One object placed in a Tiled object layer. */
|
||||
typedef struct {
|
||||
float x; /**< Position in map pixels, from the Tiled object. Copied onto the actor it spawns. */
|
||||
float y; /**< Position in map pixels. */
|
||||
int gid; /**< Global tile id, for a tile object. Not read by the loader. */
|
||||
int id; /**< Tiled's object id. Not read by the loader. */
|
||||
int height; /**< Height in map pixels. Read into the map's perspective bands, not into this field. */
|
||||
int width; /**< Width in map pixels. Not read by the loader. */
|
||||
int rotation; /**< Rotation in degrees. Not read by the loader. */
|
||||
int type; /**< #AKGL_TILEMAP_OBJECT_TYPE_ACTOR, or 0 for anything else. */
|
||||
bool visible; /**< Copied onto the actor. Forced to `false` for perspective markers, which are geometry rather than scenery. */
|
||||
akgl_Actor *actorptr; /**< The actor this object spawned or attached to, for an actor object. `NULL` otherwise. Borrowed. */
|
||||
char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE]; /**< Object name. For an actor object this is the registry key; for a perspective marker it is `p_foreground` or `p_vanishing`. */
|
||||
} akgl_TilemapObject;
|
||||
|
||||
/** @brief One layer of a tilemap: a tile grid, an image, or a group of objects. */
|
||||
typedef struct {
|
||||
short type; /**< Which of the `AKGL_TILEMAP_LAYER_TYPE_*` kinds this is; decides which of the members below mean anything. */
|
||||
float opacity; /**< 0.0 to 1.0, from Tiled. Recorded but not yet applied at draw time. */
|
||||
bool visible; /**< From Tiled. Recorded but not yet consulted at draw time. */
|
||||
int height; /**< Tile layer: height in tiles. Image layer: the texture's height in pixels. */
|
||||
int width; /**< Tile layer: width in tiles. Image layer: the texture's width in pixels. */
|
||||
int x; /**< Layer offset from Tiled. Recorded but not applied at draw time. */
|
||||
int y; /**< Layer offset from Tiled. */
|
||||
int id; /**< Tiled's layer id. Not the same as the index into akgl_Tilemap::layers. */
|
||||
SDL_Texture *texture; /**< Image layers only: the whole layer as one texture. `NULL` otherwise. Owned by this struct. */
|
||||
int data[AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT]; /**< Tile layers only: global tile ids in row-major order. 0 means an empty cell. */
|
||||
akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER]; /**< Object layers only. */
|
||||
} akgl_TilemapLayer;
|
||||
|
||||
/** @brief One tileset: an image cut into a grid, plus the lookup table that finds a tile in it. */
|
||||
typedef struct {
|
||||
int columns; /**< Tiles per row in the image. What turns a linear tile id into a row and column. */
|
||||
int firstgid; /**< Global id of this tileset's first tile. Subtracting it from a map cell gives the local tile id. */
|
||||
char imagefilename[AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE]; /**< Resolved absolute path to the image, from the map file's directory. */
|
||||
int imageheight; /**< Image height in pixels, as declared by Tiled. */
|
||||
int imagewidth; /**< Image width in pixels, as declared. */
|
||||
char name[AKGL_TILEMAP_MAX_TILESET_NAME_SIZE]; /**< Tileset name from Tiled. Diagnostic only; lookups go by `firstgid`. */
|
||||
SDL_Texture *texture; /**< The image as one texture. Owned by this struct. */
|
||||
// Use this as a lookup table instead of storing tiles
|
||||
// in individual textures to blit them from a single
|
||||
// texture at runtime
|
||||
// FIXME: This is probably not very efficient. For a map
|
||||
// with a single tileset it makes sense. For a map with
|
||||
// multiple tilesets you may have set A start at firstgid 1
|
||||
// and have 1728 tiles. Set B may start at firstgid 1729 and
|
||||
// have 1728 more tiles. This means Set B has 1728 empty
|
||||
// tile_offsets[] entries before firstgid 1729 because of the
|
||||
// way akgl_tilemap_load_tilesets() works. This is really inefficient
|
||||
// and should be improved in the future, and will eventually
|
||||
// lead to premature exhaustion of AKGL_TILEMAP_MAX_TILES_PER_IMAGE
|
||||
// because set D or E may only have 64 tiles but they may be
|
||||
// at the upper end of the array bound already because of this.
|
||||
int tile_offsets[AKGL_TILEMAP_MAX_TILES_PER_IMAGE][2]; /**< Local tile id -> {x, y} pixel offset into the image. Computed once at load by akgl_tilemap_compute_tileset_offsets. */
|
||||
int tilecount; /**< How many tiles the image holds. */
|
||||
int tileheight; /**< Height of one tile in pixels. */
|
||||
int tilewidth; /**< Width of one tile in pixels. */
|
||||
int spacing; /**< Pixels between adjacent tiles in the image. */
|
||||
int margin; /**< Pixels of border around the whole grid. Recorded but **not** accounted for by the offset computation. */
|
||||
} akgl_Tileset;
|
||||
|
||||
/** @brief Represents a complete tilemap and its optional physics backend. */
|
||||
typedef struct {
|
||||
int tilewidth; /**< Width of one map cell in pixels. Tiles from a tileset with a different tile size are not rescaled. */
|
||||
int tileheight; /**< Height of one map cell in pixels. */
|
||||
int width; /**< Map width in tiles. */
|
||||
int height; /**< Map height in tiles. */
|
||||
int numlayers; /**< Layers actually loaded, at most #AKGL_TILEMAP_MAX_LAYERS. */
|
||||
int orientation; /**< 0 = orthogonal, 1 = isometric. Always set to 0 by the loader; isometric is not implemented. */
|
||||
int numtilesets; /**< Tilesets actually loaded. */
|
||||
int p_foreground_y; /**< Y of the `p_foreground` marker: the row at which actors are at full size. 0 disables perspective. */
|
||||
int p_vanishing_y; /**< Y of the `p_vanishing` marker: the row at which actors are smallest. 0 disables perspective. */
|
||||
int p_foreground_h; /**< Height of the `p_foreground` marker object. Recorded; not used in the current rate calculation. */
|
||||
int p_vanishing_h; /**< Height of the `p_vanishing` marker object. Recorded, likewise. */
|
||||
float p_foreground_scale; /**< Actor scale at `p_foreground_y`. Defaults to 1.0. */
|
||||
float p_vanishing_scale; /**< Actor scale at `p_vanishing_y`. Defaults to 1.0. */
|
||||
float p_scale; /**< Unused. Left from an earlier formulation of the perspective maths. */
|
||||
float p_rate; /**< Scale change per pixel of y between the two markers. Derived at load; what akgl_tilemap_scale_actor interpolates with. */
|
||||
akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS]; /**< The tilesets, in the order Tiled listed them. */
|
||||
akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS]; /**< The layers, in draw order: index 0 is furthest back. */
|
||||
|
||||
// Different levels may have different physics.
|
||||
bool use_own_physics; /**< Set when the map declared a `physics.model` property. A caller that honours it simulates through `physics` instead of the global backend. */
|
||||
akgl_PhysicsBackend physics; /**< This map's own backend, valid only when `use_own_physics` is set. */
|
||||
} akgl_Tilemap;
|
||||
|
||||
/**
|
||||
* @brief Load a Tiled JSON map: physics, geometry, layers, tilesets, and any actors it spawns.
|
||||
*
|
||||
* Zeroes @p dest, then reads the map's own physics properties, its tile
|
||||
* dimensions, its size in tiles, its layers, and its tilesets -- in that order,
|
||||
* because layers reference tilesets by global id and objects reference
|
||||
* characters by name. Finally, if the map carried both perspective markers, it
|
||||
* works out the per-pixel scaling rate between them.
|
||||
*
|
||||
* Loading a map has side effects beyond @p dest: tileset and layer images are
|
||||
* uploaded as textures, and every `actor` object in an object layer is created
|
||||
* in the actor pool and published in #AKGL_REGISTRY_ACTOR. So the renderer, the
|
||||
* pools, the registries, and the characters the map names all have to be in
|
||||
* place first.
|
||||
*
|
||||
* @param fname Path to the map JSON. Required. Its directory becomes the root
|
||||
* that every tileset and layer image inside is resolved against.
|
||||
* @param dest The tilemap to fill in. Required. Zeroed first, so a map already
|
||||
* loaded into it has its textures leaked rather than destroyed --
|
||||
* call akgl_tilemap_release first if you are reusing one. Do not
|
||||
* put one of these on the stack; see the note on this file.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p fname or @p dest is `NULL`; if the map file
|
||||
* cannot be opened or does not parse, with jansson's line and text; or
|
||||
* if a tileset image cannot be loaded.
|
||||
* @throws AKERR_OUTOFBOUNDS If the map is larger than
|
||||
* #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT tiles, if it has
|
||||
* more than #AKGL_TILEMAP_MAX_LAYERS layers, or if a layer's own
|
||||
* declared size exceeds the same bound.
|
||||
* @throws AKERR_KEY If a required key is absent, if an `actor` object has an
|
||||
* empty name, or if the map names a physics model that does not exist.
|
||||
* @throws AKERR_TYPE If a key is present with the wrong JSON type.
|
||||
* @throws ENOENT If @p fname or a path referenced inside it does not exist.
|
||||
* @throws AKGL_ERR_SDL If a layer image cannot be loaded.
|
||||
* @throws AKGL_ERR_HEAP If the string or actor pool is exhausted.
|
||||
*
|
||||
* @note Nothing is released on a failure part-way through: textures already
|
||||
* uploaded and actors already created stay. Treat a failed load as
|
||||
* needing akgl_tilemap_release and a fresh actor registry.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load(char *fname, akgl_Tilemap *dest);
|
||||
/**
|
||||
* @brief Draw one layer of a map, clipped to a viewport.
|
||||
*
|
||||
* Only the tiles that intersect @p viewport are drawn, and the tiles at its
|
||||
* edges are drawn partially, so scrolling is smooth rather than snapping to the
|
||||
* tile grid. An image layer ignores the viewport entirely and is drawn once at
|
||||
* the origin.
|
||||
*
|
||||
* Drawing goes through the global `renderer`, not through a backend passed in.
|
||||
*
|
||||
* @param dest The map to draw from. Required. Not an output parameter
|
||||
* despite the name.
|
||||
* @param viewport The rectangle of the map, in map pixels, that is on screen.
|
||||
* Required. Usually the global `camera`.
|
||||
* @param layeridx Which layer to draw, 0 to `numlayers - 1`. **Not**
|
||||
* bounds-checked: an index past the end reads a neighbouring
|
||||
* layer, or past the array.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest or @p viewport is `NULL`.
|
||||
* @throws AKERR_* Whatever the renderer's `draw_texture` raises. The first
|
||||
* failure abandons the rest of the layer.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRect *viewport, int layeridx);
|
||||
/**
|
||||
* @brief Draw a whole tileset to the screen, tile by tile, from its offset table.
|
||||
*
|
||||
* A debugging tool, not a rendering path. It reconstructs the original tileset
|
||||
* image out of the offsets computed at load time, so if the picture that appears
|
||||
* matches the source image the offset table is right -- and if tiles are shifted
|
||||
* or duplicated, it is not. The `charviewer` utility uses it.
|
||||
*
|
||||
* @param dest The map holding the tileset. Required. Not an output
|
||||
* parameter despite the name.
|
||||
* @param tilesetidx Which tileset to draw, 0 to `numtilesets - 1`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p tilesetidx is at or above `numtilesets`. A
|
||||
* negative index is not rejected.
|
||||
* @throws AKERR_* Whatever the renderer's `draw_texture` raises.
|
||||
*
|
||||
* @note Tiles are laid out using the *map's* tile size rather than the
|
||||
* tileset's, so a tileset whose tiles are a different size from the map's
|
||||
* is reconstructed at the wrong pitch.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest, int tilesetidx);
|
||||
|
||||
/*
|
||||
* These functions are part of the internal API and should not be called by the user.
|
||||
* They are only exposed here for unit testing.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Find one entry in a Tiled `properties` array by name, checking its declared type.
|
||||
*
|
||||
* Tiled does not store custom properties as JSON object members. It stores them
|
||||
* as an array of `{"name": ..., "type": ..., "value": ...}` objects, so getting
|
||||
* one means a linear scan and a type-string comparison rather than a lookup.
|
||||
* That is what this does; the `akgl_get_json_properties_*` wrappers put a typed
|
||||
* face on it.
|
||||
*
|
||||
* @param obj The Tiled object -- map, layer, or object -- whose `properties`
|
||||
* array to search. Required.
|
||||
* @param key The property name to find. Required.
|
||||
* @param type The type string Tiled wrote, e.g. `"string"`, `"int"`, `"float"`.
|
||||
* A property found under the right name but the wrong type is an
|
||||
* error, not a miss. Not `NULL`-checked.
|
||||
* @param dest Receives a borrowed pointer to the whole property object -- the
|
||||
* one with `name`, `type`, and `value` -- not to its value. Not
|
||||
* `NULL`-checked.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
|
||||
* @throws AKERR_KEY If @p obj has no `properties` array, or if no entry in it
|
||||
* has that name.
|
||||
* @throws AKERR_TYPE If `properties` is not an array, an entry is malformed, or
|
||||
* the named property's declared type is not @p type. That last message
|
||||
* reports both the expected and the actual type.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest);
|
||||
/**
|
||||
* @brief Read a Tiled custom property declared as `string`.
|
||||
* @param obj The Tiled object whose `properties` array to search. Required.
|
||||
* @param key The property name. Required.
|
||||
* @param dest Receives the value in a *newly claimed* pool string. Required. Note
|
||||
* that this always claims -- unlike akgl_get_json_string_value it
|
||||
* does not reuse a string already in `*dest`, so passing one leaks
|
||||
* it. The caller releases the result with akgl_heap_release_string.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
|
||||
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
|
||||
* @throws AKERR_TYPE If the property is not declared `string`.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest);
|
||||
/**
|
||||
* @brief Read a Tiled custom property declared as `int`.
|
||||
* @param obj The Tiled object whose `properties` array to search. Required.
|
||||
* @param key The property name. Required.
|
||||
* @param dest Receives the value. Not written on any failure path.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
|
||||
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
|
||||
* @throws AKERR_TYPE If the property is not declared `int`, or its `value` is
|
||||
* not an integer.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest);
|
||||
/**
|
||||
* @brief Build a tileset's local-id-to-pixel-offset table.
|
||||
*
|
||||
* Run once per tileset at load time so drawing a tile is an array lookup rather
|
||||
* than a division. Walks the grid left to right and top to bottom, stepping by
|
||||
* tile size plus `spacing`.
|
||||
*
|
||||
* @param dest The map holding the tileset. Required in practice, though
|
||||
* not checked -- it is dereferenced immediately.
|
||||
* @param tilesetidx Which tileset, 0 to `numtilesets - 1`. Not bounds-checked.
|
||||
* @return `NULL`. There is no failure path: it is arithmetic into an already
|
||||
* allocated table.
|
||||
*
|
||||
* @note Two known limits. `margin` is not accounted for, so a tileset image with
|
||||
* a border produces offsets shifted by it. And the table is indexed by
|
||||
* local id from 0, while akgl_tilemap_draw indexes it by
|
||||
* `tilenum - firstgid`, so the entries a second tileset needs sit at the
|
||||
* front of its own table -- see the FIXME on akgl_Tileset::tile_offsets
|
||||
* for why that wastes space.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx);
|
||||
/**
|
||||
* @brief Load one object layer: spawn its actors and record its perspective markers.
|
||||
*
|
||||
* Two kinds of object are understood. `actor` objects name a registered actor,
|
||||
* or create one -- claiming it from the pool, initializing it, and binding the
|
||||
* character named in its `character` property -- and place it at the object's
|
||||
* coordinates on this layer. `perspective` objects named `p_foreground` or
|
||||
* `p_vanishing` set the map's scaling band and are themselves invisible.
|
||||
* Anything else is recorded and otherwise ignored.
|
||||
*
|
||||
* An actor named by two objects is not duplicated: the second takes another
|
||||
* reference on the first.
|
||||
*
|
||||
* @param dest The map to load into. Required.
|
||||
* @param root The layer's JSON object -- not the map root. Required.
|
||||
* @param layerid Index of the layer being loaded, which becomes each spawned
|
||||
* actor's `layer`. Not bounds-checked.
|
||||
* @param dirname Directory to resolve paths against. Accepted for signature
|
||||
* consistency with the other layer loaders; nothing here uses it.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest or @p root is `NULL`.
|
||||
* @throws AKERR_KEY If the layer has no `objects` array, if a required key is
|
||||
* absent, or if an `actor` object has an empty name.
|
||||
* @throws AKERR_TYPE If a key is present with the wrong JSON type.
|
||||
* @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
|
||||
* @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
|
||||
* @throws AKERR_* Whatever akgl_actor_initialize or akgl_actor_set_character
|
||||
* raises -- notably AKERR_KEY if the named character is not registered.
|
||||
*
|
||||
* @warning The object count is not checked against
|
||||
* #AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER, so a layer with more than 128
|
||||
* objects writes past the array.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Load one tile layer's grid of global tile ids.
|
||||
*
|
||||
* @param dest The map to load into. Required.
|
||||
* @param root The layer's JSON object -- not the map root. Required.
|
||||
* @param layerid Index of the layer being loaded. Not bounds-checked.
|
||||
* @param dirname Directory to resolve paths against. Required, though a tile
|
||||
* layer references no files and nothing here uses it.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If the layer's declared `width` times `height`
|
||||
* reaches #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT, or if the
|
||||
* `data` array is shorter than that product.
|
||||
* @throws AKERR_KEY If `height`, `width`, or `data` is absent.
|
||||
* @throws AKERR_TYPE If one of them has the wrong JSON type, or a cell is not an
|
||||
* integer.
|
||||
*
|
||||
* @note The declared `width` and `height` are trusted over the actual length of
|
||||
* `data`: a layer declaring more cells than it lists fails on the read
|
||||
* past the end rather than on a length check.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Load every layer of a map, dispatching on each layer's declared type.
|
||||
*
|
||||
* Reads the common fields -- id, opacity, visibility, offset -- then hands the
|
||||
* layer to the loader for its kind: `objectgroup`, `tilelayer`, or `imagelayer`.
|
||||
* A layer of any other type keeps its common fields and is otherwise skipped,
|
||||
* with `type` left at 0.
|
||||
*
|
||||
* @param dest The map to load into. Required. `numlayers` is set from the
|
||||
* array's length before any bound is checked, so it may briefly
|
||||
* exceed #AKGL_TILEMAP_MAX_LAYERS on the failure path.
|
||||
* @param root The map's root JSON object. Required.
|
||||
* @param dirname Directory to resolve layer image paths against. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If the map has more than #AKGL_TILEMAP_MAX_LAYERS
|
||||
* layers.
|
||||
* @throws AKERR_KEY If the map has no `layers` array, or a layer is missing a
|
||||
* common field.
|
||||
* @throws AKERR_TYPE If a field has the wrong JSON type.
|
||||
* @throws AKERR_* Whatever the per-kind loader raises.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Load one tileset's metadata and upload its image.
|
||||
*
|
||||
* @param tileset The tileset's JSON object. Required in practice, though not
|
||||
* checked -- it is passed straight to the accessors, which
|
||||
* report it.
|
||||
* @param dest The map to load into. Required, unchecked, dereferenced at once.
|
||||
* @param tsidx Which slot to fill, 0 to #AKGL_TILEMAP_MAX_TILESETS - 1. Not
|
||||
* bounds-checked.
|
||||
* @param dirname Directory to resolve the tileset's `image` path against --
|
||||
* the map file's own directory. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If a required key is missing its object, or if the
|
||||
* tileset image fails to load. The image message carries
|
||||
* `SDL_GetError()`.
|
||||
* @throws AKERR_KEY If one of `columns`, `firstgid`, `imageheight`,
|
||||
* `imagewidth`, `margin`, `spacing`, `tilecount`, `tileheight`,
|
||||
* `tilewidth`, `name`, or `image` is absent.
|
||||
* @throws AKERR_TYPE If one of them has the wrong JSON type.
|
||||
* @throws AKERR_OUTOFBOUNDS If the resolved image path is too long for a pooled
|
||||
* string.
|
||||
* @throws ENOENT If the image path does not exist.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @note This does not go through the spritesheet registry, so a tileset image
|
||||
* shared between two maps is loaded twice.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Load every tileset of a map and compute each one's offset table.
|
||||
*
|
||||
* @param dest The map to load into. Required. `numtilesets` is reset to 0 and
|
||||
* incremented as each tileset succeeds, so it always reflects
|
||||
* what actually loaded.
|
||||
* @param root The map's root JSON object. Required.
|
||||
* @param dirname Directory to resolve tileset image paths against. Required in
|
||||
* practice; not checked here.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest or @p root is `NULL`.
|
||||
* @throws AKERR_KEY If the map has no `tilesets` array.
|
||||
* @throws AKERR_TYPE If `tilesets` is not an array of objects.
|
||||
* @throws AKERR_* Whatever akgl_tilemap_load_tilesets_each raises.
|
||||
*
|
||||
* @warning The tileset count is not checked against
|
||||
* #AKGL_TILEMAP_MAX_TILESETS, so a map with more than 16 tilesets
|
||||
* writes past the array.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Destroy the textures a map owns.
|
||||
*
|
||||
* Call before reusing a tilemap struct for a different map; akgl_tilemap_load
|
||||
* zeroes rather than releases, so without this the previous map's textures are
|
||||
* leaked.
|
||||
*
|
||||
* @param dest The map to release. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
|
||||
*
|
||||
* @warning Known defect: the layer loop destroys `tilesets[i].texture` rather
|
||||
* than `layers[i].texture`, so tileset textures are destroyed twice and
|
||||
* image-layer textures never. Nothing is set to `NULL` either, so a
|
||||
* second call is a use-after-free. It also does not release the actors
|
||||
* the map's object layers created. TODO.md, "Known and still open"
|
||||
* item 2.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest);
|
||||
/**
|
||||
* @brief Scale an actor for its distance up the screen, using the map's perspective band.
|
||||
*
|
||||
* The pseudo-3D trick: an actor at or below `p_foreground_y` is drawn at
|
||||
* `p_foreground_scale`, one at or above `p_vanishing_y` at `p_vanishing_scale`,
|
||||
* and one in between is interpolated linearly. A map with no perspective markers
|
||||
* has both scales at 1.0 and a rate of 0, so this is a no-op rather than a
|
||||
* special case.
|
||||
*
|
||||
* Only the actor's `scale` is written; nothing is drawn. akgl_game_update calls
|
||||
* it before each actor's update when #AKGL_ITERATOR_OP_TILEMAPSCALE is set.
|
||||
*
|
||||
* @param map The map supplying the perspective band. Required.
|
||||
* @param actor The actor to scale. Required. Its `y` is read and its `scale`
|
||||
* written.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p map or @p actor is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor);
|
||||
|
||||
#endif //_TILEMAP_H_
|
||||
23
include/akgl/types.h
Normal file
23
include/akgl/types.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @file types.h
|
||||
* @brief Width-named floating point aliases, to match the `<stdint.h>` spelling.
|
||||
*
|
||||
* C names its integers by width and its floats by rank, so a struct full of
|
||||
* `uint32_t` and `float` reads inconsistently. These are the float spellings the
|
||||
* rest of the library uses. They are plain aliases -- no guarantee beyond what
|
||||
* the platform's `float` and `double` already provide -- and are deliberately
|
||||
* *not* C23's `_Float32`/`_Float64`, which carry IEEE-754 requirements this
|
||||
* library does not need.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_TYPES_H_
|
||||
#define _AKGL_TYPES_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** @brief Single-precision float. Positions, velocities, and scale factors. */
|
||||
typedef float float32_t;
|
||||
/** @brief Double-precision float. Declared for symmetry; unused so far. */
|
||||
typedef double float64_t;
|
||||
|
||||
#endif // _AKGL_TYPES_H_
|
||||
191
include/akgl/util.h
Normal file
191
include/akgl/util.h
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* @file util.h
|
||||
* @brief Axis-aligned collision tests, path resolution, and two test-only image helpers.
|
||||
*
|
||||
* The grab bag. Three unrelated groups live here: rectangle/point overlap for
|
||||
* the physics backend, path resolution for the asset loaders, and a pair of
|
||||
* pixel-comparison routines that exist only so tests can assert on what was
|
||||
* actually drawn.
|
||||
*
|
||||
* All the geometry here is axis-aligned and treats edges as touching: a point
|
||||
* exactly on a boundary is inside. There is no rotation support and no
|
||||
* separating-axis test.
|
||||
*/
|
||||
|
||||
#ifndef _UTIL_H_
|
||||
#define _UTIL_H_
|
||||
|
||||
#include <akerror.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
/** @brief An integer point. Carries a `z` the collision routines do not use. */
|
||||
typedef struct point {
|
||||
int x; /**< Horizontal position, in whatever space the caller is working in. */
|
||||
int y; /**< Vertical position. */
|
||||
int z; /**< Depth. Never written by akgl_rectangle_points and never read by the collision tests. */
|
||||
} point;
|
||||
|
||||
/**
|
||||
* @brief The four corners of an axis-aligned rectangle, precomputed.
|
||||
*
|
||||
* akgl_collide_rectangles works corner by corner rather than by comparing edge
|
||||
* spans, so it wants the corners as points. akgl_rectangle_points derives one of
|
||||
* these from an `SDL_FRect`.
|
||||
*/
|
||||
typedef struct RectanglePoints {
|
||||
point topleft; /**< (x, y). */
|
||||
point topright; /**< (x + w, y). */
|
||||
point bottomleft; /**< (x, y + h). */
|
||||
point bottomright; /**< (x + w, y + h). */
|
||||
} RectanglePoints;
|
||||
|
||||
/**
|
||||
* @brief Do not use. Three open parentheses, two closes -- any expansion is a
|
||||
* syntax error. It duplicates akgl_collide_rectangles(), has no callers,
|
||||
* and is slated for deletion. TODO.md item 20.
|
||||
*/
|
||||
#define AKGL_COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x)
|
||||
|
||||
/**
|
||||
* @brief Expand a rectangle into its four corner points.
|
||||
*
|
||||
* Coordinates are truncated from `float` to `int` on the way in, so a rectangle
|
||||
* at x = 10.9 has its corners at 10. That is deliberate for tile-grid work and
|
||||
* wrong for sub-pixel work; callers needing the latter should not round-trip
|
||||
* through this.
|
||||
*
|
||||
* @param dest Receives the corners. Required.
|
||||
* @param rect The rectangle, in any coordinate space. Required. `w` and `h` are
|
||||
* taken as extents from `x`/`y`, so a negative one produces a
|
||||
* rectangle whose "bottom right" is above and left of its "top
|
||||
* left" -- which every test here then reports as empty.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest or @p rect is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect);
|
||||
/**
|
||||
* @brief Test whether a point falls inside a rectangle, edges included.
|
||||
*
|
||||
* Compares against `topleft` and `bottomright` only, so it assumes @p r is
|
||||
* well-formed -- the two corners actually being the minimum and maximum. `z` is
|
||||
* ignored on both sides: this is a 2D test.
|
||||
*
|
||||
* @param p The point to test. Required.
|
||||
* @param r The rectangle, as corners from akgl_rectangle_points. Required.
|
||||
* @param collide Receives `true` when the point is inside or exactly on an edge,
|
||||
* `false` otherwise. Required -- the return value is the error
|
||||
* context. Not written on any failure path.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p p, @p r, or @p collide is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(point *p, RectanglePoints *r, bool *collide);
|
||||
/**
|
||||
* @brief Test whether two rectangles overlap, edges included.
|
||||
*
|
||||
* Tests all eight corners -- each rectangle's four against the other -- and
|
||||
* stops at the first hit. Checking both directions is what catches the case
|
||||
* where one rectangle is entirely inside the other and so has no corner within
|
||||
* its neighbour.
|
||||
*
|
||||
* @param r1 First rectangle. Required.
|
||||
* @param r2 Second rectangle. Required. Order does not matter.
|
||||
* @param collide Receives `true` on any overlap or shared edge, `false`
|
||||
* otherwise. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p r1, @p r2, or @p collide is `NULL`.
|
||||
*
|
||||
* @note A corner-containment test misses the one arrangement where two
|
||||
* rectangles overlap in a cross without either enclosing a corner of the
|
||||
* other -- a tall thin rectangle crossing a short wide one. Both are
|
||||
* reported as not colliding.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);
|
||||
|
||||
/**
|
||||
* @brief Resolve an asset path, trying the working directory before the given root.
|
||||
*
|
||||
* Asset files name their neighbours relatively -- a sprite definition names its
|
||||
* spritesheet, a tilemap names its tilesets -- and "relative" has to mean
|
||||
* relative to the file doing the naming, not to wherever the game was launched
|
||||
* from. So this tries @p path against the process working directory first, and
|
||||
* only if that does not exist joins it onto @p root and resolves that. Either
|
||||
* way the result is absolute, with symlinks and `..` folded out.
|
||||
*
|
||||
* @param root Directory to fall back to, normally `dirname` of the file that
|
||||
* contained @p path. Required, even when unused.
|
||||
* @param path The path to resolve, relative or absolute. Required.
|
||||
* @param dst Receives the resolved absolute path. Required, and must already be
|
||||
* a claimed pool string -- this writes into it, it does not claim
|
||||
* one for you.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dst is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If `root + "/" + path` would not fit in
|
||||
* #AKGL_MAX_STRING_LENGTH.
|
||||
* @throws ENOENT If neither spelling names an existing file. Any other `errno`
|
||||
* `realpath(3)` can raise -- EACCES on an unsearchable directory,
|
||||
* ELOOP, ENOTDIR -- propagates the same way.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @note The fallback path -- the common one, since most asset references are
|
||||
* relative to their own file rather than to the working directory --
|
||||
* returns straight out of the ENOENT handler and so never releases the
|
||||
* error context it was handling. Each such call consumes one slot of
|
||||
* libakerror's fixed 128-entry context array for the life of the process.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, akgl_String *dst);
|
||||
|
||||
// These are REALLY slow routines that are only useful in testing harnesses
|
||||
/**
|
||||
* @brief Assert that two surfaces hold byte-identical pixels.
|
||||
*
|
||||
* A `memcmp` over the raw pixel buffer, so it is exact: one differing byte in
|
||||
* one pixel is a failure. Meant for test harnesses asserting on rendered output,
|
||||
* not for anything on a frame path.
|
||||
*
|
||||
* @param s1 First surface. Required. Its `pitch * h` is what determines how many
|
||||
* bytes are compared.
|
||||
* @param s2 Second surface. Required.
|
||||
* @return `NULL` when the pixels match, otherwise an error context owned by the
|
||||
* caller. "Not equal" is reported as an error, not as an out-param.
|
||||
* @throws AKERR_NULLPOINTER If @p s1 or @p s2 is `NULL`.
|
||||
* @throws AKERR_VALUE If the pixels differ.
|
||||
*
|
||||
* @warning The surfaces' dimensions, pitch, and format are not compared, so a
|
||||
* smaller @p s2 is read past its end rather than reported as a
|
||||
* mismatch. TODO.md, "Known and still open" item 5.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2);
|
||||
/**
|
||||
* @brief Draw two textures in turn, read the framebuffer back after each, and compare.
|
||||
*
|
||||
* The test-harness counterpart to akgl_compare_sdl_surfaces: it answers "do
|
||||
* these two textures *render* the same", which is not the same question as "are
|
||||
* these two textures identical", because the renderer's scaling and blending sit
|
||||
* in between. Both are drawn into the same rectangle against a cleared target.
|
||||
*
|
||||
* @param t1 First texture. Required.
|
||||
* @param t2 Second texture. Required.
|
||||
* @param x Left edge of the region, in pixels. Used for the source
|
||||
* rectangle, the destination, and the readback alike.
|
||||
* @param y Top edge of the region.
|
||||
* @param w Width of the region.
|
||||
* @param h Height of the region.
|
||||
* @param writeout Optional filename for a PNG of the *first* render, written
|
||||
* under `SDL_GetBasePath()`. `NULL` skips it. This is a
|
||||
* debugging aid -- when an image assertion fails, this is how
|
||||
* you see what was actually drawn.
|
||||
* @return `NULL` when the two renders match, otherwise an error context owned by
|
||||
* the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p t1 or @p t2 is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the framebuffer cannot be read back.
|
||||
* @throws AKERR_IO If @p writeout is given and the PNG cannot be written.
|
||||
* @throws AKERR_VALUE If the two renders differ.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @warning Known defect: both passes draw @p t1 -- @p t2 is never rendered -- so
|
||||
* this currently always reports a match. TODO.md, "Known and still
|
||||
* open" item 1.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout);
|
||||
|
||||
#endif // _UTIL_H_
|
||||
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_
|
||||
@@ -1,105 +0,0 @@
|
||||
#ifndef _ACTOR_H_
|
||||
#define _ACTOR_H_
|
||||
|
||||
#include "character.h"
|
||||
|
||||
// ---- LOW WORD STATUSES ----
|
||||
|
||||
#define ACTOR_STATE_FACE_DOWN 1 << 0 // 1 0000 0000 0000 0001
|
||||
#define ACTOR_STATE_FACE_LEFT 1 << 1 // 2 0000 0000 0000 0010
|
||||
#define ACTOR_STATE_FACE_RIGHT 1 << 2 // 4 0000 0000 0000 0100
|
||||
#define ACTOR_STATE_FACE_UP 1 << 3 // 8 0000 0000 0000 1000
|
||||
#define ACTOR_STATE_ALIVE 1 << 4 // 16 0000 0000 0001 0000
|
||||
#define ACTOR_STATE_DYING 1 << 5 // 32 0000 0000 0010 0000
|
||||
#define ACTOR_STATE_DEAD 1 << 6 // 64 0000 0000 0100 0000
|
||||
#define ACTOR_STATE_MOVING_LEFT 1 << 7 // 128 0000 0000 1000 0000
|
||||
#define ACTOR_STATE_MOVING_RIGHT 1 << 8 // 256 0000 0001 0000 0000
|
||||
#define ACTOR_STATE_MOVING_UP 1 << 9 // 512 0000 0010 0000 0000
|
||||
#define ACTOR_STATE_MOVING_DOWN 1 << 10 // 1024 0000 0100 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_11 1 << 11 // 2048 0000 1000 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_12 1 << 12 // 4096 0001 0000 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_13 1 << 13 // 8192 0010 0000 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_14 1 << 14 // 16384 0100 0000 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_15 1 << 15 // 32768 1000 0000 0000 0000
|
||||
|
||||
// ----- HIGH WORD STATUSES -----
|
||||
|
||||
#define ACTOR_STATE_UNDEFINED_16 1 << 16 // 65536 0000 0000 0000 0001
|
||||
#define ACTOR_STATE_UNDEFINED_17 1 << 17 // 131072 0000 0000 0000 0010
|
||||
#define ACTOR_STATE_UNDEFINED_18 1 << 18 // 262144 0000 0000 0000 0100
|
||||
#define ACTOR_STATE_UNDEFINED_19 1 << 19 // 524288 0000 0000 0000 1000
|
||||
#define ACTOR_STATE_UNDEFINED_20 1 << 20 // 1048576 0000 0000 0001 0000
|
||||
#define ACTOR_STATE_UNDEFINED_21 1 << 21 // 2097152 0000 0000 0010 0000
|
||||
#define ACTOR_STATE_UNDEFINED_22 1 << 22 // 4194304 0000 0000 0100 0000
|
||||
#define ACTOR_STATE_UNDEFINED_23 1 << 23 // 8388608 0000 0000 1000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_24 1 << 24 // 16777216 0000 0001 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_25 1 << 25 // 33554432 0000 0010 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_26 1 << 26 // 67108864 0000 0100 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_27 1 << 27 // 134217728 0000 1000 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_28 1 << 28 // 268435456 0001 0000 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_29 1 << 29 // 536870912 0010 0000 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_30 1 << 30 // 1073741824 0100 0000 0000 0000
|
||||
#define ACTOR_STATE_UNDEFINED_31 1 << 31 // 2147483648 1000 0000 0000 0000
|
||||
|
||||
#define ACTOR_MAX_STATES 32
|
||||
|
||||
// This is an array of strings equal to actor states from 1-32.
|
||||
// This is built by a utility script and not kept in git, see
|
||||
// the Makefile for lib_src/actor_state_string_names.c
|
||||
extern char *ACTOR_STATE_STRING_NAMES[ACTOR_MAX_STATES+1];
|
||||
|
||||
#define ACTOR_STATE_FACE_ALL (ACTOR_STATE_FACE_DOWN | ACTOR_STATE_FACE_LEFT | ACTOR_STATE_FACE_RIGHT | ACTOR_STATE_FACE_UP)
|
||||
#define ACTOR_STATE_MOVING_ALL (ACTOR_STATE_MOVING_LEFT | ACTOR_STATE_MOVING_RIGHT | ACTOR_STATE_MOVING_UP | ACTOR_STATE_MOVING_DOWN)
|
||||
|
||||
#define ACTOR_MAX_NAME_LENGTH 128
|
||||
#define ACTOR_MAX_CHILDREN 8
|
||||
|
||||
#define MAX_HEAP_ACTOR 64
|
||||
|
||||
typedef struct actor {
|
||||
int refcount;
|
||||
char name[ACTOR_MAX_NAME_LENGTH];
|
||||
character *basechar;
|
||||
int curSpriteFrameId;
|
||||
SDL_Time curSpriteFrameTimer;
|
||||
bool curSpriteReversing;
|
||||
int layer;
|
||||
int state;
|
||||
bool movement_controls_face;
|
||||
void *actorData;
|
||||
bool visible;
|
||||
int logictimer;
|
||||
float x;
|
||||
float y;
|
||||
struct actor *children[ACTOR_MAX_CHILDREN];
|
||||
struct actor *parent;
|
||||
ErrorContext ERROR_NOIGNORE *(*updatefunc)(struct actor *obj);
|
||||
ErrorContext ERROR_NOIGNORE *(*renderfunc)(struct actor *obj, SDL_Renderer *renderer);
|
||||
ErrorContext ERROR_NOIGNORE *(*facefunc)(struct actor *obj);
|
||||
ErrorContext ERROR_NOIGNORE *(*movementlogicfunc)(struct actor *obj, SDL_Time curtimems);
|
||||
ErrorContext ERROR_NOIGNORE *(*changeframefunc)(struct actor *obj, sprite *curSprite, SDL_Time curtimems);
|
||||
ErrorContext ERROR_NOIGNORE *(*addchild)(struct actor *obj, struct actor *child);
|
||||
} actor;
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *actor_initialize(actor *obj, char *name);
|
||||
ErrorContext ERROR_NOIGNORE *actor_set_character(actor *obj, char *basecharname);
|
||||
ErrorContext ERROR_NOIGNORE *actor_render(actor *obj, SDL_Renderer *renderer);
|
||||
ErrorContext ERROR_NOIGNORE *actor_update(actor *obj);
|
||||
ErrorContext ERROR_NOIGNORE *actor_logic_movement(actor *obj, SDL_Time curtimems);
|
||||
ErrorContext ERROR_NOIGNORE *actor_logic_changeframe(actor *obj, sprite *curSprite, SDL_Time curtimems);
|
||||
ErrorContext ERROR_NOIGNORE *actor_automatic_face(actor *obj);
|
||||
ErrorContext ERROR_NOIGNORE *actor_add_child(actor *obj, actor *child);
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_left_on(actor *obj, SDL_Event *event);
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_left_off(actor *obj, SDL_Event *event);
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_right_on(actor *obj, SDL_Event *event);
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_right_off(actor *obj, SDL_Event *event);
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_up_on(actor *obj, SDL_Event *event);
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_up_off(actor *obj, SDL_Event *event);
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_down_on(actor *obj, SDL_Event *event);
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_down_off(actor *obj, SDL_Event *event);
|
||||
|
||||
|
||||
void registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name);
|
||||
|
||||
#endif // _ACTOR_H_
|
||||
@@ -1,8 +0,0 @@
|
||||
#ifndef _ASSETS_H_
|
||||
#define _ASSETS_H_
|
||||
|
||||
#include <sdlerror.h>
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *load_start_bgm(char *fname);
|
||||
|
||||
#endif //_ASSETS_H_
|
||||
@@ -1,31 +0,0 @@
|
||||
#ifndef _CHARACTER_H_
|
||||
#define _CHARACTER_H_
|
||||
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include "sprite.h"
|
||||
|
||||
#define SPRITE_MAX_CHARACTER_NAME_LENGTH 128
|
||||
#define MAX_HEAP_CHARACTER 256
|
||||
|
||||
typedef struct character {
|
||||
int refcount;
|
||||
char name[SPRITE_MAX_CHARACTER_NAME_LENGTH];
|
||||
SDL_PropertiesID state_sprites;
|
||||
ErrorContext ERROR_NOIGNORE *(*sprite_add)(struct character *, sprite *, int);
|
||||
ErrorContext ERROR_NOIGNORE *(*sprite_get)(struct character *, int, sprite **);
|
||||
int movementspeed;
|
||||
float vx;
|
||||
float vy;
|
||||
} character;
|
||||
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *character_initialize(character *basechar, char *name);
|
||||
ErrorContext ERROR_NOIGNORE *character_sprite_add(character *basechar, sprite *ref, int state);
|
||||
ErrorContext ERROR_NOIGNORE *character_sprite_get(character *basechar, int state, sprite **dest);
|
||||
|
||||
// This is an SDL iterator so we can't return our error state from it.
|
||||
void character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name);
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *character_load_json(char *filename);
|
||||
|
||||
#endif // _CHARACTER_H_
|
||||
@@ -1,40 +0,0 @@
|
||||
#ifndef _CONTROLLER_H_
|
||||
#define _CONTROLLER_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <sdlerror.h>
|
||||
|
||||
#define MAX_CONTROL_MAPS 8
|
||||
#define MAX_CONTROLS 32
|
||||
|
||||
typedef struct {
|
||||
Uint32 event_on;
|
||||
Uint32 event_off;
|
||||
Uint8 button;
|
||||
SDL_Keycode key;
|
||||
Uint8 axis;
|
||||
Uint8 axis_range_min;
|
||||
Uint8 axis_range_max;
|
||||
ErrorContext ERROR_NOIGNORE *(*handler_on)(actor *obj, SDL_Event *event);
|
||||
ErrorContext ERROR_NOIGNORE *(*handler_off)(actor *obj, SDL_Event *event);
|
||||
} SDL3GControl;
|
||||
|
||||
typedef struct {
|
||||
actor *target;
|
||||
SDL3GControl controls[MAX_CONTROLS];
|
||||
SDL_KeyboardID kbid;
|
||||
SDL_JoystickID jsid;
|
||||
SDL_MouseID mouseid;
|
||||
SDL_PenID penid;
|
||||
} SDL3GControlMap;
|
||||
|
||||
extern SDL3GControlMap GAME_ControlMaps[MAX_CONTROL_MAPS];
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *controller_handle_event(void *appstate, SDL_Event *event);
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *controller_handle_button_down(void *appstate, SDL_Event *event);
|
||||
ErrorContext ERROR_NOIGNORE *controller_handle_button_up(void *appstate, SDL_Event *event);
|
||||
ErrorContext ERROR_NOIGNORE *controller_handle_added(void *appstate, SDL_Event *event);
|
||||
ErrorContext ERROR_NOIGNORE *controller_handle_removed(void *appstate, SDL_Event *event);
|
||||
|
||||
#endif // _CONTROLLER_H_
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef _DRAW_H_
|
||||
#define _DRAW_H_
|
||||
|
||||
void GAME_draw_background(int w, int h);
|
||||
|
||||
#endif //_DRAW_H_
|
||||
@@ -1,46 +0,0 @@
|
||||
#ifndef _GAME_H_
|
||||
#define _GAME_H_
|
||||
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
#include "tilemap.h"
|
||||
|
||||
/* ==================== GAME STATE VARIABLES =================== */
|
||||
|
||||
typedef struct {
|
||||
float w;
|
||||
float h;
|
||||
SDL_Texture *texture;
|
||||
} GAME_frame;
|
||||
|
||||
typedef struct {
|
||||
int flags;
|
||||
} GameState;
|
||||
|
||||
typedef struct {
|
||||
char name[256];
|
||||
char version[32];
|
||||
char uri[256];
|
||||
int screenwidth;
|
||||
int screenheight;
|
||||
GameState state;
|
||||
MIX_Mixer *mixer;
|
||||
MIX_Track *tracks[64];
|
||||
} Game;
|
||||
|
||||
#define GAME_AUDIO_TRACK_BGM 1
|
||||
|
||||
extern SDL_Window *window;
|
||||
extern SDL_Renderer *renderer;
|
||||
extern tilemap gamemap;
|
||||
extern MIX_Audio *bgm;
|
||||
extern SDL_FRect camera;
|
||||
extern Game game;
|
||||
|
||||
#define BITMASK_HAS(x, y) (x & y) == y
|
||||
#define BITMASK_ADD(x, y) x |= y
|
||||
#define BITMASK_DEL(x, y) x &= ~(y)
|
||||
#define BITMASK_CLEAR(x) x = 0;
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *GAME_init();
|
||||
|
||||
#endif //_GAME_H_
|
||||
@@ -1,35 +0,0 @@
|
||||
#ifndef _HEAP_H_
|
||||
#define _HEAP_H_
|
||||
|
||||
#include "sprite.h"
|
||||
#include "actor.h"
|
||||
#include "character.h"
|
||||
#include "staticstring.h"
|
||||
#include <sdlerror.h>
|
||||
|
||||
#define MAX_HEAP_ACTOR 64
|
||||
#define MAX_HEAP_SPRITE (MAX_HEAP_ACTOR * 16)
|
||||
#define MAX_HEAP_SPRITESHEET MAX_HEAP_SPRITE
|
||||
#define MAX_HEAP_CHARACTER 256
|
||||
#define MAX_HEAP_STRING 256
|
||||
|
||||
extern actor HEAP_ACTOR[MAX_HEAP_ACTOR];
|
||||
extern sprite HEAP_SPRITE[MAX_HEAP_SPRITE];
|
||||
extern spritesheet HEAP_SPRITESHEET[MAX_HEAP_SPRITESHEET];
|
||||
extern character HEAP_CHARACTER[MAX_HEAP_CHARACTER];
|
||||
extern string HEAP_STRING[MAX_HEAP_STRING];
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *heap_init();
|
||||
ErrorContext ERROR_NOIGNORE *heap_next_actor(actor **dest);
|
||||
ErrorContext ERROR_NOIGNORE *heap_next_sprite(sprite **dest);
|
||||
ErrorContext ERROR_NOIGNORE *heap_next_spritesheet(spritesheet **dest);
|
||||
ErrorContext ERROR_NOIGNORE *heap_next_character(character **dest);
|
||||
ErrorContext ERROR_NOIGNORE *heap_next_string(string **dest);
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *heap_release_actor(actor *ptr);
|
||||
ErrorContext ERROR_NOIGNORE *heap_release_sprite(sprite *ptr);
|
||||
ErrorContext ERROR_NOIGNORE *heap_release_spritesheet(spritesheet *ptr);
|
||||
ErrorContext ERROR_NOIGNORE *heap_release_character(character *ptr);
|
||||
ErrorContext ERROR_NOIGNORE *heap_release_string(string *ptr);
|
||||
|
||||
#endif //_HEAP_H_
|
||||
@@ -1,43 +0,0 @@
|
||||
#ifndef _ITERATOR_H_
|
||||
#define _ITERATOR_H_
|
||||
|
||||
typedef struct {
|
||||
int flags;
|
||||
int layerid;
|
||||
} iterator;
|
||||
|
||||
#define ITERATOR_OP_UPDATE 1 // 1
|
||||
#define ITERATOR_OP_RENDER 1 << 1 // 2
|
||||
#define ITERATOR_OP_RELEASE 1 << 2 // 4
|
||||
#define ITERATOR_OP_LAYERMASK 1 << 3 // 8
|
||||
#define ITERATOR_OP_UNDEFINED_4 1 << 4 // 16
|
||||
#define ITERATOR_OP_UNDEFINED_5 1 << 5 // 32
|
||||
#define ITERATOR_OP_UNDEFINED_6 1 << 6 // 64
|
||||
#define ITERATOR_OP_UNDEFINED_7 1 << 7 // 128
|
||||
#define ITERATOR_OP_UNDEFINED_8 1 << 8 // 256
|
||||
#define ITERATOR_OP_UNDEFINED_9 1 << 9 // 512
|
||||
#define ITERATOR_OP_UNDEFINED_10 1 << 10 // 1024
|
||||
#define ITERATOR_OP_UNDEFINED_11 1 << 11 // 2048
|
||||
#define ITERATOR_OP_UNDEFINED_12 1 << 12 // 4096
|
||||
#define ITERATOR_OP_UNDEFINED_13 1 << 13 // 8192
|
||||
#define ITERATOR_OP_UNDEFINED_14 1 << 14 // 16384
|
||||
#define ITERATOR_OP_UNDEFINED_15 1 << 15 // 32768
|
||||
#define ITERATOR_OP_UNDEFINED_16 1 << 16 // 65536
|
||||
#define ITERATOR_OP_UNDEFINED_17 1 << 17 // 131072
|
||||
#define ITERATOR_OP_UNDEFINED_18 1 << 18 // 262144
|
||||
#define ITERATOR_OP_UNDEFINED_19 1 << 19 // 524288
|
||||
#define ITERATOR_OP_UNDEFINED_20 1 << 20 // 1048576
|
||||
#define ITERATOR_OP_UNDEFINED_21 1 << 21 // 2097152
|
||||
#define ITERATOR_OP_UNDEFINED_22 1 << 22 // 4194304
|
||||
#define ITERATOR_OP_UNDEFINED_23 1 << 23 // 8388608
|
||||
#define ITERATOR_OP_UNDEFINED_24 1 << 24 // 16777216
|
||||
#define ITERATOR_OP_UNDEFINED_25 1 << 25 // 33554432
|
||||
#define ITERATOR_OP_UNDEFINED_26 1 << 26 // 67108864
|
||||
#define ITERATOR_OP_UNDEFINED_27 1 << 27 // 134217728
|
||||
#define ITERATOR_OP_UNDEFINED_28 1 << 28 // 268435456
|
||||
#define ITERATOR_OP_UNDEFINED_29 1 << 29 // 536870912
|
||||
#define ITERATOR_OP_UNDEFINED_30 1 << 30 // 1073741824
|
||||
#define ITERATOR_OP_UNDEFINED_31 1 << 31 // 2147483648
|
||||
|
||||
|
||||
#endif // _ITERATOR_H_
|
||||
@@ -1,17 +0,0 @@
|
||||
#ifndef _JSON_HELPERS_H_
|
||||
#define _JSON_HELPERS_H_
|
||||
|
||||
#include <sdlerror.h>
|
||||
#include "staticstring.h"
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *get_json_object_value(json_t *obj, char *key, json_t **dest);
|
||||
ErrorContext ERROR_NOIGNORE *get_json_boolean_value(json_t *obj, char *key, bool *dest);
|
||||
ErrorContext ERROR_NOIGNORE *get_json_integer_value(json_t *obj, char *key, int *dest);
|
||||
ErrorContext ERROR_NOIGNORE *get_json_number_value(json_t *obj, char *key, float *dest);
|
||||
ErrorContext ERROR_NOIGNORE *get_json_string_value(json_t *obj, char *key, string **dest);
|
||||
ErrorContext ERROR_NOIGNORE *get_json_array_value(json_t *obj, char *key, json_t **dest);
|
||||
ErrorContext ERROR_NOIGNORE *get_json_array_index_object(json_t *array, int index, json_t **dest);
|
||||
ErrorContext ERROR_NOIGNORE *get_json_array_index_integer(json_t *array, int index, int *dest);
|
||||
ErrorContext ERROR_NOIGNORE *get_json_array_index_string(json_t *array, int index, string **dest);
|
||||
|
||||
#endif // _JSON_HELPERS_H_
|
||||
@@ -1,20 +0,0 @@
|
||||
#ifndef _REGISTRY_H_
|
||||
#define _REGISTRY_H_
|
||||
|
||||
#include "error.h"
|
||||
|
||||
extern SDL_PropertiesID REGISTRY_ACTOR;
|
||||
extern SDL_PropertiesID REGISTRY_SPRITE;
|
||||
extern SDL_PropertiesID REGISTRY_SPRITESHEET;
|
||||
extern SDL_PropertiesID REGISTRY_CHARACTER;
|
||||
extern SDL_PropertiesID REGISTRY_ACTOR_STATE_STRINGS;
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *registry_init();
|
||||
ErrorContext ERROR_NOIGNORE *registry_init_actor();
|
||||
ErrorContext ERROR_NOIGNORE *registry_init_sprite();
|
||||
ErrorContext ERROR_NOIGNORE *registry_init_spritesheet();
|
||||
ErrorContext ERROR_NOIGNORE *registry_init_character();
|
||||
ErrorContext ERROR_NOIGNORE *registry_init_actor_state_strings();
|
||||
|
||||
|
||||
#endif //_REGISTRY_H_
|
||||
@@ -1,43 +0,0 @@
|
||||
#ifndef _SPRITE_H_
|
||||
#define _SPRITE_H_
|
||||
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include <sdlerror.h>
|
||||
|
||||
|
||||
#define SPRITE_MAX_FRAMES 16
|
||||
#define SPRITE_MAX_NAME_LENGTH 128
|
||||
#define SPRITE_MAX_REGISTRY_SIZE 1024
|
||||
#define SPRITE_SHEET_MAX_FILENAME_LENGTH 512
|
||||
|
||||
#define MAX_HEAP_SPRITE (MAX_HEAP_ACTOR * 16)
|
||||
#define MAX_HEAP_SPRITESHEET MAX_HEAP_SPRITE
|
||||
|
||||
typedef struct {
|
||||
int refcount;
|
||||
SDL_Texture *texture;
|
||||
char name[SPRITE_SHEET_MAX_FILENAME_LENGTH];
|
||||
int sprite_w;
|
||||
int sprite_h;
|
||||
} spritesheet;
|
||||
|
||||
typedef struct {
|
||||
int refcount;
|
||||
spritesheet *sheet;
|
||||
int frameids[SPRITE_MAX_FRAMES]; // which IDs on the spritesheet belong to our frames
|
||||
int frames; // how many frames are in this animation
|
||||
int width;
|
||||
int height;
|
||||
int speed; // how many milliseconds a given sprite frame should be visible before cycling
|
||||
bool loop; // when this sprite is done playing, it should immediately start again
|
||||
bool loopReverse; // when this sprite is done playing, it should go in reverse order through its frames
|
||||
char name[SPRITE_MAX_NAME_LENGTH];
|
||||
} sprite;
|
||||
|
||||
// initializes a new sprite to use the given sheet and otherwise sets to zero
|
||||
ErrorContext ERROR_NOIGNORE *sprite_initialize(sprite *spr, char *name, spritesheet *sheet);
|
||||
// loads a given image file into a new spritesheet
|
||||
ErrorContext ERROR_NOIGNORE *spritesheet_initialize(spritesheet *sheet, int sprite_w, int sprite_h, char *filename);
|
||||
ErrorContext ERROR_NOIGNORE *sprite_load_json(char *filename);
|
||||
|
||||
#endif //_SPRITE_H_
|
||||
@@ -1,17 +0,0 @@
|
||||
#ifndef _STRING_H_
|
||||
#define _STRING_H_
|
||||
|
||||
#include "string.h"
|
||||
#include <sdlerror.h>
|
||||
|
||||
#define MAX_STRING_LENGTH 256
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int refcount;
|
||||
char data[MAX_STRING_LENGTH];
|
||||
} string;
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *string_initialize(string *obj, char *init);
|
||||
|
||||
#endif //_STRING_H_
|
||||
@@ -1,112 +0,0 @@
|
||||
#ifndef _TILEMAP_H_
|
||||
#define _TILEMAP_H_
|
||||
|
||||
#include "actor.h"
|
||||
#include "staticstring.h"
|
||||
#include <jansson.h>
|
||||
|
||||
#define TILEMAP_MAX_WIDTH 512
|
||||
#define TILEMAP_MAX_HEIGHT 512
|
||||
#define TILEMAP_MAX_LAYERS 16
|
||||
#define TILEMAP_MAX_TILESETS 16
|
||||
#define TILEMAP_MAX_TILES_PER_IMAGE 65536
|
||||
#define TILEMAP_MAX_TILESET_NAME_SIZE 512
|
||||
#define TILEMAP_MAX_TILESET_FILENAME_SIZE 512
|
||||
#define TILEMAP_MAX_OBJECT_NAME_SIZE 512
|
||||
#define TILEMAP_MAX_OBJECTS_PER_LAYER 128
|
||||
|
||||
#define TILEMAP_OBJECT_TYPE_ACTOR 1
|
||||
|
||||
#define TILEMAP_LAYER_TYPE_TILES 1
|
||||
#define TILEMAP_LAYER_TYPE_OBJECTS 2
|
||||
|
||||
typedef struct {
|
||||
float x;
|
||||
float y;
|
||||
int gid;
|
||||
int id;
|
||||
int height;
|
||||
int width;
|
||||
int rotation;
|
||||
int type;
|
||||
bool visible;
|
||||
actor *actorptr;
|
||||
char name[TILEMAP_MAX_OBJECT_NAME_SIZE];
|
||||
} tilemap_object;
|
||||
|
||||
typedef struct {
|
||||
short type;
|
||||
float opacity;
|
||||
bool visible;
|
||||
int height;
|
||||
int width;
|
||||
int x;
|
||||
int y;
|
||||
int id;
|
||||
int data[TILEMAP_MAX_WIDTH * TILEMAP_MAX_HEIGHT];
|
||||
tilemap_object objects[TILEMAP_MAX_OBJECTS_PER_LAYER];
|
||||
} tilemap_layer;
|
||||
|
||||
typedef struct {
|
||||
int columns;
|
||||
int firstgid;
|
||||
char imagefilename[TILEMAP_MAX_TILESET_FILENAME_SIZE];
|
||||
int imageheight;
|
||||
int imagewidth;
|
||||
char name[TILEMAP_MAX_TILESET_NAME_SIZE];
|
||||
SDL_Texture *texture;
|
||||
// Use this as a lookup table instead of storing tiles
|
||||
// in individual textures to blit them from a single
|
||||
// texture at runtime
|
||||
// FIXME: This is probably not very efficient. For a map
|
||||
// with a single tileset it makes sense. For a map with
|
||||
// multiple tilesets you may have set A start at firstgid 1
|
||||
// and have 1728 tiles. Set B may start at firstgid 1729 and
|
||||
// have 1728 more tiles. This means Set B has 1728 empty
|
||||
// tile_offsets[] entries before firstgid 1729 because of the
|
||||
// way tilemap_load_tilesets() works. This is really inefficient
|
||||
// and should be improved in the future, and will eventually
|
||||
// lead to premature exhaustion of TILEMAP_MAX_TILES_PER_IMAGE
|
||||
// because set D or E may only have 64 tiles but they may be
|
||||
// at the upper end of the array bound already because of this.
|
||||
int tile_offsets[TILEMAP_MAX_TILES_PER_IMAGE][2];
|
||||
int tilecount;
|
||||
int tileheight;
|
||||
int tilewidth;
|
||||
int spacing;
|
||||
int margin;
|
||||
} tileset;
|
||||
|
||||
typedef struct {
|
||||
int tilewidth;
|
||||
int tileheight;
|
||||
int width;
|
||||
int height;
|
||||
int numlayers;
|
||||
int orientation; // 0 = orthogonal, 1 = isometric
|
||||
int numtilesets;
|
||||
tileset tilesets[TILEMAP_MAX_TILESETS];
|
||||
tilemap_layer layers[TILEMAP_MAX_LAYERS];
|
||||
} tilemap;
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *tilemap_load(char *fname, tilemap *dest);
|
||||
ErrorContext ERROR_NOIGNORE *tilemap_draw(SDL_Renderer *renderer, tilemap *dest, SDL_FRect *viewport, int layeridx);
|
||||
ErrorContext ERROR_NOIGNORE *tilemap_draw_tileset(SDL_Renderer *renderer, tilemap *dest, int tilesetidx);
|
||||
|
||||
/*
|
||||
* These functions are part of the internal API and should not be called by the user.
|
||||
* They are only exposed here for unit testing.
|
||||
*/
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest);
|
||||
ErrorContext ERROR_NOIGNORE *get_json_properties_string(json_t *obj, char *key, string **dest);
|
||||
ErrorContext ERROR_NOIGNORE *get_json_properties_integer(json_t *obj, char *key, int *dest);
|
||||
ErrorContext ERROR_NOIGNORE *tilemap_compute_tileset_offsets(tilemap *dest, int tilesetidx);
|
||||
ErrorContext ERROR_NOIGNORE *tilemap_load_layer_objects(tilemap *dest, json_t *root, int layerid);
|
||||
ErrorContext ERROR_NOIGNORE *tilemap_load_layer_tile(tilemap *dest, json_t *root, int layerid);
|
||||
ErrorContext ERROR_NOIGNORE *tilemap_load_layers(tilemap *dest, json_t *root);
|
||||
ErrorContext ERROR_NOIGNORE *tilemap_load_tilesets_each(json_t *tileset, tilemap *dest, int tsidx);
|
||||
ErrorContext ERROR_NOIGNORE *tilemap_load_tilesets(tilemap *dest, json_t *root);
|
||||
|
||||
|
||||
#endif //_TILEMAP_H_
|
||||
@@ -1,29 +0,0 @@
|
||||
#ifndef _UTIL_H_
|
||||
#define _UTIL_H_
|
||||
|
||||
#include <sdlerror.h>
|
||||
|
||||
typedef struct point {
|
||||
int x;
|
||||
int y;
|
||||
int z;
|
||||
} point;
|
||||
|
||||
typedef struct RectanglePoints {
|
||||
point topleft;
|
||||
point topright;
|
||||
point bottomleft;
|
||||
point bottomright;
|
||||
} RectanglePoints;
|
||||
|
||||
#define COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x)
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *rectangle_points(RectanglePoints *dest, SDL_FRect *rect);
|
||||
ErrorContext ERROR_NOIGNORE *collide_point_rectangle(point *p, RectanglePoints *r, bool *collide);
|
||||
ErrorContext ERROR_NOIGNORE *collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);
|
||||
|
||||
// These are REALLY slow routines that are only useful in testing harnesses
|
||||
ErrorContext ERROR_NOIGNORE *compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2);
|
||||
ErrorContext ERROR_NOIGNORE *render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout);
|
||||
|
||||
#endif // _UTIL_H_
|
||||
32
mkcontrollermappings.sh
Executable file
32
mkcontrollermappings.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
rootdir=$1
|
||||
|
||||
curl https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt | grep -v '^#' | grep -v '^$' | sed s/',$'//g > mappings.txt
|
||||
|
||||
filelen=$(wc -l mappings.txt | cut -d ' ' -f 1)
|
||||
|
||||
cat > ${rootdir}/include/akgl/SDL_GameControllerDB.h <<EOF
|
||||
#ifndef _SDL_GAMECONTROLLERDB_H_
|
||||
#define _SDL_GAMECONTROLLERDB_H_
|
||||
|
||||
// Taken from https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt on $(date)
|
||||
|
||||
#define AKGL_SDL_GAMECONTROLLER_DB_LEN ${filelen}
|
||||
|
||||
const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
EOF
|
||||
|
||||
counter=0
|
||||
|
||||
cat mappings.txt | while read LINE;
|
||||
do
|
||||
if [[ $counter -gt 0 ]]; then
|
||||
printf ",\n" >> ${rootdir}/include/akgl/SDL_GameControllerDB.h;
|
||||
fi
|
||||
printf " \"${LINE}\"" >> ${rootdir}/include/akgl/SDL_GameControllerDB.h;
|
||||
counter=$((counter + 1))
|
||||
done
|
||||
|
||||
printf "\n};\n" >> ${rootdir}/include/akgl/SDL_GameControllerDB.h
|
||||
printf "#endif // _SDL_GAMECONTROLLERDB_H_\n" >> ${rootdir}/include/akgl/SDL_GameControllerDB.h
|
||||
11
rebuild.sh
Normal file
11
rebuild.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
export CMAKE_PREFIX_PATH=/home/andrew/local:/home/andrew/local/lib/cmake
|
||||
export CMAKE_MODULE_PATH=/home/andrew/local/lib/cmake
|
||||
#export SDL3_DIR=/home/andrew/local
|
||||
|
||||
rm -fr ~/local/lib/*akgl* ~/local/include/akgl build
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cmake --build build --parallel 4
|
||||
cmake --install build --prefix /home/andrew/local
|
||||
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
|
||||
108
scripts/memcheck.sh
Executable file
108
scripts/memcheck.sh
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Run the existing CTest suites under valgrind and report what leaked.
|
||||
#
|
||||
# scripts/memcheck.sh check every suite in ./build
|
||||
# scripts/memcheck.sh -R tilemap check only the suites matching a regex
|
||||
# scripts/memcheck.sh -LE perf skip the perf suites
|
||||
#
|
||||
# Anything after the build directory is handed straight to ctest, so every
|
||||
# selection flag ctest understands works here.
|
||||
#
|
||||
# Environment:
|
||||
# AKGL_BUILD_DIR build tree to check (default: build)
|
||||
# AKGL_MEMCHECK_LOG directory for the per-suite valgrind logs
|
||||
# (default: $AKGL_BUILD_DIR/Testing/Temporary)
|
||||
#
|
||||
# Exit status: 0 when valgrind found nothing, 1 when it found something, 2 on a
|
||||
# usage or environment error. This is the part `ctest -T memcheck` will not do
|
||||
# on its own -- it records defects and still exits 0, which is no use as a gate.
|
||||
|
||||
set -eu
|
||||
|
||||
AKGL_BUILD_DIR="${AKGL_BUILD_DIR:-build}"
|
||||
|
||||
function memcheck_die()
|
||||
{
|
||||
echo "memcheck: $*" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# The suite runs headless on purpose. Loading the real GPU stack costs thousands
|
||||
# of unfixable findings inside the vendor driver, and none of them are libakgl's.
|
||||
# A suite that sets these hints itself is unaffected; the ones that do not --
|
||||
# tests/tilemap.c and tests/sprite.c -- pick them up from here.
|
||||
export SDL_VIDEO_DRIVER="${SDL_VIDEO_DRIVER:-dummy}"
|
||||
export SDL_RENDER_DRIVER="${SDL_RENDER_DRIVER:-software}"
|
||||
export SDL_AUDIO_DRIVER="${SDL_AUDIO_DRIVER:-dummy}"
|
||||
|
||||
command -v valgrind >/dev/null 2>&1 || memcheck_die "valgrind not found"
|
||||
command -v ctest >/dev/null 2>&1 || memcheck_die "ctest not found"
|
||||
[ -f "$AKGL_BUILD_DIR/CMakeCache.txt" ] || memcheck_die "$AKGL_BUILD_DIR is not a configured build tree"
|
||||
|
||||
AKGL_MEMCHECK_LOG="${AKGL_MEMCHECK_LOG:-$AKGL_BUILD_DIR/Testing/Temporary}"
|
||||
|
||||
# Old logs would be counted as this run's findings. CTest numbers them by test
|
||||
# id, so a run narrowed with -R leaves the others behind.
|
||||
rm -f "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log
|
||||
|
||||
# The run itself is allowed to fail: a suite that valgrind makes slow enough to
|
||||
# trip an assertion still produced a log worth reading, and the log is what
|
||||
# decides the exit status below.
|
||||
set +e
|
||||
ctest --test-dir "$AKGL_BUILD_DIR" -T memcheck --output-on-failure "$@"
|
||||
AKGL_CTEST_STATUS=$?
|
||||
set -e
|
||||
|
||||
# What counts as a finding. "Definitely lost" is a leak nobody can argue with;
|
||||
# the invalid-access lines are memcheck telling us the library read or wrote
|
||||
# outside an allocation, which is worse than a leak and easier to fix.
|
||||
#
|
||||
# "are definitely lost", not "definitely lost": the LEAK SUMMARY block ends every
|
||||
# clean log with "definitely lost: 0 bytes in 0 blocks", and matching that
|
||||
# reported a finding in all twenty-three suites, including the ones that never
|
||||
# call malloc.
|
||||
AKGL_MEMCHECK_PATTERN='are definitely lost|Invalid read|Invalid write|Invalid free|Mismatched free|Source and destination overlap|depends on uninitialised|Use of uninitialised'
|
||||
|
||||
total=0
|
||||
echo
|
||||
echo "== valgrind findings by suite =="
|
||||
for log in "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log; do
|
||||
[ -f "$log" ] || continue
|
||||
# CTest writes the test name into the log's first line as the command it ran.
|
||||
suite=$(grep -m1 -oE 'akgl_test_[a-z_]+' "$log" || basename "$log")
|
||||
count=$(grep -cE "$AKGL_MEMCHECK_PATTERN" "$log" || true)
|
||||
total=$((total + count))
|
||||
if [ "$count" -gt 0 ]; then
|
||||
printf '%-24s %3d finding(s) %s\n' "$suite" "$count" "$log"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$total" -eq 0 ]; then
|
||||
echo "none"
|
||||
echo
|
||||
# A clean valgrind run and a failing test are different things: a suite can
|
||||
# assert its way to a non-zero exit without leaking a byte, and saying
|
||||
# "clean" and returning 0 for that would hide it.
|
||||
if [ "$AKGL_CTEST_STATUS" -ne 0 ]; then
|
||||
echo "memcheck: valgrind found nothing, but ctest exited $AKGL_CTEST_STATUS -- a suite failed on its own terms" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "memcheck: clean ($AKGL_MEMCHECK_LOG)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== library frames in those stacks, most frequent first =="
|
||||
# Keyed on the function name rather than the file, because src/physics.c and
|
||||
# tests/physics.c are both "physics.c" in a valgrind frame and only one of them
|
||||
# is the library. Anything a caller can reach is an akgl_ symbol by convention,
|
||||
# which makes the prefix a usable filter; a static helper appears under whatever
|
||||
# akgl_ function called it, one frame further down.
|
||||
grep -hoE 'by 0x[0-9A-Fa-f]+: akgl_[A-Za-z0-9_]+ \([a-z_]+\.c:[0-9]+\)' \
|
||||
"$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log 2>/dev/null |
|
||||
sed -E 's/^by 0x[0-9A-Fa-f]+: //' | sort | uniq -c | sort -rn | head -20
|
||||
|
||||
echo
|
||||
echo "memcheck: $total finding(s); full stacks in $AKGL_MEMCHECK_LOG" >&2
|
||||
exit 1
|
||||
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
|
||||
85
scripts/valgrind.supp
Normal file
85
scripts/valgrind.supp
Normal file
@@ -0,0 +1,85 @@
|
||||
# Valgrind suppressions for the libakgl memory-check run.
|
||||
#
|
||||
# Used by `cmake --build build --target memcheck` and by any hand-run
|
||||
# `ctest -T memcheck`; see AGENTS.md -> "Memory checking".
|
||||
#
|
||||
# Every entry here is a deliberate decision that a finding belongs to somebody
|
||||
# else's code and cannot be fixed from this repository. Nothing that libakgl
|
||||
# allocates is suppressed, and nothing is suppressed merely because it is noisy.
|
||||
# If a suppression starts hiding a real finding, delete it -- a false negative in
|
||||
# a leak check is worth more than a quiet log.
|
||||
#
|
||||
# The memcheck target runs the suite headless (SDL_VIDEO_DRIVER=dummy,
|
||||
# SDL_RENDER_DRIVER=software, SDL_AUDIO_DRIVER=dummy) precisely so that the GPU
|
||||
# stack is never loaded. That removes thousands of unfixable findings inside
|
||||
# amdgpu_dri.so / Mesa / libGLX without suppressing anything, which is why there
|
||||
# are no driver entries below. Run the suite against a real driver and you will
|
||||
# need them; do not add them here on that account.
|
||||
|
||||
# SDL allocates its global state -- the hint table, the property registry, the
|
||||
# log category array, the clipboard -- on first use and reclaims it in SDL_Quit.
|
||||
# A test that fails before SDL_Quit, and every test that deliberately leaves SDL
|
||||
# up so a later assertion can look at it, leaves those blocks behind. They are
|
||||
# one-per-process, not per-call, so they cannot accumulate into a leak.
|
||||
{
|
||||
sdl3-global-state-still-reachable
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
...
|
||||
fun:SDL_InitSubSystem_REAL
|
||||
}
|
||||
|
||||
# dlopen keeps the link map and the loaded objects' TLS blocks for the process
|
||||
# lifetime; a dlclose does not return them. SDL loads its video, audio, and
|
||||
# render backends this way, and SDL_image and SDL_mixer load their codecs the
|
||||
# same way.
|
||||
{
|
||||
dl-open-keeps-its-link-map
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable,possible
|
||||
...
|
||||
fun:_dl_open
|
||||
}
|
||||
|
||||
# FreeType, reached through SDL_ttf, caches per-face and per-size structures
|
||||
# inside the library instance and frees them in FT_Done_FreeType, which SDL_ttf
|
||||
# calls from TTF_Quit. A suite that opens a font and exits without TTF_Quit
|
||||
# reports them; a font libakgl itself failed to close is a different finding and
|
||||
# is NOT covered here -- it appears under akgl_text_loadfont, not under
|
||||
# FT_Init_FreeType.
|
||||
{
|
||||
freetype-library-instance
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
...
|
||||
fun:FT_Init_FreeType
|
||||
}
|
||||
|
||||
# deps/semver's own unit test leaks the buffers it cuts strings into --
|
||||
# semver_unit.c:8 and :21, one calloc each per case. Vendored code, and its test
|
||||
# program at that: not ours to edit, and not worth carrying a local patch for.
|
||||
# Named by function rather than by file so that a rewrite of those cases stops
|
||||
# being suppressed and comes back as a finding.
|
||||
#
|
||||
# This is the only entry here that hides a real leak in a program this build
|
||||
# runs. It is bounded, it is in a test, and the alternative is either a fork of
|
||||
# a vendored dependency or a memcheck run that is always red -- see TODO.md,
|
||||
# "Memory checking" item 39.
|
||||
{
|
||||
vendored-semver-unit-test-strcut-first
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: definite
|
||||
fun:calloc
|
||||
fun:test_strcut_first
|
||||
fun:test_strcut
|
||||
fun:main
|
||||
}
|
||||
{
|
||||
vendored-semver-unit-test-strcut-second
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: definite
|
||||
fun:calloc
|
||||
fun:test_strcut_second
|
||||
fun:test_strcut
|
||||
fun:main
|
||||
}
|
||||
503
src/actor.c
503
src/actor.c
@@ -1,38 +1,45 @@
|
||||
/**
|
||||
* @file actor.c
|
||||
* @brief Implements the actor subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <string.h>
|
||||
#include <sdlerror.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <sdl3game/game.h>
|
||||
#include <sdl3game/sprite.h>
|
||||
#include <sdl3game/actor.h>
|
||||
#include <sdl3game/heap.h>
|
||||
#include <sdl3game/registry.h>
|
||||
#include <sdl3game/staticstring.h>
|
||||
#include <sdl3game/iterator.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/iterator.h>
|
||||
|
||||
ErrorContext *actor_initialize(actor *obj, char *name)
|
||||
akerr_ErrorContext *akgl_actor_initialize(akgl_Actor *obj, char *name)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "actor_initialize received null actor pointer");
|
||||
FAIL_ZERO_RETURN(errctx, name, ERR_NULLPOINTER, "actor_initialize received null name string pointer");
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "akgl_actor_initialize received null actor pointer");
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "akgl_actor_initialize received null name string pointer");
|
||||
|
||||
memset(obj, 0x00, sizeof(actor));
|
||||
strncpy((char *)obj->name, name, ACTOR_MAX_NAME_LENGTH);
|
||||
memset(obj, 0x00, sizeof(akgl_Actor));
|
||||
strncpy((char *)obj->name, name, AKGL_ACTOR_MAX_NAME_LENGTH);
|
||||
obj->curSpriteReversing = false;
|
||||
obj->scale = 1.0;
|
||||
obj->movement_controls_face = true;
|
||||
|
||||
obj->updatefunc = &actor_update;
|
||||
obj->renderfunc = &actor_render;
|
||||
obj->facefunc = &actor_automatic_face;
|
||||
obj->movementlogicfunc = &actor_logic_movement;
|
||||
obj->changeframefunc = &actor_logic_changeframe;
|
||||
obj->addchild = &actor_add_child;
|
||||
|
||||
|
||||
obj->updatefunc = &akgl_actor_update;
|
||||
obj->renderfunc = &akgl_actor_render;
|
||||
obj->facefunc = &akgl_actor_automatic_face;
|
||||
obj->movementlogicfunc = &akgl_actor_logic_movement;
|
||||
obj->changeframefunc = &akgl_actor_logic_changeframe;
|
||||
obj->addchild = &akgl_actor_add_child;
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(REGISTRY_ACTOR, name, (void *)obj),
|
||||
ERR_KEY,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj),
|
||||
AKERR_KEY,
|
||||
"Unable to add actor to registry"
|
||||
);
|
||||
obj->refcount += 1;
|
||||
@@ -40,34 +47,38 @@ ErrorContext *actor_initialize(actor *obj, char *name)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *actor_set_character(actor *obj, char *basecharname)
|
||||
akerr_ErrorContext *akgl_actor_set_character(akgl_Actor *obj, char *basecharname)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "Null actor reference");
|
||||
FAIL_ZERO_RETURN(errctx, basecharname, ERR_NULLPOINTER, "Null character reference");
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
|
||||
FAIL_ZERO_RETURN(errctx, basecharname, AKERR_NULLPOINTER, "Null character reference");
|
||||
|
||||
obj->basechar = SDL_GetPointerProperty(REGISTRY_CHARACTER, basecharname, NULL);
|
||||
obj->basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, basecharname, NULL);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, ERR_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);
|
||||
}
|
||||
|
||||
ErrorContext *actor_automatic_face(actor *obj)
|
||||
akerr_ErrorContext *akgl_actor_automatic_face(akgl_Actor *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "Null actor reference");
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
|
||||
ATTEMPT {
|
||||
if ( obj->movement_controls_face == true ) {
|
||||
// TODO : This doesn't really work properly
|
||||
BITMASK_DEL(obj->state, ACTOR_STATE_FACE_ALL);
|
||||
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_LEFT) ) {
|
||||
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_LEFT);
|
||||
} else if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_RIGHT) ) {
|
||||
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_RIGHT);
|
||||
} else if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_UP) ) {
|
||||
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_UP);
|
||||
} else if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_DOWN) ) {
|
||||
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_DOWN);
|
||||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT) ) {
|
||||
AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
} else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
|
||||
AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_RIGHT);
|
||||
} else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
|
||||
AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_UP);
|
||||
} else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
|
||||
AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_DOWN);
|
||||
}
|
||||
}
|
||||
} CLEANUP {
|
||||
@@ -76,10 +87,10 @@ ErrorContext *actor_automatic_face(actor *obj)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *actor_logic_changeframe(actor *obj, sprite *curSprite, SDL_Time curtimems)
|
||||
akerr_ErrorContext *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtime)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "Null actor reference");
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
|
||||
ATTEMPT {
|
||||
// are we currently looping in reverse?
|
||||
if ( curSprite->loop == true && obj->curSpriteReversing == true ) {
|
||||
@@ -91,13 +102,14 @@ ErrorContext *actor_logic_changeframe(actor *obj, sprite *curSprite, SDL_Time cu
|
||||
obj->curSpriteFrameId -= 1;
|
||||
}
|
||||
// are we at the end of the animation?
|
||||
} else if ( obj->curSpriteFrameId == (curSprite->frames-1) ) {
|
||||
} else if ( obj->curSpriteFrameId >= (curSprite->frames-1) ) {
|
||||
// are we set to loop in reverse?
|
||||
if ( curSprite->loop == true && curSprite->loopReverse == true ) {
|
||||
obj->curSpriteReversing = true;
|
||||
obj->curSpriteFrameId -= 1;
|
||||
// are we set to loop forward?
|
||||
} else if ( curSprite->loop == true ) {
|
||||
} else {
|
||||
// we are at the end of the animation and we either loop forward or do not loop
|
||||
obj->curSpriteFrameId = 0;
|
||||
}
|
||||
// we are not looping in reverse and we are not at the end of the animation
|
||||
@@ -110,88 +122,94 @@ ErrorContext *actor_logic_changeframe(actor *obj, sprite *curSprite, SDL_Time cu
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *actor_logic_movement(actor *obj, SDL_Time curtimems)
|
||||
// raises AKGL_ERR_LOGICINTERRUPT if we don't want the physics simulator to process us
|
||||
akerr_ErrorContext *akgl_actor_logic_movement(akgl_Actor *actor, float32_t dt)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "Null actor reference");
|
||||
if ( obj->parent != NULL ) {
|
||||
// Children don't move independently of their parents, they just have an offset
|
||||
SUCCEED_RETURN(errctx);
|
||||
} else {
|
||||
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_LEFT) ) {
|
||||
obj->x -= obj->basechar->vx;
|
||||
}
|
||||
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_RIGHT) ) {
|
||||
obj->x += obj->basechar->vx;
|
||||
}
|
||||
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_UP) ) {
|
||||
obj->y -= obj->basechar->vy;
|
||||
}
|
||||
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_DOWN) ) {
|
||||
obj->y += obj->basechar->vy;
|
||||
}
|
||||
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
||||
FAIL_ZERO_RETURN(errctx, actor->basechar, AKERR_NULLPOINTER, "actor->basechar");
|
||||
actor->sx = actor->basechar->sx;
|
||||
actor->sy = actor->basechar->sy;
|
||||
actor->sz = actor->basechar->sz;
|
||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ) {
|
||||
actor->ax = -actor->basechar->ax;
|
||||
} else if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
|
||||
actor->ax = actor->basechar->ax;
|
||||
}
|
||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
|
||||
actor->ay = -actor->basechar->ay;
|
||||
} else if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
|
||||
actor->ay = actor->basechar->ay;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *actor_update(actor *obj)
|
||||
akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Time curtime = 0;
|
||||
SDL_Time curtimems = 0;
|
||||
sprite *curSprite = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor reference");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, ERR_NULLPOINTER, "Actor has NULL base character reference");
|
||||
akgl_Sprite *curSprite = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor reference");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
|
||||
|
||||
SDL_GetCurrentTime(&curtime);
|
||||
PASS(errctx, obj->facefunc(obj));
|
||||
|
||||
ATTEMPT {
|
||||
SDL_GetCurrentTime(&curtime);
|
||||
curtimems = curtime / 1000000;
|
||||
CATCH(errctx, obj->facefunc(obj));
|
||||
// is it time to apply movement logic?
|
||||
if ( (curtimems - obj->logictimer) >= obj->basechar->movementspeed ) {
|
||||
CATCH(errctx, obj->movementlogicfunc(obj, curtimems));
|
||||
obj->logictimer = curtimems;
|
||||
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
|
||||
if ( ((curtime) - obj->curSpriteFrameTimer) >= curSprite->speed) {
|
||||
CATCH(errctx, obj->changeframefunc(obj, curSprite, curtime));
|
||||
obj->curSpriteFrameTimer = curtime;
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, false);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, character_sprite_get(obj->basechar, obj->state, &curSprite));
|
||||
// is it time to change frames?
|
||||
if ( (curtimems - obj->curSpriteFrameTimer) >= curSprite->speed ) {
|
||||
CATCH(errctx, obj->changeframefunc(obj, curSprite, curtimems));
|
||||
obj->curSpriteFrameTimer = curtimems;
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, ERR_KEY) {
|
||||
} HANDLE(errctx, AKERR_KEY) {
|
||||
// TODO : Why are we passing this error? It could only come from akgl_character_sprite_get
|
||||
// or changeframefunc, both of which should never return AKERR_KEY...
|
||||
SUCCEED_RETURN(errctx);
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static ErrorContext *actor_visible(actor *obj, SDL_FRect *camera, bool *visible)
|
||||
/**
|
||||
* @brief Decide whether an actor is worth drawing this frame.
|
||||
*
|
||||
* Two questions at once: is the actor on camera, and does it want to be drawn.
|
||||
* The camera test allows a sprite's own width and height on the near edges, so
|
||||
* an actor partly on screen still counts as visible rather than popping in once
|
||||
* its origin crosses the boundary.
|
||||
*
|
||||
* An actor with no sprite for its current state is reported as not visible
|
||||
* rather than as an error -- there is nothing to draw, which is an answer.
|
||||
*
|
||||
* @param obj The actor to test. Required, along with its `basechar`.
|
||||
* @param camera The visible rectangle in map coordinates. Required in practice;
|
||||
* not checked, and dereferenced once a sprite has been found.
|
||||
* @param visible Receives the verdict. Required; not checked. Written on the
|
||||
* no-sprite path as well as the ordinary ones.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
|
||||
*/
|
||||
static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, bool *visible)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
sprite *curSprite = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, renderer, ERR_NULLPOINTER, "NULL renderer");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, ERR_NULLPOINTER, "Actor has NULL base character reference");
|
||||
|
||||
akgl_Sprite *curSprite = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, character_sprite_get(obj->basechar, obj->state, &curSprite));
|
||||
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, ERR_KEY) {
|
||||
} HANDLE(errctx, AKERR_KEY) {
|
||||
// TODO: Actor has no sprite matching the current state. Should we treat this as an error and throw?
|
||||
*visible = false;
|
||||
SUCCEED_RETURN(errctx);
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
if ( (obj->x < (camera->x - curSprite->width)) ||
|
||||
(obj->x > (camera->x + camera->w)) ||
|
||||
(obj->y < (camera->y - curSprite->height)) ||
|
||||
@@ -203,218 +221,225 @@ static ErrorContext *actor_visible(actor *obj, SDL_FRect *camera, bool *visible)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *actor_render(actor *obj, SDL_Renderer *renderer)
|
||||
akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
sprite *curSprite = NULL;
|
||||
akgl_Sprite *curSprite = NULL;
|
||||
bool visible = false;
|
||||
SDL_FRect src;
|
||||
SDL_FRect dest;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, renderer, ERR_NULLPOINTER, "NULL renderer");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, ERR_NULLPOINTER, "Actor has NULL base character reference");
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, character_sprite_get(obj->basechar, obj->state, &curSprite));
|
||||
CATCH(errctx, actor_visible(obj, &camera, &visible));
|
||||
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
|
||||
CATCH(errctx, actor_visible(obj, camera, &visible));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, ERR_KEY) {
|
||||
} HANDLE(errctx, AKERR_KEY) {
|
||||
} HANDLE_GROUP(errctx, AKERR_OUTOFBOUNDS) {
|
||||
// If an actor doesn't have a sprite for a state, just log it and move on
|
||||
LOG_ERROR(errctx);
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
if ( ! visible ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
src.x = curSprite->width * curSprite->frameids[obj->curSpriteFrameId];
|
||||
if ( src.x >= curSprite->sheet->texture->w ) {
|
||||
src.y = ((int)src.x / curSprite->sheet->texture->w) * curSprite->height;
|
||||
src.x = ((int)src.x % curSprite->sheet->texture->w);
|
||||
} else {
|
||||
src.y = 0;
|
||||
}
|
||||
src.w = curSprite->width;
|
||||
src.h = curSprite->height;
|
||||
if ( obj->parent != NULL ) {
|
||||
dest.x = (obj->parent->x + obj->x - camera.x);
|
||||
dest.y = (obj->parent->y + obj->y - camera.y);
|
||||
} else {
|
||||
dest.x = (obj->x - camera.x);
|
||||
dest.y = (obj->y - camera.y);
|
||||
}
|
||||
dest.w = curSprite->width;
|
||||
dest.h = curSprite->width;
|
||||
|
||||
SDL_RenderTexture(renderer, curSprite->sheet->texture, &src, &dest);
|
||||
if ( (obj->curSpriteFrameId > curSprite->frames) ) {
|
||||
// This isn't necessarily an error - this actor's frame index is outside the range of
|
||||
// their current sprite. There are a number of reasons this could happen, and it will
|
||||
// get cleaned up on the next logic update. Just pass on rendering them this frame.
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx,
|
||||
akgl_sprite_sheet_coords_for_frame(
|
||||
curSprite,
|
||||
&src,
|
||||
obj->curSpriteFrameId)
|
||||
);
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
if ( obj->parent != NULL ) {
|
||||
dest.x = (obj->parent->x + obj->x - camera->x);
|
||||
dest.y = (obj->parent->y + obj->y - camera->y);
|
||||
} else {
|
||||
dest.x = (obj->x - camera->x);
|
||||
dest.y = (obj->y - camera->y);
|
||||
}
|
||||
dest.w = curSprite->width * obj->scale;
|
||||
dest.h = curSprite->width * obj->scale;
|
||||
|
||||
PASS(errctx, renderer->draw_texture(renderer, curSprite->sheet->texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *actor_add_child(actor *obj, actor *child)
|
||||
akerr_ErrorContext *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child)
|
||||
{
|
||||
int i = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL parent pointer");
|
||||
FAIL_ZERO_RETURN(errctx, child, ERR_NULLPOINTER, "NULL child pointer");
|
||||
|
||||
FAIL_NONZERO_RETURN(errctx, child->parent, ERR_RELATIONSHIP, "Child object already has a parent");
|
||||
for ( i = 0; i < ACTOR_MAX_CHILDREN ; i++ ) {
|
||||
if ( obj->children[i] == NULL ) {
|
||||
obj->children[i] = child;
|
||||
child->parent = obj;
|
||||
child->refcount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
int i = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL parent pointer");
|
||||
FAIL_ZERO_RETURN(errctx, child, AKERR_NULLPOINTER, "NULL child pointer");
|
||||
|
||||
FAIL_NONZERO_RETURN(errctx, child->parent, AKERR_RELATIONSHIP, "Child object already has a parent");
|
||||
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN ; i++ ) {
|
||||
if ( obj->children[i] == NULL ) {
|
||||
obj->children[i] = child;
|
||||
child->parent = obj;
|
||||
child->refcount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
FAIL_RETURN(errctx, ERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
|
||||
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
|
||||
}
|
||||
|
||||
// SDL iterator so we can't return error information here, void only
|
||||
// this means we don't have anywhere to send exceptions up to, so if we hit an error, we log and exit(1) here
|
||||
void registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name)
|
||||
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
iterator *opflags = (iterator *)userdata;
|
||||
|
||||
akgl_Iterator *opflags = (akgl_Iterator *)userdata;
|
||||
akgl_Actor *obj = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, name, ERR_NULLPOINTER, "registry_iterate_actor received NULL property name");
|
||||
FAIL_ZERO_BREAK(errctx, opflags, ERR_NULLPOINTER, "received NULL iterator flags");
|
||||
actor *obj = (actor *)SDL_GetPointerProperty(registry, name, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, obj, ERR_KEY, "registry_iterate_actor received property name that was not in the registry");
|
||||
if ( BITMASK_HAS(opflags->flags, ITERATOR_OP_LAYERMASK) ) {
|
||||
if ( obj->layer != opflags->layerid ) {
|
||||
break;
|
||||
}
|
||||
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 ( BITMASK_HAS(opflags->flags, ITERATOR_OP_UPDATE) ) {
|
||||
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_UPDATE)) {
|
||||
CATCH(errctx, obj->updatefunc(obj));
|
||||
}
|
||||
if ( BITMASK_HAS(opflags->flags, ITERATOR_OP_RENDER) ) {
|
||||
CATCH(errctx, obj->renderfunc(obj, renderer));
|
||||
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);
|
||||
}
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_left_on(actor *obj, SDL_Event *event)
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event");
|
||||
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_LEFT) ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
SDL_Log("event %d (button %d / key %d) moves actor left", event->type, event->gbutton.which, event->key.key);
|
||||
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL));
|
||||
BITMASK_ADD(obj->state, (ACTOR_STATE_MOVING_LEFT | ACTOR_STATE_FACE_LEFT));
|
||||
SDL_Log("new target actor state: %b", obj->state);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
|
||||
//SDL_Log("event %d (button %d / key %d) moves actor left", event->type, event->gbutton.which, event->key.key);
|
||||
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
|
||||
obj->ax = -(obj->basechar->ax);
|
||||
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT));
|
||||
//SDL_Log("new target actor state: %b", obj->state);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_left_off(actor *obj, SDL_Event *event)
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event");
|
||||
if ( !BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_LEFT) ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
SDL_Log("event %d (button %d / key %d) stops moving actor left", event->type, event->gbutton.which, event->key.key);
|
||||
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL));
|
||||
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_LEFT);
|
||||
SDL_Log("new target actor state: %b", obj->state);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
//SDL_Log("event %d (button %d / key %d) stops moving actor left", event->type, event->gbutton.which, event->key.key);
|
||||
obj->ax = 0;
|
||||
obj->ex = 0;
|
||||
obj->tx = 0;
|
||||
obj->vx = 0;
|
||||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT);
|
||||
//SDL_Log("new target actor state: %b", obj->state);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_right_on(actor *obj, SDL_Event *event)
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event");
|
||||
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_RIGHT) ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
SDL_Log("event %d (button %d / key %d) moves actor right", event->type, event->gbutton.which, event->key.key);
|
||||
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL));
|
||||
BITMASK_ADD(obj->state, (ACTOR_STATE_MOVING_RIGHT | ACTOR_STATE_FACE_RIGHT));
|
||||
SDL_Log("new target actor state: %b", obj->state);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
|
||||
//SDL_Log("event %d (button %d / key %d) moves actor right", event->type, event->gbutton.which, event->key.key);
|
||||
obj->ax = obj->basechar->ax;
|
||||
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
|
||||
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT));
|
||||
//SDL_Log("new target actor state: %b", obj->state);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_right_off(actor *obj, SDL_Event *event)
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event");
|
||||
if ( !BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_RIGHT) ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
SDL_Log("event %d (button %d / key %d) stops moving actor right", event->type, event->gbutton.which, event->key.key);
|
||||
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL));
|
||||
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_RIGHT);
|
||||
SDL_Log("new target actor state: %b", obj->state);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
//SDL_Log("event %d (button %d / key %d) stops moving actor right", event->type, event->gbutton.which, event->key.key);
|
||||
obj->ax = 0;
|
||||
obj->ex = 0;
|
||||
obj->tx = 0;
|
||||
obj->vx = 0;
|
||||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
|
||||
//SDL_Log("new target actor state: %b", obj->state);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_up_on(actor *obj, SDL_Event *event)
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event");
|
||||
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_UP) ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
SDL_Log("event %d (button %d / key %d) moves actor up", event->type, event->gbutton.which, event->key.key);
|
||||
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL));
|
||||
BITMASK_ADD(obj->state, (ACTOR_STATE_FACE_UP | ACTOR_STATE_MOVING_UP));
|
||||
SDL_Log("new target actor state: %b", obj->state);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
|
||||
//SDL_Log("event %d (button %d / key %d) moves actor up", event->type, event->gbutton.which, event->key.key);
|
||||
obj->ay = -(obj->basechar->ay);
|
||||
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
|
||||
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_UP | AKGL_ACTOR_STATE_MOVING_UP));
|
||||
//SDL_Log("new target actor state: %b", obj->state);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_up_off(actor *obj, SDL_Event *event)
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event");
|
||||
if ( !BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_UP) ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
SDL_Log("event %d (button %d / key %d) stops moving actor up", event->type, event->gbutton.which, event->key.key);
|
||||
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL));
|
||||
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_UP);
|
||||
SDL_Log("new target actor state: %b", obj->state);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
//SDL_Log("event %d (button %d / key %d) stops moving actor up", event->type, event->gbutton.which, event->key.key);
|
||||
obj->ay = 0;
|
||||
obj->ey = 0;
|
||||
obj->ty = 0;
|
||||
obj->vy = 0;
|
||||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_UP);
|
||||
//SDL_Log("new target actor state: %b", obj->state);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_down_on(actor *obj, SDL_Event *event)
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event");
|
||||
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_DOWN) ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
SDL_Log("event %d (button %d / key %d) moves actor down", event->type, event->gbutton.which, event->key.key);
|
||||
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL));
|
||||
BITMASK_ADD(obj->state, (ACTOR_STATE_MOVING_DOWN | ACTOR_STATE_FACE_DOWN));
|
||||
SDL_Log("new target actor state: %b", obj->state);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
|
||||
//SDL_Log("event %d (button %d / key %d) moves actor down", event->type, event->gbutton.which, event->key.key);
|
||||
obj->ay = obj->basechar->ay;
|
||||
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
|
||||
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN));
|
||||
//SDL_Log("new target actor state: %b", obj->state);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_down_off(actor *obj, SDL_Event *event)
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event");
|
||||
if ( !BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_DOWN) ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
SDL_Log("event %d (button %d / key %d) stops moving actor down", event->type, event->gbutton.which, event->key.key);
|
||||
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL));
|
||||
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_DOWN);
|
||||
SDL_Log("new target actor state: %b", obj->state);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
//SDL_Log("event %d (button %d / key %d) stops moving actor down", event->type, event->gbutton.which, event->key.key);
|
||||
obj->ty = 0;
|
||||
obj->ey = 0;
|
||||
obj->ay = 0;
|
||||
obj->vy = 0;
|
||||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_DOWN);
|
||||
//SDL_Log("new target actor state: %b", obj->state);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
char *ACTOR_STATE_STRING_NAMES[32] = {
|
||||
"ACTOR_STATE_FACE_DOWN",
|
||||
"ACTOR_STATE_FACE_LEFT",
|
||||
"ACTOR_STATE_FACE_RIGHT",
|
||||
"ACTOR_STATE_FACE_UP",
|
||||
"ACTOR_STATE_ALIVE",
|
||||
"ACTOR_STATE_DYING",
|
||||
"ACTOR_STATE_DEAD",
|
||||
"ACTOR_STATE_MOVING_LEFT",
|
||||
"ACTOR_STATE_MOVING_RIGHT",
|
||||
"ACTOR_STATE_MOVING_UP",
|
||||
"ACTOR_STATE_MOVING_DOWN",
|
||||
"ACTOR_STATE_UNDEFINED_11",
|
||||
"ACTOR_STATE_UNDEFINED_12",
|
||||
"ACTOR_STATE_UNDEFINED_13",
|
||||
"ACTOR_STATE_UNDEFINED_14",
|
||||
"ACTOR_STATE_UNDEFINED_15",
|
||||
"ACTOR_STATE_UNDEFINED_16",
|
||||
"ACTOR_STATE_UNDEFINED_17",
|
||||
"ACTOR_STATE_UNDEFINED_18",
|
||||
"ACTOR_STATE_UNDEFINED_19",
|
||||
"ACTOR_STATE_UNDEFINED_20",
|
||||
"ACTOR_STATE_UNDEFINED_21",
|
||||
"ACTOR_STATE_UNDEFINED_22",
|
||||
"ACTOR_STATE_UNDEFINED_23",
|
||||
"ACTOR_STATE_UNDEFINED_24",
|
||||
"ACTOR_STATE_UNDEFINED_25",
|
||||
"ACTOR_STATE_UNDEFINED_26",
|
||||
"ACTOR_STATE_UNDEFINED_27",
|
||||
"ACTOR_STATE_UNDEFINED_28",
|
||||
"ACTOR_STATE_UNDEFINED_29",
|
||||
"ACTOR_STATE_UNDEFINED_30",
|
||||
"ACTOR_STATE_UNDEFINED_31",
|
||||
/**
|
||||
* @file actor_state_string_names.c
|
||||
* @brief Implements the actor state string names subsystem.
|
||||
*/
|
||||
|
||||
char *AKGL_ACTOR_STATE_STRING_NAMES[32] = {
|
||||
"AKGL_ACTOR_STATE_FACE_DOWN",
|
||||
"AKGL_ACTOR_STATE_FACE_LEFT",
|
||||
"AKGL_ACTOR_STATE_FACE_RIGHT",
|
||||
"AKGL_ACTOR_STATE_FACE_UP",
|
||||
"AKGL_ACTOR_STATE_ALIVE",
|
||||
"AKGL_ACTOR_STATE_DYING",
|
||||
"AKGL_ACTOR_STATE_DEAD",
|
||||
"AKGL_ACTOR_STATE_MOVING_LEFT",
|
||||
"AKGL_ACTOR_STATE_MOVING_RIGHT",
|
||||
"AKGL_ACTOR_STATE_MOVING_UP",
|
||||
"AKGL_ACTOR_STATE_MOVING_DOWN",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_11",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_12",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_13",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_14",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_15",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_16",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_17",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_18",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_19",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_20",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_21",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_22",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_23",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_24",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_25",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_26",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_27",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_28",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_29",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_30",
|
||||
"AKGL_ACTOR_STATE_UNDEFINED_31",
|
||||
};
|
||||
|
||||
92
src/assets.c
92
src/assets.c
@@ -1,53 +1,59 @@
|
||||
/**
|
||||
* @file assets.c
|
||||
* @brief Implements the assets subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
#include <sdlerror.h>
|
||||
#include <sdl3game/game.h>
|
||||
#include <sdl3game/staticstring.h>
|
||||
#include <sdl3game/heap.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
ErrorContext *load_start_bgm(char *fname)
|
||||
akerr_ErrorContext *akgl_load_start_bgm(char *fname)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
string *tmpstr = NULL;
|
||||
MIX_Track *bgmtrack = NULL;
|
||||
SDL_PropertiesID bgmprops = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
//akgl_String *tmpstr = NULL;
|
||||
MIX_Track *bgmtrack = NULL;
|
||||
SDL_PropertiesID bgmprops = 0;
|
||||
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, fname, ERR_NULLPOINTER, "load_start_bgm received NULL filename");
|
||||
CATCH(errctx, heap_next_string(&tmpstr));
|
||||
CATCH(errctx, string_initialize(tmpstr, NULL));
|
||||
|
||||
SDL_snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
|
||||
SDL_Log("Loading music asset from %s", (char *)&tmpstr->data);
|
||||
bgm = MIX_LoadAudio(game.mixer, (char *)&tmpstr->data, true);
|
||||
FAIL_ZERO_BREAK(errctx, bgm, ERR_NULLPOINTER, "Failed to load music asset %s : %s", (char *)&tmpstr->data, SDL_GetError());
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, fname, AKERR_NULLPOINTER, "akgl_load_start_bgm received NULL filename");
|
||||
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
||||
|
||||
bgmtrack = MIX_CreateTrack(game.mixer);
|
||||
FAIL_ZERO_BREAK(errctx, bgmtrack, ERR_NULLPOINTER, "Failed to create audio track for background music: %s", SDL_GetError());
|
||||
//SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
|
||||
SDL_Log("Loading music asset from %s", fname);
|
||||
bgm = MIX_LoadAudio(akgl_mixer, fname, true);
|
||||
FAIL_ZERO_BREAK(errctx, bgm, AKERR_NULLPOINTER, "Failed to load music asset %s : %s", fname, SDL_GetError());
|
||||
|
||||
game.tracks[GAME_AUDIO_TRACK_BGM] = bgmtrack;
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
MIX_SetTrackAudio(bgmtrack, bgm),
|
||||
ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
bgmtrack = MIX_CreateTrack(akgl_mixer);
|
||||
FAIL_ZERO_BREAK(errctx, bgmtrack, AKERR_NULLPOINTER, "Failed to create audio track for background music: %s", SDL_GetError());
|
||||
|
||||
SDL_SetNumberProperty(bgmprops, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
|
||||
|
||||
if (!MIX_PlayTrack(bgmtrack, bgmprops)) {
|
||||
FAIL_BREAK(errctx, ERR_SDL, "Failed to play music asset %s", fname);
|
||||
}
|
||||
} CLEANUP {
|
||||
IGNORE(heap_release_string(tmpstr));
|
||||
if ( errctx != NULL ) {
|
||||
if ( errctx->status != 0 && bgm != NULL) {
|
||||
MIX_DestroyAudio(bgm);
|
||||
}
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM] = bgmtrack;
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
MIX_SetTrackAudio(bgmtrack, bgm),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
|
||||
SDL_SetNumberProperty(bgmprops, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
|
||||
|
||||
if (!MIX_PlayTrack(bgmtrack, bgmprops)) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Failed to play music asset %s", fname);
|
||||
}
|
||||
} CLEANUP {
|
||||
//IGNORE(akgl_heap_release_string(tmpstr));
|
||||
if ( errctx != NULL ) {
|
||||
if ( errctx->status != 0 && bgm != NULL) {
|
||||
MIX_DestroyAudio(bgm);
|
||||
}
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
518
src/audio.c
Normal file
518
src/audio.c
Normal file
@@ -0,0 +1,518 @@
|
||||
/**
|
||||
* @file audio.c
|
||||
* @brief Implements the audio subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/audio.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES];
|
||||
|
||||
/*
|
||||
* The device akgl_audio_init() opened, or NULL when the voice table is not
|
||||
* connected to one. Everything that mutates a voice locks this stream when it
|
||||
* is open, because the device callback reads the same table on SDL's audio
|
||||
* thread. When it is NULL there is no other thread to race with.
|
||||
*/
|
||||
static SDL_AudioStream *audiostream = NULL;
|
||||
|
||||
/** @brief Level every voice is scaled by before the mix is clamped. */
|
||||
static float32_t mastervolume = 1.0f;
|
||||
|
||||
/** @brief Scratch the device callback mixes into before handing it to SDL. */
|
||||
static float32_t mixbuffer[AKGL_AUDIO_MIX_FRAMES];
|
||||
|
||||
/*
|
||||
* State for the noise waveform. A 32-bit xorshift rather than rand(): it needs
|
||||
* no allocation, no locking beyond what already guards the voice table, and it
|
||||
* produces the same sequence every run, so a test can assert on noise output.
|
||||
*/
|
||||
static uint32_t noisestate = 0x13579bdfu;
|
||||
|
||||
/** @brief Whether the voice table has been given its defaults yet. */
|
||||
static bool voicesready = false;
|
||||
|
||||
/**
|
||||
* @brief Put every voice back to a flat, audible default.
|
||||
*
|
||||
* A zeroed voice has a sustain of 0.0, which is silence -- so a caller who
|
||||
* sounded a note without first describing an envelope would get nothing and no
|
||||
* error saying why. The default is instead the simplest thing that makes noise:
|
||||
* a square wave with no attack, no decay and no release, held at full level for
|
||||
* as long as the gate is open.
|
||||
*/
|
||||
static void reset_voices(void)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
|
||||
akgl_audio_voices[i].active = false;
|
||||
akgl_audio_voices[i].waveform = AKGL_AUDIO_WAVE_SQUARE;
|
||||
akgl_audio_voices[i].hz = 0.0f;
|
||||
akgl_audio_voices[i].phase = 0.0f;
|
||||
akgl_audio_voices[i].duration_frames = 0;
|
||||
akgl_audio_voices[i].elapsed_frames = 0;
|
||||
akgl_audio_voices[i].attack_frames = 0;
|
||||
akgl_audio_voices[i].decay_frames = 0;
|
||||
akgl_audio_voices[i].release_frames = 0;
|
||||
akgl_audio_voices[i].sustain = 1.0f;
|
||||
akgl_audio_voices[i].sweep_from_hz = 0.0f;
|
||||
akgl_audio_voices[i].sweep_to_hz = 0.0f;
|
||||
akgl_audio_voices[i].sweep_step_hz = 0.0f;
|
||||
}
|
||||
mastervolume = 1.0f;
|
||||
voicesready = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Give the voice table its defaults the first time anything touches it.
|
||||
*
|
||||
* The table is process-wide static storage, so it is reachable before
|
||||
* akgl_audio_init() has run. Rather than make every entry point document an
|
||||
* ordering requirement, the first one to arrive sets the defaults.
|
||||
*/
|
||||
static void ensure_voices(void)
|
||||
{
|
||||
if ( voicesready == false ) {
|
||||
reset_voices();
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Lock the voice table against the device callback, if one is running. */
|
||||
static void lock_voices(void)
|
||||
{
|
||||
if ( audiostream != NULL ) {
|
||||
SDL_LockAudioStream(audiostream);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Release the lock taken by lock_voices(). */
|
||||
static void unlock_voices(void)
|
||||
{
|
||||
if ( audiostream != NULL ) {
|
||||
SDL_UnlockAudioStream(audiostream);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Convert a duration in milliseconds to a whole number of frames. */
|
||||
static uint32_t frames_for_ms(uint32_t ms)
|
||||
{
|
||||
return (uint32_t)(((uint64_t)ms * AKGL_AUDIO_SAMPLE_RATE) / 1000);
|
||||
}
|
||||
|
||||
/** @brief Next value of the noise oscillator, in the range -1.0 to 1.0. */
|
||||
static float32_t noise_sample(void)
|
||||
{
|
||||
noisestate ^= noisestate << 13;
|
||||
noisestate ^= noisestate >> 17;
|
||||
noisestate ^= noisestate << 5;
|
||||
// The top 24 bits are the well-mixed ones; scale them to -1..1.
|
||||
return (((float32_t)(noisestate >> 8) / 8388607.5f) - 1.0f);
|
||||
}
|
||||
|
||||
/** @brief One sample of @p voice's oscillator at its current phase. */
|
||||
static float32_t voice_oscillator(akgl_AudioVoice *voice)
|
||||
{
|
||||
float32_t value = 0.0f;
|
||||
|
||||
switch ( voice->waveform ) {
|
||||
case AKGL_AUDIO_WAVE_TRIANGLE:
|
||||
if ( voice->phase < 0.5f ) {
|
||||
value = (4.0f * voice->phase) - 1.0f;
|
||||
} else {
|
||||
value = 3.0f - (4.0f * voice->phase);
|
||||
}
|
||||
break;
|
||||
case AKGL_AUDIO_WAVE_SAWTOOTH:
|
||||
value = (2.0f * voice->phase) - 1.0f;
|
||||
break;
|
||||
case AKGL_AUDIO_WAVE_SQUARE:
|
||||
value = ( voice->phase < 0.5f ) ? 1.0f : -1.0f;
|
||||
break;
|
||||
case AKGL_AUDIO_WAVE_NOISE:
|
||||
value = noise_sample();
|
||||
break;
|
||||
case AKGL_AUDIO_WAVE_SINE:
|
||||
value = SDL_sinf(voice->phase * 2.0f * SDL_PI_F);
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Envelope level @p frame frames into the gate, before the release.
|
||||
*
|
||||
* Split out because the release has to start from wherever the gate left off,
|
||||
* which for a gate shorter than attack plus decay is partway up or down a
|
||||
* ramp rather than at the sustain level.
|
||||
*/
|
||||
static float32_t voice_gate_level(akgl_AudioVoice *voice, uint32_t frame)
|
||||
{
|
||||
uint32_t elapsed = frame;
|
||||
|
||||
if ( elapsed < voice->attack_frames ) {
|
||||
return (float32_t)elapsed / (float32_t)voice->attack_frames;
|
||||
}
|
||||
elapsed -= voice->attack_frames;
|
||||
if ( elapsed < voice->decay_frames ) {
|
||||
return 1.0f - ((1.0f - voice->sustain) * ((float32_t)elapsed / (float32_t)voice->decay_frames));
|
||||
}
|
||||
return voice->sustain;
|
||||
}
|
||||
|
||||
/** @brief Envelope level for @p voice where it currently stands. */
|
||||
static float32_t voice_envelope(akgl_AudioVoice *voice)
|
||||
{
|
||||
uint32_t released = 0;
|
||||
float32_t gatelevel = 0.0f;
|
||||
|
||||
if ( voice->elapsed_frames < voice->duration_frames ) {
|
||||
return voice_gate_level(voice, voice->elapsed_frames);
|
||||
}
|
||||
released = voice->elapsed_frames - voice->duration_frames;
|
||||
if ( released >= voice->release_frames ) {
|
||||
return 0.0f;
|
||||
}
|
||||
gatelevel = voice_gate_level(voice, voice->duration_frames);
|
||||
return gatelevel * (1.0f - ((float32_t)released / (float32_t)voice->release_frames));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Pitch @p voice sounds at where its sweep currently stands.
|
||||
*
|
||||
* Recomputed from the frame counter each time rather than added to as it goes,
|
||||
* so a sweep lands on exactly the frequencies its step size describes however
|
||||
* the caller happens to have chopped up its calls to akgl_audio_mix().
|
||||
*
|
||||
* A voice that is not sweeping keeps the frequency it was given.
|
||||
*/
|
||||
static float32_t voice_sweep_hz(akgl_AudioVoice *voice)
|
||||
{
|
||||
uint32_t ticks = 0;
|
||||
float32_t moved = 0.0f;
|
||||
float32_t hz = 0.0f;
|
||||
|
||||
if ( voice->sweep_step_hz <= 0.0f ) {
|
||||
return voice->hz;
|
||||
}
|
||||
ticks = voice->elapsed_frames / AKGL_AUDIO_SWEEP_TICK_FRAMES;
|
||||
moved = voice->sweep_step_hz * (float32_t)ticks;
|
||||
if ( voice->sweep_to_hz < voice->sweep_from_hz ) {
|
||||
hz = voice->sweep_from_hz - moved;
|
||||
if ( hz < voice->sweep_to_hz ) {
|
||||
hz = voice->sweep_to_hz;
|
||||
}
|
||||
} else {
|
||||
hz = voice->sweep_from_hz + moved;
|
||||
if ( hz > voice->sweep_to_hz ) {
|
||||
hz = voice->sweep_to_hz;
|
||||
}
|
||||
}
|
||||
return hz;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start a note on @p voice, sweeping or held.
|
||||
*
|
||||
* The whole of what akgl_audio_tone() and akgl_audio_sweep() do once their
|
||||
* arguments have been checked. A held note is a sweep with a step of 0, which
|
||||
* is also what makes a voice reused for a plain tone stop sweeping.
|
||||
*/
|
||||
static void start_note(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
|
||||
{
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
akgl_audio_voices[voice].hz = from_hz;
|
||||
akgl_audio_voices[voice].phase = 0.0f;
|
||||
akgl_audio_voices[voice].sweep_from_hz = from_hz;
|
||||
akgl_audio_voices[voice].sweep_to_hz = to_hz;
|
||||
akgl_audio_voices[voice].sweep_step_hz = step_hz;
|
||||
akgl_audio_voices[voice].duration_frames = frames_for_ms(ms);
|
||||
akgl_audio_voices[voice].elapsed_frames = 0;
|
||||
akgl_audio_voices[voice].active = true;
|
||||
unlock_voices();
|
||||
}
|
||||
|
||||
/** @brief Refuse a voice index that is not in the table. */
|
||||
static akerr_ErrorContext *check_voice(int voice)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((voice < 0) || (voice >= AKGL_AUDIO_MAX_VOICES)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Voice %d is not in the range 0 to %d",
|
||||
voice,
|
||||
AKGL_AUDIO_MAX_VOICES - 1);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fill SDL's request from the voice table.
|
||||
*
|
||||
* SDL holds the stream lock for the duration of this callback, which is the
|
||||
* same lock lock_voices() takes, so the voice table cannot change underneath a
|
||||
* mix in progress.
|
||||
*/
|
||||
static void SDLCALL audio_stream_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount)
|
||||
{
|
||||
akerr_ErrorContext *errctx = NULL;
|
||||
int frames = 0;
|
||||
|
||||
while ( additional_amount > 0 ) {
|
||||
frames = additional_amount / (int)sizeof(float32_t);
|
||||
if ( frames > AKGL_AUDIO_MIX_FRAMES ) {
|
||||
frames = AKGL_AUDIO_MIX_FRAMES;
|
||||
}
|
||||
if ( frames <= 0 ) {
|
||||
return;
|
||||
}
|
||||
errctx = akgl_audio_mix(mixbuffer, frames);
|
||||
if ( errctx != NULL ) {
|
||||
// There is nobody to return an error to on the audio thread, and
|
||||
// refusing to write leaves SDL underrunning. Report and go quiet.
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "** AUDIO CALLBACK **");
|
||||
errctx->handled = true;
|
||||
errctx = akerr_release_error(errctx);
|
||||
return;
|
||||
}
|
||||
SDL_PutAudioStreamData(stream, mixbuffer, frames * (int)sizeof(float32_t));
|
||||
additional_amount -= frames * (int)sizeof(float32_t);
|
||||
}
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void)
|
||||
{
|
||||
SDL_AudioSpec spec;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
if ( audiostream != NULL ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ensure_voices();
|
||||
|
||||
spec.format = SDL_AUDIO_F32;
|
||||
spec.channels = 1;
|
||||
spec.freq = AKGL_AUDIO_SAMPLE_RATE;
|
||||
|
||||
audiostream = SDL_OpenAudioDeviceStream(
|
||||
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||
&spec,
|
||||
&audio_stream_callback,
|
||||
NULL);
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
audiostream,
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't open an audio device: %s",
|
||||
SDL_GetError());
|
||||
|
||||
// Devices open paused so a caller can set a stream up before it is heard.
|
||||
// Nothing here needs that, and a caller who expected akgl_audio_tone() to
|
||||
// make a sound would otherwise get silence with no error to explain it.
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_ResumeAudioStreamDevice(audiostream),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't start the audio device: %s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void)
|
||||
{
|
||||
SDL_AudioStream *closing = audiostream;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
// Clear the pointer before destroying the stream: lock_voices() checks it,
|
||||
// and SDL_DestroyAudioStream can run the callback one last time.
|
||||
audiostream = NULL;
|
||||
if ( closing != NULL ) {
|
||||
SDL_DestroyAudioStream(closing);
|
||||
}
|
||||
reset_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
FAIL_NONZERO_RETURN(errctx, (hz <= 0.0f), AKERR_OUTOFBOUNDS, "Frequency %f is not positive", hz);
|
||||
FAIL_ZERO_RETURN(errctx, ms, AKERR_OUTOFBOUNDS, "A tone needs a duration; use akgl_audio_stop to silence a voice");
|
||||
|
||||
// A step of 0 is what says "one pitch, held", so this also clears a sweep
|
||||
// left on the voice by an earlier akgl_audio_sweep().
|
||||
start_note(voice, hz, hz, 0.0f, ms);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
FAIL_NONZERO_RETURN(errctx, (from_hz <= 0.0f), AKERR_OUTOFBOUNDS, "Start frequency %f is not positive", from_hz);
|
||||
FAIL_NONZERO_RETURN(errctx, (to_hz <= 0.0f), AKERR_OUTOFBOUNDS, "Target frequency %f is not positive", to_hz);
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
(step_hz <= 0.0f),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Step %f is not positive; the direction of a sweep comes from the two frequencies",
|
||||
step_hz);
|
||||
FAIL_ZERO_RETURN(errctx, ms, AKERR_OUTOFBOUNDS, "A tone needs a duration; use akgl_audio_stop to silence a voice");
|
||||
|
||||
start_note(voice, from_hz, to_hz, step_hz, ms);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
akgl_audio_voices[voice].active = false;
|
||||
akgl_audio_voices[voice].elapsed_frames = 0;
|
||||
akgl_audio_voices[voice].phase = 0.0f;
|
||||
unlock_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((waveform < AKGL_AUDIO_WAVE_TRIANGLE) || (waveform > AKGL_AUDIO_WAVE_SINE)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Waveform %d is not one of the %d shapes",
|
||||
(int)waveform,
|
||||
(int)AKGL_AUDIO_WAVE_SINE + 1);
|
||||
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
akgl_audio_voices[voice].waveform = waveform;
|
||||
unlock_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((sustain < 0.0f) || (sustain > 1.0f)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Sustain level %f is not between 0.0 and 1.0",
|
||||
sustain);
|
||||
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
akgl_audio_voices[voice].attack_frames = frames_for_ms(attack);
|
||||
akgl_audio_voices[voice].decay_frames = frames_for_ms(decay);
|
||||
akgl_audio_voices[voice].release_frames = frames_for_ms(release);
|
||||
akgl_audio_voices[voice].sustain = sustain;
|
||||
unlock_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((level < 0.0f) || (level > 1.0f)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Volume level %f is not between 0.0 and 1.0",
|
||||
level);
|
||||
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
mastervolume = level;
|
||||
unlock_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
FAIL_ZERO_RETURN(errctx, active, AKERR_NULLPOINTER, "NULL activity destination");
|
||||
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
*active = akgl_audio_voices[voice].active;
|
||||
unlock_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames)
|
||||
{
|
||||
akgl_AudioVoice *voice = NULL;
|
||||
float32_t sum = 0.0f;
|
||||
bool sweeping = false;
|
||||
int i = 0;
|
||||
int v = 0;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL sample destination");
|
||||
FAIL_NONZERO_RETURN(errctx, (frames < 0), AKERR_OUTOFBOUNDS, "Frame count %d is negative", frames);
|
||||
|
||||
for ( i = 0; i < frames; i++ ) {
|
||||
sum = 0.0f;
|
||||
for ( v = 0; v < AKGL_AUDIO_MAX_VOICES; v++ ) {
|
||||
voice = &akgl_audio_voices[v];
|
||||
if ( voice->active == false ) {
|
||||
continue;
|
||||
}
|
||||
if ( voice->elapsed_frames >= (voice->duration_frames + voice->release_frames) ) {
|
||||
// Gate and release are both spent. The voice goes quiet on its
|
||||
// own so a caller does not have to keep a clock to stop it.
|
||||
voice->active = false;
|
||||
voice->elapsed_frames = 0;
|
||||
voice->phase = 0.0f;
|
||||
continue;
|
||||
}
|
||||
sweeping = (voice->sweep_step_hz > 0.0f);
|
||||
if ( sweeping ) {
|
||||
voice->hz = voice_sweep_hz(voice);
|
||||
} else {
|
||||
// Derived from the frame counter rather than accumulated one
|
||||
// increment at a time. A float increment of hz/rate is not exact,
|
||||
// and adding it 44100 times a second walks the pitch off over the
|
||||
// length of a held note.
|
||||
voice->phase = (float32_t)SDL_fmod(
|
||||
((double)voice->elapsed_frames * (double)voice->hz) / (double)AKGL_AUDIO_SAMPLE_RATE,
|
||||
1.0);
|
||||
}
|
||||
sum += voice_oscillator(voice) * voice_envelope(voice);
|
||||
if ( sweeping ) {
|
||||
// The derived form above assumes one frequency for the whole note.
|
||||
// Under a sweep it would jump the waveform at every step -- an
|
||||
// audible click -- so a swept voice accumulates instead and takes
|
||||
// the drift the derived form exists to avoid.
|
||||
voice->phase = (float32_t)SDL_fmod(
|
||||
(double)voice->phase + ((double)voice->hz / (double)AKGL_AUDIO_SAMPLE_RATE),
|
||||
1.0);
|
||||
}
|
||||
voice->elapsed_frames += 1;
|
||||
}
|
||||
sum = sum * mastervolume;
|
||||
// Three voices at full level can sum past full scale. Clamping rather
|
||||
// than scaling by the voice count keeps a single voice at the level it
|
||||
// was asked for instead of a third of it.
|
||||
if ( sum > 1.0f ) {
|
||||
sum = 1.0f;
|
||||
} else if ( sum < -1.0f ) {
|
||||
sum = -1.0f;
|
||||
}
|
||||
dest[i] = sum;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
238
src/character.c
238
src/character.c
@@ -1,46 +1,51 @@
|
||||
/**
|
||||
* @file character.c
|
||||
* @brief Implements the character subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <sdlerror.h>
|
||||
#include <akerror.h>
|
||||
#include <string.h>
|
||||
#include <jansson.h>
|
||||
|
||||
#include <sdl3game/game.h>
|
||||
#include <sdl3game/sprite.h>
|
||||
#include <sdl3game/json_helpers.h>
|
||||
#include <sdl3game/heap.h>
|
||||
#include <sdl3game/registry.h>
|
||||
#include <sdl3game/staticstring.h>
|
||||
#include <sdl3game/iterator.h>
|
||||
#include <sdl3game/util.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/json_helpers.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/iterator.h>
|
||||
#include <akgl/util.h>
|
||||
|
||||
ErrorContext *character_initialize(character *obj, char *name)
|
||||
akerr_ErrorContext *akgl_character_initialize(akgl_Character *obj, char *name)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL character reference");
|
||||
FAIL_ZERO_RETURN(errctx, name, ERR_NULLPOINTER, "NULL name string pointer");
|
||||
memset(obj, 0x00, sizeof(character));
|
||||
strncpy(obj->name, name, SPRITE_MAX_CHARACTER_NAME_LENGTH);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL akgl_Character reference");
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL name string pointer");
|
||||
memset(obj, 0x00, sizeof(akgl_Character));
|
||||
strncpy(obj->name, name, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH);
|
||||
obj->state_sprites = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, obj->state_sprites, ERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map");
|
||||
|
||||
obj->sprite_add = &character_sprite_add;
|
||||
obj->sprite_get = &character_sprite_get;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, obj->state_sprites, AKERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map");
|
||||
|
||||
obj->sprite_add = &akgl_character_sprite_add;
|
||||
obj->sprite_get = &akgl_character_sprite_get;
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(REGISTRY_CHARACTER, name, (void *)obj),
|
||||
ERR_KEY,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_CHARACTER, name, (void *)obj),
|
||||
AKERR_KEY,
|
||||
"Unable to add character to registry");
|
||||
obj->refcount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *character_sprite_add(character *basechar, sprite *ref, int state)
|
||||
akerr_ErrorContext *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
char stateval[32];
|
||||
FAIL_ZERO_RETURN(errctx, basechar, ERR_NULLPOINTER, "NULL character reference");
|
||||
FAIL_ZERO_RETURN(errctx, ref, ERR_NULLPOINTER, "NULL sprite reference");
|
||||
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
|
||||
FAIL_ZERO_RETURN(errctx, ref, AKERR_NULLPOINTER, "NULL sprite reference");
|
||||
memset(&stateval, 0x00, 32);
|
||||
SDL_itoa(state, (char *)&stateval, 10);
|
||||
SDL_SetPointerProperty(basechar->state_sprites, (char *)&stateval, ref);
|
||||
@@ -49,17 +54,17 @@ ErrorContext *character_sprite_add(character *basechar, sprite *ref, int state)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *character_sprite_get(character *basechar, int state, sprite **dest)
|
||||
akerr_ErrorContext *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest)
|
||||
{
|
||||
sprite *target = NULL;
|
||||
akgl_Sprite *target = NULL;
|
||||
PREPARE_ERROR(errctx);
|
||||
char stateval[32];
|
||||
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "NULL pointer to sprite pointer (**dest)");
|
||||
FAIL_ZERO_RETURN(errctx, basechar, ERR_NULLPOINTER, "NULL character reference");
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer to sprite pointer (**dest)");
|
||||
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
|
||||
memset(&stateval, 0x00, 32);
|
||||
SDL_itoa(state, (char *)&stateval, 10);
|
||||
*dest = (sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL);
|
||||
FAIL_ZERO_RETURN(errctx, *dest, ERR_KEY, "Sprite for state %d (%b) not found in the character's registry", state, state);
|
||||
*dest = (akgl_Sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL);
|
||||
FAIL_ZERO_RETURN(errctx, *dest, AKERR_KEY, "Sprite for state %d (%b) not found in the character's registry", state, state);
|
||||
target = *dest;
|
||||
//SDL_Log("Sprite state %d (%s) has character %s", state, (char *)&stateval, target->name);
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -67,134 +72,193 @@ ErrorContext *character_sprite_get(character *basechar, int state, sprite **dest
|
||||
|
||||
// SDL iterator so we can't return error information here, void only
|
||||
// this means we don't have anywhere to send exceptions up to, so if we hit an error, we log and exit(1) here
|
||||
void character_state_sprites_iterate(void *userdata, SDL_PropertiesID registry, const char *name)
|
||||
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID registry, const char *name)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
sprite *spriteptr;
|
||||
iterator *opflags = (iterator *)userdata;
|
||||
akgl_Sprite *spriteptr;
|
||||
akgl_Iterator *opflags = (akgl_Iterator *)userdata;
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, opflags, ERR_NULLPOINTER, "Character state sprite iterator received null iterator op pointer");
|
||||
FAIL_ZERO_BREAK(errctx, name, ERR_NULLPOINTER, "Character state sprite iterator received null sprite name");
|
||||
spriteptr = (sprite *)SDL_GetPointerProperty(registry, name, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, spriteptr, ERR_NULLPOINTER, "Character state sprite for %s not found", name);
|
||||
if ( BITMASK_HAS(opflags->flags, ITERATOR_OP_RELEASE) ) {
|
||||
CATCH(errctx, heap_release_sprite(spriteptr));
|
||||
FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "Character state sprite iterator received null iterator op pointer");
|
||||
FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "Character state sprite iterator received null sprite name");
|
||||
spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(registry, name, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, spriteptr, AKERR_NULLPOINTER, "Character state sprite for %s not found", name);
|
||||
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RELEASE) ) {
|
||||
CATCH(errctx, akgl_heap_release_sprite(spriteptr));
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
|
||||
static ErrorContext *character_load_json_state_int_from_strings(json_t *states, int *dest)
|
||||
/**
|
||||
* @brief OR a JSON array of actor-state names together into one bitmask.
|
||||
*
|
||||
* This is what lets a character JSON say `["FACE_LEFT", "MOVING_LEFT"]` rather
|
||||
* than a number: each name is looked up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS
|
||||
* and OR-ed in. A name that is not a known state is an error rather than being
|
||||
* skipped -- a typo in a state name would otherwise bind a sprite to the wrong
|
||||
* combination and show up as art that never appears.
|
||||
*
|
||||
* @param states JSON array of state-name strings. Required. An empty array is
|
||||
* legal and leaves @p dest as it was.
|
||||
* @param dest Receives the OR of every named bit. Required in practice, and
|
||||
* **not** checked -- the guard that should test it tests @p states
|
||||
* a second time instead, so a `NULL` here is a crash. It is OR-ed
|
||||
* into rather than assigned, so the caller must zero it first.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p states is `NULL`.
|
||||
* @throws AKERR_KEY If a name is not in the actor-state registry. The message
|
||||
* quotes it. Note that bits 11 and 12 are unreachable by name; see
|
||||
* akgl_registry_init_actor_state_strings.
|
||||
* @throws AKERR_TYPE If an array element is not a string.
|
||||
* @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_t *states, int *dest)
|
||||
{
|
||||
int i = 0;
|
||||
long newstate = 0;
|
||||
string *tmpstring = NULL;
|
||||
akgl_String *tmpstring = NULL;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, states, ERR_NULLPOINTER, "NULL states array");
|
||||
FAIL_ZERO_RETURN(errctx, states, ERR_NULLPOINTER, "NULL destination integer");
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL states array");
|
||||
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL destination integer");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, heap_next_string(&tmpstring));
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstring));
|
||||
for ( i = 0; i < json_array_size((json_t *)states) ; i++ ) {
|
||||
CATCH(errctx, get_json_array_index_string(states, i, &tmpstring));
|
||||
newstate = (long)SDL_GetNumberProperty(REGISTRY_ACTOR_STATE_STRINGS, (char *)&tmpstring->data, 0);
|
||||
FAIL_ZERO_BREAK(errctx, newstate, ERR_KEY, "Unknown actor state %s", (char *)&tmpstring->data);
|
||||
CATCH(errctx, akgl_get_json_array_index_string(states, i, &tmpstring));
|
||||
newstate = (long)SDL_GetNumberProperty(AKGL_REGISTRY_ACTOR_STATE_STRINGS, (char *)&tmpstring->data, 0);
|
||||
FAIL_ZERO_BREAK(errctx, newstate, AKERR_KEY, "Unknown actor state %s", (char *)&tmpstring->data);
|
||||
*dest = (*dest | (int)(newstate));
|
||||
}
|
||||
} CLEANUP {
|
||||
IGNORE(heap_release_string(tmpstring));
|
||||
IGNORE(akgl_heap_release_string(tmpstring));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static ErrorContext *character_load_json_inner(json_t *json, character *obj)
|
||||
/**
|
||||
* @brief Name a character and build its state-to-sprite map from a JSON document.
|
||||
*
|
||||
* The half of akgl_character_load_json that deals with identity and sprites; the
|
||||
* caller handles the numeric fields afterwards. Reads `name`, initializes the
|
||||
* character with it, then walks `sprite_mappings` binding each named sprite to
|
||||
* the bitmask its `state` array adds up to.
|
||||
*
|
||||
* Every sprite named must already be in #AKGL_REGISTRY_SPRITE.
|
||||
*
|
||||
* @param json The parsed character document. Required in practice; not checked,
|
||||
* and passed straight to accessors that report it.
|
||||
* @param obj The pooled character to fill in. Required, unchecked -- it is
|
||||
* handed to akgl_character_initialize, which does check it.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If a mapping names a sprite that is not registered.
|
||||
* The message names the character, the state, and the sprite.
|
||||
* @throws AKERR_KEY If `name`, `sprite_mappings`, or a mapping's `sprite` or
|
||||
* `state` is absent, or if a state name is unknown.
|
||||
* @throws AKERR_TYPE If one of those keys has the wrong JSON type.
|
||||
* @throws AKERR_OUTOFBOUNDS If an array is indexed past its end.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Character *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
json_t *mappings = NULL;
|
||||
json_t *curmapping = NULL;
|
||||
json_t *statearray = NULL;
|
||||
sprite *spriteptr = NULL;
|
||||
akgl_Sprite *spriteptr = NULL;
|
||||
int i = 0;
|
||||
string *tmpstr = NULL;
|
||||
string *tmpstr2 = NULL;
|
||||
akgl_String *tmpstr = NULL;
|
||||
akgl_String *tmpstr2 = NULL;
|
||||
int stateval = 0;
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, get_json_string_value((json_t *)json, "name", &tmpstr));
|
||||
CATCH(errctx, character_initialize((character *)obj, tmpstr->data));
|
||||
CATCH(errctx, get_json_array_value((json_t *)json, "sprite_mappings", &mappings));
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr));
|
||||
CATCH(errctx, akgl_character_initialize((akgl_Character *)obj, tmpstr->data));
|
||||
CATCH(errctx, akgl_get_json_array_value((json_t *)json, "sprite_mappings", &mappings));
|
||||
for ( i = 0; i < json_array_size((json_t *)mappings) ; i++ ) {
|
||||
stateval = 0;
|
||||
CATCH(errctx, get_json_array_index_object((json_t *)mappings, i, &curmapping));
|
||||
CATCH(errctx, get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
|
||||
spriteptr = (sprite *)SDL_GetPointerProperty(
|
||||
REGISTRY_SPRITE,
|
||||
CATCH(errctx, akgl_get_json_array_index_object((json_t *)mappings, i, &curmapping));
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
|
||||
spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(
|
||||
AKGL_REGISTRY_SPRITE,
|
||||
tmpstr->data,
|
||||
NULL
|
||||
);
|
||||
CATCH(errctx, get_json_string_value((json_t *)json, "name", &tmpstr2));
|
||||
|
||||
CATCH(errctx, get_json_array_value((json_t *)curmapping, "state", &statearray));
|
||||
CATCH(errctx, character_load_json_state_int_from_strings(statearray, &stateval));
|
||||
|
||||
CATCH(errctx, get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr2));
|
||||
|
||||
CATCH(errctx, akgl_get_json_array_value((json_t *)curmapping, "state", &statearray));
|
||||
CATCH(errctx, akgl_character_load_json_state_int_from_strings(statearray, &stateval));
|
||||
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
spriteptr,
|
||||
ERR_NULLPOINTER,
|
||||
AKERR_NULLPOINTER,
|
||||
"Character %s for state %b references sprite %s but not found in the registry",
|
||||
tmpstr2->data,
|
||||
stateval,
|
||||
tmpstr->data
|
||||
);
|
||||
CATCH(errctx, character_sprite_add((character *)obj, (sprite *)spriteptr, stateval));
|
||||
CATCH(errctx, akgl_character_sprite_add((akgl_Character *)obj, (akgl_Sprite *)spriteptr, stateval));
|
||||
}
|
||||
} CLEANUP {
|
||||
if ( tmpstr != NULL ) {
|
||||
IGNORE(heap_release_string(tmpstr));
|
||||
IGNORE(akgl_heap_release_string(tmpstr));
|
||||
}
|
||||
if ( tmpstr2 != NULL ) {
|
||||
IGNORE(heap_release_string(tmpstr2));
|
||||
IGNORE(akgl_heap_release_string(tmpstr2));
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *character_load_json(char *filename)
|
||||
akerr_ErrorContext *akgl_character_load_json(char *filename)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
json_t *json;
|
||||
json_t *json = NULL;
|
||||
json_error_t error;
|
||||
character *obj = NULL;
|
||||
//string *tmpstr = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, filename, ERR_NULLPOINTER, "Received null filename");
|
||||
akgl_Character *obj = NULL;
|
||||
//akgl_String *tmpstr = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, heap_next_character(&obj));
|
||||
//CATCH(errctx, heap_next_string(&tmpstr));
|
||||
//CATCH(errctx, string_initialize(tmpstr, NULL));
|
||||
//SDL_snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
|
||||
CATCH(errctx, akgl_heap_next_character(&obj));
|
||||
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
||||
//SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
|
||||
json = (json_t *)json_load_file(filename, 0, &error);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
json,
|
||||
ERR_NULLPOINTER,
|
||||
AKERR_NULLPOINTER,
|
||||
"Error while loading character from %s on line %d: %s", filename, error.line, error.text
|
||||
);
|
||||
CATCH(errctx, character_load_json_inner(json, obj));
|
||||
CATCH(errctx, get_json_number_value(json, "velocity_x", &obj->vx));
|
||||
CATCH(errctx, get_json_number_value(json, "velocity_y", &obj->vy));
|
||||
CATCH(errctx, akgl_character_load_json_inner(json, obj));
|
||||
CATCH(errctx, akgl_get_json_integer_value(json, "speedtime", (int *)&obj->speedtime));
|
||||
obj->speedtime = obj->speedtime * AKGL_TIME_ONESEC_MS;
|
||||
CATCH(errctx, akgl_get_json_number_value(json, "speed_x", &obj->sx));
|
||||
CATCH(errctx, akgl_get_json_number_value(json, "speed_y", &obj->sy));
|
||||
CATCH(errctx, akgl_get_json_number_value(json, "acceleration_x", &obj->ax));
|
||||
CATCH(errctx, akgl_get_json_number_value(json, "acceleration_y", &obj->ay));
|
||||
} CLEANUP {
|
||||
//IGNORE(heap_release_string(tmpstr));
|
||||
//IGNORE(akgl_heap_release_string(tmpstr));
|
||||
// The character keeps nothing that points into the document -- every
|
||||
// field above is copied out of it -- so the whole parsed tree goes back
|
||||
// here, on the success path as well as the failure one.
|
||||
if ( json != NULL ) {
|
||||
json_decref(json);
|
||||
json = NULL;
|
||||
}
|
||||
if ( errctx != NULL ) {
|
||||
if ( errctx->status != 0 ) {
|
||||
IGNORE(heap_release_character(obj));
|
||||
IGNORE(akgl_heap_release_character(obj));
|
||||
}
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SDL_Log("Character %s loaded from %s", obj->name, filename);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
615
src/controller.c
615
src/controller.c
@@ -1,37 +1,277 @@
|
||||
/**
|
||||
* @file controller.c
|
||||
* @brief Implements the controller subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <sdlerror.h>
|
||||
#include <sdl3game/heap.h>
|
||||
#include <sdl3game/registry.h>
|
||||
#include <sdl3game/game.h>
|
||||
#include <sdl3game/controller.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/controller.h>
|
||||
|
||||
SDL3GControlMap GAME_ControlMaps[MAX_CONTROL_MAPS];
|
||||
akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
|
||||
|
||||
ErrorContext *controller_handle_event(void *appstate, SDL_Event *event)
|
||||
/*
|
||||
* Keystrokes waiting for akgl_controller_poll_key() and
|
||||
* akgl_controller_poll_keystroke(). Filled by akgl_controller_handle_event()
|
||||
* before it consults the control maps, so a key that also drives an actor is
|
||||
* still delivered to a polling caller.
|
||||
*
|
||||
* head is the next slot to read, count is how many are waiting. Both index a
|
||||
* fixed array rather than a queue object, which is the whole point: a host that
|
||||
* never polls cannot make this grow.
|
||||
*/
|
||||
static akgl_Keystroke keybuffer[AKGL_CONTROLLER_KEY_BUFFER];
|
||||
static int keybuffer_head = 0;
|
||||
static int keybuffer_count = 0;
|
||||
|
||||
/*
|
||||
* Whether the newest entry is a key press that has not been given its composed
|
||||
* text yet. SDL reports one keystroke as two events -- the key going down, and
|
||||
* then the text it composed to, if it composed to anything -- so the text that
|
||||
* arrives next belongs to the press that arrived last. Without this flag a
|
||||
* press dropped by a full buffer would hand its text to whatever older press
|
||||
* happened to still be sitting at the end of the ring.
|
||||
*/
|
||||
static bool keybuffer_awaiting_text = false;
|
||||
|
||||
/**
|
||||
* @brief Length in bytes of the UTF-8 sequence @p src begins with, or 0.
|
||||
*
|
||||
* 0 both for a byte that cannot start a sequence and for a sequence the string
|
||||
* ends in the middle of. SDL should hand over neither, and reading past a
|
||||
* terminator to find out otherwise is not a risk worth taking for input that
|
||||
* arrives from a keyboard driver.
|
||||
*
|
||||
* @param src UTF-8 text, NUL terminated. Required.
|
||||
*/
|
||||
static size_t utf8_sequence_length(const char *src)
|
||||
{
|
||||
unsigned char lead = (unsigned char)src[0];
|
||||
size_t len = 0;
|
||||
size_t i = 0;
|
||||
|
||||
if ( lead < 0x80 ) {
|
||||
return 1;
|
||||
} else if ( (lead & 0xe0) == 0xc0 ) {
|
||||
len = 2;
|
||||
} else if ( (lead & 0xf0) == 0xe0 ) {
|
||||
len = 3;
|
||||
} else if ( (lead & 0xf8) == 0xf0 ) {
|
||||
len = 4;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
for ( i = 1; i < len; i++ ) {
|
||||
// A NUL fails this too, which is what stops the scan at the end of a
|
||||
// truncated string instead of past it.
|
||||
if ( ((unsigned char)src[i] & 0xc0) != 0x80 ) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Copy as much of @p src as fits in @p dest without splitting a character.
|
||||
*
|
||||
* SDL hands over whatever the platform composed, which for an input method can
|
||||
* be several characters at once, and the ring entry holds one. Truncating on a
|
||||
* byte boundary would leave a partial UTF-8 sequence that nothing downstream
|
||||
* can render, so this stops at the last code point that fits whole.
|
||||
*
|
||||
* @param dest Receives the prefix, always NUL terminated. Required.
|
||||
* @param destsize Bytes available in @p dest, terminator included.
|
||||
* @param src UTF-8 text to copy. Required.
|
||||
*/
|
||||
static void copy_utf8_prefix(char *dest, size_t destsize, const char *src)
|
||||
{
|
||||
size_t used = 0;
|
||||
size_t len = 0;
|
||||
|
||||
dest[0] = '\0';
|
||||
while ( src[used] != '\0' ) {
|
||||
len = utf8_sequence_length(&src[used]);
|
||||
if ( len == 0 ) {
|
||||
// Not valid UTF-8 from here on. Keep what is already known good.
|
||||
break;
|
||||
}
|
||||
if ( (used + len) >= destsize ) {
|
||||
break;
|
||||
}
|
||||
memcpy(&dest[used], &src[used], len);
|
||||
used += len;
|
||||
}
|
||||
dest[used] = '\0';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Record one key press, discarding it if the buffer is already full.
|
||||
*
|
||||
* Dropping the newest rather than overwriting the oldest is deliberate. A
|
||||
* caller reading a line of input wants the characters that were typed first;
|
||||
* overwriting would hand it the tail of what the user typed and silently lose
|
||||
* the head.
|
||||
*
|
||||
* The entry starts with no text. Whatever the press composes to arrives as a
|
||||
* separate event and is attached by keybuffer_attach_text().
|
||||
*/
|
||||
static void keybuffer_push_key(SDL_Keycode key, SDL_Keymod mod)
|
||||
{
|
||||
int tail = 0;
|
||||
|
||||
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
|
||||
keybuffer_awaiting_text = false;
|
||||
return;
|
||||
}
|
||||
tail = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
|
||||
keybuffer[tail].key = key;
|
||||
keybuffer[tail].mod = mod;
|
||||
keybuffer[tail].text[0] = '\0';
|
||||
keybuffer_count += 1;
|
||||
keybuffer_awaiting_text = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Give composed text to the press it belongs to, or buffer it alone.
|
||||
*
|
||||
* An entry with no keycode is not a degenerate case to be avoided: an input
|
||||
* method or a dead key finishes a character with no key press of its own, and a
|
||||
* line editor wants that character. akgl_controller_poll_keystroke() reports it;
|
||||
* akgl_controller_poll_key(), which has no keycode to give, discards it.
|
||||
*/
|
||||
static void keybuffer_attach_text(const char *text)
|
||||
{
|
||||
int newest = 0;
|
||||
|
||||
if ( (keybuffer_awaiting_text == true) && (keybuffer_count > 0) ) {
|
||||
newest = (keybuffer_head + keybuffer_count - 1) % AKGL_CONTROLLER_KEY_BUFFER;
|
||||
copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text);
|
||||
keybuffer_awaiting_text = false;
|
||||
return;
|
||||
}
|
||||
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
|
||||
return;
|
||||
}
|
||||
newest = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
|
||||
keybuffer[newest].key = 0;
|
||||
keybuffer[newest].mod = 0;
|
||||
copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text);
|
||||
keybuffer_count += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Take the oldest entry, whatever it carries.
|
||||
*
|
||||
* @param dest Receives the entry. Required.
|
||||
* @return `true` when an entry was taken, `false` when the buffer was empty.
|
||||
*/
|
||||
static bool keybuffer_take(akgl_Keystroke *dest)
|
||||
{
|
||||
if ( keybuffer_count == 0 ) {
|
||||
return false;
|
||||
}
|
||||
memcpy(dest, &keybuffer[keybuffer_head], sizeof(akgl_Keystroke));
|
||||
keybuffer_head = (keybuffer_head + 1) % AKGL_CONTROLLER_KEY_BUFFER;
|
||||
keybuffer_count -= 1;
|
||||
if ( keybuffer_count == 0 ) {
|
||||
// Nothing is left for a text event to attach itself to.
|
||||
keybuffer_awaiting_text = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
|
||||
{
|
||||
int count;
|
||||
SDL_KeyboardID *keyboards = SDL_GetKeyboards(&count);
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
FAIL_ZERO_RETURN(e, keyboards, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
|
||||
// The array is SDL's to allocate and ours to free -- that is the contract on
|
||||
// every SDL_Get*s() enumeration. Released in CLEANUP so the loop can fail
|
||||
// without taking the array with it.
|
||||
ATTEMPT {
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *name = SDL_GetKeyboardNameForID(keyboards[i]);
|
||||
SDL_Log("Keyboard %d: ID %u, Name: %s\n", i, keyboards[i], name);
|
||||
}
|
||||
} CLEANUP {
|
||||
SDL_free(keyboards);
|
||||
keyboards = NULL;
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void)
|
||||
{
|
||||
int count = 0;
|
||||
int i = 0;
|
||||
SDL_JoystickID *gamepads = NULL;
|
||||
SDL_Gamepad *gamepad = NULL;
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
if ( SDL_HasGamepad() ) {
|
||||
gamepads = SDL_GetGamepads(&count);
|
||||
if ( count > 0 ) {
|
||||
FAIL_ZERO_RETURN(e, gamepads, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
for ( i = 0; i < count ; i++ ) {
|
||||
gamepad = SDL_OpenGamepad(gamepads[i]);
|
||||
FAIL_ZERO_RETURN(e, gamepad, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
SDL_Log("Gamepad %d is %s", i, SDL_GetGamepadNameForID(gamepads[i]));
|
||||
}
|
||||
SDL_free(gamepads);
|
||||
} else {
|
||||
SDL_Log("No gamepads enumerated");
|
||||
}
|
||||
} else {
|
||||
SDL_Log("No gamepads connected");
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *event)
|
||||
{
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int eventButtonComboMatch = 0;
|
||||
SDL3GControlMap *curmap = NULL;
|
||||
SDL3GControl *curcontrol = NULL;
|
||||
akgl_ControlMap *curmap = NULL;
|
||||
akgl_Control *curcontrol = NULL;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
|
||||
// Before the control maps, not after: a key bound to an actor is still a
|
||||
// key an interpreter polling with akgl_controller_poll_key() wants to see,
|
||||
// and the scan below returns as soon as a binding claims the event.
|
||||
if ( event->type == SDL_EVENT_KEY_DOWN ) {
|
||||
keybuffer_push_key(event->key.key, event->key.mod);
|
||||
} else if ( event->type == SDL_EVENT_TEXT_INPUT ) {
|
||||
// The character SDL composed for the press recorded a moment ago. A
|
||||
// keycode cannot express it: the layout, the shift state, a compose key
|
||||
// and a dead key all have their say between the two events.
|
||||
keybuffer_attach_text(event->text.text);
|
||||
}
|
||||
|
||||
ATTEMPT {
|
||||
for ( i = 0 ; i < MAX_CONTROL_MAPS; i++ ) {
|
||||
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
||||
curmap = &GAME_ControlMaps[i];
|
||||
if ( curmap->target == NULL ) {
|
||||
continue;
|
||||
}
|
||||
//SDL_Log("Control map %d maps to actor %s", i, curmap->target->name);
|
||||
//SDL_Log("event from keyboard %d", event->key.which);
|
||||
for ( j = 0; j < MAX_CONTROLS; j++ ) {
|
||||
for ( j = 0; j < AKGL_MAX_CONTROLS; j++ ) {
|
||||
curcontrol = &curmap->controls[j];
|
||||
//SDL_Log("button/key is processed by controlmap %d control %d", i, j);
|
||||
//SDL_Log("event %d -> control on %d off %d", event->type, curcontrol->event_on, curcontrol->event_off);
|
||||
// This controlmap processes this control
|
||||
eventButtonComboMatch = (
|
||||
((event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
|
||||
event->type == SDL_EVENT_GAMEPAD_BUTTON_UP) &&
|
||||
event->type == SDL_EVENT_GAMEPAD_BUTTON_UP) &&
|
||||
event->gbutton.which == curmap->jsid &&
|
||||
event->gbutton.button == curcontrol->button) ||
|
||||
((event->type == SDL_EVENT_KEY_DOWN ||
|
||||
@@ -39,135 +279,207 @@ ErrorContext *controller_handle_event(void *appstate, SDL_Event *event)
|
||||
event->key.which == curmap->kbid &&
|
||||
event->key.key == curcontrol->key)
|
||||
);
|
||||
if ( event->type == 768 && event->key.which == 11 && event->key.key == 13 ) {
|
||||
SDL_Log("Event type=%d, keyboard=%d, key=%d", event->type, event->key.which, event->key.key);
|
||||
SDL_Log("ControlMap[%d].Controls[%d] keyboard=%d, key=%d", i, j, curmap->kbid, curcontrol->key);
|
||||
SDL_Log("event %d -> control on %d off %d", event->type, curcontrol->event_on, curcontrol->event_off);
|
||||
SDL_Log("eventButtonComboMatch for controlmap %d id %d = %d",
|
||||
i, j, eventButtonComboMatch);
|
||||
}
|
||||
if ( event->type == curcontrol->event_on && eventButtonComboMatch) {
|
||||
CATCH(errctx, curcontrol->handler_on(curmap->target, event));
|
||||
goto _controller_handle_event_success;
|
||||
goto _akgl_controller_handle_event_success;
|
||||
} else if ( event->type == curcontrol->event_off && eventButtonComboMatch ) {
|
||||
CATCH(errctx, curcontrol->handler_off(curmap->target, event));
|
||||
goto _controller_handle_event_success;
|
||||
goto _akgl_controller_handle_event_success;
|
||||
}
|
||||
}
|
||||
}
|
||||
_controller_handle_event_success:
|
||||
_akgl_controller_handle_event_success:
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
||||
/**
|
||||
* @brief Set the movement and facing bits on the "player" actor for a D-pad or arrow press.
|
||||
*
|
||||
* A whole-application shortcut rather than a control-map binding: it looks the
|
||||
* actor up by the literal name `"player"` in #AKGL_REGISTRY_ACTOR instead of
|
||||
* being told which actor to act on, so it only ever drives one. The
|
||||
* akgl_Actor_cmhf_* handlers are the general form.
|
||||
*
|
||||
* Facing follows movement unless the actor sets `movement_controls_face`, which
|
||||
* is how an actor that aims independently of the direction it walks opts out.
|
||||
*
|
||||
* Declared nowhere -- `controller.h` advertises this as
|
||||
* `akgl_controller_handle_button_down`, which does not exist. TODO.md, "Known
|
||||
* and still open" item 10.
|
||||
*
|
||||
* @param appstate Passed through from SDL. Required as a non-`NULL` token only;
|
||||
* never read.
|
||||
* @param event The button or key event. Required. Both the gamepad and
|
||||
* keyboard unions are read on every call, so the arm that does
|
||||
* not correspond to the event type is read as garbage -- which
|
||||
* works only because no real button and keycode pair collides.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
|
||||
* no actor registered under the name "player".
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
||||
{
|
||||
actor *player = NULL;
|
||||
|
||||
akgl_Actor *player = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL event");
|
||||
player = SDL_GetPointerProperty(REGISTRY_ACTOR, "player", NULL);
|
||||
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "Player actor does not exist");
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
|
||||
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
|
||||
|
||||
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
|
||||
event->key.key == SDLK_DOWN ) {
|
||||
event->key.key == SDLK_DOWN ) {
|
||||
SDL_Log("Processing dpad down : state %d", player->state);
|
||||
BITMASK_ADD(player->state, ACTOR_STATE_MOVING_DOWN);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
|
||||
if ( !player->movement_controls_face ) {
|
||||
BITMASK_DEL(player->state, ACTOR_STATE_FACE_ALL);
|
||||
BITMASK_ADD(player->state, ACTOR_STATE_FACE_DOWN);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_DOWN);
|
||||
}
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP ||
|
||||
event->key.key == SDLK_UP ) {
|
||||
event->key.key == SDLK_UP ) {
|
||||
SDL_Log("Processing dpad up");
|
||||
BITMASK_ADD(player->state, ACTOR_STATE_MOVING_UP);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_UP);
|
||||
if ( !player->movement_controls_face ) {
|
||||
BITMASK_DEL(player->state, ACTOR_STATE_FACE_ALL);
|
||||
BITMASK_ADD(player->state, ACTOR_STATE_FACE_UP);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_UP);
|
||||
}
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT ||
|
||||
event->key.key == SDLK_LEFT ) {
|
||||
event->key.key == SDLK_LEFT ) {
|
||||
SDL_Log("Processing dpad left");
|
||||
BITMASK_ADD(player->state, ACTOR_STATE_MOVING_LEFT);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
|
||||
if ( !player->movement_controls_face ) {
|
||||
BITMASK_DEL(player->state, ACTOR_STATE_FACE_ALL);
|
||||
BITMASK_ADD(player->state, ACTOR_STATE_FACE_LEFT);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
}
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT ||
|
||||
event->key.key == SDLK_RIGHT ) {
|
||||
event->key.key == SDLK_RIGHT ) {
|
||||
SDL_Log("Processing dpad right");
|
||||
BITMASK_ADD(player->state, ACTOR_STATE_MOVING_RIGHT);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
|
||||
if ( !player->movement_controls_face ) {
|
||||
BITMASK_DEL(player->state, ACTOR_STATE_FACE_ALL);
|
||||
BITMASK_ADD(player->state, ACTOR_STATE_FACE_RIGHT);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_RIGHT);
|
||||
}
|
||||
SDL_Log("New state : %d", player->state);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
||||
/**
|
||||
* @brief Clear the movement bit on the "player" actor for a D-pad or arrow release, and reset its animation.
|
||||
*
|
||||
* The counterpart to gamepad_handle_button_down. It clears only the movement
|
||||
* bit, leaving the facing bits alone so the actor keeps looking the way it was
|
||||
* walking, and rewinds `curSpriteFrameId` to 0 so the next step starts from the
|
||||
* first frame of the walk cycle rather than wherever it stopped.
|
||||
*
|
||||
* Declared nowhere; see gamepad_handle_button_down.
|
||||
*
|
||||
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
|
||||
* @param event The button or key release event. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
|
||||
* no actor registered under the name "player".
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
||||
{
|
||||
actor *player = NULL;
|
||||
|
||||
akgl_Actor *player = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL event");
|
||||
player = SDL_GetPointerProperty(REGISTRY_ACTOR, "player", NULL);
|
||||
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "Player actor does not exist");
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
|
||||
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
|
||||
|
||||
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
|
||||
event->key.key == SDLK_DOWN ) {
|
||||
event->key.key == SDLK_DOWN ) {
|
||||
SDL_Log("processing down release");
|
||||
BITMASK_DEL(player->state, ACTOR_STATE_MOVING_DOWN);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
|
||||
player->curSpriteFrameId = 0;
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP ||
|
||||
event->key.key == SDLK_UP ) {
|
||||
event->key.key == SDLK_UP ) {
|
||||
SDL_Log("processing up release");
|
||||
BITMASK_DEL(player->state, ACTOR_STATE_MOVING_UP);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_UP);
|
||||
player->curSpriteFrameId = 0;
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT ||
|
||||
event->key.key == SDLK_RIGHT) {
|
||||
event->key.key == SDLK_RIGHT) {
|
||||
SDL_Log("processing right release");
|
||||
BITMASK_DEL(player->state, ACTOR_STATE_MOVING_RIGHT);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
|
||||
player->curSpriteFrameId = 0;
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT ||
|
||||
event->key.key == SDLK_LEFT ) {
|
||||
event->key.key == SDLK_LEFT ) {
|
||||
SDL_Log("processing left release");
|
||||
BITMASK_DEL(player->state, ACTOR_STATE_MOVING_LEFT);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
|
||||
player->curSpriteFrameId = 0;
|
||||
SDL_Log("New state : %d", player->state);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
||||
/**
|
||||
* @brief Open a gamepad that has just been plugged in, and log its mapping.
|
||||
*
|
||||
* SDL delivers no button events from an unopened gamepad, so a device that
|
||||
* appears mid-session has to be opened here the way akgl_controller_open_gamepads
|
||||
* opens the ones present at startup. A gamepad SDL has already opened is logged
|
||||
* and left alone.
|
||||
*
|
||||
* The mapping is logged because a controller with no entry in the database
|
||||
* produces no button events at all, and that is otherwise indistinguishable
|
||||
* from a broken binding.
|
||||
*
|
||||
* Declared nowhere; see gamepad_handle_button_down.
|
||||
*
|
||||
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
|
||||
* @param event The SDL_EVENT_GAMEPAD_ADDED event. Required. The joystick id
|
||||
* is read out of the `gbutton` arm rather than `gdevice`, which
|
||||
* works because the two share their `which` field.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
|
||||
*
|
||||
* @note A failed `SDL_OpenGamepad` is logged rather than reported: this returns
|
||||
* success and the device stays silent.
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
||||
{
|
||||
SDL_JoystickID which;
|
||||
SDL_Gamepad *gamepad = NULL;
|
||||
char *mapping = NULL;
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL event");
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
|
||||
which = event->gbutton.which;
|
||||
gamepad = SDL_GetGamepadFromID(which);
|
||||
|
||||
|
||||
if (!gamepad) {
|
||||
SDL_Log("Gamepad #%u add, but not opened: %s", (unsigned int) which, SDL_GetError());
|
||||
gamepad = SDL_OpenGamepad(which);
|
||||
SDL_Log("Gamepad #%u opened: %s", (unsigned int) which, SDL_GetError());
|
||||
mapping = SDL_GetGamepadMapping(gamepad);
|
||||
if ( mapping == NULL ) {
|
||||
SDL_Log("Gamepad #%u has no mapping!", (unsigned int) which);
|
||||
SDL_Log("Gamepad #%u has no mapping!", (unsigned int) which);
|
||||
} else if ( mapping != NULL ) {
|
||||
SDL_Log("Gamepad #%u mapping : %s", (unsigned int) which, mapping);
|
||||
SDL_free(mapping);
|
||||
SDL_Log("Gamepad #%u mapping : %s", (unsigned int) which, mapping);
|
||||
SDL_free(mapping);
|
||||
}
|
||||
} else {
|
||||
SDL_Log("Gamepad #%u ('%s') added", (unsigned int) which, SDL_GetGamepadName(gamepad));
|
||||
@@ -175,21 +487,190 @@ ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
|
||||
/**
|
||||
* @brief Close a gamepad that has been unplugged.
|
||||
*
|
||||
* An unplugged device SDL still has open is a leaked handle, so this closes it.
|
||||
* A removal for a device that was never opened is logged and otherwise ignored.
|
||||
* Any control map still holding that `jsid` simply stops matching -- the map is
|
||||
* not torn down.
|
||||
*
|
||||
* Declared nowhere; see gamepad_handle_button_down.
|
||||
*
|
||||
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
|
||||
* @param event The SDL_EVENT_GAMEPAD_REMOVED event. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
|
||||
{
|
||||
SDL_JoystickID which;
|
||||
SDL_Gamepad *gamepad = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL event");
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
|
||||
which = event->gbutton.which;
|
||||
gamepad = SDL_GetGamepadFromID(which);
|
||||
|
||||
|
||||
if (gamepad) {
|
||||
SDL_CloseGamepad(gamepad); /* the joystick was unplugged. */
|
||||
}
|
||||
SDL_Log("Gamepad #%u removed", (unsigned int) which);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control)
|
||||
{
|
||||
int newmapid = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_RETURN(errctx, control, AKERR_NULLPOINTER, "NULL Control");
|
||||
FAIL_NONZERO_RETURN(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS);
|
||||
newmapid = GAME_ControlMaps[controlmapid].nextMap;
|
||||
FAIL_ZERO_RETURN(errctx, (AKGL_MAX_CONTROLS - newmapid), AKERR_OUTOFBOUNDS, "Control map ID %d is full", controlmapid);
|
||||
memcpy((void *)&GAME_ControlMaps[controlmapid].controls[newmapid], control, sizeof(akgl_Control));
|
||||
GAME_ControlMaps[controlmapid].nextMap = newmapid + 1;
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid)
|
||||
{
|
||||
akgl_ControlMap *controlmap;
|
||||
akgl_Control control;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
// set up the control map
|
||||
FAIL_NONZERO_RETURN(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS);
|
||||
memset((void *)&control, 0x00, sizeof(akgl_Control));
|
||||
controlmap = &GAME_ControlMaps[controlmapid];
|
||||
controlmap->kbid = kbid;
|
||||
controlmap->jsid = jsid;
|
||||
|
||||
controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, controlmap->target, AKGL_ERR_REGISTRY, "Actor %s not found in registry", actorname);
|
||||
|
||||
// ---- KEYBOARD CONTROLS ----
|
||||
|
||||
// Move down
|
||||
control.key = SDLK_DOWN;
|
||||
control.event_on = SDL_EVENT_KEY_DOWN;
|
||||
control.event_off = SDL_EVENT_KEY_UP;
|
||||
control.handler_on = &akgl_Actor_cmhf_down_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_down_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
// Move up
|
||||
control.key = SDLK_UP;
|
||||
control.event_on = SDL_EVENT_KEY_DOWN;
|
||||
control.event_off = SDL_EVENT_KEY_UP;
|
||||
control.handler_on = &akgl_Actor_cmhf_up_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_up_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
// Move left
|
||||
control.key = SDLK_LEFT;
|
||||
control.event_on = SDL_EVENT_KEY_DOWN;
|
||||
control.event_off = SDL_EVENT_KEY_UP;
|
||||
control.handler_on = &akgl_Actor_cmhf_left_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_left_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
// Move right
|
||||
control.key = SDLK_RIGHT;
|
||||
control.event_on = SDL_EVENT_KEY_DOWN;
|
||||
control.event_off = SDL_EVENT_KEY_UP;
|
||||
control.handler_on = &akgl_Actor_cmhf_right_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_right_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
control.key = 0;
|
||||
// ----- GAMEPAD CONTROLS
|
||||
// Move down
|
||||
control.button = SDL_GAMEPAD_BUTTON_DPAD_DOWN;
|
||||
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
|
||||
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
|
||||
control.handler_on = &akgl_Actor_cmhf_down_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_down_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
// Move up
|
||||
control.button = SDL_GAMEPAD_BUTTON_DPAD_UP;
|
||||
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
|
||||
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
|
||||
control.handler_on = &akgl_Actor_cmhf_up_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_up_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
// Move left
|
||||
control.button = SDL_GAMEPAD_BUTTON_DPAD_LEFT;
|
||||
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
|
||||
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
|
||||
control.handler_on = &akgl_Actor_cmhf_left_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_left_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
// Move right
|
||||
control.button = SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
|
||||
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
|
||||
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
|
||||
control.handler_on = &akgl_Actor_cmhf_right_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_right_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available)
|
||||
{
|
||||
akgl_Keystroke keystroke;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, keycode, AKERR_NULLPOINTER, "NULL keycode destination");
|
||||
FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");
|
||||
|
||||
// An empty buffer is the ordinary case, not a failure: the caller is asking
|
||||
// whether a key is waiting, and the answer is no.
|
||||
*keycode = 0;
|
||||
*available = false;
|
||||
while ( keybuffer_take(&keystroke) == true ) {
|
||||
if ( keystroke.key == 0 ) {
|
||||
// Composed text with no key behind it. This form has no way to
|
||||
// report it, so it is dropped rather than handed back as key 0.
|
||||
continue;
|
||||
}
|
||||
*keycode = (int)keystroke.key;
|
||||
*available = true;
|
||||
break;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL keystroke destination");
|
||||
FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");
|
||||
|
||||
memset(dest, 0x00, sizeof(akgl_Keystroke));
|
||||
*available = keybuffer_take(dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
keybuffer_head = 0;
|
||||
keybuffer_count = 0;
|
||||
keybuffer_awaiting_text = false;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
584
src/draw.c
584
src/draw.c
@@ -1,14 +1,39 @@
|
||||
/**
|
||||
* @file draw.c
|
||||
* @brief Implements the draw subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
#include <sdl3game/game.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/draw.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
|
||||
/** @brief One horizontal run of pixels the flood fill has still to examine. */
|
||||
typedef struct {
|
||||
int x1; /**< Leftmost column of the run, inclusive. */
|
||||
int x2; /**< Rightmost column of the run, inclusive. */
|
||||
int y; /**< The row the run is on. */
|
||||
} FloodSpan;
|
||||
|
||||
/*
|
||||
* The flood fill's working stack. File scope and fixed size rather than a local
|
||||
* array because AKGL_DRAW_MAX_FLOOD_SPANS spans is 48 KB, which does not belong
|
||||
* on the stack of a function a game may call every frame. The consequence is
|
||||
* that akgl_draw_flood_fill is not reentrant -- it is a single-threaded
|
||||
* immediate-mode operation on a single render target, and so is everything else
|
||||
* that touches an SDL_Renderer.
|
||||
*/
|
||||
static FloodSpan floodspans[AKGL_DRAW_MAX_FLOOD_SPANS];
|
||||
|
||||
/* Draw a Gimpish background pattern to show transparency in the image */
|
||||
void GAME_draw_background(int w, int h)
|
||||
void akgl_draw_background(int w, int h)
|
||||
{
|
||||
SDL_Color col[2] = {
|
||||
{ 0x66, 0x66, 0x66, 0xff },
|
||||
{ 0x99, 0x99, 0x99, 0xff },
|
||||
{ 0x66, 0x66, 0x66, 0xff },
|
||||
{ 0x99, 0x99, 0x99, 0xff },
|
||||
};
|
||||
int i, x, y;
|
||||
SDL_FRect rect;
|
||||
@@ -17,14 +42,549 @@ void GAME_draw_background(int w, int h)
|
||||
rect.w = (float)dx;
|
||||
rect.h = (float)dy;
|
||||
for (y = 0; y < h; y += dy) {
|
||||
for (x = 0; x < w; x += dx) {
|
||||
/* use an 8x8 checkerboard pattern */
|
||||
i = (((x ^ y) >> 3) & 1);
|
||||
SDL_SetRenderDrawColor(renderer, col[i].r, col[i].g, col[i].b, col[i].a);
|
||||
for (x = 0; x < w; x += dx) {
|
||||
/* use an 8x8 checkerboard pattern */
|
||||
i = (((x ^ y) >> 3) & 1);
|
||||
SDL_SetRenderDrawColor(renderer->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a);
|
||||
|
||||
rect.x = (float)x;
|
||||
rect.y = (float)y;
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
}
|
||||
rect.x = (float)x;
|
||||
rect.y = (float)y;
|
||||
SDL_RenderFillRect(renderer->sdl_renderer, &rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Remember the renderer's draw color and replace it with @p color.
|
||||
*
|
||||
* @p previous is written before anything that can fail, so a caller may restore
|
||||
* it unconditionally from a CLEANUP block.
|
||||
*
|
||||
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`;
|
||||
* every caller has already checked both.
|
||||
* @param color The colour to install.
|
||||
* @param previous Receives the colour that was in place. Assumed non-`NULL`.
|
||||
* Pre-filled with opaque black, so it is safe to restore from
|
||||
* even if the query below fails.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_SDL If the current draw colour cannot be read or the new one
|
||||
* cannot be set. The message carries `SDL_GetError()`.
|
||||
*/
|
||||
static akerr_ErrorContext *push_draw_color(akgl_RenderBackend *self, SDL_Color color, SDL_Color *previous)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
previous->r = 0x00;
|
||||
previous->g = 0x00;
|
||||
previous->b = 0x00;
|
||||
previous->a = SDL_ALPHA_OPAQUE;
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_GetRenderDrawColor(self->sdl_renderer, &previous->r, &previous->g, &previous->b, &previous->a),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetRenderDrawColor(self->sdl_renderer, color.r, color.g, color.b, color.a),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Put back the draw color push_draw_color() recorded.
|
||||
*
|
||||
* Called from `CLEANUP` blocks under `IGNORE()`, so its error is logged rather
|
||||
* than propagated -- failing to restore a colour must not mask the failure the
|
||||
* cleanup is unwinding from.
|
||||
*
|
||||
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`.
|
||||
* @param previous The colour push_draw_color() recorded. Assumed non-`NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be set.
|
||||
*/
|
||||
static akerr_ErrorContext *pop_draw_color(akgl_RenderBackend *self, SDL_Color *previous)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetRenderDrawColor(self->sdl_renderer, previous->r, previous->g, previous->b, previous->a),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fill the four-connected region of @p oldpixel around a seed pixel.
|
||||
*
|
||||
* A scanline fill: each entry on the stack is a run of pixels on one row that
|
||||
* still has to be examined. Finding a matching pixel expands it to the whole
|
||||
* run it belongs to, fills that run, and pushes the rows above and below.
|
||||
* Filled pixels no longer match @p oldpixel, which is what terminates it.
|
||||
*
|
||||
* @p surface must be SDL_PIXELFORMAT_RGBA32; the fill compares and writes whole
|
||||
* 32-bit words rather than going through SDL_ReadSurfacePixel per pixel.
|
||||
*
|
||||
* @p dirty is set to the bounding box of everything written, so the caller can
|
||||
* put back only the pixels that changed.
|
||||
*
|
||||
* Running out of stack leaves the region partially filled and reports
|
||||
* AKERR_OUTOFBOUNDS. There is no way to unwind a partial fill short of keeping
|
||||
* a copy of the whole surface, and the caller asked for a bounded operation.
|
||||
*
|
||||
* @param surface The pixels to fill, in SDL_PIXELFORMAT_RGBA32. Assumed
|
||||
* non-`NULL` and locked-or-lockless; the caller converts.
|
||||
* @param x Seed column. Assumed inside the surface -- akgl_draw_flood_fill
|
||||
* has already range-checked it.
|
||||
* @param y Seed row, likewise.
|
||||
* @param oldpixel The packed pixel value the region is made of. Everything else
|
||||
* is a boundary.
|
||||
* @param newpixel The packed pixel value to write. Must differ from @p oldpixel:
|
||||
* if they are equal the fill never terminates making progress,
|
||||
* which is why the caller checks that case first.
|
||||
* @param dirty Receives the bounding box of everything written, so the caller
|
||||
* can put back only the pixels that changed. Assumed non-`NULL`.
|
||||
* Left describing an empty box if nothing matched.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If the region needs more than
|
||||
* #AKGL_DRAW_MAX_FLOOD_SPANS pending spans. @p dirty is then not written
|
||||
* and the surface is partially filled.
|
||||
*/
|
||||
static akerr_ErrorContext *flood_region(SDL_Surface *surface, int x, int y, uint32_t oldpixel, uint32_t newpixel, SDL_Rect *dirty)
|
||||
{
|
||||
uint32_t *pixels = (uint32_t *)surface->pixels;
|
||||
int pitch = surface->pitch / (int)sizeof(uint32_t);
|
||||
int count = 0;
|
||||
int col = 0;
|
||||
int left = 0;
|
||||
int right = 0;
|
||||
int i = 0;
|
||||
int minx = surface->w;
|
||||
int miny = surface->h;
|
||||
int maxx = -1;
|
||||
int maxy = -1;
|
||||
FloodSpan span;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
floodspans[0].x1 = x;
|
||||
floodspans[0].x2 = x;
|
||||
floodspans[0].y = y;
|
||||
count = 1;
|
||||
|
||||
while ( count > 0 ) {
|
||||
count -= 1;
|
||||
span = floodspans[count];
|
||||
col = span.x1;
|
||||
while ( col <= span.x2 ) {
|
||||
if ( pixels[(span.y * pitch) + col] != oldpixel ) {
|
||||
col += 1;
|
||||
continue;
|
||||
}
|
||||
left = col;
|
||||
while ( left > 0 && pixels[(span.y * pitch) + (left - 1)] == oldpixel ) {
|
||||
left -= 1;
|
||||
}
|
||||
right = col;
|
||||
while ( right < (surface->w - 1) && pixels[(span.y * pitch) + (right + 1)] == oldpixel ) {
|
||||
right += 1;
|
||||
}
|
||||
for ( i = left; i <= right; i++ ) {
|
||||
pixels[(span.y * pitch) + i] = newpixel;
|
||||
}
|
||||
if ( left < minx ) {
|
||||
minx = left;
|
||||
}
|
||||
if ( right > maxx ) {
|
||||
maxx = right;
|
||||
}
|
||||
if ( span.y < miny ) {
|
||||
miny = span.y;
|
||||
}
|
||||
if ( span.y > maxy ) {
|
||||
maxy = span.y;
|
||||
}
|
||||
// Two pushes per run, so the check is for room for both.
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((count + 2) > AKGL_DRAW_MAX_FLOOD_SPANS),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Region needs more than %d pending spans; it is partially filled",
|
||||
AKGL_DRAW_MAX_FLOOD_SPANS);
|
||||
if ( span.y > 0 ) {
|
||||
floodspans[count].x1 = left;
|
||||
floodspans[count].x2 = right;
|
||||
floodspans[count].y = span.y - 1;
|
||||
count += 1;
|
||||
}
|
||||
if ( span.y < (surface->h - 1) ) {
|
||||
floodspans[count].x1 = left;
|
||||
floodspans[count].x2 = right;
|
||||
floodspans[count].y = span.y + 1;
|
||||
count += 1;
|
||||
}
|
||||
col = right + 1;
|
||||
}
|
||||
}
|
||||
|
||||
dirty->x = minx;
|
||||
dirty->y = miny;
|
||||
dirty->w = (maxx - minx) + 1;
|
||||
dirty->h = (maxy - miny) + 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color)
|
||||
{
|
||||
SDL_Color previous;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderPoint(self->sdl_renderer, x, y),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
IGNORE(pop_draw_color(self, &previous));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color)
|
||||
{
|
||||
SDL_Color previous;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderLine(self->sdl_renderer, x1, y1, x2, y2),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
IGNORE(pop_draw_color(self, &previous));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
|
||||
{
|
||||
SDL_Color previous;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderRect(self->sdl_renderer, rect),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
IGNORE(pop_draw_color(self, &previous));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
|
||||
{
|
||||
SDL_Color previous;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderFillRect(self->sdl_renderer, rect),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
IGNORE(pop_draw_color(self, &previous));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color)
|
||||
{
|
||||
SDL_Color previous;
|
||||
SDL_FPoint octants[8];
|
||||
int centerx = 0;
|
||||
int centery = 0;
|
||||
int r = 0;
|
||||
int offsetx = 0;
|
||||
int offsety = 0;
|
||||
int decision = 0;
|
||||
bool plotted = true;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_NONZERO_RETURN(errctx, (radius < 0), AKERR_OUTOFBOUNDS, "Negative radius %f", radius);
|
||||
|
||||
centerx = (int)SDL_lroundf(x);
|
||||
centery = (int)SDL_lroundf(y);
|
||||
r = (int)SDL_lroundf(radius);
|
||||
offsety = r;
|
||||
// The midpoint decision variable, started so the first step chooses between
|
||||
// (0, r) and (1, r-1) correctly.
|
||||
decision = 1 - r;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||
while ( offsety >= offsetx ) {
|
||||
// Eight-way symmetry: one computed point in the second octant gives
|
||||
// the seven others by reflection.
|
||||
octants[0].x = (float)(centerx + offsetx);
|
||||
octants[0].y = (float)(centery + offsety);
|
||||
octants[1].x = (float)(centerx - offsetx);
|
||||
octants[1].y = (float)(centery + offsety);
|
||||
octants[2].x = (float)(centerx + offsetx);
|
||||
octants[2].y = (float)(centery - offsety);
|
||||
octants[3].x = (float)(centerx - offsetx);
|
||||
octants[3].y = (float)(centery - offsety);
|
||||
octants[4].x = (float)(centerx + offsety);
|
||||
octants[4].y = (float)(centery + offsetx);
|
||||
octants[5].x = (float)(centerx - offsety);
|
||||
octants[5].y = (float)(centery + offsetx);
|
||||
octants[6].x = (float)(centerx + offsety);
|
||||
octants[6].y = (float)(centery - offsetx);
|
||||
octants[7].x = (float)(centerx - offsety);
|
||||
octants[7].y = (float)(centery - offsetx);
|
||||
// A CATCH here would break this loop rather than leave the function,
|
||||
// so failure is recorded and reported once the loop is done.
|
||||
if ( !SDL_RenderPoints(self->sdl_renderer, octants, 8) ) {
|
||||
plotted = false;
|
||||
}
|
||||
offsetx += 1;
|
||||
if ( decision < 0 ) {
|
||||
decision += (2 * offsetx) + 1;
|
||||
} else {
|
||||
offsety -= 1;
|
||||
decision += 2 * (offsetx - offsety) + 1;
|
||||
}
|
||||
}
|
||||
FAIL_ZERO_BREAK(errctx, plotted, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
} CLEANUP {
|
||||
IGNORE(pop_draw_color(self, &previous));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color)
|
||||
{
|
||||
SDL_Surface *target = NULL;
|
||||
SDL_Surface *rgba = NULL;
|
||||
SDL_Texture *patch = NULL;
|
||||
// Only written by a successful flood_region(), and only read after one, but
|
||||
// the paths in between are far enough apart that the compiler cannot see it.
|
||||
SDL_Rect dirty = { 0, 0, 0, 0 };
|
||||
SDL_FRect src;
|
||||
SDL_FRect dest;
|
||||
uint32_t *pixels = NULL;
|
||||
uint32_t oldpixel = 0;
|
||||
uint32_t newpixel = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
((x < 0) || (y < 0) || (x >= width) || (y >= height)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Seed pixel %d,%d is outside the %dx%d render target",
|
||||
x, y, width, height);
|
||||
|
||||
target = SDL_RenderReadPixels(self->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, target, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
// The fill works on 32-bit words, so the layout has to be known rather
|
||||
// than whatever the render target happens to use.
|
||||
rgba = SDL_ConvertSurface(target, SDL_PIXELFORMAT_RGBA32);
|
||||
FAIL_ZERO_BREAK(errctx, rgba, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
|
||||
pixels = (uint32_t *)rgba->pixels;
|
||||
oldpixel = pixels[(y * (rgba->pitch / (int)sizeof(uint32_t))) + x];
|
||||
newpixel = SDL_MapSurfaceRGBA(rgba, color.r, color.g, color.b, color.a);
|
||||
if ( oldpixel == newpixel ) {
|
||||
// Already the requested color. Walking it would compare filled
|
||||
// pixels against themselves and find nothing, so say so up front.
|
||||
SUCCEED_BREAK(errctx);
|
||||
}
|
||||
CATCH(errctx, flood_region(rgba, x, y, oldpixel, newpixel, &dirty));
|
||||
|
||||
patch = SDL_CreateTextureFromSurface(self->sdl_renderer, rgba);
|
||||
FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
// Replace rather than blend: this is a framebuffer operation, and the
|
||||
// pixels being put back are the ones that were just read out of it.
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
|
||||
// Only the bounding box of what changed goes back to the target.
|
||||
src.x = (float)dirty.x;
|
||||
src.y = (float)dirty.y;
|
||||
src.w = (float)dirty.w;
|
||||
src.h = (float)dirty.h;
|
||||
dest = src;
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderTexture(self->sdl_renderer, patch, &src, &dest),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
if ( patch != NULL ) {
|
||||
SDL_DestroyTexture(patch);
|
||||
}
|
||||
if ( rgba != NULL ) {
|
||||
SDL_DestroySurface(rgba);
|
||||
}
|
||||
if ( target != NULL ) {
|
||||
SDL_DestroySurface(target);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest)
|
||||
{
|
||||
SDL_Surface *saved = NULL;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||||
ATTEMPT {
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
((src->w <= 0) || (src->h <= 0)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Region %dx%d has no area",
|
||||
src->w, src->h);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
// SDL clips a read to the target and hands back a smaller surface than
|
||||
// was asked for, which a caller pasting it back would not notice.
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
((src->x < 0) || (src->y < 0) ||
|
||||
((src->x + src->w) > width) || ((src->y + src->h) > height)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Region %d,%d %dx%d does not fit inside the %dx%d render target",
|
||||
src->x, src->y, src->w, src->h, width, height);
|
||||
|
||||
saved = SDL_RenderReadPixels(self->sdl_renderer, src);
|
||||
FAIL_ZERO_BREAK(errctx, saved, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
|
||||
if ( *dest == NULL ) {
|
||||
*dest = saved;
|
||||
// Ownership has moved to the caller; CLEANUP must not free it.
|
||||
saved = NULL;
|
||||
} else {
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
(((*dest)->w != src->w) || ((*dest)->h != src->h)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Destination surface is %dx%d, region is %dx%d",
|
||||
(*dest)->w, (*dest)->h, src->w, src->h);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_SetSurfaceBlendMode(saved, SDL_BLENDMODE_NONE),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_BlitSurface(saved, NULL, *dest, NULL),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
}
|
||||
} CLEANUP {
|
||||
if ( saved != NULL ) {
|
||||
SDL_DestroySurface(saved);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y)
|
||||
{
|
||||
SDL_Texture *patch = NULL;
|
||||
SDL_FRect dest;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
|
||||
ATTEMPT {
|
||||
patch = SDL_CreateTextureFromSurface(self->sdl_renderer, src);
|
||||
FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
// Replace what is on the target, the way GSHAPE does by default.
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
dest.x = x;
|
||||
dest.y = y;
|
||||
dest.w = (float)src->w;
|
||||
dest.h = (float)src->h;
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderTexture(self->sdl_renderer, patch, NULL, &dest),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
if ( patch != NULL ) {
|
||||
SDL_DestroyTexture(patch);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
738
src/game.c
738
src/game.c
@@ -1,86 +1,704 @@
|
||||
/**
|
||||
* @file game.c
|
||||
* @brief Implements the game subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <stdio.h>
|
||||
#include <sdlerror.h>
|
||||
#include <akerror.h>
|
||||
#include <semver.h>
|
||||
|
||||
#include <sdl3game/game.h>
|
||||
#include <sdl3game/controller.h>
|
||||
#include <sdl3game/tilemap.h>
|
||||
#include <sdl3game/sprite.h>
|
||||
#include <sdl3game/heap.h>
|
||||
#include <sdl3game/registry.h>
|
||||
#include <sdl3game/staticstring.h>
|
||||
#include <sdl3game/iterator.h>
|
||||
#include <akstdlib.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/tilemap.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/iterator.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/SDL_GameControllerDB.h>
|
||||
|
||||
SDL_Window *window = NULL;
|
||||
SDL_Renderer *renderer = NULL;
|
||||
GAME_frame ball;
|
||||
GAME_frame paddle1;
|
||||
GAME_frame paddle2;
|
||||
GAME_frame table;
|
||||
tilemap gamemap;
|
||||
MIX_Audio *bgm = NULL;
|
||||
MIX_Mixer *GAME_mixer = NULL;
|
||||
MIX_Track *GAME_tracks[64];
|
||||
SDL_FRect camera;
|
||||
Game game;
|
||||
|
||||
ErrorContext ERROR_NOIGNORE *GAME_init()
|
||||
// Currently active objects
|
||||
akgl_RenderBackend *renderer;
|
||||
akgl_PhysicsBackend *physics;
|
||||
SDL_FRect *camera;
|
||||
akgl_Tilemap *gamemap;
|
||||
|
||||
// Default objects
|
||||
akgl_RenderBackend _akgl_renderer;
|
||||
akgl_PhysicsBackend _akgl_physics;
|
||||
SDL_FRect _akgl_camera;
|
||||
akgl_Tilemap _akgl_gamemap;
|
||||
|
||||
MIX_Audio *bgm = NULL;
|
||||
MIX_Mixer *akgl_mixer = NULL;
|
||||
MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
|
||||
akgl_Game game;
|
||||
|
||||
static akerr_ErrorContext *write_exact(const void *ptr, size_t size, size_t nmemb, FILE *fp)
|
||||
{
|
||||
size_t transferred;
|
||||
|
||||
return aksl_fwrite(ptr, size, nmemb, fp, &transferred);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Widest name field any of the save tables writes.
|
||||
*
|
||||
* The spritesheet table is the widest at #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH;
|
||||
* the other three are name lengths half that or less.
|
||||
*/
|
||||
#define AKGL_GAME_SAVE_MAX_NAME_FIELD AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH
|
||||
|
||||
/**
|
||||
* @brief Fail the build if a table ever declares a field wider than the staging buffer.
|
||||
*
|
||||
* A negative array size is a compile error, so widening one of these constants
|
||||
* without widening #AKGL_GAME_SAVE_MAX_NAME_FIELD cannot get past the compiler
|
||||
* and turn back into the overread this buffer exists to prevent.
|
||||
*/
|
||||
typedef char akgl_game_save_name_field_fits[
|
||||
((AKGL_ACTOR_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_SPRITE_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD)) ? 1 : -1];
|
||||
|
||||
/**
|
||||
* @brief Write a registry key as a fixed-width, zero-padded field.
|
||||
*
|
||||
* The name tables are read back without a length prefix, so every entry has to
|
||||
* occupy exactly @p width bytes. Writing @p width bytes *from the key itself*
|
||||
* is what the four iterators used to do, and the key is an SDL allocation sized
|
||||
* to the name -- 40 bytes for `benchactor0`. That read past the end of it on
|
||||
* every entry, and put whatever was next to it in the heap into the save file:
|
||||
* up to half a kilobyte of this process's memory per registered object, in a
|
||||
* file a player might send to somebody else.
|
||||
*
|
||||
* Staging through a zeroed buffer costs one `memset` per entry and makes the
|
||||
* padding deterministic, which the reader wanted anyway.
|
||||
*
|
||||
* @param name The registry key. Required, and NUL-terminated -- it comes from
|
||||
* SDL's property table, which stores C strings.
|
||||
* @param width Field width in bytes, including the padding. Required to be at
|
||||
* most #AKGL_GAME_SAVE_MAX_NAME_FIELD; the compile-time check
|
||||
* above is what keeps that true for the four call sites.
|
||||
* @param fp Open output stream. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p name or @p fp is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p width exceeds the staging buffer.
|
||||
*/
|
||||
static akerr_ErrorContext *write_name_field(const char *name, size_t width, FILE *fp)
|
||||
{
|
||||
char field[AKGL_GAME_SAVE_MAX_NAME_FIELD];
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL name");
|
||||
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
FAIL_NONZERO_RETURN(
|
||||
e,
|
||||
(width > sizeof(field)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Name field width %zu exceeds the staging buffer (%zu)",
|
||||
width,
|
||||
sizeof(field));
|
||||
|
||||
memset(&field, 0x00, sizeof(field));
|
||||
strncpy((char *)&field, name, (width - 1));
|
||||
PASS(e, write_exact((char *)&field, 1, width, fp));
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *read_exact(void *ptr, size_t size, size_t nmemb, FILE *fp)
|
||||
{
|
||||
size_t transferred;
|
||||
|
||||
return aksl_fread(ptr, size, nmemb, fp, &transferred);
|
||||
}
|
||||
|
||||
void akgl_game_lowfps(void)
|
||||
{
|
||||
SDL_Log("Low FPS! %d", game.fps);
|
||||
return;
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
|
||||
{
|
||||
int screenwidth = 0;
|
||||
int screenheight = 0;
|
||||
|
||||
int i = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, strlen((char *)&game.name), ERR_NULLPOINTER, "Must provide game name");
|
||||
FAIL_ZERO_BREAK(errctx, strlen((char *)&game.version), ERR_NULLPOINTER, "Must provide game version");
|
||||
FAIL_ZERO_BREAK(errctx, strlen((char *)&game.uri), ERR_NULLPOINTER, "Must provide game uri");
|
||||
CATCH(errctx, heap_init());
|
||||
CATCH(errctx, registry_init_actor());
|
||||
CATCH(errctx, registry_init_sprite());
|
||||
CATCH(errctx, registry_init_spritesheet());
|
||||
CATCH(errctx, registry_init_character());
|
||||
CATCH(errctx, registry_init_actor_state_strings());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true)
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
// First, before anything that can raise: everything below reports through
|
||||
// AKGL_ERR_* codes, and a code raised before its name is registered prints
|
||||
// as "Unknown Error" in the stack trace the caller is left holding.
|
||||
PASS(e, akgl_error_init());
|
||||
strncpy((char *)&game.libversion, AKGL_VERSION, 32);
|
||||
game.gameStartTime = SDL_GetTicksNS();
|
||||
game.lastIterTime = game.gameStartTime;
|
||||
game.lastFPSTime = game.gameStartTime;
|
||||
game.lowfpsfunc = &akgl_game_lowfps;
|
||||
game.statelock = SDL_CreateMutex();
|
||||
FAIL_ZERO_RETURN(e, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
PASS(e, akgl_game_state_lock());
|
||||
FAIL_ZERO_RETURN(e, strlen((char *)&game.name), AKERR_NULLPOINTER, "Must provide game name");
|
||||
FAIL_ZERO_RETURN(e, strlen((char *)&game.version), AKERR_NULLPOINTER, "Must provide game version");
|
||||
FAIL_ZERO_RETURN(e, strlen((char *)&game.uri), AKERR_NULLPOINTER, "Must provide game uri");
|
||||
PASS(e, akgl_heap_init());
|
||||
PASS(e, akgl_registry_init_actor());
|
||||
PASS(e, akgl_registry_init_sprite());
|
||||
PASS(e, akgl_registry_init_spritesheet());
|
||||
PASS(e, akgl_registry_init_character());
|
||||
PASS(e, akgl_registry_init_font());
|
||||
PASS(e, akgl_registry_init_music());
|
||||
PASS(e, akgl_registry_init_properties());
|
||||
PASS(e, akgl_registry_init_actor_state_strings());
|
||||
|
||||
SDL_SetAppMetadata(game.name, game.version, game.uri);
|
||||
|
||||
for ( i = 0 ; i < MAX_CONTROL_MAPS; i++ ) {
|
||||
memset(&GAME_ControlMaps[i], 0x00, sizeof(SDL3GControlMap));
|
||||
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
||||
memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap));
|
||||
}
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
e,
|
||||
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO),
|
||||
ERR_SDL,
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize SDL: %s",
|
||||
SDL_GetError());
|
||||
|
||||
|
||||
// Load the Game Controller DB
|
||||
|
||||
for ( i = 0; i < AKGL_SDL_GAMECONTROLLER_DB_LEN ; i++ ) {
|
||||
if ( SDL_AddGamepadMapping(SDL_GAMECONTROLLER_DB[i]) == -1 ) {
|
||||
FAIL_ZERO_RETURN(e, 0, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
}
|
||||
}
|
||||
PASS(e, akgl_controller_open_gamepads());
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_CreateWindowAndRenderer(game.uri, game.screenwidth, game.screenheight, 0, &window, &renderer),
|
||||
ERR_SDL,
|
||||
"Couldn't create window/renderer: %s",
|
||||
SDL_GetError());
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
e,
|
||||
MIX_Init(),
|
||||
ERR_SDL,
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize audio: %s",
|
||||
SDL_GetError());
|
||||
game.mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, 0);
|
||||
akgl_mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, 0);
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
game.mixer,
|
||||
ERR_SDL,
|
||||
e,
|
||||
akgl_mixer,
|
||||
AKGL_ERR_SDL,
|
||||
"Unable to create mixer device: %s",
|
||||
SDL_GetError());
|
||||
|
||||
camera.x = 0;
|
||||
camera.y = 0;
|
||||
camera.w = game.screenwidth;
|
||||
camera.h = game.screenheight;
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
e,
|
||||
TTF_Init(),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize front engine: %s",
|
||||
SDL_GetError());
|
||||
|
||||
camera = &_akgl_camera;
|
||||
renderer = &_akgl_renderer;
|
||||
physics = &_akgl_physics;
|
||||
gamemap = &_akgl_gamemap;
|
||||
|
||||
PASS(e, akgl_game_state_unlock());
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Time totaltime = 0;
|
||||
|
||||
while ( totaltime < AKGL_TIME_ONESEC_MS ) {
|
||||
if ( SDL_TryLockMutex(game.statelock) == true ) {
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
totaltime += 100;
|
||||
SDL_Delay(100);
|
||||
}
|
||||
FAIL_RETURN(e, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_UnlockMutex(game.statelock);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
void akgl_game_updateFPS()
|
||||
{
|
||||
SDL_Time curTime;
|
||||
curTime = SDL_GetTicksNS();
|
||||
if ( (curTime - game.lastFPSTime) > AKGL_TIME_ONESEC_NS ) {
|
||||
game.fps = game.framesSinceUpdate;
|
||||
game.framesSinceUpdate = 0;
|
||||
game.lastFPSTime = curTime;
|
||||
}
|
||||
if ( game.fps < 30 ) {
|
||||
game.lowfpsfunc();
|
||||
}
|
||||
game.framesSinceUpdate += 1;
|
||||
game.lastIterTime = curTime;
|
||||
}
|
||||
|
||||
/*
|
||||
* entity name -> pointer map tables
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Write one actor's name and save-time address to the name table.
|
||||
*
|
||||
* An `SDL_EnumerateProperties` callback. The name is written at a fixed
|
||||
* #AKGL_ACTOR_MAX_NAME_LENGTH bytes so the reader can walk the table without a
|
||||
* length prefix, and the pointer is written raw -- it is not a pointer the
|
||||
* loader will ever dereference, only a key it maps back to the object that has
|
||||
* the same name next run.
|
||||
*
|
||||
* @param userdata The open output stream, as a `FILE *`. Required.
|
||||
* @param props #AKGL_REGISTRY_ACTOR, supplied by SDL.
|
||||
* @param name The actor's registry key. Read for
|
||||
* #AKGL_ACTOR_MAX_NAME_LENGTH bytes regardless of its actual
|
||||
* length, so a shorter key is read past its end.
|
||||
*
|
||||
* @warning Ends in `FINISH_NORETURN`: an unhandled error here logs a stack trace
|
||||
* and then calls libakerror's unhandled-error handler, which by default
|
||||
* **exits the process**. A failed write during a save terminates the
|
||||
* game rather than returning an error.
|
||||
*/
|
||||
void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
FILE *fp = (FILE *)userdata;
|
||||
akgl_Actor *actor = NULL;
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
CATCH(e, write_name_field(name, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
|
||||
actor = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(e, write_exact(&actor, 1, sizeof(akgl_Actor *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Write one sprite's name and save-time address to the name table.
|
||||
*
|
||||
* As akgl_game_save_actorname_iterator, but the name field is
|
||||
* #AKGL_SPRITE_MAX_NAME_LENGTH wide.
|
||||
*
|
||||
* @param userdata The open output stream, as a `FILE *`. Required.
|
||||
* @param props #AKGL_REGISTRY_SPRITE, supplied by SDL.
|
||||
* @param name The sprite's registry key, written at fixed width.
|
||||
*
|
||||
* @warning Terminates the process on an unhandled error. See
|
||||
* akgl_game_save_actorname_iterator.
|
||||
*/
|
||||
void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
FILE *fp = (FILE *)userdata;
|
||||
akgl_Sprite *sprite = NULL;
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
sprite = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(e, write_name_field(name, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, write_exact(&sprite, 1, sizeof(akgl_Sprite *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Write one spritesheet's name and save-time address to the name table.
|
||||
*
|
||||
* As akgl_game_save_actorname_iterator, but the name field is
|
||||
* #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH wide -- the widest of the four, since
|
||||
* a spritesheet is keyed by its resolved path.
|
||||
*
|
||||
* @param userdata The open output stream, as a `FILE *`. Required.
|
||||
* @param props #AKGL_REGISTRY_SPRITESHEET, supplied by SDL.
|
||||
* @param name The spritesheet's registry key, written at fixed width.
|
||||
*
|
||||
* @warning Terminates the process on an unhandled error. See
|
||||
* akgl_game_save_actorname_iterator.
|
||||
* @note This is the table the loader disagrees with: it reads every table at
|
||||
* #AKGL_ACTOR_MAX_NAME_LENGTH, so a save containing any spritesheet cannot
|
||||
* be read back. TODO.md, "Known and still open" item 7.
|
||||
*/
|
||||
void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
FILE *fp = (FILE *)userdata;
|
||||
akgl_SpriteSheet *spritesheet = NULL;
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
spritesheet = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(e, write_name_field(name, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
|
||||
CATCH(e, write_exact(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Write one character's name and save-time address to the name table.
|
||||
*
|
||||
* As akgl_game_save_actorname_iterator, but the name field is
|
||||
* #AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH wide.
|
||||
*
|
||||
* @param userdata The open output stream, as a `FILE *`. Required.
|
||||
* @param props #AKGL_REGISTRY_CHARACTER, supplied by SDL.
|
||||
* @param name The character's registry key, written at fixed width.
|
||||
*
|
||||
* @warning Terminates the process on an unhandled error. See
|
||||
* akgl_game_save_actorname_iterator.
|
||||
*/
|
||||
void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
FILE *fp = (FILE *)userdata;
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Character *character = NULL;
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
character = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(e, write_name_field(name, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
|
||||
CATCH(e, write_exact(&character, 1, sizeof(akgl_Character *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Write all four name-to-address tables, each with its terminator.
|
||||
*
|
||||
* Actors, sprites, spritesheets, characters, in that order -- which is the order
|
||||
* akgl_game_load reads them back in. Each table is the registry enumerated one
|
||||
* entry at a time, followed by a zeroed name field and a zeroed pointer that the
|
||||
* reader stops on.
|
||||
*
|
||||
* @param fp The open save-game stream. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p fp is `NULL`.
|
||||
* @throws AKERR_IO On a stream error or a short write while emitting a
|
||||
* terminator.
|
||||
*
|
||||
* @note Only the terminators are written through the error-reporting path. The
|
||||
* entries themselves go through `SDL_EnumerateProperties`, whose callbacks
|
||||
* cannot report failure upward and instead terminate the process -- so a
|
||||
* write error mid-table never reaches this function's return value.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
// Each name table ends with a zeroed name field and a zeroed pointer, which
|
||||
// is what akgl_game_load_objectnamemap() looks for to stop reading. The
|
||||
// terminator has to come from a buffer at least as long as the longest name
|
||||
// field: writing N bytes from the address of a single char would emit N-1
|
||||
// bytes of whatever happened to follow it on the stack, which both leaks
|
||||
// stack contents into the save file and produces a sentinel the loader
|
||||
// cannot recognize.
|
||||
char nullbuf[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
|
||||
|
||||
memset((void *)&nullbuf, 0x00, sizeof(nullbuf));
|
||||
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
// write the actor name pointer table
|
||||
SDL_EnumerateProperties(
|
||||
AKGL_REGISTRY_ACTOR,
|
||||
&akgl_game_save_actorname_iterator,
|
||||
(void *)fp);
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Actor *), fp));
|
||||
// write the sprite name pointer table
|
||||
SDL_EnumerateProperties(
|
||||
AKGL_REGISTRY_SPRITE,
|
||||
&akgl_game_save_spritename_iterator,
|
||||
(void *)fp);
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Sprite *), fp));
|
||||
// write the spritesheet name pointer table
|
||||
SDL_EnumerateProperties(
|
||||
AKGL_REGISTRY_SPRITESHEET,
|
||||
&akgl_game_save_spritesheetname_iterator,
|
||||
(void *)fp);
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_SpriteSheet *), fp));
|
||||
// write the character name pointer table
|
||||
SDL_EnumerateProperties(
|
||||
AKGL_REGISTRY_CHARACTER,
|
||||
&akgl_game_save_charactername_iterator,
|
||||
(void *)fp);
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Character *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
|
||||
{
|
||||
FILE *fp = NULL;
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fpath, AKERR_NULLPOINTER, "NULL file path");
|
||||
CATCH(e, aksl_fopen(fpath, "wb", &fp));
|
||||
CATCH(e, write_exact(&game, 1, sizeof(akgl_Game), fp));
|
||||
CATCH(e, akgl_game_save_actors(fp));
|
||||
} CLEANUP {
|
||||
// CLEANUP must precede PROCESS: with the two transposed, the fclose
|
||||
// lands inside the PROCESS switch and only runs when an error context
|
||||
// exists and reports success, so an ordinary save never flushed or
|
||||
// closed its stream.
|
||||
if ( fp != NULL )
|
||||
fclose(fp);
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e); // SUCCEED_NORETURN if in main().
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read one name table back, building an old-address to current-object map.
|
||||
*
|
||||
* This is the mechanism that makes a save file portable across runs. Objects are
|
||||
* serialized carrying the addresses they had at save time; those addresses mean
|
||||
* nothing now, but the table says which *name* lived at each one, and the name
|
||||
* still resolves through the registry. So each entry becomes
|
||||
* `"0x7f..." -> current object`, and a deserialized pointer can be translated by
|
||||
* looking up its printed form.
|
||||
*
|
||||
* The keys are the old pointers rendered with `%p`, because SDL property sets
|
||||
* take string keys only.
|
||||
*
|
||||
* Reading stops at the terminator -- a zeroed name and a zeroed pointer.
|
||||
*
|
||||
* @param fp The open save-game stream, positioned at the start of a
|
||||
* table. Required.
|
||||
* @param map The property set to fill in. Required, and created by the
|
||||
* caller.
|
||||
* @param namelength Width of the table's name field, which must match what the
|
||||
* corresponding save iterator wrote. It also sizes a
|
||||
* variable-length array on the stack.
|
||||
* @param ptrlength Width of the table's pointer field, i.e. `sizeof` the
|
||||
* pointer type that table holds.
|
||||
* @param registry The registry to resolve names against -- #AKGL_REGISTRY_ACTOR
|
||||
* for the actor table, and so on.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_EOF If the stream ends before a terminator is found, which is
|
||||
* what a truncated save file looks like.
|
||||
* @throws AKERR_IO On a stream error.
|
||||
* @throws AKERR_NULLPOINTER If @p fp is `NULL`, by way of the read wrapper.
|
||||
*
|
||||
* @note A name in the table that is no longer in @p registry maps to `NULL`
|
||||
* rather than being reported: the entry is written with whatever
|
||||
* `SDL_GetPointerProperty` returned.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry)
|
||||
{
|
||||
void *ptr = NULL;
|
||||
char ptrstring[32];
|
||||
char objname[namelength];
|
||||
int retval = 0;
|
||||
bool done = false;
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
// The ATTEMPT block sits inside the loop on purpose. CATCH reports a
|
||||
// failure by breaking, and a break binds to the innermost enclosing switch
|
||||
// or loop, so a CATCH written directly inside `while` would leave the loop
|
||||
// and then fall through to SUCCEED_RETURN -- reporting a truncated or
|
||||
// corrupt name table as a successful load.
|
||||
while ( done == false ) {
|
||||
ATTEMPT {
|
||||
CATCH(e, read_exact((void *)&objname, 1, namelength, fp));
|
||||
CATCH(e, read_exact((void *)&ptr, 1, ptrlength, fp));
|
||||
// End of the map
|
||||
if ( ptr == 0x00 && objname[0] == 0x00 ) {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
// The map allows us to say "Object X has a reference to object Y at
|
||||
// address Z. The object they had at address Z was named A. Our current
|
||||
// instance of object named A is at address B. So we map address Z to
|
||||
// address B, so that we can reconnect function pointers on objects loaded
|
||||
// from the save game state."
|
||||
|
||||
// SDL_Properties objects can only use string keys, so we can't use the
|
||||
// old pointer as a key without first converting it to a string.
|
||||
CATCH(e, aksl_memset((void *)&ptrstring, 0x00, 32));
|
||||
snprintf((char *)&ptrstring, 32, "%p", ptr);
|
||||
SDL_SetPointerProperty(
|
||||
map,
|
||||
ptrstring,
|
||||
SDL_GetPointerProperty(registry, objname, NULL));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
};
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Refuse a save file unless its version matches the running one exactly.
|
||||
*
|
||||
* Both strings are parsed as semver and compared with `"="` -- exact equality,
|
||||
* not compatibility. That is deliberate and strict: a save file is a raw memory
|
||||
* image of `akgl_Game` and the object tables, so any change to a struct layout
|
||||
* invalidates it, and semver has no way to say "the layout did not move".
|
||||
*
|
||||
* @param versiontype What is being compared -- `"library"` or `"game"`. Used
|
||||
* only to build the message, but required, since a bare
|
||||
* "incompatible version" error would not say which.
|
||||
* @param newversion The version read out of the save file. Required.
|
||||
* @param curversion The version of the running program or library. Required.
|
||||
* @return `NULL` when the two match, otherwise an error context owned by the
|
||||
* caller.
|
||||
* @throws AKERR_NULLPOINTER If any of the three arguments is `NULL`.
|
||||
* @throws AKERR_VALUE If either string is not valid semver. The message says
|
||||
* which side it came from.
|
||||
* @throws AKERR_API If both parse but are not equal. The message quotes both.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion)
|
||||
{
|
||||
semver_t current_version = {};
|
||||
semver_t compare_version = {};
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, versiontype, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, curversion, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, newversion, AKERR_NULLPOINTER, "NULL argument");
|
||||
|
||||
ATTEMPT {
|
||||
// Check save game library version
|
||||
FAIL_NONZERO_BREAK(
|
||||
e,
|
||||
semver_parse((const char *)curversion, ¤t_version),
|
||||
AKERR_VALUE,
|
||||
"Invalid semantic %s version in current game: %s",
|
||||
versiontype,
|
||||
(char *)curversion);
|
||||
FAIL_NONZERO_BREAK(
|
||||
e,
|
||||
semver_parse((const char *)newversion, &compare_version),
|
||||
AKERR_VALUE,
|
||||
"Invalid semantic %s version in save game: %s",
|
||||
versiontype,
|
||||
(char *)&newversion);
|
||||
FAIL_ZERO_BREAK(
|
||||
e,
|
||||
semver_satisfies(compare_version, current_version, "="),
|
||||
AKERR_API,
|
||||
"Incompatible save game %s version (%s != %s)",
|
||||
versiontype,
|
||||
curversion,
|
||||
(char *)&newversion);
|
||||
} CLEANUP {
|
||||
semver_free(¤t_version);
|
||||
semver_free(&compare_version);
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
|
||||
{
|
||||
akgl_Game savegame;
|
||||
SDL_PropertiesID actormap;
|
||||
SDL_PropertiesID spritemap;
|
||||
SDL_PropertiesID spritesheetmap;
|
||||
SDL_PropertiesID charactermap;
|
||||
FILE *fp = NULL;
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, fpath, AKERR_NULLPOINTER, "NULL file path");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, aksl_fopen(fpath, "rb", &fp));
|
||||
CATCH(e, read_exact((void *)&savegame, 1, sizeof(akgl_Game), fp));
|
||||
CATCH(e, akgl_game_load_versioncmp("library", (char *)&savegame.libversion, (char *)AKGL_VERSION));
|
||||
CATCH(e, akgl_game_load_versioncmp("game", (char *)&savegame.version, (char *)&game.version));
|
||||
FAIL_NONZERO_RETURN(
|
||||
e,
|
||||
strncmp((char *)&savegame.name, (char *)&game.name, 256),
|
||||
AKERR_API,
|
||||
"Savegame is not compatible with this game");
|
||||
FAIL_NONZERO_RETURN(
|
||||
e,
|
||||
strncmp((char *)&savegame.uri, (char *)&game.uri, 256),
|
||||
AKERR_API,
|
||||
"Savegame is not compatible with this game");
|
||||
|
||||
memcpy((void *)&game, (void *)&savegame, sizeof(akgl_Game));
|
||||
// Load actor name map
|
||||
actormap = SDL_CreateProperties();
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
|
||||
// Load sprite name map
|
||||
spritemap = SDL_CreateProperties();
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, spritemap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
|
||||
// Load spritesheet name map
|
||||
spritesheetmap = SDL_CreateProperties();
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, spritesheetmap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
|
||||
// Load character name map
|
||||
charactermap = SDL_CreateProperties();
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, charactermap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER));
|
||||
// Now that we have all of our pointer maps built, we can load the actual binary objects and reset their pointers
|
||||
} CLEANUP {
|
||||
if ( fp != NULL ) {
|
||||
fclose(fp);
|
||||
}
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Iterator defflags = {
|
||||
.flags = (AKGL_ITERATOR_OP_LAYERMASK | AKGL_ITERATOR_OP_LAYERMASK),
|
||||
.layerid = 0
|
||||
};
|
||||
SDL_Time curTime = SDL_GetTicksNS();
|
||||
akgl_Actor *actor = NULL;
|
||||
|
||||
if ( opflags == NULL ) {
|
||||
opflags = &defflags;
|
||||
}
|
||||
|
||||
PASS(e, akgl_game_state_lock());
|
||||
|
||||
akgl_game_updateFPS();
|
||||
|
||||
for ( int i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
|
||||
if ( opflags == &defflags ) {
|
||||
opflags->layerid = i;
|
||||
}
|
||||
for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
|
||||
actor = &HEAP_ACTOR[j];
|
||||
if ( actor->refcount == 0 ) {
|
||||
continue;
|
||||
}
|
||||
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE) ) {
|
||||
PASS(e, akgl_tilemap_scale_actor(gamemap, actor));
|
||||
} else {
|
||||
actor->scale = 1.0;
|
||||
}
|
||||
PASS(e, actor->updatefunc(actor));
|
||||
}
|
||||
}
|
||||
PASS(e, physics->simulate(physics, NULL));
|
||||
PASS(e, renderer->draw_world(renderer, NULL));
|
||||
PASS(e, akgl_game_state_unlock());
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
148
src/heap.c
148
src/heap.c
@@ -1,192 +1,198 @@
|
||||
/**
|
||||
* @file heap.c
|
||||
* @brief Implements the heap subsystem.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <sdlerror.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <sdl3game/game.h>
|
||||
#include <sdl3game/sprite.h>
|
||||
#include <sdl3game/heap.h>
|
||||
#include <sdl3game/registry.h>
|
||||
#include <sdl3game/staticstring.h>
|
||||
#include <sdl3game/iterator.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/iterator.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
actor HEAP_ACTOR[MAX_HEAP_ACTOR];
|
||||
sprite HEAP_SPRITE[MAX_HEAP_SPRITE];
|
||||
spritesheet HEAP_SPRITESHEET[MAX_HEAP_SPRITESHEET];
|
||||
character HEAP_CHARACTER[MAX_HEAP_CHARACTER];
|
||||
string HEAP_STRING[MAX_HEAP_STRING];
|
||||
akgl_Actor HEAP_ACTOR[AKGL_MAX_HEAP_ACTOR];
|
||||
akgl_Sprite HEAP_SPRITE[AKGL_MAX_HEAP_SPRITE];
|
||||
akgl_SpriteSheet HEAP_SPRITESHEET[AKGL_MAX_HEAP_SPRITESHEET];
|
||||
akgl_Character HEAP_CHARACTER[AKGL_MAX_HEAP_CHARACTER];
|
||||
akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING];
|
||||
|
||||
ErrorContext *heap_init()
|
||||
akerr_ErrorContext *akgl_heap_init()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int i = 0;
|
||||
for ( i = 0; i < MAX_HEAP_ACTOR; i++) {
|
||||
memset(&HEAP_ACTOR[i], 0x00, sizeof(actor));
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) {
|
||||
memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite));
|
||||
}
|
||||
for ( i = 0; i < MAX_HEAP_SPRITE; i++) {
|
||||
memset(&HEAP_SPRITE[i], 0x00, sizeof(sprite));
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++) {
|
||||
memset(&HEAP_SPRITESHEET[i], 0x00, sizeof(akgl_SpriteSheet));
|
||||
}
|
||||
for ( i = 0; i < MAX_HEAP_SPRITESHEET; i++) {
|
||||
memset(&HEAP_SPRITESHEET[i], 0x00, sizeof(spritesheet));
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++) {
|
||||
memset(&HEAP_CHARACTER[i], 0x00, sizeof(akgl_Character));
|
||||
}
|
||||
for ( i = 0; i < MAX_HEAP_CHARACTER; i++) {
|
||||
memset(&HEAP_CHARACTER[i], 0x00, sizeof(character));
|
||||
}
|
||||
for ( i = 0; i < MAX_HEAP_STRING; i++) {
|
||||
memset(&HEAP_STRING[i], 0x00, sizeof(string));
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++) {
|
||||
memset(&HEAP_STRING[i], 0x00, sizeof(akgl_String));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *heap_next_actor(actor **dest)
|
||||
akerr_ErrorContext *akgl_heap_init_actor(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++) {
|
||||
memset(&HEAP_ACTOR[i], 0x00, sizeof(akgl_Actor));
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_heap_next_actor(akgl_Actor **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
for (int i = 0; i < MAX_HEAP_ACTOR; i++ ) {
|
||||
for (int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||
if ( HEAP_ACTOR[i].refcount != 0 ) {
|
||||
continue;
|
||||
}
|
||||
*dest = &HEAP_ACTOR[i];
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, ERR_HEAP, "Unable to find unused actor on the heap");
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused actor on the heap");
|
||||
}
|
||||
|
||||
ErrorContext *heap_next_sprite(sprite **dest)
|
||||
akerr_ErrorContext *akgl_heap_next_sprite(akgl_Sprite **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
for (int i = 0; i < MAX_HEAP_SPRITE; i++ ) {
|
||||
for (int i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
|
||||
if ( HEAP_SPRITE[i].refcount != 0 ) {
|
||||
continue;
|
||||
}
|
||||
*dest = &HEAP_SPRITE[i];
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, ERR_HEAP, "Unable to find unused sprite on the heap");
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused sprite on the heap");
|
||||
}
|
||||
|
||||
ErrorContext *heap_next_spritesheet(spritesheet **dest)
|
||||
akerr_ErrorContext *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
for (int i = 0; i < MAX_HEAP_SPRITESHEET; i++ ) {
|
||||
for (int i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
|
||||
if ( HEAP_SPRITESHEET[i].refcount != 0 ) {
|
||||
continue;
|
||||
}
|
||||
*dest = &HEAP_SPRITESHEET[i];
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, ERR_HEAP, "Unable to find unused spritesheet on the heap");
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused spritesheet on the heap");
|
||||
}
|
||||
|
||||
ErrorContext *heap_next_character(character **dest)
|
||||
akerr_ErrorContext *akgl_heap_next_character(akgl_Character **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
for (int i = 0; i < MAX_HEAP_CHARACTER; i++ ) {
|
||||
for (int i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
|
||||
if ( HEAP_CHARACTER[i].refcount != 0 ) {
|
||||
continue;
|
||||
}
|
||||
*dest = &HEAP_CHARACTER[i];
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, ERR_HEAP, "Unable to find unused character on the heap");
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused character on the heap");
|
||||
}
|
||||
|
||||
ErrorContext *heap_next_string(string **dest)
|
||||
akerr_ErrorContext *akgl_heap_next_string(akgl_String **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
for (int i = 0; i < MAX_HEAP_STRING; i++ ) {
|
||||
for (int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
|
||||
if ( HEAP_STRING[i].refcount != 0 ) {
|
||||
continue;
|
||||
}
|
||||
*dest = &HEAP_STRING[i];
|
||||
HEAP_STRING[i].refcount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, ERR_HEAP, "Unable to find unused string on the heap");
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused string on the heap");
|
||||
}
|
||||
|
||||
ErrorContext *heap_release_actor(actor *ptr)
|
||||
akerr_ErrorContext *akgl_heap_release_actor(akgl_Actor *ptr)
|
||||
{
|
||||
int i = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, ptr, ERR_NULLPOINTER, "NULL actor reference");
|
||||
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "NULL actor reference");
|
||||
if ( ptr->refcount > 0 ) {
|
||||
ptr->refcount -= 1;
|
||||
}
|
||||
if ( ptr->refcount == 0 ) {
|
||||
for ( i = 0; i < ACTOR_MAX_CHILDREN; i++ ) {
|
||||
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN; i++ ) {
|
||||
if ( ptr->children[i] != NULL ) {
|
||||
CATCH_AND_RETURN(errctx, heap_release_actor(ptr->children[i]));
|
||||
PASS(errctx, akgl_heap_release_actor(ptr->children[i]));
|
||||
}
|
||||
}
|
||||
if ( ptr->basechar != NULL ) {
|
||||
CATCH_AND_RETURN(errctx, heap_release_character(ptr->basechar));
|
||||
}
|
||||
memset(ptr, 0x00, sizeof(actor));
|
||||
SDL_ClearProperty(REGISTRY_ACTOR, (char *)&ptr->name);
|
||||
SDL_ClearProperty(AKGL_REGISTRY_ACTOR, (char *)&ptr->name);
|
||||
memset(ptr, 0x00, sizeof(akgl_Actor));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *heap_release_character(character *basechar)
|
||||
akerr_ErrorContext *akgl_heap_release_character(akgl_Character *basechar)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
iterator opflags;
|
||||
FAIL_ZERO_RETURN(errctx, basechar, ERR_NULLPOINTER, "NULL character reference");
|
||||
BITMASK_CLEAR(opflags.flags);
|
||||
BITMASK_ADD(opflags.flags, ITERATOR_OP_RELEASE);
|
||||
|
||||
akgl_Iterator opflags;
|
||||
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
|
||||
AKGL_BITMASK_CLEAR(opflags.flags);
|
||||
|
||||
if ( basechar->refcount > 0 ) {
|
||||
basechar->refcount -= 1;
|
||||
}
|
||||
if ( basechar->refcount == 0 ) {
|
||||
SDL_EnumerateProperties(basechar->state_sprites, &character_state_sprites_iterate, (void *)&opflags);
|
||||
SDL_ClearProperty(REGISTRY_CHARACTER, (char *)&basechar->name);
|
||||
SDL_ClearProperty(AKGL_REGISTRY_CHARACTER, (char *)&basechar->name);
|
||||
memset(basechar, 0x00, sizeof(akgl_Character));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *heap_release_sprite(sprite *ptr)
|
||||
akerr_ErrorContext *akgl_heap_release_sprite(akgl_Sprite *ptr)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, ptr, ERR_NULLPOINTER, "Received NULL sprite reference");
|
||||
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "Received NULL sprite reference");
|
||||
if ( ptr->refcount > 0 ) {
|
||||
ptr->refcount -= 1;
|
||||
}
|
||||
if ( ptr->refcount == 0 ) {
|
||||
ATTEMPT {
|
||||
CATCH(errctx, heap_release_spritesheet(ptr->sheet));
|
||||
} CLEANUP {
|
||||
SDL_ClearProperty(REGISTRY_SPRITE, (char *)&ptr->name);
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SDL_ClearProperty(AKGL_REGISTRY_SPRITE, (char *)&ptr->name);
|
||||
memset(ptr, 0x00, sizeof(akgl_Sprite));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *heap_release_spritesheet(spritesheet *ptr)
|
||||
akerr_ErrorContext *akgl_heap_release_spritesheet(akgl_SpriteSheet *ptr)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, ptr, ERR_NULLPOINTER, "Received NULL spritesheet reference");
|
||||
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "Received NULL spritesheet reference");
|
||||
if ( ptr->refcount > 0 ) {
|
||||
ptr->refcount -= 1;
|
||||
}
|
||||
if ( ptr->refcount == 0 ) {
|
||||
// TODO : If we go threaded, make sure this is only happening on the main thread
|
||||
SDL_ClearProperty(REGISTRY_SPRITESHEET, (char *)&ptr->name);
|
||||
SDL_ClearProperty(AKGL_REGISTRY_SPRITESHEET, (char *)&ptr->name);
|
||||
if ( ptr-> texture != NULL )
|
||||
SDL_DestroyTexture(ptr->texture);
|
||||
ptr->texture = NULL;
|
||||
memset(ptr, 0x00, sizeof(akgl_SpriteSheet));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *heap_release_string(string *ptr)
|
||||
akerr_ErrorContext *akgl_heap_release_string(akgl_String *ptr)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, ptr, ERR_NULLPOINTER, "Received NULL string reference");
|
||||
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "Received NULL string reference");
|
||||
if ( ptr->refcount > 0 ) {
|
||||
ptr->refcount -= 1;
|
||||
}
|
||||
if ( ptr->refcount == 0 ) {
|
||||
memset(&ptr->data, 0x00, MAX_STRING_LENGTH);
|
||||
memset(&ptr->data, 0x00, AKGL_MAX_STRING_LENGTH);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,131 +1,170 @@
|
||||
/**
|
||||
* @file json_helpers.c
|
||||
* @brief Implements the json helpers subsystem.
|
||||
*/
|
||||
|
||||
#include <jansson.h>
|
||||
#include <string.h>
|
||||
#include <sdlerror.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <sdl3game/json_helpers.h>
|
||||
#include <sdl3game/game.h>
|
||||
#include <sdl3game/heap.h>
|
||||
#include <sdl3game/staticstring.h>
|
||||
#include <sdl3game/registry.h>
|
||||
#include <akgl/json_helpers.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/registry.h>
|
||||
|
||||
ErrorContext *get_json_object_value(json_t *obj, char *key, json_t **dest)
|
||||
akerr_ErrorContext *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), ERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *get_json_boolean_value(json_t *obj, char *key, bool *dest)
|
||||
akerr_ErrorContext *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), ERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_boolean_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_boolean_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *get_json_integer_value(json_t *obj, char *key, int *dest)
|
||||
akerr_ErrorContext *akgl_get_json_integer_value(json_t *obj, char *key, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), ERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_integer_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_integer_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *get_json_number_value(json_t *obj, char *key, float *dest)
|
||||
akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), ERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_number_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_number_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *get_json_string_value(json_t *obj, char *key, string **dest)
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest)
|
||||
{
|
||||
json_t *value = NULL;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, key, ERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "NULL pointer reference");
|
||||
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);
|
||||
}
|
||||
|
||||
value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), ERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
ATTEMPT {
|
||||
if ( *dest == NULL ) {
|
||||
CATCH(errctx, heap_next_string(dest));
|
||||
CATCH(errctx, string_initialize(*dest, NULL));
|
||||
akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest)
|
||||
{
|
||||
json_t *value = NULL;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
|
||||
value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
ATTEMPT {
|
||||
if ( *dest == NULL ) {
|
||||
CATCH(errctx, akgl_heap_next_string(dest));
|
||||
CATCH(errctx, akgl_string_initialize(*dest, NULL));
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, false);
|
||||
|
||||
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_array_index_integer(json_t *array, int index, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
*dest = json_integer_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
ATTEMPT {
|
||||
if ( *dest == NULL ) {
|
||||
CATCH(errctx, akgl_heap_next_string(dest));
|
||||
CATCH(errctx, akgl_string_initialize(*dest, NULL));
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, false);
|
||||
|
||||
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *err, void *defval, void *dest, uint32_t defsize)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
if ( err == NULL ) {
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, false);
|
||||
|
||||
strncpy((char *)&(*dest)->data, json_string_value(value), MAX_STRING_LENGTH);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
int docopy = 0;
|
||||
|
||||
ErrorContext *get_json_array_value(json_t *obj, char *key, json_t **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_array(value)), ERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_ZERO_RETURN(e, err, AKERR_NULLPOINTER, "err");
|
||||
FAIL_ZERO_RETURN(e, defval, AKERR_NULLPOINTER, "defval");
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
|
||||
|
||||
ErrorContext *get_json_array_index_object(json_t *array, int index, json_t **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, ERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, ERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), ERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
ATTEMPT {
|
||||
} CLEANUP {
|
||||
} PROCESS(err) {
|
||||
} HANDLE_GROUP(err, AKERR_KEY) {
|
||||
} HANDLE_GROUP(err, AKERR_INDEX) {
|
||||
memcpy(dest, defval, defsize);
|
||||
} FINISH(err, true);
|
||||
|
||||
ErrorContext *get_json_array_index_integer(json_t *array, int index, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, ERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, ERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), ERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
*dest = json_integer_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *get_json_array_index_string(json_t *array, int index, string **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, ERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "NULL destination pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, ERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), ERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
ATTEMPT {
|
||||
if ( *dest == NULL ) {
|
||||
CATCH(errctx, heap_next_string(dest));
|
||||
CATCH(errctx, string_initialize(*dest, NULL));
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, false);
|
||||
|
||||
strncpy((char *)&(*dest)->data, json_string_value(value), MAX_STRING_LENGTH);
|
||||
SUCCEED_RETURN(errctx);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
246
src/physics.c
Normal file
246
src/physics.c
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* @file physics.c
|
||||
* @brief Implements the physics subsystem.
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <akstdlib.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
|
||||
self->gravity = akgl_physics_null_gravity;
|
||||
self->collide = akgl_physics_null_collide;
|
||||
self->move = akgl_physics_null_move;
|
||||
self->simulate = akgl_physics_simulate;
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor");
|
||||
|
||||
if ( self->gravity_x != 0 ) {
|
||||
// Assume the X origin is - (screen left)
|
||||
actor->ex -= (self->gravity_x * dt);
|
||||
}
|
||||
if ( self->gravity_y != 0 ) {
|
||||
// Assume Y origin is + (down screen)
|
||||
actor->ey += (self->gravity_y * dt);
|
||||
}
|
||||
if ( self->gravity_z != 0 ) {
|
||||
// Assume Z origin is - (behind the camera)
|
||||
actor->ez -= (self->gravity_z * dt);
|
||||
}
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_RETURN(e, AKERR_API, "Not implemented");
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor");
|
||||
actor->x += actor->vx * dt;
|
||||
actor->y += actor->vy * dt;
|
||||
actor->z += actor->vz * dt;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self)
|
||||
{
|
||||
akgl_String *tmp;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
PASS(e, akgl_heap_next_string(&tmp));
|
||||
|
||||
self->gravity = akgl_physics_arcade_gravity;
|
||||
self->collide = akgl_physics_arcade_collide;
|
||||
self->move = akgl_physics_arcade_move;
|
||||
self->simulate = akgl_physics_simulate;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_get_property("physics.gravity.x", &tmp, "0.0"));
|
||||
CATCH(e, aksl_atof(tmp->data, &self->gravity_x));
|
||||
CATCH(e, akgl_get_property("physics.gravity.y", &tmp, "0.0"));
|
||||
CATCH(e, aksl_atof(tmp->data, &self->gravity_y));
|
||||
CATCH(e, akgl_get_property("physics.gravity.z", &tmp, "0.0"));
|
||||
CATCH(e, aksl_atof(tmp->data, &self->gravity_z));
|
||||
CATCH(e, akgl_get_property("physics.drag.x", &tmp, "0.0"));
|
||||
CATCH(e, aksl_atof(tmp->data, &self->drag_x));
|
||||
CATCH(e, akgl_get_property("physics.drag.y", &tmp, "0.0"));
|
||||
CATCH(e, aksl_atof(tmp->data, &self->drag_y));
|
||||
CATCH(e, akgl_get_property("physics.drag.z", &tmp, "0.0"));
|
||||
CATCH(e, aksl_atof(tmp->data, &self->drag_z));
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(tmp));
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Iterator defflags = {
|
||||
.flags = 0,
|
||||
.layerid = 0
|
||||
};
|
||||
SDL_Time curtime = 0;
|
||||
float32_t dt = 0;
|
||||
akgl_Actor *actor = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(e, self->move, AKERR_NULLPOINTER, "self->move");
|
||||
|
||||
// Reading the elapsed time requires self, so it cannot be hoisted above
|
||||
// the null check.
|
||||
curtime = SDL_GetTicksNS();
|
||||
dt = (float32_t)(curtime - self->gravity_time) / (float32_t)AKGL_TIME_ONESEC_NS;
|
||||
|
||||
if ( opflags == NULL ) {
|
||||
opflags = &defflags;
|
||||
}
|
||||
|
||||
|
||||
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||
actor = &HEAP_ACTOR[i];
|
||||
if ( actor->refcount == 0 ) {
|
||||
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 ( actor->layer != opflags->layerid ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// thrust is a function of acceleration on a given axis
|
||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ||
|
||||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
|
||||
actor->tx += actor->ax * dt;
|
||||
}
|
||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ||
|
||||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
|
||||
actor->ty += actor->ay * dt;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
self->gravity_time = curtime;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type)
|
||||
{
|
||||
uint32_t hashval;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(e, type, AKERR_NULLPOINTER, "type");
|
||||
|
||||
if ( strncmp(type->data, "null", 4) == 0) {
|
||||
PASS(e, akgl_physics_init_null(self));
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
if ( strncmp(type->data, "arcade", 6) == 0) {
|
||||
PASS(e, akgl_physics_init_arcade(self));
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
FAIL_RETURN(e, AKERR_KEY, "Invalid physics engine %s", type->data);
|
||||
}
|
||||
220
src/registry.c
220
src/registry.c
@@ -1,77 +1,229 @@
|
||||
/**
|
||||
* @file registry.c
|
||||
* @brief Implements the registry subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <sdlerror.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
#include <jansson.h>
|
||||
|
||||
#include <sdl3game/sprite.h>
|
||||
#include <sdl3game/registry.h>
|
||||
#include <sdl3game/iterator.h>
|
||||
#include <sdl3game/actor.h>
|
||||
#include <akstdlib.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/iterator.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/json_helpers.h>
|
||||
|
||||
SDL_PropertiesID REGISTRY_ACTOR;
|
||||
SDL_PropertiesID REGISTRY_ACTOR_STATE_STRINGS;
|
||||
SDL_PropertiesID REGISTRY_SPRITE;
|
||||
SDL_PropertiesID REGISTRY_SPRITESHEET;
|
||||
SDL_PropertiesID REGISTRY_CHARACTER;
|
||||
SDL_PropertiesID AKGL_REGISTRY_ACTOR = 0;
|
||||
SDL_PropertiesID AKGL_REGISTRY_ACTOR_STATE_STRINGS = 0;
|
||||
SDL_PropertiesID AKGL_REGISTRY_SPRITE = 0;
|
||||
SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET = 0;
|
||||
SDL_PropertiesID AKGL_REGISTRY_CHARACTER = 0;
|
||||
SDL_PropertiesID AKGL_REGISTRY_MUSIC = 0;
|
||||
SDL_PropertiesID AKGL_REGISTRY_FONT = 0;
|
||||
SDL_PropertiesID AKGL_REGISTRY_PROPERTIES = 0;
|
||||
|
||||
ErrorContext *registry_init()
|
||||
akerr_ErrorContext *akgl_registry_init()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, registry_init_spritesheet());
|
||||
CATCH(errctx, registry_init_sprite());
|
||||
CATCH(errctx, registry_init_character());
|
||||
CATCH(errctx, registry_init_actor());
|
||||
CATCH(errctx, registry_init_actor_state_strings());
|
||||
CATCH(errctx, akgl_registry_init_spritesheet());
|
||||
CATCH(errctx, akgl_registry_init_sprite());
|
||||
CATCH(errctx, akgl_registry_init_character());
|
||||
CATCH(errctx, akgl_registry_init_actor());
|
||||
CATCH(errctx, akgl_registry_init_actor_state_strings());
|
||||
CATCH(errctx, akgl_registry_init_font());
|
||||
CATCH(errctx, akgl_registry_init_music());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *registry_init_actor()
|
||||
akerr_ErrorContext *akgl_registry_init_actor()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
REGISTRY_ACTOR = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, REGISTRY_ACTOR, ERR_NULLPOINTER, "Error initializing actor registry");
|
||||
if ( AKGL_REGISTRY_ACTOR != 0 ) {
|
||||
SDL_DestroyProperties(AKGL_REGISTRY_ACTOR);
|
||||
}
|
||||
AKGL_REGISTRY_ACTOR = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_ACTOR, AKERR_NULLPOINTER, "Error initializing actor registry");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *registry_init_actor_state_strings()
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
AKGL_REGISTRY_FONT = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_FONT, AKERR_NULLPOINTER, "Error initializing font registry");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_registry_init_music()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
AKGL_REGISTRY_MUSIC = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_MUSIC, AKERR_NULLPOINTER, "Error initializing music registry");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_registry_init_properties()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
AKGL_REGISTRY_PROPERTIES = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_PROPERTIES, AKERR_NULLPOINTER, "Error initializing properties registry");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_registry_init_actor_state_strings()
|
||||
{
|
||||
int i = 0;
|
||||
int flag = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
REGISTRY_ACTOR_STATE_STRINGS = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, REGISTRY_ACTOR_STATE_STRINGS, ERR_NULLPOINTER, "Error initializing actor state strings registry");
|
||||
for ( i = 0 ; i < ACTOR_MAX_STATES; i++ ) {
|
||||
AKGL_REGISTRY_ACTOR_STATE_STRINGS = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_ACTOR_STATE_STRINGS, AKERR_NULLPOINTER, "Error initializing actor state strings registry");
|
||||
for ( i = 0 ; i < AKGL_ACTOR_MAX_STATES; i++ ) {
|
||||
flag = (1 << i);
|
||||
SDL_SetNumberProperty(
|
||||
REGISTRY_ACTOR_STATE_STRINGS,
|
||||
ACTOR_STATE_STRING_NAMES[i],
|
||||
AKGL_REGISTRY_ACTOR_STATE_STRINGS,
|
||||
AKGL_ACTOR_STATE_STRING_NAMES[i],
|
||||
flag);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *registry_init_sprite()
|
||||
akerr_ErrorContext *akgl_registry_init_sprite()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
REGISTRY_SPRITE = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, REGISTRY_SPRITE, ERR_NULLPOINTER, "Error initializing sprite registry");
|
||||
AKGL_REGISTRY_SPRITE = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_SPRITE, AKERR_NULLPOINTER, "Error initializing sprite registry");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *registry_init_spritesheet()
|
||||
akerr_ErrorContext *akgl_registry_init_spritesheet()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
REGISTRY_SPRITESHEET = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, REGISTRY_SPRITESHEET, ERR_NULLPOINTER, "Error initializing spritesheet registry");
|
||||
AKGL_REGISTRY_SPRITESHEET = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_SPRITESHEET, AKERR_NULLPOINTER, "Error initializing spritesheet registry");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *registry_init_character()
|
||||
akerr_ErrorContext *akgl_registry_init_character()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
REGISTRY_CHARACTER = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, REGISTRY_CHARACTER, ERR_NULLPOINTER, "Error initializing character registry");
|
||||
AKGL_REGISTRY_CHARACTER = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_CHARACTER, AKERR_NULLPOINTER, "Error initializing character registry");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname)
|
||||
{
|
||||
json_t *json = NULL;
|
||||
json_t *props = NULL;
|
||||
const char *pkey = NULL;
|
||||
json_t *pvalue = NULL;
|
||||
|
||||
json_error_t error;
|
||||
akgl_String *tmpstr;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "null filename");
|
||||
// One ATTEMPT around the whole function, rather than one for the parse and
|
||||
// a bare loop after it. `props` is a borrowed reference into `json` and is
|
||||
// read by the loop, so the document cannot be released until the loop is
|
||||
// done -- and with the loop outside the block, every path out of it leaked
|
||||
// the document. The per-property ATTEMPT stays nested inside the loop,
|
||||
// which is what AGENTS.md requires: a CATCH written directly in a loop
|
||||
// breaks the loop rather than leaving the function.
|
||||
ATTEMPT {
|
||||
SDL_Log("Loading from %s", fname);
|
||||
json = json_load_file(fname, 0, &error);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
json,
|
||||
AKERR_NULLPOINTER,
|
||||
"Error while loading properties from %s on line %d: %s-",
|
||||
fname,
|
||||
error.line,
|
||||
error.text);
|
||||
CATCH(errctx, akgl_get_json_object_value(json, "properties", &props));
|
||||
|
||||
json_object_foreach(props, pkey, pvalue) {
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
CATCH(errctx, akgl_get_json_string_value(props, (char *)pkey, &tmpstr));
|
||||
SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, pkey, tmpstr->data);
|
||||
SDL_Log("Set property %s = %s", pkey, tmpstr->data);
|
||||
CATCH(errctx, akgl_heap_release_string(tmpstr));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
}
|
||||
} CLEANUP {
|
||||
// Every value was copied into the property store on the way past, so
|
||||
// nothing outlives the document.
|
||||
if ( json != NULL ) {
|
||||
json_decref(json);
|
||||
json = NULL;
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
SDL_Log("Properties loaded");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *src)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL char *");
|
||||
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "NULL char *");
|
||||
SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, name, src);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def)
|
||||
{
|
||||
const char *value = NULL;
|
||||
size_t valuelen = 0;
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL char *");
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "NULL akgl_String *");
|
||||
ATTEMPT {
|
||||
if ( *dest == NULL ) {
|
||||
CATCH(e, akgl_heap_next_string(dest));
|
||||
}
|
||||
// Copy the value's own length, not the destination's capacity. What
|
||||
// SDL hands back is its strdup of the value -- four bytes for "0.0" --
|
||||
// so copying a fixed AKGL_MAX_STRING_LENGTH read up to 4 KiB past the
|
||||
// end of somebody else's allocation on every call. It returned garbage
|
||||
// past the terminator, and it would have faulted outright on a value
|
||||
// that happened to sit at the end of a page.
|
||||
value = SDL_GetStringProperty(AKGL_REGISTRY_PROPERTIES, (const char *)name, (const char *)def);
|
||||
// An unset property with a NULL default still reports AKERR_NULLPOINTER,
|
||||
// which is what the header has always promised. It used to arrive from
|
||||
// inside aksl_memcpy; now it is raised here, before anything is measured.
|
||||
FAIL_ZERO_BREAK(
|
||||
e,
|
||||
value,
|
||||
AKERR_NULLPOINTER,
|
||||
"Property %s is not set and no default was given",
|
||||
name);
|
||||
valuelen = strlen(value);
|
||||
FAIL_NONZERO_BREAK(
|
||||
e,
|
||||
(valuelen >= AKGL_MAX_STRING_LENGTH),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Property %s is %zu bytes, which does not fit an akgl_String (%d)",
|
||||
name,
|
||||
valuelen,
|
||||
AKGL_MAX_STRING_LENGTH);
|
||||
CATCH(e, aksl_memcpy((*dest)->data, (void *)value, (valuelen + 1)));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
153
src/renderer.c
Normal file
153
src/renderer.c
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* @file renderer.c
|
||||
* @brief Implements the renderer subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/game.h>
|
||||
|
||||
#include <akerror.h>
|
||||
#include <akstdlib.h>
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self)
|
||||
{
|
||||
akgl_String *width = NULL;
|
||||
akgl_String *height = NULL;
|
||||
int screenwidth;
|
||||
int screenheight;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
|
||||
PASS(e, akgl_get_property("game.screenwidth", &width, "0"));
|
||||
PASS(e, akgl_get_property("game.screenheight", &height, "0"));
|
||||
PASS(e, aksl_atoi(width->data, &screenwidth));
|
||||
PASS(e, aksl_atoi(height->data, &screenheight));
|
||||
SDL_Log("Initializing screen (%sx%s = %dx%d)", width->data, height->data, screenwidth, screenheight);
|
||||
PASS(e, akgl_heap_release_string(width));
|
||||
PASS(e, akgl_heap_release_string(height));
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
e,
|
||||
SDL_CreateWindowAndRenderer(game.uri, screenwidth, screenheight, 0, &window, &self->sdl_renderer),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't create window/renderer: %s",
|
||||
SDL_GetError());
|
||||
|
||||
camera->x = 0;
|
||||
camera->y = 0;
|
||||
camera->w = screenwidth;
|
||||
camera->h = screenheight;
|
||||
|
||||
PASS(e, akgl_render_bind2d(self));
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_bind2d(akgl_RenderBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
|
||||
// Deliberately does not touch self->sdl_renderer: a host that owns its own
|
||||
// window has already put one there, and this is the only way it gets a
|
||||
// usable backend without libakgl creating a second window.
|
||||
self->shutdown = &akgl_render_2d_shutdown;
|
||||
self->frame_start = &akgl_render_2d_frame_start;
|
||||
self->frame_end = &akgl_render_2d_frame_end;
|
||||
self->draw_texture = &akgl_render_2d_draw_texture;
|
||||
self->draw_mesh = &akgl_render_2d_draw_mesh;
|
||||
self->draw_world = &akgl_render_2d_draw_world;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
SDL_SetRenderDrawColor(self->sdl_renderer, 0, 0, 0, 255);
|
||||
SDL_RenderClear(self->sdl_renderer);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
SDL_RenderPresent(self->sdl_renderer);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(e, texture, AKERR_NULLPOINTER, "texture");
|
||||
//FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "src");
|
||||
//FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
|
||||
|
||||
if ( angle != 0 ) {
|
||||
FAIL_ZERO_RETURN(e, center, AKERR_NULLPOINTER, "center");
|
||||
FAIL_ZERO_RETURN(
|
||||
e,
|
||||
SDL_RenderTextureRotated(self->sdl_renderer, texture, src, dest, angle, center, flip),
|
||||
AKERR_NULLPOINTER, "%s", SDL_GetError()
|
||||
);
|
||||
} else {
|
||||
FAIL_ZERO_RETURN(
|
||||
e,
|
||||
SDL_RenderTexture(self->sdl_renderer, texture, src, dest),
|
||||
AKERR_NULLPOINTER, "%s", SDL_GetError()
|
||||
);
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_RETURN(e, AKERR_API, "Not implemented");
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Iterator defflags;
|
||||
SDL_Time curTime = SDL_GetTicksNS();
|
||||
akgl_Actor *actor = NULL;
|
||||
int j = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
|
||||
if ( opflags == NULL ) {
|
||||
opflags = &defflags;
|
||||
PASS(e, aksl_memset((void *)opflags, 0x00, sizeof(akgl_Iterator)));
|
||||
}
|
||||
for ( int i = 0; i < AKGL_TILEMAP_MAX_LAYERS ; i++ ) {
|
||||
if ( i < gamemap->numlayers ) {
|
||||
PASS(e, akgl_tilemap_draw(gamemap, camera, i));
|
||||
}
|
||||
for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR ; j++ ) {
|
||||
actor = &HEAP_ACTOR[j];
|
||||
if ( actor->refcount == 0 ) {
|
||||
continue;
|
||||
}
|
||||
if ( actor->layer != i ) {
|
||||
continue;
|
||||
}
|
||||
PASS(e, actor->renderfunc(actor));
|
||||
}
|
||||
}
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
249
src/sprite.c
249
src/sprite.c
@@ -1,172 +1,239 @@
|
||||
/**
|
||||
* @file sprite.c
|
||||
* @brief Implements the sprite subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <string.h>
|
||||
#include <jansson.h>
|
||||
#include <sdlerror.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <libgen.h>
|
||||
#include <sdl3game/game.h>
|
||||
#include <sdl3game/sprite.h>
|
||||
#include <sdl3game/json_helpers.h>
|
||||
#include <sdl3game/heap.h>
|
||||
#include <sdl3game/registry.h>
|
||||
#include <sdl3game/staticstring.h>
|
||||
#include <sdl3game/iterator.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/json_helpers.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/iterator.h>
|
||||
#include <akgl/util.h>
|
||||
|
||||
static ErrorContext *sprite_load_json_spritesheet(json_t *json, spritesheet **sheet, char *relative_path)
|
||||
akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "NULL sprite");
|
||||
FAIL_ZERO_RETURN(e, srccoords, AKERR_NULLPOINTER, "NULL SDL_Rect");
|
||||
FAIL_ZERO_RETURN(e, self->sheet, AKERR_NULLPOINTER, "NULL spritesheet");
|
||||
|
||||
srccoords->x = self->width * self->frameids[frameid];
|
||||
if ( srccoords->x >= self->sheet->texture->w ) {
|
||||
srccoords->y = ((int)srccoords->x / self->sheet->texture->w) * self->height;
|
||||
srccoords->x = ((int)srccoords->x % self->sheet->texture->w);
|
||||
} else {
|
||||
srccoords->y = 0;
|
||||
}
|
||||
srccoords->w = self->width;
|
||||
srccoords->h = self->height;
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Find or load the spritesheet a sprite definition names.
|
||||
*
|
||||
* Resolves the `spritesheet.filename` path relative to the sprite definition's
|
||||
* own directory, then looks that resolved path up in #AKGL_REGISTRY_SPRITESHEET.
|
||||
* A hit is reused as is -- which is what keeps ten sprites cut from one image
|
||||
* down to one texture -- and a miss claims a sheet from the pool and loads it,
|
||||
* reading `frame_width` and `frame_height` only in that case.
|
||||
*
|
||||
* @param json The parsed sprite document, whose `spritesheet` object
|
||||
* this reads. Required in practice; not checked.
|
||||
* @param sheet Receives the sheet, whether found or freshly loaded.
|
||||
* Required; not checked. On the reuse path the sheet's
|
||||
* reference count is *not* incremented, so the sprite
|
||||
* borrows it.
|
||||
* @param relative_path Directory to resolve the image path against -- the
|
||||
* directory holding the sprite JSON. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY If the document has no `spritesheet` object, or that object
|
||||
* has no `filename` -- or, on the load path, no `frame_width` or
|
||||
* `frame_height`.
|
||||
* @throws AKERR_TYPE If one of those has the wrong JSON type.
|
||||
* @throws AKERR_OUTOFBOUNDS If the joined path is too long for a pooled string.
|
||||
* @throws ENOENT If the image path does not exist.
|
||||
* @throws AKGL_ERR_SDL If the image cannot be decoded.
|
||||
* @throws AKGL_ERR_HEAP If the spritesheet or string pool is exhausted.
|
||||
*/
|
||||
static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
json_t *spritesheet_json = NULL;
|
||||
int ss_frame_width = 0;
|
||||
int ss_frame_height = 0;
|
||||
string *ss_filename = NULL;
|
||||
string *tmpstr = NULL;
|
||||
akgl_String *ss_filename = NULL;
|
||||
akgl_String *tmpstr = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, heap_next_string(&tmpstr));
|
||||
CATCH(errctx, string_initialize(tmpstr, NULL));
|
||||
CATCH(errctx, get_json_object_value((json_t *)json, "spritesheet", &spritesheet_json));
|
||||
CATCH(errctx, get_json_string_value((json_t *)spritesheet_json, "filename", &ss_filename));
|
||||
if ( ss_filename->data[0] != '/' ) {
|
||||
SDL_snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s/%s", relative_path, ss_filename->data);
|
||||
} else {
|
||||
SDL_snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s", ss_filename->data);
|
||||
}
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
||||
CATCH(errctx, akgl_get_json_object_value((json_t *)json, "spritesheet", &spritesheet_json));
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)spritesheet_json, "filename", &ss_filename));
|
||||
CATCH(errctx, akgl_path_relative(relative_path, ss_filename->data, tmpstr));
|
||||
*sheet = SDL_GetPointerProperty(
|
||||
REGISTRY_SPRITESHEET,
|
||||
AKGL_REGISTRY_SPRITESHEET,
|
||||
(char *)&tmpstr->data,
|
||||
NULL
|
||||
);
|
||||
if ( *sheet == NULL ) {
|
||||
CATCH(errctx, heap_next_spritesheet(sheet));
|
||||
CATCH(errctx, get_json_integer_value((json_t *)spritesheet_json, "frame_width", &ss_frame_width));
|
||||
CATCH(errctx, get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_width));
|
||||
CATCH(errctx, akgl_heap_next_spritesheet(sheet));
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_width", &ss_frame_width));
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_height));
|
||||
CATCH(errctx,
|
||||
spritesheet_initialize(
|
||||
(spritesheet *)*sheet,
|
||||
akgl_spritesheet_initialize(
|
||||
(akgl_SpriteSheet *)*sheet,
|
||||
ss_frame_width,
|
||||
ss_frame_height,
|
||||
(char *)&tmpstr->data)
|
||||
);
|
||||
}
|
||||
} CLEANUP {
|
||||
IGNORE(heap_release_string(ss_filename));
|
||||
IGNORE(heap_release_string(tmpstr));
|
||||
IGNORE(akgl_heap_release_string(ss_filename));
|
||||
IGNORE(akgl_heap_release_string(tmpstr));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *sprite_load_json(char *filename)
|
||||
|
||||
akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
json_t *json = NULL;
|
||||
json_t *frames = NULL;
|
||||
json_error_t error;
|
||||
sprite *obj = NULL;
|
||||
spritesheet *sheet = NULL;
|
||||
string *spritename = NULL;
|
||||
//string *tmpstr = NULL;
|
||||
akgl_Sprite *obj = NULL;
|
||||
akgl_SpriteSheet *sheet = NULL;
|
||||
akgl_String *spritename = NULL;
|
||||
akgl_String *filename_copy = NULL;
|
||||
int i = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, filename, ERR_NULLPOINTER, "Received null filename");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, heap_next_sprite(&obj));
|
||||
//CATCH(errctx, heap_next_string(&tmpstr));
|
||||
//CATCH(errctx, string_initialize(tmpstr, NULL));
|
||||
CATCH(errctx, heap_next_string(&spritename));
|
||||
CATCH(errctx, string_initialize(spritename, NULL));
|
||||
|
||||
//SDL_snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
|
||||
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_sprite(&obj));
|
||||
CATCH(errctx, akgl_heap_next_string(&filename_copy));
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
strlen(filename) >= AKGL_MAX_STRING_LENGTH,
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Sprite filename exceeds temporary string capacity"
|
||||
);
|
||||
SDL_strlcpy(filename_copy->data, filename, AKGL_MAX_STRING_LENGTH);
|
||||
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
||||
CATCH(errctx, akgl_heap_next_string(&spritename));
|
||||
CATCH(errctx, akgl_string_initialize(spritename, NULL));
|
||||
|
||||
//SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
|
||||
json = (json_t *)json_load_file(filename, 0, &error);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
json,
|
||||
ERR_NULLPOINTER,
|
||||
AKERR_NULLPOINTER,
|
||||
"Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
|
||||
);
|
||||
|
||||
CATCH(errctx, sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename)));
|
||||
CATCH(errctx, get_json_string_value((json_t *)json, "name", &spritename));
|
||||
|
||||
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,
|
||||
sprite_initialize(
|
||||
(sprite *)obj,
|
||||
akgl_sprite_initialize(
|
||||
(akgl_Sprite *)obj,
|
||||
spritename->data,
|
||||
(spritesheet *)sheet)
|
||||
(akgl_SpriteSheet *)sheet)
|
||||
);
|
||||
|
||||
CATCH(errctx, get_json_integer_value((json_t *)json, "width", &obj->width));
|
||||
CATCH(errctx, get_json_integer_value((json_t *)json, "height", &obj->height));
|
||||
CATCH(errctx, get_json_integer_value((json_t *)json, "speed", &obj->speed));
|
||||
CATCH(errctx, get_json_boolean_value((json_t *)json, "loop", &obj->loop));
|
||||
CATCH(errctx, get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));
|
||||
|
||||
CATCH(errctx, get_json_array_value((json_t *)json, "frames", &frames));
|
||||
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &obj->width));
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &obj->height));
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &obj->speed));
|
||||
obj->speed = obj->speed * AKGL_TIME_ONESEC_MS;
|
||||
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loop", &obj->loop));
|
||||
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));
|
||||
|
||||
CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames));
|
||||
obj->frames = json_array_size((json_t *)frames);
|
||||
for ( i = 0 ; i < obj->frames; i++ ) {
|
||||
CATCH(errctx, get_json_array_index_integer((json_t *)frames, i, &obj->frameids[i]));
|
||||
CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, (uint32_t *)&obj->frameids[i]));
|
||||
}
|
||||
} CLEANUP {
|
||||
if ( errctx != NULL && errctx->status != 0 ) {
|
||||
IGNORE(heap_release_sprite(obj));
|
||||
IGNORE(heap_release_spritesheet(sheet));
|
||||
// The sprite copies every field it wants out of the document and the
|
||||
// sheet is found by resolved path, so nothing here points into the
|
||||
// parsed tree once this block runs. Released on both paths.
|
||||
if ( json != NULL ) {
|
||||
json_decref(json);
|
||||
json = NULL;
|
||||
}
|
||||
IGNORE(heap_release_string(spritename));
|
||||
//IGNORE(heap_release_string(tmpstr));
|
||||
if ( errctx != NULL && errctx->status != 0 ) {
|
||||
IGNORE(akgl_heap_release_sprite(obj));
|
||||
IGNORE(akgl_heap_release_spritesheet(sheet));
|
||||
}
|
||||
IGNORE(akgl_heap_release_string(spritename));
|
||||
IGNORE(akgl_heap_release_string(filename_copy));
|
||||
//IGNORE(akgl_heap_release_string(tmpstr));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *sprite_initialize(sprite *spr, char *name, spritesheet *sheet)
|
||||
akerr_ErrorContext *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, spr, ERR_NULLPOINTER, "Null sprite reference");
|
||||
FAIL_ZERO_RETURN(errctx, name, ERR_NULLPOINTER, "Empty sprite name");
|
||||
FAIL_ZERO_RETURN(errctx, sheet, ERR_NULLPOINTER, "Null spritesheet reference");
|
||||
|
||||
memset(spr, 0x00, sizeof(sprite));
|
||||
memcpy(spr->name, name, SPRITE_MAX_NAME_LENGTH);
|
||||
FAIL_ZERO_RETURN(errctx, spr, AKERR_NULLPOINTER, "Null sprite reference");
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Empty sprite name");
|
||||
FAIL_ZERO_RETURN(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet reference");
|
||||
|
||||
memset(spr, 0x00, sizeof(akgl_Sprite));
|
||||
memcpy(spr->name, name, AKGL_SPRITE_MAX_NAME_LENGTH);
|
||||
spr->sheet = sheet;
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(REGISTRY_SPRITE, (char *)&spr->name, (void *)spr),
|
||||
ERR_KEY,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITE, (char *)&spr->name, (void *)spr),
|
||||
AKERR_KEY,
|
||||
"Unable to add sprite to registry");
|
||||
spr->refcount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *spritesheet_initialize(spritesheet *sheet, int sprite_w, int sprite_h, char *filename)
|
||||
akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
//string *tmpstr = NULL;
|
||||
//akgl_String *tmpstr = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, sheet, ERR_NULLPOINTER, "Null spritesheet pointer");
|
||||
FAIL_ZERO_BREAK(errctx, filename, ERR_NULLPOINTER, "Null filename pointer");
|
||||
FAIL_ZERO_BREAK(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet pointer");
|
||||
FAIL_ZERO_BREAK(errctx, filename, AKERR_NULLPOINTER, "Null filename pointer");
|
||||
|
||||
memset(sheet, 0x00, sizeof(spritesheet));
|
||||
memset(sheet, 0x00, sizeof(akgl_SpriteSheet));
|
||||
|
||||
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
|
||||
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
||||
strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH);
|
||||
|
||||
//snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
|
||||
sheet->texture = IMG_LoadTexture(renderer->sdl_renderer, filename);
|
||||
FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());
|
||||
|
||||
//CATCH(errctx, heap_next_string(&tmpstr));
|
||||
|
||||
//CATCH(errctx, string_initialize(tmpstr, NULL));
|
||||
strncpy((char *)&sheet->name, filename, SPRITE_SHEET_MAX_FILENAME_LENGTH);
|
||||
|
||||
//snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
|
||||
sheet->texture = IMG_LoadTexture(renderer, filename);
|
||||
FAIL_ZERO_BREAK(errctx, sheet->texture, ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet),
|
||||
ERR_KEY,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet),
|
||||
AKERR_KEY,
|
||||
"Unable to add spritesheet to registry: %s",
|
||||
SDL_GetError());
|
||||
sheet->refcount += 1;
|
||||
} CLEANUP {
|
||||
//IGNORE(heap_release_string(tmpstr));
|
||||
//IGNORE(akgl_heap_release_string(tmpstr));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
#include <sdlerror.h>
|
||||
#include <sdl3game/staticstring.h>
|
||||
/**
|
||||
* @file staticstring.c
|
||||
* @brief Implements the staticstring subsystem.
|
||||
*/
|
||||
|
||||
ErrorContext *string_initialize(string *obj, char *init)
|
||||
#include <akerror.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <errno.h>
|
||||
|
||||
akerr_ErrorContext *akgl_string_initialize(akgl_String *obj, char *init)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "Attempted to initialize NULL string reference");
|
||||
if ( init != NULL ) {
|
||||
strncpy((char *)&obj->data, init, MAX_STRING_LENGTH);
|
||||
} else {
|
||||
memset(&obj->data, 0x00, sizeof(string));
|
||||
}
|
||||
obj->refcount = 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Attempted to initialize NULL string reference");
|
||||
if ( init != NULL ) {
|
||||
strncpy((char *)&obj->data, init, AKGL_MAX_STRING_LENGTH);
|
||||
} else {
|
||||
memset(&obj->data, 0x00, sizeof(akgl_String));
|
||||
}
|
||||
obj->refcount = 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_string_copy(akgl_String *src, akgl_String *dst, int count)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
|
||||
if ( count == 0 ) {
|
||||
count = AKGL_MAX_STRING_LENGTH;
|
||||
}
|
||||
if ( (char *)dst->data != strncpy((char *)&dst->data, (char *)&src->data, count) ) {
|
||||
FAIL_RETURN(e, errno, "strncpy");
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
140
src/text.c
Normal file
140
src/text.c
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @file text.c
|
||||
* @brief Implements the text subsystem.
|
||||
*/
|
||||
|
||||
#include <akerror.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <akgl/text.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/game.h>
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size)
|
||||
{
|
||||
TTF_Font *font = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
|
||||
FAIL_ZERO_RETURN(errctx, filepath, AKERR_NULLPOINTER, "Null filepath");
|
||||
font = TTF_OpenFont(filepath, size);
|
||||
FAIL_ZERO_RETURN(errctx, font, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
// Loading over an existing name used to abandon the font it displaced --
|
||||
// there was nothing in the library that could close one. The old font goes
|
||||
// back first now, and only once the new one has opened, so a failed load
|
||||
// leaves the caller with the font they already had.
|
||||
if ( SDL_GetPointerProperty(AKGL_REGISTRY_FONT, name, NULL) != NULL ) {
|
||||
PASS(errctx, akgl_text_unloadfont(name));
|
||||
}
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_FONT, name, (void *)font),
|
||||
AKERR_KEY,
|
||||
"Unable to add font %p to registry as %s : %s",
|
||||
(void *)font,
|
||||
name,
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name)
|
||||
{
|
||||
TTF_Font *font = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
|
||||
|
||||
font = (TTF_Font *)SDL_GetPointerProperty(AKGL_REGISTRY_FONT, name, NULL);
|
||||
FAIL_ZERO_RETURN(errctx, font, AKERR_KEY, "No font named %s in the registry", name);
|
||||
|
||||
// Cleared before the close, so a font is never reachable through the
|
||||
// registry after it has been handed back to SDL_ttf.
|
||||
SDL_ClearProperty(AKGL_REGISTRY_FONT, name);
|
||||
TTF_CloseFont(font);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y)
|
||||
{
|
||||
SDL_Surface *textsurf = NULL;
|
||||
SDL_Texture *texture = NULL;
|
||||
SDL_FRect dest;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
|
||||
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
|
||||
// Checked before anything is rasterized, and checked at all because a
|
||||
// backend that has an SDL_Renderer but was never run through
|
||||
// akgl_render_bind2d has a NULL draw_texture -- which is exactly the state
|
||||
// akgl_render_init2d used to be the only escape from.
|
||||
FAIL_ZERO_RETURN(errctx, renderer, AKERR_NULLPOINTER, "No renderer backend");
|
||||
FAIL_ZERO_RETURN(errctx, renderer->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, renderer->draw_texture, AKERR_NULLPOINTER, "Renderer backend has no draw_texture");
|
||||
if ( wraplength > 0 ) {
|
||||
textsurf = TTF_RenderText_Blended_Wrapped(
|
||||
font,
|
||||
text,
|
||||
0,
|
||||
color,
|
||||
wraplength);
|
||||
} else {
|
||||
textsurf = TTF_RenderText_Blended(
|
||||
font,
|
||||
text,
|
||||
0,
|
||||
color);
|
||||
}
|
||||
FAIL_ZERO_RETURN(errctx, textsurf, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
texture = SDL_CreateTextureFromSurface(renderer->sdl_renderer, textsurf);
|
||||
FAIL_ZERO_RETURN(errctx, texture, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
dest.x = x;
|
||||
dest.y = y;
|
||||
SDL_GetTextureSize(texture, &dest.w, &dest.h);
|
||||
PASS(errctx, renderer->draw_texture(renderer, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||
SDL_DestroyTexture(texture);
|
||||
SDL_DestroySurface(textsurf);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
|
||||
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
|
||||
FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
|
||||
FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
|
||||
// A zero length means "the string is null terminated", not "the empty
|
||||
// string" -- an empty text measures 0 wide and one line high.
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
TTF_GetStringSize(font, text, 0, w, h),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
|
||||
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
|
||||
FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
|
||||
FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
|
||||
// SDL_ttf takes the wrap width as an int and reads a negative one as a
|
||||
// very large unsigned width, which silently disables wrapping instead of
|
||||
// reporting anything. Refuse it here rather than return a wrong measurement.
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
(wraplength < 0),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Wrap length %d is negative",
|
||||
wraplength);
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
TTF_GetStringSizeWrapped(font, text, 0, wraplength, w, h),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
790
src/tilemap.c
790
src/tilemap.c
File diff suppressed because it is too large
Load Diff
395
src/util.c
395
src/util.c
@@ -1,169 +1,312 @@
|
||||
/**
|
||||
* @file util.c
|
||||
* @brief Implements the util subsystem.
|
||||
*/
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <libgen.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <sdlerror.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <sdl3game/util.h>
|
||||
#include <sdl3game/heap.h>
|
||||
#include <sdl3game/registry.h>
|
||||
#include <sdl3game/game.h>
|
||||
#include <akgl/util.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
ErrorContext *rectangle_points(RectanglePoints *dest, SDL_FRect *rect)
|
||||
#include <akstdlib.h>
|
||||
|
||||
/**
|
||||
* @brief Resolve @p path against @p root and write the absolute result to @p dst.
|
||||
*
|
||||
* The second half of akgl_path_relative: joins the two with a `/` and resolves
|
||||
* the join, so symlinks and `..` are folded out and the result is absolute. It
|
||||
* is the fallback, reached only once resolving @p path on its own has failed.
|
||||
*
|
||||
* Not declared in util.h -- it is reachable only through akgl_path_relative.
|
||||
*
|
||||
* @param root Directory to resolve against, normally `dirname` of the file that
|
||||
* named @p path. Required. A trailing `/` is harmless; the join adds
|
||||
* one unconditionally and `realpath` collapses the double.
|
||||
* @param path Relative path to resolve. Required. An absolute @p path still gets
|
||||
* @p root pasted in front of it, which will not exist -- so callers
|
||||
* must not send absolute paths down this branch.
|
||||
* @param dst Receives the resolved absolute path. Required, and must already be
|
||||
* a claimed pool string.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dst is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If `root + path` is at least #AKGL_MAX_STRING_LENGTH
|
||||
* bytes. The message reports both the combined length and the limit.
|
||||
* @throws ENOENT If the joined path does not exist. Any other `errno`
|
||||
* `realpath(3)` raises -- EACCES, ELOOP, ENOTDIR -- propagates likewise.
|
||||
* @throws AKGL_ERR_HEAP If the string pool cannot supply the two scratch buffers.
|
||||
*
|
||||
* @note The AKERR_OUTOFBOUNDS check uses `FAIL_RETURN` from inside the `ATTEMPT`
|
||||
* block, which returns past `CLEANUP` -- so on that one path the two
|
||||
* scratch strings are never released. This is exactly the hazard AGENTS.md
|
||||
* warns about; it wants a `FAIL_BREAK`.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String *dst)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "NULL RectanglePoints reference");
|
||||
FAIL_ZERO_RETURN(errctx, rect, ERR_NULLPOINTER, "NULL Rectangle reference");
|
||||
dest->topleft.x = rect->x;
|
||||
dest->topleft.y = rect->y;
|
||||
dest->bottomleft.x = rect->x;
|
||||
dest->bottomleft.y = rect->y + rect->h;
|
||||
dest->topright.x = rect->x + rect->w;
|
||||
dest->topright.y = rect->y;
|
||||
dest->bottomright.x = rect->x + rect->w;
|
||||
dest->bottomright.y = rect->y + rect->h;
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(e);
|
||||
akgl_String *pathbuf;
|
||||
akgl_String *strbuf;
|
||||
char *result;
|
||||
int rootlen;
|
||||
int pathlen;
|
||||
int count;
|
||||
|
||||
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
|
||||
|
||||
PASS(e, akgl_heap_next_string(&strbuf));
|
||||
PASS(e, akgl_heap_next_string(&pathbuf));
|
||||
|
||||
ATTEMPT {
|
||||
// Is it relative to the root?
|
||||
rootlen = strlen(root);
|
||||
pathlen = strlen(path);
|
||||
if ( (rootlen + pathlen) >= AKGL_MAX_STRING_LENGTH ) {
|
||||
FAIL_RETURN(e, AKERR_OUTOFBOUNDS, "Total path length (%d) is greater than maximum akgl_String length (%d)", (rootlen + pathlen), AKGL_MAX_STRING_LENGTH);
|
||||
}
|
||||
DISABLE_GCC_WARNING_FORMAT_TRUNCATION
|
||||
CATCH(e, aksl_snprintf(
|
||||
&count,
|
||||
(char *)&pathbuf->data,
|
||||
sizeof(pathbuf->data),
|
||||
"%s/%s",
|
||||
root,
|
||||
path
|
||||
));
|
||||
RESTORE_GCC_WARNINGS
|
||||
CATCH(e, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data, sizeof(strbuf->data)));
|
||||
CATCH(e, akgl_string_copy(strbuf, dst, 0));
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(strbuf));
|
||||
IGNORE(akgl_heap_release_string(pathbuf));
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
ErrorContext *collide_point_rectangle(point *p, RectanglePoints *rp, bool *collide)
|
||||
akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, p, ERR_NULLPOINTER, "NULL Point reference");
|
||||
FAIL_ZERO_RETURN(errctx, rp, ERR_NULLPOINTER, "NULL RectanglePoints reference");
|
||||
FAIL_ZERO_RETURN(errctx, collide, ERR_NULLPOINTER, "NULL boolean reference");
|
||||
if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) &&
|
||||
(p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) {
|
||||
*collide = true;
|
||||
} else {
|
||||
PREPARE_ERROR(e);
|
||||
akgl_String *strbuf;
|
||||
char *result;
|
||||
bool relative_to_root = false;
|
||||
|
||||
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
|
||||
|
||||
PASS(e, akgl_heap_next_string(&strbuf));
|
||||
|
||||
ATTEMPT {
|
||||
// Is path relative to our current working directory?
|
||||
CATCH(e, aksl_realpath(path, (char *)&strbuf->data, sizeof(strbuf->data)));
|
||||
// Yes it is. strbuf->data contains the absolute path.
|
||||
CATCH(e, akgl_string_copy(strbuf, dst, 0));
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(strbuf));
|
||||
} PROCESS(e) {
|
||||
} HANDLE(e, ENOENT) {
|
||||
// Path is not relative to our current working directory. Resolve it
|
||||
// against root instead -- but after FINISH, not from in here. Returning
|
||||
// from inside a HANDLE block skips the RELEASE_ERROR that FINISH ends
|
||||
// with, so the handled context is never given back to
|
||||
// AKERR_ARRAY_ERROR. That leaks one context per call, and the 129th
|
||||
// call takes the whole process down with "Unable to pull an error
|
||||
// context from the array!". Every map load resolves several paths this
|
||||
// way.
|
||||
relative_to_root = true;
|
||||
} FINISH(e, true);
|
||||
|
||||
if ( relative_to_root == true ) {
|
||||
PASS(e, akgl_path_relative_root(root, path, dst));
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Intended to resolve @p path against the directory holding @p from. Unfinished.
|
||||
*
|
||||
* The shape is there -- resolve @p from, take its `dirname` -- but the body
|
||||
* stops before it does anything with @p path, and `*dst` is never written. Not
|
||||
* declared in util.h, and nothing in the tree calls it.
|
||||
*
|
||||
* @param path The path to resolve. Required, and currently ignored past the
|
||||
* `NULL` check.
|
||||
* @param from A file whose directory @p path should be taken as relative to.
|
||||
* Required. Must exist -- it is resolved with `realpath(3)`.
|
||||
* @param dst Intended to receive the resolved path. Never written. Note the
|
||||
* double indirection, which disagrees with the rest of the
|
||||
* `akgl_path_relative*` family; see TODO.md item 40.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p path or @p from is `NULL`.
|
||||
* @throws ENOENT If @p from does not exist, along with any other `errno`
|
||||
* `realpath(3)` raises.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @warning Known defect: the scratch string it claims is never released, so 256
|
||||
* calls exhaust the string pool. TODO.md, "Known and still open" item 4.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String **dst)
|
||||
{
|
||||
akgl_String *dirnamestr;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path");
|
||||
FAIL_ZERO_RETURN(e, from, AKERR_NULLPOINTER, "from");
|
||||
PASS(e, akgl_heap_next_string(&dirnamestr));
|
||||
PASS(e, aksl_realpath(from, (char *)&dirnamestr->data, sizeof(dirnamestr->data)));
|
||||
dirname((char *)&dirnamestr->data);
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
|
||||
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL Rectangle reference");
|
||||
dest->topleft.x = rect->x;
|
||||
dest->topleft.y = rect->y;
|
||||
dest->bottomleft.x = rect->x;
|
||||
dest->bottomleft.y = rect->y + rect->h;
|
||||
dest->topright.x = rect->x + rect->w;
|
||||
dest->topright.y = rect->y;
|
||||
dest->bottomright.x = rect->x + rect->w;
|
||||
dest->bottomright.y = rect->y + rect->h;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_collide_point_rectangle(point *p, RectanglePoints *rp, bool *collide)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, p, AKERR_NULLPOINTER, "NULL Point reference");
|
||||
FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
|
||||
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL boolean reference");
|
||||
if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) &&
|
||||
(p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) {
|
||||
*collide = true;
|
||||
} else {
|
||||
*collide = false;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide)
|
||||
{
|
||||
RectanglePoints r1p;
|
||||
RectanglePoints r2p;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, r1, AKERR_NULLPOINTER, "NULL rectangle reference");
|
||||
FAIL_ZERO_RETURN(errctx, r2, AKERR_NULLPOINTER, "NULL rectangle reference");
|
||||
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL collision flag reference");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_rectangle_points(&r1p, r1));
|
||||
CATCH(errctx, akgl_rectangle_points(&r2p, r2));
|
||||
|
||||
// is the upper left corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topleft, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the upper left corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topleft, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the top right corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topright, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the top right corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topright, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom left corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomleft, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom left corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomleft, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom right corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomright, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom right corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomright, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
*collide = false;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide)
|
||||
|
||||
akerr_ErrorContext *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2)
|
||||
{
|
||||
RectanglePoints r1p;
|
||||
RectanglePoints r2p;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, r1, ERR_NULLPOINTER, "NULL rectangle reference");
|
||||
FAIL_ZERO_RETURN(errctx, r2, ERR_NULLPOINTER, "NULL rectangle reference");
|
||||
FAIL_ZERO_RETURN(errctx, collide, ERR_NULLPOINTER, "NULL collision flag reference");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, rectangle_points(&r1p, r1));
|
||||
CATCH(errctx, rectangle_points(&r2p, r2));
|
||||
|
||||
// is the upper left corner of r1 contacting r2?
|
||||
CATCH(errctx, collide_point_rectangle(&r1p.topleft, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the upper left corner of r2 contacting r1?
|
||||
CATCH(errctx, collide_point_rectangle(&r2p.topleft, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the top right corner of r1 contacting r2?
|
||||
CATCH(errctx, collide_point_rectangle(&r1p.topright, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the top right corner of r2 contacting r1?
|
||||
CATCH(errctx, collide_point_rectangle(&r2p.topright, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom left corner of r1 contacting r2?
|
||||
CATCH(errctx, collide_point_rectangle(&r1p.bottomleft, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom left corner of r2 contacting r1?
|
||||
CATCH(errctx, collide_point_rectangle(&r2p.bottomleft, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom right corner of r1 contacting r2?
|
||||
CATCH(errctx, collide_point_rectangle(&r1p.bottomright, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom right corner of r2 contacting r1?
|
||||
CATCH(errctx, collide_point_rectangle(&r2p.bottomright, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
*collide = false;
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, s1, AKERR_NULLPOINTER, "NULL Surface pointer");
|
||||
FAIL_ZERO_RETURN(errctx, s2, AKERR_NULLPOINTER, "NULL Surface pointer");
|
||||
FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), AKERR_VALUE, "Comparison surfaces are not equal");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
|
||||
ErrorContext *compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, s1, ERR_NULLPOINTER, "NULL Surface pointer");
|
||||
FAIL_ZERO_RETURN(errctx, s2, ERR_NULLPOINTER, "NULL Surface pointer");
|
||||
FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), ERR_VALUE, "Comparison surfaces are not equal");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout)
|
||||
akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout)
|
||||
{
|
||||
SDL_Surface *s1 = NULL;
|
||||
SDL_Surface *s2 = NULL;
|
||||
SDL_FRect src = {.x = x, .y = y, .w = w, .h = h};
|
||||
SDL_FRect dest = {.x = x, .y = y, .w = w, .h = h};
|
||||
SDL_Rect read = {.x = x, .y = y, .w = w, .h = h};
|
||||
string *tmpstring = NULL;
|
||||
akgl_String *tmpstring = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, t1, ERR_NULLPOINTER, "NULL texture");
|
||||
FAIL_ZERO_BREAK(errctx, t2, ERR_NULLPOINTER, "NULL texture");
|
||||
FAIL_ZERO_BREAK(errctx, t1, AKERR_NULLPOINTER, "NULL texture");
|
||||
FAIL_ZERO_BREAK(errctx, t2, AKERR_NULLPOINTER, "NULL texture");
|
||||
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstring));
|
||||
SDL_RenderClear(renderer->sdl_renderer);
|
||||
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||
s1 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
|
||||
FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer");
|
||||
|
||||
CATCH(errctx, heap_next_string(&tmpstring));
|
||||
SDL_RenderClear(renderer);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderTexture(
|
||||
renderer,
|
||||
t1,
|
||||
&src,
|
||||
&dest),
|
||||
ERR_SDL,
|
||||
"Failed to render test texture");
|
||||
s1 = SDL_RenderReadPixels(renderer, &read);
|
||||
FAIL_ZERO_BREAK(errctx, s1, ERR_SDL, "Failed to read pixels from renderer");
|
||||
|
||||
if ( writeout != NULL ) {
|
||||
snprintf((char *)&tmpstring->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), writeout);
|
||||
snprintf((char *)&tmpstring->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), writeout);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
IMG_SavePNG(s1, (char *)&tmpstring->data),
|
||||
ERR_IO,
|
||||
AKERR_IO,
|
||||
"Unable to save %s: %s",
|
||||
(char *)&tmpstring->data,
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
SDL_RenderClear(renderer);
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderTexture(
|
||||
renderer,
|
||||
t2,
|
||||
&src,
|
||||
&dest),
|
||||
ERR_SDL,
|
||||
"Failed to render test texture");
|
||||
s2 = SDL_RenderReadPixels(renderer, &read);
|
||||
FAIL_ZERO_BREAK(errctx, s2, ERR_SDL, "Failed to read pixels from renderer");
|
||||
|
||||
CATCH(errctx, compare_sdl_surfaces(s1, s2));
|
||||
|
||||
SDL_RenderClear(renderer->sdl_renderer);
|
||||
|
||||
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||
s2 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
|
||||
FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer");
|
||||
|
||||
CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2));
|
||||
} CLEANUP {
|
||||
if ( s1 != NULL )
|
||||
SDL_DestroySurface(s1);
|
||||
if ( s2 != NULL )
|
||||
SDL_DestroySurface(s2);
|
||||
IGNORE(heap_release_string(tmpstring));
|
||||
IGNORE(akgl_heap_release_string(tmpstring));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
|
||||
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;
|
||||
}
|
||||
873
tests/actor.c
873
tests/actor.c
File diff suppressed because it is too large
Load Diff
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,18 +1,22 @@
|
||||
{
|
||||
"name": "testcharacter",
|
||||
"velocity_x": 0.20,
|
||||
"velocity_y": 0.20,
|
||||
"movementspeed": 1,
|
||||
"speedtime": 1,
|
||||
"speed_x": 0.20,
|
||||
"speed_y": 0.20,
|
||||
"acceleration_x": 0.20,
|
||||
"acceleration_y": 0.20,
|
||||
"sprite_mappings": [
|
||||
{
|
||||
"state": [
|
||||
"ACTOR_STATE_ALIVE",
|
||||
"ACTOR_STATE_FACE_LEFT"
|
||||
"AKGL_ACTOR_STATE_ALIVE",
|
||||
"AKGL_ACTOR_STATE_FACE_LEFT"
|
||||
],
|
||||
"sprite": "testsprite"
|
||||
},
|
||||
{
|
||||
"state": [
|
||||
"ACTOR_STATE_DEAD"
|
||||
"AKGL_ACTOR_STATE_DEAD"
|
||||
],
|
||||
"sprite": "testsprite2"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"spritesheet": {
|
||||
"filename": "assets/spritesheet.png",
|
||||
"filename": "spritesheet.png",
|
||||
"frame_width": 48,
|
||||
"frame_height": 48
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"spritesheet": {
|
||||
"filename": "assets/spritesheet.png",
|
||||
"filename": "spritesheet.png",
|
||||
"frame_width": 48,
|
||||
"frame_height": 48
|
||||
},
|
||||
|
||||
625
tests/audio.c
Normal file
625
tests/audio.c
Normal file
@@ -0,0 +1,625 @@
|
||||
/**
|
||||
* @file audio.c
|
||||
* @brief Unit tests for the tone generator.
|
||||
*
|
||||
* Almost everything here drives akgl_audio_mix() by hand rather than opening a
|
||||
* device. That is not a workaround: a device pulls samples on SDL's audio
|
||||
* thread at whatever rate it likes, so a test that opened one and then asserted
|
||||
* on voice state would be racing the callback. Pulling the samples ourselves
|
||||
* makes the synthesis deterministic -- the same input produces the same
|
||||
* waveform, sample for sample, every run.
|
||||
*
|
||||
* The device is opened once, at the end, to prove akgl_audio_init() and
|
||||
* akgl_audio_shutdown() work against the dummy driver.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/audio.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/**
|
||||
* @brief Frames of output most tests generate at a time.
|
||||
*
|
||||
* Enough to hold a 10 ms attack and a 10 ms decay back to back at 44100 frames
|
||||
* per second, which is the longest single stretch any test here inspects.
|
||||
*/
|
||||
#define TEST_MIX_FRAMES 1024
|
||||
|
||||
/** @brief 441 Hz at 44100 frames per second is exactly 100 frames per cycle. */
|
||||
#define TEST_TONE_HZ 441.0f
|
||||
/** @brief Frames in one cycle of TEST_TONE_HZ. */
|
||||
#define TEST_TONE_PERIOD 100
|
||||
|
||||
/** @brief Somewhere to mix into. */
|
||||
static float32_t samples[TEST_MIX_FRAMES];
|
||||
|
||||
/** @brief Silence every voice and put the master level back. */
|
||||
static akerr_ErrorContext *reset_audio(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
|
||||
CATCH(errctx, akgl_audio_stop(i));
|
||||
CATCH(errctx, akgl_audio_waveform(i, AKGL_AUDIO_WAVE_SQUARE));
|
||||
CATCH(errctx, akgl_audio_envelope(i, 0, 0, 1.0f, 0));
|
||||
}
|
||||
CATCH(errctx, akgl_audio_volume(1.0f));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief Report the largest absolute sample in the first @p frames. */
|
||||
static float32_t peak_of(int frames)
|
||||
{
|
||||
float32_t peak = 0.0f;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < frames; i++ ) {
|
||||
if ( fabsf(samples[i]) > peak ) {
|
||||
peak = fabsf(samples[i]);
|
||||
}
|
||||
}
|
||||
return peak;
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_defaults(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
// Deliberately the first thing this file does, and deliberately without
|
||||
// a call to reset_audio(): a voice nobody has configured has to make a
|
||||
// sound. A zeroed voice would have a sustain of 0.0 and be silent, with
|
||||
// no error to say why, which is exactly the trap this defends against.
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
||||
"an unconfigured voice mixed to %f, expected a full-level square wave",
|
||||
samples[0]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[50], -1.0f,
|
||||
"an unconfigured voice is not a square wave (%f half a cycle in)",
|
||||
samples[50]);
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_silence(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool silent = true;
|
||||
bool active = true;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// With nothing sounding, the mixer produces silence rather than
|
||||
// whatever was left in the buffer.
|
||||
SDL_memset((void *)&samples, 0xff, sizeof(samples));
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES), "mixing an idle voice table");
|
||||
for ( i = 0; i < TEST_MIX_FRAMES; i++ ) {
|
||||
TEST_ASSERT_FLAG(silent, samples[i] == 0.0f);
|
||||
}
|
||||
TEST_ASSERT(errctx, silent == true, "an idle voice table did not mix to silence");
|
||||
|
||||
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
|
||||
CATCH(errctx, akgl_audio_voice_active(i, &active));
|
||||
TEST_ASSERT(errctx, active == false, "voice %d reports active with nothing playing", i);
|
||||
}
|
||||
|
||||
// Zero frames is a legal request that writes nothing.
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, 0), "mixing zero frames");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_audio_mix(NULL, 16),
|
||||
"mixing into a NULL destination");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_mix(samples, -1),
|
||||
"mixing a negative number of frames");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_square_tone(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool firsthalf = true;
|
||||
bool secondhalf = true;
|
||||
bool active = false;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// A square wave with a flat envelope is the one waveform whose every
|
||||
// sample is known exactly: +1 for the first half of each cycle, -1 for
|
||||
// the second.
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000), "sounding a tone");
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == true, "a sounded voice does not report itself active");
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES), "mixing a square wave");
|
||||
for ( i = 0; i < (TEST_TONE_PERIOD / 2); i++ ) {
|
||||
TEST_ASSERT_FLAG(firsthalf, samples[i] == 1.0f);
|
||||
}
|
||||
for ( i = (TEST_TONE_PERIOD / 2); i < TEST_TONE_PERIOD; i++ ) {
|
||||
TEST_ASSERT_FLAG(secondhalf, samples[i] == -1.0f);
|
||||
}
|
||||
TEST_ASSERT(errctx, firsthalf == true,
|
||||
"the first half cycle of a square wave is not at full positive level");
|
||||
TEST_ASSERT(errctx, secondhalf == true,
|
||||
"the second half cycle of a square wave is not at full negative level");
|
||||
|
||||
// The wave repeats: the second cycle matches the first.
|
||||
TEST_ASSERT(errctx, samples[TEST_TONE_PERIOD] == samples[0],
|
||||
"the wave did not repeat after one period");
|
||||
TEST_ASSERT(errctx, samples[TEST_TONE_PERIOD + 60] == samples[60],
|
||||
"the wave did not repeat after one period");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, 0.0f, 100),
|
||||
"sounding a tone at zero hertz");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, -100.0f, 100),
|
||||
"sounding a tone at a negative frequency");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, TEST_TONE_HZ, 0),
|
||||
"sounding a tone with no duration");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(-1, TEST_TONE_HZ, 100),
|
||||
"sounding a tone on a negative voice");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_tone(AKGL_AUDIO_MAX_VOICES, TEST_TONE_HZ, 100),
|
||||
"sounding a tone on a voice past the last one");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_waveforms(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool inrange = true;
|
||||
int i = 0;
|
||||
int w = 0;
|
||||
akgl_AudioWaveform shapes[4] = {
|
||||
AKGL_AUDIO_WAVE_TRIANGLE,
|
||||
AKGL_AUDIO_WAVE_SAWTOOTH,
|
||||
AKGL_AUDIO_WAVE_NOISE,
|
||||
AKGL_AUDIO_WAVE_SINE,
|
||||
};
|
||||
|
||||
ATTEMPT {
|
||||
// Every shape has to stay inside full scale and actually move. The
|
||||
// exact sample values differ per shape; these two properties do not.
|
||||
for ( w = 0; w < 4; w++ ) {
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_waveform(0, shapes[w]));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
for ( i = 0; i < TEST_MIX_FRAMES; i++ ) {
|
||||
TEST_ASSERT_FLAG(inrange, (samples[i] >= -1.0f) && (samples[i] <= 1.0f));
|
||||
}
|
||||
TEST_ASSERT_FLAG(inrange, peak_of(TEST_MIX_FRAMES) > 0.5f);
|
||||
}
|
||||
TEST_ASSERT(errctx, inrange == true,
|
||||
"a waveform either left full scale or produced nothing");
|
||||
|
||||
// The triangle is symmetric about the middle of its cycle, which the
|
||||
// square and sawtooth are not -- enough to tell it was really selected.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_waveform(0, AKGL_AUDIO_WAVE_TRIANGLE));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[25], 0.0f,
|
||||
"a triangle wave is at %f a quarter of the way up, expected 0",
|
||||
samples[25]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[50], 1.0f,
|
||||
"a triangle wave is at %f at its peak, expected 1", samples[50]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[75], 0.0f,
|
||||
"a triangle wave is at %f three quarters through, expected 0",
|
||||
samples[75]);
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_waveform(0, (akgl_AudioWaveform)(AKGL_AUDIO_WAVE_SINE + 1)),
|
||||
"selecting a waveform past the last one");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_waveform(AKGL_AUDIO_MAX_VOICES, AKGL_AUDIO_WAVE_SINE),
|
||||
"selecting a waveform on a voice past the last one");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_envelope(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool rising = true;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// A 10 ms attack at 44100 is 441 frames, so the level should climb from
|
||||
// nothing to full across the first 441 samples of a square wave and
|
||||
// then hold at the sustain level.
|
||||
CATCH(errctx, akgl_audio_envelope(0, 10, 0, 1.0f, 0));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 0.0f,
|
||||
"an attack does not start from silence (first sample %f)", samples[0]);
|
||||
TEST_ASSERT(errctx, fabsf(samples[100]) > fabsf(samples[10]),
|
||||
"the attack is not climbing (%f at frame 10, %f at frame 100)",
|
||||
samples[10], samples[100]);
|
||||
// Frames 0..49 are the positive half of the square, so their level is
|
||||
// the envelope value directly.
|
||||
for ( i = 1; i < 50; i++ ) {
|
||||
TEST_ASSERT_FLAG(rising, samples[i] > samples[i - 1]);
|
||||
}
|
||||
TEST_ASSERT(errctx, rising == true, "the attack ramp is not monotonic");
|
||||
|
||||
// Half a millisecond of decay to a half sustain level, no attack: the
|
||||
// very first sample is at full level and it settles at the sustain.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_envelope(0, 0, 10, 0.5f, 0));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
||||
"with no attack the first sample is %f, expected full level", samples[0]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[450], -0.5f,
|
||||
"after the decay the level is %f, expected the 0.5 sustain", samples[450]);
|
||||
|
||||
// Attack and decay together, which is the case where the decay has to
|
||||
// measure from the end of the attack rather than from the start of the
|
||||
// note. 10 ms of each is 441 frames of each at 44100.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_envelope(0, 10, 10, 0.5f, 0));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[441], 1.0f,
|
||||
"at the end of the attack the level is %f, expected full", samples[441]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[661], -(1.0f - (0.5f * (220.0f / 441.0f))),
|
||||
"halfway through the decay the level is %f, expected the halfway ramp",
|
||||
samples[661]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[900], 0.5f,
|
||||
"after the decay the level is %f, expected the 0.5 sustain", samples[900]);
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_envelope(0, 1, 1, -0.1f, 1),
|
||||
"setting a negative sustain level");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_envelope(0, 1, 1, 1.5f, 1),
|
||||
"setting a sustain level past full");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_envelope(AKGL_AUDIO_MAX_VOICES, 1, 1, 1.0f, 1),
|
||||
"setting an envelope on a voice past the last one");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_duration_and_release(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool active = false;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// A 10 ms gate is 441 frames. The voice is still sounding while they
|
||||
// are being generated and goes quiet on its own afterwards, without the
|
||||
// caller having to stop it.
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10));
|
||||
CATCH(errctx, akgl_audio_mix(samples, 440));
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == true, "the voice went quiet before its gate closed");
|
||||
|
||||
CATCH(errctx, akgl_audio_mix(samples, 8));
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == false, "the voice is still sounding past its gate");
|
||||
CATCH(errctx, akgl_audio_mix(samples, 64));
|
||||
TEST_ASSERT_FEQ(errctx, peak_of(64), 0.0f,
|
||||
"a finished voice is still producing sound (peak %f)", peak_of(64));
|
||||
|
||||
// With a release, the voice outlives its gate and fades rather than
|
||||
// stopping dead.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_envelope(0, 0, 0, 1.0f, 10));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10));
|
||||
CATCH(errctx, akgl_audio_mix(samples, 441));
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == true, "a voice with a release stopped when its gate closed");
|
||||
// The release starts from the level the gate left off at and falls from
|
||||
// there, so the end of this window has to be quieter than its start.
|
||||
CATCH(errctx, akgl_audio_mix(samples, 220));
|
||||
TEST_ASSERT(errctx, fabsf(samples[219]) < fabsf(samples[0]),
|
||||
"the release is not attenuating (%f at its start, %f 220 frames later)",
|
||||
samples[0], samples[219]);
|
||||
TEST_ASSERT(errctx, peak_of(220) > 0.0f, "the release went silent immediately");
|
||||
CATCH(errctx, akgl_audio_mix(samples, 250));
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == false, "the voice outlived its gate and its release");
|
||||
|
||||
// Stopping cuts a voice off where it stands, release and all.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_envelope(0, 0, 0, 1.0f, 1000));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, 64));
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_stop(0), "stopping a sounding voice");
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == false, "a stopped voice still reports itself active");
|
||||
CATCH(errctx, akgl_audio_mix(samples, 64));
|
||||
TEST_ASSERT_FEQ(errctx, peak_of(64), 0.0f,
|
||||
"a stopped voice is still producing sound (peak %f)", peak_of(64));
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_stop(AKGL_AUDIO_MAX_VOICES),
|
||||
"stopping a voice past the last one");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_voice_active(AKGL_AUDIO_MAX_VOICES, &active),
|
||||
"asking about a voice past the last one");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_audio_voice_active(0, NULL),
|
||||
"asking about a voice with nowhere to put the answer");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief Mix @p ticks whole sweep steps, one step per call. */
|
||||
static akerr_ErrorContext *mix_sweep_ticks(int ticks)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
for ( i = 0; i < ticks; i++ ) {
|
||||
CATCH(errctx, akgl_audio_mix(samples, AKGL_AUDIO_SWEEP_TICK_FRAMES));
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_sweep(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool active = false;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// 400 Hz climbing to 500 in 25 Hz steps: four steps, at 60 steps per
|
||||
// second. The pitch is advanced by the mixer's own frame counter, so
|
||||
// these assertions are about exact frames rather than about how often
|
||||
// the caller happened to ask for samples.
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 400.0f, 500.0f, 25.0f, 1000),
|
||||
"sounding a rising sweep");
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
|
||||
"a sweep starts at %f, expected its start frequency",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
// One tick's worth of frames is not yet a step: the last frame of that
|
||||
// window is still the first tick.
|
||||
CATCH(errctx, mix_sweep_ticks(1));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
|
||||
"the pitch moved at %f before the first step was due",
|
||||
akgl_audio_voices[0].hz);
|
||||
TEST_ASSERT(errctx, peak_of(AKGL_AUDIO_SWEEP_TICK_FRAMES) > 0.5f,
|
||||
"a sweep is not making a sound (peak %f)",
|
||||
peak_of(AKGL_AUDIO_SWEEP_TICK_FRAMES));
|
||||
|
||||
// The frame after it is.
|
||||
CATCH(errctx, akgl_audio_mix(samples, 1));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 425.0f,
|
||||
"one step in, the pitch is %f, expected 425",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
// It stops when it arrives rather than running past the target.
|
||||
CATCH(errctx, mix_sweep_ticks(8));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 500.0f,
|
||||
"a finished sweep sits at %f, expected its target 500",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
// Downward is the same sweep with the frequencies the other way round.
|
||||
// The step stays positive; the direction is not its sign.
|
||||
CATCH(errctx, reset_audio());
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 500.0f, 400.0f, 25.0f, 1000),
|
||||
"sounding a falling sweep");
|
||||
CATCH(errctx, mix_sweep_ticks(1));
|
||||
CATCH(errctx, akgl_audio_mix(samples, 1));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 475.0f,
|
||||
"one step down, the pitch is %f, expected 475",
|
||||
akgl_audio_voices[0].hz);
|
||||
CATCH(errctx, mix_sweep_ticks(8));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
|
||||
"a finished falling sweep sits at %f, expected its target 400",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
// Equal frequencies are a held tone, not an error: a caller translating
|
||||
// a statement that computes its own limits does not have to special
|
||||
// case them.
|
||||
CATCH(errctx, reset_audio());
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 440.0f, 440.0f, 10.0f, 1000),
|
||||
"sweeping between two identical frequencies");
|
||||
CATCH(errctx, mix_sweep_ticks(4));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 440.0f,
|
||||
"a sweep with nowhere to go moved to %f", akgl_audio_voices[0].hz);
|
||||
|
||||
// The gate still governs: a sweep with further to go than the note lasts
|
||||
// ends with the note, part way up. 30 ms is 1323 frames, which is one
|
||||
// whole step and most of a second.
|
||||
CATCH(errctx, reset_audio());
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 400.0f, 4000.0f, 25.0f, 30),
|
||||
"sounding a sweep longer than its gate");
|
||||
CATCH(errctx, mix_sweep_ticks(3));
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == false, "a swept voice outlived its gate");
|
||||
TEST_ASSERT(errctx, akgl_audio_voices[0].hz > 400.0f,
|
||||
"a sweep cut off by its gate never stepped at all (%f)",
|
||||
akgl_audio_voices[0].hz);
|
||||
TEST_ASSERT(errctx, akgl_audio_voices[0].hz < 4000.0f,
|
||||
"a sweep cut off by its gate reached %f anyway",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
// A voice reused for a plain tone stops sweeping.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_sweep(0, 400.0f, 800.0f, 25.0f, 1000));
|
||||
CATCH(errctx, mix_sweep_ticks(2));
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000),
|
||||
"sounding a plain tone on a voice that was sweeping");
|
||||
CATCH(errctx, mix_sweep_ticks(4));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, TEST_TONE_HZ,
|
||||
"a plain tone on a swept voice drifted to %f",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(AKGL_AUDIO_MAX_VOICES, 400.0f, 500.0f, 25.0f, 100),
|
||||
"sweeping a voice past the last one");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(0, 0.0f, 500.0f, 25.0f, 100),
|
||||
"sweeping from zero hertz");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(0, 400.0f, -1.0f, 25.0f, 100),
|
||||
"sweeping to a negative frequency");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(0, 400.0f, 500.0f, 0.0f, 100),
|
||||
"sweeping with no step");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(0, 500.0f, 400.0f, -25.0f, 100),
|
||||
"sweeping down with a negative step");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(0, 400.0f, 500.0f, 25.0f, 0),
|
||||
"sweeping with no duration");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_volume_and_mixing(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// The master level scales everything.
|
||||
CATCH(errctx, akgl_audio_volume(0.25f));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 0.25f,
|
||||
"at a quarter volume the first sample is %f, expected 0.25", samples[0]);
|
||||
|
||||
CATCH(errctx, akgl_audio_volume(0.0f));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, peak_of(TEST_MIX_FRAMES), 0.0f,
|
||||
"at zero volume the peak is %f, expected silence",
|
||||
peak_of(TEST_MIX_FRAMES));
|
||||
|
||||
// Voices sum, and the sum is clamped rather than wrapped: three square
|
||||
// waves in phase at full level would be 3.0 without the clamp.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_tone(1, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_tone(2, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
||||
"three voices in phase mixed to %f, expected a clamp at 1.0", samples[0]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[50], -1.0f,
|
||||
"three voices in phase mixed to %f, expected a clamp at -1.0", samples[50]);
|
||||
|
||||
// Two voices at different levels sum to their total rather than to
|
||||
// either one of them, which is only visible below the clamp.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_volume(0.5f));
|
||||
CATCH(errctx, akgl_audio_envelope(1, 0, 0, 0.5f, 0));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_tone(1, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 0.75f,
|
||||
"a full voice and a half voice at half volume mixed to %f, expected 0.75",
|
||||
samples[0]);
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_volume(-0.1f),
|
||||
"setting a negative master volume");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_volume(1.1f),
|
||||
"setting a master volume past full");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_device(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool active = true;
|
||||
|
||||
ATTEMPT {
|
||||
// Everything above ran without a device. This is the one test that
|
||||
// opens one, so nothing that asserts on voice state runs after it.
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_init(), "opening an audio device");
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_init(), "opening an audio device a second time");
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10), "sounding a tone on a device");
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_shutdown(), "closing the audio device");
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == false, "shutting down left a voice sounding");
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_shutdown(), "closing an audio device that is not open");
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_audio_shutdown());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_Init(SDL_INIT_AUDIO),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize SDL: %s",
|
||||
SDL_GetError());
|
||||
|
||||
CATCH(errctx, test_audio_defaults());
|
||||
CATCH(errctx, test_audio_silence());
|
||||
CATCH(errctx, test_audio_square_tone());
|
||||
CATCH(errctx, test_audio_waveforms());
|
||||
CATCH(errctx, test_audio_envelope());
|
||||
CATCH(errctx, test_audio_duration_and_release());
|
||||
CATCH(errctx, test_audio_sweep());
|
||||
CATCH(errctx, test_audio_volume_and_mixing());
|
||||
CATCH(errctx, test_audio_device());
|
||||
} CLEANUP {
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
336
tests/benchutil.h
Normal file
336
tests/benchutil.h
Normal file
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* @file benchutil.h
|
||||
* @brief The timing harness behind the perf suites: run it, record it, report it, hold it to a budget.
|
||||
*
|
||||
* A benchmark here is a timed region: bench_start() takes a timestamp,
|
||||
* bench_stop() takes another and divides by the number of units of work that
|
||||
* happened in between. The unit is whatever the measurement is *about* -- one
|
||||
* call, one actor, one tile, one frame -- and it is printed alongside the
|
||||
* number so nobody has to guess what "42 ns" was 42 nanoseconds of.
|
||||
*
|
||||
* Two things make the numbers usable rather than merely present:
|
||||
*
|
||||
* 1. **Best of #AKGL_BENCH_REPETITIONS.** Calling bench_start() again with a
|
||||
* name already in the table folds the new run into the old entry and keeps
|
||||
* the *lowest* ns/op seen. A benchmark loop therefore repeats itself and the
|
||||
* harness reports the run that was interrupted least. The mean is the wrong
|
||||
* statistic here: every source of noise on a shared machine makes a run
|
||||
* slower and none makes it faster.
|
||||
* 2. **The timed region contains no error checking.** `PASS` and `CATCH` both
|
||||
* call `akerr_valid_error_address`, which walks `AKERR_ARRAY_ERROR` looking
|
||||
* for a match -- more work than several of the calls being measured. Use
|
||||
* #BENCH_LOOP, which stashes the context and stops at the first failure, and
|
||||
* hand it to `PASS` after the clock has stopped.
|
||||
*
|
||||
* A budget is a per-op ceiling in nanoseconds, and a benchmark that exceeds it
|
||||
* fails the suite. Budgets are set at roughly ten times the measured baseline:
|
||||
* loose enough that a busy machine does not turn CI red, tight enough that an
|
||||
* algorithmic regression -- a linear scan that becomes quadratic, a per-frame
|
||||
* allocation that becomes a per-actor one -- cannot hide. They are enforced
|
||||
* only in an optimized build at full scale; a coverage build measures the
|
||||
* instrumentation, not the library, and reports without judging.
|
||||
*
|
||||
* Set `AKGL_BENCH_SCALE` in the environment to change how long the suite runs:
|
||||
* 0.1 for a tenth of the iterations, 10 for ten times as many. Below 1.0 the
|
||||
* budgets are not enforced, because a short run is a noisy one.
|
||||
*
|
||||
* **Under valgrind these suites become the memory-check suites**, and the scale
|
||||
* drops itself to #AKGL_BENCH_VALGRIND_SCALE without being asked. That is the
|
||||
* whole reason `ctest -T memcheck` does not need benchmark programs of its own:
|
||||
* a leak check wants every path walked *once*, and a benchmark is a program that
|
||||
* walks one path a hundred thousand times. Divide the iteration counts by two
|
||||
* thousand and the same binary is exactly the right shape for memcheck --
|
||||
* broader coverage of asset loading, drawing, and the frame loop than any unit
|
||||
* suite, at a runtime valgrind can survive. Timings from such a run are
|
||||
* meaningless and the report says so.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_BENCHUTIL_H_
|
||||
#define _AKGL_BENCHUTIL_H_
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
/** @brief How many benchmarks one suite can record. */
|
||||
#define AKGL_BENCH_MAX_RESULTS 48
|
||||
/** @brief Longest benchmark name kept, including the terminator. */
|
||||
#define AKGL_BENCH_MAX_NAME 56
|
||||
/** @brief Longest unit name kept, including the terminator. */
|
||||
#define AKGL_BENCH_MAX_UNIT 12
|
||||
/** @brief How many times a benchmark is repeated before the best run is reported. */
|
||||
#define AKGL_BENCH_REPETITIONS 5
|
||||
/** @brief Environment variable scaling every iteration count. */
|
||||
#define AKGL_BENCH_SCALE_ENV "AKGL_BENCH_SCALE"
|
||||
/** @brief Scale forced on when running under valgrind. Enough to walk every path, few enough to finish. */
|
||||
#define AKGL_BENCH_VALGRIND_SCALE 0.0005
|
||||
|
||||
/** @brief One row of the report: what was measured, how fast, and what it was allowed to cost. */
|
||||
typedef struct akgl_Benchmark {
|
||||
char name[AKGL_BENCH_MAX_NAME]; /**< What was measured. Also the key runs are merged under. */
|
||||
char unit[AKGL_BENCH_MAX_UNIT]; /**< What one op is: "call", "actor", "tile", "frame". */
|
||||
double budget_ns; /**< Per-op ceiling in nanoseconds. 0 reports without judging. */
|
||||
double best_ns; /**< Lowest ns/op seen across every run under this name. */
|
||||
uint64_t ops; /**< Units of work in the run that produced best_ns. */
|
||||
uint64_t elapsed_ns; /**< Wall time of that run. */
|
||||
int runs; /**< How many runs were folded in. */
|
||||
} akgl_Benchmark;
|
||||
|
||||
/** @brief Every benchmark this suite has recorded, in the order it first ran. */
|
||||
static akgl_Benchmark bench_results[AKGL_BENCH_MAX_RESULTS];
|
||||
/** @brief How many entries of #bench_results are in use. */
|
||||
static int bench_result_count = 0;
|
||||
/** @brief The entry bench_stop() will write to, claimed by bench_start(). */
|
||||
static akgl_Benchmark *bench_running = NULL;
|
||||
/** @brief `SDL_GetTicksNS()` at the last bench_start(). */
|
||||
static uint64_t bench_started_ns = 0;
|
||||
|
||||
/**
|
||||
* @brief Report whether this process is running under valgrind.
|
||||
*
|
||||
* Detected from `LD_PRELOAD`, which valgrind fills with its own
|
||||
* `vgpreload_*.so` before handing the process over. That is a deliberate choice
|
||||
* over `RUNNING_ON_VALGRIND` from `valgrind/valgrind.h`: the macro is exact, but
|
||||
* it makes the test suite fail to compile anywhere the valgrind headers are not
|
||||
* installed, and a benchmark is not worth a build dependency. The cost of the
|
||||
* check being wrong is a slow run or a fast one, never a wrong answer.
|
||||
*/
|
||||
static bool bench_under_valgrind(void)
|
||||
{
|
||||
static int detected = -1;
|
||||
const char *preload = NULL;
|
||||
|
||||
if ( detected >= 0 ) {
|
||||
return ( detected == 1 );
|
||||
}
|
||||
detected = 0;
|
||||
preload = SDL_getenv("LD_PRELOAD");
|
||||
if ( preload != NULL && strstr(preload, "vgpreload") != NULL ) {
|
||||
detected = 1;
|
||||
}
|
||||
return ( detected == 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Multiplier applied to every iteration count, from `AKGL_BENCH_SCALE`.
|
||||
*
|
||||
* Read once and cached. Anything unparseable, negative, or absent gives 1.0.
|
||||
* Under valgrind the result is capped at #AKGL_BENCH_VALGRIND_SCALE -- the
|
||||
* smaller of the two wins, so asking for an even shorter run still works.
|
||||
*/
|
||||
static double bench_scale(void)
|
||||
{
|
||||
static double scale = -1.0;
|
||||
const char *env = NULL;
|
||||
|
||||
if ( scale >= 0.0 ) {
|
||||
return scale;
|
||||
}
|
||||
scale = 1.0;
|
||||
env = SDL_getenv(AKGL_BENCH_SCALE_ENV);
|
||||
if ( env != NULL ) {
|
||||
scale = SDL_atof(env);
|
||||
if ( scale <= 0.0 ) {
|
||||
scale = 1.0;
|
||||
}
|
||||
}
|
||||
if ( bench_under_valgrind() == true && scale > AKGL_BENCH_VALGRIND_SCALE ) {
|
||||
scale = AKGL_BENCH_VALGRIND_SCALE;
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Scale a nominal iteration count, never down to zero.
|
||||
* @param count The count the benchmark was written for.
|
||||
* @return @p count times the scale factor, at least 1.
|
||||
*/
|
||||
static int bench_iterations(int count)
|
||||
{
|
||||
int scaled = (int)((double)count * bench_scale());
|
||||
|
||||
if ( scaled < 1 ) {
|
||||
scaled = 1;
|
||||
}
|
||||
return scaled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Report whether budgets are being enforced in this run.
|
||||
*
|
||||
* An unoptimized build measures the instrumentation rather than the library, a
|
||||
* scaled-down run is too short to trust, and a run under valgrind is measuring
|
||||
* valgrind. All three report without failing.
|
||||
*/
|
||||
static bool bench_budgets_enforced(void)
|
||||
{
|
||||
if ( bench_under_valgrind() == true ) {
|
||||
return false;
|
||||
}
|
||||
#ifdef __OPTIMIZE__
|
||||
return ( bench_scale() >= 1.0 );
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start timing, claiming or reusing the table entry named @p name.
|
||||
*
|
||||
* @param name What is being measured. Truncated at #AKGL_BENCH_MAX_NAME.
|
||||
* Reusing a name folds this run into that entry.
|
||||
* @param unit What one op is. Truncated at #AKGL_BENCH_MAX_UNIT.
|
||||
* @param budget_ns Per-op ceiling in nanoseconds; 0 to report only. The value
|
||||
* from the first run under a name wins.
|
||||
*
|
||||
* @note The table is fixed. Past #AKGL_BENCH_MAX_RESULTS entries the run is
|
||||
* dropped with a message rather than overwriting somebody else's row.
|
||||
*/
|
||||
static void bench_start(char *name, char *unit, double budget_ns)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
bench_running = NULL;
|
||||
for ( i = 0; i < bench_result_count; i++ ) {
|
||||
if ( strncmp(bench_results[i].name, name, AKGL_BENCH_MAX_NAME - 1) == 0 ) {
|
||||
bench_running = &bench_results[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( bench_running == NULL ) {
|
||||
if ( bench_result_count >= AKGL_BENCH_MAX_RESULTS ) {
|
||||
SDL_Log("benchutil: no room for benchmark '%s', raise AKGL_BENCH_MAX_RESULTS", name);
|
||||
return;
|
||||
}
|
||||
bench_running = &bench_results[bench_result_count];
|
||||
bench_result_count += 1;
|
||||
memset(bench_running, 0x00, sizeof(akgl_Benchmark));
|
||||
strncpy(bench_running->name, name, AKGL_BENCH_MAX_NAME - 1);
|
||||
strncpy(bench_running->unit, unit, AKGL_BENCH_MAX_UNIT - 1);
|
||||
bench_running->budget_ns = budget_ns;
|
||||
bench_running->best_ns = -1.0;
|
||||
}
|
||||
bench_started_ns = SDL_GetTicksNS();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop timing and keep the run if it beat every previous one.
|
||||
* @param ops Units of work done since bench_start(). Zero is ignored -- a
|
||||
* benchmark that did nothing has no rate to report.
|
||||
*/
|
||||
static void bench_stop(uint64_t ops)
|
||||
{
|
||||
uint64_t elapsed = SDL_GetTicksNS() - bench_started_ns;
|
||||
double per_op = 0.0;
|
||||
|
||||
if ( bench_running == NULL || ops == 0 ) {
|
||||
return;
|
||||
}
|
||||
per_op = (double)elapsed / (double)ops;
|
||||
bench_running->runs += 1;
|
||||
if ( bench_running->best_ns < 0.0 || per_op < bench_running->best_ns ) {
|
||||
bench_running->best_ns = per_op;
|
||||
bench_running->ops = ops;
|
||||
bench_running->elapsed_ns = elapsed;
|
||||
}
|
||||
bench_running = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run @p stmt @p count times, stopping at the first error.
|
||||
*
|
||||
* The timed loop deliberately holds no `PASS` or `CATCH`: their validity check
|
||||
* walks the whole error array and costs more than some of the calls being
|
||||
* measured. Hand @p errvar to `PASS` once the clock has stopped.
|
||||
*
|
||||
* @param errvar Receives the first non-`NULL` context returned, or `NULL`.
|
||||
* @param i Loop variable, declared by the caller.
|
||||
* @param count How many times to run @p stmt.
|
||||
* @param stmt The call under test. Must evaluate to an `akerr_ErrorContext *`.
|
||||
*/
|
||||
#define BENCH_LOOP(errvar, i, count, stmt) \
|
||||
for ( i = 0; i < (count); i++ ) { \
|
||||
errvar = (stmt); \
|
||||
if ( errvar != NULL ) { \
|
||||
break; \
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print the recorded benchmarks as a table and count the ones over budget.
|
||||
*
|
||||
* The table goes to stdout so `ctest --output-on-failure` and a plain run of the
|
||||
* executable both show it. Rates are printed as ops per second, which is the
|
||||
* number a frame budget is actually built from.
|
||||
*
|
||||
* @return How many benchmarks exceeded their budget. Always 0 when
|
||||
* bench_budgets_enforced() is false.
|
||||
*/
|
||||
static int bench_report(void)
|
||||
{
|
||||
int i = 0;
|
||||
int over = 0;
|
||||
bool enforced = bench_budgets_enforced();
|
||||
char *verdict = NULL;
|
||||
|
||||
printf("\n");
|
||||
if ( bench_under_valgrind() == true ) {
|
||||
printf("running under valgrind: this is a memory check, not a measurement.\n");
|
||||
printf("The timings below are valgrind's and mean nothing about libakgl.\n");
|
||||
}
|
||||
printf("scale %.4gx, best of %d runs, budgets %s\n",
|
||||
bench_scale(),
|
||||
AKGL_BENCH_REPETITIONS,
|
||||
enforced ? "enforced" : "reported only");
|
||||
printf("%-54s %-6s %9s %12s %16s %10s %s\n",
|
||||
"benchmark", "unit", "ops", "ns/op", "ops/sec", "budget", "verdict");
|
||||
printf("%-54s %-6s %9s %12s %16s %10s %s\n",
|
||||
"------------------------------------------------------",
|
||||
"------", "---------", "------------", "----------------", "----------", "-------");
|
||||
for ( i = 0; i < bench_result_count; i++ ) {
|
||||
verdict = "-";
|
||||
if ( bench_results[i].budget_ns > 0.0 ) {
|
||||
if ( bench_results[i].best_ns > bench_results[i].budget_ns ) {
|
||||
verdict = enforced ? "OVER" : "over";
|
||||
if ( enforced ) {
|
||||
over += 1;
|
||||
}
|
||||
} else {
|
||||
verdict = "ok";
|
||||
}
|
||||
}
|
||||
printf("%-54s %-6s %9llu %12.1f %16.0f %10.0f %s\n",
|
||||
bench_results[i].name,
|
||||
bench_results[i].unit,
|
||||
(unsigned long long)bench_results[i].ops,
|
||||
bench_results[i].best_ns,
|
||||
( bench_results[i].best_ns > 0.0 ) ? (1000000000.0 / bench_results[i].best_ns) : 0.0,
|
||||
bench_results[i].budget_ns,
|
||||
verdict);
|
||||
}
|
||||
printf("\n");
|
||||
fflush(stdout);
|
||||
return over;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fail the enclosing ATTEMPT block if any benchmark blew its budget.
|
||||
*
|
||||
* The report is printed either way -- a run that fails still has to say what the
|
||||
* numbers were.
|
||||
*/
|
||||
#define BENCH_REPORT_BREAK(e) \
|
||||
{ \
|
||||
int __bench_over = bench_report(); \
|
||||
if ( __bench_over > 0 ) { \
|
||||
FAIL_BREAK( \
|
||||
e, \
|
||||
AKGL_ERR_BEHAVIOR, \
|
||||
"%d benchmark(s) exceeded their budget", \
|
||||
__bench_over); \
|
||||
} \
|
||||
}
|
||||
|
||||
#endif // _AKGL_BENCHUTIL_H_
|
||||
@@ -1,31 +1,31 @@
|
||||
#include <sdl3game/game.h>
|
||||
#include <sdl3game/actor.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/actor.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int mask = 0;
|
||||
BITMASK_ADD(mask, ACTOR_STATE_ALIVE);
|
||||
if ( mask != ACTOR_STATE_ALIVE )
|
||||
return 1;
|
||||
BITMASK_ADD(mask, ACTOR_STATE_FACE_LEFT);
|
||||
if ( mask != (ACTOR_STATE_ALIVE | ACTOR_STATE_FACE_LEFT) )
|
||||
return 1;
|
||||
BITMASK_DEL(mask, ACTOR_STATE_ALIVE);
|
||||
if ( mask != (ACTOR_STATE_FACE_LEFT) )
|
||||
return 1;
|
||||
BITMASK_CLEAR(mask);
|
||||
if ( mask != 0 )
|
||||
return 1;
|
||||
BITMASK_ADD(mask, ACTOR_STATE_FACE_LEFT);
|
||||
if ( !(BITMASK_HAS(mask, ACTOR_STATE_FACE_LEFT)) )
|
||||
return 1;
|
||||
mask = ACTOR_STATE_ALIVE | ACTOR_STATE_FACE_UP;
|
||||
BITMASK_DEL(mask, ACTOR_STATE_FACE_ALL);
|
||||
if ( mask != ACTOR_STATE_ALIVE )
|
||||
return 1;
|
||||
BITMASK_ADD(mask, ACTOR_STATE_MOVING_DOWN);
|
||||
BITMASK_ADD(mask, ACTOR_STATE_FACE_DOWN);
|
||||
if ( mask != (ACTOR_STATE_ALIVE | ACTOR_STATE_MOVING_DOWN | ACTOR_STATE_FACE_DOWN) )
|
||||
return 1;
|
||||
return 0;
|
||||
int mask = 0;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_ALIVE);
|
||||
if ( mask != AKGL_ACTOR_STATE_ALIVE )
|
||||
return 1;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT) )
|
||||
return 1;
|
||||
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_ALIVE);
|
||||
if ( mask != (AKGL_ACTOR_STATE_FACE_LEFT) )
|
||||
return 1;
|
||||
AKGL_BITMASK_CLEAR(mask);
|
||||
if ( mask != 0 )
|
||||
return 1;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
if ( !(AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_FACE_LEFT)) )
|
||||
return 1;
|
||||
mask = AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP;
|
||||
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
if ( mask != AKGL_ACTOR_STATE_ALIVE )
|
||||
return 1;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_MOVING_DOWN);
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_DOWN);
|
||||
if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN) )
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,186 +1,189 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <sdlerror.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <sdl3game/character.h>
|
||||
#include <sdl3game/actor.h>
|
||||
#include <sdl3game/heap.h>
|
||||
#include <sdl3game/registry.h>
|
||||
#include <sdl3game/iterator.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/iterator.h>
|
||||
|
||||
SDL_Window *window;
|
||||
SDL_Renderer *renderer;
|
||||
|
||||
ErrorContext *test_character_initialize()
|
||||
akerr_ErrorContext *test_akgl_character_initialize()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
character *testchar = NULL;
|
||||
akgl_Character *testchar = NULL;
|
||||
ATTEMPT {
|
||||
CATCH(errctx, heap_next_character(&testchar));
|
||||
CATCH(errctx, character_initialize(testchar, "testchar"));
|
||||
CATCH(errctx, akgl_heap_next_character(&testchar));
|
||||
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_GetPointerProperty(REGISTRY_CHARACTER, "testchar", NULL),
|
||||
ERR_KEY,
|
||||
SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testchar", NULL),
|
||||
AKERR_KEY,
|
||||
"Character was not placed in the registry");
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
strcmp((char *)&testchar->name, "testchar"),
|
||||
ERR_VALUE,
|
||||
AKERR_VALUE,
|
||||
"Character was not named properly ('testchar' vs '%s')",
|
||||
(char *)&testchar->name);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testchar->state_sprites,
|
||||
ERR_NULLPOINTER,
|
||||
AKERR_NULLPOINTER,
|
||||
"Character state sprites map was not initialized");
|
||||
} CLEANUP {
|
||||
IGNORE(heap_release_character(testchar));
|
||||
IGNORE(akgl_heap_release_character(testchar));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *test_character_sprite_mgmt()
|
||||
akerr_ErrorContext *test_character_sprite_mgmt()
|
||||
{
|
||||
character *testchar = NULL;
|
||||
sprite *testsprite = NULL;
|
||||
sprite *testsprite2 = NULL;
|
||||
sprite *comparesprite = NULL;
|
||||
akgl_Character *testchar = NULL;
|
||||
akgl_Sprite *testsprite = NULL;
|
||||
akgl_Sprite *testsprite2 = NULL;
|
||||
akgl_Sprite *comparesprite = NULL;
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, heap_next_character(&testchar));
|
||||
CATCH(errctx, sprite_load_json("assets/testsprite.json"));
|
||||
testsprite = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite", NULL);
|
||||
CATCH(errctx, akgl_heap_next_character(&testchar));
|
||||
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
||||
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testsprite,
|
||||
ERR_KEY,
|
||||
AKERR_KEY,
|
||||
"Sprite loaded from json but not in registry");
|
||||
CATCH(errctx, sprite_load_json("assets/testsprite2.json"));
|
||||
testsprite2 = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite2", NULL);
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
||||
testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testsprite2,
|
||||
ERR_KEY,
|
||||
AKERR_KEY,
|
||||
"Sprite 2 loaded from json but not in registry");
|
||||
CATCH(errctx, testchar->sprite_add(testchar, testsprite, ACTOR_STATE_ALIVE));
|
||||
CATCH(errctx, testchar->sprite_add(testchar, testsprite2, ACTOR_STATE_DEAD));
|
||||
CATCH(errctx, testchar->sprite_get(testchar, (ACTOR_STATE_ALIVE), &comparesprite));
|
||||
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), ERR_VALUE, "Wrong sprite for state ACTOR_STATE_ALIVE | ACTOR_STATE_FACE_LEFT");
|
||||
CATCH(errctx, testchar->sprite_get(testchar, ACTOR_STATE_DEAD, &comparesprite));
|
||||
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), ERR_VALUE, "Wrong sprite for state ACTOR_STATE_DEAD");
|
||||
CATCH(errctx, testchar->sprite_add(testchar, testsprite, AKGL_ACTOR_STATE_ALIVE));
|
||||
CATCH(errctx, testchar->sprite_add(testchar, testsprite2, AKGL_ACTOR_STATE_DEAD));
|
||||
CATCH(errctx, testchar->sprite_get(testchar, (AKGL_ACTOR_STATE_ALIVE), &comparesprite));
|
||||
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT");
|
||||
CATCH(errctx, testchar->sprite_get(testchar, AKGL_ACTOR_STATE_DEAD, &comparesprite));
|
||||
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_DEAD");
|
||||
} CLEANUP {
|
||||
IGNORE(heap_release_sprite(testsprite));
|
||||
IGNORE(heap_release_sprite(testsprite2));
|
||||
IGNORE(heap_release_character(testchar));
|
||||
IGNORE(akgl_heap_release_sprite(testsprite));
|
||||
IGNORE(akgl_heap_release_sprite(testsprite2));
|
||||
IGNORE(akgl_heap_release_character(testchar));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *test_character_iterate_state_sprites()
|
||||
akerr_ErrorContext *test_character_iterate_state_sprites()
|
||||
{
|
||||
character *testchar = NULL;
|
||||
sprite *testsprite = NULL;
|
||||
sprite *testsprite2 = NULL;
|
||||
iterator opflags = {.flags = ITERATOR_OP_RELEASE, .layerid = 0};
|
||||
|
||||
akgl_Character *testchar = NULL;
|
||||
akgl_Sprite *testsprite = NULL;
|
||||
akgl_Sprite *testsprite2 = NULL;
|
||||
akgl_Iterator opflags = {.flags = AKGL_ITERATOR_OP_RELEASE, .layerid = 0};
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, heap_next_character(&testchar));
|
||||
CATCH(errctx, sprite_load_json("assets/testsprite.json"));
|
||||
testsprite = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite", NULL);
|
||||
CATCH(errctx, akgl_heap_next_character(&testchar));
|
||||
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
||||
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testsprite,
|
||||
ERR_KEY,
|
||||
AKERR_KEY,
|
||||
"Sprite loaded from json but not in registry");
|
||||
CATCH(errctx, sprite_load_json("assets/testsprite2.json"));
|
||||
testsprite2 = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite2", NULL);
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
||||
testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testsprite2,
|
||||
ERR_KEY,
|
||||
AKERR_KEY,
|
||||
"Sprite 2 loaded from json but not in registry");
|
||||
CATCH(errctx, testchar->sprite_add(testchar, testsprite, ACTOR_STATE_ALIVE));
|
||||
CATCH(errctx, testchar->sprite_add(testchar, testsprite2, ACTOR_STATE_DEAD));
|
||||
CATCH(errctx, testchar->sprite_add(testchar, testsprite, AKGL_ACTOR_STATE_ALIVE));
|
||||
CATCH(errctx, testchar->sprite_add(testchar, testsprite2, AKGL_ACTOR_STATE_DEAD));
|
||||
|
||||
SDL_EnumerateProperties(testchar->state_sprites, &character_state_sprites_iterate, &opflags);
|
||||
SDL_EnumerateProperties(testchar->state_sprites, &akgl_character_state_sprites_iterate, &opflags);
|
||||
|
||||
// This is called by heap_release_character so we should assume that our property map is being
|
||||
// This is called by akgl_heap_release_character so we should assume that our property map is being
|
||||
// deleted soon after this. So we don't care if the sprites have been deleted from the sprite_states.
|
||||
// We just want to know they've been released.
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
(testsprite->refcount > 1),
|
||||
ERR_VALUE,
|
||||
"heap_release_sprite not called for testsprite from iterator");
|
||||
AKERR_VALUE,
|
||||
"akgl_heap_release_sprite not called for testsprite from iterator");
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
(testsprite2->refcount > 1),
|
||||
ERR_VALUE,
|
||||
"heap_release_sprite not called for testsprite from iterator");
|
||||
AKERR_VALUE,
|
||||
"akgl_heap_release_sprite not called for testsprite from iterator");
|
||||
} CLEANUP {
|
||||
IGNORE(heap_release_sprite(testsprite));
|
||||
IGNORE(heap_release_sprite(testsprite2));
|
||||
IGNORE(heap_release_character(testchar));
|
||||
IGNORE(akgl_heap_release_sprite(testsprite));
|
||||
IGNORE(akgl_heap_release_sprite(testsprite2));
|
||||
IGNORE(akgl_heap_release_character(testchar));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *test_character_load_json()
|
||||
akerr_ErrorContext *test_akgl_character_load_json()
|
||||
{
|
||||
character *testcharacter = NULL;
|
||||
sprite *testsprite = NULL;
|
||||
sprite *testsprite2 = NULL;
|
||||
sprite *comparesprite = NULL;
|
||||
akgl_Character *testcharacter = NULL;
|
||||
akgl_Sprite *testsprite = NULL;
|
||||
akgl_Sprite *testsprite2 = NULL;
|
||||
akgl_Sprite *comparesprite = NULL;
|
||||
int tsrc = 0;
|
||||
int tsrc2 = 0;
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, heap_next_character(&testcharacter));
|
||||
CATCH(errctx, sprite_load_json("assets/testsprite.json"));
|
||||
testsprite = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite", NULL);
|
||||
CATCH(errctx, akgl_heap_next_character(&testcharacter));
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
||||
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testsprite,
|
||||
ERR_KEY,
|
||||
AKERR_KEY,
|
||||
"Sprite loaded from json but not in registry");
|
||||
CATCH(errctx, sprite_load_json("assets/testsprite2.json"));
|
||||
testsprite2 = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite2", NULL);
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
||||
testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testsprite2,
|
||||
ERR_KEY,
|
||||
AKERR_KEY,
|
||||
"Sprite 2 loaded from json but not in registry");
|
||||
|
||||
CATCH(errctx, character_load_json("assets/testcharacter.json"));
|
||||
testcharacter = SDL_GetPointerProperty(REGISTRY_CHARACTER, "testcharacter", NULL);
|
||||
|
||||
CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));
|
||||
testcharacter = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testcharacter,
|
||||
ERR_KEY,
|
||||
AKERR_KEY,
|
||||
"Character loaded from json but not in registry");
|
||||
CATCH(errctx, testcharacter->sprite_get(testcharacter, (ACTOR_STATE_ALIVE | ACTOR_STATE_FACE_LEFT), &comparesprite));
|
||||
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), ERR_VALUE, "Wrong sprite for state ACTOR_STATE_ALIVE");
|
||||
CATCH(errctx, testcharacter->sprite_get(testcharacter, ACTOR_STATE_DEAD, &comparesprite));
|
||||
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), ERR_VALUE, "Wrong sprite for state ACTOR_STATE_DEAD");
|
||||
CATCH(errctx, testcharacter->sprite_get(testcharacter, (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT), &comparesprite));
|
||||
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_ALIVE");
|
||||
CATCH(errctx, testcharacter->sprite_get(testcharacter, AKGL_ACTOR_STATE_DEAD, &comparesprite));
|
||||
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_DEAD");
|
||||
|
||||
FAIL_ZERO_BREAK(errctx, (testcharacter->vx != 0.200000003), ERR_VALUE, "Wrong X velocity for test character");
|
||||
FAIL_ZERO_BREAK(errctx, (testcharacter->vy != 0.200000003), ERR_VALUE, "Wrong Y 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->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
|
||||
CATCH(errctx, heap_release_sprite(testsprite));
|
||||
CATCH(errctx, heap_release_sprite(testsprite2));
|
||||
CATCH(errctx, akgl_heap_release_sprite(testsprite));
|
||||
CATCH(errctx, akgl_heap_release_sprite(testsprite2));
|
||||
tsrc = testsprite->refcount;
|
||||
tsrc2 = testsprite2->refcount;
|
||||
CATCH(errctx, heap_release_character(testcharacter));
|
||||
CATCH(errctx, akgl_heap_release_character(testcharacter));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
((testsprite->refcount < tsrc) || (testsprite2->refcount < tsrc2)),
|
||||
ERR_VALUE,
|
||||
AKERR_VALUE,
|
||||
"character did not reduce reference count of its child sprites when released");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
@@ -192,22 +195,23 @@ int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
||||
FAIL_BREAK(errctx, ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libsdl3game/test_character", 640, 480, SDL_WINDOW_HIDDEN, &window, &renderer)) {
|
||||
FAIL_BREAK(errctx, ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
CATCH(errctx, heap_init());
|
||||
CATCH(errctx, registry_init());
|
||||
CATCH(errctx, test_character_initialize());
|
||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_character", 640, 480, SDL_WINDOW_HIDDEN, &window, &renderer)) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, test_akgl_character_initialize());
|
||||
CATCH(errctx, test_character_sprite_mgmt());
|
||||
CATCH(errctx, test_character_iterate_state_sprites());
|
||||
CATCH(errctx, test_character_load_json());
|
||||
CATCH(errctx, test_akgl_character_load_json());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
125
tests/charviewer.c
Normal file
125
tests/charviewer.c
Normal file
@@ -0,0 +1,125 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_main.h>
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
|
||||
#include <akerror.h>
|
||||
#include <akgl/assets.h>
|
||||
#include <akgl/iterator.h>
|
||||
#include <akgl/tilemap.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/draw.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
int numsprites = 8;
|
||||
char *spritepaths[] = {
|
||||
"assets/sprites/little_guy_walking_left.json",
|
||||
"assets/sprites/little_guy_walking_right.json",
|
||||
"assets/sprites/little_guy_walking_up.json",
|
||||
"assets/sprites/little_guy_walking_down.json",
|
||||
"assets/sprites/little_guy_facing_left.json",
|
||||
"assets/sprites/little_guy_facing_right.json",
|
||||
"assets/sprites/little_guy_facing_up.json",
|
||||
"assets/sprites/little_guy_facing_down.json"
|
||||
};
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL3GControlMap *controlmap;
|
||||
actor *actorptr = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
|
||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer)) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
|
||||
strcpy((char *)&game.name, "charviewer");
|
||||
strcpy((char *)&game.version, "0.0.1");
|
||||
strcpy((char *)&game.uri, "net.aklabs.libakgl.charviewer");
|
||||
game.screenwidth = 640;
|
||||
game.screenheight = 480;
|
||||
|
||||
CATCH(errctx, akgl_GAME_init());
|
||||
|
||||
for ( int i = 0; i < numsprites ; i++) {
|
||||
CATCH(errctx, akgl_sprite_load_json(spritepaths[i]));
|
||||
}
|
||||
CATCH(errctx, akgl_character_load_json("assets/characters/littleguy.json"));
|
||||
CATCH(errctx, akgl_heap_next_actor(&actorptr));
|
||||
CATCH(errctx, akgl_actor_initialize((actor *)actorptr, "player"));
|
||||
actorptr->basechar = SDL_GetPointerProperty(
|
||||
AKGL_REGISTRY_CHARACTER,
|
||||
"little guy",
|
||||
NULL);
|
||||
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKGL_ERR_REGISTRY, "Can't load character 'little guy' from the registry");
|
||||
actorptr->movement_controls_face = false;
|
||||
actorptr->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
actorptr->x = 320;
|
||||
actorptr->y = 240;
|
||||
actorptr->visible = true;
|
||||
|
||||
// set up the control map
|
||||
controlmap = &GAME_ControlMaps[0];
|
||||
controlmap->kbid = 0;
|
||||
controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
|
||||
// Move down
|
||||
controlmap->controls[0].key = SDLK_DOWN;
|
||||
//controlmap->controls[0].target_state_gate = AKGL_ACTOR_STATE_MOVING_DOWN;
|
||||
controlmap->controls[0].target_add_state_on = AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN;
|
||||
controlmap->controls[0].target_del_state_on = AKGL_ACTOR_STATE_MOVING_UP | AKGL_ACTOR_STATE_FACE_ALL;
|
||||
controlmap->controls[0].target_del_state_off = AKGL_ACTOR_STATE_MOVING_DOWN;
|
||||
controlmap->controls[0].event_on = SDL_EVENT_KEY_DOWN;
|
||||
controlmap->controls[0].event_off = SDL_EVENT_KEY_UP;
|
||||
|
||||
// Move up
|
||||
controlmap->controls[1].key = SDLK_UP;
|
||||
//controlmap->controls[1].target_state_gate = AKGL_ACTOR_STATE_MOVING_UP;
|
||||
controlmap->controls[1].target_add_state_on = AKGL_ACTOR_STATE_MOVING_UP | AKGL_ACTOR_STATE_FACE_UP;
|
||||
controlmap->controls[1].target_del_state_on = AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_ALL;
|
||||
controlmap->controls[1].target_del_state_off = AKGL_ACTOR_STATE_MOVING_UP;
|
||||
controlmap->controls[1].event_on = SDL_EVENT_KEY_DOWN;
|
||||
controlmap->controls[1].event_off = SDL_EVENT_KEY_UP;
|
||||
|
||||
// Move left
|
||||
controlmap->controls[2].key = SDLK_LEFT;
|
||||
//controlmap->controls[2].target_state_gate = AKGL_ACTOR_STATE_MOVING_LEFT;
|
||||
controlmap->controls[2].target_add_state_on = AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT;
|
||||
controlmap->controls[2].target_del_state_on = AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_ALL;
|
||||
controlmap->controls[2].target_del_state_off = AKGL_ACTOR_STATE_MOVING_LEFT;
|
||||
controlmap->controls[2].event_on = SDL_EVENT_KEY_DOWN;
|
||||
controlmap->controls[2].event_off = SDL_EVENT_KEY_UP;
|
||||
|
||||
// Move right
|
||||
controlmap->controls[3].key = SDLK_RIGHT;
|
||||
//controlmap->controls[3].target_state_gate = AKGL_ACTOR_STATE_MOVING_RIGHT;
|
||||
controlmap->controls[3].target_add_state_on = AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT;
|
||||
controlmap->controls[3].target_del_state_on = AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_ALL;
|
||||
controlmap->controls[3].target_del_state_off = AKGL_ACTOR_STATE_MOVING_RIGHT;
|
||||
controlmap->controls[3].event_on = SDL_EVENT_KEY_DOWN;
|
||||
controlmap->controls[3].event_off = SDL_EVENT_KEY_UP;
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
LOG_ERROR(errctx);
|
||||
return 1;
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
884
tests/controller.c
Normal file
884
tests/controller.c
Normal file
@@ -0,0 +1,884 @@
|
||||
/**
|
||||
* @file controller.c
|
||||
* @brief Unit tests for control maps and SDL input dispatch.
|
||||
*
|
||||
* SDL events are synthesized directly rather than pumped through the event
|
||||
* queue, so none of this needs a physical keyboard or gamepad. The device
|
||||
* enumeration helpers are exercised against the dummy drivers, where the
|
||||
* expected outcome is "no devices, no crash".
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
// For the akgl_Actor_cmhf_* handlers these tests bind. akgl/controller.h pulls
|
||||
// actor.h in for itself now; tests/headers.c is what keeps it doing so.
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/*
|
||||
* akgl/controller.h declares these as akgl_controller_handle_*, but src has
|
||||
* always defined them as gamepad_handle_*. Declared here under the names that
|
||||
* actually link, so the tests can reach them without renaming shipped symbols.
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event);
|
||||
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event);
|
||||
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event);
|
||||
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event);
|
||||
|
||||
/** @brief Keyboard id the tests bind their control maps to. */
|
||||
#define TEST_KBID 11
|
||||
/** @brief Gamepad id the tests bind their control maps to. */
|
||||
#define TEST_JSID 22
|
||||
|
||||
/** @brief Stand-in for the application state pointer SDL hands to callbacks. */
|
||||
static int appstate_placeholder = 0;
|
||||
|
||||
/** @brief The actor every control map in this file targets. */
|
||||
static akgl_Actor *player = NULL;
|
||||
/** @brief Base character supplying the player's acceleration constants. */
|
||||
static akgl_Character *playerchar = NULL;
|
||||
|
||||
/** @brief Clear every control map so each test starts from an empty binding set. */
|
||||
static void reset_control_maps(void)
|
||||
{
|
||||
int i = 0;
|
||||
for ( i = 0; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
||||
memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap));
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Register the "player" actor the gamepad handlers look up by name. */
|
||||
static akerr_ErrorContext *make_player(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_registry_init_actor());
|
||||
CATCH(e, akgl_registry_init_character());
|
||||
CATCH(e, akgl_heap_init());
|
||||
|
||||
CATCH(e, akgl_heap_next_character(&playerchar));
|
||||
CATCH(e, akgl_character_initialize(playerchar, "playerchar"));
|
||||
playerchar->ax = 6.0f;
|
||||
playerchar->ay = 8.0f;
|
||||
|
||||
CATCH(e, akgl_heap_next_actor(&player));
|
||||
CATCH(e, akgl_actor_initialize(player, "player"));
|
||||
player->basechar = playerchar;
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/** @brief Build a synthetic keyboard event. */
|
||||
static void make_key_event(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key)
|
||||
{
|
||||
memset(event, 0x00, sizeof(SDL_Event));
|
||||
event->type = type;
|
||||
event->key.which = which;
|
||||
event->key.key = key;
|
||||
}
|
||||
|
||||
/** @brief Build a synthetic keyboard event carrying modifier state. */
|
||||
static void make_key_event_mod(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key, SDL_Keymod mod)
|
||||
{
|
||||
make_key_event(event, type, which, key);
|
||||
event->key.mod = mod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build the text input event SDL sends after a key press composes.
|
||||
*
|
||||
* @p text is not copied by SDL or by the library at this point, so callers pass
|
||||
* a string literal.
|
||||
*/
|
||||
static void make_text_event(SDL_Event *event, const char *text)
|
||||
{
|
||||
memset(event, 0x00, sizeof(SDL_Event));
|
||||
event->type = SDL_EVENT_TEXT_INPUT;
|
||||
event->text.text = text;
|
||||
}
|
||||
|
||||
/** @brief Build a synthetic gamepad button event. */
|
||||
static void make_button_event(SDL_Event *event, uint32_t type, SDL_JoystickID which, uint8_t button)
|
||||
{
|
||||
memset(event, 0x00, sizeof(SDL_Event));
|
||||
event->type = type;
|
||||
event->gbutton.which = which;
|
||||
event->gbutton.button = button;
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_pushmap(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Control control;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
memset(&control, 0x00, sizeof(akgl_Control));
|
||||
control.key = SDLK_SPACE;
|
||||
control.event_on = SDL_EVENT_KEY_DOWN;
|
||||
control.event_off = SDL_EVENT_KEY_UP;
|
||||
control.handler_on = &akgl_Actor_cmhf_left_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_left_off;
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing one control");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 1,
|
||||
"pushing one control left nextMap at %d, expected 1",
|
||||
GAME_ControlMaps[0].nextMap);
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].key == SDLK_SPACE,
|
||||
"the pushed control did not land in slot 0");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].handler_on == &akgl_Actor_cmhf_left_on,
|
||||
"the pushed control lost its press handler");
|
||||
|
||||
// Pushes accumulate rather than overwrite.
|
||||
TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing a second control");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 2,
|
||||
"pushing a second control left nextMap at %d, expected 2",
|
||||
GAME_ControlMaps[0].nextMap);
|
||||
|
||||
// Maps are independent of each other.
|
||||
TEST_EXPECT_OK(e, akgl_controller_pushmap(3, &control), "pushing into a different map");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[3].nextMap == 1,
|
||||
"map 3 nextMap is %d, expected 1", GAME_ControlMaps[3].nextMap);
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 2,
|
||||
"pushing into map 3 disturbed map 0 (nextMap %d)", GAME_ControlMaps[0].nextMap);
|
||||
|
||||
// Filling a map exactly to capacity still succeeds.
|
||||
reset_control_maps();
|
||||
for ( i = 0; i < AKGL_MAX_CONTROLS; i++ ) {
|
||||
akerr_ErrorContext *pushresult = akgl_controller_pushmap(0, &control);
|
||||
if ( pushresult != NULL ) {
|
||||
pushresult->handled = true;
|
||||
pushresult = akerr_release_error(pushresult);
|
||||
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, "pushing control %d of %d failed", i, AKGL_MAX_CONTROLS);
|
||||
}
|
||||
}
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == AKGL_MAX_CONTROLS,
|
||||
"a full map reports nextMap %d, expected %d",
|
||||
GAME_ControlMaps[0].nextMap, AKGL_MAX_CONTROLS);
|
||||
|
||||
// One past capacity is refused.
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_controller_pushmap(0, &control),
|
||||
"pushing into a full control map");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_pushmap(0, NULL),
|
||||
"pushing a NULL control");
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
|
||||
akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS, &control),
|
||||
"pushing into a control map id at the limit");
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
|
||||
akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS + 5, &control),
|
||||
"pushing into a control map id past the limit");
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_default_bindings(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, make_player());
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID),
|
||||
"installing the default control map");
|
||||
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].target == player,
|
||||
"the default map did not target the player actor");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].kbid == TEST_KBID,
|
||||
"the default map recorded keyboard %d, expected %d",
|
||||
GAME_ControlMaps[0].kbid, TEST_KBID);
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].jsid == TEST_JSID,
|
||||
"the default map recorded gamepad %d, expected %d",
|
||||
GAME_ControlMaps[0].jsid, TEST_JSID);
|
||||
|
||||
// Four keyboard bindings then four gamepad bindings.
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 8,
|
||||
"the default map installed %d controls, expected 8",
|
||||
GAME_ControlMaps[0].nextMap);
|
||||
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].key == SDLK_DOWN,
|
||||
"the first default binding is not the down arrow");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].handler_on == &akgl_Actor_cmhf_down_on,
|
||||
"the down arrow is not bound to the down handler");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[1].key == SDLK_UP,
|
||||
"the second default binding is not the up arrow");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[2].key == SDLK_LEFT,
|
||||
"the third default binding is not the left arrow");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[3].key == SDLK_RIGHT,
|
||||
"the fourth default binding is not the right arrow");
|
||||
|
||||
// The gamepad half binds buttons and leaves the keycode clear, so a
|
||||
// keyboard event cannot accidentally match a dpad binding.
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[4].button == SDL_GAMEPAD_BUTTON_DPAD_DOWN,
|
||||
"the fifth default binding is not the dpad down button");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[4].key == 0,
|
||||
"a gamepad binding also carries a keycode");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[7].button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT,
|
||||
"the eighth default binding is not the dpad right button");
|
||||
|
||||
// An unknown actor name is a registry error, not a crash.
|
||||
reset_control_maps();
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_REGISTRY,
|
||||
akgl_controller_default(1, "no_such_actor", TEST_KBID, TEST_JSID),
|
||||
"installing a default map for an unregistered actor");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
|
||||
akgl_controller_default(AKGL_MAX_CONTROL_MAPS, "player", TEST_KBID, TEST_JSID),
|
||||
"installing a default map at a control map id past the limit");
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_handle_keyboard_events(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, make_player());
|
||||
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
|
||||
|
||||
// Pressing left routes to the left press handler.
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a left key press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"a left key press did not start the actor moving left (state %d)", player->state);
|
||||
TEST_ASSERT_FEQ(e, player->ax, -6.0f,
|
||||
"a left key press set ax to %f, expected -6", player->ax);
|
||||
|
||||
// Releasing it routes to the release handler.
|
||||
make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a left key release");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"a left key release did not stop the actor (state %d)", player->state);
|
||||
|
||||
// The other three directions route the same way.
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_RIGHT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a right key press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
|
||||
"a right key press did not start the actor moving right (state %d)", player->state);
|
||||
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_UP);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching an up key press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
|
||||
"an up key press did not start the actor moving up (state %d)", player->state);
|
||||
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_DOWN);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a down key press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
|
||||
"a down key press did not start the actor moving down (state %d)", player->state);
|
||||
|
||||
// An unbound key is ignored.
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_F12);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching an unbound key");
|
||||
TEST_ASSERT(e, player->state == 0,
|
||||
"an unbound key changed the actor state to %d", player->state);
|
||||
|
||||
// A bound key from a different keyboard is ignored, so split-keyboard
|
||||
// local multiplayer does not cross-talk.
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID + 1, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a bound key from an unbound keyboard");
|
||||
TEST_ASSERT(e, player->state == 0,
|
||||
"a key from another keyboard changed the actor state to %d", player->state);
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_handle_gamepad_events(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, make_player());
|
||||
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
|
||||
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a dpad left press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"a dpad left press did not start the actor moving left (state %d)", player->state);
|
||||
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_UP, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a dpad left release");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"a dpad left release did not stop the actor (state %d)", player->state);
|
||||
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a dpad up press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
|
||||
"a dpad up press did not start the actor moving up (state %d)", player->state);
|
||||
|
||||
// A press from an unbound gamepad is ignored.
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID + 1, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a dpad press from an unbound gamepad");
|
||||
TEST_ASSERT(e, player->state == 0,
|
||||
"a press from another gamepad changed the actor state to %d", player->state);
|
||||
|
||||
// An unbound button on the bound gamepad is ignored.
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_START);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching an unbound gamepad button");
|
||||
TEST_ASSERT(e, player->state == 0,
|
||||
"an unbound gamepad button changed the actor state to %d", player->state);
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_handle_event_edge_cases(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, make_player());
|
||||
|
||||
// With no control maps installed at all, every event is a no-op.
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching into an empty control map table");
|
||||
|
||||
// A map in a later slot is still reached, so the scan does not stop at
|
||||
// the first unpopulated entry.
|
||||
CATCH(e, akgl_controller_default(5, "player", TEST_KBID, TEST_JSID));
|
||||
player->state = 0;
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching to a control map in a later slot");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"a control map in slot 5 was not consulted (state %d)", player->state);
|
||||
|
||||
// An event type that no binding uses falls through harmlessly.
|
||||
memset(&event, 0x00, sizeof(SDL_Event));
|
||||
event.type = SDL_EVENT_MOUSE_MOTION;
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching an event type with no bindings");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_event(NULL, &event),
|
||||
"dispatching with a NULL appstate");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
akgl_controller_handle_event(&appstate_placeholder, NULL),
|
||||
"dispatching a NULL event");
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_gamepad_button_handlers(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, make_player());
|
||||
|
||||
// These handlers find their actor by looking up "player" in the registry
|
||||
// rather than taking it as an argument.
|
||||
player->state = 0;
|
||||
player->movement_controls_face = false;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event),
|
||||
"dpad down press handler");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
|
||||
"the dpad down handler did not set MOVING_DOWN (state %d)", player->state);
|
||||
// With automatic facing off, the handler sets the facing itself.
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
|
||||
"the dpad down handler did not set FACE_DOWN (state %d)", player->state);
|
||||
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event),
|
||||
"dpad down release handler");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
|
||||
"the dpad down release handler did not clear MOVING_DOWN (state %d)", player->state);
|
||||
TEST_ASSERT(e, player->curSpriteFrameId == 0,
|
||||
"the release handler did not reset the animation frame");
|
||||
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad up press handler");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
|
||||
"the dpad up handler did not set MOVING_UP (state %d)", player->state);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad up release handler");
|
||||
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad left press handler");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"the dpad left handler did not set MOVING_LEFT (state %d)", player->state);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad left release handler");
|
||||
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad right press handler");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
|
||||
"the dpad right handler did not set MOVING_RIGHT (state %d)", player->state);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad right release handler");
|
||||
|
||||
// With automatic facing on, the handler leaves facing to the actor's own
|
||||
// face function.
|
||||
player->state = 0;
|
||||
player->movement_controls_face = true;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event),
|
||||
"dpad down press with automatic facing on");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
|
||||
"the handler set facing even though the actor faces automatically (state %d)",
|
||||
player->state);
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_down(NULL, &event),
|
||||
"dpad press handler with a NULL appstate");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_button_down(&appstate_placeholder, NULL),
|
||||
"dpad press handler with a NULL event");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_up(NULL, &event),
|
||||
"dpad release handler with a NULL appstate");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_button_up(&appstate_placeholder, NULL),
|
||||
"dpad release handler with a NULL event");
|
||||
|
||||
// With no actor named "player" registered there is nothing to drive.
|
||||
CATCH(e, akgl_registry_init_actor());
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_button_down(&appstate_placeholder, &event),
|
||||
"dpad press handler with no player actor registered");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_button_up(&appstate_placeholder, &event),
|
||||
"dpad release handler with no player actor registered");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_device_events(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
// The dummy drivers present no gamepads, so the add and remove handlers
|
||||
// take their "unknown device" paths. Neither may crash.
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_ADDED, 999, 0);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_added(&appstate_placeholder, &event),
|
||||
"handling an add for a device that cannot be opened");
|
||||
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_REMOVED, 999, 0);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_removed(&appstate_placeholder, &event),
|
||||
"handling a remove for a device that was never open");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_added(NULL, &event),
|
||||
"add handler with a NULL appstate");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_added(&appstate_placeholder, NULL),
|
||||
"add handler with a NULL event");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_removed(NULL, &event),
|
||||
"remove handler with a NULL appstate");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_removed(&appstate_placeholder, NULL),
|
||||
"remove handler with a NULL event");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_device_enumeration(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
// Enumeration has to succeed on a machine with no input devices, which
|
||||
// is the normal state under the dummy drivers and in CI.
|
||||
TEST_EXPECT_OK(e, akgl_controller_list_keyboards(), "listing keyboards");
|
||||
TEST_EXPECT_OK(e, akgl_controller_open_gamepads(), "opening gamepads");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_poll_key(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
int keycode = -1;
|
||||
bool available = true;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, make_player());
|
||||
CATCH(e, akgl_controller_flush_keys());
|
||||
|
||||
// An empty buffer answers "no key waiting" and succeeds.
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling an empty key buffer");
|
||||
TEST_ASSERT(e, available == false,
|
||||
"polling an empty buffer reported a key was available");
|
||||
TEST_ASSERT(e, keycode == 0,
|
||||
"polling an empty buffer left keycode at %d, expected 0", keycode);
|
||||
|
||||
// A key press pumped through the handler is drained by the poller,
|
||||
// exactly once.
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a key press with no control maps installed");
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling for that key");
|
||||
TEST_ASSERT(e, available == true, "the pressed key was not available to the poller");
|
||||
TEST_ASSERT(e, keycode == SDLK_A,
|
||||
"the poller returned keycode %d, expected %d", keycode, (int)SDLK_A);
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling again");
|
||||
TEST_ASSERT(e, available == false, "the same keystroke was delivered twice");
|
||||
|
||||
// Key releases are not keystrokes.
|
||||
make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_A);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a key release");
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a release");
|
||||
TEST_ASSERT(e, available == false, "a key release was buffered as a keystroke");
|
||||
|
||||
// Keys come back in the order they were pressed.
|
||||
CATCH(e, akgl_controller_flush_keys());
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_1);
|
||||
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_2);
|
||||
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_3);
|
||||
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the first key");
|
||||
TEST_ASSERT(e, keycode == SDLK_1, "the first key out was %d, expected %d",
|
||||
keycode, (int)SDLK_1);
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the second key");
|
||||
TEST_ASSERT(e, keycode == SDLK_2, "the second key out was %d, expected %d",
|
||||
keycode, (int)SDLK_2);
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the third key");
|
||||
TEST_ASSERT(e, keycode == SDLK_3, "the third key out was %d, expected %d",
|
||||
keycode, (int)SDLK_3);
|
||||
|
||||
// A key that a control map also acts on still reaches the poller, so a
|
||||
// game and an embedded interpreter can share one keyboard.
|
||||
CATCH(e, akgl_controller_flush_keys());
|
||||
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a bound key press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"the bound key stopped driving the actor (state %d)", player->state);
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling for a key a control map claimed");
|
||||
TEST_ASSERT(e, available == true, "a bound key never reached the key buffer");
|
||||
TEST_ASSERT(e, keycode == SDLK_LEFT, "the poller returned keycode %d, expected %d",
|
||||
keycode, (int)SDLK_LEFT);
|
||||
|
||||
// Flushing discards the backlog.
|
||||
CATCH(e, akgl_controller_flush_keys());
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B);
|
||||
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(e, akgl_controller_flush_keys(), "flushing a buffer with a key in it");
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a flush");
|
||||
TEST_ASSERT(e, available == false, "a flush left a keystroke behind");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(NULL, &available),
|
||||
"polling into a NULL keycode");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(&keycode, NULL),
|
||||
"polling into a NULL availability flag");
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
IGNORE(akgl_controller_flush_keys());
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_poll_key_overflow(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
int keycode = -1;
|
||||
bool available = true;
|
||||
bool ordered = true;
|
||||
bool pumped = true;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, akgl_controller_flush_keys());
|
||||
|
||||
// Fill the buffer exactly, then press one more. The overflowing key is
|
||||
// the one that is dropped -- what was typed first survives.
|
||||
for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER + 1; i++ ) {
|
||||
akerr_ErrorContext *pumpresult = NULL;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A + i);
|
||||
pumpresult = akgl_controller_handle_event(&appstate_placeholder, &event);
|
||||
if ( pumpresult != NULL ) {
|
||||
pumpresult->handled = true;
|
||||
pumpresult = akerr_release_error(pumpresult);
|
||||
pumped = false;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT(e, pumped == true, "dispatching the overflow key presses failed");
|
||||
|
||||
for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER; i++ ) {
|
||||
akerr_ErrorContext *pollresult = akgl_controller_poll_key(&keycode, &available);
|
||||
if ( pollresult != NULL ) {
|
||||
pollresult->handled = true;
|
||||
pollresult = akerr_release_error(pollresult);
|
||||
ordered = false;
|
||||
}
|
||||
TEST_ASSERT_FLAG(ordered, available == true);
|
||||
TEST_ASSERT_FLAG(ordered, keycode == (int)(SDLK_A + i));
|
||||
}
|
||||
TEST_ASSERT(e, ordered == true,
|
||||
"a full buffer did not return the first %d keys in order",
|
||||
AKGL_CONTROLLER_KEY_BUFFER);
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling after draining a full buffer");
|
||||
TEST_ASSERT(e, available == false,
|
||||
"the key pressed past capacity was buffered anyway (keycode %d)", keycode);
|
||||
|
||||
// The buffer is reusable after an overflow rather than wedged.
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_Z);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a key after an overflow");
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling after an overflow");
|
||||
TEST_ASSERT(e, available == true, "the buffer stayed full after being drained");
|
||||
TEST_ASSERT(e, keycode == SDLK_Z, "the poller returned keycode %d, expected %d",
|
||||
keycode, (int)SDLK_Z);
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
IGNORE(akgl_controller_flush_keys());
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_poll_keystroke(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Event event;
|
||||
akgl_Keystroke keystroke;
|
||||
int keycode = -1;
|
||||
bool available = true;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
|
||||
// An empty buffer answers "nothing waiting" and zeroes the destination.
|
||||
memset(&keystroke, 0xff, sizeof(akgl_Keystroke));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling an empty keystroke buffer");
|
||||
TEST_ASSERT(errctx, available == false,
|
||||
"polling an empty buffer reported a keystroke was available");
|
||||
TEST_ASSERT(errctx, keystroke.key == 0,
|
||||
"polling an empty buffer left a keycode of %d behind", (int)keystroke.key);
|
||||
TEST_ASSERT(errctx, keystroke.text[0] == '\0',
|
||||
"polling an empty buffer left text behind");
|
||||
|
||||
// The case the whole thing exists for: a shifted key. The keycode is
|
||||
// the unshifted one, so the composed character is the only place the
|
||||
// double quote can come from.
|
||||
make_key_event_mod(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_APOSTROPHE, SDL_KMOD_LSHIFT);
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
make_text_event(&event, "\"");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for a shifted key");
|
||||
TEST_ASSERT(errctx, available == true, "a shifted key press never reached the buffer");
|
||||
TEST_ASSERT(errctx, keystroke.key == SDLK_APOSTROPHE,
|
||||
"the keystroke reported keycode %d, expected %d",
|
||||
(int)keystroke.key, (int)SDLK_APOSTROPHE);
|
||||
TEST_ASSERT(errctx, (keystroke.mod & SDL_KMOD_SHIFT) != 0,
|
||||
"the keystroke lost its shift state (mod %d)", (int)keystroke.mod);
|
||||
TEST_ASSERT(errctx, strcmp(keystroke.text, "\"") == 0,
|
||||
"the keystroke composed to \"%s\", expected a double quote", keystroke.text);
|
||||
|
||||
// A key that composes to nothing still arrives, with empty text -- that
|
||||
// is how a line editor recognises Backspace and the arrows.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for a non-printing key");
|
||||
TEST_ASSERT(errctx, keystroke.key == SDLK_LEFT,
|
||||
"a non-printing key reported keycode %d, expected %d",
|
||||
(int)keystroke.key, (int)SDLK_LEFT);
|
||||
TEST_ASSERT(errctx, keystroke.text[0] == '\0',
|
||||
"a non-printing key composed to \"%s\", expected nothing", keystroke.text);
|
||||
|
||||
// Text with no key press behind it -- an input method, or a character
|
||||
// finished by a dead key -- is buffered on its own rather than dropped.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_text_event(&event, "e");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for composed text with no key press");
|
||||
TEST_ASSERT(errctx, available == true, "composed text with no key press was dropped");
|
||||
TEST_ASSERT(errctx, keystroke.key == 0,
|
||||
"text with no key press reported keycode %d, expected 0", (int)keystroke.key);
|
||||
TEST_ASSERT(errctx, strcmp(keystroke.text, "e") == 0,
|
||||
"text with no key press composed to \"%s\", expected \"e\"", keystroke.text);
|
||||
TEST_ASSERT(errctx, keystroke.mod == 0,
|
||||
"a text-only entry reported modifiers %d; the character it carries already "
|
||||
"reflects them", (int)keystroke.mod);
|
||||
|
||||
// The keycode form has nowhere to report that entry, so it discards it
|
||||
// on the way past rather than handing back a keystroke with no key.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_text_event(&event, "e");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B);
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling by keycode past a text-only entry");
|
||||
TEST_ASSERT(errctx, available == true, "the key behind a text-only entry was lost");
|
||||
TEST_ASSERT(errctx, keycode == SDLK_B,
|
||||
"polling by keycode returned %d, expected %d", keycode, (int)SDLK_B);
|
||||
|
||||
// Both pollers drain the same buffer.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_C);
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling by keycode after the same key was taken as a keystroke");
|
||||
TEST_ASSERT(errctx, available == false, "one keystroke was delivered to both pollers");
|
||||
|
||||
// Text that arrives after its press has already been drained becomes an
|
||||
// entry of its own instead of being attached to somebody else's key.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_D);
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available));
|
||||
make_text_event(&event, "d");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for text that outlived its key press");
|
||||
TEST_ASSERT(errctx, available == true, "text that outlived its key press was dropped");
|
||||
TEST_ASSERT(errctx, keystroke.key == 0,
|
||||
"text that outlived its key press was attached to keycode %d",
|
||||
(int)keystroke.key);
|
||||
|
||||
// More text than an entry holds is cut on a character boundary, never
|
||||
// through the middle of one. Each of these is two bytes, so three fit
|
||||
// in the eight-byte field and the fourth does not.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_text_event(&event, "\xce\xb1\xce\xb1\xce\xb1\xce\xb1");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for text longer than one entry holds");
|
||||
TEST_ASSERT(errctx, strcmp(keystroke.text, "\xce\xb1\xce\xb1\xce\xb1") == 0,
|
||||
"oversized text was truncated to \"%s\", expected three whole characters",
|
||||
keystroke.text);
|
||||
|
||||
// A sequence the string ends in the middle of is dropped rather than
|
||||
// copied by reading past the terminator to find its other half.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_text_event(&event, "a\xce");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for text that ends mid-character");
|
||||
TEST_ASSERT(errctx, strcmp(keystroke.text, "a") == 0,
|
||||
"a truncated character came back as \"%s\", expected just the whole one",
|
||||
keystroke.text);
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_controller_poll_keystroke(NULL, &available),
|
||||
"polling into a NULL keystroke");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_controller_poll_keystroke(&keystroke, NULL),
|
||||
"polling into a NULL availability flag");
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
IGNORE(akgl_controller_flush_keys());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize SDL: %s",
|
||||
SDL_GetError());
|
||||
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
|
||||
CATCH(errctx, test_controller_pushmap());
|
||||
CATCH(errctx, test_controller_default_bindings());
|
||||
CATCH(errctx, test_controller_handle_keyboard_events());
|
||||
CATCH(errctx, test_controller_handle_gamepad_events());
|
||||
CATCH(errctx, test_controller_handle_event_edge_cases());
|
||||
CATCH(errctx, test_controller_gamepad_button_handlers());
|
||||
CATCH(errctx, test_controller_device_events());
|
||||
CATCH(errctx, test_controller_device_enumeration());
|
||||
CATCH(errctx, test_controller_poll_key());
|
||||
CATCH(errctx, test_controller_poll_key_overflow());
|
||||
CATCH(errctx, test_controller_poll_keystroke());
|
||||
} CLEANUP {
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
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);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user