Compare commits
9 Commits
ad71072cb3
...
coverage-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
bc782fbffe
|
|||
|
22162db2da
|
|||
|
6f6bd2d563
|
|||
|
ff88f48fa2
|
|||
|
549b27d3eb
|
|||
|
a2995e81df
|
|||
|
dc1bd0a798
|
|||
|
3854b33750
|
|||
|
cf9ebb206f
|
89
.gitea/workflows/ci.yaml
Normal file
89
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
name: libakgl CI Build
|
||||
run-name: ${{ gitea.actor }} libakgl test
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
cmake_build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y \
|
||||
cmake doxygen gcc gcovr pkg-config \
|
||||
libasound2-dev libfreetype-dev libharfbuzz-dev \
|
||||
libpng-dev libtiff-dev libwebp-dev \
|
||||
libudev-dev libx11-dev libxcursor-dev libxext-dev \
|
||||
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
|
||||
libxss-dev libxtst-dev
|
||||
- name: Configure and build
|
||||
run: |
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DAKGL_COVERAGE=ON
|
||||
cmake --build build --parallel
|
||||
- name: Build API documentation
|
||||
run: doxygen Doxyfile
|
||||
- name: Test (JUnit)
|
||||
run: |
|
||||
export LD_LIBRARY_PATH="$PWD/build:$PWD/build/deps/SDL:$PWD/build/deps/SDL_image:$PWD/build/deps/SDL_mixer:$PWD/build/deps/SDL_ttf"
|
||||
ctest \
|
||||
--test-dir build \
|
||||
-E '^character$' \
|
||||
--output-on-failure \
|
||||
--output-junit "$(pwd)/ctest-junit.xml"
|
||||
- name: Publish test results
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
with:
|
||||
report_paths: 'ctest-junit.xml'
|
||||
annotate_only: true
|
||||
detailed_summary: true
|
||||
include_passed: true
|
||||
fail_on_failure: 'true'
|
||||
- name: Upload coverage reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: code-coverage
|
||||
path: build/coverage/
|
||||
if-no-files-found: warn
|
||||
|
||||
mutation_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install mutation-test dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y \
|
||||
cmake gcc pkg-config python3 \
|
||||
libasound2-dev libfreetype-dev libharfbuzz-dev \
|
||||
libpng-dev libtiff-dev libwebp-dev \
|
||||
libudev-dev libx11-dev libxcursor-dev libxext-dev \
|
||||
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
|
||||
libxss-dev libxtst-dev
|
||||
# Keep CI bounded to a focused source file. Run the default target locally
|
||||
# for the complete libakgl-owned src/ tree.
|
||||
- name: Mutation testing
|
||||
run: |
|
||||
python3 scripts/mutation_test.py \
|
||||
--target src/staticstring.c \
|
||||
--junit mutation-junit.xml \
|
||||
--threshold 50
|
||||
- name: Publish mutation results
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
with:
|
||||
report_paths: 'mutation-junit.xml'
|
||||
annotate_only: true
|
||||
detailed_summary: true
|
||||
include_passed: true
|
||||
fail_on_failure: 'false'
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -15,10 +15,10 @@
|
||||
url = git@github.com:libsdl-org/SDL_ttf.git
|
||||
[submodule "deps/libsdlerror"]
|
||||
path = deps/libakerror
|
||||
url = https://source.home.aklabs.net/andrew/libsdlerror.git
|
||||
url = https://source.starfort.tech/andrew/libakerror.git
|
||||
[submodule "deps/libakstdlib"]
|
||||
path = deps/libakstdlib
|
||||
url = https://source.home.aklabs.net/andrew/libakstdlib.git
|
||||
url = https://source.starfort.tech/andrew/libakstdlib.git
|
||||
[submodule "deps/jansson"]
|
||||
path = deps/jansson
|
||||
url = git@github.com:akheron/jansson.git
|
||||
|
||||
48
AGENTS.md
Normal file
48
AGENTS.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 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/`, generated `akgl.pc` files, and `include/akgl/SDL_GameControllerDB.h` as build outputs rather than hand-maintained source.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
Configure an out-of-tree development build:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
```
|
||||
|
||||
Build all library, utility, and test targets:
|
||||
|
||||
```sh
|
||||
cmake --build build --parallel
|
||||
```
|
||||
|
||||
Run the complete test suite with failure details:
|
||||
|
||||
```sh
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
Run one test while iterating, for example `ctest --test-dir build -R sprite --output-on-failure`. The `rebuild.sh` script also installs into a developer-specific `/home/andrew/local` prefix and removes existing outputs; prefer the portable commands above unless that exact workflow is intended.
|
||||
|
||||
Run mutation testing with `cmake --build build --target mutation`. For a quick smoke run, use `scripts/mutation_test.py --target src/tilemap.c --max-mutants 10`; the harness mutates only a scratch copy and excludes the intentionally failing character test.
|
||||
|
||||
Generate HTML and Cobertura coverage reports with `cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug`, then build and run CTest. Reports are written to `build-coverage/coverage/`; this mode requires `gcovr` and GCC or Clang.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Follow the surrounding C style: braces on the next line for function bodies, spaces inside control-flow parentheses, and short, focused functions. Preserve the indentation of the file being edited; older files contain both spaces and tabs. Public symbols use the `akgl_` prefix, public types use `akgl_TypeName`, and constants/macros use `AKGL_UPPER_SNAKE_CASE`. Name matching header/source pairs by feature, such as `include/akgl/sprite.h` and `src/sprite.c`. No repository-wide formatter or linter is configured, so avoid unrelated formatting churn.
|
||||
|
||||
## Rules
|
||||
|
||||
- Add yourself (agent program name, model name and version) as a co-author on every commit message
|
||||
- Avoid dynamic memory allocation. Use the `akgl_heap_*` methods to retrieve necessary objects at runtime, and to release them when done. If the akgl heap facilities don't provide the kind of object needed, create a new heap layer to support that type of object.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Tests use simple executable return codes and are registered through CTest in `CMakeLists.txt`; there is no declared coverage threshold. Add focused tests as `tests/<feature>.c`, create a matching `test_<feature>` target, and register it with `add_test`. Put reusable fixtures in `tests/assets/` and keep paths compatible with tests launched from the build tree. Coverage mode wraps the suite in a CTest fixture so counters are reset before tests and reports are generated afterward.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Recent commits use concise, imperative summaries such as `Fix a scale bug...` and often explain related changes in one sentence. Keep each commit scoped and describe user-visible behavior. Pull requests should summarize the change, identify affected modules, list the CMake/CTest commands run, and link relevant issues. Include screenshots only for rendering, map, or `charviewer` changes.
|
||||
207
CMakeLists.txt
207
CMakeLists.txt
@@ -2,9 +2,35 @@ cmake_minimum_required(VERSION 3.10)
|
||||
project(akgl LANGUAGES C)
|
||||
|
||||
include(CTest)
|
||||
option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF)
|
||||
|
||||
if(AKGL_COVERAGE)
|
||||
if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
message(FATAL_ERROR "AKGL_COVERAGE requires GCC or Clang")
|
||||
endif()
|
||||
find_program(GCOVR_EXECUTABLE gcovr REQUIRED)
|
||||
endif()
|
||||
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
|
||||
|
||||
# Vendored projects own their test suites. Suppress their CTest registration
|
||||
# while embedded so the top-level suite contains only targets built here.
|
||||
set(AKGL_SUPPRESS_DEPENDENCY_TESTS TRUE)
|
||||
function(add_test)
|
||||
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
|
||||
_add_test(${ARGV})
|
||||
endif()
|
||||
endfunction()
|
||||
function(set_tests_properties)
|
||||
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
|
||||
_set_tests_properties(${ARGV})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
set(JANSSON_WITHOUT_TESTS ON CACHE BOOL "Do not build vendored Jansson tests" FORCE)
|
||||
set(JANSSON_EXAMPLES OFF CACHE BOOL "Do not build vendored Jansson examples" FORCE)
|
||||
set(JANSSON_BUILD_DOCS OFF CACHE BOOL "Do not build vendored Jansson docs" FORCE)
|
||||
|
||||
add_subdirectory(deps/jansson EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
|
||||
@@ -13,6 +39,13 @@ add_subdirectory(deps/SDL_image EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL)
|
||||
|
||||
# Reserve error-name table entries for libakgl-specific status codes.
|
||||
target_compile_definitions(akerror PUBLIC
|
||||
AKERR_MAX_ERR_VALUE=256
|
||||
)
|
||||
|
||||
set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE)
|
||||
|
||||
else()
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
@@ -48,6 +81,11 @@ 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)
|
||||
|
||||
# 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}
|
||||
@@ -81,31 +119,84 @@ add_library(akgl SHARED
|
||||
add_library(akgl::akgl ALIAS akgl)
|
||||
|
||||
add_executable(charviewer util/charviewer.c)
|
||||
add_executable(test_actor tests/actor.c)
|
||||
add_executable(test_bitmasks tests/bitmasks.c)
|
||||
add_executable(test_character tests/character.c)
|
||||
add_executable(test_registry tests/registry.c)
|
||||
add_executable(test_sprite tests/sprite.c)
|
||||
add_executable(test_staticstring tests/staticstring.c)
|
||||
add_executable(test_tilemap tests/tilemap.c)
|
||||
add_executable(test_util tests/util.c)
|
||||
add_executable(test_semver_unit deps/semver/semver_unit.c)
|
||||
add_test(NAME actor COMMAND test_actor)
|
||||
add_test(NAME bitmasks COMMAND test_bitmasks)
|
||||
add_test(NAME character COMMAND test_character)
|
||||
add_test(NAME registry COMMAND test_registry)
|
||||
add_test(NAME sprite COMMAND test_sprite)
|
||||
add_test(NAME staticstring COMMAND test_staticstring)
|
||||
add_test(NAME tilemap COMMAND test_tilemap)
|
||||
add_test(NAME util COMMAND test_util)
|
||||
|
||||
# Every suite here is a standalone C program named tests/<name>.c, built as
|
||||
# test_<name> and registered with CTest under <name>.
|
||||
set(AKGL_TEST_SUITES
|
||||
actor
|
||||
bitmasks
|
||||
character
|
||||
controller
|
||||
game
|
||||
heap
|
||||
json_helpers
|
||||
physics
|
||||
registry
|
||||
sprite
|
||||
staticstring
|
||||
tilemap
|
||||
util
|
||||
)
|
||||
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
add_executable(test_${suite} tests/${suite}.c)
|
||||
add_test(NAME ${suite} COMMAND test_${suite})
|
||||
endforeach()
|
||||
|
||||
add_test(NAME semver_unit COMMAND test_semver_unit)
|
||||
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES} semver_unit
|
||||
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 30
|
||||
)
|
||||
|
||||
# Specify include directories for the library's headers (if applicable)
|
||||
target_include_directories(akgl PUBLIC
|
||||
include/
|
||||
deps/semver/
|
||||
)
|
||||
|
||||
if(AKGL_COVERAGE)
|
||||
target_compile_options(akgl PRIVATE --coverage -O0 -g)
|
||||
target_link_options(akgl PRIVATE --coverage)
|
||||
|
||||
set(AKGL_COVERAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/coverage")
|
||||
file(MAKE_DIRECTORY "${AKGL_COVERAGE_DIR}")
|
||||
|
||||
add_test(
|
||||
NAME coverage_reset
|
||||
COMMAND ${GCOVR_EXECUTABLE}
|
||||
--root "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
--delete
|
||||
)
|
||||
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP akgl_coverage)
|
||||
# Any suite missing from this list runs outside the fixture and has its
|
||||
# counters discarded by coverage_reset.
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES} semver_unit
|
||||
PROPERTIES FIXTURES_REQUIRED akgl_coverage
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME coverage_report
|
||||
COMMAND ${GCOVR_EXECUTABLE}
|
||||
--root "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
--filter "${CMAKE_CURRENT_SOURCE_DIR}/src/"
|
||||
--xml-pretty
|
||||
--xml "${AKGL_COVERAGE_DIR}/coverage.xml"
|
||||
--html-details "${AKGL_COVERAGE_DIR}/index.html"
|
||||
)
|
||||
set_tests_properties(
|
||||
coverage_report
|
||||
PROPERTIES
|
||||
FIXTURES_CLEANUP akgl_coverage
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(akgl
|
||||
PUBLIC
|
||||
SDL3::SDL3
|
||||
@@ -117,17 +208,83 @@ target_link_libraries(akgl
|
||||
jansson::jansson
|
||||
)
|
||||
|
||||
target_link_libraries(test_actor PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_link_libraries(test_bitmasks PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_link_libraries(test_character PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_link_libraries(test_registry PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_link_libraries(test_sprite PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_link_libraries(test_staticstring PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_link_libraries(test_tilemap PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_link_libraries(test_util PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
target_link_libraries(test_${suite} PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_include_directories(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.
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(AKGL_VENDORED_RPATH
|
||||
"$<TARGET_FILE_DIR:SDL3::SDL3>"
|
||||
"$<TARGET_FILE_DIR:SDL3_image::SDL3_image>"
|
||||
"$<TARGET_FILE_DIR:SDL3_ttf::SDL3_ttf>"
|
||||
"$<TARGET_FILE_DIR:SDL3_mixer::SDL3_mixer>"
|
||||
"$<TARGET_FILE_DIR:akerror::akerror>"
|
||||
"$<TARGET_FILE_DIR:akstdlib::akstdlib>"
|
||||
)
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
set_target_properties(test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
endforeach()
|
||||
set_target_properties(charviewer akgl PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
|
||||
# RPATH alone is not enough: LD_LIBRARY_PATH is searched first, so a developer
|
||||
# who has previously run rebuild.sh has an installed libakgl.so ahead of the
|
||||
# one under test. Prepend the build tree for the CTest run so the suite always
|
||||
# exercises what was just compiled.
|
||||
set(AKGL_TEST_LIBPATH
|
||||
"${CMAKE_CURRENT_BINARY_DIR}"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_image"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_ttf"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_mixer"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakerror"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakstdlib"
|
||||
)
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
|
||||
set(AKGL_TEST_ENV_MOD "")
|
||||
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
|
||||
list(APPEND AKGL_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
|
||||
endforeach()
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES}
|
||||
PROPERTIES ENVIRONMENT_MODIFICATION "${AKGL_TEST_ENV_MOD}"
|
||||
)
|
||||
else()
|
||||
string(REPLACE ";" ":" AKGL_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES}
|
||||
PROPERTIES ENVIRONMENT "LD_LIBRARY_PATH=${AKGL_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Mutation testing copies the repository to scratch space, applies one small
|
||||
# source change at a time, and verifies that the passing tests detect it. The
|
||||
# intentionally failing character test is excluded by the harness.
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
if(Python3_FOUND)
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(AKGL_MUTATION_TARGET mutation)
|
||||
else()
|
||||
set(AKGL_MUTATION_TARGET akgl_mutation)
|
||||
endif()
|
||||
add_custom_target(${AKGL_MUTATION_TARGET}
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
||||
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
USES_TERMINAL
|
||||
COMMENT "Running mutation tests (breaks a scratch copy, expects tests to fail)"
|
||||
)
|
||||
endif()
|
||||
|
||||
set(main_lib_dest "lib/akgl-${MY_LIBRARY_VERSION}")
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/")
|
||||
install(TARGETS akgl DESTINATION "lib/")
|
||||
|
||||
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
|
||||
13
README.md
13
README.md
@@ -269,3 +269,16 @@ PASS(e, aksl_atoi(width->data, &screenwidth));
|
||||
PASS(e, akgl_heap_release_string(width));
|
||||
```
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
227
TODO.md
Normal file
227
TODO.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# TODO
|
||||
|
||||
## Coverage status
|
||||
|
||||
Generated with:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
|
||||
cmake --build build-coverage --parallel
|
||||
ctest --test-dir build-coverage --output-on-failure
|
||||
```
|
||||
|
||||
Reports land in `build-coverage/coverage/` (`index.html`, `coverage.xml`).
|
||||
|
||||
**Line coverage 72.2%, function coverage 78.6%** (1534/2125 lines), up from a
|
||||
39.6% / 44.3% baseline. `character` is the one intentionally failing suite;
|
||||
everything else passes.
|
||||
|
||||
| File | Lines | Functions |
|
||||
|---|---|---|
|
||||
| `src/actor.c` | 205/258 (80%) | 16/18 |
|
||||
| `src/assets.c` | 0/21 (0%) | 0/1 |
|
||||
| `src/character.c` | 104/118 (88%) | 6/7 |
|
||||
| `src/controller.c` | 220/243 (90%) | 9/9 |
|
||||
| `src/draw.c` | 0/13 (0%) | 0/1 |
|
||||
| `src/game.c` | 124/230 (54%) | 10/15 |
|
||||
| `src/heap.c` | 116/116 (100%) | 12/12 |
|
||||
| `src/json_helpers.c` | 111/111 (100%) | 11/11 |
|
||||
| `src/physics.c` | 140/140 (100%) | 10/10 |
|
||||
| `src/registry.c` | 76/102 (74%) | 11/12 |
|
||||
| `src/renderer.c` | 7/70 (10%) | 1/7 |
|
||||
| `src/sprite.c` | 93/101 (92%) | 5/5 |
|
||||
| `src/staticstring.c` | 16/17 (94%) | 2/2 |
|
||||
| `src/text.c` | 0/28 (0%) | 0/2 |
|
||||
| `src/tilemap.c` | 201/426 (47%) | 10/20 |
|
||||
| `src/util.c` | 121/131 (92%) | 7/8 |
|
||||
|
||||
Branch coverage reads 18.6% and should not be used as a target. The akerror
|
||||
control-flow macros (`ATTEMPT`/`CATCH`/`PROCESS`/`FINISH`, `FAIL_*_RETURN`)
|
||||
expand into large branch trees per call site, most of them unreachable in normal
|
||||
operation — `src/game.c` reports over 1700 branches across 230 lines. Track line
|
||||
and function coverage; treat branch coverage as a relative signal within a file.
|
||||
|
||||
### Suites
|
||||
|
||||
Every suite is registered through the `AKGL_TEST_SUITES` list in
|
||||
`CMakeLists.txt`, which drives target creation, CTest registration, the
|
||||
`WORKING_DIRECTORY`/`TIMEOUT` properties, the link line, and the
|
||||
`FIXTURES_REQUIRED akgl_coverage` list together. Adding `tests/<name>.c` and the
|
||||
name to that list is all a new suite needs; it can no longer be accidentally
|
||||
left out of the coverage fixture.
|
||||
|
||||
Shared assertion helpers are in `tests/testutil.h`: `TEST_ASSERT`,
|
||||
`TEST_ASSERT_FEQ`, `TEST_EXPECT_STATUS`, `TEST_EXPECT_OK`, `TEST_EXPECT_ANY_ERROR`,
|
||||
and `TEST_ASSERT_FLAG`. All except the last expand to a `break` on failure, so
|
||||
they belong directly inside an `ATTEMPT` block, not inside a loop nested in one.
|
||||
|
||||
Done:
|
||||
|
||||
- `tests/physics.c` — both backends, the factory, and the full simulation loop
|
||||
including thrust clamping, drag, layer masking, parent/child positioning, and
|
||||
logic-interrupt handling. 100%.
|
||||
- `tests/heap.c` — pool exhaustion for all five pools, refcount clamping,
|
||||
recursive child release, registry cleanup on release. 100%.
|
||||
- `tests/json_helpers.c` — every typed accessor, both string accessors'
|
||||
allocate-vs-reuse paths, array bounds, and `akgl_get_json_with_default`. 100%.
|
||||
- `tests/controller.c` — control map push and capacity, the default binding set,
|
||||
keyboard and gamepad dispatch including cross-device rejection, the dpad
|
||||
handlers, and device add/remove against the dummy drivers. 90%.
|
||||
- `tests/game.c` — version gating, the save/load roundtrip, foreign-save
|
||||
rejection, truncated-table detection, the state lock, and FPS accounting. 54%.
|
||||
- `tests/actor.c` — extended with the eight control-map handlers, automatic
|
||||
facing, movement logic, the animation frame state machine, `akgl_actor_update`,
|
||||
and character/sprite binding lookups. 80%.
|
||||
|
||||
## Remaining work
|
||||
|
||||
### Needs the offscreen renderer harness
|
||||
|
||||
`src/renderer.c` (63 lines), `src/text.c` (28), `src/draw.c` (13),
|
||||
`src/assets.c` (21), `akgl_actor_render`/`actor_visible` in `src/actor.c` (53),
|
||||
and the drawing half of `src/tilemap.c` all need a live `renderer` global.
|
||||
|
||||
Build `tests/harness.c` / `tests/harness.h` with `akgl_test_init_headless()` and
|
||||
`akgl_test_shutdown_headless()`: set the dummy video and audio drivers,
|
||||
`SDL_Init()`, `akgl_heap_init()`, `akgl_registry_init()`, create a software
|
||||
`SDL_CreateWindowAndRenderer`, and point the global `renderer` at it. Four
|
||||
existing tests hand-roll this today (`tests/sprite.c:194`, `tests/character.c:200`,
|
||||
`tests/tilemap.c:421`, `tests/charviewer.c:42`); collapse them onto the shared
|
||||
harness in the same change. The new suites set the driver hints inline and need
|
||||
no window, so they are unaffected.
|
||||
|
||||
Then:
|
||||
|
||||
- **`tests/renderer.c`** — `akgl_render_init2d` populating all six function
|
||||
pointers and the camera; frame start/end against a NULL `sdl_renderer`; the
|
||||
rotated `draw_texture` path including `angle != 0` with a NULL center; the
|
||||
`draw_mesh` "not implemented" stub; and `draw_world` layer ordering. Note
|
||||
`defflags` at `src/renderer.c:113` is uninitialized until the `if` body runs.
|
||||
- **`tests/text.c`**, **`tests/draw.c`**, **`tests/assets.c`** — font loading
|
||||
into `AKGL_REGISTRY_FONT`, text rendering with wrap on and off, background
|
||||
drawing at zero/negative/oversized dimensions, and BGM loading into
|
||||
`AKGL_REGISTRY_MUSIC` under the dummy audio driver.
|
||||
- **`tests/tilemap.c` extensions** — `akgl_tilemap_draw`, `_draw_tileset`, and
|
||||
`akgl_tilemap_load_layer_image`.
|
||||
|
||||
### Does not need a renderer
|
||||
|
||||
- **`src/tilemap.c`** — `akgl_tilemap_scale_actor` is pure math over three
|
||||
branches (`src/tilemap.c:824-830`); `akgl_get_json_properties_number`,
|
||||
`_float`, and `_double` need only a JSON snippet; `akgl_tilemap_load_physics`
|
||||
needs a fixture with the physics property block present, absent, and
|
||||
malformed. Together roughly 100 of the 225 uncovered lines.
|
||||
- **`src/game.c`** — `akgl_game_init`, `akgl_game_update`, `akgl_game_lowfps`,
|
||||
and `akgl_game_updateFPS`'s frame loop need a window; revisit after the
|
||||
harness lands.
|
||||
- **`src/registry.c`** — `akgl_registry_load_properties` needs a fixture with a
|
||||
`properties` object, plus the missing-file, missing-key, and wrong-value-type
|
||||
cases. Assert the loop at `src/registry.c:148-158` does not leak the string
|
||||
heap.
|
||||
|
||||
## Defects
|
||||
|
||||
### Fixed while building the suites
|
||||
|
||||
Each was found by a test written to assert correct behavior.
|
||||
|
||||
1. **`akgl_physics_simulate` dereferenced `self` before its NULL check.**
|
||||
`src/physics.c:132` read `self->gravity_time` at declaration time, three
|
||||
lines above `FAIL_ZERO_RETURN(e, self, ...)`. A NULL backend segfaulted.
|
||||
2. **`akgl_game_save` never flushed or closed its stream.** `CLEANUP` and
|
||||
`PROCESS` were transposed, which put the `fclose` inside the `PROCESS`
|
||||
switch, where it only ran if an error context existed and reported success.
|
||||
An ordinary save produced an empty file.
|
||||
3. **`akgl_game_save_actors` wrote name-table terminators from a single char.**
|
||||
`aksl_fwrite((void *)&nullval, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp)` emitted
|
||||
127 bytes of adjacent stack memory into the save file and produced a sentinel
|
||||
the loader could not recognize.
|
||||
4. **`akgl_game_load_objectnamemap` swallowed read failures.** `CATCH` used
|
||||
directly inside `while (1)` breaks the loop, not the function, so a truncated
|
||||
or corrupt name table loaded as a successful game.
|
||||
5. **`akgl_Actor_cmhf_up_on` and `_down_on` dereferenced `basechar` unguarded**,
|
||||
unlike their left and right counterparts.
|
||||
6. **`akgl_actor_logic_movement` checked `actor` twice** instead of checking
|
||||
`actor->basechar` before dereferencing it.
|
||||
7. **The gamepad handlers checked `appstate` three times each**, so a NULL event
|
||||
or a missing player actor was never caught and `player->state` was
|
||||
dereferenced regardless.
|
||||
|
||||
### Known and still open
|
||||
|
||||
1. **`akgl_render_and_compare` compares a texture against itself.**
|
||||
`src/util.c:228` and `src/util.c:245` both draw `t1`; `t2` is never rendered,
|
||||
so the function always passes and the image assertions in `tests/sprite.c`
|
||||
assert nothing.
|
||||
2. **`akgl_tilemap_release` double-frees tileset textures.**
|
||||
`src/tilemap.c:849` destroys `dest->tilesets[i].texture` inside the *layers*
|
||||
loop; it should be `dest->layers[i].texture`. It also never NULLs the
|
||||
pointers, so a second release is a use-after-free.
|
||||
3. **`akgl_registry_init` never initializes the properties registry.**
|
||||
`akgl_registry_init_properties()` is not called from `akgl_registry_init()`
|
||||
(`src/registry.c:27`), so `AKGL_REGISTRY_PROPERTIES` stays 0 unless the
|
||||
caller initializes it separately. `akgl_set_property` is then a silent no-op
|
||||
and `akgl_get_property` always returns the caller's default, which means
|
||||
`akgl_physics_init_arcade` and `akgl_render_init2d` silently ignore
|
||||
configuration. `akgl_game_init` does call it; a caller that does not use
|
||||
`akgl_game_init` does not get it.
|
||||
4. **`akgl_path_relative_from` is a stub that leaks.** `src/util.c:105` claims a
|
||||
heap string, never writes `*dst`, and never releases it. 256 calls exhaust
|
||||
the string pool.
|
||||
5. **`akgl_compare_sdl_surfaces` memcmps without checking geometry.**
|
||||
`src/util.c:208` compares `s1->pitch * s1->h` bytes of `s2` without verifying
|
||||
the surfaces share dimensions, pitch, or format.
|
||||
6. **`akgl_string_initialize` overflows by four bytes when `init` is NULL.**
|
||||
`src/staticstring.c:17` does `memset(&obj->data, 0x00, sizeof(akgl_String))`,
|
||||
but `data` starts four bytes into the struct (after `refcount`), so the memset
|
||||
runs four bytes past the end.
|
||||
7. **Savegame name lengths disagree between writer and reader.**
|
||||
`akgl_game_save_actors` writes spritesheet names at
|
||||
`AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH` (512) and character names at
|
||||
`AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH` (128), but `akgl_game_load` reads
|
||||
every table at `AKGL_ACTOR_MAX_NAME_LENGTH` (128). A save with any registered
|
||||
spritesheet cannot be read back. The current roundtrip test passes only
|
||||
because empty registries write nothing but zeroed sentinels.
|
||||
8. **Heap acquire functions are asymmetric.** `akgl_heap_next_string` increments
|
||||
`refcount`; `next_actor`, `next_sprite`, `next_spritesheet`, and
|
||||
`next_character` do not. `tests/heap.c` pins the current behavior and says so;
|
||||
decide whether to make them symmetric or document the split.
|
||||
9. **`tests/util.c` defines `test_akgl_collide_point_rectangle_logic` but
|
||||
`main()` never calls it.**
|
||||
10. **`controller.h` declares functions that do not exist.** It declares
|
||||
`akgl_controller_handle_button_down`, `_button_up`, `_added`, and `_removed`,
|
||||
but `src/controller.c` defines them as `gamepad_handle_*`. Anything compiled
|
||||
against the header alone fails to link.
|
||||
11. **`akgl_controller_pushmap` and `akgl_controller_default` accept negative map
|
||||
ids.** Both check `controlmapid >= AKGL_MAX_CONTROL_MAPS` but not
|
||||
`controlmapid < 0`, so a negative id indexes before `GAME_ControlMaps`.
|
||||
|
||||
## Build notes
|
||||
|
||||
The vendored SDL satellite libraries are built into per-project subdirectories
|
||||
that are not on the loader's default search path, and `LD_LIBRARY_PATH` is
|
||||
searched ahead of RPATH — so a developer who has previously run `rebuild.sh` had
|
||||
their installed `libakgl.so` shadow the one under test, and a developer who had
|
||||
not saw every test abort before `main()` with "cannot open shared object file".
|
||||
`CMakeLists.txt` now sets `BUILD_RPATH` on the library, the utility, and every
|
||||
test target, and prepends the build tree to `LD_LIBRARY_PATH` for the CTest run.
|
||||
|
||||
## Carried over
|
||||
|
||||
1. **Make character-to-sprite state bindings release their references symmetrically.**
|
||||
`akgl_character_sprite_add()` increments the sprite refcount for every state-map
|
||||
binding, but no corresponding removal API exists. Replacing a state binding
|
||||
also leaves the previous sprite's refcount incremented. When
|
||||
`akgl_heap_release_character()` drops the character refcount to zero, it clears
|
||||
the character registry entry and zeroes the structure without enumerating
|
||||
`state_sprites`, decrementing the bound sprites, or destroying the SDL property
|
||||
map. Implement binding removal/replacement so each removed binding releases
|
||||
exactly one sprite reference. On final character release, enumerate every
|
||||
remaining binding (the existing `akgl_character_state_sprites_iterate()` release
|
||||
path may be reusable), release each reference, destroy `state_sprites`, and then
|
||||
clear the character. Add tests for removal, replacement, duplicate sprite
|
||||
bindings across multiple states, and final character release.
|
||||
|
||||
`akgl_character_state_sprites_iterate` is still uncovered and is the natural
|
||||
place to start; `tests/actor.c` now covers `akgl_character_sprite_get` and the
|
||||
composite-state binding behavior it depends on.
|
||||
2
deps/libakerror
vendored
2
deps/libakerror
vendored
Submodule deps/libakerror updated: 4a09ca87fd...0c0d81249f
2
deps/libakstdlib
vendored
2
deps/libakstdlib
vendored
Submodule deps/libakstdlib updated: b9d703f48a...a87cbfb26d
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file actor.h
|
||||
* @brief Declares the public actor API.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_ACTOR_H_
|
||||
#define _AKGL_ACTOR_H_
|
||||
|
||||
@@ -48,6 +53,7 @@
|
||||
// This is an array of strings equal to actor states from 1-32.
|
||||
// This is built by a utility script and not kept in git, see
|
||||
// the Makefile for lib_src/actor_state_string_names.c
|
||||
/** @brief Maps actor-state bit positions to symbolic names. */
|
||||
extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1];
|
||||
|
||||
#define AKGL_ACTOR_STATE_FACE_ALL (AKGL_ACTOR_STATE_FACE_DOWN | AKGL_ACTOR_STATE_FACE_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_FACE_UP)
|
||||
@@ -58,6 +64,7 @@ extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1];
|
||||
|
||||
#define AKGL_MAX_HEAP_ACTOR 64
|
||||
|
||||
/** @brief Represents a live actor, including state, motion, hierarchy, and behavior callbacks. */
|
||||
typedef struct akgl_Actor {
|
||||
uint8_t refcount;
|
||||
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||||
@@ -110,24 +117,146 @@ typedef struct akgl_Actor {
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*addchild)(struct akgl_Actor *obj, struct akgl_Actor *child);
|
||||
} akgl_Actor;
|
||||
|
||||
/**
|
||||
* @brief Actor initialize.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_initialize(akgl_Actor *obj, char *name);
|
||||
/**
|
||||
* @brief Actor set character.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param basecharname Registry name of the character to assign.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_set_character(akgl_Actor *obj, char *basecharname);
|
||||
/**
|
||||
* @brief Actor render.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_render(akgl_Actor *obj);
|
||||
/**
|
||||
* @brief Actor update.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_update(akgl_Actor *obj);
|
||||
/**
|
||||
* @brief Actor logic movement.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param dt Elapsed simulation time in seconds.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_movement(akgl_Actor *obj, float32_t dt);
|
||||
/**
|
||||
* @brief Actor logic changeframe.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param curSprite Sprite currently selected for the actor.
|
||||
* @param curtimems Current frame-clock value.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems);
|
||||
/**
|
||||
* @brief Actor automatic face.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_automatic_face(akgl_Actor *obj);
|
||||
/**
|
||||
* @brief Actor add child.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param child Actor to attach as a child.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_RELATIONSHIP When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child);
|
||||
|
||||
/**
|
||||
* @brief Actor cmhf left on.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf left off.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf right on.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf right off.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf up on.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf up off.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf down on.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf down off.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event);
|
||||
|
||||
/**
|
||||
* @brief Registry iterate actor.
|
||||
* @param userdata Caller data supplied to the SDL property iterator.
|
||||
* @param registry SDL property registry being queried.
|
||||
* @param name Registry key or human-readable object name.
|
||||
*/
|
||||
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name);
|
||||
|
||||
#endif // _AKGL_ACTOR_H_
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
/**
|
||||
* @file assets.h
|
||||
* @brief Declares the public assets API.
|
||||
*/
|
||||
|
||||
#ifndef _ASSETS_H_
|
||||
#define _ASSETS_H_
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
/**
|
||||
* @brief Load start bgm.
|
||||
* @param fname Path to the input or output file.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_load_start_bgm(char *fname);
|
||||
|
||||
#endif //_ASSETS_H_
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file character.h
|
||||
* @brief Declares the public character API.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_CHARACTER_H_
|
||||
#define _AKGL_CHARACTER_H_
|
||||
|
||||
@@ -8,6 +13,7 @@
|
||||
#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;
|
||||
char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH];
|
||||
@@ -24,13 +30,50 @@ typedef struct akgl_Character {
|
||||
} akgl_Character;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Character initialize.
|
||||
* @param basechar Character whose state-to-sprite map is accessed.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *basechar, char *name);
|
||||
/**
|
||||
* @brief Character sprite add.
|
||||
* @param basechar Character whose state-to-sprite map is accessed.
|
||||
* @param ref Sprite reference to associate with the character.
|
||||
* @param state Actor-state bit mask used as a sprite-map key.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state);
|
||||
/**
|
||||
* @brief Character sprite get.
|
||||
* @param basechar Character whose state-to-sprite map is accessed.
|
||||
* @param state Actor-state bit mask used as a sprite-map key.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest);
|
||||
|
||||
// This is an SDL iterator so we can't return our error state from it.
|
||||
/**
|
||||
* @brief Character state sprites iterate.
|
||||
* @param userdata Caller data supplied to the SDL property iterator.
|
||||
* @param props SDL property collection being iterated.
|
||||
* @param name Registry key or human-readable object name.
|
||||
*/
|
||||
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name);
|
||||
|
||||
/**
|
||||
* @brief Character load json.
|
||||
* @param filename Path to the source asset or JSON document.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_load_json(char *filename);
|
||||
|
||||
#endif // _AKGL_CHARACTER_H_
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file controller.h
|
||||
* @brief Declares the public controller API.
|
||||
*/
|
||||
|
||||
#ifndef _CONTROLLER_H_
|
||||
#define _CONTROLLER_H_
|
||||
|
||||
@@ -8,6 +13,7 @@
|
||||
#define AKGL_MAX_CONTROL_MAPS 8
|
||||
#define AKGL_MAX_CONTROLS 32
|
||||
|
||||
/** @brief Maps one SDL input to pressed and released callbacks. */
|
||||
typedef struct {
|
||||
uint32_t event_on;
|
||||
uint32_t event_off;
|
||||
@@ -20,6 +26,7 @@ typedef struct {
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event);
|
||||
} akgl_Control;
|
||||
|
||||
/** @brief Groups input bindings for one actor and its input devices. */
|
||||
typedef struct {
|
||||
akgl_Actor *target;
|
||||
uint16_t nextMap;
|
||||
@@ -30,20 +37,84 @@ typedef struct {
|
||||
SDL_PenID penid;
|
||||
} akgl_ControlMap;
|
||||
|
||||
/** @brief Stores all process-wide actor input maps. */
|
||||
extern akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
|
||||
|
||||
/**
|
||||
* @brief Controller list keyboards.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void);
|
||||
|
||||
/**
|
||||
* @brief Controller handle event.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_event(void *appstate, SDL_Event *event);
|
||||
|
||||
/**
|
||||
* @brief Controller handle button down.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_down(void *appstate, SDL_Event *event);
|
||||
/**
|
||||
* @brief Controller handle button up.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_up(void *appstate, SDL_Event *event);
|
||||
/**
|
||||
* @brief Controller handle added.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_added(void *appstate, SDL_Event *event);
|
||||
/**
|
||||
* @brief Controller handle removed.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate, SDL_Event *event);
|
||||
|
||||
/**
|
||||
* @brief Controller pushmap.
|
||||
* @param controlmapid Index of the control map to update.
|
||||
* @param control Control binding to append to the selected map.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control);
|
||||
|
||||
/**
|
||||
* @brief Controller default.
|
||||
* @param controlmapid Index of the control map to update.
|
||||
* @param actorname Registry name of the controlled actor.
|
||||
* @param kbid SDL keyboard identifier assigned to the map.
|
||||
* @param jsid SDL joystick or gamepad identifier assigned to the map.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_REGISTRY When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid);
|
||||
|
||||
/**
|
||||
* @brief Controller open gamepads.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void);
|
||||
#endif // _CONTROLLER_H_
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
/**
|
||||
* @file draw.h
|
||||
* @brief Declares the public draw API.
|
||||
*/
|
||||
|
||||
#ifndef _DRAW_H_
|
||||
#define _DRAW_H_
|
||||
|
||||
/**
|
||||
* @brief Draw background.
|
||||
* @param w Destination width.
|
||||
* @param h Destination height.
|
||||
*/
|
||||
void akgl_draw_background(int w, int h);
|
||||
|
||||
#endif //_DRAW_H_
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file error.h
|
||||
* @brief Declares the public error API.
|
||||
*/
|
||||
|
||||
#ifndef _ERROR_H_
|
||||
#define _ERROR_H_
|
||||
|
||||
@@ -12,7 +17,10 @@
|
||||
#define RESTORE_GCC_WARNINGS \
|
||||
_Pragma("GCC diagnostic pop")
|
||||
|
||||
#define AKGL_ERR_SDL AKERR_LAST_ERRNO_VALUE + 1
|
||||
#define AKGL_ERR_LOGICINTERRUPT AKERR_LAST_ERRNO_VALUE + 2
|
||||
#define AKGL_ERR_SDL (AKERR_LAST_ERRNO_VALUE + 18)
|
||||
#define AKGL_ERR_REGISTRY (AKERR_LAST_ERRNO_VALUE + 19)
|
||||
#define AKGL_ERR_HEAP (AKERR_LAST_ERRNO_VALUE + 20)
|
||||
#define AKGL_ERR_BEHAVIOR (AKERR_LAST_ERRNO_VALUE + 21)
|
||||
#define AKGL_ERR_LOGICINTERRUPT (AKERR_LAST_ERRNO_VALUE + 22)
|
||||
|
||||
#endif // _ERROR_H_
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file game.h
|
||||
* @brief Declares the public game API.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_GAME_H_
|
||||
#define _AKGL_GAME_H_
|
||||
|
||||
@@ -18,16 +23,19 @@
|
||||
|
||||
/* ==================== GAME STATE VARIABLES =================== */
|
||||
|
||||
/** @brief Describes a renderable frame. */
|
||||
typedef struct {
|
||||
float32_t w;
|
||||
float32_t h;
|
||||
SDL_Texture *texture;
|
||||
} akgl_Frame;
|
||||
|
||||
/** @brief Stores application-defined game-state flags. */
|
||||
typedef struct {
|
||||
int32_t flags;
|
||||
} akgl_GameState;
|
||||
|
||||
/** @brief Stores game metadata, timing, synchronization, and FPS accounting. */
|
||||
typedef struct {
|
||||
char libversion[32];
|
||||
char version[32];
|
||||
@@ -43,19 +51,32 @@ typedef struct {
|
||||
void (*lowfpsfunc)(void);
|
||||
} akgl_Game;
|
||||
|
||||
/** @brief SDL window used by the active renderer. */
|
||||
extern SDL_Window *window;
|
||||
/** @brief Loaded background-music resource. */
|
||||
extern MIX_Audio *bgm;
|
||||
/** @brief Mixer used for game audio playback. */
|
||||
extern MIX_Mixer *akgl_mixer;
|
||||
/** @brief Process-wide audio-track table. */
|
||||
extern MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
|
||||
/** @brief Storage for the default camera. */
|
||||
extern SDL_FRect _akgl_camera;
|
||||
/** @brief Process-wide game metadata and timing state. */
|
||||
extern akgl_Game game;
|
||||
/** @brief Storage for the default renderer. */
|
||||
extern akgl_RenderBackend _akgl_renderer;
|
||||
/** @brief Storage for the default physics backend. */
|
||||
extern akgl_PhysicsBackend _akgl_physics;
|
||||
/** @brief Storage for the default tilemap. */
|
||||
extern akgl_Tilemap _akgl_gamemap;
|
||||
|
||||
/** @brief Currently active tilemap. */
|
||||
extern akgl_Tilemap *gamemap;
|
||||
/** @brief Currently active renderer. */
|
||||
extern akgl_RenderBackend *renderer;
|
||||
/** @brief Currently active physics backend. */
|
||||
extern akgl_PhysicsBackend *physics;
|
||||
/** @brief Currently active camera. */
|
||||
extern SDL_FRect *camera;
|
||||
|
||||
#define AKGL_BITMASK_HAS(x, y) (x & y) == y
|
||||
@@ -64,14 +85,60 @@ extern SDL_FRect *camera;
|
||||
#define AKGL_BITMASK_DEL(x, y) x &= ~(y)
|
||||
#define AKGL_BITMASK_CLEAR(x) x = 0;
|
||||
|
||||
/**
|
||||
* @brief Game init.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init();
|
||||
/**
|
||||
* @brief Game init screen.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init_screen();
|
||||
/**
|
||||
* @brief Game updatefps.
|
||||
*/
|
||||
void akgl_game_updateFPS();
|
||||
/**
|
||||
* @brief Game save.
|
||||
* @param fpath Path to the input or output file.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath);
|
||||
/**
|
||||
* @brief Game load.
|
||||
* @param fpath Path to the input or output file.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath);
|
||||
/**
|
||||
* @brief Game lowfps.
|
||||
*/
|
||||
void akgl_game_lowfps(void);
|
||||
/**
|
||||
* @brief Game state lock.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void);
|
||||
/**
|
||||
* @brief Game state unlock.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
|
||||
/**
|
||||
* @brief Game update.
|
||||
* @param opflags Optional iterator operation flags; `NULL` selects defaults.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags);
|
||||
|
||||
#endif //_AKGL_GAME_H_
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file heap.h
|
||||
* @brief Declares the public heap API.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_HEAP_H_
|
||||
#define _AKGL_HEAP_H_
|
||||
|
||||
@@ -23,24 +28,103 @@
|
||||
#define AKGL_MAX_HEAP_STRING 256
|
||||
#endif
|
||||
|
||||
/** @brief Fixed-capacity actor object pool. */
|
||||
extern akgl_Actor HEAP_ACTOR[AKGL_MAX_HEAP_ACTOR];
|
||||
/** @brief Fixed-capacity sprite object pool. */
|
||||
extern akgl_Sprite HEAP_SPRITE[AKGL_MAX_HEAP_SPRITE];
|
||||
/** @brief Fixed-capacity spritesheet object pool. */
|
||||
extern akgl_SpriteSheet HEAP_SPRITESHEET[AKGL_MAX_HEAP_SPRITESHEET];
|
||||
/** @brief Fixed-capacity character object pool. */
|
||||
extern akgl_Character HEAP_CHARACTER[AKGL_MAX_HEAP_CHARACTER];
|
||||
/** @brief Fixed-capacity static-string object pool. */
|
||||
extern akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING];
|
||||
|
||||
/**
|
||||
* @brief Heap init.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_BEHAVIOR When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_LOGICINTERRUPT When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_REGISTRY When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init();
|
||||
/**
|
||||
* @brief Heap init actor.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor();
|
||||
/**
|
||||
* @brief Heap next actor.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_actor(akgl_Actor **dest);
|
||||
/**
|
||||
* @brief Heap next sprite.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_sprite(akgl_Sprite **dest);
|
||||
/**
|
||||
* @brief Heap next spritesheet.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest);
|
||||
/**
|
||||
* @brief Heap next character.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_character(akgl_Character **dest);
|
||||
/**
|
||||
* @brief Heap next string.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_string(akgl_String **dest);
|
||||
|
||||
/**
|
||||
* @brief Heap release actor.
|
||||
* @param ptr Heap object whose reference count is decremented.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_actor(akgl_Actor *ptr);
|
||||
/**
|
||||
* @brief Heap release sprite.
|
||||
* @param ptr Heap object whose reference count is decremented.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_sprite(akgl_Sprite *ptr);
|
||||
/**
|
||||
* @brief Heap release spritesheet.
|
||||
* @param ptr Heap object whose reference count is decremented.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_spritesheet(akgl_SpriteSheet *ptr);
|
||||
/**
|
||||
* @brief Heap release character.
|
||||
* @param ptr Heap object whose reference count is decremented.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_character(akgl_Character *ptr);
|
||||
/**
|
||||
* @brief Heap release string.
|
||||
* @param ptr Heap object whose reference count is decremented.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_string(akgl_String *ptr);
|
||||
|
||||
#endif //_AKGL_HEAP_H_
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
/**
|
||||
* @file iterator.h
|
||||
* @brief Declares the public iterator API.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_ITERATOR_H_
|
||||
#define _AKGL_ITERATOR_H_
|
||||
|
||||
/** @brief Selects operations and an optional layer for actor traversal. */
|
||||
typedef struct {
|
||||
uint32_t flags;
|
||||
uint8_t layerid;
|
||||
|
||||
@@ -1,20 +1,136 @@
|
||||
/**
|
||||
* @file json_helpers.h
|
||||
* @brief Declares the public json helpers API.
|
||||
*/
|
||||
|
||||
#ifndef _JSON_HELPERS_H_
|
||||
#define _JSON_HELPERS_H_
|
||||
|
||||
#include <akerror.h>
|
||||
#include "staticstring.h"
|
||||
|
||||
/**
|
||||
* @brief Get json object value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest);
|
||||
/**
|
||||
* @brief Get json boolean value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest);
|
||||
/**
|
||||
* @brief Get json integer value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_integer_value(json_t *obj, char *key, int *dest);
|
||||
/**
|
||||
* @brief Get json number value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char *key, float *dest);
|
||||
/**
|
||||
* @brief Get json double value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest);
|
||||
/**
|
||||
* @brief Get json string value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest);
|
||||
/**
|
||||
* @brief Get json array value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest);
|
||||
/**
|
||||
* @brief Get json array index object.
|
||||
* @param array JSON array to read.
|
||||
* @param index Zero-based element index.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest);
|
||||
/**
|
||||
* @brief Get json array index integer.
|
||||
* @param array JSON array to read.
|
||||
* @param index Zero-based element index.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_integer(json_t *array, int index, int *dest);
|
||||
/**
|
||||
* @brief Get json array index string.
|
||||
* @param array JSON array to read.
|
||||
* @param index Zero-based element index.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest);
|
||||
|
||||
/**
|
||||
* @brief Get json with default.
|
||||
* @param e Error context to inspect and optionally consume.
|
||||
* @param defval Fallback value copied when the supplied error is eligible.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param defsize Size in bytes of the fallback value.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_INDEX When the corresponding validation or operation fails.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize);
|
||||
|
||||
#endif // _JSON_HELPERS_H_
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file physics.h
|
||||
* @brief Declares the public physics API.
|
||||
*/
|
||||
|
||||
#ifndef _PHYSICS_H_
|
||||
#define _PHYSICS_H_
|
||||
|
||||
@@ -7,6 +12,7 @@
|
||||
#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);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*gravity)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
@@ -23,19 +29,96 @@ typedef struct akgl_PhysicsBackend {
|
||||
SDL_Time timer_gravity;
|
||||
} akgl_PhysicsBackend;
|
||||
|
||||
/**
|
||||
* @brief Physics null gravity.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param actor Actor to inspect or modify.
|
||||
* @param dt Elapsed simulation time in seconds.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Physics null collide.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param a1 First actor supplied to collision handling.
|
||||
* @param a2 Second actor supplied to collision handling.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
||||
/**
|
||||
* @brief Physics null move.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param actor Actor to inspect or modify.
|
||||
* @param dt Elapsed simulation time in seconds.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Physics init null.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *self);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Physics arcade gravity.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param actor Actor to inspect or modify.
|
||||
* @param dt Elapsed simulation time in seconds.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Physics arcade collide.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param a1 First actor supplied to collision handling.
|
||||
* @param a2 Second actor supplied to collision handling.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
||||
/**
|
||||
* @brief Physics arcade move.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param actor Actor to inspect or modify.
|
||||
* @param dt Elapsed simulation time in seconds.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Physics init arcade.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self);
|
||||
|
||||
/**
|
||||
* @brief Physics factory.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param type Expected JSON property type or backend type name.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type);
|
||||
|
||||
/**
|
||||
* @brief Physics simulate.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param opflags Optional iterator operation flags; `NULL` selects defaults.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_LOGICINTERRUPT When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags);
|
||||
|
||||
#endif // _PHYSICS_H_
|
||||
|
||||
@@ -1,29 +1,108 @@
|
||||
/**
|
||||
* @file registry.h
|
||||
* @brief Declares the public registry API.
|
||||
*/
|
||||
|
||||
#ifndef _REGISTRY_H_
|
||||
#define _REGISTRY_H_
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
/** @brief Registry of actors keyed by name. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR;
|
||||
/** @brief Registry of sprites keyed by name. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_SPRITE;
|
||||
/** @brief Registry of spritesheets keyed by asset path. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET;
|
||||
/** @brief Registry of characters keyed by name. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_CHARACTER;
|
||||
/** @brief Registry mapping actor-state names to bit values. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR_STATE_STRINGS;
|
||||
/** @brief Registry of fonts keyed by name. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_FONT;
|
||||
/** @brief Registry of music assets keyed by name. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_MUSIC;
|
||||
/** @brief Registry of string configuration properties. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_PROPERTIES;
|
||||
|
||||
/**
|
||||
* @brief Registry init.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init();
|
||||
/**
|
||||
* @brief Registry init music.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music();
|
||||
/**
|
||||
* @brief Registry init font.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font();
|
||||
/**
|
||||
* @brief Registry init actor.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor();
|
||||
/**
|
||||
* @brief Registry init sprite.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite();
|
||||
/**
|
||||
* @brief Registry init spritesheet.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet();
|
||||
/**
|
||||
* @brief Registry init character.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character();
|
||||
/**
|
||||
* @brief Registry init properties.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_properties();
|
||||
/**
|
||||
* @brief Registry load properties.
|
||||
* @param fname Path to the input or output file.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname);
|
||||
/**
|
||||
* @brief Registry init actor state strings.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor_state_strings();
|
||||
/**
|
||||
* @brief Set property.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @param value Property value to store.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *value);
|
||||
/**
|
||||
* @brief Get property.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param def Fallback string returned when the property is absent.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def);
|
||||
|
||||
#endif //_REGISTRY_H_
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file renderer.h
|
||||
* @brief Declares the public renderer API.
|
||||
*/
|
||||
|
||||
#ifndef _RENDERER_H_
|
||||
#define _RENDERER_H_
|
||||
|
||||
@@ -7,6 +12,7 @@
|
||||
|
||||
#include <akgl/iterator.h>
|
||||
|
||||
/** @brief Defines a pluggable renderer backend and drawing callbacks. */
|
||||
typedef struct akgl_RenderBackend {
|
||||
SDL_Renderer *sdl_renderer;
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self);
|
||||
@@ -17,13 +23,63 @@ typedef struct akgl_RenderBackend {
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags);
|
||||
} akgl_RenderBackend;
|
||||
|
||||
/**
|
||||
* @brief Render 2d shutdown.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief Render 2d frame start.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief Render 2d frame end.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief Render 2d draw texture.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param texture SDL texture to draw.
|
||||
* @param src Source value to copy or inspect.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param angle Clockwise rotation angle in degrees.
|
||||
* @param center Optional rotation center.
|
||||
* @param flip SDL texture flip mode.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_texture(akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip);
|
||||
/**
|
||||
* @brief Render 2d draw mesh.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief Render 2d draw world.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param opflags Optional iterator operation flags; `NULL` selects defaults.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags);
|
||||
|
||||
/**
|
||||
* @brief Render init2d.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self);
|
||||
|
||||
#endif // _RENDERER_H_
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file sprite.h
|
||||
* @brief Declares the public sprite API.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_SPRITE_H_
|
||||
#define _AKGL_SPRITE_H_
|
||||
|
||||
@@ -14,6 +19,7 @@
|
||||
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
|
||||
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
|
||||
|
||||
/** @brief Stores a loaded spritesheet texture and frame geometry. */
|
||||
typedef struct {
|
||||
uint8_t refcount;
|
||||
SDL_Texture *texture;
|
||||
@@ -22,6 +28,7 @@ typedef struct {
|
||||
uint16_t sprite_h;
|
||||
} akgl_SpriteSheet;
|
||||
|
||||
/** @brief Describes an animated sprite and its spritesheet reference. */
|
||||
typedef struct {
|
||||
uint8_t refcount;
|
||||
uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; // which IDs on the spritesheet belong to our frames
|
||||
@@ -36,11 +43,46 @@ typedef struct {
|
||||
} akgl_Sprite;
|
||||
|
||||
// initializes a new sprite to use the given sheet and otherwise sets to zero
|
||||
/**
|
||||
* @brief Sprite initialize.
|
||||
* @param spr Sprite object to initialize.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @param sheet Spritesheet used by the sprite.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet);
|
||||
// loads a given image file into a new spritesheet
|
||||
/**
|
||||
* @brief Spritesheet initialize.
|
||||
* @param sheet Spritesheet used by the sprite.
|
||||
* @param sprite_w Width of one sprite frame in pixels.
|
||||
* @param sprite_h Height of one sprite frame in pixels.
|
||||
* @param filename Path to the source asset or JSON document.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename);
|
||||
/**
|
||||
* @brief Sprite load json.
|
||||
* @param filename Path to the source asset or JSON document.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_load_json(char *filename);
|
||||
|
||||
/**
|
||||
* @brief Sprite sheet coords for frame.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param srccoords Output source rectangle for the selected frame.
|
||||
* @param frameid Sprite frame index to resolve.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid);
|
||||
|
||||
#endif //_AKGL_SPRITE_H_
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file staticstring.h
|
||||
* @brief Declares the public staticstring API.
|
||||
*/
|
||||
|
||||
#ifndef _STRING_H_
|
||||
#define _STRING_H_
|
||||
|
||||
@@ -7,12 +12,28 @@
|
||||
|
||||
#define AKGL_MAX_STRING_LENGTH PATH_MAX
|
||||
|
||||
/** @brief Provides a fixed-capacity, heap-managed string buffer. */
|
||||
typedef struct
|
||||
{
|
||||
int refcount;
|
||||
char data[AKGL_MAX_STRING_LENGTH];
|
||||
} akgl_String;
|
||||
|
||||
/**
|
||||
* @brief String initialize.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param init Optional initial string contents; `NULL` clears the buffer.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_initialize(akgl_String *obj, char *init);
|
||||
/**
|
||||
* @brief String copy.
|
||||
* @param src Source value to copy or inspect.
|
||||
* @param dst Output destination populated by the function.
|
||||
* @param count Maximum number of elements or bytes to process; zero selects the default.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_copy(akgl_String *src, akgl_String *dst, int count);
|
||||
#endif //_STRING_H_
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
/**
|
||||
* @file text.h
|
||||
* @brief Declares the public text API.
|
||||
*/
|
||||
|
||||
#ifndef _TEXT_H_
|
||||
#define _TEXT_H_
|
||||
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
|
||||
/**
|
||||
* @brief Text loadfont.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @param filepath Path to the requested file.
|
||||
* @param size Requested font size.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size);
|
||||
/**
|
||||
* @brief Text rendertextat.
|
||||
* @param font Font used to render the text.
|
||||
* @param text UTF-8 text to render.
|
||||
* @param color Text color.
|
||||
* @param wraplength Maximum rendered line width; zero disables wrapping.
|
||||
* @param x Horizontal destination coordinate.
|
||||
* @param y Vertical destination coordinate.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y);
|
||||
|
||||
#endif // _TEXT_H_
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file tilemap.h
|
||||
* @brief Declares the public tilemap API.
|
||||
*/
|
||||
|
||||
#ifndef _TILEMAP_H_
|
||||
#define _TILEMAP_H_
|
||||
|
||||
@@ -23,6 +28,7 @@
|
||||
#define AKGL_TILEMAP_LAYER_TYPE_OBJECTS 2
|
||||
#define AKGL_TILEMAP_LAYER_TYPE_IMAGE 3
|
||||
|
||||
/** @brief Stores tileset metadata, texture, and frame offsets. */
|
||||
typedef struct {
|
||||
float x;
|
||||
float y;
|
||||
@@ -37,6 +43,7 @@ typedef struct {
|
||||
char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE];
|
||||
} akgl_TilemapObject;
|
||||
|
||||
/** @brief Represents an object embedded in a tilemap layer. */
|
||||
typedef struct {
|
||||
short type;
|
||||
float opacity;
|
||||
@@ -51,6 +58,7 @@ typedef struct {
|
||||
akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER];
|
||||
} akgl_TilemapLayer;
|
||||
|
||||
/** @brief Stores tile, image, or object data for one map layer. */
|
||||
typedef struct {
|
||||
int columns;
|
||||
int firstgid;
|
||||
@@ -81,6 +89,7 @@ typedef struct {
|
||||
int margin;
|
||||
} akgl_Tileset;
|
||||
|
||||
/** @brief Represents a complete tilemap and its optional physics backend. */
|
||||
typedef struct {
|
||||
int tilewidth;
|
||||
int tileheight;
|
||||
@@ -105,8 +114,32 @@ typedef struct {
|
||||
akgl_PhysicsBackend physics;
|
||||
} akgl_Tilemap;
|
||||
|
||||
/**
|
||||
* @brief Tilemap load.
|
||||
* @param fname Path to the input or output file.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load(char *fname, akgl_Tilemap *dest);
|
||||
/**
|
||||
* @brief Tilemap draw.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param viewport World-space rectangle visible to the renderer.
|
||||
* @param layeridx Zero-based tilemap layer index.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRect *viewport, int layeridx);
|
||||
/**
|
||||
* @brief Tilemap draw tileset.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param tilesetidx Zero-based tileset index.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest, int tilesetidx);
|
||||
|
||||
/*
|
||||
@@ -114,16 +147,108 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest,
|
||||
* They are only exposed here for unit testing.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Get json tilemap property.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param type Expected JSON property type or backend type name.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest);
|
||||
/**
|
||||
* @brief Get json properties string.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest);
|
||||
/**
|
||||
* @brief Get json properties integer.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest);
|
||||
/**
|
||||
* @brief Tilemap compute tileset offsets.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param tilesetidx Zero-based tileset index.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx);
|
||||
/**
|
||||
* @brief Tilemap load layer objects.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param layerid Zero-based tilemap layer index.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Tilemap load layer tile.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param layerid Zero-based tilemap layer index.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Tilemap load layers.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Tilemap load tilesets each.
|
||||
* @param tileset JSON tileset object to load.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param tsidx Zero-based destination tileset index.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Tilemap load tilesets.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Tilemap release.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest);
|
||||
/**
|
||||
* @brief Tilemap scale actor.
|
||||
* @param map Tilemap to inspect, draw, or modify.
|
||||
* @param actor Actor to inspect or modify.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor);
|
||||
|
||||
#endif //_TILEMAP_H_
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file types.h
|
||||
* @brief Declares the public types API.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_TYPES_H_
|
||||
#define _AKGL_TYPES_H_
|
||||
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
/**
|
||||
* @file util.h
|
||||
* @brief Declares the public util API.
|
||||
*/
|
||||
|
||||
#ifndef _UTIL_H_
|
||||
#define _UTIL_H_
|
||||
|
||||
#include <akerror.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
/** @brief Represents a two-dimensional point. */
|
||||
typedef struct point {
|
||||
int x;
|
||||
int y;
|
||||
int z;
|
||||
} point;
|
||||
|
||||
/** @brief Stores the corners of an axis-aligned rectangle. */
|
||||
typedef struct RectanglePoints {
|
||||
point topleft;
|
||||
point topright;
|
||||
@@ -19,14 +26,67 @@ typedef struct RectanglePoints {
|
||||
|
||||
#define AKGL_COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x)
|
||||
|
||||
/**
|
||||
* @brief Rectangle points.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param rect Rectangle whose corner points are calculated.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect);
|
||||
/**
|
||||
* @brief Collide point rectangle.
|
||||
* @param p Point to test for containment.
|
||||
* @param r Rectangle corner data used for collision testing.
|
||||
* @param collide Output set to whether the tested geometry intersects.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(point *p, RectanglePoints *r, bool *collide);
|
||||
/**
|
||||
* @brief Collide rectangles.
|
||||
* @param r1 First rectangle to compare.
|
||||
* @param r2 Second rectangle to compare.
|
||||
* @param collide Output set to whether the tested geometry intersects.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);
|
||||
|
||||
/**
|
||||
* @brief Path relative.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param path Path to resolve or transform.
|
||||
* @param dst Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, akgl_String *dst);
|
||||
|
||||
// These are REALLY slow routines that are only useful in testing harnesses
|
||||
/**
|
||||
* @brief Compare sdl surfaces.
|
||||
* @param s1 First SDL surface to compare.
|
||||
* @param s2 Second SDL surface to compare.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_VALUE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2);
|
||||
/**
|
||||
* @brief Render and compare.
|
||||
* @param t1 First texture to render for comparison.
|
||||
* @param t2 Second texture to render for comparison.
|
||||
* @param x Horizontal destination coordinate.
|
||||
* @param y Vertical destination coordinate.
|
||||
* @param w Destination width.
|
||||
* @param h Destination height.
|
||||
* @param writeout Optional filename for the rendered diagnostic image.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_IO When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout);
|
||||
|
||||
#endif // _UTIL_H_
|
||||
|
||||
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())
|
||||
50
src/actor.c
50
src/actor.c
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file actor.c
|
||||
* @brief Implements the actor subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <string.h>
|
||||
@@ -122,7 +127,7 @@ akerr_ErrorContext *akgl_actor_logic_movement(akgl_Actor *actor, float32_t dt)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
||||
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor->basechar");
|
||||
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;
|
||||
@@ -168,6 +173,15 @@ akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Actor visible.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param camera Camera rectangle used for visibility testing.
|
||||
* @param visible Output set to whether the actor intersects the camera.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, bool *visible)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -274,6 +288,36 @@ akerr_ErrorContext *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child)
|
||||
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
|
||||
}
|
||||
|
||||
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_Iterator *opflags = (akgl_Iterator *)userdata;
|
||||
akgl_Actor *obj = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "registry_iterate_actor received NULL property name");
|
||||
FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "received NULL iterator flags");
|
||||
obj = SDL_GetPointerProperty(registry, name, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, obj, AKERR_KEY, "registry_iterate_actor received property name that was not in the registry");
|
||||
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) && obj->layer != opflags->layerid) {
|
||||
break;
|
||||
}
|
||||
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_UPDATE)) {
|
||||
CATCH(errctx, obj->updatefunc(obj));
|
||||
}
|
||||
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE)) {
|
||||
CATCH(errctx, akgl_tilemap_scale_actor(gamemap, obj));
|
||||
} else {
|
||||
obj->scale = 1.0;
|
||||
}
|
||||
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RENDER)) {
|
||||
CATCH(errctx, obj->renderfunc(obj));
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -337,7 +381,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Ev
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
//SDL_Log("event %d (button %d / key %d) moves actor up", event->type, event->gbutton.which, event->key.key);
|
||||
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));
|
||||
@@ -365,6 +410,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
|
||||
//SDL_Log("event %d (button %d / key %d) moves actor down", event->type, event->gbutton.which, event->key.key);
|
||||
obj->ay = obj->basechar->ay;
|
||||
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file actor_state_string_names.c
|
||||
* @brief Implements the actor state string names subsystem.
|
||||
*/
|
||||
|
||||
char *AKGL_ACTOR_STATE_STRING_NAMES[32] = {
|
||||
"AKGL_ACTOR_STATE_FACE_DOWN",
|
||||
"AKGL_ACTOR_STATE_FACE_LEFT",
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file assets.c
|
||||
* @brief Implements the assets subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file character.c
|
||||
* @brief Implements the character subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <akerror.h>
|
||||
@@ -85,6 +90,14 @@ void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID regis
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Character load json state int from strings.
|
||||
* @param states JSON array of actor-state names.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_t *states, int *dest)
|
||||
{
|
||||
int i = 0;
|
||||
@@ -109,6 +122,13 @@ static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Character load json inner.
|
||||
* @param json JSON object to parse.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Character *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file controller.c
|
||||
* @brief Implements the controller subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/heap.h>
|
||||
@@ -106,6 +111,13 @@ _akgl_controller_handle_event_success:
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gamepad handle button down.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
||||
{
|
||||
akgl_Actor *player = NULL;
|
||||
@@ -113,9 +125,9 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "Player actor does not exist");
|
||||
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
|
||||
|
||||
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
|
||||
event->key.key == SDLK_DOWN ) {
|
||||
@@ -157,6 +169,13 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gamepad handle button up.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
||||
{
|
||||
akgl_Actor *player = NULL;
|
||||
@@ -164,10 +183,10 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "Player actor does not exist");
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
|
||||
|
||||
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
|
||||
event->key.key == SDLK_DOWN ) {
|
||||
SDL_Log("processing down release");
|
||||
@@ -196,6 +215,13 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gamepad handle added.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
||||
{
|
||||
SDL_JoystickID which;
|
||||
@@ -204,7 +230,7 @@ akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
|
||||
which = event->gbutton.which;
|
||||
gamepad = SDL_GetGamepadFromID(which);
|
||||
@@ -226,6 +252,13 @@ akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gamepad handle removed.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
|
||||
{
|
||||
SDL_JoystickID which;
|
||||
@@ -233,7 +266,7 @@ akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
|
||||
which = event->gbutton.which;
|
||||
gamepad = SDL_GetGamepadFromID(which);
|
||||
@@ -277,7 +310,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
|
||||
controlmap->jsid = jsid;
|
||||
|
||||
controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, controlmap->target, AKERR_REGISTRY, "Actor %s not found in registry", actorname);
|
||||
FAIL_ZERO_BREAK(errctx, controlmap->target, AKGL_ERR_REGISTRY, "Actor %s not found in registry", actorname);
|
||||
|
||||
// ---- KEYBOARD CONTROLS ----
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file draw.c
|
||||
* @brief Implements the draw subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
|
||||
105
src/game.c
105
src/game.c
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file game.c
|
||||
* @brief Implements the game subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
@@ -165,6 +170,12 @@ void akgl_game_updateFPS()
|
||||
* entity name -> pointer map tables
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Serializes an actor name-to-pointer entry.
|
||||
* @param userdata Open output stream supplied by the caller.
|
||||
* @param props Actor registry being iterated.
|
||||
* @param name Actor registry key to serialize.
|
||||
*/
|
||||
void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
FILE *fp = (FILE *)userdata;
|
||||
@@ -180,6 +191,12 @@ void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, c
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game save spritename iterator.
|
||||
* @param userdata Caller data supplied to the SDL property iterator.
|
||||
* @param props SDL property collection being iterated.
|
||||
* @param name Registry key or human-readable object name.
|
||||
*/
|
||||
void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
FILE *fp = (FILE *)userdata;
|
||||
@@ -195,6 +212,12 @@ void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props,
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game save spritesheetname iterator.
|
||||
* @param userdata Caller data supplied to the SDL property iterator.
|
||||
* @param props SDL property collection being iterated.
|
||||
* @param name Registry key or human-readable object name.
|
||||
*/
|
||||
void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
FILE *fp = (FILE *)userdata;
|
||||
@@ -210,6 +233,12 @@ void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID pr
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game save charactername iterator.
|
||||
* @param userdata Caller data supplied to the SDL property iterator.
|
||||
* @param props SDL property collection being iterated.
|
||||
* @param name Registry key or human-readable object name.
|
||||
*/
|
||||
void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
FILE *fp = (FILE *)userdata;
|
||||
@@ -225,11 +254,26 @@ void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID prop
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game save actors.
|
||||
* @param fp Open save-game stream.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
char nullval = 0x00;
|
||||
|
||||
// Each name table ends with a zeroed name field and a zeroed pointer, which
|
||||
// is what akgl_game_load_objectnamemap() looks for to stop reading. The
|
||||
// terminator has to come from a buffer at least as long as the longest name
|
||||
// field: writing N bytes from the address of a single char would emit N-1
|
||||
// bytes of whatever happened to follow it on the stack, which both leaks
|
||||
// stack contents into the save file and produces a sentinel the loader
|
||||
// cannot recognize.
|
||||
char nullbuf[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
|
||||
|
||||
memset((void *)&nullbuf, 0x00, sizeof(nullbuf));
|
||||
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
// write the actor name pointer table
|
||||
@@ -237,29 +281,29 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
|
||||
AKGL_REGISTRY_ACTOR,
|
||||
&akgl_game_save_actorname_iterator,
|
||||
(void *)fp);
|
||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_Actor *), fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((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, aksl_fwrite((void *)&nullval, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_Sprite *), fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((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, aksl_fwrite((void *)&nullval, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_SpriteSheet *), fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((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, aksl_fwrite((void *)&nullval, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullval, 1, sizeof(akgl_Character *), fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_Character *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
@@ -276,27 +320,49 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
|
||||
CATCH(e, aksl_fopen(fpath, "wb", &fp));
|
||||
CATCH(e, aksl_fwrite(&game, 1, sizeof(akgl_Game), fp));
|
||||
CATCH(e, akgl_game_save_actors(fp));
|
||||
} PROCESS(e) {
|
||||
} CLEANUP {
|
||||
// CLEANUP must precede PROCESS: with the two transposed, the fclose
|
||||
// lands inside the PROCESS switch and only runs when an error context
|
||||
// exists and reports success, so an ordinary save never flushed or
|
||||
// closed its stream.
|
||||
if ( fp != NULL )
|
||||
fclose(fp);
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e); // SUCCEED_NORETURN if in main().
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game load objectnamemap.
|
||||
* @param fp Open save-game stream.
|
||||
* @param map Tilemap to inspect, draw, or modify.
|
||||
* @param namelength Fixed serialized name-field length.
|
||||
* @param ptrlength Serialized pointer-field length.
|
||||
* @param registry SDL property registry being queried.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry)
|
||||
{
|
||||
void *ptr = NULL;
|
||||
char ptrstring[32];
|
||||
char objname[namelength];
|
||||
int retval = 0;
|
||||
|
||||
bool done = false;
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
while ( 1 ) {
|
||||
// The ATTEMPT block sits inside the loop on purpose. CATCH reports a
|
||||
// failure by breaking, and a break binds to the innermost enclosing switch
|
||||
// or loop, so a CATCH written directly inside `while` would leave the loop
|
||||
// and then fall through to SUCCEED_RETURN -- reporting a truncated or
|
||||
// corrupt name table as a successful load.
|
||||
while ( done == false ) {
|
||||
ATTEMPT {
|
||||
CATCH(e, aksl_fread((void *)&objname, 1, namelength, fp));
|
||||
CATCH(e, aksl_fread((void *)&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
|
||||
@@ -313,10 +379,23 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_Pr
|
||||
map,
|
||||
ptrstring,
|
||||
SDL_GetPointerProperty(registry, objname, NULL));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
};
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game load versioncmp.
|
||||
* @param versiontype Description of the version being compared.
|
||||
* @param newversion Version read from the save data.
|
||||
* @param curversion Version supported by the running library.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_VALUE When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion)
|
||||
{
|
||||
semver_t current_version = {};
|
||||
|
||||
19
src/heap.c
19
src/heap.c
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file heap.c
|
||||
* @brief Implements the heap subsystem.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <akerror.h>
|
||||
|
||||
@@ -20,6 +25,10 @@ akerr_ErrorContext *akgl_heap_init()
|
||||
PREPARE_ERROR(errctx);
|
||||
int i = 0;
|
||||
akerr_name_for_status(AKGL_ERR_SDL, "SDL Error");
|
||||
akerr_name_for_status(AKGL_ERR_REGISTRY, "Registry Error");
|
||||
akerr_name_for_status(AKGL_ERR_HEAP, "Heap Error");
|
||||
akerr_name_for_status(AKGL_ERR_BEHAVIOR, "Behavior Error");
|
||||
akerr_name_for_status(AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt");
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) {
|
||||
memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite));
|
||||
@@ -55,7 +64,7 @@ akerr_ErrorContext *akgl_heap_next_actor(akgl_Actor **dest)
|
||||
*dest = &HEAP_ACTOR[i];
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused actor on the heap");
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused actor on the heap");
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_heap_next_sprite(akgl_Sprite **dest)
|
||||
@@ -68,7 +77,7 @@ akerr_ErrorContext *akgl_heap_next_sprite(akgl_Sprite **dest)
|
||||
*dest = &HEAP_SPRITE[i];
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused sprite on the heap");
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused sprite on the heap");
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest)
|
||||
@@ -81,7 +90,7 @@ akerr_ErrorContext *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest)
|
||||
*dest = &HEAP_SPRITESHEET[i];
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused spritesheet on the heap");
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused spritesheet on the heap");
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_heap_next_character(akgl_Character **dest)
|
||||
@@ -94,7 +103,7 @@ akerr_ErrorContext *akgl_heap_next_character(akgl_Character **dest)
|
||||
*dest = &HEAP_CHARACTER[i];
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused character on the heap");
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused character on the heap");
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_heap_next_string(akgl_String **dest)
|
||||
@@ -108,7 +117,7 @@ akerr_ErrorContext *akgl_heap_next_string(akgl_String **dest)
|
||||
HEAP_STRING[i].refcount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, AKERR_HEAP, "Unable to find unused string on the heap");
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused string on the heap");
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_heap_release_actor(akgl_Actor *ptr)
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file json_helpers.c
|
||||
* @brief Implements the json helpers subsystem.
|
||||
*/
|
||||
|
||||
#include <jansson.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file physics.c
|
||||
* @brief Implements the physics subsystem.
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <akstdlib.h>
|
||||
#include <akgl/physics.h>
|
||||
@@ -123,13 +128,18 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
|
||||
.flags = 0,
|
||||
.layerid = 0
|
||||
};
|
||||
SDL_Time curtime = SDL_GetTicksNS();
|
||||
float32_t dt = (float32_t)(curtime - self->gravity_time) / (float32_t)AKGL_TIME_ONESEC_NS;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file registry.c
|
||||
* @brief Implements the registry subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include <jansson.h>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file renderer.c
|
||||
* @brief Implements the renderer subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <akgl/renderer.h>
|
||||
|
||||
28
src/sprite.c
28
src/sprite.c
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file sprite.c
|
||||
* @brief Implements the sprite subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <string.h>
|
||||
@@ -35,6 +40,14 @@ akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FR
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sprite load json spritesheet.
|
||||
* @param json JSON object to parse.
|
||||
* @param sheet Spritesheet used by the sprite.
|
||||
* @param relative_path Base directory for relative asset references.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -58,7 +71,7 @@ static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_
|
||||
if ( *sheet == NULL ) {
|
||||
CATCH(errctx, akgl_heap_next_spritesheet(sheet));
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_width", &ss_frame_width));
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_width));
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_height));
|
||||
CATCH(errctx,
|
||||
akgl_spritesheet_initialize(
|
||||
(akgl_SpriteSheet *)*sheet,
|
||||
@@ -85,12 +98,20 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
||||
akgl_Sprite *obj = NULL;
|
||||
akgl_SpriteSheet *sheet = NULL;
|
||||
akgl_String *spritename = NULL;
|
||||
//string *tmpstr = NULL;
|
||||
akgl_String *filename_copy = NULL;
|
||||
int i = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_sprite(&obj));
|
||||
CATCH(errctx, akgl_heap_next_string(&filename_copy));
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
strlen(filename) >= AKGL_MAX_STRING_LENGTH,
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Sprite filename exceeds temporary string capacity"
|
||||
);
|
||||
SDL_strlcpy(filename_copy->data, filename, AKGL_MAX_STRING_LENGTH);
|
||||
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
||||
CATCH(errctx, akgl_heap_next_string(&spritename));
|
||||
@@ -105,7 +126,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
||||
"Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
|
||||
);
|
||||
|
||||
CATCH(errctx, akgl_sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename)));
|
||||
CATCH(errctx, akgl_sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data)));
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
|
||||
CATCH(errctx,
|
||||
akgl_sprite_initialize(
|
||||
@@ -132,6 +153,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
||||
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);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file staticstring.c
|
||||
* @brief Implements the staticstring subsystem.
|
||||
*/
|
||||
|
||||
#include <akerror.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <errno.h>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file text.c
|
||||
* @brief Implements the text subsystem.
|
||||
*/
|
||||
|
||||
#include <akerror.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file tilemap.c
|
||||
* @brief Implements the tilemap subsystem.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <libgen.h>
|
||||
|
||||
@@ -82,6 +87,14 @@ akerr_ErrorContext *akgl_get_json_properties_integer(json_t *obj, char *key, int
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get json properties number.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, float *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -92,6 +105,14 @@ akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, floa
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get json properties float.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -102,6 +123,14 @@ akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get json properties double.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, double *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -129,7 +158,6 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
|
||||
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "tilewidth", &dest->tilesets[tsidx].tilewidth));
|
||||
|
||||
PASS(e, akgl_get_json_string_value((json_t *)tileset, "name", &tmpstr));
|
||||
PASS(e, akgl_heap_next_string(&tmpstr));
|
||||
PASS(e, akgl_heap_next_string(&tmppath));
|
||||
ATTEMPT {
|
||||
strncpy((char *)&dest->tilesets[tsidx].name,
|
||||
@@ -233,6 +261,15 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tilemap load layer object actor.
|
||||
* @param curobj Tilemap object being populated.
|
||||
* @param layerdatavalue JSON object describing the layer entry.
|
||||
* @param layerid Zero-based tilemap layer index.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -354,6 +391,16 @@ akerr_ErrorContext *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *roo
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tilemap load layer image.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param layerid Zero-based tilemap layer index.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -440,6 +487,13 @@ akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, a
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tilemap load physics.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
22
src/util.c
22
src/util.c
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file util.c
|
||||
* @brief Implements the util subsystem.
|
||||
*/
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
@@ -15,6 +20,15 @@
|
||||
|
||||
#include <akstdlib.h>
|
||||
|
||||
/**
|
||||
* @brief Path relative root.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param path Path to resolve or transform.
|
||||
* @param dst Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String *dst)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
@@ -80,6 +94,14 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Path relative from.
|
||||
* @param path Path to resolve or transform.
|
||||
* @param from Base path from which the result is made relative.
|
||||
* @param dst Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String **dst)
|
||||
{
|
||||
akgl_String *dirnamestr;
|
||||
|
||||
609
tests/actor.c
609
tests/actor.c
@@ -2,6 +2,7 @@
|
||||
handle_unhandled_error(errctx);
|
||||
|
||||
#include <akerror.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
#define UNHANDLED_ERROR_EXIT 0
|
||||
#define UNHANDLED_ERROR_SET 1
|
||||
@@ -12,8 +13,13 @@
|
||||
#include <akgl/iterator.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/heap.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
int UNHANDLED_ERROR_BEHAVIOR;
|
||||
akerr_ErrorContext *unhandled_error_context;
|
||||
|
||||
@@ -42,7 +48,7 @@ akerr_ErrorContext *akgl_actor_update_noop(akgl_Actor *obj)
|
||||
|
||||
// Currently the renderer assumes there is a global variable named `renderer`
|
||||
int akgl_actor_rendered;
|
||||
akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj, SDL_Renderer *r)
|
||||
akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_actor_rendered = 1;
|
||||
@@ -129,13 +135,13 @@ akerr_ErrorContext *test_registry_actor_iterator_updaterender(void)
|
||||
FAIL_ZERO_BREAK(
|
||||
unhandled_error_context,
|
||||
akgl_actor_updated,
|
||||
AKERR_BEHAVIOR,
|
||||
AKGL_ERR_BEHAVIOR,
|
||||
"actor->updatefunc not called by the iterator"
|
||||
);
|
||||
FAIL_ZERO_BREAK(
|
||||
unhandled_error_context,
|
||||
akgl_actor_rendered,
|
||||
AKERR_BEHAVIOR,
|
||||
AKGL_ERR_BEHAVIOR,
|
||||
"actor->renderfunc not called by the iterator"
|
||||
);
|
||||
} CLEANUP {
|
||||
@@ -235,7 +241,7 @@ akerr_ErrorContext *test_actor_manage_children(void)
|
||||
CATCH(errctx, parent->addchild(parent, child));
|
||||
} CLEANUP {
|
||||
if ( errctx == NULL ) {
|
||||
FAIL(errctx, AKERR_BEHAVIOR, "addchild does not throw AKERR_RELATIONSHIP when child already has a parent");
|
||||
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_RELATIONSHIP when child already has a parent");
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_RELATIONSHIP) {
|
||||
@@ -248,7 +254,7 @@ akerr_ErrorContext *test_actor_manage_children(void)
|
||||
CATCH(errctx, parent->addchild(parent, child));
|
||||
} CLEANUP {
|
||||
if ( errctx == NULL ) {
|
||||
FAIL(errctx, AKERR_BEHAVIOR, "addchild does not throw AKERR_OUTOFBOUNDS when all children already set");
|
||||
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_OUTOFBOUNDS when all children already set");
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_OUTOFBOUNDS) {
|
||||
@@ -307,12 +313,595 @@ _test_actor_addchild_heaprelease_cleanup:
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build an actor bound to a character, without touching the renderer.
|
||||
*
|
||||
* The sprite and spritesheet are taken straight off the heap and populated by
|
||||
* hand, because akgl_sprite_load_json() would need a live renderer to build a
|
||||
* texture and none of the logic under test reads one.
|
||||
*/
|
||||
static akerr_ErrorContext *make_bound_actor(
|
||||
akgl_Actor **actor,
|
||||
akgl_Character **basechar,
|
||||
akgl_Sprite **sprite,
|
||||
char *name,
|
||||
int state)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_SpriteSheet *sheet = NULL;
|
||||
char spritename[AKGL_SPRITE_MAX_NAME_LENGTH];
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_heap_next_character(basechar));
|
||||
CATCH(e, akgl_character_initialize(*basechar, name));
|
||||
|
||||
CATCH(e, akgl_heap_next_spritesheet(&sheet));
|
||||
sheet->refcount += 1;
|
||||
|
||||
snprintf((char *)&spritename, AKGL_SPRITE_MAX_NAME_LENGTH, "%s_sprite", name);
|
||||
CATCH(e, akgl_heap_next_sprite(sprite));
|
||||
CATCH(e, akgl_sprite_initialize(*sprite, (char *)&spritename, sheet));
|
||||
(*sprite)->width = 32;
|
||||
(*sprite)->height = 32;
|
||||
(*sprite)->frames = 4;
|
||||
(*sprite)->speed = 100;
|
||||
CATCH(e, akgl_character_sprite_add(*basechar, *sprite, state));
|
||||
|
||||
CATCH(e, akgl_heap_next_actor(actor));
|
||||
CATCH(e, akgl_actor_initialize(*actor, name));
|
||||
(*actor)->basechar = *basechar;
|
||||
(*actor)->state = state;
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_actor_control_handlers_on(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Actor actor;
|
||||
akgl_Character basechar;
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&event, 0x00, sizeof(SDL_Event));
|
||||
memset(&basechar, 0x00, sizeof(akgl_Character));
|
||||
basechar.ax = 7.0f;
|
||||
basechar.ay = 11.0f;
|
||||
|
||||
// Left: clears every facing and movement bit, then sets its own pair and
|
||||
// takes acceleration from the base character with the sign reversed.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.basechar = &basechar;
|
||||
actor.state = (AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_ALIVE);
|
||||
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_on(&actor, &event), "left on");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"left on did not set MOVING_LEFT (state %d)", actor.state);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
|
||||
"left on did not set FACE_LEFT (state %d)", actor.state);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
|
||||
"left on left MOVING_RIGHT set (state %d)", actor.state);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
|
||||
"left on left FACE_RIGHT set (state %d)", actor.state);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_ALIVE),
|
||||
"left on cleared an unrelated state bit (state %d)", actor.state);
|
||||
TEST_ASSERT_FEQ(e, actor.ax, -7.0f, "left on set ax to %f, expected -7", actor.ax);
|
||||
|
||||
// Right: same, with the sign preserved.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.basechar = &basechar;
|
||||
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_on(&actor, &event), "right on");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
|
||||
"right on did not set MOVING_RIGHT (state %d)", actor.state);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
|
||||
"right on did not set FACE_RIGHT (state %d)", actor.state);
|
||||
TEST_ASSERT_FEQ(e, actor.ax, 7.0f, "right on set ax to %f, expected 7", actor.ax);
|
||||
|
||||
// Up reverses the Y acceleration, because Y grows downward.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.basechar = &basechar;
|
||||
TEST_EXPECT_OK(e, akgl_Actor_cmhf_up_on(&actor, &event), "up on");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
|
||||
"up on did not set MOVING_UP (state %d)", actor.state);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_UP),
|
||||
"up on did not set FACE_UP (state %d)", actor.state);
|
||||
TEST_ASSERT_FEQ(e, actor.ay, -11.0f, "up on set ay to %f, expected -11", actor.ay);
|
||||
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.basechar = &basechar;
|
||||
TEST_EXPECT_OK(e, akgl_Actor_cmhf_down_on(&actor, &event), "down on");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
|
||||
"down on did not set MOVING_DOWN (state %d)", actor.state);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_DOWN),
|
||||
"down on did not set FACE_DOWN (state %d)", actor.state);
|
||||
TEST_ASSERT_FEQ(e, actor.ay, 11.0f, "down on set ay to %f, expected 11", actor.ay);
|
||||
|
||||
// Each direction is exclusive: turning right after left leaves only right.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.basechar = &basechar;
|
||||
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_on(&actor, &event), "left on before turning");
|
||||
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_on(&actor, &event), "right on after left");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"turning right left MOVING_LEFT set (state %d)", actor.state);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_actor_control_handlers_off(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Actor actor;
|
||||
akgl_Character basechar;
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&event, 0x00, sizeof(SDL_Event));
|
||||
memset(&basechar, 0x00, sizeof(akgl_Character));
|
||||
basechar.ax = 7.0f;
|
||||
basechar.ay = 11.0f;
|
||||
|
||||
// Releasing a direction zeroes that axis outright: acceleration, thrust,
|
||||
// environmental force, and velocity all go to zero together.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.basechar = &basechar;
|
||||
actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
|
||||
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
|
||||
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_off(&actor, &event), "left off");
|
||||
TEST_ASSERT_FEQ(e, actor.ax, 0.0f, "left off left ax at %f", actor.ax);
|
||||
TEST_ASSERT_FEQ(e, actor.ex, 0.0f, "left off left ex at %f", actor.ex);
|
||||
TEST_ASSERT_FEQ(e, actor.tx, 0.0f, "left off left tx at %f", actor.tx);
|
||||
TEST_ASSERT_FEQ(e, actor.vx, 0.0f, "left off left vx at %f", actor.vx);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"left off did not clear MOVING_LEFT (state %d)", actor.state);
|
||||
// Facing is deliberately sticky, so an idle actor keeps looking where it was.
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
|
||||
"left off cleared FACE_LEFT, which should persist (state %d)", actor.state);
|
||||
// The Y axis is untouched by a horizontal release.
|
||||
TEST_ASSERT_FEQ(e, actor.vy, 5.0f, "left off disturbed vy (%f)", actor.vy);
|
||||
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
|
||||
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
|
||||
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_off(&actor, &event), "right off");
|
||||
TEST_ASSERT_FEQ(e, actor.vx, 0.0f, "right off left vx at %f", actor.vx);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
|
||||
"right off did not clear MOVING_RIGHT (state %d)", actor.state);
|
||||
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
|
||||
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
|
||||
TEST_EXPECT_OK(e, akgl_Actor_cmhf_up_off(&actor, &event), "up off");
|
||||
TEST_ASSERT_FEQ(e, actor.vy, 0.0f, "up off left vy at %f", actor.vy);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
|
||||
"up off did not clear MOVING_UP (state %d)", actor.state);
|
||||
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
|
||||
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
|
||||
TEST_EXPECT_OK(e, akgl_Actor_cmhf_down_off(&actor, &event), "down off");
|
||||
TEST_ASSERT_FEQ(e, actor.vy, 0.0f, "down off left vy at %f", actor.vy);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
|
||||
"down off did not clear MOVING_DOWN (state %d)", actor.state);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_actor_control_handlers_nullpointers(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Actor actor;
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&event, 0x00, sizeof(SDL_Event));
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(NULL, &event), "left on, NULL actor");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_off(NULL, &event), "left off, NULL actor");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(NULL, &event), "right on, NULL actor");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_off(NULL, &event), "right off, NULL actor");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(NULL, &event), "up on, NULL actor");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_off(NULL, &event), "up off, NULL actor");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(NULL, &event), "down on, NULL actor");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_off(NULL, &event), "down off, NULL actor");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(&actor, NULL), "left on, NULL event");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_off(&actor, NULL), "left off, NULL event");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(&actor, NULL), "right on, NULL event");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_off(&actor, NULL), "right off, NULL event");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(&actor, NULL), "up on, NULL event");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_off(&actor, NULL), "up off, NULL event");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(&actor, NULL), "down on, NULL event");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_off(&actor, NULL), "down off, NULL event");
|
||||
|
||||
// A movement handler reads acceleration off the base character, so an
|
||||
// actor without one has to be reported rather than dereferenced.
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(&actor, &event),
|
||||
"left on, actor with no base character");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(&actor, &event),
|
||||
"right on, actor with no base character");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(&actor, &event),
|
||||
"up on, actor with no base character");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(&actor, &event),
|
||||
"down on, actor with no base character");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_actor_automatic_face(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Actor actor;
|
||||
|
||||
ATTEMPT {
|
||||
// With the flag off, facing is left entirely to the caller.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.movement_controls_face = false;
|
||||
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT);
|
||||
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face with the flag off");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
|
||||
"automatic face changed facing while disabled (state %d)", actor.state);
|
||||
|
||||
// With the flag on, facing follows movement.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.movement_controls_face = true;
|
||||
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT);
|
||||
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving left");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
|
||||
"automatic face did not turn the actor left (state %d)", actor.state);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
|
||||
"automatic face left the stale facing set (state %d)", actor.state);
|
||||
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.movement_controls_face = true;
|
||||
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
|
||||
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving right");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
|
||||
"automatic face did not turn the actor right (state %d)", actor.state);
|
||||
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.movement_controls_face = true;
|
||||
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
|
||||
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving up");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_UP),
|
||||
"automatic face did not turn the actor up (state %d)", actor.state);
|
||||
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.movement_controls_face = true;
|
||||
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
|
||||
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving down");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_DOWN),
|
||||
"automatic face did not turn the actor down (state %d)", actor.state);
|
||||
|
||||
// Left wins over up when both are set, per the order of the checks.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.movement_controls_face = true;
|
||||
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_UP);
|
||||
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving diagonally");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
|
||||
"diagonal movement did not resolve to the left facing (state %d)", actor.state);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_UP),
|
||||
"diagonal movement set two facings at once (state %d)", actor.state);
|
||||
|
||||
// A stationary actor ends up facing nowhere.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.movement_controls_face = true;
|
||||
actor.state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP);
|
||||
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while stationary");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_ALL),
|
||||
"a stationary actor kept a facing bit (state %d)", actor.state);
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_ALIVE),
|
||||
"automatic face cleared an unrelated state bit (state %d)", actor.state);
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_automatic_face(NULL),
|
||||
"automatic face with a NULL actor");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_actor_logic_movement(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Actor actor;
|
||||
akgl_Character basechar;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&basechar, 0x00, sizeof(akgl_Character));
|
||||
basechar.ax = 3.0f; basechar.ay = 4.0f;
|
||||
basechar.sx = 30.0f; basechar.sy = 40.0f; basechar.sz = 50.0f;
|
||||
|
||||
// Max speed is always refreshed from the base character.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.basechar = &basechar;
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while idle");
|
||||
TEST_ASSERT_FEQ(e, actor.sx, 30.0f, "sx copied as %f, expected 30", actor.sx);
|
||||
TEST_ASSERT_FEQ(e, actor.sy, 40.0f, "sy copied as %f, expected 40", actor.sy);
|
||||
TEST_ASSERT_FEQ(e, actor.sz, 50.0f, "sz copied as %f, expected 50", actor.sz);
|
||||
|
||||
actor.state = AKGL_ACTOR_STATE_MOVING_LEFT;
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving left");
|
||||
TEST_ASSERT_FEQ(e, actor.ax, -3.0f, "moving left set ax to %f, expected -3", actor.ax);
|
||||
|
||||
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving right");
|
||||
TEST_ASSERT_FEQ(e, actor.ax, 3.0f, "moving right set ax to %f, expected 3", actor.ax);
|
||||
|
||||
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving up");
|
||||
TEST_ASSERT_FEQ(e, actor.ay, -4.0f, "moving up set ay to %f, expected -4", actor.ay);
|
||||
|
||||
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving down");
|
||||
TEST_ASSERT_FEQ(e, actor.ay, 4.0f, "moving down set ay to %f, expected 4", actor.ay);
|
||||
|
||||
// Both axes at once.
|
||||
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_DOWN);
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving diagonally");
|
||||
TEST_ASSERT_FEQ(e, actor.ax, -3.0f, "diagonal movement set ax to %f, expected -3", actor.ax);
|
||||
TEST_ASSERT_FEQ(e, actor.ay, 4.0f, "diagonal movement set ay to %f, expected 4", actor.ay);
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_movement(NULL, 1.0f),
|
||||
"movement logic with a NULL actor");
|
||||
|
||||
// Every value the movement logic writes is read off the base character,
|
||||
// so an actor without one has to be reported rather than dereferenced.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_movement(&actor, 1.0f),
|
||||
"movement logic with no base character");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_actor_logic_changeframe(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Actor actor;
|
||||
akgl_Sprite sprite;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&sprite, 0x00, sizeof(akgl_Sprite));
|
||||
sprite.frames = 4;
|
||||
|
||||
// Mid-animation, the frame simply advances.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.curSpriteFrameId = 1;
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "advancing mid-animation");
|
||||
TEST_ASSERT(e, actor.curSpriteFrameId == 2,
|
||||
"frame advanced to %d, expected 2", actor.curSpriteFrameId);
|
||||
|
||||
// At the last frame without looping, it wraps to the start.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.curSpriteFrameId = 3;
|
||||
sprite.loop = false;
|
||||
sprite.loopReverse = false;
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "wrapping a non-looping sprite");
|
||||
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
|
||||
"the last frame wrapped to %d, expected 0", actor.curSpriteFrameId);
|
||||
|
||||
// A forward-looping sprite behaves the same way at the end.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.curSpriteFrameId = 3;
|
||||
sprite.loop = true;
|
||||
sprite.loopReverse = false;
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "wrapping a forward-looping sprite");
|
||||
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
|
||||
"the forward loop wrapped to %d, expected 0", actor.curSpriteFrameId);
|
||||
|
||||
// A ping-pong sprite turns around at the end instead of wrapping.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
actor.curSpriteFrameId = 3;
|
||||
sprite.loop = true;
|
||||
sprite.loopReverse = true;
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "reversing at the end");
|
||||
TEST_ASSERT(e, actor.curSpriteReversing == true,
|
||||
"the sprite did not enter its reverse phase at the last frame");
|
||||
TEST_ASSERT(e, actor.curSpriteFrameId == 2,
|
||||
"reversing stepped to frame %d, expected 2", actor.curSpriteFrameId);
|
||||
|
||||
// While reversing, the frame counts back down.
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "stepping back while reversing");
|
||||
TEST_ASSERT(e, actor.curSpriteFrameId == 1,
|
||||
"reversing stepped to frame %d, expected 1", actor.curSpriteFrameId);
|
||||
|
||||
// At frame zero it turns around again and resumes going forward.
|
||||
actor.curSpriteFrameId = 0;
|
||||
actor.curSpriteReversing = true;
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "turning around at frame zero");
|
||||
TEST_ASSERT(e, actor.curSpriteReversing == false,
|
||||
"the sprite stayed in its reverse phase at frame zero");
|
||||
TEST_ASSERT(e, actor.curSpriteFrameId == 1,
|
||||
"turning around stepped to frame %d, expected 1", actor.curSpriteFrameId);
|
||||
|
||||
// A single-frame sprite has nowhere to advance to.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
sprite.frames = 1;
|
||||
sprite.loop = false;
|
||||
sprite.loopReverse = false;
|
||||
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "advancing a single-frame sprite");
|
||||
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
|
||||
"a single-frame sprite moved to frame %d, expected 0", actor.curSpriteFrameId);
|
||||
|
||||
sprite.frames = 4;
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_changeframe(NULL, &sprite, 0),
|
||||
"changeframe with a NULL actor");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/** @brief Records calls made to the changeframe stub. */
|
||||
static int changeframe_calls = 0;
|
||||
|
||||
/** @brief Changeframe stub that records its invocation without advancing. */
|
||||
static akerr_ErrorContext *stub_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
changeframe_calls += 1;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_actor_update(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Actor *actor = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_Sprite *sprite = NULL;
|
||||
SDL_Time now = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_registry_init_actor());
|
||||
CATCH(e, akgl_registry_init_sprite());
|
||||
CATCH(e, akgl_registry_init_spritesheet());
|
||||
CATCH(e, akgl_registry_init_character());
|
||||
CATCH(e, akgl_heap_init());
|
||||
|
||||
CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "updatable", AKGL_ACTOR_STATE_ALIVE));
|
||||
actor->changeframefunc = &stub_changeframe;
|
||||
|
||||
// Long enough since the last frame change, so the sprite advances.
|
||||
SDL_GetCurrentTime(&now);
|
||||
sprite->speed = 1;
|
||||
actor->curSpriteFrameTimer = 0;
|
||||
changeframe_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_actor_update(actor), "updating an actor whose frame is due");
|
||||
TEST_ASSERT(e, changeframe_calls == 1,
|
||||
"a due frame change fired %d times, expected 1", changeframe_calls);
|
||||
TEST_ASSERT(e, actor->curSpriteFrameTimer != 0,
|
||||
"the frame timer was not restamped after a frame change");
|
||||
|
||||
// Too soon since the last change, so nothing happens.
|
||||
SDL_GetCurrentTime(&now);
|
||||
sprite->speed = 1000000000;
|
||||
actor->curSpriteFrameTimer = now;
|
||||
changeframe_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_actor_update(actor), "updating an actor whose frame is not due");
|
||||
TEST_ASSERT(e, changeframe_calls == 0,
|
||||
"an early frame change fired %d times, expected 0", changeframe_calls);
|
||||
|
||||
// An actor in a state with no sprite bound is skipped, not failed: the
|
||||
// missing binding is reported as AKERR_KEY and swallowed by update.
|
||||
actor->state = AKGL_ACTOR_STATE_DEAD;
|
||||
changeframe_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_actor_update(actor),
|
||||
"updating an actor whose state has no sprite");
|
||||
TEST_ASSERT(e, changeframe_calls == 0,
|
||||
"an actor with no sprite for its state still changed frames");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_update(NULL),
|
||||
"updating a NULL actor");
|
||||
|
||||
// An actor with no base character cannot resolve a sprite at all.
|
||||
actor->basechar = NULL;
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_update(actor),
|
||||
"updating an actor with no base character");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_actor_character_sprite_binding(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Actor *actor = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_Sprite *sprite = NULL;
|
||||
akgl_Sprite *found = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_registry_init_actor());
|
||||
CATCH(e, akgl_registry_init_sprite());
|
||||
CATCH(e, akgl_registry_init_spritesheet());
|
||||
CATCH(e, akgl_registry_init_character());
|
||||
CATCH(e, akgl_heap_init());
|
||||
|
||||
CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "bound", AKGL_ACTOR_STATE_ALIVE));
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_character_sprite_get(basechar, AKGL_ACTOR_STATE_ALIVE, &found),
|
||||
"reading back a bound sprite");
|
||||
TEST_ASSERT(e, found == sprite, "the bound sprite came back as a different object");
|
||||
|
||||
// A composite state is a distinct key from either of its components.
|
||||
TEST_EXPECT_STATUS(e, AKERR_KEY,
|
||||
akgl_character_sprite_get(basechar,
|
||||
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP),
|
||||
&found),
|
||||
"reading a composite state that was never bound");
|
||||
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_character_sprite_get(basechar, 0, &found),
|
||||
"reading state zero, which was never bound");
|
||||
|
||||
// Binding a composite state makes it resolvable.
|
||||
TEST_EXPECT_OK(e, akgl_character_sprite_add(basechar, sprite,
|
||||
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP)),
|
||||
"binding a sprite to a composite state");
|
||||
TEST_EXPECT_OK(e, akgl_character_sprite_get(basechar,
|
||||
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP),
|
||||
&found),
|
||||
"reading back the composite binding");
|
||||
TEST_ASSERT(e, found == sprite, "the composite binding resolved to a different sprite");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_get(NULL, 0, &found),
|
||||
"sprite_get with a NULL character");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
akgl_character_sprite_get(basechar, AKGL_ACTOR_STATE_ALIVE, NULL),
|
||||
"sprite_get with a NULL destination");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_add(NULL, sprite, 0),
|
||||
"sprite_add with a NULL character");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_add(basechar, NULL, 0),
|
||||
"sprite_add with a NULL sprite");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_actor_sprite_sheet_coords(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Sprite sprite;
|
||||
SDL_FRect coords;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&sprite, 0x00, sizeof(akgl_Sprite));
|
||||
sprite.width = 32;
|
||||
sprite.height = 32;
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(NULL, &coords, 0),
|
||||
"sheet coords with a NULL sprite");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(&sprite, NULL, 0),
|
||||
"sheet coords with a NULL rectangle");
|
||||
// The sprite has no sheet attached, so there is no texture to measure against.
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(&sprite, &coords, 0),
|
||||
"sheet coords with a NULL spritesheet");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akgl_actor_updated = 0;
|
||||
akgl_actor_rendered = 0;
|
||||
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_registry_init_actor());
|
||||
CATCH(errctx, akgl_registry_init_sprite());
|
||||
@@ -324,6 +913,16 @@ int main(void)
|
||||
CATCH(errctx, test_registry_actor_iterator_updaterender());
|
||||
CATCH(errctx, test_akgl_actor_set_character());
|
||||
CATCH(errctx, test_actor_manage_children());
|
||||
|
||||
CATCH(errctx, test_actor_control_handlers_on());
|
||||
CATCH(errctx, test_actor_control_handlers_off());
|
||||
CATCH(errctx, test_actor_control_handlers_nullpointers());
|
||||
CATCH(errctx, test_actor_automatic_face());
|
||||
CATCH(errctx, test_actor_logic_movement());
|
||||
CATCH(errctx, test_actor_logic_changeframe());
|
||||
CATCH(errctx, test_actor_update());
|
||||
CATCH(errctx, test_actor_character_sprite_binding());
|
||||
CATCH(errctx, test_actor_sprite_sheet_coords());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
19
tests/assets/snippets/test_json_helpers.json
Normal file
19
tests/assets/snippets/test_json_helpers.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "json helper fixture",
|
||||
"count": 42,
|
||||
"ratio": 2.5,
|
||||
"negative": -17,
|
||||
"enabled": true,
|
||||
"disabled": false,
|
||||
"nested": {
|
||||
"inner": "value",
|
||||
"innercount": 7
|
||||
},
|
||||
"integers": [10, 20, 30],
|
||||
"strings": ["alpha", "beta"],
|
||||
"objects": [
|
||||
{ "id": 1 },
|
||||
{ "id": 2 }
|
||||
],
|
||||
"mixed": [1, "two", { "three": 3 }]
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"name": "testcharacter",
|
||||
"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": [
|
||||
@@ -12,7 +16,7 @@
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
|
||||
@@ -50,6 +50,7 @@ akerr_ErrorContext *test_character_sprite_mgmt()
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_character(&testchar));
|
||||
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
||||
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
@@ -89,6 +90,7 @@ akerr_ErrorContext *test_character_iterate_state_sprites()
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_character(&testchar));
|
||||
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
||||
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
@@ -169,8 +171,8 @@ akerr_ErrorContext *test_akgl_character_load_json()
|
||||
CATCH(errctx, testcharacter->sprite_get(testcharacter, AKGL_ACTOR_STATE_DEAD, &comparesprite));
|
||||
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_DEAD");
|
||||
|
||||
FAIL_ZERO_BREAK(errctx, (testcharacter->vx != 0.200000003), AKERR_VALUE, "Wrong X velocity for test character");
|
||||
FAIL_ZERO_BREAK(errctx, (testcharacter->vy != 0.200000003), AKERR_VALUE, "Wrong Y velocity for test character");
|
||||
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, akgl_heap_release_sprite(testsprite));
|
||||
|
||||
@@ -68,7 +68,7 @@ int main(void)
|
||||
AKGL_REGISTRY_CHARACTER,
|
||||
"little guy",
|
||||
NULL);
|
||||
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKERR_REGISTRY, "Can't load character 'little guy' from the registry");
|
||||
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKGL_ERR_REGISTRY, "Can't load character 'little guy' from the registry");
|
||||
actorptr->movement_controls_face = false;
|
||||
actorptr->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
actorptr->x = 320;
|
||||
|
||||
557
tests/controller.c
Normal file
557
tests/controller.c
Normal file
@@ -0,0 +1,557 @@
|
||||
/**
|
||||
* @file controller.c
|
||||
* @brief Unit tests for control maps and SDL input dispatch.
|
||||
*
|
||||
* SDL events are synthesized directly rather than pumped through the event
|
||||
* queue, so none of this needs a physical keyboard or gamepad. The device
|
||||
* enumeration helpers are exercised against the dummy drivers, where the
|
||||
* expected outcome is "no devices, no crash".
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
// akgl/controller.h uses akgl_Actor without declaring it, so actor.h has to
|
||||
// come first, the same way src/controller.c reaches it through akgl/game.h.
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/*
|
||||
* akgl/controller.h declares these as akgl_controller_handle_*, but src has
|
||||
* always defined them as gamepad_handle_*. Declared here under the names that
|
||||
* actually link, so the tests can reach them without renaming shipped symbols.
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event);
|
||||
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event);
|
||||
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event);
|
||||
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event);
|
||||
|
||||
/** @brief Keyboard id the tests bind their control maps to. */
|
||||
#define TEST_KBID 11
|
||||
/** @brief Gamepad id the tests bind their control maps to. */
|
||||
#define TEST_JSID 22
|
||||
|
||||
/** @brief Stand-in for the application state pointer SDL hands to callbacks. */
|
||||
static int appstate_placeholder = 0;
|
||||
|
||||
/** @brief The actor every control map in this file targets. */
|
||||
static akgl_Actor *player = NULL;
|
||||
/** @brief Base character supplying the player's acceleration constants. */
|
||||
static akgl_Character *playerchar = NULL;
|
||||
|
||||
/** @brief Clear every control map so each test starts from an empty binding set. */
|
||||
static void reset_control_maps(void)
|
||||
{
|
||||
int i = 0;
|
||||
for ( i = 0; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
||||
memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap));
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Register the "player" actor the gamepad handlers look up by name. */
|
||||
static akerr_ErrorContext *make_player(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_registry_init_actor());
|
||||
CATCH(e, akgl_registry_init_character());
|
||||
CATCH(e, akgl_heap_init());
|
||||
|
||||
CATCH(e, akgl_heap_next_character(&playerchar));
|
||||
CATCH(e, akgl_character_initialize(playerchar, "playerchar"));
|
||||
playerchar->ax = 6.0f;
|
||||
playerchar->ay = 8.0f;
|
||||
|
||||
CATCH(e, akgl_heap_next_actor(&player));
|
||||
CATCH(e, akgl_actor_initialize(player, "player"));
|
||||
player->basechar = playerchar;
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/** @brief Build a synthetic keyboard event. */
|
||||
static void make_key_event(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key)
|
||||
{
|
||||
memset(event, 0x00, sizeof(SDL_Event));
|
||||
event->type = type;
|
||||
event->key.which = which;
|
||||
event->key.key = key;
|
||||
}
|
||||
|
||||
/** @brief Build a synthetic gamepad button event. */
|
||||
static void make_button_event(SDL_Event *event, uint32_t type, SDL_JoystickID which, uint8_t button)
|
||||
{
|
||||
memset(event, 0x00, sizeof(SDL_Event));
|
||||
event->type = type;
|
||||
event->gbutton.which = which;
|
||||
event->gbutton.button = button;
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_pushmap(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Control control;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
memset(&control, 0x00, sizeof(akgl_Control));
|
||||
control.key = SDLK_SPACE;
|
||||
control.event_on = SDL_EVENT_KEY_DOWN;
|
||||
control.event_off = SDL_EVENT_KEY_UP;
|
||||
control.handler_on = &akgl_Actor_cmhf_left_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_left_off;
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing one control");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 1,
|
||||
"pushing one control left nextMap at %d, expected 1",
|
||||
GAME_ControlMaps[0].nextMap);
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].key == SDLK_SPACE,
|
||||
"the pushed control did not land in slot 0");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].handler_on == &akgl_Actor_cmhf_left_on,
|
||||
"the pushed control lost its press handler");
|
||||
|
||||
// Pushes accumulate rather than overwrite.
|
||||
TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing a second control");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 2,
|
||||
"pushing a second control left nextMap at %d, expected 2",
|
||||
GAME_ControlMaps[0].nextMap);
|
||||
|
||||
// Maps are independent of each other.
|
||||
TEST_EXPECT_OK(e, akgl_controller_pushmap(3, &control), "pushing into a different map");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[3].nextMap == 1,
|
||||
"map 3 nextMap is %d, expected 1", GAME_ControlMaps[3].nextMap);
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 2,
|
||||
"pushing into map 3 disturbed map 0 (nextMap %d)", GAME_ControlMaps[0].nextMap);
|
||||
|
||||
// Filling a map exactly to capacity still succeeds.
|
||||
reset_control_maps();
|
||||
for ( i = 0; i < AKGL_MAX_CONTROLS; i++ ) {
|
||||
akerr_ErrorContext *pushresult = akgl_controller_pushmap(0, &control);
|
||||
if ( pushresult != NULL ) {
|
||||
pushresult->handled = true;
|
||||
pushresult = akerr_release_error(pushresult);
|
||||
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, "pushing control %d of %d failed", i, AKGL_MAX_CONTROLS);
|
||||
}
|
||||
}
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == AKGL_MAX_CONTROLS,
|
||||
"a full map reports nextMap %d, expected %d",
|
||||
GAME_ControlMaps[0].nextMap, AKGL_MAX_CONTROLS);
|
||||
|
||||
// One past capacity is refused.
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_controller_pushmap(0, &control),
|
||||
"pushing into a full control map");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_pushmap(0, NULL),
|
||||
"pushing a NULL control");
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
|
||||
akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS, &control),
|
||||
"pushing into a control map id at the limit");
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
|
||||
akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS + 5, &control),
|
||||
"pushing into a control map id past the limit");
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_default_bindings(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, make_player());
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID),
|
||||
"installing the default control map");
|
||||
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].target == player,
|
||||
"the default map did not target the player actor");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].kbid == TEST_KBID,
|
||||
"the default map recorded keyboard %d, expected %d",
|
||||
GAME_ControlMaps[0].kbid, TEST_KBID);
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].jsid == TEST_JSID,
|
||||
"the default map recorded gamepad %d, expected %d",
|
||||
GAME_ControlMaps[0].jsid, TEST_JSID);
|
||||
|
||||
// Four keyboard bindings then four gamepad bindings.
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 8,
|
||||
"the default map installed %d controls, expected 8",
|
||||
GAME_ControlMaps[0].nextMap);
|
||||
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].key == SDLK_DOWN,
|
||||
"the first default binding is not the down arrow");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].handler_on == &akgl_Actor_cmhf_down_on,
|
||||
"the down arrow is not bound to the down handler");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[1].key == SDLK_UP,
|
||||
"the second default binding is not the up arrow");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[2].key == SDLK_LEFT,
|
||||
"the third default binding is not the left arrow");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[3].key == SDLK_RIGHT,
|
||||
"the fourth default binding is not the right arrow");
|
||||
|
||||
// The gamepad half binds buttons and leaves the keycode clear, so a
|
||||
// keyboard event cannot accidentally match a dpad binding.
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[4].button == SDL_GAMEPAD_BUTTON_DPAD_DOWN,
|
||||
"the fifth default binding is not the dpad down button");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[4].key == 0,
|
||||
"a gamepad binding also carries a keycode");
|
||||
TEST_ASSERT(e, GAME_ControlMaps[0].controls[7].button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT,
|
||||
"the eighth default binding is not the dpad right button");
|
||||
|
||||
// An unknown actor name is a registry error, not a crash.
|
||||
reset_control_maps();
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_REGISTRY,
|
||||
akgl_controller_default(1, "no_such_actor", TEST_KBID, TEST_JSID),
|
||||
"installing a default map for an unregistered actor");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
|
||||
akgl_controller_default(AKGL_MAX_CONTROL_MAPS, "player", TEST_KBID, TEST_JSID),
|
||||
"installing a default map at a control map id past the limit");
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_handle_keyboard_events(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, make_player());
|
||||
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
|
||||
|
||||
// Pressing left routes to the left press handler.
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a left key press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"a left key press did not start the actor moving left (state %d)", player->state);
|
||||
TEST_ASSERT_FEQ(e, player->ax, -6.0f,
|
||||
"a left key press set ax to %f, expected -6", player->ax);
|
||||
|
||||
// Releasing it routes to the release handler.
|
||||
make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a left key release");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"a left key release did not stop the actor (state %d)", player->state);
|
||||
|
||||
// The other three directions route the same way.
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_RIGHT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a right key press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
|
||||
"a right key press did not start the actor moving right (state %d)", player->state);
|
||||
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_UP);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching an up key press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
|
||||
"an up key press did not start the actor moving up (state %d)", player->state);
|
||||
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_DOWN);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a down key press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
|
||||
"a down key press did not start the actor moving down (state %d)", player->state);
|
||||
|
||||
// An unbound key is ignored.
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_F12);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching an unbound key");
|
||||
TEST_ASSERT(e, player->state == 0,
|
||||
"an unbound key changed the actor state to %d", player->state);
|
||||
|
||||
// A bound key from a different keyboard is ignored, so split-keyboard
|
||||
// local multiplayer does not cross-talk.
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID + 1, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a bound key from an unbound keyboard");
|
||||
TEST_ASSERT(e, player->state == 0,
|
||||
"a key from another keyboard changed the actor state to %d", player->state);
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_handle_gamepad_events(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, make_player());
|
||||
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
|
||||
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a dpad left press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"a dpad left press did not start the actor moving left (state %d)", player->state);
|
||||
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_UP, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a dpad left release");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"a dpad left release did not stop the actor (state %d)", player->state);
|
||||
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a dpad up press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
|
||||
"a dpad up press did not start the actor moving up (state %d)", player->state);
|
||||
|
||||
// A press from an unbound gamepad is ignored.
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID + 1, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a dpad press from an unbound gamepad");
|
||||
TEST_ASSERT(e, player->state == 0,
|
||||
"a press from another gamepad changed the actor state to %d", player->state);
|
||||
|
||||
// An unbound button on the bound gamepad is ignored.
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_START);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching an unbound gamepad button");
|
||||
TEST_ASSERT(e, player->state == 0,
|
||||
"an unbound gamepad button changed the actor state to %d", player->state);
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_handle_event_edge_cases(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, make_player());
|
||||
|
||||
// With no control maps installed at all, every event is a no-op.
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching into an empty control map table");
|
||||
|
||||
// A map in a later slot is still reached, so the scan does not stop at
|
||||
// the first unpopulated entry.
|
||||
CATCH(e, akgl_controller_default(5, "player", TEST_KBID, TEST_JSID));
|
||||
player->state = 0;
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching to a control map in a later slot");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"a control map in slot 5 was not consulted (state %d)", player->state);
|
||||
|
||||
// An event type that no binding uses falls through harmlessly.
|
||||
memset(&event, 0x00, sizeof(SDL_Event));
|
||||
event.type = SDL_EVENT_MOUSE_MOTION;
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching an event type with no bindings");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_event(NULL, &event),
|
||||
"dispatching with a NULL appstate");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
akgl_controller_handle_event(&appstate_placeholder, NULL),
|
||||
"dispatching a NULL event");
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_gamepad_button_handlers(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, make_player());
|
||||
|
||||
// These handlers find their actor by looking up "player" in the registry
|
||||
// rather than taking it as an argument.
|
||||
player->state = 0;
|
||||
player->movement_controls_face = false;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event),
|
||||
"dpad down press handler");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
|
||||
"the dpad down handler did not set MOVING_DOWN (state %d)", player->state);
|
||||
// With automatic facing off, the handler sets the facing itself.
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
|
||||
"the dpad down handler did not set FACE_DOWN (state %d)", player->state);
|
||||
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event),
|
||||
"dpad down release handler");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
|
||||
"the dpad down release handler did not clear MOVING_DOWN (state %d)", player->state);
|
||||
TEST_ASSERT(e, player->curSpriteFrameId == 0,
|
||||
"the release handler did not reset the animation frame");
|
||||
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad up press handler");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
|
||||
"the dpad up handler did not set MOVING_UP (state %d)", player->state);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad up release handler");
|
||||
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad left press handler");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"the dpad left handler did not set MOVING_LEFT (state %d)", player->state);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad left release handler");
|
||||
|
||||
player->state = 0;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad right press handler");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
|
||||
"the dpad right handler did not set MOVING_RIGHT (state %d)", player->state);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad right release handler");
|
||||
|
||||
// With automatic facing on, the handler leaves facing to the actor's own
|
||||
// face function.
|
||||
player->state = 0;
|
||||
player->movement_controls_face = true;
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event),
|
||||
"dpad down press with automatic facing on");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
|
||||
"the handler set facing even though the actor faces automatically (state %d)",
|
||||
player->state);
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_down(NULL, &event),
|
||||
"dpad press handler with a NULL appstate");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_button_down(&appstate_placeholder, NULL),
|
||||
"dpad press handler with a NULL event");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_up(NULL, &event),
|
||||
"dpad release handler with a NULL appstate");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_button_up(&appstate_placeholder, NULL),
|
||||
"dpad release handler with a NULL event");
|
||||
|
||||
// With no actor named "player" registered there is nothing to drive.
|
||||
CATCH(e, akgl_registry_init_actor());
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_button_down(&appstate_placeholder, &event),
|
||||
"dpad press handler with no player actor registered");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_button_up(&appstate_placeholder, &event),
|
||||
"dpad release handler with no player actor registered");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_device_events(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
// The dummy drivers present no gamepads, so the add and remove handlers
|
||||
// take their "unknown device" paths. Neither may crash.
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_ADDED, 999, 0);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_added(&appstate_placeholder, &event),
|
||||
"handling an add for a device that cannot be opened");
|
||||
|
||||
make_button_event(&event, SDL_EVENT_GAMEPAD_REMOVED, 999, 0);
|
||||
TEST_EXPECT_OK(e, gamepad_handle_removed(&appstate_placeholder, &event),
|
||||
"handling a remove for a device that was never open");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_added(NULL, &event),
|
||||
"add handler with a NULL appstate");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_added(&appstate_placeholder, NULL),
|
||||
"add handler with a NULL event");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_removed(NULL, &event),
|
||||
"remove handler with a NULL appstate");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
gamepad_handle_removed(&appstate_placeholder, NULL),
|
||||
"remove handler with a NULL event");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_device_enumeration(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
// Enumeration has to succeed on a machine with no input devices, which
|
||||
// is the normal state under the dummy drivers and in CI.
|
||||
TEST_EXPECT_OK(e, akgl_controller_list_keyboards(), "listing keyboards");
|
||||
TEST_EXPECT_OK(e, akgl_controller_open_gamepads(), "opening gamepads");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
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());
|
||||
} CLEANUP {
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
421
tests/game.c
Normal file
421
tests/game.c
Normal file
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* @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_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);
|
||||
}
|
||||
378
tests/heap.c
Normal file
378
tests/heap.c
Normal file
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* @file heap.c
|
||||
* @brief Unit tests for the fixed-size object pools and their refcounting.
|
||||
*
|
||||
* The pools are process-wide arrays, so every test that fills one calls
|
||||
* akgl_heap_init() first and leaves the heap empty behind it.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/**
|
||||
* @brief Reset every pool and the registries that reference their objects.
|
||||
*
|
||||
* The registries hold raw pointers into the pools, so clearing a pool without
|
||||
* clearing the registry would leave dangling entries for the next test.
|
||||
*/
|
||||
static akerr_ErrorContext *reset_all_heaps(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_registry_init_actor());
|
||||
CATCH(e, akgl_registry_init_sprite());
|
||||
CATCH(e, akgl_registry_init_spritesheet());
|
||||
CATCH(e, akgl_registry_init_character());
|
||||
CATCH(e, akgl_heap_init());
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_heap_init_clears_every_pool(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
bool clean = true;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
// Dirty every pool, then require that init scrubs all of them.
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||
HEAP_ACTOR[i].refcount = 5;
|
||||
}
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
|
||||
HEAP_SPRITE[i].refcount = 5;
|
||||
}
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
|
||||
HEAP_SPRITESHEET[i].refcount = 5;
|
||||
}
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
|
||||
HEAP_CHARACTER[i].refcount = 5;
|
||||
}
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
|
||||
HEAP_STRING[i].refcount = 5;
|
||||
}
|
||||
|
||||
CATCH(e, akgl_heap_init());
|
||||
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||
TEST_ASSERT_FLAG(clean, HEAP_ACTOR[i].refcount == 0);
|
||||
}
|
||||
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the actor pool");
|
||||
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
|
||||
TEST_ASSERT_FLAG(clean, HEAP_SPRITE[i].refcount == 0);
|
||||
}
|
||||
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the sprite pool");
|
||||
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
|
||||
TEST_ASSERT_FLAG(clean, HEAP_SPRITESHEET[i].refcount == 0);
|
||||
}
|
||||
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the spritesheet pool");
|
||||
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
|
||||
TEST_ASSERT_FLAG(clean, HEAP_CHARACTER[i].refcount == 0);
|
||||
}
|
||||
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the character pool");
|
||||
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
|
||||
TEST_ASSERT_FLAG(clean, HEAP_STRING[i].refcount == 0);
|
||||
}
|
||||
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the string pool");
|
||||
|
||||
// akgl_heap_init_actor clears only the actor pool.
|
||||
HEAP_ACTOR[0].refcount = 9;
|
||||
HEAP_SPRITE[0].refcount = 9;
|
||||
CATCH(e, akgl_heap_init_actor());
|
||||
TEST_ASSERT(e, HEAP_ACTOR[0].refcount == 0,
|
||||
"akgl_heap_init_actor did not clear the actor pool");
|
||||
TEST_ASSERT(e, HEAP_SPRITE[0].refcount == 9,
|
||||
"akgl_heap_init_actor cleared the sprite pool as well");
|
||||
HEAP_SPRITE[0].refcount = 0;
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_heap_next_string_refcounting(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_String *first = NULL;
|
||||
akgl_String *second = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_all_heaps());
|
||||
|
||||
// akgl_heap_next_string is the only acquire function that claims the slot
|
||||
// it hands out; the others leave refcount at zero for the caller to set.
|
||||
CATCH(e, akgl_heap_next_string(&first));
|
||||
TEST_ASSERT(e, first->refcount == 1,
|
||||
"akgl_heap_next_string returned a slot with refcount %d, expected 1",
|
||||
first->refcount);
|
||||
|
||||
CATCH(e, akgl_heap_next_string(&second));
|
||||
TEST_ASSERT(e, second != first,
|
||||
"akgl_heap_next_string handed out the same slot twice");
|
||||
|
||||
CATCH(e, akgl_heap_release_string(first));
|
||||
CATCH(e, akgl_heap_release_string(second));
|
||||
TEST_ASSERT(e, first->refcount == 0,
|
||||
"releasing a string left refcount at %d", first->refcount);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_heap_exhaustion(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Actor *actor = NULL;
|
||||
akgl_Sprite *sprite = NULL;
|
||||
akgl_SpriteSheet *sheet = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_String *str = NULL;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
// Actors: the acquire function does not claim the slot, so the test does.
|
||||
CATCH(e, reset_all_heaps());
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||
HEAP_ACTOR[i].refcount = 1;
|
||||
}
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_actor(&actor),
|
||||
"akgl_heap_next_actor with every slot claimed");
|
||||
|
||||
CATCH(e, reset_all_heaps());
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
|
||||
HEAP_SPRITE[i].refcount = 1;
|
||||
}
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_sprite(&sprite),
|
||||
"akgl_heap_next_sprite with every slot claimed");
|
||||
|
||||
CATCH(e, reset_all_heaps());
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
|
||||
HEAP_SPRITESHEET[i].refcount = 1;
|
||||
}
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_spritesheet(&sheet),
|
||||
"akgl_heap_next_spritesheet with every slot claimed");
|
||||
|
||||
CATCH(e, reset_all_heaps());
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
|
||||
HEAP_CHARACTER[i].refcount = 1;
|
||||
}
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_character(&basechar),
|
||||
"akgl_heap_next_character with every slot claimed");
|
||||
|
||||
// Strings claim their own slots, so draining the pool needs no help.
|
||||
CATCH(e, reset_all_heaps());
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
|
||||
CATCH(e, akgl_heap_next_string(&str));
|
||||
}
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_string(&str),
|
||||
"akgl_heap_next_string with the pool drained");
|
||||
|
||||
// A single release makes exactly one slot available again.
|
||||
CATCH(e, akgl_heap_release_string(&HEAP_STRING[0]));
|
||||
TEST_EXPECT_OK(e, akgl_heap_next_string(&str),
|
||||
"akgl_heap_next_string after freeing one slot");
|
||||
TEST_ASSERT(e, str == &HEAP_STRING[0],
|
||||
"the reclaimed string was not the slot that was released");
|
||||
|
||||
CATCH(e, reset_all_heaps());
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_heap_release_refcounting(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Sprite *sprite = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_all_heaps());
|
||||
CATCH(e, akgl_heap_next_sprite(&sprite));
|
||||
|
||||
// A shared object survives until the last reference goes away.
|
||||
sprite->refcount = 2;
|
||||
strncpy((char *)&sprite->name, "shared", AKGL_SPRITE_MAX_NAME_LENGTH - 1);
|
||||
CATCH(e, akgl_heap_release_sprite(sprite));
|
||||
TEST_ASSERT(e, sprite->refcount == 1,
|
||||
"releasing a twice-referenced sprite left refcount %d, expected 1",
|
||||
sprite->refcount);
|
||||
TEST_ASSERT(e, sprite->name[0] == 's',
|
||||
"releasing a still-referenced sprite cleared its data");
|
||||
|
||||
CATCH(e, akgl_heap_release_sprite(sprite));
|
||||
TEST_ASSERT(e, sprite->refcount == 0,
|
||||
"the final release left refcount %d, expected 0", sprite->refcount);
|
||||
TEST_ASSERT(e, sprite->name[0] == 0x00,
|
||||
"the final release did not clear the sprite");
|
||||
|
||||
// Releasing an already-free object is clamped, not wrapped below zero.
|
||||
CATCH(e, akgl_heap_release_sprite(sprite));
|
||||
TEST_ASSERT(e, sprite->refcount == 0,
|
||||
"over-releasing drove refcount to %d, expected a clamp at 0",
|
||||
sprite->refcount);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_heap_release_actor_children(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Actor *parent = NULL;
|
||||
akgl_Actor *child = NULL;
|
||||
char namebuf[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||||
bool released = true;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_all_heaps());
|
||||
CATCH(e, akgl_heap_next_actor(&parent));
|
||||
CATCH(e, akgl_actor_initialize(parent, "parent"));
|
||||
|
||||
// Fill every child slot, then release the parent once.
|
||||
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN; i++ ) {
|
||||
snprintf((char *)&namebuf, AKGL_ACTOR_MAX_NAME_LENGTH, "child%d", i);
|
||||
CATCH(e, akgl_heap_next_actor(&child));
|
||||
CATCH(e, akgl_actor_initialize(child, (char *)&namebuf));
|
||||
CATCH(e, akgl_actor_add_child(parent, child));
|
||||
}
|
||||
|
||||
CATCH(e, akgl_heap_release_actor(parent));
|
||||
TEST_ASSERT(e, parent->refcount == 0,
|
||||
"releasing the parent left refcount %d", parent->refcount);
|
||||
|
||||
// Each child was initialized to 1 and incremented to 2 by add_child, so
|
||||
// the recursive release should have brought every one of them back to 1.
|
||||
for ( i = 1; i <= AKGL_ACTOR_MAX_CHILDREN; i++ ) {
|
||||
TEST_ASSERT_FLAG(released, HEAP_ACTOR[i].refcount == 1);
|
||||
}
|
||||
TEST_ASSERT(e, released,
|
||||
"releasing a parent did not decrement every child exactly once");
|
||||
|
||||
CATCH(e, reset_all_heaps());
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_heap_release_clears_registry(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_Actor *actor = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_all_heaps());
|
||||
|
||||
CATCH(e, akgl_heap_next_actor(&actor));
|
||||
CATCH(e, akgl_actor_initialize(actor, "registered"));
|
||||
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "registered", NULL) != NULL,
|
||||
"akgl_actor_initialize did not register the actor");
|
||||
CATCH(e, akgl_heap_release_actor(actor));
|
||||
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "registered", NULL) == NULL,
|
||||
"releasing an actor left a dangling registry entry");
|
||||
|
||||
CATCH(e, akgl_heap_next_character(&basechar));
|
||||
CATCH(e, akgl_character_initialize(basechar, "regchar"));
|
||||
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "regchar", NULL) != NULL,
|
||||
"akgl_character_initialize did not register the character");
|
||||
CATCH(e, akgl_heap_release_character(basechar));
|
||||
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "regchar", NULL) == NULL,
|
||||
"releasing a character left a dangling registry entry");
|
||||
|
||||
CATCH(e, reset_all_heaps());
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_heap_release_nullpointers(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_actor(NULL),
|
||||
"akgl_heap_release_actor(NULL)");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_sprite(NULL),
|
||||
"akgl_heap_release_sprite(NULL)");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_spritesheet(NULL),
|
||||
"akgl_heap_release_spritesheet(NULL)");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_character(NULL),
|
||||
"akgl_heap_release_character(NULL)");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_string(NULL),
|
||||
"akgl_heap_release_string(NULL)");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_heap_release_spritesheet_texture(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_SpriteSheet *sheet = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_all_heaps());
|
||||
CATCH(e, akgl_heap_next_spritesheet(&sheet));
|
||||
|
||||
// A spritesheet with no texture must still release cleanly; the texture
|
||||
// branch is exercised by the sprite suite, which has a live renderer.
|
||||
sheet->refcount = 1;
|
||||
sheet->texture = NULL;
|
||||
CATCH(e, akgl_heap_release_spritesheet(sheet));
|
||||
TEST_ASSERT(e, sheet->refcount == 0,
|
||||
"releasing a textureless spritesheet left refcount %d", sheet->refcount);
|
||||
TEST_ASSERT(e, sheet->texture == NULL,
|
||||
"releasing a spritesheet left a non-NULL texture pointer");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
|
||||
CATCH(errctx, test_heap_init_clears_every_pool());
|
||||
CATCH(errctx, test_heap_next_string_refcounting());
|
||||
CATCH(errctx, test_heap_exhaustion());
|
||||
CATCH(errctx, test_heap_release_refcounting());
|
||||
CATCH(errctx, test_heap_release_actor_children());
|
||||
CATCH(errctx, test_heap_release_clears_registry());
|
||||
CATCH(errctx, test_heap_release_nullpointers());
|
||||
CATCH(errctx, test_heap_release_spritesheet_texture());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
366
tests/json_helpers.c
Normal file
366
tests/json_helpers.c
Normal file
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* @file json_helpers.c
|
||||
* @brief Unit tests for the typed JSON accessors.
|
||||
*
|
||||
* These exercise the accessors directly rather than through sprite, character,
|
||||
* or tilemap loading, so the error paths are reachable without building a
|
||||
* malformed asset for every case.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <jansson.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/json_helpers.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief Fixture document shared by every test in this file. */
|
||||
static json_t *fixture = NULL;
|
||||
|
||||
/** @brief Load tests/assets/snippets/test_json_helpers.json into @ref fixture. */
|
||||
static akerr_ErrorContext *load_fixture(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
json_error_t jsonerr;
|
||||
akgl_String *pathstr = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_heap_next_string(&pathstr));
|
||||
snprintf(
|
||||
(char *)&pathstr->data,
|
||||
AKGL_MAX_STRING_LENGTH,
|
||||
"%s%s",
|
||||
SDL_GetBasePath(),
|
||||
"assets/snippets/test_json_helpers.json");
|
||||
fixture = json_load_file((char *)&pathstr->data, 0, &jsonerr);
|
||||
FAIL_ZERO_BREAK(e, fixture, AKERR_IO,
|
||||
"Unable to load the JSON fixture: %s", (char *)jsonerr.text);
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(pathstr));
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_json_scalar_accessors(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
int intval = 0;
|
||||
float floatval = 0;
|
||||
bool boolval = false;
|
||||
json_t *objval = NULL;
|
||||
json_t *arrayval = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
TEST_EXPECT_OK(e, akgl_get_json_integer_value(fixture, "count", &intval), "read count");
|
||||
TEST_ASSERT(e, intval == 42, "count read as %d, expected 42", intval);
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_get_json_integer_value(fixture, "negative", &intval), "read negative");
|
||||
TEST_ASSERT(e, intval == -17, "negative read as %d, expected -17", intval);
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_get_json_number_value(fixture, "ratio", &floatval), "read ratio");
|
||||
TEST_ASSERT_FEQ(e, floatval, 2.5f, "ratio read as %f, expected 2.5", floatval);
|
||||
|
||||
// A JSON integer is a number, so the float accessor accepts it too.
|
||||
TEST_EXPECT_OK(e, akgl_get_json_number_value(fixture, "count", &floatval), "read count as a number");
|
||||
TEST_ASSERT_FEQ(e, floatval, 42.0f, "count read as number %f, expected 42", floatval);
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_get_json_boolean_value(fixture, "enabled", &boolval), "read enabled");
|
||||
TEST_ASSERT(e, boolval == true, "enabled read as false");
|
||||
TEST_EXPECT_OK(e, akgl_get_json_boolean_value(fixture, "disabled", &boolval), "read disabled");
|
||||
TEST_ASSERT(e, boolval == false, "disabled read as true");
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_get_json_object_value(fixture, "nested", &objval), "read nested");
|
||||
TEST_ASSERT(e, objval != NULL, "nested object came back NULL");
|
||||
TEST_EXPECT_OK(e, akgl_get_json_integer_value(objval, "innercount", &intval), "read nested innercount");
|
||||
TEST_ASSERT(e, intval == 7, "nested innercount read as %d, expected 7", intval);
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_get_json_array_value(fixture, "integers", &arrayval), "read integers array");
|
||||
TEST_ASSERT(e, json_array_size(arrayval) == 3,
|
||||
"integers array had %d entries, expected 3", (int)json_array_size(arrayval));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_json_double_value(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
double dblval = 0;
|
||||
|
||||
ATTEMPT {
|
||||
TEST_EXPECT_OK(e, akgl_get_json_double_value(fixture, "ratio", &dblval), "read ratio as a double");
|
||||
TEST_ASSERT(e, dblval > 2.4999 && dblval < 2.5001,
|
||||
"ratio read as double %f, expected 2.5", dblval);
|
||||
|
||||
// Integers satisfy json_is_number, so the double accessor takes them.
|
||||
TEST_EXPECT_OK(e, akgl_get_json_double_value(fixture, "count", &dblval), "read count as a double");
|
||||
TEST_ASSERT(e, dblval > 41.999 && dblval < 42.001,
|
||||
"count read as double %f, expected 42", dblval);
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_double_value(fixture, "absent", &dblval),
|
||||
"double accessor on a missing key");
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_double_value(fixture, "name", &dblval),
|
||||
"double accessor on a string value");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_double_value(NULL, "ratio", &dblval),
|
||||
"double accessor on a NULL object");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_json_string_accessor(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_String *allocated = NULL;
|
||||
akgl_String *reused = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
// A NULL destination makes the accessor claim a heap string.
|
||||
TEST_EXPECT_OK(e, akgl_get_json_string_value(fixture, "name", &allocated),
|
||||
"read name into a NULL destination");
|
||||
TEST_ASSERT(e, allocated != NULL, "the accessor did not allocate a destination string");
|
||||
TEST_ASSERT(e, strcmp((char *)&allocated->data, "json helper fixture") == 0,
|
||||
"name read as \"%s\"", (char *)&allocated->data);
|
||||
|
||||
// A caller-supplied destination is written in place, not replaced.
|
||||
CATCH(e, akgl_heap_next_string(&reused));
|
||||
CATCH(e, akgl_string_initialize(reused, "placeholder"));
|
||||
{
|
||||
akgl_String *before = reused;
|
||||
TEST_EXPECT_OK(e, akgl_get_json_string_value(fixture, "name", &reused),
|
||||
"read name into a preallocated destination");
|
||||
TEST_ASSERT(e, reused == before,
|
||||
"the accessor replaced a caller-supplied destination pointer");
|
||||
}
|
||||
TEST_ASSERT(e, strcmp((char *)&reused->data, "json helper fixture") == 0,
|
||||
"in-place read produced \"%s\"", (char *)&reused->data);
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_string_value(fixture, "absent", &reused),
|
||||
"string accessor on a missing key");
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_string_value(fixture, "count", &reused),
|
||||
"string accessor on an integer value");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_string_value(NULL, "name", &reused),
|
||||
"string accessor on a NULL object");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_string_value(fixture, NULL, &reused),
|
||||
"string accessor with a NULL key");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_string_value(fixture, "name", NULL),
|
||||
"string accessor with a NULL destination");
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(allocated));
|
||||
IGNORE(akgl_heap_release_string(reused));
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_json_array_index_accessors(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
json_t *integers = NULL;
|
||||
json_t *strings = NULL;
|
||||
json_t *objects = NULL;
|
||||
json_t *mixed = NULL;
|
||||
json_t *element = NULL;
|
||||
akgl_String *strval = NULL;
|
||||
int intval = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_get_json_array_value(fixture, "integers", &integers));
|
||||
CATCH(e, akgl_get_json_array_value(fixture, "strings", &strings));
|
||||
CATCH(e, akgl_get_json_array_value(fixture, "objects", &objects));
|
||||
CATCH(e, akgl_get_json_array_value(fixture, "mixed", &mixed));
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_get_json_array_index_integer(integers, 0, &intval), "integers[0]");
|
||||
TEST_ASSERT(e, intval == 10, "integers[0] read as %d, expected 10", intval);
|
||||
TEST_EXPECT_OK(e, akgl_get_json_array_index_integer(integers, 2, &intval), "integers[2]");
|
||||
TEST_ASSERT(e, intval == 30, "integers[2] read as %d, expected 30", intval);
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_get_json_array_index_string(strings, 1, &strval), "strings[1]");
|
||||
TEST_ASSERT(e, strcmp((char *)&strval->data, "beta") == 0,
|
||||
"strings[1] read as \"%s\", expected \"beta\"", (char *)&strval->data);
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_get_json_array_index_object(objects, 1, &element), "objects[1]");
|
||||
TEST_EXPECT_OK(e, akgl_get_json_integer_value(element, "id", &intval), "objects[1].id");
|
||||
TEST_ASSERT(e, intval == 2, "objects[1].id read as %d, expected 2", intval);
|
||||
|
||||
// One past the end, and far past the end, are both out of bounds.
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_integer(integers, 3, &intval),
|
||||
"integers[3] is one past the end");
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_integer(integers, 99, &intval),
|
||||
"integers[99] is far past the end");
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_object(objects, 5, &element),
|
||||
"objects[5] is past the end");
|
||||
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_string(strings, 5, &strval),
|
||||
"strings[5] is past the end");
|
||||
|
||||
// The mixed array holds one of each type, so every accessor can be shown
|
||||
// to reject the entries that are not its own.
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_index_integer(mixed, 1, &intval),
|
||||
"integer accessor on a string element");
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_index_string(mixed, 0, &strval),
|
||||
"string accessor on an integer element");
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_index_object(mixed, 0, &element),
|
||||
"object accessor on an integer element");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_integer(NULL, 0, &intval),
|
||||
"integer index accessor on a NULL array");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_object(NULL, 0, &element),
|
||||
"object index accessor on a NULL array");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_string(NULL, 0, &strval),
|
||||
"string index accessor on a NULL array");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_string(strings, 0, NULL),
|
||||
"string index accessor with a NULL destination");
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(strval));
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_json_type_and_key_errors(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
int intval = 0;
|
||||
float floatval = 0;
|
||||
bool boolval = false;
|
||||
json_t *objval = NULL;
|
||||
json_t *arrayval = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
// Missing keys.
|
||||
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_integer_value(fixture, "absent", &intval),
|
||||
"integer accessor on a missing key");
|
||||
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_number_value(fixture, "absent", &floatval),
|
||||
"number accessor on a missing key");
|
||||
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_boolean_value(fixture, "absent", &boolval),
|
||||
"boolean accessor on a missing key");
|
||||
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_object_value(fixture, "absent", &objval),
|
||||
"object accessor on a missing key");
|
||||
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_array_value(fixture, "absent", &arrayval),
|
||||
"array accessor on a missing key");
|
||||
|
||||
// Wrong types.
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_integer_value(fixture, "name", &intval),
|
||||
"integer accessor on a string");
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_integer_value(fixture, "ratio", &intval),
|
||||
"integer accessor on a real");
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_number_value(fixture, "name", &floatval),
|
||||
"number accessor on a string");
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_boolean_value(fixture, "count", &boolval),
|
||||
"boolean accessor on an integer");
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_object_value(fixture, "integers", &objval),
|
||||
"object accessor on an array");
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_value(fixture, "nested", &arrayval),
|
||||
"array accessor on an object");
|
||||
|
||||
// NULL containers.
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_integer_value(NULL, "count", &intval),
|
||||
"integer accessor on a NULL object");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_number_value(NULL, "ratio", &floatval),
|
||||
"number accessor on a NULL object");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_boolean_value(NULL, "enabled", &boolval),
|
||||
"boolean accessor on a NULL object");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_object_value(NULL, "nested", &objval),
|
||||
"object accessor on a NULL object");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_value(NULL, "integers", &arrayval),
|
||||
"array accessor on a NULL object");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_json_with_default(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
int dest = 0;
|
||||
int defval = 99;
|
||||
akerr_ErrorContext *keyerr = NULL;
|
||||
akerr_ErrorContext *typeerr = NULL;
|
||||
int junk = 0;
|
||||
|
||||
ATTEMPT {
|
||||
// A NULL error means the read succeeded, so the default is not applied.
|
||||
dest = 1;
|
||||
TEST_EXPECT_OK(e, akgl_get_json_with_default(NULL, (void *)&defval, (void *)&dest, sizeof(int)),
|
||||
"with_default on a NULL error");
|
||||
TEST_ASSERT(e, dest == 1, "with_default overwrote a successful read (dest is now %d)", dest);
|
||||
|
||||
// An AKERR_KEY failure substitutes the default.
|
||||
dest = 1;
|
||||
keyerr = akgl_get_json_integer_value(fixture, "absent", &junk);
|
||||
TEST_ASSERT(e, keyerr != NULL, "the missing-key read unexpectedly succeeded");
|
||||
TEST_EXPECT_OK(e, akgl_get_json_with_default(keyerr, (void *)&defval, (void *)&dest, sizeof(int)),
|
||||
"with_default on an AKERR_KEY error");
|
||||
TEST_ASSERT(e, dest == 99, "with_default did not apply the default (dest is %d)", dest);
|
||||
keyerr = NULL;
|
||||
|
||||
// An unrelated failure is not swallowed, so the caller still sees it.
|
||||
dest = 1;
|
||||
typeerr = akgl_get_json_integer_value(fixture, "name", &junk);
|
||||
TEST_ASSERT(e, typeerr != NULL, "the wrong-type read unexpectedly succeeded");
|
||||
TEST_EXPECT_STATUS(e, AKERR_TYPE,
|
||||
akgl_get_json_with_default(typeerr, (void *)&defval, (void *)&dest, sizeof(int)),
|
||||
"with_default must propagate an unrelated error");
|
||||
TEST_ASSERT(e, dest == 1, "with_default applied the default for an unrelated error");
|
||||
typeerr = NULL;
|
||||
|
||||
// NULL arguments alongside a real error are a contract violation.
|
||||
keyerr = akgl_get_json_integer_value(fixture, "absent", &junk);
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
akgl_get_json_with_default(keyerr, NULL, (void *)&dest, sizeof(int)),
|
||||
"with_default with a NULL default value");
|
||||
keyerr = NULL;
|
||||
keyerr = akgl_get_json_integer_value(fixture, "absent", &junk);
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
||||
akgl_get_json_with_default(keyerr, (void *)&defval, NULL, sizeof(int)),
|
||||
"with_default with a NULL destination");
|
||||
keyerr = NULL;
|
||||
} CLEANUP {
|
||||
if ( keyerr != NULL ) {
|
||||
keyerr->handled = true;
|
||||
keyerr = akerr_release_error(keyerr);
|
||||
}
|
||||
if ( typeerr != NULL ) {
|
||||
typeerr->handled = true;
|
||||
typeerr = akerr_release_error(typeerr);
|
||||
}
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, load_fixture());
|
||||
|
||||
CATCH(errctx, test_json_scalar_accessors());
|
||||
CATCH(errctx, test_json_double_value());
|
||||
CATCH(errctx, test_json_string_accessor());
|
||||
CATCH(errctx, test_json_array_index_accessors());
|
||||
CATCH(errctx, test_json_type_and_key_errors());
|
||||
CATCH(errctx, test_json_with_default());
|
||||
} CLEANUP {
|
||||
if ( fixture != NULL ) {
|
||||
json_decref(fixture);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
747
tests/physics.c
Normal file
747
tests/physics.c
Normal file
@@ -0,0 +1,747 @@
|
||||
/**
|
||||
* @file physics.c
|
||||
* @brief Unit tests for the physics backends and the simulation loop.
|
||||
*
|
||||
* None of these tests need a renderer or a window. The simulation loop walks
|
||||
* HEAP_ACTOR directly, so each test rebuilds the actor heap rather than relying
|
||||
* on state left behind by an earlier one.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief Records whether the stub movement logic ran, and with what dt. */
|
||||
static int stub_movement_calls = 0;
|
||||
static float32_t stub_movement_last_dt = 0;
|
||||
|
||||
/** @brief Movement logic stub that records its invocation and does nothing else. */
|
||||
static akerr_ErrorContext *stub_movement_noop(akgl_Actor *actor, float32_t dt)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
stub_movement_calls += 1;
|
||||
stub_movement_last_dt = dt;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/** @brief Movement logic stub that raises the interrupt the simulator must swallow. */
|
||||
static akerr_ErrorContext *stub_movement_interrupt(akgl_Actor *actor, float32_t dt)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
stub_movement_calls += 1;
|
||||
FAIL_RETURN(e, AKGL_ERR_LOGICINTERRUPT, "deliberate interrupt from test stub");
|
||||
}
|
||||
|
||||
/** @brief Movement logic stub that raises an error the simulator must propagate. */
|
||||
static akerr_ErrorContext *stub_movement_error(akgl_Actor *actor, float32_t dt)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
stub_movement_calls += 1;
|
||||
FAIL_RETURN(e, AKERR_VALUE, "deliberate error from test stub");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a character and an actor bound to it, ready for simulation.
|
||||
*
|
||||
* The actor is placed on the heap with a nonzero refcount and a movement logic
|
||||
* stub, which is the minimum the simulation loop requires to process it.
|
||||
*/
|
||||
static akerr_ErrorContext *make_sim_actor(akgl_Actor **actor, akgl_Character **basechar, char *name)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_heap_next_character(basechar));
|
||||
CATCH(e, akgl_character_initialize(*basechar, name));
|
||||
CATCH(e, akgl_heap_next_actor(actor));
|
||||
CATCH(e, akgl_actor_initialize(*actor, name));
|
||||
(*actor)->basechar = *basechar;
|
||||
(*actor)->movementlogicfunc = &stub_movement_noop;
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/** @brief Reset the actor heap so each simulation test starts from a clean slate. */
|
||||
static akerr_ErrorContext *reset_sim_heap(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_registry_init_actor());
|
||||
CATCH(e, akgl_registry_init_character());
|
||||
CATCH(e, akgl_heap_init());
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
stub_movement_calls = 0;
|
||||
stub_movement_last_dt = 0;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_init_null(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
TEST_EXPECT_OK(e, akgl_physics_init_null(&backend), "akgl_physics_init_null");
|
||||
|
||||
TEST_ASSERT(e, backend.gravity == &akgl_physics_null_gravity,
|
||||
"init_null did not install the null gravity function");
|
||||
TEST_ASSERT(e, backend.collide == &akgl_physics_null_collide,
|
||||
"init_null did not install the null collide function");
|
||||
TEST_ASSERT(e, backend.move == &akgl_physics_null_move,
|
||||
"init_null did not install the null move function");
|
||||
TEST_ASSERT(e, backend.simulate == &akgl_physics_simulate,
|
||||
"init_null did not install the shared simulate function");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_init_null(NULL),
|
||||
"akgl_physics_init_null(NULL)");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_null_backend_is_inert(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_Actor actor;
|
||||
akgl_Actor reference;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
TEST_EXPECT_OK(e, akgl_physics_init_null(&backend), "akgl_physics_init_null");
|
||||
|
||||
actor.x = 3.0f; actor.y = 5.0f; actor.z = 7.0f;
|
||||
actor.vx = 11.0f; actor.vy = 13.0f; actor.vz = 17.0f;
|
||||
actor.ex = 19.0f; actor.ey = 23.0f; actor.ez = 29.0f;
|
||||
memcpy(&reference, &actor, sizeof(akgl_Actor));
|
||||
|
||||
TEST_EXPECT_OK(e, backend.gravity(&backend, &actor, 1.0f), "null gravity");
|
||||
TEST_EXPECT_OK(e, backend.move(&backend, &actor, 1.0f), "null move");
|
||||
TEST_EXPECT_OK(e, backend.collide(&backend, &actor, &reference), "null collide");
|
||||
|
||||
TEST_ASSERT(e, memcmp(&actor, &reference, sizeof(akgl_Actor)) == 0,
|
||||
"the null backend modified the actor");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_gravity(NULL, &actor, 1.0f),
|
||||
"null gravity with NULL self");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_move(NULL, &actor, 1.0f),
|
||||
"null move with NULL self");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_collide(NULL, &actor, &reference),
|
||||
"null collide with NULL self");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_arcade_gravity(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_Actor actor;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
|
||||
// X gravity subtracts, because the X origin is screen-left.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
backend.gravity_x = 10.0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 0.5f), "arcade gravity x");
|
||||
TEST_ASSERT_FEQ(e, actor.ex, -5.0f, "gravity_x 10 over dt 0.5 gave ex %f, expected -5", actor.ex);
|
||||
TEST_ASSERT_FEQ(e, actor.ey, 0.0f, "gravity_x leaked into ey (%f)", actor.ey);
|
||||
TEST_ASSERT_FEQ(e, actor.ez, 0.0f, "gravity_x leaked into ez (%f)", actor.ez);
|
||||
|
||||
// Y gravity adds, because the Y origin is down-screen.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
backend.gravity_x = 0;
|
||||
backend.gravity_y = 10.0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 0.5f), "arcade gravity y");
|
||||
TEST_ASSERT_FEQ(e, actor.ey, 5.0f, "gravity_y 10 over dt 0.5 gave ey %f, expected 5", actor.ey);
|
||||
TEST_ASSERT_FEQ(e, actor.ex, 0.0f, "gravity_y leaked into ex (%f)", actor.ex);
|
||||
|
||||
// Z gravity subtracts, because the Z origin is behind the camera.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
backend.gravity_y = 0;
|
||||
backend.gravity_z = 4.0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 0.25f), "arcade gravity z");
|
||||
TEST_ASSERT_FEQ(e, actor.ez, -1.0f, "gravity_z 4 over dt 0.25 gave ez %f, expected -1", actor.ez);
|
||||
|
||||
// Negative gravity reverses each axis.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
backend.gravity_x = -10.0;
|
||||
backend.gravity_y = -10.0;
|
||||
backend.gravity_z = -10.0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 1.0f), "arcade gravity negative");
|
||||
TEST_ASSERT_FEQ(e, actor.ex, 10.0f, "negative gravity_x gave ex %f, expected 10", actor.ex);
|
||||
TEST_ASSERT_FEQ(e, actor.ey, -10.0f, "negative gravity_y gave ey %f, expected -10", actor.ey);
|
||||
TEST_ASSERT_FEQ(e, actor.ez, 10.0f, "negative gravity_z gave ez %f, expected 10", actor.ez);
|
||||
|
||||
// A zero dt applies no force even when gravity is set.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
backend.gravity_x = 10.0;
|
||||
backend.gravity_y = 10.0;
|
||||
backend.gravity_z = 10.0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 0.0f), "arcade gravity dt 0");
|
||||
TEST_ASSERT_FEQ(e, actor.ex, 0.0f, "dt 0 still moved ex (%f)", actor.ex);
|
||||
TEST_ASSERT_FEQ(e, actor.ey, 0.0f, "dt 0 still moved ey (%f)", actor.ey);
|
||||
TEST_ASSERT_FEQ(e, actor.ez, 0.0f, "dt 0 still moved ez (%f)", actor.ez);
|
||||
|
||||
// All-zero gravity leaves every axis alone.
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
actor.ex = 1.0f; actor.ey = 2.0f; actor.ez = 3.0f;
|
||||
TEST_EXPECT_OK(e, akgl_physics_arcade_gravity(&backend, &actor, 1.0f), "arcade gravity all zero");
|
||||
TEST_ASSERT_FEQ(e, actor.ex, 1.0f, "zero gravity_x changed ex (%f)", actor.ex);
|
||||
TEST_ASSERT_FEQ(e, actor.ey, 2.0f, "zero gravity_y changed ey (%f)", actor.ey);
|
||||
TEST_ASSERT_FEQ(e, actor.ez, 3.0f, "zero gravity_z changed ez (%f)", actor.ez);
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_arcade_gravity(NULL, &actor, 1.0f),
|
||||
"arcade gravity with NULL self");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_arcade_gravity(&backend, NULL, 1.0f),
|
||||
"arcade gravity with NULL actor");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_arcade_move(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_Actor actor;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
memset(&actor, 0x00, sizeof(akgl_Actor));
|
||||
|
||||
actor.x = 100.0f; actor.y = 200.0f; actor.z = 300.0f;
|
||||
actor.vx = 10.0f; actor.vy = -20.0f; actor.vz = 30.0f;
|
||||
TEST_EXPECT_OK(e, akgl_physics_arcade_move(&backend, &actor, 0.5f), "arcade move");
|
||||
TEST_ASSERT_FEQ(e, actor.x, 105.0f, "x moved to %f, expected 105", actor.x);
|
||||
TEST_ASSERT_FEQ(e, actor.y, 190.0f, "y moved to %f, expected 190", actor.y);
|
||||
TEST_ASSERT_FEQ(e, actor.z, 315.0f, "z moved to %f, expected 315", actor.z);
|
||||
|
||||
// dt of zero is a no-op regardless of velocity.
|
||||
TEST_EXPECT_OK(e, akgl_physics_arcade_move(&backend, &actor, 0.0f), "arcade move dt 0");
|
||||
TEST_ASSERT_FEQ(e, actor.x, 105.0f, "dt 0 moved x to %f", actor.x);
|
||||
TEST_ASSERT_FEQ(e, actor.y, 190.0f, "dt 0 moved y to %f", actor.y);
|
||||
TEST_ASSERT_FEQ(e, actor.z, 315.0f, "dt 0 moved z to %f", actor.z);
|
||||
|
||||
// Zero velocity is a no-op regardless of dt.
|
||||
actor.vx = 0; actor.vy = 0; actor.vz = 0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_arcade_move(&backend, &actor, 10.0f), "arcade move zero velocity");
|
||||
TEST_ASSERT_FEQ(e, actor.x, 105.0f, "zero velocity moved x to %f", actor.x);
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_arcade_move(NULL, &actor, 1.0f),
|
||||
"arcade move with NULL self");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_arcade_move(&backend, NULL, 1.0f),
|
||||
"arcade move with NULL actor");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_arcade_collide_unimplemented(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_Actor a1;
|
||||
akgl_Actor a2;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
memset(&a1, 0x00, sizeof(akgl_Actor));
|
||||
memset(&a2, 0x00, sizeof(akgl_Actor));
|
||||
|
||||
// Pins the stub so that implementing collision has to update this test.
|
||||
TEST_EXPECT_STATUS(e, AKERR_API, akgl_physics_arcade_collide(&backend, &a1, &a2),
|
||||
"arcade collide is still a stub");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_arcade_collide(NULL, &a1, &a2),
|
||||
"arcade collide with NULL self");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_factory(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_String typestr;
|
||||
|
||||
ATTEMPT {
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
|
||||
CATCH(e, akgl_string_initialize(&typestr, "null"));
|
||||
TEST_EXPECT_OK(e, akgl_physics_factory(&backend, &typestr), "factory(\"null\")");
|
||||
TEST_ASSERT(e, backend.move == &akgl_physics_null_move,
|
||||
"factory(\"null\") did not install the null backend");
|
||||
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
CATCH(e, akgl_string_initialize(&typestr, "arcade"));
|
||||
TEST_EXPECT_OK(e, akgl_physics_factory(&backend, &typestr), "factory(\"arcade\")");
|
||||
TEST_ASSERT(e, backend.move == &akgl_physics_arcade_move,
|
||||
"factory(\"arcade\") did not install the arcade backend");
|
||||
|
||||
CATCH(e, akgl_string_initialize(&typestr, "newtonian"));
|
||||
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_physics_factory(&backend, &typestr),
|
||||
"factory with an unknown backend name");
|
||||
|
||||
// An empty name must not match either prefix.
|
||||
CATCH(e, akgl_string_initialize(&typestr, ""));
|
||||
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_physics_factory(&backend, &typestr),
|
||||
"factory with an empty backend name");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_factory(NULL, &typestr),
|
||||
"factory with NULL self");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_factory(&backend, NULL),
|
||||
"factory with NULL type");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_init_arcade_properties(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_registry_init_properties());
|
||||
|
||||
// With nothing configured, every environmental constant defaults to zero.
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
TEST_EXPECT_OK(e, akgl_physics_init_arcade(&backend), "init_arcade with no properties set");
|
||||
TEST_ASSERT_FEQ(e, backend.gravity_y, 0.0f,
|
||||
"unset physics.gravity.y defaulted to %f, expected 0", backend.gravity_y);
|
||||
TEST_ASSERT_FEQ(e, backend.drag_x, 0.0f,
|
||||
"unset physics.drag.x defaulted to %f, expected 0", backend.drag_x);
|
||||
TEST_ASSERT(e, backend.gravity == &akgl_physics_arcade_gravity,
|
||||
"init_arcade did not install the arcade gravity function");
|
||||
TEST_ASSERT(e, backend.simulate == &akgl_physics_simulate,
|
||||
"init_arcade did not install the shared simulate function");
|
||||
|
||||
// Configured values are read out of the property registry.
|
||||
CATCH(e, akgl_set_property("physics.gravity.x", "1.5"));
|
||||
CATCH(e, akgl_set_property("physics.gravity.y", "9.8"));
|
||||
CATCH(e, akgl_set_property("physics.gravity.z", "2.25"));
|
||||
CATCH(e, akgl_set_property("physics.drag.x", "0.5"));
|
||||
CATCH(e, akgl_set_property("physics.drag.y", "0.25"));
|
||||
CATCH(e, akgl_set_property("physics.drag.z", "0.125"));
|
||||
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
TEST_EXPECT_OK(e, akgl_physics_init_arcade(&backend), "init_arcade with properties set");
|
||||
TEST_ASSERT_FEQ(e, backend.gravity_x, 1.5f, "physics.gravity.x read as %f", backend.gravity_x);
|
||||
TEST_ASSERT_FEQ(e, backend.gravity_y, 9.8f, "physics.gravity.y read as %f", backend.gravity_y);
|
||||
TEST_ASSERT_FEQ(e, backend.gravity_z, 2.25f, "physics.gravity.z read as %f", backend.gravity_z);
|
||||
TEST_ASSERT_FEQ(e, backend.drag_x, 0.5f, "physics.drag.x read as %f", backend.drag_x);
|
||||
TEST_ASSERT_FEQ(e, backend.drag_y, 0.25f, "physics.drag.y read as %f", backend.drag_y);
|
||||
TEST_ASSERT_FEQ(e, backend.drag_z, 0.125f, "physics.drag.z read as %f", backend.drag_z);
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_init_arcade(NULL),
|
||||
"init_arcade with NULL self");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_simulate_skips_inactive(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_Actor *actor = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_sim_heap());
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
CATCH(e, akgl_physics_init_null(&backend));
|
||||
|
||||
// A released actor (refcount 0) is skipped entirely.
|
||||
CATCH(e, make_sim_actor(&actor, &basechar, "inactive"));
|
||||
actor->refcount = 0;
|
||||
stub_movement_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with a released actor");
|
||||
TEST_ASSERT(e, stub_movement_calls == 0,
|
||||
"simulate ran movement logic for an actor with refcount 0 (%d calls)",
|
||||
stub_movement_calls);
|
||||
|
||||
// An actor with no base character is skipped.
|
||||
CATCH(e, reset_sim_heap());
|
||||
CATCH(e, make_sim_actor(&actor, &basechar, "nochar"));
|
||||
actor->basechar = NULL;
|
||||
stub_movement_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with a character-less actor");
|
||||
TEST_ASSERT(e, stub_movement_calls == 0,
|
||||
"simulate ran movement logic for an actor with no base character (%d calls)",
|
||||
stub_movement_calls);
|
||||
|
||||
// A fully wired actor is processed.
|
||||
CATCH(e, reset_sim_heap());
|
||||
CATCH(e, make_sim_actor(&actor, &basechar, "active"));
|
||||
stub_movement_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with an active actor");
|
||||
TEST_ASSERT(e, stub_movement_calls == 1,
|
||||
"simulate ran movement logic %d times for one active actor, expected 1",
|
||||
stub_movement_calls);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_simulate_children_follow_parents(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_Actor *parent = NULL;
|
||||
akgl_Actor *child = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_Character *childchar = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_sim_heap());
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
CATCH(e, akgl_physics_init_null(&backend));
|
||||
|
||||
CATCH(e, make_sim_actor(&parent, &basechar, "parent"));
|
||||
CATCH(e, make_sim_actor(&child, &childchar, "child"));
|
||||
|
||||
parent->x = 100.0f; parent->y = 200.0f; parent->z = 300.0f;
|
||||
// For a child, vx/vy/vz are read as a fixed offset from the parent.
|
||||
child->vx = 5.0f; child->vy = -5.0f; child->vz = 2.0f;
|
||||
child->x = -999.0f; child->y = -999.0f; child->z = -999.0f;
|
||||
CATCH(e, akgl_actor_add_child(parent, child));
|
||||
|
||||
stub_movement_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with a parented child");
|
||||
|
||||
TEST_ASSERT_FEQ(e, child->x, 105.0f, "child x resolved to %f, expected parent 100 + offset 5", child->x);
|
||||
TEST_ASSERT_FEQ(e, child->y, 195.0f, "child y resolved to %f, expected parent 200 - offset 5", child->y);
|
||||
TEST_ASSERT_FEQ(e, child->z, 302.0f, "child z resolved to %f, expected parent 300 + offset 2", child->z);
|
||||
|
||||
// Only the parent goes through the movement logic; the child is positional only.
|
||||
TEST_ASSERT(e, stub_movement_calls == 1,
|
||||
"simulate ran movement logic %d times, expected 1 (parent only)",
|
||||
stub_movement_calls);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_simulate_layer_mask(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_Actor *layer0 = NULL;
|
||||
akgl_Actor *layer3 = NULL;
|
||||
akgl_Character *char0 = NULL;
|
||||
akgl_Character *char3 = NULL;
|
||||
akgl_Iterator opflags;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_sim_heap());
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
CATCH(e, akgl_physics_init_null(&backend));
|
||||
|
||||
CATCH(e, make_sim_actor(&layer0, &char0, "onlayer0"));
|
||||
CATCH(e, make_sim_actor(&layer3, &char3, "onlayer3"));
|
||||
layer0->layer = 0;
|
||||
layer3->layer = 3;
|
||||
|
||||
// Masking to layer 3 processes only the layer 3 actor.
|
||||
opflags.flags = AKGL_ITERATOR_OP_LAYERMASK;
|
||||
opflags.layerid = 3;
|
||||
stub_movement_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, &opflags), "simulate masked to layer 3");
|
||||
TEST_ASSERT(e, stub_movement_calls == 1,
|
||||
"layer mask 3 processed %d actors, expected 1", stub_movement_calls);
|
||||
|
||||
// Masking to a layer with no actors processes nothing.
|
||||
opflags.layerid = 7;
|
||||
stub_movement_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, &opflags), "simulate masked to an empty layer");
|
||||
TEST_ASSERT(e, stub_movement_calls == 0,
|
||||
"empty layer mask processed %d actors, expected 0", stub_movement_calls);
|
||||
|
||||
// Without the mask flag, layerid is ignored and both actors are processed.
|
||||
opflags.flags = 0;
|
||||
opflags.layerid = 7;
|
||||
stub_movement_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, &opflags), "simulate with the layer mask off");
|
||||
TEST_ASSERT(e, stub_movement_calls == 2,
|
||||
"unmasked simulate processed %d actors, expected 2", stub_movement_calls);
|
||||
|
||||
// A NULL iterator selects the documented defaults, which mask nothing.
|
||||
stub_movement_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with NULL opflags");
|
||||
TEST_ASSERT(e, stub_movement_calls == 2,
|
||||
"NULL opflags processed %d actors, expected 2", stub_movement_calls);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_simulate_thrust_and_clamp(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_Actor *actor = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_sim_heap());
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
CATCH(e, akgl_physics_init_null(&backend));
|
||||
CATCH(e, make_sim_actor(&actor, &basechar, "thruster"));
|
||||
|
||||
// Thrust beyond the actor's max speed clamps to +sx, preserving sign.
|
||||
actor->sx = 50.0f; actor->sy = 40.0f; actor->sz = 30.0f;
|
||||
actor->tx = 500.0f; actor->ty = 400.0f; actor->tz = 300.0f;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with positive over-thrust");
|
||||
TEST_ASSERT_FEQ(e, actor->tx, 50.0f, "tx clamped to %f, expected 50", actor->tx);
|
||||
TEST_ASSERT_FEQ(e, actor->ty, 40.0f, "ty clamped to %f, expected 40", actor->ty);
|
||||
TEST_ASSERT_FEQ(e, actor->tz, 30.0f, "tz clamped to %f, expected 30", actor->tz);
|
||||
|
||||
// Negative over-thrust clamps to -sx.
|
||||
actor->tx = -500.0f; actor->ty = -400.0f; actor->tz = -300.0f;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with negative over-thrust");
|
||||
TEST_ASSERT_FEQ(e, actor->tx, -50.0f, "tx clamped to %f, expected -50", actor->tx);
|
||||
TEST_ASSERT_FEQ(e, actor->ty, -40.0f, "ty clamped to %f, expected -40", actor->ty);
|
||||
TEST_ASSERT_FEQ(e, actor->tz, -30.0f, "tz clamped to %f, expected -30", actor->tz);
|
||||
|
||||
// Thrust inside the limit is left alone.
|
||||
actor->tx = 10.0f; actor->ty = -10.0f; actor->tz = 5.0f;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with in-range thrust");
|
||||
TEST_ASSERT_FEQ(e, actor->tx, 10.0f, "in-range tx changed to %f", actor->tx);
|
||||
TEST_ASSERT_FEQ(e, actor->ty, -10.0f, "in-range ty changed to %f", actor->ty);
|
||||
TEST_ASSERT_FEQ(e, actor->tz, 5.0f, "in-range tz changed to %f", actor->tz);
|
||||
|
||||
// Velocity is the sum of environmental force and thrust.
|
||||
actor->ex = 3.0f; actor->ey = 4.0f; actor->ez = 5.0f;
|
||||
actor->tx = 1.0f; actor->ty = 2.0f; actor->tz = 3.0f;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate velocity composition");
|
||||
TEST_ASSERT_FEQ(e, actor->vx, 4.0f, "vx composed to %f, expected ex 3 + tx 1", actor->vx);
|
||||
TEST_ASSERT_FEQ(e, actor->vy, 6.0f, "vy composed to %f, expected ey 4 + ty 2", actor->vy);
|
||||
TEST_ASSERT_FEQ(e, actor->vz, 8.0f, "vz composed to %f, expected ez 5 + tz 3", actor->vz);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_simulate_drag(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_Actor *actor = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_sim_heap());
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
CATCH(e, akgl_physics_init_null(&backend));
|
||||
CATCH(e, make_sim_actor(&actor, &basechar, "dragged"));
|
||||
|
||||
// Zero drag leaves environmental velocity untouched.
|
||||
actor->ex = 100.0f; actor->ey = 100.0f; actor->ez = 100.0f;
|
||||
actor->sx = 1000.0f; actor->sy = 1000.0f; actor->sz = 1000.0f;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with zero drag");
|
||||
TEST_ASSERT_FEQ(e, actor->ex, 100.0f, "zero drag changed ex to %f", actor->ex);
|
||||
TEST_ASSERT_FEQ(e, actor->ey, 100.0f, "zero drag changed ey to %f", actor->ey);
|
||||
TEST_ASSERT_FEQ(e, actor->ez, 100.0f, "zero drag changed ez to %f", actor->ez);
|
||||
|
||||
// Nonzero drag bleeds off environmental velocity in proportion to dt.
|
||||
// The simulator derives dt from the wall clock, so assert the direction
|
||||
// and bounds of the change rather than an exact figure.
|
||||
backend.drag_x = 0.5;
|
||||
backend.drag_y = 0.5;
|
||||
backend.drag_z = 0.5;
|
||||
backend.gravity_time = SDL_GetTicksNS();
|
||||
SDL_Delay(20);
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate with drag applied");
|
||||
TEST_ASSERT(e, actor->ex < 100.0f && actor->ex > 0.0f,
|
||||
"drag left ex at %f, expected a value between 0 and 100", actor->ex);
|
||||
TEST_ASSERT(e, actor->ey < 100.0f && actor->ey > 0.0f,
|
||||
"drag left ey at %f, expected a value between 0 and 100", actor->ey);
|
||||
TEST_ASSERT(e, actor->ez < 100.0f && actor->ez > 0.0f,
|
||||
"drag left ez at %f, expected a value between 0 and 100", actor->ez);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_simulate_movement_states(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_Actor *actor = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_sim_heap());
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
CATCH(e, akgl_physics_init_null(&backend));
|
||||
CATCH(e, make_sim_actor(&actor, &basechar, "mover"));
|
||||
|
||||
actor->sx = 1000.0f; actor->sy = 1000.0f; actor->sz = 1000.0f;
|
||||
actor->ax = 10.0f; actor->ay = 20.0f;
|
||||
|
||||
// An idle actor accumulates no thrust on either axis.
|
||||
actor->state = 0;
|
||||
actor->tx = 0; actor->ty = 0;
|
||||
backend.gravity_time = SDL_GetTicksNS();
|
||||
SDL_Delay(10);
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate an idle actor");
|
||||
TEST_ASSERT_FEQ(e, actor->tx, 0.0f, "idle actor accumulated tx %f", actor->tx);
|
||||
TEST_ASSERT_FEQ(e, actor->ty, 0.0f, "idle actor accumulated ty %f", actor->ty);
|
||||
|
||||
// Moving horizontally accumulates thrust on X only.
|
||||
actor->state = AKGL_ACTOR_STATE_MOVING_LEFT;
|
||||
actor->tx = 0; actor->ty = 0;
|
||||
backend.gravity_time = SDL_GetTicksNS();
|
||||
SDL_Delay(10);
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate a horizontally moving actor");
|
||||
TEST_ASSERT(e, actor->tx > 0.0f, "MOVING_LEFT accumulated tx %f, expected a positive value", actor->tx);
|
||||
TEST_ASSERT_FEQ(e, actor->ty, 0.0f, "MOVING_LEFT leaked thrust into ty (%f)", actor->ty);
|
||||
|
||||
// Moving vertically accumulates thrust on Y only.
|
||||
actor->state = AKGL_ACTOR_STATE_MOVING_DOWN;
|
||||
actor->tx = 0; actor->ty = 0;
|
||||
backend.gravity_time = SDL_GetTicksNS();
|
||||
SDL_Delay(10);
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate a vertically moving actor");
|
||||
TEST_ASSERT(e, actor->ty > 0.0f, "MOVING_DOWN accumulated ty %f, expected a positive value", actor->ty);
|
||||
TEST_ASSERT_FEQ(e, actor->tx, 0.0f, "MOVING_DOWN leaked thrust into tx (%f)", actor->tx);
|
||||
|
||||
// The right and up states drive the same accumulators.
|
||||
actor->state = (AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_MOVING_UP);
|
||||
actor->tx = 0; actor->ty = 0;
|
||||
backend.gravity_time = SDL_GetTicksNS();
|
||||
SDL_Delay(10);
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL), "simulate a diagonally moving actor");
|
||||
TEST_ASSERT(e, actor->tx > 0.0f, "MOVING_RIGHT accumulated tx %f, expected a positive value", actor->tx);
|
||||
TEST_ASSERT(e, actor->ty > 0.0f, "MOVING_UP accumulated ty %f, expected a positive value", actor->ty);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_simulate_logic_interrupt(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
akgl_Actor *first = NULL;
|
||||
akgl_Actor *second = NULL;
|
||||
akgl_Character *char1 = NULL;
|
||||
akgl_Character *char2 = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_sim_heap());
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
CATCH(e, akgl_physics_init_null(&backend));
|
||||
|
||||
CATCH(e, make_sim_actor(&first, &char1, "interrupter"));
|
||||
CATCH(e, make_sim_actor(&second, &char2, "follower"));
|
||||
first->movementlogicfunc = &stub_movement_interrupt;
|
||||
|
||||
// AKGL_ERR_LOGICINTERRUPT means "skip me this frame", not "abort the frame",
|
||||
// so the second actor must still be reached.
|
||||
stub_movement_calls = 0;
|
||||
TEST_EXPECT_OK(e, akgl_physics_simulate(&backend, NULL),
|
||||
"simulate must swallow AKGL_ERR_LOGICINTERRUPT");
|
||||
TEST_ASSERT(e, stub_movement_calls == 2,
|
||||
"an interrupting actor stopped the loop after %d of 2 actors",
|
||||
stub_movement_calls);
|
||||
|
||||
// An unrelated error is not swallowed.
|
||||
CATCH(e, reset_sim_heap());
|
||||
CATCH(e, make_sim_actor(&first, &char1, "failer"));
|
||||
first->movementlogicfunc = &stub_movement_error;
|
||||
TEST_EXPECT_STATUS(e, AKERR_VALUE, akgl_physics_simulate(&backend, NULL),
|
||||
"simulate must propagate a non-interrupt movement error");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_physics_simulate_nullpointers(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend backend;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, reset_sim_heap());
|
||||
|
||||
// A backend with no move function cannot simulate.
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_simulate(&backend, NULL),
|
||||
"simulate with no move function installed");
|
||||
|
||||
// A NULL backend must be reported, not dereferenced.
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_simulate(NULL, NULL),
|
||||
"simulate with NULL self");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, akgl_registry_init_properties());
|
||||
|
||||
CATCH(errctx, test_physics_init_null());
|
||||
CATCH(errctx, test_physics_null_backend_is_inert());
|
||||
CATCH(errctx, test_physics_arcade_gravity());
|
||||
CATCH(errctx, test_physics_arcade_move());
|
||||
CATCH(errctx, test_physics_arcade_collide_unimplemented());
|
||||
CATCH(errctx, test_physics_factory());
|
||||
CATCH(errctx, test_physics_init_arcade_properties());
|
||||
CATCH(errctx, test_physics_simulate_skips_inactive());
|
||||
CATCH(errctx, test_physics_simulate_children_follow_parents());
|
||||
CATCH(errctx, test_physics_simulate_layer_mask());
|
||||
CATCH(errctx, test_physics_simulate_thrust_and_clamp());
|
||||
CATCH(errctx, test_physics_simulate_drag());
|
||||
CATCH(errctx, test_physics_simulate_movement_states());
|
||||
CATCH(errctx, test_physics_simulate_logic_interrupt());
|
||||
CATCH(errctx, test_physics_simulate_nullpointers());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ akerr_ErrorContext *test_akgl_registry_init(RegistryFuncPtr funcptr)
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
FAIL_RETURN(errctx, AKERR_BEHAVIOR, "SDL memory allocator fails but registry reports successful property creation");
|
||||
FAIL_RETURN(errctx, AKGL_ERR_BEHAVIOR, "SDL memory allocator fails but registry reports successful property creation");
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/util.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/renderer.h>
|
||||
|
||||
SDL_Window *window;
|
||||
SDL_Renderer *renderer;
|
||||
|
||||
akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
|
||||
{
|
||||
@@ -37,7 +37,7 @@ akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
|
||||
"Loaded texture was not the correct size");
|
||||
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||
image = IMG_LoadTexture(renderer, (char *)&tmpstr->data);
|
||||
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
|
||||
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
||||
|
||||
CATCH(
|
||||
@@ -138,7 +138,7 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void)
|
||||
// Is it using the right spritesheet?
|
||||
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||
image = IMG_LoadTexture(renderer, (char *)&tmpstr->data);
|
||||
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
|
||||
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
||||
|
||||
CATCH(
|
||||
@@ -187,6 +187,7 @@ int main(void)
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
renderer = &_akgl_renderer;
|
||||
|
||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||
|
||||
@@ -194,9 +195,10 @@ int main(void)
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer)) {
|
||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer->sdl_renderer)) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||
}
|
||||
renderer->draw_texture = &akgl_render_2d_draw_texture;
|
||||
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init_sprite());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
@@ -111,7 +112,7 @@ akerr_ErrorContext *test_akgl_string_initialize(void)
|
||||
FAIL_NONZERO_BREAK(errctx, strcmp((char *)&ptr->data, "Test value"), AKERR_VALUE, "Expected 'Test value', got %s", (char *)&ptr->data);
|
||||
|
||||
CATCH(errctx, akgl_heap_release_string(NULL));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Failure to properly handle NULL pointer");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Failure to properly handle NULL pointer");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
99
tests/testutil.h
Normal file
99
tests/testutil.h
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* @file testutil.h
|
||||
* @brief Shared assertion helpers and headless bootstrap for the libakgl test suites.
|
||||
*
|
||||
* The akerror ATTEMPT/CATCH/PROCESS/FINISH macros are verbose when what a test
|
||||
* wants to say is "this call must fail with exactly this status". These helpers
|
||||
* wrap that pattern.
|
||||
*
|
||||
* All of the TEST_* assertion macros expand to a `break` on failure, so they
|
||||
* must be used directly inside an ATTEMPT block. Inside a `for` or `while`
|
||||
* nested in an ATTEMPT they would break the inner loop instead; use
|
||||
* TEST_ASSERT_FLAG in that case and check the flag after the loop.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_TESTUTIL_H_
|
||||
#define _AKGL_TESTUTIL_H_
|
||||
|
||||
#include <math.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
/** @brief Fail the enclosing ATTEMPT block unless @p cond holds. */
|
||||
#define TEST_ASSERT(e, cond, ...) \
|
||||
if ( ! (cond) ) { \
|
||||
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__); \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run @p stmt and require that it reports exactly @p expected.
|
||||
*
|
||||
* Releases whatever context @p stmt returns, so a test can assert many failure
|
||||
* paths in a row without draining AKERR_ARRAY_ERROR. Pass 0 for @p expected to
|
||||
* require success.
|
||||
*/
|
||||
#define TEST_EXPECT_STATUS(e, expected, stmt, desc) \
|
||||
{ \
|
||||
akerr_ErrorContext *__tec = (stmt); \
|
||||
int __tst = ( __tec == NULL ) ? 0 : __tec->status; \
|
||||
if ( __tec != NULL ) { \
|
||||
__tec->handled = true; \
|
||||
__tec = akerr_release_error(__tec); \
|
||||
} \
|
||||
if ( __tst != (expected) ) { \
|
||||
FAIL_BREAK( \
|
||||
e, \
|
||||
AKGL_ERR_BEHAVIOR, \
|
||||
"%s: expected status %d (%s), got %d (%s)", \
|
||||
desc, \
|
||||
(int)(expected), \
|
||||
akerr_name_for_status((int)(expected), NULL), \
|
||||
__tst, \
|
||||
akerr_name_for_status(__tst, NULL)); \
|
||||
} \
|
||||
}
|
||||
|
||||
/** @brief Require that @p stmt succeeds, reporting @p desc if it does not. */
|
||||
#define TEST_EXPECT_OK(e, stmt, desc) \
|
||||
TEST_EXPECT_STATUS(e, 0, stmt, desc)
|
||||
|
||||
/**
|
||||
* @brief Require that @p stmt fails, without pinning which status it reports.
|
||||
*
|
||||
* For paths that are delegated to a dependency, where the exact status is that
|
||||
* dependency's business and asserting it would make the test brittle.
|
||||
*/
|
||||
#define TEST_EXPECT_ANY_ERROR(e, stmt, desc) \
|
||||
{ \
|
||||
akerr_ErrorContext *__tec = (stmt); \
|
||||
int __tst = ( __tec == NULL ) ? 0 : __tec->status; \
|
||||
if ( __tec != NULL ) { \
|
||||
__tec->handled = true; \
|
||||
__tec = akerr_release_error(__tec); \
|
||||
} \
|
||||
if ( __tst == 0 ) { \
|
||||
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, "%s: expected a failure, got success", desc); \
|
||||
} \
|
||||
}
|
||||
|
||||
/** @brief Require that two floats agree to within AKGL_TEST_EPSILON. */
|
||||
#define AKGL_TEST_EPSILON 0.0001f
|
||||
|
||||
#define TEST_ASSERT_FEQ(e, actual, expected, ...) \
|
||||
if ( fabsf((float)(actual) - (float)(expected)) > AKGL_TEST_EPSILON ) { \
|
||||
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__); \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Record a failure into a flag instead of breaking.
|
||||
*
|
||||
* For assertions inside a loop nested in an ATTEMPT block, where `break` would
|
||||
* only leave the loop.
|
||||
*/
|
||||
#define TEST_ASSERT_FLAG(flag, cond) \
|
||||
if ( ! (cond) ) { \
|
||||
(flag) = false; \
|
||||
}
|
||||
|
||||
#endif // _AKGL_TESTUTIL_H_
|
||||
163
tests/tilemap.c
163
tests/tilemap.c
@@ -20,13 +20,15 @@ akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void)
|
||||
int propnum;
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
snprintf(
|
||||
(char *)&tmpstr->data,
|
||||
AKGL_MAX_STRING_LENGTH,
|
||||
"%s%s",
|
||||
SDL_GetBasePath(),
|
||||
"assets/snippets/test_tilemap_akgl_get_json_tilemap_property.json"
|
||||
"assets/snippets/test_tilemap_get_json_tilemap_property.json"
|
||||
);
|
||||
jsondoc = json_load_file((char *)&tmpstr->data, 0, (json_error_t *)&jsonerr);
|
||||
FAIL_ZERO_BREAK(errctx, jsondoc, AKERR_NULLPOINTER, "Failure loading json fixture: %s", (char *)jsonerr.text);
|
||||
@@ -85,32 +87,34 @@ akerr_ErrorContext *test_akgl_tilemap_compute_tileset_offsets(void)
|
||||
10 // Tile 2 Y
|
||||
};
|
||||
int *comparison_ptrs[8] = {
|
||||
&gamemap.tilesets[0].tile_offsets[0][0], // Tile 0 X
|
||||
&gamemap.tilesets[0].tile_offsets[0][1], // Tile 0 Y
|
||||
&gamemap.tilesets[0].tile_offsets[1][0], // Tile 1 X
|
||||
&gamemap.tilesets[0].tile_offsets[1][1], // Tile 0 Y
|
||||
&gamemap.tilesets[0].tile_offsets[2][0], // Tile 2 X
|
||||
&gamemap.tilesets[0].tile_offsets[2][1], // Tile 2 Y
|
||||
&gamemap.tilesets[0].tile_offsets[3][0], // Tile 3 X
|
||||
&gamemap.tilesets[0].tile_offsets[3][0], // Tile 3 Y
|
||||
&gamemap->tilesets[0].tile_offsets[0][0], // Tile 0 X
|
||||
&gamemap->tilesets[0].tile_offsets[0][1], // Tile 0 Y
|
||||
&gamemap->tilesets[0].tile_offsets[1][0], // Tile 1 X
|
||||
&gamemap->tilesets[0].tile_offsets[1][1], // Tile 0 Y
|
||||
&gamemap->tilesets[0].tile_offsets[2][0], // Tile 2 X
|
||||
&gamemap->tilesets[0].tile_offsets[2][1], // Tile 2 Y
|
||||
&gamemap->tilesets[0].tile_offsets[3][0], // Tile 3 X
|
||||
&gamemap->tilesets[0].tile_offsets[3][1], // Tile 3 Y
|
||||
};
|
||||
int i = 0;
|
||||
|
||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
gamemap.tilesets[0].tilecount = 4;
|
||||
gamemap.tilesets[0].columns = 2;
|
||||
gamemap.tilesets[0].firstgid = 1;
|
||||
gamemap.tilesets[0].imageheight = 20;
|
||||
gamemap.tilesets[0].imagewidth = 20;
|
||||
gamemap.tilesets[0].tilecount = 4;
|
||||
gamemap.tilesets[0].tileheight = 10;
|
||||
gamemap.tilesets[0].tilewidth = 10;
|
||||
gamemap.tilesets[0].spacing = 0;
|
||||
gamemap.tilesets[0].margin = 0;
|
||||
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
gamemap->tilesets[0].tilecount = 4;
|
||||
gamemap->tilesets[0].columns = 2;
|
||||
gamemap->tilesets[0].firstgid = 1;
|
||||
gamemap->tilesets[0].imageheight = 20;
|
||||
gamemap->tilesets[0].imagewidth = 20;
|
||||
gamemap->tilesets[0].tilecount = 4;
|
||||
gamemap->tilesets[0].tileheight = 10;
|
||||
gamemap->tilesets[0].tilewidth = 10;
|
||||
gamemap->tilesets[0].spacing = 0;
|
||||
gamemap->tilesets[0].margin = 0;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(&gamemap, 0));
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(gamemap, 0));
|
||||
// Tile 0 X offset
|
||||
for ( i = 0; i < 8; i++ ) {
|
||||
FAIL_NONZERO_BREAK(
|
||||
@@ -139,15 +143,18 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_objects(void)
|
||||
json_error_t errdata;
|
||||
akgl_Actor *testactor;
|
||||
|
||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
||||
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
|
||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
|
||||
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
|
||||
CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
|
||||
CATCH(errctx, akgl_get_json_array_index_object(layers, 1, &objectlayer));
|
||||
CATCH(errctx, akgl_tilemap_load_layer_objects(&gamemap, objectlayer, 1));
|
||||
CATCH(errctx, akgl_tilemap_load_layer_objects(gamemap, objectlayer, 1, pathstr));
|
||||
|
||||
testactor = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
@@ -186,21 +193,24 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
|
||||
json_t *tiledata = NULL;
|
||||
json_error_t errdata;
|
||||
|
||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
||||
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
|
||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
|
||||
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
|
||||
CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
|
||||
CATCH(errctx, akgl_get_json_array_index_object(layers, 0, &tilelayer));
|
||||
CATCH(errctx, akgl_get_json_array_value(tilelayer, "data", &tiledata));
|
||||
CATCH(errctx, akgl_tilemap_load_layer_tile(&gamemap, tilelayer, 0));
|
||||
if ( (gamemap.layers[0].data[0] != 1) ||
|
||||
(gamemap.layers[0].data[1] != 2) ||
|
||||
(gamemap.layers[0].data[2] != 3) ||
|
||||
(gamemap.layers[0].data[3] != 4) ) {
|
||||
CATCH(errctx, akgl_tilemap_load_layer_tile(gamemap, tilelayer, 0, pathstr));
|
||||
if ( (gamemap->layers[0].data[0] != 1) ||
|
||||
(gamemap->layers[0].data[1] != 2) ||
|
||||
(gamemap->layers[0].data[2] != 3) ||
|
||||
(gamemap->layers[0].data[3] != 4) ) {
|
||||
FAIL_BREAK(errctx, AKERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)");
|
||||
}
|
||||
} CLEANUP {
|
||||
@@ -223,58 +233,61 @@ akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
|
||||
json_error_t errdata;
|
||||
int i = 0;
|
||||
|
||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
||||
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
|
||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
|
||||
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
|
||||
CATCH(errctx, akgl_tilemap_load_layers(&gamemap, doc));
|
||||
CATCH(errctx, akgl_tilemap_load_layers(gamemap, doc, pathstr));
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
(gamemap.numlayers != 3),
|
||||
(gamemap->numlayers != 3),
|
||||
AKERR_VALUE,
|
||||
"Map layer count incorrect"
|
||||
);
|
||||
for ( i = 0; i < gamemap.numlayers; i++ ) {
|
||||
if ( (gamemap.layers[i].opacity != 1) ||
|
||||
(gamemap.layers[i].visible != true) ||
|
||||
(gamemap.layers[i].id != (i + 1)) ||
|
||||
(gamemap.layers[i].x != 0) ||
|
||||
(gamemap.layers[i].y != 0) ) {
|
||||
for ( i = 0; i < gamemap->numlayers; i++ ) {
|
||||
if ( (gamemap->layers[i].opacity != 1) ||
|
||||
(gamemap->layers[i].visible != true) ||
|
||||
(gamemap->layers[i].id != (i + 1)) ||
|
||||
(gamemap->layers[i].x != 0) ||
|
||||
(gamemap->layers[i].y != 0) ) {
|
||||
FAIL(errctx, AKERR_VALUE, "Map layer data loaded incorrectly (see gdb)");
|
||||
goto _test_akgl_tilemap_load_layers_cleanup;
|
||||
}
|
||||
}
|
||||
// Layer 2 should have 1 object loaded
|
||||
if ( (gamemap.layers[1].objects[0].actorptr != SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL)) ||
|
||||
(gamemap.layers[1].objects[1].name[0] != '\0' ) ||
|
||||
(gamemap.layers[1].objects[1].id != 0) ) {
|
||||
if ( (gamemap->layers[1].objects[0].actorptr != SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL)) ||
|
||||
(gamemap->layers[1].objects[1].name[0] != '\0' ) ||
|
||||
(gamemap->layers[1].objects[1].id != 0) ) {
|
||||
FAIL_BREAK(errctx, AKERR_VALUE, "Map layer 2 should have 1 loaded object (testactor) and nothing else (see gdb)");
|
||||
}
|
||||
// Layer 1 and 3 should have no objects
|
||||
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
|
||||
if ( gamemap.layers[0].objects[i].id != 0 ) {
|
||||
if ( gamemap->layers[0].objects[i].id != 0 ) {
|
||||
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
|
||||
goto _test_akgl_tilemap_load_layers_cleanup;
|
||||
}
|
||||
}
|
||||
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
|
||||
if ( gamemap.layers[2].objects[i].id != 0 ) {
|
||||
if ( gamemap->layers[2].objects[i].id != 0 ) {
|
||||
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
|
||||
goto _test_akgl_tilemap_load_layers_cleanup;
|
||||
}
|
||||
}
|
||||
// Layers 1 and 3 should have tile data
|
||||
if ( (gamemap.layers[0].data[0] != 1) ||
|
||||
(gamemap.layers[0].data[1] != 2) ||
|
||||
(gamemap.layers[0].data[2] != 3) ||
|
||||
(gamemap.layers[0].data[3] != 4) ||
|
||||
(gamemap.layers[2].data[0] != 0) ||
|
||||
(gamemap.layers[2].data[1] != 5) ||
|
||||
(gamemap.layers[2].data[2] != 0) ||
|
||||
(gamemap.layers[2].data[3] != 6)
|
||||
if ( (gamemap->layers[0].data[0] != 1) ||
|
||||
(gamemap->layers[0].data[1] != 2) ||
|
||||
(gamemap->layers[0].data[2] != 3) ||
|
||||
(gamemap->layers[0].data[3] != 4) ||
|
||||
(gamemap->layers[2].data[0] != 0) ||
|
||||
(gamemap->layers[2].data[1] != 5) ||
|
||||
(gamemap->layers[2].data[2] != 0) ||
|
||||
(gamemap->layers[2].data[3] != 6)
|
||||
) {
|
||||
FAIL_BREAK(errctx, AKERR_VALUE, "Map layers 1 and 3 should have tile data but it is incorrect");
|
||||
}
|
||||
@@ -299,40 +312,43 @@ akerr_ErrorContext *test_akgl_tilemap_load_tilesets(void)
|
||||
json_error_t errdata;
|
||||
SDL_Texture *image = NULL;
|
||||
|
||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
CATCH(errctx, akgl_heap_next_string(&pathstr));
|
||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
|
||||
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
|
||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
|
||||
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
|
||||
CATCH(errctx, akgl_tilemap_load_tilesets(&gamemap, doc));
|
||||
FAIL_NONZERO_BREAK(errctx, (gamemap.numtilesets != 1), AKERR_VALUE, "Incorrect number of tilesets loaded for map");
|
||||
if ( (gamemap.tilesets[0].columns != 48 ) ||
|
||||
(gamemap.tilesets[0].firstgid != 1) ||
|
||||
(gamemap.tilesets[0].imageheight != 576) ||
|
||||
(gamemap.tilesets[0].imagewidth != 768) ||
|
||||
(gamemap.tilesets[0].margin != 0) ||
|
||||
(gamemap.tilesets[0].spacing != 0) ||
|
||||
(gamemap.tilesets[0].tilecount != 1728) ||
|
||||
(gamemap.tilesets[0].tileheight != 16) ||
|
||||
(gamemap.tilesets[0].tilewidth != 16) ) {
|
||||
CATCH(errctx, akgl_tilemap_load_tilesets(gamemap, doc, pathstr));
|
||||
FAIL_NONZERO_BREAK(errctx, (gamemap->numtilesets != 1), AKERR_VALUE, "Incorrect number of tilesets loaded for map");
|
||||
if ( (gamemap->tilesets[0].columns != 48 ) ||
|
||||
(gamemap->tilesets[0].firstgid != 1) ||
|
||||
(gamemap->tilesets[0].imageheight != 576) ||
|
||||
(gamemap->tilesets[0].imagewidth != 768) ||
|
||||
(gamemap->tilesets[0].margin != 0) ||
|
||||
(gamemap->tilesets[0].spacing != 0) ||
|
||||
(gamemap->tilesets[0].tilecount != 1728) ||
|
||||
(gamemap->tilesets[0].tileheight != 16) ||
|
||||
(gamemap->tilesets[0].tilewidth != 16) ) {
|
||||
FAIL_BREAK(errctx, AKERR_VALUE, "Tileset loaded with incorrect values");
|
||||
}
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
strcmp((char *)&gamemap.tilesets[0].name, "World_A1"),
|
||||
strcmp((char *)&gamemap->tilesets[0].name, "World_A1"),
|
||||
AKERR_VALUE,
|
||||
"Tileset loaded with incorrect name");
|
||||
|
||||
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/World_A1.png");
|
||||
image = IMG_LoadTexture(renderer, (char *)&pathstr->data);
|
||||
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&pathstr->data);
|
||||
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
||||
|
||||
CATCH(
|
||||
errctx,
|
||||
akgl_render_and_compare(
|
||||
gamemap.tilesets[0].texture,
|
||||
gamemap->tilesets[0].texture,
|
||||
image,
|
||||
0, 0, 768, 576,
|
||||
"test_akgl_tilemap_loaded_tileset.png")
|
||||
@@ -354,10 +370,12 @@ akerr_ErrorContext *test_akgl_tilemap_load(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
memset((void *)&gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_tilemap_load("assets/testmap.tmj", &gamemap));
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
CATCH(errctx, akgl_tilemap_load("assets/testmap.tmj", gamemap));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
@@ -369,6 +387,8 @@ akerr_ErrorContext *test_akgl_tilemap_draw(void)
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
@@ -381,6 +401,8 @@ akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void)
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
@@ -392,15 +414,18 @@ int main(void)
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 768, 576, 0, &window, &renderer)) {
|
||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 768, 576, 0, &window, &renderer->sdl_renderer)) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||
}
|
||||
renderer->draw_texture = &akgl_render_2d_draw_texture;
|
||||
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
|
||||
47
tests/util.c
47
tests/util.c
@@ -1,5 +1,6 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/util.h>
|
||||
|
||||
akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
||||
@@ -10,7 +11,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_rectangle_points(NULL, NULL));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with all NULL pointers");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with all NULL pointers");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
@@ -19,7 +20,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_rectangle_points(NULL, &testrect));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL SDL_FRect pointer");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL SDL_FRect pointer");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
@@ -28,7 +29,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_rectangle_points(&points, NULL));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL RectanglePoints pointer");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL RectanglePoints pointer");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
@@ -63,7 +64,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_math(void)
|
||||
points.bottomright.y != 32 ) {
|
||||
FAIL_BREAK(
|
||||
errctx,
|
||||
AKERR_BEHAVIOR,
|
||||
AKGL_ERR_BEHAVIOR,
|
||||
"akgl_rectangle_points incorrectly calculated points for {x=0, y=0, w=32, h=32} to {topleft={%d, %d}, topright={%d, %d}, bottomleft={%d, %d}, bottomright={%d, %d}}",
|
||||
points.topleft.x, points.topleft.y,
|
||||
points.topright.x, points.topright.y,
|
||||
@@ -87,7 +88,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, NULL));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(*, *, NULL) failed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, *, NULL) failed");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
@@ -96,7 +97,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, NULL, &testcollide));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(*, NULL, *) failed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, NULL, *) failed");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
@@ -105,7 +106,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_collide_point_rectangle(NULL, &testrectpoints, &testcollide));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, *, *) failed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, *, *) failed");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
@@ -114,7 +115,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_collide_point_rectangle(NULL, NULL, NULL));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, NULL, NULL) failed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, NULL, NULL) failed");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
@@ -142,14 +143,14 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void)
|
||||
CATCH(errctx, akgl_rectangle_points(&testrectpoints, &testrect));
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
|
||||
if ( testcollide == false ) {
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||
}
|
||||
|
||||
testpoint.x = 48;
|
||||
testpoint.y = 48;
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
|
||||
if ( testcollide == true ) {
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Invalid collision reported");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
@@ -167,7 +168,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, NULL));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(*, *, NULL) failed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, *, NULL) failed");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
@@ -176,7 +177,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, NULL, &testcollide));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(*, NULL, *) failed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, NULL, *) failed");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
@@ -185,7 +186,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_collide_rectangles(NULL, &testrect2, &testcollide));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(NULL, *, *) failed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, *, *) failed");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
@@ -194,7 +195,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_collide_rectangles(NULL, NULL, NULL));
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "akgl_collide_rectangles(NULL, NULL, NULL) failed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, NULL, NULL) failed");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
@@ -222,14 +223,14 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
// Collision overlapping on the top left
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||
if ( testcollide == false ) {
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||
}
|
||||
|
||||
// Collision overlapping on the top right
|
||||
testrect1.x = 64;
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||
if ( testcollide == false ) {
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||
}
|
||||
|
||||
// Collision overlapping on the bottom left
|
||||
@@ -237,7 +238,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
testrect1.y = 32;
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||
if ( testcollide == false ) {
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||
}
|
||||
|
||||
// Collision overlapping on the bottom right
|
||||
@@ -245,7 +246,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
testrect1.y = 32;
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||
if ( testcollide == false ) {
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||
}
|
||||
|
||||
// Collision overlapping the top edge
|
||||
@@ -255,7 +256,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
testrect1.h = 32;
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||
if ( testcollide == false ) {
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||
}
|
||||
|
||||
// Collision overlapping the left edge
|
||||
@@ -265,7 +266,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
testrect1.h = 80;
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||
if ( testcollide == false ) {
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||
}
|
||||
|
||||
// Collision overlapping the right edge
|
||||
@@ -275,7 +276,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
testrect1.h = 80;
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||
if ( testcollide == false ) {
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||
}
|
||||
|
||||
// Collision overlapping the bottom edge
|
||||
@@ -285,7 +286,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
testrect1.h = 32;
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||
if ( testcollide == false ) {
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Valid collision missed");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
|
||||
}
|
||||
|
||||
// Not colliding
|
||||
@@ -295,7 +296,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
testrect1.h = 16;
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||
if ( testcollide == true ) {
|
||||
FAIL_BREAK(errctx, AKERR_BEHAVIOR, "Invalid collision reported");
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
|
||||
}
|
||||
|
||||
} CLEANUP {
|
||||
|
||||
@@ -48,12 +48,11 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
||||
strcpy((char *)&game.name, "charviewer");
|
||||
strcpy((char *)&game.version, "0.0.1");
|
||||
strcpy((char *)&game.uri, "net.aklabs.libakgl.charviewer");
|
||||
game.screenwidth = 640;
|
||||
game.screenheight = 480;
|
||||
|
||||
CATCH(errctx, akgl_game_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, akgl_set_property("game.screenwidth", "640"));
|
||||
CATCH(errctx, akgl_set_property("game.screenheight", "480"));
|
||||
CATCH(errctx, akgl_render_init2d(renderer));
|
||||
CATCH(errctx, akgl_physics_init_null(physics));
|
||||
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
@@ -94,7 +93,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
||||
AKGL_REGISTRY_CHARACTER,
|
||||
"little guy",
|
||||
NULL);
|
||||
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKERR_REGISTRY, "Can't load character 'little guy' from the registry");
|
||||
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKGL_ERR_REGISTRY, "Can't load character 'little guy' from the registry");
|
||||
actorptr->movement_controls_face = false;
|
||||
actorptr->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN);
|
||||
actorptr->x = 320;
|
||||
@@ -145,9 +144,9 @@ SDL_AppResult SDL_AppIterate(void *appstate)
|
||||
AKGL_BITMASK_CLEAR(opflags.flags);
|
||||
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_UPDATE);
|
||||
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_RENDER);
|
||||
akgl_draw_background(game.screenwidth, game.screenheight);
|
||||
akgl_draw_background((int)camera->w, (int)camera->h);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_tilemap_draw(renderer, (akgl_Tilemap *)&gamemap, &camera, i));
|
||||
CATCH(errctx, akgl_tilemap_draw(gamemap, camera, i));
|
||||
SDL_EnumerateProperties(AKGL_REGISTRY_ACTOR, &akgl_registry_iterate_actor, (void *)&opflags);
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
@@ -155,7 +154,7 @@ SDL_AppResult SDL_AppIterate(void *appstate)
|
||||
LOG_ERROR(errctx);
|
||||
return SDL_APP_FAILURE;
|
||||
} FINISH_NORETURN(errctx);
|
||||
SDL_RenderPresent(renderer);
|
||||
SDL_RenderPresent(renderer->sdl_renderer);
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user