59 Commits

Author SHA1 Message Date
bc782fbffe Add controller test suite and reach 72 percent line coverage
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 20s
libakgl CI Build / mutation_test (push) Failing after 17s
Cover control map capacity, the default binding set, keyboard and gamepad
dispatch including cross-device rejection, the dpad handlers, and device
add and remove against the dummy SDL drivers. Synthesize SDL events directly
rather than pumping the event queue, so the suite needs no real hardware.

Fix the three-way appstate check in the four gamepad handlers, which tested
appstate in place of both the event pointer and the looked-up player actor,
so a null event or a missing player was dereferenced instead of reported.

Record the new coverage status in TODO.md along with the seven defects the
suites uncovered and fixed, eleven that remain open, and the reason the
build tree has to precede LD_LIBRARY_PATH for the CTest run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:08:38 -04:00
22162db2da Add physics, heap, json_helpers, game, and actor test suites
Raise line coverage from 39.6 to 61.8 percent with four new suites and an
extension to the actor suite, and register every suite through a single
CMake list so a new test file cannot be left out of the coverage fixture.
Give the test targets a build-tree RPATH and prepend the build tree to
LD_LIBRARY_PATH for CTest, so a developer with a previously installed
libakgl.so exercises the library that was just compiled.

Fix six defects the new tests exposed:

- akgl_physics_simulate read self->gravity_time before its NULL check, so a
  NULL backend crashed instead of reporting AKERR_NULLPOINTER.
- akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose
  inside the PROCESS switch. An ordinary save never flushed or closed its
  stream and produced an empty file.
- akgl_game_save_actors wrote each name table terminator from the address of
  a single char, emitting stack contents into the save file and a sentinel
  the loader could not recognize.
- akgl_game_load_objectnamemap used CATCH directly inside while(1), where the
  break leaves the loop rather than propagating, so a truncated name table
  loaded as a successful game.
- akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no
  NULL check, unlike their left and right counterparts.
- akgl_actor_logic_movement checked actor twice instead of checking
  actor->basechar before dereferencing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
6f6bd2d563 Plan a unit test suite from the coverage baseline
Record the 39.6 percent line and 44.3 percent function coverage baseline
measured with the AKGL_COVERAGE build, and plan twelve test suites to raise
it to roughly 70 percent lines and 80 percent functions. Tier the work by
what each suite needs: pure logic suites for physics, json_helpers, heap,
and savegame handling that require no renderer; renderer suites gated behind
a shared offscreen harness; and controller and frame-loop coverage deferred.

Note that vendored dependency shared objects are absent from the loader path
in a fresh out-of-tree build, which reports every test as zero coverage, and
record ten suspected defects found while reading the uncovered code so the
new tests assert correct behavior rather than pinning the current one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 01:40:06 -04:00
ff88f48fa2 Add CTest coverage reporting
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 19s
libakgl CI Build / mutation_test (push) Failing after 16s
Instrument libakgl in opt-in coverage builds, use CTest fixtures to reset counters and emit Cobertura XML and detailed HTML reports, and upload those reports from Gitea CI. Document the local coverage workflow for contributors.

Co-authored-by: Codex (GPT-5) <noreply@openai.com>
2026-07-30 01:25:26 -04:00
549b27d3eb Document the libakgl API with Doxygen
Add file, structure, global, and function documentation across all libakgl-owned headers and sources, including parameter contracts and likely AKERR/AKGL_ERR exceptions. Add a strict Doxyfile and build the generated API documentation in Gitea CI with warnings treated as failures.

Co-authored-by: Codex (GPT-5) <noreply@openai.com>
2026-07-30 01:10:31 -04:00
a2995e81df Add Gitea test and mutation pipeline
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 18s
libakgl CI Build / mutation_test (push) Failing after 16s
Build libakgl with recursive vendored dependencies, run the passing CTest suite with JUnit reporting, and publish a bounded static-string mutation gate with a 50 percent score threshold.

Co-authored-by: Codex (GPT-5) <noreply@openai.com>
2026-07-29 18:46:26 -04:00
dc1bd0a798 Add mutation testing harness
Port the scratch-copy mutation runner used by libakerror and libakstdlib, covering libakgl source files with sampling, thresholds, JUnit reports, and test timeouts. Add a CMake target and documentation, preserve the intentionally failing character test exclusion, and migrate the sprite test to the renderer backend so mutation baselines are green.

Co-authored-by: Codex (GPT-5) <noreply@openai.com>
2026-07-29 18:42:09 -04:00
3854b33750 Update tilemap tests for renderer backends
Migrate tilemap tests to pointer-based game globals and current loader signatures, initialize the renderer callback used by image comparisons, and refresh the character fixture schema. Preserve loaded tileset names by removing a duplicate heap allocation.

Co-authored-by: Codex (GPT-5) <noreply@openai.com>
2026-07-29 18:25:36 -04:00
cf9ebb206f Repair embedded dependency build and test setup
Isolate vendored test registration, namespace project error codes, and update dependency revisions. Fix mutable sprite path handling, out-of-tree fixtures, and charviewer initialization while documenting the outstanding character-to-sprite refcount bug.

Co-authored-by: Codex (GPT-5) <noreply@openai.com>
2026-07-29 18:17:08 -04:00
74867ea82e README 2026-06-20 13:19:58 -04:00
4e510dd6d6 README updates 2026-06-19 10:47:07 -04:00
dca03cb50d Start README docs 2026-06-19 09:52:08 -04:00
652ee4cdf3 Tilemaps can now load their own physics engines from map properties. The gravity for the arcade simulation is acting a little funny when populated from the map properties. 2026-06-02 17:11:16 -04:00
9fed59c4c8 Abstract the global renderer, physics, camera and gamemap objects behind a pointer, so you can swap in and out different ones as needed 2026-06-02 13:15:26 -04:00
941eeb2493 Got the physics system applying gravity and drag correctly. (Mostly? Seems like it? Seems good.) 2026-05-26 16:45:04 -04:00
314ce5e10d Physics simulation engine implemented, basic cases (null and 2d SideScroller) works 2026-05-26 10:36:31 -04:00
d87c5d2c20 More rendering subsystem breakout, added a physics subsystem, everything now fires from akgl_game_update() for the user 2026-05-25 21:29:18 -04:00
6314ad7f26 Start abstracting the rendering backend away to make it easier to switch to GPU rendering later 2026-05-24 21:59:29 -04:00
8f613397d6 Loading sprites and tiles both work correctly with relative paths, now using akgl_path_relative* helpers 2026-05-24 19:57:57 -04:00
980bbc56fb Fixed loading of tilesets and images from tilemaps to correctly use relative paths. 2026-05-24 10:28:35 -04:00
e3edd5b855 Add a method that automatically opens gamepads so they will send events 2026-05-21 21:43:51 -04:00
f695a035c8 Add a low FPS callback to aid in debugging, and fix a huge memory leak in akgl_text_rendertextat 2026-05-15 11:07:31 -04:00
73b1a4cab0 Make perspective scale work more explicitly from properties 2026-05-13 23:36:49 -04:00
aad196d84e akgl_heap_release* should not cascade into dependent objects (releasing actors should not release characters, sprites, and spritesheets) 2026-05-13 22:45:52 -04:00
9443fa8532 Fix bug in akgl_heap_release_actor preventing it from being cleared out of the registry 2026-05-13 19:28:38 -04:00
dc2e88b72f Fix a scale bug (default should be 1.0), add tilemap release function 2026-05-13 16:56:24 -04:00
53e4f5c14f Tilemaps can have two objects placed on it, 'p_foreground' and 'p_vanishing' whose placement form a virtual perspective plane, which causes sprites to scale as they move up and down the screen between them, creating the illusion of depth on the screen 2026-05-13 09:57:24 -04:00
b8dee456ca Add keyboard enumeration function. Make image layers work. 2026-05-13 08:02:59 -04:00
36dfd47a06 Fixed a bug in akgl_heap_next_string, made screen width and height load from properties, not on the game object 2026-05-13 04:55:19 -04:00
23dbc7d985 Make mkcontrollers script work when building as a submodule 2026-05-12 21:40:31 -04:00
6f62e674d5 Make mkcontrollers script work when building as a submodule 2026-05-12 21:38:08 -04:00
8ae99120b5 Wrap subdirectory compilation in conditional 2026-05-12 21:06:29 -04:00
c79d93dd58 Wrap subdirectory compilation in conditional 2026-05-12 21:01:47 -04:00
ccd26494d9 Import all dependencies as submodules, make cmake do the right thing 2026-05-12 16:45:42 -04:00
5439b8004b Move some of the sprite math out of the actor into the sprite 2026-05-12 15:21:36 -04:00
0f01126bad More akstdlib conversion 2026-05-11 20:58:36 -04:00
d928a8af0f Complete rename to akgl 2026-05-10 00:06:58 -04:00
4a02f0364f Start moving away from wrapping libc stuff in akerror, and move towards akstdlib that does this for me 2026-05-10 00:03:45 -04:00
cc0916cd1f Added a (registry object name -> registry object pointer) map to the save method so that registry reference by name can have their actor, sprite, spritesheet, and character references reset 2026-05-09 14:45:37 -04:00
4c771227f5 Encode library version and game version into savegame files and check them both 2026-05-08 23:48:35 -04:00
ff6b282112 Added basic game save function, doesn't save much yet, but implements version comparison of binary saves for compatibility 2026-05-08 23:15:11 -04:00
0bd1ae1df8 Move music tracks out of akgl_Game to make it more suitable for serialization 2026-05-08 22:16:43 -04:00
e0a59e2447 Add types.h, standardize integer types on stdint 2026-05-08 22:01:56 -04:00
5dda86d887 Add properties registry. Start bounding defines with conditionals so users can change library allocation size. 2026-05-08 10:16:33 -04:00
a0b2dda4cf Rename library to AKGL (AKLabs Game Library) 2026-05-07 22:20:10 -04:00
359ae23414 Unify the library on an akgl_ namespace 2026-05-07 22:01:27 -04:00
f416cb5dee Fix bad pointer reference in SDL3G_controller_default 2026-05-06 23:05:17 -04:00
ef24d7b843 Add missing text header file 2026-05-06 22:49:05 -04:00
87a5b1da21 Add ability to push controls to a control map without having to manually track controlmap index IDs 2026-05-06 12:02:36 -04:00
c3847160da Added a font registry, a text rendering helper, and an FPS counter function 2026-05-06 11:12:42 -04:00
284ffd7b4a Fix missing sdl3game.pc for pkg-config, change sprite speed and character movement values to long int expressed in nanoseconds for better compatibility with SDL_Time and locking against game clock, still expressed as milliseconds in json 2026-05-05 20:40:25 -04:00
90cbf41d75 Added missing error include 2026-05-04 00:02:31 -04:00
6763b5629f Got the suite rebuilding, most tests pass, actor and sprite are failing 2026-05-03 23:57:55 -04:00
f475dfb6ee Move from 'libsdlerror' to 'libakerror' 2026-01-05 08:58:06 -05:00
7cff27f035 Added charviewer 2026-01-05 08:57:38 -05:00
0601a8d767 Add rebuild script to make my life easier 2025-08-09 13:54:42 -04:00
4b901886a2 Add GameControllerDB support 2025-08-09 13:53:37 -04:00
62ba23a1a0 Simplified SDL3GActor_cmhf_* functions 2025-08-09 09:28:28 -04:00
93bc3addfe actor_logic_changeframe was allowing frame counters to increment out of control leading to unpredictable behavior when changing state, animations would be outside their frame range 2025-08-09 08:59:52 -04:00
103 changed files with 12462 additions and 2247 deletions

89
.gitea/workflows/ci.yaml Normal file
View 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'

2
.gitignore vendored
View File

@@ -1 +1,3 @@
./build/* ./build/*
.aider*
*~

24
.gitmodules vendored Normal file
View File

@@ -0,0 +1,24 @@
[submodule "deps/semver"]
path = deps/semver
url = git@github.com:h2non/semver.c.git
[submodule "deps/SDL"]
path = deps/SDL
url = git@github.com:libsdl-org/SDL.git
[submodule "deps/SDL_image"]
path = deps/SDL_image
url = git@github.com:libsdl-org/SDL_image.git
[submodule "deps/SDL_mixer"]
path = deps/SDL_mixer
url = git@github.com:libsdl-org/SDL_mixer.git
[submodule "deps/SDL_ttf"]
path = deps/SDL_ttf
url = git@github.com:libsdl-org/SDL_ttf.git
[submodule "deps/libsdlerror"]
path = deps/libakerror
url = https://source.starfort.tech/andrew/libakerror.git
[submodule "deps/libakstdlib"]
path = deps/libakstdlib
url = https://source.starfort.tech/andrew/libakstdlib.git
[submodule "deps/jansson"]
path = deps/jansson
url = git@github.com:akheron/jansson.git

48
AGENTS.md Normal file
View 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.

View File

@@ -1,28 +1,104 @@
cmake_minimum_required(VERSION 3.10) cmake_minimum_required(VERSION 3.10)
project(sdl3game LANGUAGES C) project(akgl LANGUAGES C)
include(CTest) include(CTest)
option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF)
find_package(PkgConfig REQUIRED) if(AKGL_COVERAGE)
find_package(SDL3 REQUIRED) if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
find_package(SDL3_image REQUIRED) message(FATAL_ERROR "AKGL_COVERAGE requires GCC or Clang")
find_package(SDL3_mixer REQUIRED) endif()
find_package(sdlerror REQUIRED) find_program(GCOVR_EXECUTABLE gcovr REQUIRED)
find_package(jansson REQUIRED) endif()
find_package(box2d REQUIRED)
# Check for SDL3 using pkg-config if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
pkg_check_modules(SDL3 REQUIRED sdl3)
pkg_check_modules(SDL3_image REQUIRED sdl3-image) # Vendored projects own their test suites. Suppress their CTest registration
pkg_check_modules(SDL3_mixer REQUIRED sdl3-mixer) # while embedded so the top-level suite contains only targets built here.
pkg_check_modules(jansson REQUIRED jansson) set(AKGL_SUPPRESS_DEPENDENCY_TESTS TRUE)
pkg_check_modules(sdlerror REQUIRED sdlerror) 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)
add_subdirectory(deps/SDL EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_image EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL)
# 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)
if(NOT TARGET SDL3::SDL3)
find_package(SDL3 REQUIRED)
endif()
if(NOT TARGET SDL3_image::SDL3_image)
find_package(SDL3_image REQUIRED)
endif()
if(NOT TARGET SDL3_mixer::SDL3_mixer)
find_package(SDL3_mixer REQUIRED)
endif()
if(NOT TARGET SDL3_ttf::SDL3_ttf)
find_package(SDL3_ttf REQUIRED)
endif()
if(NOT TARGET akerror::akerror)
find_package(akerror REQUIRED)
endif()
if(NOT TARGET akstdlib::akstdlib)
find_package(akstdlib REQUIRED)
endif()
if(NOT TARGET jansson::jansson)
find_package(jansson)
endif()
endif()
set(GAMECONTROLLERDB_H "include/akgl/SDL_GameControllerDB.h")
set(prefix ${CMAKE_INSTALL_PREFIX})
set(exec_prefix "\${prefix}")
set(libdir "\${exec_prefix}/lib")
set(includedir "\${prefix}/include")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akgl.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc @ONLY)
# Tests use both relative paths and SDL_GetBasePath(), so stage fixtures beside
# test executables in every out-of-tree build.
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/tests/assets"
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
add_custom_command(
OUTPUT ${GAMECONTROLLERDB_H}
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/mkcontrollermappings.sh ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Generating controller mappings ..."
)
# Add include directories # Add include directories
include_directories(${SDL3_INCLUDE_DIRS}) include_directories(${SDL3_INCLUDE_DIRS})
add_library(sdl3game SHARED add_library(akgl SHARED
deps/semver/semver.c
src/actor.c src/actor.c
src/actor_state_string_names.c src/actor_state_string_names.c
src/text.c
src/assets.c src/assets.c
src/character.c src/character.c
src/draw.c src/draw.c
@@ -31,60 +107,205 @@ add_library(sdl3game SHARED
src/heap.c src/heap.c
src/json_helpers.c src/json_helpers.c
src/registry.c src/registry.c
src/renderer.c
src/physics.c
src/sprite.c src/sprite.c
src/staticstring.c src/staticstring.c
src/tilemap.c src/tilemap.c
src/util.c src/util.c
${GAMECONTROLLERDB_H}
) )
add_executable(charviewer util/charviewer.c) add_library(akgl::akgl ALIAS akgl)
add_executable(test_actor tests/actor.c) add_executable(charviewer util/charviewer.c)
add_executable(test_bitmasks tests/bitmasks.c) add_executable(test_semver_unit deps/semver/semver_unit.c)
add_executable(test_character tests/character.c)
add_executable(test_registry tests/registry.c) # Every suite here is a standalone C program named tests/<name>.c, built as
add_executable(test_sprite tests/sprite.c) # test_<name> and registered with CTest under <name>.
add_executable(test_staticstring tests/staticstring.c) set(AKGL_TEST_SUITES
add_executable(test_tilemap tests/tilemap.c) actor
add_executable(test_util tests/util.c) bitmasks
add_test(NAME actor COMMAND test_actor) character
add_test(NAME bitmasks COMMAND test_bitmasks) controller
add_test(NAME character COMMAND test_character) game
add_test(NAME registry COMMAND test_registry) heap
add_test(NAME sprite COMMAND test_sprite) json_helpers
add_test(NAME staticstring COMMAND test_staticstring) physics
add_test(NAME tilemap COMMAND test_tilemap) registry
add_test(NAME util COMMAND test_util) 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) # Specify include directories for the library's headers (if applicable)
target_include_directories(sdl3game PUBLIC target_include_directories(akgl PUBLIC
include/ include/
deps/semver/
) )
target_link_libraries(test_actor PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm) if(AKGL_COVERAGE)
target_link_libraries(test_bitmasks PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm) target_compile_options(akgl PRIVATE --coverage -O0 -g)
target_link_libraries(test_character PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm) target_link_options(akgl PRIVATE --coverage)
target_link_libraries(test_registry PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
target_link_libraries(test_sprite PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
target_link_libraries(test_staticstring PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
target_link_libraries(test_tilemap PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
target_link_libraries(test_util PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm)
target_link_libraries(charviewer PRIVATE sdlerror::sdlerror sdl3game SDL3::SDL3 SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer box2d::box2d jansson::jansson -lm) set(AKGL_COVERAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/coverage")
file(MAKE_DIRECTORY "${AKGL_COVERAGE_DIR}")
set(main_lib_dest "lib/sdl3game-${MY_LIBRARY_VERSION}") add_test(
install(TARGETS sdl3game DESTINATION "lib/") NAME coverage_reset
install(FILES "include/sdl3game/actor.h" DESTINATION "include/sdl3game/") COMMAND ${GCOVR_EXECUTABLE}
install(FILES "include/sdl3game/assets.h" DESTINATION "include/sdl3game/") --root "${CMAKE_CURRENT_SOURCE_DIR}"
install(FILES "include/sdl3game/character.h" DESTINATION "include/sdl3game/") --object-directory "${CMAKE_CURRENT_BINARY_DIR}"
install(FILES "include/sdl3game/draw.h" DESTINATION "include/sdl3game/") --delete
install(FILES "include/sdl3game/game.h" DESTINATION "include/sdl3game/") )
install(FILES "include/sdl3game/controller.h" DESTINATION "include/sdl3game/") set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP akgl_coverage)
install(FILES "include/sdl3game/heap.h" DESTINATION "include/sdl3game/") # Any suite missing from this list runs outside the fixture and has its
install(FILES "include/sdl3game/iterator.h" DESTINATION "include/sdl3game/") # counters discarded by coverage_reset.
install(FILES "include/sdl3game/json_helpers.h" DESTINATION "include/sdl3game/") set_tests_properties(
install(FILES "include/sdl3game/registry.h" DESTINATION "include/sdl3game/") ${AKGL_TEST_SUITES} semver_unit
install(FILES "include/sdl3game/sprite.h" DESTINATION "include/sdl3game/") PROPERTIES FIXTURES_REQUIRED akgl_coverage
install(FILES "include/sdl3game/staticstring.h" DESTINATION "include/sdl3game/") )
install(FILES "include/sdl3game/tilemap.h" DESTINATION "include/sdl3game/")
install(FILES "include/sdl3game/util.h" DESTINATION "include/sdl3game/") add_test(
NAME coverage_report
COMMAND ${GCOVR_EXECUTABLE}
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
--filter "${CMAKE_CURRENT_SOURCE_DIR}/src/"
--xml-pretty
--xml "${AKGL_COVERAGE_DIR}/coverage.xml"
--html-details "${AKGL_COVERAGE_DIR}/index.html"
)
set_tests_properties(
coverage_report
PROPERTIES
FIXTURES_CLEANUP akgl_coverage
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)
endif()
target_link_libraries(akgl
PUBLIC
SDL3::SDL3
SDL3_image::SDL3_image
SDL3_mixer::SDL3_mixer
SDL3_ttf::SDL3_ttf
akstdlib::akstdlib
akerror::akerror
jansson::jansson
)
foreach(suite IN LISTS AKGL_TEST_SUITES)
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/")
install(FILES "deps/semver/semver.h" DESTINATION "include/")
install(FILES "include/akgl/actor.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/types.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/text.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/assets.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/character.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/error.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/draw.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/game.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/controller.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/heap.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/iterator.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/json_helpers.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/renderer.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/physics.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/registry.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/sprite.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/staticstring.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/tilemap.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/util.h" DESTINATION "include/akgl/")
install(FILES ${GAMECONTROLLERDB_H} DESTINATION "include/akgl/")

18
Doxyfile Normal file
View 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

284
README.md Normal file
View File

@@ -0,0 +1,284 @@
## How do I initialize a game
Initialize the global game object with info about your game
```c
strncpy((char *)&game.name, "sdl3-gametest", 256);
strncpy((char *)&game.version, "0.0.1", 32);
strncpy((char *)&game.uri, "net.aklabs.games.sdl3-gametest", 256);
```
Call the game initialization routines and lock the game state for further initialization
```c
PASS(e, akgl_game_init());
PASS(e, akgl_game_state_lock());
```
If you have a registry properties file, load it. If you don't have a properties file, use `akgl_set_property("prop_name", "prop_value")` to populate the required game properties.
```c
PASS(e, akgl_registry_load_properties(YOUR_REGISTR_FILEPATH));
```
Initialize your physics engine and renderer of choice
```c
PASS(e, akgl_render_init2d(renderer));
PASS(e, akgl_physics_init_arcade(physics));
```
Unlock the game state
```c
PASS(e, akgl_game_state_unlock());
```
## What is in a properties file (or, What properties must I set if I don't have one?)
```json
{
"properties": {
"game.screenwidth": "640",
"game.screenheight": "480",
"physics.gravity.y": "1024.0",
"physics.drag.y": "1.0"
}
}
```
Physics properties (gravity and drag along X, Y and Z) are optional and default to `0`.
## How do I update and render the game world in my main loop
In your game loop (or in your `SDL_AppIterate` method), lock the game state, call the game update function, and then unlock the game state
```c
PASS(e, akgl_game_state_lock());
PASS(e, renderer->frame_start(renderer));
SDL_RenderClear(renderer->sdl_renderer);
PASS(e, akgl_game_update(NULL));
PASS(e, renderer->frame_end(renderer));
PASS(e, akgl_game_state_unlock());
```
## How do I get an actor on screen
Load a sprite for a character. Sprites are JSON documents describing 2D sprites, frames, and looping. Sprites are named, and sprite names must be unique.
```c
PASS(e, akgl_sprite_load_json(SOME_FILENAME))
```
Load a character from a JSON file. Characters map sprites to actor states and define physics characteristics like movement speed. Each character is named, and character names must be unique.
```c
PASS(e, akgl_character_load_json(SOME_FILENAME))
```
You don't strictly have to load sprites and characters from json files, you can initialize them yourself, it's just tedious work. Here's an example of initializing a 32x32 sprite from a spritesheet that uses the first 4 frames in a looping animation.
```c
akgl_SpriteSheet *sheet;
akgl_Sprite *sprite;
akgl_Character *character;
PASS(e, akgl_heap_next_spritesheet(&sheet);
PASS(e, akgl_spritesheet_initialize(sheet, 32, 32, IMAGE_FILENAME));
PASS(e, akgl_heap_next_sprite(&sprite));
PASS(e, akgl_sprite_initialize(sprite, SPRITE_NAME, &sheet);
sprite->frames = 4;
sprite->frameids = [0, 1, 2, 3];
sprite->width = 32;
sprite->height = 32;
sprite->speed = 1000;
sprite->loop = true;
strncpy((char *)&sprite->name, "SPRITE NAME", AKGL_SPRITE_MAX_NAME_LENGTH);
PASS(e, akgl_heap_next_character(&character));
PASS(e, akgl_character_initialize(&character, "CHAR NAME"));
PASS(e, akgl_character_sprite_add(&character, &sprite, STATE_MASK));
// Set the character acceleration and scale if desired
character->ax = 64.0;
character->ay = 64.0;
character->sx = 2.0;
character->sy = 2.0;
```
Initialize an actor. Actors are named ("player", "Quest NPC", whatever) and names must be unique.
```c
akgl_Actor *myactor = NULL;
PASS(e, akgl_heap_next_actor(&myactor);
PASS(e, akgl_actor_initialize(&myactor, "ACTOR_NAME"));
```
Assign a character to the actor by looking up the `akgl_Character` from the AKGL registry and assign it.
```c
myactor->basechar = SDL_GetPointerProperty(
AKGL_REGISTRY_CHARACTER,
"CHARACTER_NAME",
NULL);
FAIL_ZERO_BREAK(e, myactor->basechar, AKERR_REGISTRY, "Character missing");
```
Give the actor a position and a state, and turn it visible.
```c
myactor->state = 9AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT);
myactor->x = 320;
myactor->y = 240;
myactor->visible = true;
```
## What are in Sprite and Character files
Sprite files:
```json
{
"spritesheet": {
"filename": "RELATIVE_IMAGE_FILE_REFERENCE",
"frame_width": int,
"frame_height": int
},
"name": "UNIQUE_SPRITE_NAME",
"width": int,
"height": int,
"speed": int,
"loop": boolean,
"loopReverse": boolean,
"frames": [
int
]
}
```
* `frames` references the frame indexes in the spritesheet that should be used for this animation. Spritesheets are counted from the top left corner going to the right according to the spritesheet `frame_width` and `frame_height`.
* `loop` says whether or not we should loop the animation
* `loopReverse` says whether or not we should "bounce" the animation (when we reach the end of the frames, start counting back to the beginning, then count to the end, etc). Otherwise the frames are displayed from 0..n and then cycles back to 0.
* `speed` is the number of milliseconds each frame in the animation should appear on the screen
Character files:
```c
{
"name": "UNIQUE_CHARACTER_NAME",
"speedtime": 8,
"speed_x": 0,
"speed_y": 0,
"acceleration_x": 0,
"acceleration_y": 0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_UP",
"AKGL_ACTOR_STATE_MOVING_UP"
],
"sprite": "menupointer"
}[, ...]
]
}
```
* `speedtime` appears to be legacy and unused.
* `speed_[xy]` and `acceleration_[xy]` are physics parameters that specify the top speed and acceleration rate (in pixels per nanosecond) of the character in physics simulations. The effect of acceleration depends on the physics simulation being used at the time (which may or may not account for gravity, drag, etc).
* `sprite_mappings` map a set of actor state flag bitmasks (assume everything in `state` is `OR`ed together) to a sprite name. The game engine uses this to automatically pick the correct sprite (by name) for a given set of state flags. You need one of these for every possible state the character may be used in.
## How do I load a tilemap from the filesystem and display it on screen with my actors
The engine ONLY supports TilED TMJ tilemaps with tileset external references. Load a tilemap into the global `akgl_Tilemap *gamemap` object.
```c
PASS(e, akgl_tilemap_load(PATHSTRING, gamemap));
```
Actors will be automatically populated from objects in the tilemap object layers. Actor state flags here must be expressed as an integer, you can't (yet) use the same array of strings that is used in character json files.
```json
"objects":[
{
"gid":147,
"height":16,
"id":1,
"name":"player",
"properties":[
{
"name":"character",
"type":"string",
"value":"little guy"
},
{
"name":"state",
"type":"int",
"value":24
}],
"rotation":0,
"type":"actor",
"visible":true,
"width":16,
"x":440.510088317656,
"y":140.347239175702
}[, ... ]
```
Check if the tilemap wants to use its own physics, and if you want to allow that, override the global physics simulation
```c
if ( gamemap->use_own_physics == true ) {
physics = &gamemap->physics;
}
```
Tilemap physics specification follows. A map can specify its own physics properties (drag, gravity) without specifying a custom model.
```json
"properties":[
{
"name":"physics.drag.y",
"type":"float",
"value":0
},
{
"name":"physics.gravity.y",
"type":"float",
"value":0
},
{
"name":"physics.model",
"type":"string",
"value":"arcade"
}],
```
The global `gamemap` object is automatically displayed if it is populated. Actors are drawn at the appropriate map layer depending on the actor's `layer` property (warning: this may be replaced with a `z` property soon.)
## How do I get the screen width and height
The most direct is to call `SDL_GetCurrentDisplayMode` to get the parameters from the returned `SDL_DisplayMode` structure (`->w` and `->h`).
The simplest way is to check the global `camera` object's `camera->w` and `camera->h` object. You may have more than one camera on a scene, and it's theoretically possible that the global camera object has been overriden and no longer represents the full screen.
The most reliable engine-centric way is to use `akgl_get_property` to get the property from the engine. Properties are read and stored as strings, so if you need to do these kinds of things a lot, cache the integer value somewhere.
```c
akgl_String *width = NULL;
int screenwidth = NULL;
PASS(e, akgl_get_property("game.screenwidth", &width, "0"));
PASS(e, aksl_atoi(width->data, &screenwidth));
PASS(e, akgl_heap_release_string(width));
```
## 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
View 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.

View File

@@ -1,5 +1,3 @@
Character definitions should allow for specifying multiple states per sprite mapping, rather than only one state per map. Use an array instead.
Rendering should move to the SDL GPU renderer so i can do lighting and particles etc Rendering should move to the SDL GPU renderer so i can do lighting and particles etc
- Example suitable for my most primitive use case: https://github.com/TheSpydog/SDL_gpu_examples/blob/main/Examples/Blit2DArray.c - Example suitable for my most primitive use case: https://github.com/TheSpydog/SDL_gpu_examples/blob/main/Examples/Blit2DArray.c
- Try vulkan and D3D tutorials to come up to speed on the moving pieces, then figure ou the details from the examples and API docs - Try vulkan and D3D tutorials to come up to speed on the moving pieces, then figure ou the details from the examples and API docs

10
akgl.pc.in Normal file
View File

@@ -0,0 +1,10 @@
prefix=@CMAKE_INSTALL_PREFIX@
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${exec_prefix}/include
Name: akgl
Description: AKLabs Game library
Version: @PROJECT_VERSION@
Cflags: -I${includedir}/
Libs: -L${libdir} -lakgl

1
deps/SDL vendored Submodule

Submodule deps/SDL added at d9d5536704

1
deps/SDL_image vendored Submodule

Submodule deps/SDL_image added at bec9134a26

1
deps/SDL_mixer vendored Submodule

Submodule deps/SDL_mixer added at a5e1890afc

1
deps/SDL_ttf vendored Submodule

Submodule deps/SDL_ttf added at a1ce3670ae

1
deps/jansson vendored Submodule

Submodule deps/jansson added at 1eb7a81297

1
deps/libakerror vendored Submodule

Submodule deps/libakerror added at 0c0d81249f

1
deps/libakstdlib vendored Submodule

Submodule deps/libakstdlib added at a87cbfb26d

1
deps/semver vendored Submodule

Submodule deps/semver added at bd1db234a6

File diff suppressed because it is too large Load Diff

262
include/akgl/actor.h Normal file
View File

@@ -0,0 +1,262 @@
/**
* @file actor.h
* @brief Declares the public actor API.
*/
#ifndef _AKGL_ACTOR_H_
#define _AKGL_ACTOR_H_
#include <stdint.h>
#include "types.h"
#include "character.h"
// ---- LOW WORD STATUSES ----
#define AKGL_ACTOR_STATE_FACE_DOWN 1 << 0 // 1 0000 0000 0000 0001
#define AKGL_ACTOR_STATE_FACE_LEFT 1 << 1 // 2 0000 0000 0000 0010
#define AKGL_ACTOR_STATE_FACE_RIGHT 1 << 2 // 4 0000 0000 0000 0100
#define AKGL_ACTOR_STATE_FACE_UP 1 << 3 // 8 0000 0000 0000 1000
#define AKGL_ACTOR_STATE_ALIVE 1 << 4 // 16 0000 0000 0001 0000
#define AKGL_ACTOR_STATE_DYING 1 << 5 // 32 0000 0000 0010 0000
#define AKGL_ACTOR_STATE_DEAD 1 << 6 // 64 0000 0000 0100 0000
#define AKGL_ACTOR_STATE_MOVING_LEFT 1 << 7 // 128 0000 0000 1000 0000
#define AKGL_ACTOR_STATE_MOVING_RIGHT 1 << 8 // 256 0000 0001 0000 0000
#define AKGL_ACTOR_STATE_MOVING_UP 1 << 9 // 512 0000 0010 0000 0000
#define AKGL_ACTOR_STATE_MOVING_DOWN 1 << 10 // 1024 0000 0100 0000 0000
#define AKGL_ACTOR_STATE_MOVING_IN 1 << 11 // 2048 0000 1000 0000 0000
#define AKGL_ACTOR_STATE_MOVING_OUT 1 << 12 // 4096 0001 0000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_13 1 << 13 // 8192 0010 0000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_14 1 << 14 // 16384 0100 0000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_15 1 << 15 // 32768 1000 0000 0000 0000
// ----- HIGH WORD STATUSES -----
#define AKGL_ACTOR_STATE_UNDEFINED_16 1 << 16 // 65536 0000 0000 0000 0001
#define AKGL_ACTOR_STATE_UNDEFINED_17 1 << 17 // 131072 0000 0000 0000 0010
#define AKGL_ACTOR_STATE_UNDEFINED_18 1 << 18 // 262144 0000 0000 0000 0100
#define AKGL_ACTOR_STATE_UNDEFINED_19 1 << 19 // 524288 0000 0000 0000 1000
#define AKGL_ACTOR_STATE_UNDEFINED_20 1 << 20 // 1048576 0000 0000 0001 0000
#define AKGL_ACTOR_STATE_UNDEFINED_21 1 << 21 // 2097152 0000 0000 0010 0000
#define AKGL_ACTOR_STATE_UNDEFINED_22 1 << 22 // 4194304 0000 0000 0100 0000
#define AKGL_ACTOR_STATE_UNDEFINED_23 1 << 23 // 8388608 0000 0000 1000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_24 1 << 24 // 16777216 0000 0001 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_25 1 << 25 // 33554432 0000 0010 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_26 1 << 26 // 67108864 0000 0100 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_27 1 << 27 // 134217728 0000 1000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_28 1 << 28 // 268435456 0001 0000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_29 1 << 29 // 536870912 0010 0000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_30 1 << 30 // 1073741824 0100 0000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_31 1 << 31 // 2147483648 1000 0000 0000 0000
#define AKGL_ACTOR_MAX_STATES 32
// This is an array of strings equal to actor states from 1-32.
// This is built by a utility script and not kept in git, see
// the Makefile for lib_src/actor_state_string_names.c
/** @brief 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)
#define AKGL_ACTOR_STATE_MOVING_ALL (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_MOVING_UP | AKGL_ACTOR_STATE_MOVING_DOWN)
#define AKGL_ACTOR_MAX_NAME_LENGTH 128
#define AKGL_ACTOR_MAX_CHILDREN 8
#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];
akgl_Character *basechar;
uint8_t curSpriteFrameId;
SDL_Time curSpriteFrameTimer;
bool curSpriteReversing;
uint32_t layer;
int32_t state;
bool movement_controls_face;
void *actorData;
bool visible;
SDL_Time movetimer;
// Velocity. Combined effect of all forces acting on the actor resulting
// in energy along an axis.
float32_t vx;
float32_t vy;
float32_t vz;
// Environmental velocity. These are the forces acting on the actor by the
// environment (such as gravity and atmospheric drag)
float32_t ex;
float32_t ey;
float32_t ez;
// Thrust. Energy originating only from the actor's own acceleration on a
// given axis, before the effects of gravity and drag.
float32_t tx;
float32_t ty;
float32_t tz;
// Acceleration. These are borrowed from the base character object.
// A given axis resets to 0 when the actor stops moving in a given axis.
float32_t ax;
float32_t ay;
float32_t az;
// Max speed. These are borrowed from the base character object.
float32_t sx;
float32_t sy;
float32_t sz;
// Position.
float32_t x;
float32_t y;
float32_t z;
float32_t scale;
struct akgl_Actor *children[AKGL_ACTOR_MAX_CHILDREN];
struct akgl_Actor *parent;
akerr_ErrorContext AKERR_NOIGNORE *(*updatefunc)(struct akgl_Actor *obj);
akerr_ErrorContext AKERR_NOIGNORE *(*renderfunc)(struct akgl_Actor *obj);
akerr_ErrorContext AKERR_NOIGNORE *(*facefunc)(struct akgl_Actor *obj);
akerr_ErrorContext AKERR_NOIGNORE *(*movementlogicfunc)(struct akgl_Actor *obj, float32_t dt);
akerr_ErrorContext AKERR_NOIGNORE *(*changeframefunc)(struct akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems);
akerr_ErrorContext AKERR_NOIGNORE *(*addchild)(struct akgl_Actor *obj, struct akgl_Actor *child);
} akgl_Actor;
/**
* @brief 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_

20
include/akgl/assets.h Normal file
View File

@@ -0,0 +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_

79
include/akgl/character.h Normal file
View File

@@ -0,0 +1,79 @@
/**
* @file character.h
* @brief Declares the public character API.
*/
#ifndef _AKGL_CHARACTER_H_
#define _AKGL_CHARACTER_H_
#include <SDL3/SDL_properties.h>
#include "types.h"
#include "sprite.h"
#define AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH 128
#define AKGL_MAX_HEAP_CHARACTER 256
/** @brief Defines reusable movement parameters and actor-state sprite bindings. */
typedef struct akgl_Character {
uint8_t refcount;
char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH];
SDL_PropertiesID state_sprites;
uint64_t speedtime;
float32_t ax;
float32_t ay;
float32_t az;
float32_t sx;
float32_t sy;
float32_t sz;
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_add)(struct akgl_Character *, akgl_Sprite *, int);
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_get)(struct akgl_Character *, int, akgl_Sprite **);
} akgl_Character;
/**
* @brief 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_

120
include/akgl/controller.h Normal file
View File

@@ -0,0 +1,120 @@
/**
* @file controller.h
* @brief Declares the public controller API.
*/
#ifndef _CONTROLLER_H_
#define _CONTROLLER_H_
#include <SDL3/SDL.h>
#include <akerror.h>
#include "types.h"
#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;
uint8_t button;
SDL_Keycode key;
uint8_t axis;
uint8_t axis_range_min;
uint8_t axis_range_max;
akerr_ErrorContext AKERR_NOIGNORE *(*handler_on)(akgl_Actor *obj, SDL_Event *event);
akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event);
} akgl_Control;
/** @brief Groups input bindings for one actor and its input devices. */
typedef struct {
akgl_Actor *target;
uint16_t nextMap;
akgl_Control controls[AKGL_MAX_CONTROLS];
SDL_KeyboardID kbid;
SDL_JoystickID jsid;
SDL_MouseID mouseid;
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_

16
include/akgl/draw.h Normal file
View File

@@ -0,0 +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_

26
include/akgl/error.h Normal file
View File

@@ -0,0 +1,26 @@
/**
* @file error.h
* @brief Declares the public error API.
*/
#ifndef _ERROR_H_
#define _ERROR_H_
// This macro is used to silence warnings on string concatenation operations that may fail.
// e.g., combining two element of PATH_MAX into a string buffer of AKGL_STRING_MAX_LENGTH.
// We have to draw a line in the sand somewhere or we will just let our buffers grow forever
// to keep the compiler happy.
#define DISABLE_GCC_WARNING_FORMAT_TRUNCATION \
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wformat-truncation\"")
#define RESTORE_GCC_WARNINGS \
_Pragma("GCC diagnostic pop")
#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_

144
include/akgl/game.h Normal file
View File

@@ -0,0 +1,144 @@
/**
* @file game.h
* @brief Declares the public game API.
*/
#ifndef _AKGL_GAME_H_
#define _AKGL_GAME_H_
#include <stdint.h>
#include <SDL3_mixer/SDL_mixer.h>
#include "types.h"
#include "tilemap.h"
#include "renderer.h"
#include "physics.h"
#define AKGL_VERSION "0.1.0"
#define AKGL_GAME_AUDIO_TRACK_BGM 1
#define AKGL_GAME_AUDIO_MAX_TRACKS 64
#define AKGL_TIME_ONESEC_NS 1000000000
#define AKGL_TIME_ONESEC_MS 1000000
/* ==================== 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];
char name[256];
char uri[256];
akgl_GameState state;
SDL_Mutex *statelock;
int16_t fps;
SDL_Time gameStartTime;
SDL_Time lastIterTime;
SDL_Time lastFPSTime;
int16_t framesSinceUpdate;
void (*lowfpsfunc)(void);
} akgl_Game;
/** @brief 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
#define AKGL_BITMASK_HASNOT(x, y) (x & y) != y
#define AKGL_BITMASK_ADD(x, y) x |= y
#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_

130
include/akgl/heap.h Normal file
View File

@@ -0,0 +1,130 @@
/**
* @file heap.h
* @brief Declares the public heap API.
*/
#ifndef _AKGL_HEAP_H_
#define _AKGL_HEAP_H_
#include "sprite.h"
#include "actor.h"
#include "character.h"
#include "staticstring.h"
#include <akerror.h>
#ifndef AKGL_MAX_HEAP_ACTOR
#define AKGL_MAX_HEAP_ACTOR 64
#endif
#ifndef AKGL_MAX_HEAP_SPRITE
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
#endif
#ifndef AKGL_MAX_HEAP_SPRITESHEET
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
#endif
#ifndef AKGL_MAX_HEAP_CHARACTER
#define AKGL_MAX_HEAP_CHARACTER 256
#endif
#ifndef AKGL_MAX_HEAP_STRING
#define AKGL_MAX_HEAP_STRING 256
#endif
/** @brief 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_

49
include/akgl/iterator.h Normal file
View File

@@ -0,0 +1,49 @@
/**
* @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;
} akgl_Iterator;
#define AKGL_ITERATOR_OP_UPDATE 1 // 1
#define AKGL_ITERATOR_OP_RENDER 1 << 1 // 2
#define AKGL_ITERATOR_OP_RELEASE 1 << 2 // 4
#define AKGL_ITERATOR_OP_LAYERMASK 1 << 3 // 8
#define AKGL_ITERATOR_OP_TILEMAPSCALE 1 << 4 // 16
#define AKGL_ITERATOR_OP_UNDEFINED_5 1 << 5 // 32
#define AKGL_ITERATOR_OP_UNDEFINED_6 1 << 6 // 64
#define AKGL_ITERATOR_OP_UNDEFINED_7 1 << 7 // 128
#define AKGL_ITERATOR_OP_UNDEFINED_8 1 << 8 // 256
#define AKGL_ITERATOR_OP_UNDEFINED_9 1 << 9 // 512
#define AKGL_ITERATOR_OP_UNDEFINED_10 1 << 10 // 1024
#define AKGL_ITERATOR_OP_UNDEFINED_11 1 << 11 // 2048
#define AKGL_ITERATOR_OP_UNDEFINED_12 1 << 12 // 4096
#define AKGL_ITERATOR_OP_UNDEFINED_13 1 << 13 // 8192
#define AKGL_ITERATOR_OP_UNDEFINED_14 1 << 14 // 16384
#define AKGL_ITERATOR_OP_UNDEFINED_15 1 << 15 // 32768
#define AKGL_ITERATOR_OP_UNDEFINED_16 1 << 16 // 65536
#define AKGL_ITERATOR_OP_UNDEFINED_17 1 << 17 // 131072
#define AKGL_ITERATOR_OP_UNDEFINED_18 1 << 18 // 262144
#define AKGL_ITERATOR_OP_UNDEFINED_19 1 << 19 // 524288
#define AKGL_ITERATOR_OP_UNDEFINED_20 1 << 20 // 1048576
#define AKGL_ITERATOR_OP_UNDEFINED_21 1 << 21 // 2097152
#define AKGL_ITERATOR_OP_UNDEFINED_22 1 << 22 // 4194304
#define AKGL_ITERATOR_OP_UNDEFINED_23 1 << 23 // 8388608
#define AKGL_ITERATOR_OP_UNDEFINED_24 1 << 24 // 16777216
#define AKGL_ITERATOR_OP_UNDEFINED_25 1 << 25 // 33554432
#define AKGL_ITERATOR_OP_UNDEFINED_26 1 << 26 // 67108864
#define AKGL_ITERATOR_OP_UNDEFINED_27 1 << 27 // 134217728
#define AKGL_ITERATOR_OP_UNDEFINED_28 1 << 28 // 268435456
#define AKGL_ITERATOR_OP_UNDEFINED_29 1 << 29 // 536870912
#define AKGL_ITERATOR_OP_UNDEFINED_30 1 << 30 // 1073741824
#define AKGL_ITERATOR_OP_UNDEFINED_31 1 << 31 // 2147483648
#endif // _AKGL_ITERATOR_H_

136
include/akgl/json_helpers.h Normal file
View File

@@ -0,0 +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_

124
include/akgl/physics.h Normal file
View File

@@ -0,0 +1,124 @@
/**
* @file physics.h
* @brief Declares the public physics API.
*/
#ifndef _PHYSICS_H_
#define _PHYSICS_H_
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/actor.h>
#include <akgl/iterator.h>
#include <akgl/staticstring.h>
/** @brief Defines a pluggable physics backend and its environmental parameters. */
typedef struct akgl_PhysicsBackend {
akerr_ErrorContext AKERR_NOIGNORE *(*simulate)(struct akgl_PhysicsBackend *self, akgl_Iterator *opflags);
akerr_ErrorContext AKERR_NOIGNORE *(*gravity)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
akerr_ErrorContext AKERR_NOIGNORE *(*collide)(struct akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
double drag_x;
double drag_y;
double drag_z;
double gravity_x;
double gravity_y;
double gravity_z;
SDL_Time gravity_time;
SDL_Time timer_gravity;
} akgl_PhysicsBackend;
/**
* @brief 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_

108
include/akgl/registry.h Normal file
View File

@@ -0,0 +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_

85
include/akgl/renderer.h Normal file
View File

@@ -0,0 +1,85 @@
/**
* @file renderer.h
* @brief Declares the public renderer API.
*/
#ifndef _RENDERER_H_
#define _RENDERER_H_
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/iterator.h>
/** @brief Defines a pluggable renderer backend and drawing callbacks. */
typedef struct akgl_RenderBackend {
SDL_Renderer *sdl_renderer;
akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self);
akerr_ErrorContext AKERR_NOIGNORE *(*frame_start)(struct akgl_RenderBackend *self);
akerr_ErrorContext AKERR_NOIGNORE *(*frame_end)(struct akgl_RenderBackend *self);
akerr_ErrorContext AKERR_NOIGNORE *(*draw_texture)(struct akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip);
akerr_ErrorContext AKERR_NOIGNORE *(*draw_mesh)(struct akgl_RenderBackend *self);
akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags);
} akgl_RenderBackend;
/**
* @brief 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_

88
include/akgl/sprite.h Normal file
View File

@@ -0,0 +1,88 @@
/**
* @file sprite.h
* @brief Declares the public sprite API.
*/
#ifndef _AKGL_SPRITE_H_
#define _AKGL_SPRITE_H_
#include <SDL3/SDL_properties.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#define AKGL_SPRITE_MAX_FRAMES 16
#define AKGL_SPRITE_MAX_NAME_LENGTH 128
#define AKGL_SPRITE_MAX_REGISTRY_SIZE 1024
#define AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH 512
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
/** @brief Stores a loaded spritesheet texture and frame geometry. */
typedef struct {
uint8_t refcount;
SDL_Texture *texture;
char name[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
uint16_t sprite_w;
uint16_t sprite_h;
} akgl_SpriteSheet;
/** @brief Describes an animated sprite and its spritesheet reference. */
typedef struct {
uint8_t refcount;
uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; // which IDs on the spritesheet belong to our frames
uint32_t frames; // how many frames are in this animation
uint32_t width;
uint32_t height;
uint32_t speed; // how many milliseconds a given sprite frame should be visible before cycling
bool loop; // when this sprite is done playing, it should immediately start again
bool loopReverse; // when this sprite is done playing, it should go in reverse order through its frames
char name[AKGL_SPRITE_MAX_NAME_LENGTH];
akgl_SpriteSheet *sheet;
} akgl_Sprite;
// initializes a new sprite to use the given sheet and otherwise sets to zero
/**
* @brief 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_

View File

@@ -0,0 +1,39 @@
/**
* @file staticstring.h
* @brief Declares the public staticstring API.
*/
#ifndef _STRING_H_
#define _STRING_H_
#include "string.h"
#include <akerror.h>
#include <limits.h>
#define AKGL_MAX_STRING_LENGTH PATH_MAX
/** @brief Provides a fixed-capacity, heap-managed string buffer. */
typedef struct
{
int refcount;
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_

35
include/akgl/text.h Normal file
View File

@@ -0,0 +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_

254
include/akgl/tilemap.h Normal file
View File

@@ -0,0 +1,254 @@
/**
* @file tilemap.h
* @brief Declares the public tilemap API.
*/
#ifndef _TILEMAP_H_
#define _TILEMAP_H_
#include <limits.h>
#include <akgl/actor.h>
#include <akgl/staticstring.h>
#include <akgl/physics.h>
#include <jansson.h>
#define AKGL_TILEMAP_MAX_WIDTH 512
#define AKGL_TILEMAP_MAX_HEIGHT 512
#define AKGL_TILEMAP_MAX_LAYERS 16
#define AKGL_TILEMAP_MAX_TILESETS 16
#define AKGL_TILEMAP_MAX_TILES_PER_IMAGE 65536
#define AKGL_TILEMAP_MAX_TILESET_NAME_SIZE 512
#define AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE PATH_MAX
#define AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE 512
#define AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER 128
#define AKGL_TILEMAP_OBJECT_TYPE_ACTOR 1
#define AKGL_TILEMAP_LAYER_TYPE_TILES 1
#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;
int gid;
int id;
int height;
int width;
int rotation;
int type;
bool visible;
akgl_Actor *actorptr;
char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE];
} akgl_TilemapObject;
/** @brief Represents an object embedded in a tilemap layer. */
typedef struct {
short type;
float opacity;
bool visible;
int height;
int width;
int x;
int y;
int id;
SDL_Texture *texture;
int data[AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT];
akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER];
} akgl_TilemapLayer;
/** @brief Stores tile, image, or object data for one map layer. */
typedef struct {
int columns;
int firstgid;
char imagefilename[AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE];
int imageheight;
int imagewidth;
char name[AKGL_TILEMAP_MAX_TILESET_NAME_SIZE];
SDL_Texture *texture;
// Use this as a lookup table instead of storing tiles
// in individual textures to blit them from a single
// texture at runtime
// FIXME: This is probably not very efficient. For a map
// with a single tileset it makes sense. For a map with
// multiple tilesets you may have set A start at firstgid 1
// and have 1728 tiles. Set B may start at firstgid 1729 and
// have 1728 more tiles. This means Set B has 1728 empty
// tile_offsets[] entries before firstgid 1729 because of the
// way akgl_tilemap_load_tilesets() works. This is really inefficient
// and should be improved in the future, and will eventually
// lead to premature exhaustion of AKGL_TILEMAP_MAX_TILES_PER_IMAGE
// because set D or E may only have 64 tiles but they may be
// at the upper end of the array bound already because of this.
int tile_offsets[AKGL_TILEMAP_MAX_TILES_PER_IMAGE][2];
int tilecount;
int tileheight;
int tilewidth;
int spacing;
int margin;
} akgl_Tileset;
/** @brief Represents a complete tilemap and its optional physics backend. */
typedef struct {
int tilewidth;
int tileheight;
int width;
int height;
int numlayers;
int orientation; // 0 = orthogonal, 1 = isometric
int numtilesets;
int p_foreground_y;
int p_vanishing_y;
int p_foreground_h;
int p_vanishing_h;
float p_foreground_scale;
float p_vanishing_scale;
float p_scale;
float p_rate;
akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS];
akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS];
// Different levels may have different physics.
bool use_own_physics;
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);
/*
* These functions are part of the internal API and should not be called by the user.
* They are only exposed here for unit testing.
*/
/**
* @brief 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_

14
include/akgl/types.h Normal file
View File

@@ -0,0 +1,14 @@
/**
* @file types.h
* @brief Declares the public types API.
*/
#ifndef _AKGL_TYPES_H_
#define _AKGL_TYPES_H_
#include <stdint.h>
typedef float float32_t;
typedef double float64_t;
#endif // _AKGL_TYPES_H_

92
include/akgl/util.h Normal file
View File

@@ -0,0 +1,92 @@
/**
* @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;
point bottomleft;
point bottomright;
} 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_

View File

@@ -1,105 +0,0 @@
#ifndef _ACTOR_H_
#define _ACTOR_H_
#include "character.h"
// ---- LOW WORD STATUSES ----
#define ACTOR_STATE_FACE_DOWN 1 << 0 // 1 0000 0000 0000 0001
#define ACTOR_STATE_FACE_LEFT 1 << 1 // 2 0000 0000 0000 0010
#define ACTOR_STATE_FACE_RIGHT 1 << 2 // 4 0000 0000 0000 0100
#define ACTOR_STATE_FACE_UP 1 << 3 // 8 0000 0000 0000 1000
#define ACTOR_STATE_ALIVE 1 << 4 // 16 0000 0000 0001 0000
#define ACTOR_STATE_DYING 1 << 5 // 32 0000 0000 0010 0000
#define ACTOR_STATE_DEAD 1 << 6 // 64 0000 0000 0100 0000
#define ACTOR_STATE_MOVING_LEFT 1 << 7 // 128 0000 0000 1000 0000
#define ACTOR_STATE_MOVING_RIGHT 1 << 8 // 256 0000 0001 0000 0000
#define ACTOR_STATE_MOVING_UP 1 << 9 // 512 0000 0010 0000 0000
#define ACTOR_STATE_MOVING_DOWN 1 << 10 // 1024 0000 0100 0000 0000
#define ACTOR_STATE_UNDEFINED_11 1 << 11 // 2048 0000 1000 0000 0000
#define ACTOR_STATE_UNDEFINED_12 1 << 12 // 4096 0001 0000 0000 0000
#define ACTOR_STATE_UNDEFINED_13 1 << 13 // 8192 0010 0000 0000 0000
#define ACTOR_STATE_UNDEFINED_14 1 << 14 // 16384 0100 0000 0000 0000
#define ACTOR_STATE_UNDEFINED_15 1 << 15 // 32768 1000 0000 0000 0000
// ----- HIGH WORD STATUSES -----
#define ACTOR_STATE_UNDEFINED_16 1 << 16 // 65536 0000 0000 0000 0001
#define ACTOR_STATE_UNDEFINED_17 1 << 17 // 131072 0000 0000 0000 0010
#define ACTOR_STATE_UNDEFINED_18 1 << 18 // 262144 0000 0000 0000 0100
#define ACTOR_STATE_UNDEFINED_19 1 << 19 // 524288 0000 0000 0000 1000
#define ACTOR_STATE_UNDEFINED_20 1 << 20 // 1048576 0000 0000 0001 0000
#define ACTOR_STATE_UNDEFINED_21 1 << 21 // 2097152 0000 0000 0010 0000
#define ACTOR_STATE_UNDEFINED_22 1 << 22 // 4194304 0000 0000 0100 0000
#define ACTOR_STATE_UNDEFINED_23 1 << 23 // 8388608 0000 0000 1000 0000
#define ACTOR_STATE_UNDEFINED_24 1 << 24 // 16777216 0000 0001 0000 0000
#define ACTOR_STATE_UNDEFINED_25 1 << 25 // 33554432 0000 0010 0000 0000
#define ACTOR_STATE_UNDEFINED_26 1 << 26 // 67108864 0000 0100 0000 0000
#define ACTOR_STATE_UNDEFINED_27 1 << 27 // 134217728 0000 1000 0000 0000
#define ACTOR_STATE_UNDEFINED_28 1 << 28 // 268435456 0001 0000 0000 0000
#define ACTOR_STATE_UNDEFINED_29 1 << 29 // 536870912 0010 0000 0000 0000
#define ACTOR_STATE_UNDEFINED_30 1 << 30 // 1073741824 0100 0000 0000 0000
#define ACTOR_STATE_UNDEFINED_31 1 << 31 // 2147483648 1000 0000 0000 0000
#define ACTOR_MAX_STATES 32
// This is an array of strings equal to actor states from 1-32.
// This is built by a utility script and not kept in git, see
// the Makefile for lib_src/actor_state_string_names.c
extern char *ACTOR_STATE_STRING_NAMES[ACTOR_MAX_STATES+1];
#define ACTOR_STATE_FACE_ALL (ACTOR_STATE_FACE_DOWN | ACTOR_STATE_FACE_LEFT | ACTOR_STATE_FACE_RIGHT | ACTOR_STATE_FACE_UP)
#define ACTOR_STATE_MOVING_ALL (ACTOR_STATE_MOVING_LEFT | ACTOR_STATE_MOVING_RIGHT | ACTOR_STATE_MOVING_UP | ACTOR_STATE_MOVING_DOWN)
#define ACTOR_MAX_NAME_LENGTH 128
#define ACTOR_MAX_CHILDREN 8
#define MAX_HEAP_ACTOR 64
typedef struct actor {
int refcount;
char name[ACTOR_MAX_NAME_LENGTH];
character *basechar;
int curSpriteFrameId;
SDL_Time curSpriteFrameTimer;
bool curSpriteReversing;
int layer;
int state;
bool movement_controls_face;
void *actorData;
bool visible;
int logictimer;
float x;
float y;
struct actor *children[ACTOR_MAX_CHILDREN];
struct actor *parent;
ErrorContext ERROR_NOIGNORE *(*updatefunc)(struct actor *obj);
ErrorContext ERROR_NOIGNORE *(*renderfunc)(struct actor *obj, SDL_Renderer *renderer);
ErrorContext ERROR_NOIGNORE *(*facefunc)(struct actor *obj);
ErrorContext ERROR_NOIGNORE *(*movementlogicfunc)(struct actor *obj, SDL_Time curtimems);
ErrorContext ERROR_NOIGNORE *(*changeframefunc)(struct actor *obj, sprite *curSprite, SDL_Time curtimems);
ErrorContext ERROR_NOIGNORE *(*addchild)(struct actor *obj, struct actor *child);
} actor;
ErrorContext ERROR_NOIGNORE *actor_initialize(actor *obj, char *name);
ErrorContext ERROR_NOIGNORE *actor_set_character(actor *obj, char *basecharname);
ErrorContext ERROR_NOIGNORE *actor_render(actor *obj, SDL_Renderer *renderer);
ErrorContext ERROR_NOIGNORE *actor_update(actor *obj);
ErrorContext ERROR_NOIGNORE *actor_logic_movement(actor *obj, SDL_Time curtimems);
ErrorContext ERROR_NOIGNORE *actor_logic_changeframe(actor *obj, sprite *curSprite, SDL_Time curtimems);
ErrorContext ERROR_NOIGNORE *actor_automatic_face(actor *obj);
ErrorContext ERROR_NOIGNORE *actor_add_child(actor *obj, actor *child);
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_left_on(actor *obj, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_left_off(actor *obj, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_right_on(actor *obj, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_right_off(actor *obj, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_up_on(actor *obj, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_up_off(actor *obj, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_down_on(actor *obj, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_down_off(actor *obj, SDL_Event *event);
void registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name);
#endif // _ACTOR_H_

View File

@@ -1,8 +0,0 @@
#ifndef _ASSETS_H_
#define _ASSETS_H_
#include <sdlerror.h>
ErrorContext ERROR_NOIGNORE *load_start_bgm(char *fname);
#endif //_ASSETS_H_

View File

@@ -1,31 +0,0 @@
#ifndef _CHARACTER_H_
#define _CHARACTER_H_
#include <SDL3/SDL_properties.h>
#include "sprite.h"
#define SPRITE_MAX_CHARACTER_NAME_LENGTH 128
#define MAX_HEAP_CHARACTER 256
typedef struct character {
int refcount;
char name[SPRITE_MAX_CHARACTER_NAME_LENGTH];
SDL_PropertiesID state_sprites;
ErrorContext ERROR_NOIGNORE *(*sprite_add)(struct character *, sprite *, int);
ErrorContext ERROR_NOIGNORE *(*sprite_get)(struct character *, int, sprite **);
int movementspeed;
float vx;
float vy;
} character;
ErrorContext ERROR_NOIGNORE *character_initialize(character *basechar, char *name);
ErrorContext ERROR_NOIGNORE *character_sprite_add(character *basechar, sprite *ref, int state);
ErrorContext ERROR_NOIGNORE *character_sprite_get(character *basechar, int state, sprite **dest);
// This is an SDL iterator so we can't return our error state from it.
void character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name);
ErrorContext ERROR_NOIGNORE *character_load_json(char *filename);
#endif // _CHARACTER_H_

View File

@@ -1,40 +0,0 @@
#ifndef _CONTROLLER_H_
#define _CONTROLLER_H_
#include <SDL3/SDL.h>
#include <sdlerror.h>
#define MAX_CONTROL_MAPS 8
#define MAX_CONTROLS 32
typedef struct {
Uint32 event_on;
Uint32 event_off;
Uint8 button;
SDL_Keycode key;
Uint8 axis;
Uint8 axis_range_min;
Uint8 axis_range_max;
ErrorContext ERROR_NOIGNORE *(*handler_on)(actor *obj, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *(*handler_off)(actor *obj, SDL_Event *event);
} SDL3GControl;
typedef struct {
actor *target;
SDL3GControl controls[MAX_CONTROLS];
SDL_KeyboardID kbid;
SDL_JoystickID jsid;
SDL_MouseID mouseid;
SDL_PenID penid;
} SDL3GControlMap;
extern SDL3GControlMap GAME_ControlMaps[MAX_CONTROL_MAPS];
ErrorContext ERROR_NOIGNORE *controller_handle_event(void *appstate, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *controller_handle_button_down(void *appstate, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *controller_handle_button_up(void *appstate, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *controller_handle_added(void *appstate, SDL_Event *event);
ErrorContext ERROR_NOIGNORE *controller_handle_removed(void *appstate, SDL_Event *event);
#endif // _CONTROLLER_H_

View File

@@ -1,6 +0,0 @@
#ifndef _DRAW_H_
#define _DRAW_H_
void GAME_draw_background(int w, int h);
#endif //_DRAW_H_

View File

@@ -1,46 +0,0 @@
#ifndef _GAME_H_
#define _GAME_H_
#include <SDL3_mixer/SDL_mixer.h>
#include "tilemap.h"
/* ==================== GAME STATE VARIABLES =================== */
typedef struct {
float w;
float h;
SDL_Texture *texture;
} GAME_frame;
typedef struct {
int flags;
} GameState;
typedef struct {
char name[256];
char version[32];
char uri[256];
int screenwidth;
int screenheight;
GameState state;
MIX_Mixer *mixer;
MIX_Track *tracks[64];
} Game;
#define GAME_AUDIO_TRACK_BGM 1
extern SDL_Window *window;
extern SDL_Renderer *renderer;
extern tilemap gamemap;
extern MIX_Audio *bgm;
extern SDL_FRect camera;
extern Game game;
#define BITMASK_HAS(x, y) (x & y) == y
#define BITMASK_ADD(x, y) x |= y
#define BITMASK_DEL(x, y) x &= ~(y)
#define BITMASK_CLEAR(x) x = 0;
ErrorContext ERROR_NOIGNORE *GAME_init();
#endif //_GAME_H_

View File

@@ -1,35 +0,0 @@
#ifndef _HEAP_H_
#define _HEAP_H_
#include "sprite.h"
#include "actor.h"
#include "character.h"
#include "staticstring.h"
#include <sdlerror.h>
#define MAX_HEAP_ACTOR 64
#define MAX_HEAP_SPRITE (MAX_HEAP_ACTOR * 16)
#define MAX_HEAP_SPRITESHEET MAX_HEAP_SPRITE
#define MAX_HEAP_CHARACTER 256
#define MAX_HEAP_STRING 256
extern actor HEAP_ACTOR[MAX_HEAP_ACTOR];
extern sprite HEAP_SPRITE[MAX_HEAP_SPRITE];
extern spritesheet HEAP_SPRITESHEET[MAX_HEAP_SPRITESHEET];
extern character HEAP_CHARACTER[MAX_HEAP_CHARACTER];
extern string HEAP_STRING[MAX_HEAP_STRING];
ErrorContext ERROR_NOIGNORE *heap_init();
ErrorContext ERROR_NOIGNORE *heap_next_actor(actor **dest);
ErrorContext ERROR_NOIGNORE *heap_next_sprite(sprite **dest);
ErrorContext ERROR_NOIGNORE *heap_next_spritesheet(spritesheet **dest);
ErrorContext ERROR_NOIGNORE *heap_next_character(character **dest);
ErrorContext ERROR_NOIGNORE *heap_next_string(string **dest);
ErrorContext ERROR_NOIGNORE *heap_release_actor(actor *ptr);
ErrorContext ERROR_NOIGNORE *heap_release_sprite(sprite *ptr);
ErrorContext ERROR_NOIGNORE *heap_release_spritesheet(spritesheet *ptr);
ErrorContext ERROR_NOIGNORE *heap_release_character(character *ptr);
ErrorContext ERROR_NOIGNORE *heap_release_string(string *ptr);
#endif //_HEAP_H_

View File

@@ -1,43 +0,0 @@
#ifndef _ITERATOR_H_
#define _ITERATOR_H_
typedef struct {
int flags;
int layerid;
} iterator;
#define ITERATOR_OP_UPDATE 1 // 1
#define ITERATOR_OP_RENDER 1 << 1 // 2
#define ITERATOR_OP_RELEASE 1 << 2 // 4
#define ITERATOR_OP_LAYERMASK 1 << 3 // 8
#define ITERATOR_OP_UNDEFINED_4 1 << 4 // 16
#define ITERATOR_OP_UNDEFINED_5 1 << 5 // 32
#define ITERATOR_OP_UNDEFINED_6 1 << 6 // 64
#define ITERATOR_OP_UNDEFINED_7 1 << 7 // 128
#define ITERATOR_OP_UNDEFINED_8 1 << 8 // 256
#define ITERATOR_OP_UNDEFINED_9 1 << 9 // 512
#define ITERATOR_OP_UNDEFINED_10 1 << 10 // 1024
#define ITERATOR_OP_UNDEFINED_11 1 << 11 // 2048
#define ITERATOR_OP_UNDEFINED_12 1 << 12 // 4096
#define ITERATOR_OP_UNDEFINED_13 1 << 13 // 8192
#define ITERATOR_OP_UNDEFINED_14 1 << 14 // 16384
#define ITERATOR_OP_UNDEFINED_15 1 << 15 // 32768
#define ITERATOR_OP_UNDEFINED_16 1 << 16 // 65536
#define ITERATOR_OP_UNDEFINED_17 1 << 17 // 131072
#define ITERATOR_OP_UNDEFINED_18 1 << 18 // 262144
#define ITERATOR_OP_UNDEFINED_19 1 << 19 // 524288
#define ITERATOR_OP_UNDEFINED_20 1 << 20 // 1048576
#define ITERATOR_OP_UNDEFINED_21 1 << 21 // 2097152
#define ITERATOR_OP_UNDEFINED_22 1 << 22 // 4194304
#define ITERATOR_OP_UNDEFINED_23 1 << 23 // 8388608
#define ITERATOR_OP_UNDEFINED_24 1 << 24 // 16777216
#define ITERATOR_OP_UNDEFINED_25 1 << 25 // 33554432
#define ITERATOR_OP_UNDEFINED_26 1 << 26 // 67108864
#define ITERATOR_OP_UNDEFINED_27 1 << 27 // 134217728
#define ITERATOR_OP_UNDEFINED_28 1 << 28 // 268435456
#define ITERATOR_OP_UNDEFINED_29 1 << 29 // 536870912
#define ITERATOR_OP_UNDEFINED_30 1 << 30 // 1073741824
#define ITERATOR_OP_UNDEFINED_31 1 << 31 // 2147483648
#endif // _ITERATOR_H_

View File

@@ -1,17 +0,0 @@
#ifndef _JSON_HELPERS_H_
#define _JSON_HELPERS_H_
#include <sdlerror.h>
#include "staticstring.h"
ErrorContext ERROR_NOIGNORE *get_json_object_value(json_t *obj, char *key, json_t **dest);
ErrorContext ERROR_NOIGNORE *get_json_boolean_value(json_t *obj, char *key, bool *dest);
ErrorContext ERROR_NOIGNORE *get_json_integer_value(json_t *obj, char *key, int *dest);
ErrorContext ERROR_NOIGNORE *get_json_number_value(json_t *obj, char *key, float *dest);
ErrorContext ERROR_NOIGNORE *get_json_string_value(json_t *obj, char *key, string **dest);
ErrorContext ERROR_NOIGNORE *get_json_array_value(json_t *obj, char *key, json_t **dest);
ErrorContext ERROR_NOIGNORE *get_json_array_index_object(json_t *array, int index, json_t **dest);
ErrorContext ERROR_NOIGNORE *get_json_array_index_integer(json_t *array, int index, int *dest);
ErrorContext ERROR_NOIGNORE *get_json_array_index_string(json_t *array, int index, string **dest);
#endif // _JSON_HELPERS_H_

View File

@@ -1,20 +0,0 @@
#ifndef _REGISTRY_H_
#define _REGISTRY_H_
#include "error.h"
extern SDL_PropertiesID REGISTRY_ACTOR;
extern SDL_PropertiesID REGISTRY_SPRITE;
extern SDL_PropertiesID REGISTRY_SPRITESHEET;
extern SDL_PropertiesID REGISTRY_CHARACTER;
extern SDL_PropertiesID REGISTRY_ACTOR_STATE_STRINGS;
ErrorContext ERROR_NOIGNORE *registry_init();
ErrorContext ERROR_NOIGNORE *registry_init_actor();
ErrorContext ERROR_NOIGNORE *registry_init_sprite();
ErrorContext ERROR_NOIGNORE *registry_init_spritesheet();
ErrorContext ERROR_NOIGNORE *registry_init_character();
ErrorContext ERROR_NOIGNORE *registry_init_actor_state_strings();
#endif //_REGISTRY_H_

View File

@@ -1,43 +0,0 @@
#ifndef _SPRITE_H_
#define _SPRITE_H_
#include <SDL3/SDL_properties.h>
#include <sdlerror.h>
#define SPRITE_MAX_FRAMES 16
#define SPRITE_MAX_NAME_LENGTH 128
#define SPRITE_MAX_REGISTRY_SIZE 1024
#define SPRITE_SHEET_MAX_FILENAME_LENGTH 512
#define MAX_HEAP_SPRITE (MAX_HEAP_ACTOR * 16)
#define MAX_HEAP_SPRITESHEET MAX_HEAP_SPRITE
typedef struct {
int refcount;
SDL_Texture *texture;
char name[SPRITE_SHEET_MAX_FILENAME_LENGTH];
int sprite_w;
int sprite_h;
} spritesheet;
typedef struct {
int refcount;
spritesheet *sheet;
int frameids[SPRITE_MAX_FRAMES]; // which IDs on the spritesheet belong to our frames
int frames; // how many frames are in this animation
int width;
int height;
int speed; // how many milliseconds a given sprite frame should be visible before cycling
bool loop; // when this sprite is done playing, it should immediately start again
bool loopReverse; // when this sprite is done playing, it should go in reverse order through its frames
char name[SPRITE_MAX_NAME_LENGTH];
} sprite;
// initializes a new sprite to use the given sheet and otherwise sets to zero
ErrorContext ERROR_NOIGNORE *sprite_initialize(sprite *spr, char *name, spritesheet *sheet);
// loads a given image file into a new spritesheet
ErrorContext ERROR_NOIGNORE *spritesheet_initialize(spritesheet *sheet, int sprite_w, int sprite_h, char *filename);
ErrorContext ERROR_NOIGNORE *sprite_load_json(char *filename);
#endif //_SPRITE_H_

View File

@@ -1,17 +0,0 @@
#ifndef _STRING_H_
#define _STRING_H_
#include "string.h"
#include <sdlerror.h>
#define MAX_STRING_LENGTH 256
typedef struct
{
int refcount;
char data[MAX_STRING_LENGTH];
} string;
ErrorContext ERROR_NOIGNORE *string_initialize(string *obj, char *init);
#endif //_STRING_H_

View File

@@ -1,112 +0,0 @@
#ifndef _TILEMAP_H_
#define _TILEMAP_H_
#include "actor.h"
#include "staticstring.h"
#include <jansson.h>
#define TILEMAP_MAX_WIDTH 512
#define TILEMAP_MAX_HEIGHT 512
#define TILEMAP_MAX_LAYERS 16
#define TILEMAP_MAX_TILESETS 16
#define TILEMAP_MAX_TILES_PER_IMAGE 65536
#define TILEMAP_MAX_TILESET_NAME_SIZE 512
#define TILEMAP_MAX_TILESET_FILENAME_SIZE 512
#define TILEMAP_MAX_OBJECT_NAME_SIZE 512
#define TILEMAP_MAX_OBJECTS_PER_LAYER 128
#define TILEMAP_OBJECT_TYPE_ACTOR 1
#define TILEMAP_LAYER_TYPE_TILES 1
#define TILEMAP_LAYER_TYPE_OBJECTS 2
typedef struct {
float x;
float y;
int gid;
int id;
int height;
int width;
int rotation;
int type;
bool visible;
actor *actorptr;
char name[TILEMAP_MAX_OBJECT_NAME_SIZE];
} tilemap_object;
typedef struct {
short type;
float opacity;
bool visible;
int height;
int width;
int x;
int y;
int id;
int data[TILEMAP_MAX_WIDTH * TILEMAP_MAX_HEIGHT];
tilemap_object objects[TILEMAP_MAX_OBJECTS_PER_LAYER];
} tilemap_layer;
typedef struct {
int columns;
int firstgid;
char imagefilename[TILEMAP_MAX_TILESET_FILENAME_SIZE];
int imageheight;
int imagewidth;
char name[TILEMAP_MAX_TILESET_NAME_SIZE];
SDL_Texture *texture;
// Use this as a lookup table instead of storing tiles
// in individual textures to blit them from a single
// texture at runtime
// FIXME: This is probably not very efficient. For a map
// with a single tileset it makes sense. For a map with
// multiple tilesets you may have set A start at firstgid 1
// and have 1728 tiles. Set B may start at firstgid 1729 and
// have 1728 more tiles. This means Set B has 1728 empty
// tile_offsets[] entries before firstgid 1729 because of the
// way tilemap_load_tilesets() works. This is really inefficient
// and should be improved in the future, and will eventually
// lead to premature exhaustion of TILEMAP_MAX_TILES_PER_IMAGE
// because set D or E may only have 64 tiles but they may be
// at the upper end of the array bound already because of this.
int tile_offsets[TILEMAP_MAX_TILES_PER_IMAGE][2];
int tilecount;
int tileheight;
int tilewidth;
int spacing;
int margin;
} tileset;
typedef struct {
int tilewidth;
int tileheight;
int width;
int height;
int numlayers;
int orientation; // 0 = orthogonal, 1 = isometric
int numtilesets;
tileset tilesets[TILEMAP_MAX_TILESETS];
tilemap_layer layers[TILEMAP_MAX_LAYERS];
} tilemap;
ErrorContext ERROR_NOIGNORE *tilemap_load(char *fname, tilemap *dest);
ErrorContext ERROR_NOIGNORE *tilemap_draw(SDL_Renderer *renderer, tilemap *dest, SDL_FRect *viewport, int layeridx);
ErrorContext ERROR_NOIGNORE *tilemap_draw_tileset(SDL_Renderer *renderer, tilemap *dest, int tilesetidx);
/*
* These functions are part of the internal API and should not be called by the user.
* They are only exposed here for unit testing.
*/
ErrorContext ERROR_NOIGNORE *get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest);
ErrorContext ERROR_NOIGNORE *get_json_properties_string(json_t *obj, char *key, string **dest);
ErrorContext ERROR_NOIGNORE *get_json_properties_integer(json_t *obj, char *key, int *dest);
ErrorContext ERROR_NOIGNORE *tilemap_compute_tileset_offsets(tilemap *dest, int tilesetidx);
ErrorContext ERROR_NOIGNORE *tilemap_load_layer_objects(tilemap *dest, json_t *root, int layerid);
ErrorContext ERROR_NOIGNORE *tilemap_load_layer_tile(tilemap *dest, json_t *root, int layerid);
ErrorContext ERROR_NOIGNORE *tilemap_load_layers(tilemap *dest, json_t *root);
ErrorContext ERROR_NOIGNORE *tilemap_load_tilesets_each(json_t *tileset, tilemap *dest, int tsidx);
ErrorContext ERROR_NOIGNORE *tilemap_load_tilesets(tilemap *dest, json_t *root);
#endif //_TILEMAP_H_

View File

@@ -1,29 +0,0 @@
#ifndef _UTIL_H_
#define _UTIL_H_
#include <sdlerror.h>
typedef struct point {
int x;
int y;
int z;
} point;
typedef struct RectanglePoints {
point topleft;
point topright;
point bottomleft;
point bottomright;
} RectanglePoints;
#define COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x)
ErrorContext ERROR_NOIGNORE *rectangle_points(RectanglePoints *dest, SDL_FRect *rect);
ErrorContext ERROR_NOIGNORE *collide_point_rectangle(point *p, RectanglePoints *r, bool *collide);
ErrorContext ERROR_NOIGNORE *collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);
// These are REALLY slow routines that are only useful in testing harnesses
ErrorContext ERROR_NOIGNORE *compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2);
ErrorContext ERROR_NOIGNORE *render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout);
#endif // _UTIL_H_

32
mkcontrollermappings.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
rootdir=$1
curl https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt | grep -v '^#' | grep -v '^$' | sed s/',$'//g > mappings.txt
filelen=$(wc -l mappings.txt | cut -d ' ' -f 1)
cat > ${rootdir}/include/akgl/SDL_GameControllerDB.h <<EOF
#ifndef _SDL_GAMECONTROLLERDB_H_
#define _SDL_GAMECONTROLLERDB_H_
// Taken from https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt on $(date)
#define AKGL_SDL_GAMECONTROLLER_DB_LEN ${filelen}
const char *SDL_GAMECONTROLLER_DB[] = {
EOF
counter=0
cat mappings.txt | while read LINE;
do
if [[ $counter -gt 0 ]]; then
printf ",\n" >> ${rootdir}/include/akgl/SDL_GameControllerDB.h;
fi
printf " \"${LINE}\"" >> ${rootdir}/include/akgl/SDL_GameControllerDB.h;
counter=$((counter + 1))
done
printf "\n};\n" >> ${rootdir}/include/akgl/SDL_GameControllerDB.h
printf "#endif // _SDL_GAMECONTROLLERDB_H_\n" >> ${rootdir}/include/akgl/SDL_GameControllerDB.h

11
rebuild.sh Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
export CMAKE_PREFIX_PATH=/home/andrew/local:/home/andrew/local/lib/cmake
export CMAKE_MODULE_PATH=/home/andrew/local/lib/cmake
#export SDL3_DIR=/home/andrew/local
rm -fr ~/local/lib/*akgl* ~/local/include/akgl build
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build --parallel 4
cmake --install build --prefix /home/andrew/local

466
scripts/mutation_test.py Executable file
View 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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
.replace('"', "&quot;"))
def write_junit(path, records, targets):
"""Write a JUnit XML report. One <testcase> per mutant; a surviving mutant
is a <failure> (test-suite gap), a killed mutant is a passing case."""
by_file = {t: [] for t in targets}
for m, result in records:
by_file.setdefault(m.path, []).append((m, result))
total = len(records)
total_fail = sum(1 for _, r in records if r == "survived")
out = ['<?xml version="1.0" encoding="UTF-8"?>',
f'<testsuites name="mutation" tests="{total}" failures="{total_fail}">']
for f, items in by_file.items():
if not items:
continue
fails = sum(1 for _, r in items if r == "survived")
out.append(f' <testsuite name="mutation:{_xml_escape(f)}" '
f'tests="{len(items)}" failures="{fails}">')
for m, result in items:
name = _xml_escape(m.describe())
cls = "mutation." + _xml_escape(m.path)
if result == "survived":
detail = _xml_escape(f"{m.before.strip()} -> {m.after.strip()}")
out.append(f' <testcase name="{name}" classname="{cls}" time="0">')
out.append(f' <failure message="survived mutant '
f'({_xml_escape(m.op)} at {_xml_escape(m.path)}:'
f'{m.lineno})">{detail}</failure>')
out.append(' </testcase>')
else:
out.append(f' <testcase name="{name}" classname="{cls}" '
f'time="0"><system-out>{_xml_escape(result)}'
'</system-out></testcase>')
out.append(' </testsuite>')
out.append('</testsuites>')
with open(path, "w") as fh:
fh.write("\n".join(out) + "\n")
def copy_tree(src, dst):
ignore = shutil.ignore_patterns("build", ".git", "*.o", "*.so", "*~",
"#*#", "*.iso")
shutil.copytree(src, dst, ignore=ignore, symlinks=True)
def read_lines(path):
with open(path) as fh:
return fh.readlines()
def write_lines(path, lines):
with open(path, "w") as fh:
fh.writelines(lines)
def main():
# Line-buffer stdout so progress is visible live under CI / the cmake target.
try:
sys.stdout.reconfigure(line_buffering=True)
except (AttributeError, ValueError):
pass
here = os.path.dirname(os.path.abspath(__file__))
default_root = os.path.dirname(here)
ap = argparse.ArgumentParser(description="Mutation testing for libakgl")
ap.add_argument("--source-root", default=default_root)
ap.add_argument("--target", action="append", default=None)
ap.add_argument("--work", default=None)
ap.add_argument("--timeout", type=int, default=120)
ap.add_argument("--exclude-test", default="^character$",
help="CTest regex to exclude (default: ^character$)")
ap.add_argument("--threshold", type=float, default=0.0)
ap.add_argument("--junit", default=None,
help="write a JUnit XML report to this path")
ap.add_argument("--max-mutants", type=int, default=0,
help="cap the run at N evenly-sampled mutants (0 = all)")
ap.add_argument("--list", action="store_true")
ap.add_argument("--keep", action="store_true")
ap.add_argument("-j", type=int, default=1)
args = ap.parse_args()
root = os.path.abspath(args.source_root)
targets = args.target or [
"src/actor.c", "src/actor_state_string_names.c", "src/assets.c",
"src/character.c", "src/controller.c", "src/draw.c",
"src/game.c", "src/heap.c", "src/json_helpers.c",
"src/physics.c", "src/registry.c", "src/renderer.c",
"src/sprite.c", "src/staticstring.c", "src/text.c",
"src/tilemap.c", "src/util.c",
]
# Enumerate mutants from the pristine sources.
all_mutants = []
for t in targets:
all_mutants.extend(generate_mutants(root, t))
print(f"Generated {len(all_mutants)} mutants across {len(targets)} file(s):")
for t in targets:
n = sum(1 for m in all_mutants if m.path == t)
print(f" {t}: {n}")
# Optional even-strided sampling to bound run time (CI / smoke tests).
if args.max_mutants and len(all_mutants) > args.max_mutants:
step = len(all_mutants) / args.max_mutants
sampled = [all_mutants[int(i * step)] for i in range(args.max_mutants)]
print(f"Sampling {len(sampled)} of {len(all_mutants)} mutants "
f"(--max-mutants {args.max_mutants}).")
all_mutants = sampled
if args.list:
for m in all_mutants:
print(" " + m.describe())
return 0
if not all_mutants:
print("No mutants generated; nothing to do.")
return 0
# Scratch working copy.
work_parent = args.work or tempfile.mkdtemp(prefix="akgl_mut_")
work = os.path.join(work_parent, "src") if args.work else work_parent
if os.path.exists(work):
shutil.rmtree(work)
print(f"\nCopying sources to scratch dir: {work}")
copy_tree(root, work)
runner = Runner(work, args.timeout, args.exclude_test)
print("Configuring baseline ...")
ok, out = runner.configure()
if not ok:
sys.stderr.write(out.decode(errors="replace"))
sys.stderr.write("\nBaseline configure FAILED; aborting.\n")
return 2
print("Verifying baseline is green (no mutation) ...")
baseline = runner.build_and_test()
if baseline != "survived":
sys.stderr.write(f"Baseline is not green ({baseline}); aborting. "
"Fix the suite before mutation testing.\n")
return 2
print("Baseline OK.\n")
# Group mutants by file so we mutate one file at a time and restore it.
killed = {"killed-compile": 0, "killed-test": 0, "killed-timeout": 0}
survivors = []
records = []
total = len(all_mutants)
# Cache pristine contents per target.
pristine = {t: read_lines(os.path.join(work, t)) for t in targets}
for idx, m in enumerate(all_mutants, start=1):
tgt_abs = os.path.join(work, m.path)
lines = list(pristine[m.path])
lines[m.lineno - 1] = m.after
write_lines(tgt_abs, lines)
try:
result = runner.build_and_test()
finally:
write_lines(tgt_abs, pristine[m.path]) # always restore
records.append((m, result))
if result == "survived":
survivors.append(m)
mark = "SURVIVED"
else:
killed[result] += 1
mark = result.upper()
print(f"[{idx}/{total}] {mark:16} {m.describe()}")
total_killed = sum(killed.values())
score = 100.0 * total_killed / total if total else 100.0
print("\n" + "=" * 72)
print("MUTATION TESTING SUMMARY")
print("=" * 72)
print(f" total mutants : {total}")
print(f" killed (test) : {killed['killed-test']}")
print(f" killed (compile): {killed['killed-compile']}")
print(f" killed (timeout): {killed['killed-timeout']}")
print(f" survived : {len(survivors)}")
print(f" mutation score : {score:.1f}%")
if survivors:
print("\nSurviving mutants (test-suite gaps -- turn these into tests):")
for m in survivors:
print(" " + m.describe())
if args.junit:
junit_path = os.path.abspath(args.junit)
write_junit(junit_path, records, targets)
print(f"\nJUnit report written to: {junit_path}")
if not args.keep and not args.work:
shutil.rmtree(work_parent, ignore_errors=True)
else:
print(f"\nScratch working copy kept at: {work}")
if args.threshold > 0 and score < args.threshold:
print(f"\nFAIL: mutation score {score:.1f}% < threshold {args.threshold:.1f}%")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,38 +1,45 @@
/**
* @file actor.c
* @brief Implements the actor subsystem.
*/
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <string.h> #include <string.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/game.h> #include <akgl/physics.h>
#include <sdl3game/sprite.h> #include <akgl/game.h>
#include <sdl3game/actor.h> #include <akgl/sprite.h>
#include <sdl3game/heap.h> #include <akgl/actor.h>
#include <sdl3game/registry.h> #include <akgl/heap.h>
#include <sdl3game/staticstring.h> #include <akgl/registry.h>
#include <sdl3game/iterator.h> #include <akgl/staticstring.h>
#include <akgl/iterator.h>
ErrorContext *actor_initialize(actor *obj, char *name) akerr_ErrorContext *akgl_actor_initialize(akgl_Actor *obj, char *name)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "actor_initialize received null actor pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "akgl_actor_initialize received null actor pointer");
FAIL_ZERO_RETURN(errctx, name, ERR_NULLPOINTER, "actor_initialize received null name string pointer"); FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "akgl_actor_initialize received null name string pointer");
memset(obj, 0x00, sizeof(actor)); memset(obj, 0x00, sizeof(akgl_Actor));
strncpy((char *)obj->name, name, ACTOR_MAX_NAME_LENGTH); strncpy((char *)obj->name, name, AKGL_ACTOR_MAX_NAME_LENGTH);
obj->curSpriteReversing = false; obj->curSpriteReversing = false;
obj->scale = 1.0;
obj->movement_controls_face = true; obj->movement_controls_face = true;
obj->updatefunc = &actor_update; obj->updatefunc = &akgl_actor_update;
obj->renderfunc = &actor_render; obj->renderfunc = &akgl_actor_render;
obj->facefunc = &actor_automatic_face; obj->facefunc = &akgl_actor_automatic_face;
obj->movementlogicfunc = &actor_logic_movement; obj->movementlogicfunc = &akgl_actor_logic_movement;
obj->changeframefunc = &actor_logic_changeframe; obj->changeframefunc = &akgl_actor_logic_changeframe;
obj->addchild = &actor_add_child; obj->addchild = &akgl_actor_add_child;
FAIL_ZERO_RETURN( FAIL_ZERO_RETURN(
errctx, errctx,
SDL_SetPointerProperty(REGISTRY_ACTOR, name, (void *)obj), SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj),
ERR_KEY, AKERR_KEY,
"Unable to add actor to registry" "Unable to add actor to registry"
); );
obj->refcount += 1; obj->refcount += 1;
@@ -40,34 +47,38 @@ ErrorContext *actor_initialize(actor *obj, char *name)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *actor_set_character(actor *obj, char *basecharname) akerr_ErrorContext *akgl_actor_set_character(akgl_Actor *obj, char *basecharname)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "Null actor reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
FAIL_ZERO_RETURN(errctx, basecharname, ERR_NULLPOINTER, "Null character reference"); FAIL_ZERO_RETURN(errctx, basecharname, AKERR_NULLPOINTER, "Null character reference");
obj->basechar = SDL_GetPointerProperty(REGISTRY_CHARACTER, basecharname, NULL); obj->basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, basecharname, NULL);
FAIL_ZERO_RETURN(errctx, obj->basechar, ERR_NULLPOINTER, "Character not found in the registry"); FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Character not found in the registry");
obj->ax = 0;
obj->ay = 0;
obj->sx = obj->basechar->sx;
obj->sy = obj->basechar->sy;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *actor_automatic_face(actor *obj) akerr_ErrorContext *akgl_actor_automatic_face(akgl_Actor *obj)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "Null actor reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
ATTEMPT { ATTEMPT {
if ( obj->movement_controls_face == true ) { if ( obj->movement_controls_face == true ) {
// TODO : This doesn't really work properly // TODO : This doesn't really work properly
BITMASK_DEL(obj->state, ACTOR_STATE_FACE_ALL); AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_FACE_ALL);
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_LEFT) ) { if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT) ) {
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_LEFT); AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_LEFT);
} else if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_RIGHT) ) { } else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_RIGHT); AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_RIGHT);
} else if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_UP) ) { } else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_UP); AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_UP);
} else if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_DOWN) ) { } else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_DOWN); AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_DOWN);
} }
} }
} CLEANUP { } CLEANUP {
@@ -76,10 +87,10 @@ ErrorContext *actor_automatic_face(actor *obj)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *actor_logic_changeframe(actor *obj, sprite *curSprite, SDL_Time curtimems) akerr_ErrorContext *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtime)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "Null actor reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
ATTEMPT { ATTEMPT {
// are we currently looping in reverse? // are we currently looping in reverse?
if ( curSprite->loop == true && obj->curSpriteReversing == true ) { if ( curSprite->loop == true && obj->curSpriteReversing == true ) {
@@ -91,13 +102,14 @@ ErrorContext *actor_logic_changeframe(actor *obj, sprite *curSprite, SDL_Time cu
obj->curSpriteFrameId -= 1; obj->curSpriteFrameId -= 1;
} }
// are we at the end of the animation? // are we at the end of the animation?
} else if ( obj->curSpriteFrameId == (curSprite->frames-1) ) { } else if ( obj->curSpriteFrameId >= (curSprite->frames-1) ) {
// are we set to loop in reverse? // are we set to loop in reverse?
if ( curSprite->loop == true && curSprite->loopReverse == true ) { if ( curSprite->loop == true && curSprite->loopReverse == true ) {
obj->curSpriteReversing = true; obj->curSpriteReversing = true;
obj->curSpriteFrameId -= 1; obj->curSpriteFrameId -= 1;
// are we set to loop forward? // are we set to loop forward?
} else if ( curSprite->loop == true ) { } else {
// we are at the end of the animation and we either loop forward or do not loop
obj->curSpriteFrameId = 0; obj->curSpriteFrameId = 0;
} }
// we are not looping in reverse and we are not at the end of the animation // we are not looping in reverse and we are not at the end of the animation
@@ -110,83 +122,79 @@ ErrorContext *actor_logic_changeframe(actor *obj, sprite *curSprite, SDL_Time cu
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *actor_logic_movement(actor *obj, SDL_Time curtimems) // raises AKGL_ERR_LOGICINTERRUPT if we don't want the physics simulator to process us
akerr_ErrorContext *akgl_actor_logic_movement(akgl_Actor *actor, float32_t dt)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "Null actor reference"); FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
if ( obj->parent != NULL ) { FAIL_ZERO_RETURN(errctx, actor->basechar, AKERR_NULLPOINTER, "actor->basechar");
// Children don't move independently of their parents, they just have an offset actor->sx = actor->basechar->sx;
SUCCEED_RETURN(errctx); actor->sy = actor->basechar->sy;
} else { actor->sz = actor->basechar->sz;
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_LEFT) ) { if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ) {
obj->x -= obj->basechar->vx; actor->ax = -actor->basechar->ax;
} } else if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_RIGHT) ) { actor->ax = actor->basechar->ax;
obj->x += obj->basechar->vx;
}
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_UP) ) {
obj->y -= obj->basechar->vy;
}
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_DOWN) ) {
obj->y += obj->basechar->vy;
} }
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
actor->ay = -actor->basechar->ay;
} else if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
actor->ay = actor->basechar->ay;
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *actor_update(actor *obj) akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
SDL_Time curtime = 0; SDL_Time curtime = 0;
SDL_Time curtimems = 0; akgl_Sprite *curSprite = NULL;
sprite *curSprite = NULL;
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor reference");
FAIL_ZERO_RETURN(errctx, obj->basechar, ERR_NULLPOINTER, "Actor has NULL base character reference"); FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
ATTEMPT {
SDL_GetCurrentTime(&curtime); SDL_GetCurrentTime(&curtime);
curtimems = curtime / 1000000; PASS(errctx, obj->facefunc(obj));
CATCH(errctx, obj->facefunc(obj));
// is it time to apply movement logic?
if ( (curtimems - obj->logictimer) >= obj->basechar->movementspeed ) {
CATCH(errctx, obj->movementlogicfunc(obj, curtimems));
obj->logictimer = curtimems;
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, false);
ATTEMPT { ATTEMPT {
CATCH(errctx, character_sprite_get(obj->basechar, obj->state, &curSprite)); CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
// is it time to change frames? if ( ((curtime) - obj->curSpriteFrameTimer) >= curSprite->speed) {
if ( (curtimems - obj->curSpriteFrameTimer) >= curSprite->speed ) { CATCH(errctx, obj->changeframefunc(obj, curSprite, curtime));
CATCH(errctx, obj->changeframefunc(obj, curSprite, curtimems)); obj->curSpriteFrameTimer = curtime;
obj->curSpriteFrameTimer = curtimems;
} }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_KEY) { } HANDLE(errctx, AKERR_KEY) {
// TODO : Why are we passing this error? It could only come from akgl_character_sprite_get
// or changeframefunc, both of which should never return AKERR_KEY...
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
static ErrorContext *actor_visible(actor *obj, SDL_FRect *camera, bool *visible) /**
* @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); PREPARE_ERROR(errctx);
sprite *curSprite = NULL; akgl_Sprite *curSprite = NULL;
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, renderer, ERR_NULLPOINTER, "NULL renderer"); FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
FAIL_ZERO_RETURN(errctx, obj->basechar, ERR_NULLPOINTER, "Actor has NULL base character reference");
ATTEMPT { ATTEMPT {
CATCH(errctx, character_sprite_get(obj->basechar, obj->state, &curSprite)); CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_KEY) { } HANDLE(errctx, AKERR_KEY) {
// TODO: Actor has no sprite matching the current state. Should we treat this as an error and throw? // TODO: Actor has no sprite matching the current state. Should we treat this as an error and throw?
*visible = false; *visible = false;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -203,24 +211,24 @@ static ErrorContext *actor_visible(actor *obj, SDL_FRect *camera, bool *visible)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *actor_render(actor *obj, SDL_Renderer *renderer) akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
sprite *curSprite = NULL; akgl_Sprite *curSprite = NULL;
bool visible = false; bool visible = false;
SDL_FRect src; SDL_FRect src;
SDL_FRect dest; SDL_FRect dest;
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, renderer, ERR_NULLPOINTER, "NULL renderer"); FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
FAIL_ZERO_RETURN(errctx, obj->basechar, ERR_NULLPOINTER, "Actor has NULL base character reference");
ATTEMPT { ATTEMPT {
CATCH(errctx, character_sprite_get(obj->basechar, obj->state, &curSprite)); CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
CATCH(errctx, actor_visible(obj, &camera, &visible)); CATCH(errctx, actor_visible(obj, camera, &visible));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_KEY) { } HANDLE(errctx, AKERR_KEY) {
} HANDLE_GROUP(errctx, AKERR_OUTOFBOUNDS) {
// If an actor doesn't have a sprite for a state, just log it and move on // If an actor doesn't have a sprite for a state, just log it and move on
LOG_ERROR(errctx); LOG_ERROR(errctx);
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -229,38 +237,47 @@ ErrorContext *actor_render(actor *obj, SDL_Renderer *renderer)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
src.x = curSprite->width * curSprite->frameids[obj->curSpriteFrameId]; if ( (obj->curSpriteFrameId > curSprite->frames) ) {
if ( src.x >= curSprite->sheet->texture->w ) { // This isn't necessarily an error - this actor's frame index is outside the range of
src.y = ((int)src.x / curSprite->sheet->texture->w) * curSprite->height; // their current sprite. There are a number of reasons this could happen, and it will
src.x = ((int)src.x % curSprite->sheet->texture->w); // get cleaned up on the next logic update. Just pass on rendering them this frame.
} else { SUCCEED_RETURN(errctx);
src.y = 0;
} }
src.w = curSprite->width;
src.h = curSprite->height;
if ( obj->parent != NULL ) {
dest.x = (obj->parent->x + obj->x - camera.x);
dest.y = (obj->parent->y + obj->y - camera.y);
} else {
dest.x = (obj->x - camera.x);
dest.y = (obj->y - camera.y);
}
dest.w = curSprite->width;
dest.h = curSprite->width;
SDL_RenderTexture(renderer, curSprite->sheet->texture, &src, &dest); ATTEMPT {
CATCH(errctx,
akgl_sprite_sheet_coords_for_frame(
curSprite,
&src,
obj->curSpriteFrameId)
);
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
if ( obj->parent != NULL ) {
dest.x = (obj->parent->x + obj->x - camera->x);
dest.y = (obj->parent->y + obj->y - camera->y);
} else {
dest.x = (obj->x - camera->x);
dest.y = (obj->y - camera->y);
}
dest.w = curSprite->width * obj->scale;
dest.h = curSprite->width * obj->scale;
PASS(errctx, renderer->draw_texture(renderer, curSprite->sheet->texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *actor_add_child(actor *obj, actor *child) akerr_ErrorContext *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child)
{ {
int i = 0; int i = 0;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL parent pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL parent pointer");
FAIL_ZERO_RETURN(errctx, child, ERR_NULLPOINTER, "NULL child pointer"); FAIL_ZERO_RETURN(errctx, child, AKERR_NULLPOINTER, "NULL child pointer");
FAIL_NONZERO_RETURN(errctx, child->parent, ERR_RELATIONSHIP, "Child object already has a parent"); FAIL_NONZERO_RETURN(errctx, child->parent, AKERR_RELATIONSHIP, "Child object already has a parent");
for ( i = 0; i < ACTOR_MAX_CHILDREN ; i++ ) { for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN ; i++ ) {
if ( obj->children[i] == NULL ) { if ( obj->children[i] == NULL ) {
obj->children[i] = child; obj->children[i] = child;
child->parent = obj; child->parent = obj;
@@ -268,153 +285,151 @@ ErrorContext *actor_add_child(actor *obj, actor *child)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
} }
FAIL_RETURN(errctx, ERR_OUTOFBOUNDS, "Parent object has no remaining child slots left"); FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
} }
// SDL iterator so we can't return error information here, void only void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name)
// this means we don't have anywhere to send exceptions up to, so if we hit an error, we log and exit(1) here
void registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
iterator *opflags = (iterator *)userdata; akgl_Iterator *opflags = (akgl_Iterator *)userdata;
akgl_Actor *obj = NULL;
ATTEMPT { ATTEMPT {
FAIL_ZERO_BREAK(errctx, name, ERR_NULLPOINTER, "registry_iterate_actor received NULL property name"); FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "registry_iterate_actor received NULL property name");
FAIL_ZERO_BREAK(errctx, opflags, ERR_NULLPOINTER, "received NULL iterator flags"); FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "received NULL iterator flags");
actor *obj = (actor *)SDL_GetPointerProperty(registry, name, NULL); obj = SDL_GetPointerProperty(registry, name, NULL);
FAIL_ZERO_BREAK(errctx, obj, ERR_KEY, "registry_iterate_actor received property name that was not in the registry"); FAIL_ZERO_BREAK(errctx, obj, AKERR_KEY, "registry_iterate_actor received property name that was not in the registry");
if ( BITMASK_HAS(opflags->flags, ITERATOR_OP_LAYERMASK) ) { if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) && obj->layer != opflags->layerid) {
if ( obj->layer != opflags->layerid ) {
break; break;
} }
} if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_UPDATE)) {
if ( BITMASK_HAS(opflags->flags, ITERATOR_OP_UPDATE) ) {
CATCH(errctx, obj->updatefunc(obj)); CATCH(errctx, obj->updatefunc(obj));
} }
if ( BITMASK_HAS(opflags->flags, ITERATOR_OP_RENDER) ) { if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE)) {
CATCH(errctx, obj->renderfunc(obj, renderer)); 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 { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);
} }
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_left_on(actor *obj, SDL_Event *event) akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "actor");
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_LEFT) ) { FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
SUCCEED_RETURN(errctx); //SDL_Log("event %d (button %d / key %d) moves actor left", event->type, event->gbutton.which, event->key.key);
} AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
SDL_Log("event %d (button %d / key %d) moves actor left", event->type, event->gbutton.which, event->key.key); obj->ax = -(obj->basechar->ax);
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL)); AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT));
BITMASK_ADD(obj->state, (ACTOR_STATE_MOVING_LEFT | ACTOR_STATE_FACE_LEFT)); //SDL_Log("new target actor state: %b", obj->state);
SDL_Log("new target actor state: %b", obj->state);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_left_off(actor *obj, SDL_Event *event) akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
if ( !BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_LEFT) ) { //SDL_Log("event %d (button %d / key %d) stops moving actor left", event->type, event->gbutton.which, event->key.key);
SUCCEED_RETURN(errctx); obj->ax = 0;
} obj->ex = 0;
SDL_Log("event %d (button %d / key %d) stops moving actor left", event->type, event->gbutton.which, event->key.key); obj->tx = 0;
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL)); obj->vx = 0;
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_LEFT); AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT);
SDL_Log("new target actor state: %b", obj->state); //SDL_Log("new target actor state: %b", obj->state);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_right_on(actor *obj, SDL_Event *event) akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_RIGHT) ) { FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
SUCCEED_RETURN(errctx); //SDL_Log("event %d (button %d / key %d) moves actor right", event->type, event->gbutton.which, event->key.key);
} obj->ax = obj->basechar->ax;
SDL_Log("event %d (button %d / key %d) moves actor right", event->type, event->gbutton.which, event->key.key); AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL)); AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT));
BITMASK_ADD(obj->state, (ACTOR_STATE_MOVING_RIGHT | ACTOR_STATE_FACE_RIGHT)); //SDL_Log("new target actor state: %b", obj->state);
SDL_Log("new target actor state: %b", obj->state);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_right_off(actor *obj, SDL_Event *event) akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
if ( !BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_RIGHT) ) { //SDL_Log("event %d (button %d / key %d) stops moving actor right", event->type, event->gbutton.which, event->key.key);
SUCCEED_RETURN(errctx); obj->ax = 0;
} obj->ex = 0;
SDL_Log("event %d (button %d / key %d) stops moving actor right", event->type, event->gbutton.which, event->key.key); obj->tx = 0;
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL)); obj->vx = 0;
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_RIGHT); AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
SDL_Log("new target actor state: %b", obj->state); //SDL_Log("new target actor state: %b", obj->state);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_up_on(actor *obj, SDL_Event *event) akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_UP) ) { FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
SUCCEED_RETURN(errctx); //SDL_Log("event %d (button %d / key %d) moves actor up", event->type, event->gbutton.which, event->key.key);
} obj->ay = -(obj->basechar->ay);
SDL_Log("event %d (button %d / key %d) moves actor up", event->type, event->gbutton.which, event->key.key); AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL)); AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_UP | AKGL_ACTOR_STATE_MOVING_UP));
BITMASK_ADD(obj->state, (ACTOR_STATE_FACE_UP | ACTOR_STATE_MOVING_UP)); //SDL_Log("new target actor state: %b", obj->state);
SDL_Log("new target actor state: %b", obj->state);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_up_off(actor *obj, SDL_Event *event) akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
if ( !BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_UP) ) { //SDL_Log("event %d (button %d / key %d) stops moving actor up", event->type, event->gbutton.which, event->key.key);
SUCCEED_RETURN(errctx); obj->ay = 0;
} obj->ey = 0;
SDL_Log("event %d (button %d / key %d) stops moving actor up", event->type, event->gbutton.which, event->key.key); obj->ty = 0;
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL)); obj->vy = 0;
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_UP); AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_UP);
SDL_Log("new target actor state: %b", obj->state); //SDL_Log("new target actor state: %b", obj->state);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_down_on(actor *obj, SDL_Event *event) akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
if ( BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_DOWN) ) { FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
SUCCEED_RETURN(errctx); //SDL_Log("event %d (button %d / key %d) moves actor down", event->type, event->gbutton.which, event->key.key);
} obj->ay = obj->basechar->ay;
SDL_Log("event %d (button %d / key %d) moves actor down", event->type, event->gbutton.which, event->key.key); AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL)); AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN));
BITMASK_ADD(obj->state, (ACTOR_STATE_MOVING_DOWN | ACTOR_STATE_FACE_DOWN)); //SDL_Log("new target actor state: %b", obj->state);
SDL_Log("new target actor state: %b", obj->state);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext ERROR_NOIGNORE *SDL3GActor_cmhf_down_off(actor *obj, SDL_Event *event) akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
if ( !BITMASK_HAS(obj->state, ACTOR_STATE_MOVING_DOWN) ) { //SDL_Log("event %d (button %d / key %d) stops moving actor down", event->type, event->gbutton.which, event->key.key);
SUCCEED_RETURN(errctx); obj->ty = 0;
} obj->ey = 0;
SDL_Log("event %d (button %d / key %d) stops moving actor down", event->type, event->gbutton.which, event->key.key); obj->ay = 0;
BITMASK_DEL(obj->state, (ACTOR_STATE_FACE_ALL | ACTOR_STATE_MOVING_ALL)); obj->vy = 0;
BITMASK_ADD(obj->state, ACTOR_STATE_FACE_DOWN); AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_DOWN);
SDL_Log("new target actor state: %b", obj->state); //SDL_Log("new target actor state: %b", obj->state);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -1,34 +1,39 @@
char *ACTOR_STATE_STRING_NAMES[32] = { /**
"ACTOR_STATE_FACE_DOWN", * @file actor_state_string_names.c
"ACTOR_STATE_FACE_LEFT", * @brief Implements the actor state string names subsystem.
"ACTOR_STATE_FACE_RIGHT", */
"ACTOR_STATE_FACE_UP",
"ACTOR_STATE_ALIVE", char *AKGL_ACTOR_STATE_STRING_NAMES[32] = {
"ACTOR_STATE_DYING", "AKGL_ACTOR_STATE_FACE_DOWN",
"ACTOR_STATE_DEAD", "AKGL_ACTOR_STATE_FACE_LEFT",
"ACTOR_STATE_MOVING_LEFT", "AKGL_ACTOR_STATE_FACE_RIGHT",
"ACTOR_STATE_MOVING_RIGHT", "AKGL_ACTOR_STATE_FACE_UP",
"ACTOR_STATE_MOVING_UP", "AKGL_ACTOR_STATE_ALIVE",
"ACTOR_STATE_MOVING_DOWN", "AKGL_ACTOR_STATE_DYING",
"ACTOR_STATE_UNDEFINED_11", "AKGL_ACTOR_STATE_DEAD",
"ACTOR_STATE_UNDEFINED_12", "AKGL_ACTOR_STATE_MOVING_LEFT",
"ACTOR_STATE_UNDEFINED_13", "AKGL_ACTOR_STATE_MOVING_RIGHT",
"ACTOR_STATE_UNDEFINED_14", "AKGL_ACTOR_STATE_MOVING_UP",
"ACTOR_STATE_UNDEFINED_15", "AKGL_ACTOR_STATE_MOVING_DOWN",
"ACTOR_STATE_UNDEFINED_16", "AKGL_ACTOR_STATE_UNDEFINED_11",
"ACTOR_STATE_UNDEFINED_17", "AKGL_ACTOR_STATE_UNDEFINED_12",
"ACTOR_STATE_UNDEFINED_18", "AKGL_ACTOR_STATE_UNDEFINED_13",
"ACTOR_STATE_UNDEFINED_19", "AKGL_ACTOR_STATE_UNDEFINED_14",
"ACTOR_STATE_UNDEFINED_20", "AKGL_ACTOR_STATE_UNDEFINED_15",
"ACTOR_STATE_UNDEFINED_21", "AKGL_ACTOR_STATE_UNDEFINED_16",
"ACTOR_STATE_UNDEFINED_22", "AKGL_ACTOR_STATE_UNDEFINED_17",
"ACTOR_STATE_UNDEFINED_23", "AKGL_ACTOR_STATE_UNDEFINED_18",
"ACTOR_STATE_UNDEFINED_24", "AKGL_ACTOR_STATE_UNDEFINED_19",
"ACTOR_STATE_UNDEFINED_25", "AKGL_ACTOR_STATE_UNDEFINED_20",
"ACTOR_STATE_UNDEFINED_26", "AKGL_ACTOR_STATE_UNDEFINED_21",
"ACTOR_STATE_UNDEFINED_27", "AKGL_ACTOR_STATE_UNDEFINED_22",
"ACTOR_STATE_UNDEFINED_28", "AKGL_ACTOR_STATE_UNDEFINED_23",
"ACTOR_STATE_UNDEFINED_29", "AKGL_ACTOR_STATE_UNDEFINED_24",
"ACTOR_STATE_UNDEFINED_30", "AKGL_ACTOR_STATE_UNDEFINED_25",
"ACTOR_STATE_UNDEFINED_31", "AKGL_ACTOR_STATE_UNDEFINED_26",
"AKGL_ACTOR_STATE_UNDEFINED_27",
"AKGL_ACTOR_STATE_UNDEFINED_28",
"AKGL_ACTOR_STATE_UNDEFINED_29",
"AKGL_ACTOR_STATE_UNDEFINED_30",
"AKGL_ACTOR_STATE_UNDEFINED_31",
}; };

View File

@@ -1,47 +1,53 @@
/**
* @file assets.c
* @brief Implements the assets subsystem.
*/
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h> #include <SDL3_mixer/SDL_mixer.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/game.h> #include <akgl/game.h>
#include <sdl3game/staticstring.h> #include <akgl/staticstring.h>
#include <sdl3game/heap.h> #include <akgl/heap.h>
#include <akgl/error.h>
ErrorContext *load_start_bgm(char *fname) akerr_ErrorContext *akgl_load_start_bgm(char *fname)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
string *tmpstr = NULL; //akgl_String *tmpstr = NULL;
MIX_Track *bgmtrack = NULL; MIX_Track *bgmtrack = NULL;
SDL_PropertiesID bgmprops = 0; SDL_PropertiesID bgmprops = 0;
ATTEMPT { ATTEMPT {
FAIL_ZERO_BREAK(errctx, fname, ERR_NULLPOINTER, "load_start_bgm received NULL filename"); FAIL_ZERO_BREAK(errctx, fname, AKERR_NULLPOINTER, "akgl_load_start_bgm received NULL filename");
CATCH(errctx, heap_next_string(&tmpstr)); //CATCH(errctx, akgl_heap_next_string(&tmpstr));
CATCH(errctx, string_initialize(tmpstr, NULL)); //CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
SDL_snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname); //SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
SDL_Log("Loading music asset from %s", (char *)&tmpstr->data); SDL_Log("Loading music asset from %s", fname);
bgm = MIX_LoadAudio(game.mixer, (char *)&tmpstr->data, true); bgm = MIX_LoadAudio(akgl_mixer, fname, true);
FAIL_ZERO_BREAK(errctx, bgm, ERR_NULLPOINTER, "Failed to load music asset %s : %s", (char *)&tmpstr->data, SDL_GetError()); FAIL_ZERO_BREAK(errctx, bgm, AKERR_NULLPOINTER, "Failed to load music asset %s : %s", fname, SDL_GetError());
bgmtrack = MIX_CreateTrack(game.mixer); bgmtrack = MIX_CreateTrack(akgl_mixer);
FAIL_ZERO_BREAK(errctx, bgmtrack, ERR_NULLPOINTER, "Failed to create audio track for background music: %s", SDL_GetError()); FAIL_ZERO_BREAK(errctx, bgmtrack, AKERR_NULLPOINTER, "Failed to create audio track for background music: %s", SDL_GetError());
game.tracks[GAME_AUDIO_TRACK_BGM] = bgmtrack; akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM] = bgmtrack;
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
MIX_SetTrackAudio(bgmtrack, bgm), MIX_SetTrackAudio(bgmtrack, bgm),
ERR_SDL, AKGL_ERR_SDL,
"%s", "%s",
SDL_GetError()); SDL_GetError());
SDL_SetNumberProperty(bgmprops, MIX_PROP_PLAY_LOOPS_NUMBER, -1); SDL_SetNumberProperty(bgmprops, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
if (!MIX_PlayTrack(bgmtrack, bgmprops)) { if (!MIX_PlayTrack(bgmtrack, bgmprops)) {
FAIL_BREAK(errctx, ERR_SDL, "Failed to play music asset %s", fname); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Failed to play music asset %s", fname);
} }
} CLEANUP { } CLEANUP {
IGNORE(heap_release_string(tmpstr)); //IGNORE(akgl_heap_release_string(tmpstr));
if ( errctx != NULL ) { if ( errctx != NULL ) {
if ( errctx->status != 0 && bgm != NULL) { if ( errctx->status != 0 && bgm != NULL) {
MIX_DestroyAudio(bgm); MIX_DestroyAudio(bgm);

View File

@@ -1,46 +1,51 @@
/**
* @file character.c
* @brief Implements the character subsystem.
*/
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <sdlerror.h> #include <akerror.h>
#include <string.h> #include <string.h>
#include <jansson.h> #include <jansson.h>
#include <sdl3game/game.h> #include <akgl/game.h>
#include <sdl3game/sprite.h> #include <akgl/sprite.h>
#include <sdl3game/json_helpers.h> #include <akgl/json_helpers.h>
#include <sdl3game/heap.h> #include <akgl/heap.h>
#include <sdl3game/registry.h> #include <akgl/registry.h>
#include <sdl3game/staticstring.h> #include <akgl/staticstring.h>
#include <sdl3game/iterator.h> #include <akgl/iterator.h>
#include <sdl3game/util.h> #include <akgl/util.h>
ErrorContext *character_initialize(character *obj, char *name) akerr_ErrorContext *akgl_character_initialize(akgl_Character *obj, char *name)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL character reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL akgl_Character reference");
FAIL_ZERO_RETURN(errctx, name, ERR_NULLPOINTER, "NULL name string pointer"); FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL name string pointer");
memset(obj, 0x00, sizeof(character)); memset(obj, 0x00, sizeof(akgl_Character));
strncpy(obj->name, name, SPRITE_MAX_CHARACTER_NAME_LENGTH); strncpy(obj->name, name, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH);
obj->state_sprites = SDL_CreateProperties(); obj->state_sprites = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, obj->state_sprites, ERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map"); FAIL_ZERO_RETURN(errctx, obj->state_sprites, AKERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map");
obj->sprite_add = &character_sprite_add; obj->sprite_add = &akgl_character_sprite_add;
obj->sprite_get = &character_sprite_get; obj->sprite_get = &akgl_character_sprite_get;
FAIL_ZERO_RETURN( FAIL_ZERO_RETURN(
errctx, errctx,
SDL_SetPointerProperty(REGISTRY_CHARACTER, name, (void *)obj), SDL_SetPointerProperty(AKGL_REGISTRY_CHARACTER, name, (void *)obj),
ERR_KEY, AKERR_KEY,
"Unable to add character to registry"); "Unable to add character to registry");
obj->refcount += 1; obj->refcount += 1;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *character_sprite_add(character *basechar, sprite *ref, int state) akerr_ErrorContext *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
char stateval[32]; char stateval[32];
FAIL_ZERO_RETURN(errctx, basechar, ERR_NULLPOINTER, "NULL character reference"); FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
FAIL_ZERO_RETURN(errctx, ref, ERR_NULLPOINTER, "NULL sprite reference"); FAIL_ZERO_RETURN(errctx, ref, AKERR_NULLPOINTER, "NULL sprite reference");
memset(&stateval, 0x00, 32); memset(&stateval, 0x00, 32);
SDL_itoa(state, (char *)&stateval, 10); SDL_itoa(state, (char *)&stateval, 10);
SDL_SetPointerProperty(basechar->state_sprites, (char *)&stateval, ref); SDL_SetPointerProperty(basechar->state_sprites, (char *)&stateval, ref);
@@ -49,17 +54,17 @@ ErrorContext *character_sprite_add(character *basechar, sprite *ref, int state)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *character_sprite_get(character *basechar, int state, sprite **dest) akerr_ErrorContext *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest)
{ {
sprite *target = NULL; akgl_Sprite *target = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
char stateval[32]; char stateval[32];
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "NULL pointer to sprite pointer (**dest)"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer to sprite pointer (**dest)");
FAIL_ZERO_RETURN(errctx, basechar, ERR_NULLPOINTER, "NULL character reference"); FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
memset(&stateval, 0x00, 32); memset(&stateval, 0x00, 32);
SDL_itoa(state, (char *)&stateval, 10); SDL_itoa(state, (char *)&stateval, 10);
*dest = (sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL); *dest = (akgl_Sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL);
FAIL_ZERO_RETURN(errctx, *dest, ERR_KEY, "Sprite for state %d (%b) not found in the character's registry", state, state); FAIL_ZERO_RETURN(errctx, *dest, AKERR_KEY, "Sprite for state %d (%b) not found in the character's registry", state, state);
target = *dest; target = *dest;
//SDL_Log("Sprite state %d (%s) has character %s", state, (char *)&stateval, target->name); //SDL_Log("Sprite state %d (%s) has character %s", state, (char *)&stateval, target->name);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -67,134 +72,154 @@ ErrorContext *character_sprite_get(character *basechar, int state, sprite **dest
// SDL iterator so we can't return error information here, void only // SDL iterator so we can't return error information here, void only
// this means we don't have anywhere to send exceptions up to, so if we hit an error, we log and exit(1) here // this means we don't have anywhere to send exceptions up to, so if we hit an error, we log and exit(1) here
void character_state_sprites_iterate(void *userdata, SDL_PropertiesID registry, const char *name) void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID registry, const char *name)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
sprite *spriteptr; akgl_Sprite *spriteptr;
iterator *opflags = (iterator *)userdata; akgl_Iterator *opflags = (akgl_Iterator *)userdata;
ATTEMPT { ATTEMPT {
FAIL_ZERO_BREAK(errctx, opflags, ERR_NULLPOINTER, "Character state sprite iterator received null iterator op pointer"); FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "Character state sprite iterator received null iterator op pointer");
FAIL_ZERO_BREAK(errctx, name, ERR_NULLPOINTER, "Character state sprite iterator received null sprite name"); FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "Character state sprite iterator received null sprite name");
spriteptr = (sprite *)SDL_GetPointerProperty(registry, name, NULL); spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(registry, name, NULL);
FAIL_ZERO_BREAK(errctx, spriteptr, ERR_NULLPOINTER, "Character state sprite for %s not found", name); FAIL_ZERO_BREAK(errctx, spriteptr, AKERR_NULLPOINTER, "Character state sprite for %s not found", name);
if ( BITMASK_HAS(opflags->flags, ITERATOR_OP_RELEASE) ) { if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RELEASE) ) {
CATCH(errctx, heap_release_sprite(spriteptr)); CATCH(errctx, akgl_heap_release_sprite(spriteptr));
} }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);
} }
static ErrorContext *character_load_json_state_int_from_strings(json_t *states, int *dest) /**
* @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; int i = 0;
long newstate = 0; long newstate = 0;
string *tmpstring = NULL; akgl_String *tmpstring = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, states, ERR_NULLPOINTER, "NULL states array"); FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL states array");
FAIL_ZERO_RETURN(errctx, states, ERR_NULLPOINTER, "NULL destination integer"); FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL destination integer");
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&tmpstring)); CATCH(errctx, akgl_heap_next_string(&tmpstring));
for ( i = 0; i < json_array_size((json_t *)states) ; i++ ) { for ( i = 0; i < json_array_size((json_t *)states) ; i++ ) {
CATCH(errctx, get_json_array_index_string(states, i, &tmpstring)); CATCH(errctx, akgl_get_json_array_index_string(states, i, &tmpstring));
newstate = (long)SDL_GetNumberProperty(REGISTRY_ACTOR_STATE_STRINGS, (char *)&tmpstring->data, 0); newstate = (long)SDL_GetNumberProperty(AKGL_REGISTRY_ACTOR_STATE_STRINGS, (char *)&tmpstring->data, 0);
FAIL_ZERO_BREAK(errctx, newstate, ERR_KEY, "Unknown actor state %s", (char *)&tmpstring->data); FAIL_ZERO_BREAK(errctx, newstate, AKERR_KEY, "Unknown actor state %s", (char *)&tmpstring->data);
*dest = (*dest | (int)(newstate)); *dest = (*dest | (int)(newstate));
} }
} CLEANUP { } CLEANUP {
IGNORE(heap_release_string(tmpstring)); IGNORE(akgl_heap_release_string(tmpstring));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
static ErrorContext *character_load_json_inner(json_t *json, character *obj) /**
* @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); PREPARE_ERROR(errctx);
json_t *mappings = NULL; json_t *mappings = NULL;
json_t *curmapping = NULL; json_t *curmapping = NULL;
json_t *statearray = NULL; json_t *statearray = NULL;
sprite *spriteptr = NULL; akgl_Sprite *spriteptr = NULL;
int i = 0; int i = 0;
string *tmpstr = NULL; akgl_String *tmpstr = NULL;
string *tmpstr2 = NULL; akgl_String *tmpstr2 = NULL;
int stateval = 0; int stateval = 0;
ATTEMPT { ATTEMPT {
CATCH(errctx, get_json_string_value((json_t *)json, "name", &tmpstr)); CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr));
CATCH(errctx, character_initialize((character *)obj, tmpstr->data)); CATCH(errctx, akgl_character_initialize((akgl_Character *)obj, tmpstr->data));
CATCH(errctx, get_json_array_value((json_t *)json, "sprite_mappings", &mappings)); CATCH(errctx, akgl_get_json_array_value((json_t *)json, "sprite_mappings", &mappings));
for ( i = 0; i < json_array_size((json_t *)mappings) ; i++ ) { for ( i = 0; i < json_array_size((json_t *)mappings) ; i++ ) {
stateval = 0; stateval = 0;
CATCH(errctx, get_json_array_index_object((json_t *)mappings, i, &curmapping)); CATCH(errctx, akgl_get_json_array_index_object((json_t *)mappings, i, &curmapping));
CATCH(errctx, get_json_string_value((json_t *)curmapping, "sprite", &tmpstr)); CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
spriteptr = (sprite *)SDL_GetPointerProperty( spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(
REGISTRY_SPRITE, AKGL_REGISTRY_SPRITE,
tmpstr->data, tmpstr->data,
NULL NULL
); );
CATCH(errctx, get_json_string_value((json_t *)json, "name", &tmpstr2)); CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr2));
CATCH(errctx, get_json_array_value((json_t *)curmapping, "state", &statearray)); CATCH(errctx, akgl_get_json_array_value((json_t *)curmapping, "state", &statearray));
CATCH(errctx, character_load_json_state_int_from_strings(statearray, &stateval)); CATCH(errctx, akgl_character_load_json_state_int_from_strings(statearray, &stateval));
CATCH(errctx, get_json_string_value((json_t *)curmapping, "sprite", &tmpstr)); CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
spriteptr, spriteptr,
ERR_NULLPOINTER, AKERR_NULLPOINTER,
"Character %s for state %b references sprite %s but not found in the registry", "Character %s for state %b references sprite %s but not found in the registry",
tmpstr2->data, tmpstr2->data,
stateval, stateval,
tmpstr->data tmpstr->data
); );
CATCH(errctx, character_sprite_add((character *)obj, (sprite *)spriteptr, stateval)); CATCH(errctx, akgl_character_sprite_add((akgl_Character *)obj, (akgl_Sprite *)spriteptr, stateval));
} }
} CLEANUP { } CLEANUP {
if ( tmpstr != NULL ) { if ( tmpstr != NULL ) {
IGNORE(heap_release_string(tmpstr)); IGNORE(akgl_heap_release_string(tmpstr));
} }
if ( tmpstr2 != NULL ) { if ( tmpstr2 != NULL ) {
IGNORE(heap_release_string(tmpstr2)); IGNORE(akgl_heap_release_string(tmpstr2));
} }
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *character_load_json(char *filename) akerr_ErrorContext *akgl_character_load_json(char *filename)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *json; json_t *json;
json_error_t error; json_error_t error;
character *obj = NULL; akgl_Character *obj = NULL;
//string *tmpstr = NULL; //akgl_String *tmpstr = NULL;
FAIL_ZERO_RETURN(errctx, filename, ERR_NULLPOINTER, "Received null filename"); FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_character(&obj)); CATCH(errctx, akgl_heap_next_character(&obj));
//CATCH(errctx, heap_next_string(&tmpstr)); //CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, string_initialize(tmpstr, NULL)); //CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
//SDL_snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename); //SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
json = (json_t *)json_load_file(filename, 0, &error); json = (json_t *)json_load_file(filename, 0, &error);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
json, json,
ERR_NULLPOINTER, AKERR_NULLPOINTER,
"Error while loading character from %s on line %d: %s", filename, error.line, error.text "Error while loading character from %s on line %d: %s", filename, error.line, error.text
); );
CATCH(errctx, character_load_json_inner(json, obj)); CATCH(errctx, akgl_character_load_json_inner(json, obj));
CATCH(errctx, get_json_number_value(json, "velocity_x", &obj->vx)); CATCH(errctx, akgl_get_json_integer_value(json, "speedtime", (int *)&obj->speedtime));
CATCH(errctx, get_json_number_value(json, "velocity_y", &obj->vy)); obj->speedtime = obj->speedtime * AKGL_TIME_ONESEC_MS;
CATCH(errctx, akgl_get_json_number_value(json, "speed_x", &obj->sx));
CATCH(errctx, akgl_get_json_number_value(json, "speed_y", &obj->sy));
CATCH(errctx, akgl_get_json_number_value(json, "acceleration_x", &obj->ax));
CATCH(errctx, akgl_get_json_number_value(json, "acceleration_y", &obj->ay));
} CLEANUP { } CLEANUP {
//IGNORE(heap_release_string(tmpstr)); //IGNORE(akgl_heap_release_string(tmpstr));
if ( errctx != NULL ) { if ( errctx != NULL ) {
if ( errctx->status != 0 ) { if ( errctx->status != 0 ) {
IGNORE(heap_release_character(obj)); IGNORE(akgl_heap_release_character(obj));
} }
} }
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SDL_Log("Character %s loaded from %s", obj->name, filename);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -1,30 +1,79 @@
/**
* @file controller.c
* @brief Implements the controller subsystem.
*/
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/heap.h> #include <akgl/heap.h>
#include <sdl3game/registry.h> #include <akgl/registry.h>
#include <sdl3game/game.h> #include <akgl/game.h>
#include <sdl3game/controller.h> #include <akgl/controller.h>
SDL3GControlMap GAME_ControlMaps[MAX_CONTROL_MAPS]; akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
ErrorContext *controller_handle_event(void *appstate, SDL_Event *event) akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
{
int count;
SDL_KeyboardID *keyboards = SDL_GetKeyboards(&count);
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, keyboards, AKERR_NULLPOINTER, "%s", SDL_GetError());
for (int i = 0; i < count; i++) {
const char *name = SDL_GetKeyboardNameForID(keyboards[i]);
SDL_Log("Keyboard %d: ID %u, Name: %s\n", i, keyboards[i], name);
}
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void)
{
int count = 0;
int i = 0;
SDL_JoystickID *gamepads = NULL;
SDL_Gamepad *gamepad = NULL;
PREPARE_ERROR(e);
if ( SDL_HasGamepad() ) {
gamepads = SDL_GetGamepads(&count);
if ( count > 0 ) {
FAIL_ZERO_RETURN(e, gamepads, AKERR_NULLPOINTER, "%s", SDL_GetError());
for ( i = 0; i < count ; i++ ) {
gamepad = SDL_OpenGamepad(gamepads[i]);
FAIL_ZERO_RETURN(e, gamepad, AKERR_NULLPOINTER, "%s", SDL_GetError());
SDL_Log("Gamepad %d is %s", i, SDL_GetGamepadNameForID(gamepads[i]));
}
SDL_free(gamepads);
} else {
SDL_Log("No gamepads enumerated");
}
} else {
SDL_Log("No gamepads connected");
}
SUCCEED_RETURN(e);
}
akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *event)
{ {
int i = 0; int i = 0;
int j = 0; int j = 0;
int eventButtonComboMatch = 0; int eventButtonComboMatch = 0;
SDL3GControlMap *curmap = NULL; akgl_ControlMap *curmap = NULL;
SDL3GControl *curcontrol = NULL; akgl_Control *curcontrol = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
ATTEMPT { ATTEMPT {
for ( i = 0 ; i < MAX_CONTROL_MAPS; i++ ) { for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
curmap = &GAME_ControlMaps[i]; curmap = &GAME_ControlMaps[i];
if ( curmap->target == NULL ) { if ( curmap->target == NULL ) {
continue; continue;
} }
//SDL_Log("Control map %d maps to actor %s", i, curmap->target->name); //SDL_Log("Control map %d maps to actor %s", i, curmap->target->name);
//SDL_Log("event from keyboard %d", event->key.which); //SDL_Log("event from keyboard %d", event->key.which);
for ( j = 0; j < MAX_CONTROLS; j++ ) { for ( j = 0; j < AKGL_MAX_CONTROLS; j++ ) {
curcontrol = &curmap->controls[j]; curcontrol = &curmap->controls[j];
//SDL_Log("button/key is processed by controlmap %d control %d", i, j); //SDL_Log("button/key is processed by controlmap %d control %d", i, j);
//SDL_Log("event %d -> control on %d off %d", event->type, curcontrol->event_on, curcontrol->event_off); //SDL_Log("event %d -> control on %d off %d", event->type, curcontrol->event_on, curcontrol->event_off);
@@ -39,121 +88,149 @@ ErrorContext *controller_handle_event(void *appstate, SDL_Event *event)
event->key.which == curmap->kbid && event->key.which == curmap->kbid &&
event->key.key == curcontrol->key) event->key.key == curcontrol->key)
); );
if ( event->type == 768 && event->key.which == 11 && event->key.key == 13 ) {
SDL_Log("Event type=%d, keyboard=%d, key=%d", event->type, event->key.which, event->key.key);
SDL_Log("ControlMap[%d].Controls[%d] keyboard=%d, key=%d", i, j, curmap->kbid, curcontrol->key);
SDL_Log("event %d -> control on %d off %d", event->type, curcontrol->event_on, curcontrol->event_off);
SDL_Log("eventButtonComboMatch for controlmap %d id %d = %d",
i, j, eventButtonComboMatch);
}
if ( event->type == curcontrol->event_on && eventButtonComboMatch) { if ( event->type == curcontrol->event_on && eventButtonComboMatch) {
CATCH(errctx, curcontrol->handler_on(curmap->target, event)); CATCH(errctx, curcontrol->handler_on(curmap->target, event));
goto _controller_handle_event_success; goto _akgl_controller_handle_event_success;
} else if ( event->type == curcontrol->event_off && eventButtonComboMatch ) { } else if ( event->type == curcontrol->event_off && eventButtonComboMatch ) {
CATCH(errctx, curcontrol->handler_off(curmap->target, event)); CATCH(errctx, curcontrol->handler_off(curmap->target, event));
goto _controller_handle_event_success; goto _akgl_controller_handle_event_success;
} }
} }
} }
_controller_handle_event_success: _akgl_controller_handle_event_success:
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event) /**
* @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)
{ {
actor *player = NULL; akgl_Actor *player = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL appstate"); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
player = SDL_GetPointerProperty(REGISTRY_ACTOR, "player", NULL); player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "Player actor does not exist"); FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN || if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
event->key.key == SDLK_DOWN ) { event->key.key == SDLK_DOWN ) {
SDL_Log("Processing dpad down : state %d", player->state); SDL_Log("Processing dpad down : state %d", player->state);
BITMASK_ADD(player->state, ACTOR_STATE_MOVING_DOWN); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
if ( !player->movement_controls_face ) { if ( !player->movement_controls_face ) {
BITMASK_DEL(player->state, ACTOR_STATE_FACE_ALL); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
BITMASK_ADD(player->state, ACTOR_STATE_FACE_DOWN); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_DOWN);
} }
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP ||
event->key.key == SDLK_UP ) { event->key.key == SDLK_UP ) {
SDL_Log("Processing dpad up"); SDL_Log("Processing dpad up");
BITMASK_ADD(player->state, ACTOR_STATE_MOVING_UP); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_UP);
if ( !player->movement_controls_face ) { if ( !player->movement_controls_face ) {
BITMASK_DEL(player->state, ACTOR_STATE_FACE_ALL); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
BITMASK_ADD(player->state, ACTOR_STATE_FACE_UP); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_UP);
} }
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT ||
event->key.key == SDLK_LEFT ) { event->key.key == SDLK_LEFT ) {
SDL_Log("Processing dpad left"); SDL_Log("Processing dpad left");
BITMASK_ADD(player->state, ACTOR_STATE_MOVING_LEFT); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
if ( !player->movement_controls_face ) { if ( !player->movement_controls_face ) {
BITMASK_DEL(player->state, ACTOR_STATE_FACE_ALL); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
BITMASK_ADD(player->state, ACTOR_STATE_FACE_LEFT); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_LEFT);
} }
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT ||
event->key.key == SDLK_RIGHT ) { event->key.key == SDLK_RIGHT ) {
SDL_Log("Processing dpad right"); SDL_Log("Processing dpad right");
BITMASK_ADD(player->state, ACTOR_STATE_MOVING_RIGHT); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
if ( !player->movement_controls_face ) { if ( !player->movement_controls_face ) {
BITMASK_DEL(player->state, ACTOR_STATE_FACE_ALL); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
BITMASK_ADD(player->state, ACTOR_STATE_FACE_RIGHT); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_RIGHT);
} }
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event) /**
* @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)
{ {
actor *player = NULL; akgl_Actor *player = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL appstate"); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
player = SDL_GetPointerProperty(REGISTRY_ACTOR, "player", NULL); player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "Player actor does not exist"); FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN || if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
event->key.key == SDLK_DOWN ) { event->key.key == SDLK_DOWN ) {
SDL_Log("processing down release"); SDL_Log("processing down release");
BITMASK_DEL(player->state, ACTOR_STATE_MOVING_DOWN); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
player->curSpriteFrameId = 0; player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP ||
event->key.key == SDLK_UP ) { event->key.key == SDLK_UP ) {
SDL_Log("processing up release"); SDL_Log("processing up release");
BITMASK_DEL(player->state, ACTOR_STATE_MOVING_UP); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_UP);
player->curSpriteFrameId = 0; player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT ||
event->key.key == SDLK_RIGHT) { event->key.key == SDLK_RIGHT) {
SDL_Log("processing right release"); SDL_Log("processing right release");
BITMASK_DEL(player->state, ACTOR_STATE_MOVING_RIGHT); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
player->curSpriteFrameId = 0; player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT ||
event->key.key == SDLK_LEFT ) { event->key.key == SDLK_LEFT ) {
SDL_Log("processing left release"); SDL_Log("processing left release");
BITMASK_DEL(player->state, ACTOR_STATE_MOVING_LEFT); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
player->curSpriteFrameId = 0; player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event) /**
* @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; SDL_JoystickID which;
SDL_Gamepad *gamepad = NULL; SDL_Gamepad *gamepad = NULL;
char *mapping = NULL; char *mapping = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL appstate"); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
which = event->gbutton.which; which = event->gbutton.which;
gamepad = SDL_GetGamepadFromID(which); gamepad = SDL_GetGamepadFromID(which);
@@ -175,14 +252,21 @@ ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event) /**
* @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; SDL_JoystickID which;
SDL_Gamepad *gamepad = NULL; SDL_Gamepad *gamepad = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL appstate"); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, appstate, ERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
which = event->gbutton.which; which = event->gbutton.which;
gamepad = SDL_GetGamepadFromID(which); gamepad = SDL_GetGamepadFromID(which);
@@ -193,3 +277,112 @@ ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
SDL_Log("Gamepad #%u removed", (unsigned int) which); SDL_Log("Gamepad #%u removed", (unsigned int) which);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control)
{
int newmapid = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
FAIL_ZERO_RETURN(errctx, control, AKERR_NULLPOINTER, "NULL Control");
FAIL_NONZERO_RETURN(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS);
newmapid = GAME_ControlMaps[controlmapid].nextMap;
FAIL_ZERO_RETURN(errctx, (AKGL_MAX_CONTROLS - newmapid), AKERR_OUTOFBOUNDS, "Control map ID %d is full", controlmapid);
memcpy((void *)&GAME_ControlMaps[controlmapid].controls[newmapid], control, sizeof(akgl_Control));
GAME_ControlMaps[controlmapid].nextMap = newmapid + 1;
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid)
{
akgl_ControlMap *controlmap;
akgl_Control control;
PREPARE_ERROR(errctx);
ATTEMPT {
// set up the control map
FAIL_NONZERO_RETURN(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS);
memset((void *)&control, 0x00, sizeof(akgl_Control));
controlmap = &GAME_ControlMaps[controlmapid];
controlmap->kbid = kbid;
controlmap->jsid = jsid;
controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL);
FAIL_ZERO_BREAK(errctx, controlmap->target, AKGL_ERR_REGISTRY, "Actor %s not found in registry", actorname);
// ---- KEYBOARD CONTROLS ----
// Move down
control.key = SDLK_DOWN;
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.handler_on = &akgl_Actor_cmhf_down_on;
control.handler_off = &akgl_Actor_cmhf_down_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move up
control.key = SDLK_UP;
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.handler_on = &akgl_Actor_cmhf_up_on;
control.handler_off = &akgl_Actor_cmhf_up_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move left
control.key = SDLK_LEFT;
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.handler_on = &akgl_Actor_cmhf_left_on;
control.handler_off = &akgl_Actor_cmhf_left_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move right
control.key = SDLK_RIGHT;
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.handler_on = &akgl_Actor_cmhf_right_on;
control.handler_off = &akgl_Actor_cmhf_right_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
control.key = 0;
// ----- GAMEPAD CONTROLS
// Move down
control.button = SDL_GAMEPAD_BUTTON_DPAD_DOWN;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
control.handler_on = &akgl_Actor_cmhf_down_on;
control.handler_off = &akgl_Actor_cmhf_down_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move up
control.button = SDL_GAMEPAD_BUTTON_DPAD_UP;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
control.handler_on = &akgl_Actor_cmhf_up_on;
control.handler_off = &akgl_Actor_cmhf_up_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move left
control.button = SDL_GAMEPAD_BUTTON_DPAD_LEFT;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
control.handler_on = &akgl_Actor_cmhf_left_on;
control.handler_off = &akgl_Actor_cmhf_left_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move right
control.button = SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
control.handler_on = &akgl_Actor_cmhf_right_on;
control.handler_off = &akgl_Actor_cmhf_right_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
SUCCEED_RETURN(errctx);
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
}

View File

@@ -1,10 +1,15 @@
/**
* @file draw.c
* @brief Implements the draw subsystem.
*/
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h> #include <SDL3_mixer/SDL_mixer.h>
#include <sdl3game/game.h> #include <akgl/game.h>
/* Draw a Gimpish background pattern to show transparency in the image */ /* Draw a Gimpish background pattern to show transparency in the image */
void GAME_draw_background(int w, int h) void akgl_draw_background(int w, int h)
{ {
SDL_Color col[2] = { SDL_Color col[2] = {
{ 0x66, 0x66, 0x66, 0xff }, { 0x66, 0x66, 0x66, 0xff },
@@ -20,11 +25,11 @@ void GAME_draw_background(int w, int h)
for (x = 0; x < w; x += dx) { for (x = 0; x < w; x += dx) {
/* use an 8x8 checkerboard pattern */ /* use an 8x8 checkerboard pattern */
i = (((x ^ y) >> 3) & 1); i = (((x ^ y) >> 3) & 1);
SDL_SetRenderDrawColor(renderer, col[i].r, col[i].g, col[i].b, col[i].a); SDL_SetRenderDrawColor(renderer->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a);
rect.x = (float)x; rect.x = (float)x;
rect.y = (float)y; rect.y = (float)y;
SDL_RenderFillRect(renderer, &rect); SDL_RenderFillRect(renderer->sdl_renderer, &rect);
} }
} }
} }

View File

@@ -1,86 +1,531 @@
/**
* @file game.c
* @brief Implements the game subsystem.
*/
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h> #include <SDL3_mixer/SDL_mixer.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <stdio.h> #include <stdio.h>
#include <sdlerror.h> #include <akerror.h>
#include <semver.h>
#include <sdl3game/game.h> #include <akstdlib.h>
#include <sdl3game/controller.h> #include <akgl/game.h>
#include <sdl3game/tilemap.h> #include <akgl/controller.h>
#include <sdl3game/sprite.h> #include <akgl/tilemap.h>
#include <sdl3game/heap.h> #include <akgl/sprite.h>
#include <sdl3game/registry.h> #include <akgl/heap.h>
#include <sdl3game/staticstring.h> #include <akgl/registry.h>
#include <sdl3game/iterator.h> #include <akgl/staticstring.h>
#include <akgl/iterator.h>
#include <akgl/physics.h>
#include <akgl/SDL_GameControllerDB.h>
SDL_Window *window = NULL; SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL;
GAME_frame ball;
GAME_frame paddle1;
GAME_frame paddle2;
GAME_frame table;
tilemap gamemap;
MIX_Audio *bgm = NULL;
MIX_Mixer *GAME_mixer = NULL;
MIX_Track *GAME_tracks[64];
SDL_FRect camera;
Game game;
ErrorContext ERROR_NOIGNORE *GAME_init() // Currently active objects
akgl_RenderBackend *renderer;
akgl_PhysicsBackend *physics;
SDL_FRect *camera;
akgl_Tilemap *gamemap;
// Default objects
akgl_RenderBackend _akgl_renderer;
akgl_PhysicsBackend _akgl_physics;
SDL_FRect _akgl_camera;
akgl_Tilemap _akgl_gamemap;
MIX_Audio *bgm = NULL;
MIX_Mixer *akgl_mixer = NULL;
MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
akgl_Game game;
void akgl_game_lowfps(void)
{ {
SDL_Log("Low FPS! %d", game.fps);
return;
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
{
int screenwidth = 0;
int screenheight = 0;
int i = 0; int i = 0;
PREPARE_ERROR(errctx); PREPARE_ERROR(e);
ATTEMPT { strncpy((char *)&game.libversion, AKGL_VERSION, 32);
FAIL_ZERO_BREAK(errctx, strlen((char *)&game.name), ERR_NULLPOINTER, "Must provide game name"); game.gameStartTime = SDL_GetTicksNS();
FAIL_ZERO_BREAK(errctx, strlen((char *)&game.version), ERR_NULLPOINTER, "Must provide game version"); game.lastIterTime = game.gameStartTime;
FAIL_ZERO_BREAK(errctx, strlen((char *)&game.uri), ERR_NULLPOINTER, "Must provide game uri"); game.lastFPSTime = game.gameStartTime;
CATCH(errctx, heap_init()); game.lowfpsfunc = &akgl_game_lowfps;
CATCH(errctx, registry_init_actor()); game.statelock = SDL_CreateMutex();
CATCH(errctx, registry_init_sprite()); FAIL_ZERO_RETURN(e, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
CATCH(errctx, registry_init_spritesheet()); PASS(e, akgl_game_state_lock());
CATCH(errctx, registry_init_character()); FAIL_ZERO_RETURN(e, strlen((char *)&game.name), AKERR_NULLPOINTER, "Must provide game name");
CATCH(errctx, registry_init_actor_state_strings()); FAIL_ZERO_RETURN(e, strlen((char *)&game.version), AKERR_NULLPOINTER, "Must provide game version");
} CLEANUP { FAIL_ZERO_RETURN(e, strlen((char *)&game.uri), AKERR_NULLPOINTER, "Must provide game uri");
} PROCESS(errctx) { PASS(e, akgl_heap_init());
} FINISH(errctx, true) PASS(e, akgl_registry_init_actor());
PASS(e, akgl_registry_init_sprite());
PASS(e, akgl_registry_init_spritesheet());
PASS(e, akgl_registry_init_character());
PASS(e, akgl_registry_init_font());
PASS(e, akgl_registry_init_music());
PASS(e, akgl_registry_init_properties());
PASS(e, akgl_registry_init_actor_state_strings());
SDL_SetAppMetadata(game.name, game.version, game.uri); SDL_SetAppMetadata(game.name, game.version, game.uri);
for ( i = 0 ; i < MAX_CONTROL_MAPS; i++ ) { for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
memset(&GAME_ControlMaps[i], 0x00, sizeof(SDL3GControlMap)); memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap));
} }
FAIL_ZERO_RETURN( FAIL_ZERO_RETURN(
errctx, e,
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO), SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO),
ERR_SDL, AKGL_ERR_SDL,
"Couldn't initialize SDL: %s", "Couldn't initialize SDL: %s",
SDL_GetError()); SDL_GetError());
FAIL_ZERO_RETURN( // Load the Game Controller DB
errctx,
SDL_CreateWindowAndRenderer(game.uri, game.screenwidth, game.screenheight, 0, &window, &renderer), for ( i = 0; i < AKGL_SDL_GAMECONTROLLER_DB_LEN ; i++ ) {
ERR_SDL, if ( SDL_AddGamepadMapping(SDL_GAMECONTROLLER_DB[i]) == -1 ) {
"Couldn't create window/renderer: %s", FAIL_ZERO_RETURN(e, 0, AKGL_ERR_SDL, "%s", SDL_GetError());
SDL_GetError()); }
}
PASS(e, akgl_controller_open_gamepads());
FAIL_ZERO_RETURN( FAIL_ZERO_RETURN(
errctx, e,
MIX_Init(), MIX_Init(),
ERR_SDL, AKGL_ERR_SDL,
"Couldn't initialize audio: %s", "Couldn't initialize audio: %s",
SDL_GetError()); SDL_GetError());
game.mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, 0); akgl_mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, 0);
FAIL_ZERO_RETURN( FAIL_ZERO_RETURN(
errctx, e,
game.mixer, akgl_mixer,
ERR_SDL, AKGL_ERR_SDL,
"Unable to create mixer device: %s", "Unable to create mixer device: %s",
SDL_GetError()); SDL_GetError());
camera.x = 0; FAIL_ZERO_RETURN(
camera.y = 0; e,
camera.w = game.screenwidth; TTF_Init(),
camera.h = game.screenheight; AKGL_ERR_SDL,
"Couldn't initialize front engine: %s",
SDL_GetError());
camera = &_akgl_camera;
renderer = &_akgl_renderer;
physics = &_akgl_physics;
gamemap = &_akgl_gamemap;
PASS(e, akgl_game_state_unlock());
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void)
{
PREPARE_ERROR(e);
SDL_Time totaltime = 0;
while ( totaltime < AKGL_TIME_ONESEC_MS ) {
if ( SDL_TryLockMutex(game.statelock) == true ) {
SUCCEED_RETURN(e);
}
totaltime += 100;
SDL_Delay(100);
}
FAIL_RETURN(e, AKGL_ERR_SDL, "%s", SDL_GetError());
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void)
{
PREPARE_ERROR(e);
SDL_UnlockMutex(game.statelock);
SUCCEED_RETURN(e);
}
void akgl_game_updateFPS()
{
SDL_Time curTime;
curTime = SDL_GetTicksNS();
if ( (curTime - game.lastFPSTime) > AKGL_TIME_ONESEC_NS ) {
game.fps = game.framesSinceUpdate;
game.framesSinceUpdate = 0;
game.lastFPSTime = curTime;
}
if ( game.fps < 30 ) {
game.lowfpsfunc();
}
game.framesSinceUpdate += 1;
game.lastIterTime = curTime;
}
/*
* entity name -> pointer map tables
*/
/**
* @brief 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;
akgl_Actor *actor = NULL;
PREPARE_ERROR(e);
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
actor = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, aksl_fwrite(&actor, 1, sizeof(akgl_Actor *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
}
/**
* @brief Game save spritename iterator.
* @param userdata Caller data supplied to the SDL property iterator.
* @param props SDL property collection being iterated.
* @param name Registry key or human-readable object name.
*/
void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
FILE *fp = (FILE *)userdata;
akgl_Sprite *sprite = NULL;
PREPARE_ERROR(e);
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
sprite = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
CATCH(e, aksl_fwrite(&sprite, 1, sizeof(akgl_Sprite *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
}
/**
* @brief 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;
akgl_SpriteSheet *spritesheet = NULL;
PREPARE_ERROR(e);
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
spritesheet = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
CATCH(e, aksl_fwrite(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
}
/**
* @brief 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;
PREPARE_ERROR(e);
akgl_Character *character = NULL;
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
character = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
CATCH(e, aksl_fwrite(&character, 1, sizeof(akgl_Character *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
}
/**
* @brief 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);
// Each name table ends with a zeroed name field and a zeroed pointer, which
// is what akgl_game_load_objectnamemap() looks for to stop reading. The
// terminator has to come from a buffer at least as long as the longest name
// field: writing N bytes from the address of a single char would emit N-1
// bytes of whatever happened to follow it on the stack, which both leaks
// stack contents into the save file and produces a sentinel the loader
// cannot recognize.
char nullbuf[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
memset((void *)&nullbuf, 0x00, sizeof(nullbuf));
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
// write the actor name pointer table
SDL_EnumerateProperties(
AKGL_REGISTRY_ACTOR,
&akgl_game_save_actorname_iterator,
(void *)fp);
CATCH(e, 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 *)&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 *)&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 *)&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);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
{
FILE *fp = NULL;
PREPARE_ERROR(e);
ATTEMPT {
FAIL_ZERO_BREAK(e, fpath, AKERR_NULLPOINTER, "NULL file path");
CATCH(e, aksl_fopen(fpath, "wb", &fp));
CATCH(e, aksl_fwrite(&game, 1, sizeof(akgl_Game), fp));
CATCH(e, akgl_game_save_actors(fp));
} CLEANUP {
// CLEANUP must precede PROCESS: with the two transposed, the fclose
// lands inside the PROCESS switch and only runs when an error context
// exists and reports success, so an ordinary save never flushed or
// closed its stream.
if ( fp != NULL )
fclose(fp);
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e); // SUCCEED_NORETURN if in main().
}
/**
* @brief 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);
// 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
// address Z. The object they had at address Z was named A. Our current
// instance of object named A is at address B. So we map address Z to
// address B, so that we can reconnect function pointers on objects loaded
// from the save game state."
// SDL_Properties objects can only use string keys, so we can't use the
// old pointer as a key without first converting it to a string.
CATCH(e, aksl_memset((void *)&ptrstring, 0x00, 32));
snprintf((char *)&ptrstring, 32, "%p", ptr);
SDL_SetPointerProperty(
map,
ptrstring,
SDL_GetPointerProperty(registry, objname, NULL));
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
};
SUCCEED_RETURN(e);
}
/**
* @brief 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 = {};
semver_t compare_version = {};
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, versiontype, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, curversion, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, newversion, AKERR_NULLPOINTER, "NULL argument");
ATTEMPT {
// Check save game library version
FAIL_NONZERO_BREAK(
e,
semver_parse((const char *)curversion, &current_version),
AKERR_VALUE,
"Invalid semantic %s version in current game: %s",
versiontype,
(char *)curversion);
FAIL_NONZERO_BREAK(
e,
semver_parse((const char *)newversion, &compare_version),
AKERR_VALUE,
"Invalid semantic %s version in save game: %s",
versiontype,
(char *)&newversion);
FAIL_ZERO_BREAK(
e,
semver_satisfies(compare_version, current_version, "="),
AKERR_API,
"Incompatible save game %s version (%s != %s)",
versiontype,
curversion,
(char *)&newversion);
} CLEANUP {
semver_free(&current_version);
semver_free(&compare_version);
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
{
akgl_Game savegame;
SDL_PropertiesID actormap;
SDL_PropertiesID spritemap;
SDL_PropertiesID spritesheetmap;
SDL_PropertiesID charactermap;
FILE *fp = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fpath, AKERR_NULLPOINTER, "NULL file path");
ATTEMPT {
CATCH(e, aksl_fopen(fpath, "rb", &fp));
CATCH(e, aksl_fread((void *)&savegame, 1, sizeof(akgl_Game), fp));
CATCH(e, akgl_game_load_versioncmp("library", (char *)&savegame.libversion, (char *)AKGL_VERSION));
CATCH(e, akgl_game_load_versioncmp("game", (char *)&savegame.version, (char *)&game.version));
FAIL_NONZERO_RETURN(
e,
strncmp((char *)&savegame.name, (char *)&game.name, 256),
AKERR_API,
"Savegame is not compatible with this game");
FAIL_NONZERO_RETURN(
e,
strncmp((char *)&savegame.uri, (char *)&game.uri, 256),
AKERR_API,
"Savegame is not compatible with this game");
memcpy((void *)&game, (void *)&savegame, sizeof(akgl_Game));
// Load actor name map
actormap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
// Load sprite name map
spritemap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, spritemap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
// Load spritesheet name map
spritesheetmap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, spritesheetmap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
// Load character name map
charactermap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, charactermap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER));
// Now that we have all of our pointer maps built, we can load the actual binary objects and reset their pointers
} CLEANUP {
if ( fp != NULL ) {
fclose(fp);
}
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags)
{
PREPARE_ERROR(e);
akgl_Iterator defflags = {
.flags = (AKGL_ITERATOR_OP_LAYERMASK | AKGL_ITERATOR_OP_LAYERMASK),
.layerid = 0
};
SDL_Time curTime = SDL_GetTicksNS();
akgl_Actor *actor = NULL;
if ( opflags == NULL ) {
opflags = &defflags;
}
PASS(e, akgl_game_state_lock());
akgl_game_updateFPS();
for ( int i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
if ( opflags == &defflags ) {
opflags->layerid = i;
}
for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
actor = &HEAP_ACTOR[j];
if ( actor->refcount == 0 ) {
continue;
}
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE) ) {
PASS(e, akgl_tilemap_scale_actor(gamemap, actor));
} else {
actor->scale = 1.0;
}
PASS(e, actor->updatefunc(actor));
}
}
PASS(e, physics->simulate(physics, NULL));
PASS(e, renderer->draw_world(renderer, NULL));
PASS(e, akgl_game_state_unlock());
SUCCEED_RETURN(e);
} }

View File

@@ -1,191 +1,203 @@
/**
* @file heap.c
* @brief Implements the heap subsystem.
*/
#include <stdlib.h> #include <stdlib.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/game.h> #include <akgl/game.h>
#include <sdl3game/sprite.h> #include <akgl/sprite.h>
#include <sdl3game/heap.h> #include <akgl/heap.h>
#include <sdl3game/registry.h> #include <akgl/registry.h>
#include <sdl3game/staticstring.h> #include <akgl/staticstring.h>
#include <sdl3game/iterator.h> #include <akgl/iterator.h>
#include <akgl/error.h>
actor HEAP_ACTOR[MAX_HEAP_ACTOR]; akgl_Actor HEAP_ACTOR[AKGL_MAX_HEAP_ACTOR];
sprite HEAP_SPRITE[MAX_HEAP_SPRITE]; akgl_Sprite HEAP_SPRITE[AKGL_MAX_HEAP_SPRITE];
spritesheet HEAP_SPRITESHEET[MAX_HEAP_SPRITESHEET]; akgl_SpriteSheet HEAP_SPRITESHEET[AKGL_MAX_HEAP_SPRITESHEET];
character HEAP_CHARACTER[MAX_HEAP_CHARACTER]; akgl_Character HEAP_CHARACTER[AKGL_MAX_HEAP_CHARACTER];
string HEAP_STRING[MAX_HEAP_STRING]; akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING];
ErrorContext *heap_init() akerr_ErrorContext *akgl_heap_init()
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int i = 0; int i = 0;
for ( i = 0; i < MAX_HEAP_ACTOR; i++) { akerr_name_for_status(AKGL_ERR_SDL, "SDL Error");
memset(&HEAP_ACTOR[i], 0x00, sizeof(actor)); 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));
} }
for ( i = 0; i < MAX_HEAP_SPRITE; i++) { for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++) {
memset(&HEAP_SPRITE[i], 0x00, sizeof(sprite)); memset(&HEAP_SPRITESHEET[i], 0x00, sizeof(akgl_SpriteSheet));
} }
for ( i = 0; i < MAX_HEAP_SPRITESHEET; i++) { for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++) {
memset(&HEAP_SPRITESHEET[i], 0x00, sizeof(spritesheet)); memset(&HEAP_CHARACTER[i], 0x00, sizeof(akgl_Character));
} }
for ( i = 0; i < MAX_HEAP_CHARACTER; i++) { for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++) {
memset(&HEAP_CHARACTER[i], 0x00, sizeof(character)); memset(&HEAP_STRING[i], 0x00, sizeof(akgl_String));
}
for ( i = 0; i < MAX_HEAP_STRING; i++) {
memset(&HEAP_STRING[i], 0x00, sizeof(string));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *heap_next_actor(actor **dest) akerr_ErrorContext *akgl_heap_init_actor(void)
{
PREPARE_ERROR(e);
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++) {
memset(&HEAP_ACTOR[i], 0x00, sizeof(akgl_Actor));
}
SUCCEED_RETURN(e);
}
akerr_ErrorContext *akgl_heap_next_actor(akgl_Actor **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
for (int i = 0; i < MAX_HEAP_ACTOR; i++ ) { for (int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
if ( HEAP_ACTOR[i].refcount != 0 ) { if ( HEAP_ACTOR[i].refcount != 0 ) {
continue; continue;
} }
*dest = &HEAP_ACTOR[i]; *dest = &HEAP_ACTOR[i];
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
FAIL_RETURN(errctx, ERR_HEAP, "Unable to find unused actor on the heap"); FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused actor on the heap");
} }
ErrorContext *heap_next_sprite(sprite **dest) akerr_ErrorContext *akgl_heap_next_sprite(akgl_Sprite **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
for (int i = 0; i < MAX_HEAP_SPRITE; i++ ) { for (int i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
if ( HEAP_SPRITE[i].refcount != 0 ) { if ( HEAP_SPRITE[i].refcount != 0 ) {
continue; continue;
} }
*dest = &HEAP_SPRITE[i]; *dest = &HEAP_SPRITE[i];
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
FAIL_RETURN(errctx, ERR_HEAP, "Unable to find unused sprite on the heap"); FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused sprite on the heap");
} }
ErrorContext *heap_next_spritesheet(spritesheet **dest) akerr_ErrorContext *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
for (int i = 0; i < MAX_HEAP_SPRITESHEET; i++ ) { for (int i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
if ( HEAP_SPRITESHEET[i].refcount != 0 ) { if ( HEAP_SPRITESHEET[i].refcount != 0 ) {
continue; continue;
} }
*dest = &HEAP_SPRITESHEET[i]; *dest = &HEAP_SPRITESHEET[i];
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
FAIL_RETURN(errctx, ERR_HEAP, "Unable to find unused spritesheet on the heap"); FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused spritesheet on the heap");
} }
ErrorContext *heap_next_character(character **dest) akerr_ErrorContext *akgl_heap_next_character(akgl_Character **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
for (int i = 0; i < MAX_HEAP_CHARACTER; i++ ) { for (int i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
if ( HEAP_CHARACTER[i].refcount != 0 ) { if ( HEAP_CHARACTER[i].refcount != 0 ) {
continue; continue;
} }
*dest = &HEAP_CHARACTER[i]; *dest = &HEAP_CHARACTER[i];
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
FAIL_RETURN(errctx, ERR_HEAP, "Unable to find unused character on the heap"); FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused character on the heap");
} }
ErrorContext *heap_next_string(string **dest) akerr_ErrorContext *akgl_heap_next_string(akgl_String **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
for (int i = 0; i < MAX_HEAP_STRING; i++ ) { for (int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
if ( HEAP_STRING[i].refcount != 0 ) { if ( HEAP_STRING[i].refcount != 0 ) {
continue; continue;
} }
*dest = &HEAP_STRING[i]; *dest = &HEAP_STRING[i];
HEAP_STRING[i].refcount += 1;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
FAIL_RETURN(errctx, ERR_HEAP, "Unable to find unused string on the heap"); FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused string on the heap");
} }
ErrorContext *heap_release_actor(actor *ptr) akerr_ErrorContext *akgl_heap_release_actor(akgl_Actor *ptr)
{ {
int i = 0; int i = 0;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, ptr, ERR_NULLPOINTER, "NULL actor reference"); FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "NULL actor reference");
if ( ptr->refcount > 0 ) { if ( ptr->refcount > 0 ) {
ptr->refcount -= 1; ptr->refcount -= 1;
} }
if ( ptr->refcount == 0 ) { if ( ptr->refcount == 0 ) {
for ( i = 0; i < ACTOR_MAX_CHILDREN; i++ ) { for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN; i++ ) {
if ( ptr->children[i] != NULL ) { if ( ptr->children[i] != NULL ) {
CATCH_AND_RETURN(errctx, heap_release_actor(ptr->children[i])); PASS(errctx, akgl_heap_release_actor(ptr->children[i]));
} }
} }
if ( ptr->basechar != NULL ) { SDL_ClearProperty(AKGL_REGISTRY_ACTOR, (char *)&ptr->name);
CATCH_AND_RETURN(errctx, heap_release_character(ptr->basechar)); memset(ptr, 0x00, sizeof(akgl_Actor));
}
memset(ptr, 0x00, sizeof(actor));
SDL_ClearProperty(REGISTRY_ACTOR, (char *)&ptr->name);
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *heap_release_character(character *basechar) akerr_ErrorContext *akgl_heap_release_character(akgl_Character *basechar)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
iterator opflags; akgl_Iterator opflags;
FAIL_ZERO_RETURN(errctx, basechar, ERR_NULLPOINTER, "NULL character reference"); FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
BITMASK_CLEAR(opflags.flags); AKGL_BITMASK_CLEAR(opflags.flags);
BITMASK_ADD(opflags.flags, ITERATOR_OP_RELEASE);
if ( basechar->refcount > 0 ) { if ( basechar->refcount > 0 ) {
basechar->refcount -= 1; basechar->refcount -= 1;
} }
if ( basechar->refcount == 0 ) { if ( basechar->refcount == 0 ) {
SDL_EnumerateProperties(basechar->state_sprites, &character_state_sprites_iterate, (void *)&opflags); SDL_ClearProperty(AKGL_REGISTRY_CHARACTER, (char *)&basechar->name);
SDL_ClearProperty(REGISTRY_CHARACTER, (char *)&basechar->name); memset(basechar, 0x00, sizeof(akgl_Character));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *heap_release_sprite(sprite *ptr) akerr_ErrorContext *akgl_heap_release_sprite(akgl_Sprite *ptr)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, ptr, ERR_NULLPOINTER, "Received NULL sprite reference"); FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "Received NULL sprite reference");
if ( ptr->refcount > 0 ) { if ( ptr->refcount > 0 ) {
ptr->refcount -= 1; ptr->refcount -= 1;
} }
if ( ptr->refcount == 0 ) { if ( ptr->refcount == 0 ) {
ATTEMPT { SDL_ClearProperty(AKGL_REGISTRY_SPRITE, (char *)&ptr->name);
CATCH(errctx, heap_release_spritesheet(ptr->sheet)); memset(ptr, 0x00, sizeof(akgl_Sprite));
} CLEANUP {
SDL_ClearProperty(REGISTRY_SPRITE, (char *)&ptr->name);
} PROCESS(errctx) {
} FINISH(errctx, true);
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *heap_release_spritesheet(spritesheet *ptr) akerr_ErrorContext *akgl_heap_release_spritesheet(akgl_SpriteSheet *ptr)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, ptr, ERR_NULLPOINTER, "Received NULL spritesheet reference"); FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "Received NULL spritesheet reference");
if ( ptr->refcount > 0 ) { if ( ptr->refcount > 0 ) {
ptr->refcount -= 1; ptr->refcount -= 1;
} }
if ( ptr->refcount == 0 ) { if ( ptr->refcount == 0 ) {
// TODO : If we go threaded, make sure this is only happening on the main thread // TODO : If we go threaded, make sure this is only happening on the main thread
SDL_ClearProperty(REGISTRY_SPRITESHEET, (char *)&ptr->name); SDL_ClearProperty(AKGL_REGISTRY_SPRITESHEET, (char *)&ptr->name);
if ( ptr-> texture != NULL ) if ( ptr-> texture != NULL )
SDL_DestroyTexture(ptr->texture); SDL_DestroyTexture(ptr->texture);
ptr->texture = NULL; ptr->texture = NULL;
memset(ptr, 0x00, sizeof(akgl_SpriteSheet));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *heap_release_string(string *ptr) akerr_ErrorContext *akgl_heap_release_string(akgl_String *ptr)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, ptr, ERR_NULLPOINTER, "Received NULL string reference"); FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "Received NULL string reference");
if ( ptr->refcount > 0 ) { if ( ptr->refcount > 0 ) {
ptr->refcount -= 1; ptr->refcount -= 1;
} }
if ( ptr->refcount == 0 ) { if ( ptr->refcount == 0 ) {
memset(&ptr->data, 0x00, MAX_STRING_LENGTH); memset(&ptr->data, 0x00, AKGL_MAX_STRING_LENGTH);
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -1,131 +1,170 @@
/**
* @file json_helpers.c
* @brief Implements the json helpers subsystem.
*/
#include <jansson.h> #include <jansson.h>
#include <string.h> #include <string.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/json_helpers.h> #include <akgl/json_helpers.h>
#include <sdl3game/game.h> #include <akgl/game.h>
#include <sdl3game/heap.h> #include <akgl/heap.h>
#include <sdl3game/staticstring.h> #include <akgl/staticstring.h>
#include <sdl3game/registry.h> #include <akgl/registry.h>
ErrorContext *get_json_object_value(json_t *obj, char *key, json_t **dest) akerr_ErrorContext *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL object pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), ERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = value; *dest = value;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *get_json_boolean_value(json_t *obj, char *key, bool *dest) akerr_ErrorContext *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL object pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), ERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_boolean_value(value); *dest = json_boolean_value(value);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *get_json_integer_value(json_t *obj, char *key, int *dest) akerr_ErrorContext *akgl_get_json_integer_value(json_t *obj, char *key, int *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL object pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), ERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_integer_value(value); *dest = json_integer_value(value);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *get_json_number_value(json_t *obj, char *key, float *dest) akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), ERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_number_value(value); *dest = json_number_value(value);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *get_json_string_value(json_t *obj, char *key, string **dest) akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_number_value(value);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest)
{ {
json_t *value = NULL; json_t *value = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, key, ERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer reference");
value = json_object_get(obj, key); value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), ERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
ATTEMPT { ATTEMPT {
if ( *dest == NULL ) { if ( *dest == NULL ) {
CATCH(errctx, heap_next_string(dest)); CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, string_initialize(*dest, NULL)); CATCH(errctx, akgl_string_initialize(*dest, NULL));
} }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, false); } FINISH(errctx, false);
strncpy((char *)&(*dest)->data, json_string_value(value), MAX_STRING_LENGTH); strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *get_json_array_value(json_t *obj, char *key, json_t **dest) akerr_ErrorContext *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, ERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_array(value)), ERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = value; *dest = value;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *get_json_array_index_object(json_t *array, int index, json_t **dest) akerr_ErrorContext *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, ERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
json_t *value = json_array_get(array, index); json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, ERR_OUTOFBOUNDS, "Index %d out of bounds for array", index); FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), ERR_TYPE, "Index %d in object has incorrect type", index); FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
*dest = value; *dest = value;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *get_json_array_index_integer(json_t *array, int index, int *dest) akerr_ErrorContext *akgl_get_json_array_index_integer(json_t *array, int index, int *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, ERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
json_t *value = json_array_get(array, index); json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, ERR_OUTOFBOUNDS, "Index %d out of bounds for array", index); FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), ERR_TYPE, "Index %d in object has incorrect type", index); FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
*dest = json_integer_value(value); *dest = json_integer_value(value);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *get_json_array_index_string(json_t *array, int index, string **dest) akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, ERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "NULL destination pointer reference"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer reference");
json_t *value = json_array_get(array, index); json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, ERR_OUTOFBOUNDS, "Index %d out of bounds for array", index); FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), ERR_TYPE, "Index %d in object has incorrect type", index); FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
ATTEMPT { ATTEMPT {
if ( *dest == NULL ) { if ( *dest == NULL ) {
CATCH(errctx, heap_next_string(dest)); CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, string_initialize(*dest, NULL)); CATCH(errctx, akgl_string_initialize(*dest, NULL));
} }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, false); } FINISH(errctx, false);
strncpy((char *)&(*dest)->data, json_string_value(value), MAX_STRING_LENGTH); strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *err, void *defval, void *dest, uint32_t defsize)
{
PREPARE_ERROR(e);
if ( err == NULL ) {
SUCCEED_RETURN(e);
}
int docopy = 0;
FAIL_ZERO_RETURN(e, err, AKERR_NULLPOINTER, "err");
FAIL_ZERO_RETURN(e, defval, AKERR_NULLPOINTER, "defval");
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
ATTEMPT {
} CLEANUP {
} PROCESS(err) {
} HANDLE_GROUP(err, AKERR_KEY) {
} HANDLE_GROUP(err, AKERR_INDEX) {
memcpy(dest, defval, defsize);
} FINISH(err, true);
SUCCEED_RETURN(e);
}

246
src/physics.c Normal file
View File

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

View File

@@ -1,77 +1,192 @@
/**
* @file registry.c
* @brief Implements the registry subsystem.
*/
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <sdlerror.h> #include <akerror.h>
#include <jansson.h>
#include <sdl3game/sprite.h> #include <akstdlib.h>
#include <sdl3game/registry.h> #include <akgl/heap.h>
#include <sdl3game/iterator.h> #include <akgl/sprite.h>
#include <sdl3game/actor.h> #include <akgl/registry.h>
#include <akgl/iterator.h>
#include <akgl/actor.h>
#include <akgl/json_helpers.h>
SDL_PropertiesID REGISTRY_ACTOR; SDL_PropertiesID AKGL_REGISTRY_ACTOR = 0;
SDL_PropertiesID REGISTRY_ACTOR_STATE_STRINGS; SDL_PropertiesID AKGL_REGISTRY_ACTOR_STATE_STRINGS = 0;
SDL_PropertiesID REGISTRY_SPRITE; SDL_PropertiesID AKGL_REGISTRY_SPRITE = 0;
SDL_PropertiesID REGISTRY_SPRITESHEET; SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET = 0;
SDL_PropertiesID REGISTRY_CHARACTER; SDL_PropertiesID AKGL_REGISTRY_CHARACTER = 0;
SDL_PropertiesID AKGL_REGISTRY_MUSIC = 0;
SDL_PropertiesID AKGL_REGISTRY_FONT = 0;
SDL_PropertiesID AKGL_REGISTRY_PROPERTIES = 0;
ErrorContext *registry_init() akerr_ErrorContext *akgl_registry_init()
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, registry_init_spritesheet()); CATCH(errctx, akgl_registry_init_spritesheet());
CATCH(errctx, registry_init_sprite()); CATCH(errctx, akgl_registry_init_sprite());
CATCH(errctx, registry_init_character()); CATCH(errctx, akgl_registry_init_character());
CATCH(errctx, registry_init_actor()); CATCH(errctx, akgl_registry_init_actor());
CATCH(errctx, registry_init_actor_state_strings()); CATCH(errctx, akgl_registry_init_actor_state_strings());
CATCH(errctx, akgl_registry_init_font());
CATCH(errctx, akgl_registry_init_music());
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *registry_init_actor() akerr_ErrorContext *akgl_registry_init_actor()
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
REGISTRY_ACTOR = SDL_CreateProperties(); if ( AKGL_REGISTRY_ACTOR != 0 ) {
FAIL_ZERO_RETURN(errctx, REGISTRY_ACTOR, ERR_NULLPOINTER, "Error initializing actor registry"); SDL_DestroyProperties(AKGL_REGISTRY_ACTOR);
}
AKGL_REGISTRY_ACTOR = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_ACTOR, AKERR_NULLPOINTER, "Error initializing actor registry");
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *registry_init_actor_state_strings() akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font()
{
PREPARE_ERROR(errctx);
AKGL_REGISTRY_FONT = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_FONT, AKERR_NULLPOINTER, "Error initializing font registry");
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_registry_init_music()
{
PREPARE_ERROR(errctx);
AKGL_REGISTRY_MUSIC = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_MUSIC, AKERR_NULLPOINTER, "Error initializing music registry");
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_registry_init_properties()
{
PREPARE_ERROR(errctx);
AKGL_REGISTRY_PROPERTIES = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_PROPERTIES, AKERR_NULLPOINTER, "Error initializing properties registry");
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_registry_init_actor_state_strings()
{ {
int i = 0; int i = 0;
int flag = 0; int flag = 0;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
REGISTRY_ACTOR_STATE_STRINGS = SDL_CreateProperties(); AKGL_REGISTRY_ACTOR_STATE_STRINGS = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, REGISTRY_ACTOR_STATE_STRINGS, ERR_NULLPOINTER, "Error initializing actor state strings registry"); FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_ACTOR_STATE_STRINGS, AKERR_NULLPOINTER, "Error initializing actor state strings registry");
for ( i = 0 ; i < ACTOR_MAX_STATES; i++ ) { for ( i = 0 ; i < AKGL_ACTOR_MAX_STATES; i++ ) {
flag = (1 << i); flag = (1 << i);
SDL_SetNumberProperty( SDL_SetNumberProperty(
REGISTRY_ACTOR_STATE_STRINGS, AKGL_REGISTRY_ACTOR_STATE_STRINGS,
ACTOR_STATE_STRING_NAMES[i], AKGL_ACTOR_STATE_STRING_NAMES[i],
flag); flag);
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *registry_init_sprite() akerr_ErrorContext *akgl_registry_init_sprite()
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
REGISTRY_SPRITE = SDL_CreateProperties(); AKGL_REGISTRY_SPRITE = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, REGISTRY_SPRITE, ERR_NULLPOINTER, "Error initializing sprite registry"); FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_SPRITE, AKERR_NULLPOINTER, "Error initializing sprite registry");
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *registry_init_spritesheet() akerr_ErrorContext *akgl_registry_init_spritesheet()
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
REGISTRY_SPRITESHEET = SDL_CreateProperties(); AKGL_REGISTRY_SPRITESHEET = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, REGISTRY_SPRITESHEET, ERR_NULLPOINTER, "Error initializing spritesheet registry"); FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_SPRITESHEET, AKERR_NULLPOINTER, "Error initializing spritesheet registry");
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *registry_init_character() akerr_ErrorContext *akgl_registry_init_character()
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
REGISTRY_CHARACTER = SDL_CreateProperties(); AKGL_REGISTRY_CHARACTER = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, REGISTRY_CHARACTER, ERR_NULLPOINTER, "Error initializing character registry"); FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_CHARACTER, AKERR_NULLPOINTER, "Error initializing character registry");
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname)
{
json_t *json = NULL;
json_t *props = NULL;
const char *pkey = NULL;
json_t *pvalue = NULL;
json_error_t error;
akgl_String *tmpstr;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "null filename");
ATTEMPT {
SDL_Log("Loading from %s", fname);
json = json_load_file(fname, 0, &error);
FAIL_ZERO_BREAK(
errctx,
json,
AKERR_NULLPOINTER,
"Error while loading properties from %s on line %d: %s-",
fname,
error.line,
error.text);
CATCH(errctx, akgl_get_json_object_value(json, "properties", &props));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
json_object_foreach(props, pkey, pvalue) {
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpstr));
CATCH(errctx, akgl_get_json_string_value(props, (char *)pkey, &tmpstr));
SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, pkey, tmpstr->data);
SDL_Log("Set property %s = %s", pkey, tmpstr->data);
CATCH(errctx, akgl_heap_release_string(tmpstr));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
}
SDL_Log("Properties loaded");
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *src)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL char *");
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "NULL char *");
SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, name, src);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL char *");
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "NULL akgl_String *");
ATTEMPT {
if ( *dest == NULL ) {
CATCH(e, akgl_heap_next_string(dest));
}
CATCH(e,
aksl_memcpy(
(*dest)->data,
(void *)SDL_GetStringProperty(AKGL_REGISTRY_PROPERTIES, (const char *)name, (const char *)def),
AKGL_MAX_STRING_LENGTH)
);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}

141
src/renderer.c Normal file
View File

@@ -0,0 +1,141 @@
/**
* @file renderer.c
* @brief Implements the renderer subsystem.
*/
#include <SDL3/SDL.h>
#include <akgl/renderer.h>
#include <akgl/staticstring.h>
#include <akgl/registry.h>
#include <akgl/heap.h>
#include <akgl/game.h>
#include <akerror.h>
#include <akstdlib.h>
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self)
{
akgl_String *width = NULL;
akgl_String *height = NULL;
int screenwidth;
int screenheight;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
PASS(e, akgl_get_property("game.screenwidth", &width, "0"));
PASS(e, akgl_get_property("game.screenheight", &height, "0"));
PASS(e, aksl_atoi(width->data, &screenwidth));
PASS(e, aksl_atoi(height->data, &screenheight));
SDL_Log("Initializing screen (%sx%s = %dx%d)", width->data, height->data, screenwidth, screenheight);
PASS(e, akgl_heap_release_string(width));
PASS(e, akgl_heap_release_string(height));
FAIL_ZERO_RETURN(
e,
SDL_CreateWindowAndRenderer(game.uri, screenwidth, screenheight, 0, &window, &self->sdl_renderer),
AKGL_ERR_SDL,
"Couldn't create window/renderer: %s",
SDL_GetError());
camera->x = 0;
camera->y = 0;
camera->w = screenwidth;
camera->h = screenheight;
self->shutdown = &akgl_render_2d_shutdown;
self->frame_start = &akgl_render_2d_frame_start;
self->frame_end = &akgl_render_2d_frame_end;
self->draw_texture = &akgl_render_2d_draw_texture;
self->draw_mesh = &akgl_render_2d_draw_mesh;
self->draw_world = &akgl_render_2d_draw_world;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self)
{
PREPARE_ERROR(e);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
SDL_SetRenderDrawColor(self->sdl_renderer, 0, 0, 0, 255);
SDL_RenderClear(self->sdl_renderer);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
SDL_RenderPresent(self->sdl_renderer);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_texture(akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(e, texture, AKERR_NULLPOINTER, "texture");
//FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "src");
//FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
if ( angle != 0 ) {
FAIL_ZERO_RETURN(e, center, AKERR_NULLPOINTER, "center");
FAIL_ZERO_RETURN(
e,
SDL_RenderTextureRotated(self->sdl_renderer, texture, src, dest, angle, center, flip),
AKERR_NULLPOINTER, "%s", SDL_GetError()
);
} else {
FAIL_ZERO_RETURN(
e,
SDL_RenderTexture(self->sdl_renderer, texture, src, dest),
AKERR_NULLPOINTER, "%s", SDL_GetError()
);
}
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self)
{
PREPARE_ERROR(e);
FAIL_RETURN(e, AKERR_API, "Not implemented");
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags)
{
PREPARE_ERROR(e);
akgl_Iterator defflags;
SDL_Time curTime = SDL_GetTicksNS();
akgl_Actor *actor = NULL;
int j = 0;
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
if ( opflags == NULL ) {
opflags = &defflags;
PASS(e, aksl_memset((void *)opflags, 0x00, sizeof(akgl_Iterator)));
}
for ( int i = 0; i < AKGL_TILEMAP_MAX_LAYERS ; i++ ) {
if ( i < gamemap->numlayers ) {
PASS(e, akgl_tilemap_draw(gamemap, camera, i));
}
for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR ; j++ ) {
actor = &HEAP_ACTOR[j];
if ( actor->refcount == 0 ) {
continue;
}
if ( actor->layer != i ) {
continue;
}
PASS(e, actor->renderfunc(actor));
}
}
SUCCEED_RETURN(e);
}

View File

@@ -1,172 +1,213 @@
/**
* @file sprite.c
* @brief Implements the sprite subsystem.
*/
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <string.h> #include <string.h>
#include <jansson.h> #include <jansson.h>
#include <sdlerror.h> #include <akerror.h>
#include <libgen.h> #include <libgen.h>
#include <sdl3game/game.h> #include <akgl/game.h>
#include <sdl3game/sprite.h> #include <akgl/sprite.h>
#include <sdl3game/json_helpers.h> #include <akgl/json_helpers.h>
#include <sdl3game/heap.h> #include <akgl/heap.h>
#include <sdl3game/registry.h> #include <akgl/registry.h>
#include <sdl3game/staticstring.h> #include <akgl/staticstring.h>
#include <sdl3game/iterator.h> #include <akgl/iterator.h>
#include <akgl/util.h>
static ErrorContext *sprite_load_json_spritesheet(json_t *json, spritesheet **sheet, char *relative_path) akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "NULL sprite");
FAIL_ZERO_RETURN(e, srccoords, AKERR_NULLPOINTER, "NULL SDL_Rect");
FAIL_ZERO_RETURN(e, self->sheet, AKERR_NULLPOINTER, "NULL spritesheet");
srccoords->x = self->width * self->frameids[frameid];
if ( srccoords->x >= self->sheet->texture->w ) {
srccoords->y = ((int)srccoords->x / self->sheet->texture->w) * self->height;
srccoords->x = ((int)srccoords->x % self->sheet->texture->w);
} else {
srccoords->y = 0;
}
srccoords->w = self->width;
srccoords->h = self->height;
SUCCEED_RETURN(e);
}
/**
* @brief 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); PREPARE_ERROR(errctx);
json_t *spritesheet_json = NULL; json_t *spritesheet_json = NULL;
int ss_frame_width = 0; int ss_frame_width = 0;
int ss_frame_height = 0; int ss_frame_height = 0;
string *ss_filename = NULL; akgl_String *ss_filename = NULL;
string *tmpstr = NULL; akgl_String *tmpstr = NULL;
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&tmpstr)); CATCH(errctx, akgl_heap_next_string(&tmpstr));
CATCH(errctx, string_initialize(tmpstr, NULL)); CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
CATCH(errctx, get_json_object_value((json_t *)json, "spritesheet", &spritesheet_json)); CATCH(errctx, akgl_get_json_object_value((json_t *)json, "spritesheet", &spritesheet_json));
CATCH(errctx, get_json_string_value((json_t *)spritesheet_json, "filename", &ss_filename)); CATCH(errctx, akgl_get_json_string_value((json_t *)spritesheet_json, "filename", &ss_filename));
if ( ss_filename->data[0] != '/' ) { CATCH(errctx, akgl_path_relative(relative_path, ss_filename->data, tmpstr));
SDL_snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s/%s", relative_path, ss_filename->data);
} else {
SDL_snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s", ss_filename->data);
}
*sheet = SDL_GetPointerProperty( *sheet = SDL_GetPointerProperty(
REGISTRY_SPRITESHEET, AKGL_REGISTRY_SPRITESHEET,
(char *)&tmpstr->data, (char *)&tmpstr->data,
NULL NULL
); );
if ( *sheet == NULL ) { if ( *sheet == NULL ) {
CATCH(errctx, heap_next_spritesheet(sheet)); CATCH(errctx, akgl_heap_next_spritesheet(sheet));
CATCH(errctx, get_json_integer_value((json_t *)spritesheet_json, "frame_width", &ss_frame_width)); CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_width", &ss_frame_width));
CATCH(errctx, get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_width)); CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_height));
CATCH(errctx, CATCH(errctx,
spritesheet_initialize( akgl_spritesheet_initialize(
(spritesheet *)*sheet, (akgl_SpriteSheet *)*sheet,
ss_frame_width, ss_frame_width,
ss_frame_height, ss_frame_height,
(char *)&tmpstr->data) (char *)&tmpstr->data)
); );
} }
} CLEANUP { } CLEANUP {
IGNORE(heap_release_string(ss_filename)); IGNORE(akgl_heap_release_string(ss_filename));
IGNORE(heap_release_string(tmpstr)); IGNORE(akgl_heap_release_string(tmpstr));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *sprite_load_json(char *filename) akerr_ErrorContext *akgl_sprite_load_json(char *filename)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *json = NULL; json_t *json = NULL;
json_t *frames = NULL; json_t *frames = NULL;
json_error_t error; json_error_t error;
sprite *obj = NULL; akgl_Sprite *obj = NULL;
spritesheet *sheet = NULL; akgl_SpriteSheet *sheet = NULL;
string *spritename = NULL; akgl_String *spritename = NULL;
//string *tmpstr = NULL; akgl_String *filename_copy = NULL;
int i = 0; int i = 0;
FAIL_ZERO_RETURN(errctx, filename, ERR_NULLPOINTER, "Received null filename"); FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_sprite(&obj)); CATCH(errctx, akgl_heap_next_sprite(&obj));
//CATCH(errctx, heap_next_string(&tmpstr)); CATCH(errctx, akgl_heap_next_string(&filename_copy));
//CATCH(errctx, string_initialize(tmpstr, NULL)); FAIL_NONZERO_BREAK(
CATCH(errctx, heap_next_string(&spritename)); errctx,
CATCH(errctx, string_initialize(spritename, NULL)); strlen(filename) >= AKGL_MAX_STRING_LENGTH,
AKERR_OUTOFBOUNDS,
"Sprite filename exceeds temporary string capacity"
);
SDL_strlcpy(filename_copy->data, filename, AKGL_MAX_STRING_LENGTH);
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
CATCH(errctx, akgl_heap_next_string(&spritename));
CATCH(errctx, akgl_string_initialize(spritename, NULL));
//SDL_snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename); //SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
json = (json_t *)json_load_file(filename, 0, &error); json = (json_t *)json_load_file(filename, 0, &error);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
json, json,
ERR_NULLPOINTER, AKERR_NULLPOINTER,
"Error while loading sprite from %s on line %d: %s", filename, error.line, error.text "Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
); );
CATCH(errctx, 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, get_json_string_value((json_t *)json, "name", &spritename)); CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
CATCH(errctx, CATCH(errctx,
sprite_initialize( akgl_sprite_initialize(
(sprite *)obj, (akgl_Sprite *)obj,
spritename->data, spritename->data,
(spritesheet *)sheet) (akgl_SpriteSheet *)sheet)
); );
CATCH(errctx, get_json_integer_value((json_t *)json, "width", &obj->width)); CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &obj->width));
CATCH(errctx, get_json_integer_value((json_t *)json, "height", &obj->height)); CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &obj->height));
CATCH(errctx, get_json_integer_value((json_t *)json, "speed", &obj->speed)); CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &obj->speed));
CATCH(errctx, get_json_boolean_value((json_t *)json, "loop", &obj->loop)); obj->speed = obj->speed * AKGL_TIME_ONESEC_MS;
CATCH(errctx, get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse)); CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loop", &obj->loop));
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));
CATCH(errctx, get_json_array_value((json_t *)json, "frames", &frames)); CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames));
obj->frames = json_array_size((json_t *)frames); obj->frames = json_array_size((json_t *)frames);
for ( i = 0 ; i < obj->frames; i++ ) { for ( i = 0 ; i < obj->frames; i++ ) {
CATCH(errctx, get_json_array_index_integer((json_t *)frames, i, &obj->frameids[i])); CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, (uint32_t *)&obj->frameids[i]));
} }
} CLEANUP { } CLEANUP {
if ( errctx != NULL && errctx->status != 0 ) { if ( errctx != NULL && errctx->status != 0 ) {
IGNORE(heap_release_sprite(obj)); IGNORE(akgl_heap_release_sprite(obj));
IGNORE(heap_release_spritesheet(sheet)); IGNORE(akgl_heap_release_spritesheet(sheet));
} }
IGNORE(heap_release_string(spritename)); IGNORE(akgl_heap_release_string(spritename));
//IGNORE(heap_release_string(tmpstr)); IGNORE(akgl_heap_release_string(filename_copy));
//IGNORE(akgl_heap_release_string(tmpstr));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *sprite_initialize(sprite *spr, char *name, spritesheet *sheet) akerr_ErrorContext *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, spr, ERR_NULLPOINTER, "Null sprite reference"); FAIL_ZERO_RETURN(errctx, spr, AKERR_NULLPOINTER, "Null sprite reference");
FAIL_ZERO_RETURN(errctx, name, ERR_NULLPOINTER, "Empty sprite name"); FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Empty sprite name");
FAIL_ZERO_RETURN(errctx, sheet, ERR_NULLPOINTER, "Null spritesheet reference"); FAIL_ZERO_RETURN(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet reference");
memset(spr, 0x00, sizeof(sprite)); memset(spr, 0x00, sizeof(akgl_Sprite));
memcpy(spr->name, name, SPRITE_MAX_NAME_LENGTH); memcpy(spr->name, name, AKGL_SPRITE_MAX_NAME_LENGTH);
spr->sheet = sheet; spr->sheet = sheet;
FAIL_ZERO_RETURN( FAIL_ZERO_RETURN(
errctx, errctx,
SDL_SetPointerProperty(REGISTRY_SPRITE, (char *)&spr->name, (void *)spr), SDL_SetPointerProperty(AKGL_REGISTRY_SPRITE, (char *)&spr->name, (void *)spr),
ERR_KEY, AKERR_KEY,
"Unable to add sprite to registry"); "Unable to add sprite to registry");
spr->refcount += 1; spr->refcount += 1;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *spritesheet_initialize(spritesheet *sheet, int sprite_w, int sprite_h, char *filename) akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
//string *tmpstr = NULL; //akgl_String *tmpstr = NULL;
ATTEMPT { ATTEMPT {
FAIL_ZERO_BREAK(errctx, sheet, ERR_NULLPOINTER, "Null spritesheet pointer"); FAIL_ZERO_BREAK(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet pointer");
FAIL_ZERO_BREAK(errctx, filename, ERR_NULLPOINTER, "Null filename pointer"); FAIL_ZERO_BREAK(errctx, filename, AKERR_NULLPOINTER, "Null filename pointer");
memset(sheet, 0x00, sizeof(spritesheet)); memset(sheet, 0x00, sizeof(akgl_SpriteSheet));
//CATCH(errctx, heap_next_string(&tmpstr)); //CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, string_initialize(tmpstr, NULL)); //CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
strncpy((char *)&sheet->name, filename, SPRITE_SHEET_MAX_FILENAME_LENGTH); strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH);
//snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename); //snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
sheet->texture = IMG_LoadTexture(renderer, filename); sheet->texture = IMG_LoadTexture(renderer->sdl_renderer, filename);
FAIL_ZERO_BREAK(errctx, sheet->texture, ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError()); FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
SDL_SetPointerProperty(REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet), SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet),
ERR_KEY, AKERR_KEY,
"Unable to add spritesheet to registry: %s", "Unable to add spritesheet to registry: %s",
SDL_GetError()); SDL_GetError());
sheet->refcount += 1; sheet->refcount += 1;
} CLEANUP { } CLEANUP {
//IGNORE(heap_release_string(tmpstr)); //IGNORE(akgl_heap_release_string(tmpstr));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);

View File

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

66
src/text.c Normal file
View File

@@ -0,0 +1,66 @@
/**
* @file text.c
* @brief Implements the text subsystem.
*/
#include <akerror.h>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <akgl/text.h>
#include <akgl/registry.h>
#include <akgl/game.h>
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size)
{
TTF_Font *font = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null filepath");
font = TTF_OpenFont(filepath, size);
FAIL_ZERO_RETURN(errctx, font, AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_RETURN(
errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_FONT, name, (void *)font),
AKERR_KEY,
"Unable to add font %p to registry as %s : %s",
(void *)font,
name,
SDL_GetError());
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y)
{
SDL_Surface *textsurf = NULL;
SDL_Texture *texture = NULL;
SDL_FRect dest;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
if ( wraplength > 0 ) {
textsurf = TTF_RenderText_Blended_Wrapped(
font,
text,
0,
color,
wraplength);
} else {
textsurf = TTF_RenderText_Blended(
font,
text,
0,
color);
}
FAIL_ZERO_RETURN(errctx, textsurf, AKERR_NULLPOINTER, "%s", SDL_GetError());
texture = SDL_CreateTextureFromSurface(renderer->sdl_renderer, textsurf);
FAIL_ZERO_RETURN(errctx, texture, AKERR_NULLPOINTER, "%s", SDL_GetError());
dest.x = x;
dest.y = y;
SDL_GetTextureSize(texture, &dest.w, &dest.h);
PASS(errctx, renderer->draw_texture(renderer, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE));
SDL_DestroyTexture(texture);
SDL_DestroySurface(textsurf);
SUCCEED_RETURN(errctx);
}

View File

@@ -1,127 +1,186 @@
/**
* @file tilemap.c
* @brief Implements the tilemap subsystem.
*/
#include <string.h>
#include <libgen.h>
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h> #include <SDL3_mixer/SDL_mixer.h>
#include <string.h>
#include <jansson.h> #include <jansson.h>
#include <sdlerror.h>
#include <sdl3game/tilemap.h> #include <akerror.h>
#include <sdl3game/actor.h> #include <akstdlib.h>
#include <sdl3game/json_helpers.h>
#include <sdl3game/heap.h>
#include <sdl3game/registry.h>
#include <sdl3game/staticstring.h>
#include <sdl3game/game.h>
ErrorContext *get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest) #include <akgl/tilemap.h>
#include <akgl/actor.h>
#include <akgl/json_helpers.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include <akgl/game.h>
#include <akgl/util.h>
akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *properties = NULL; json_t *properties = NULL;
json_t *property = NULL; json_t *property = NULL;
string *tmpstr = NULL; akgl_String *tmpstr = NULL;
akgl_String *typestr = NULL;
int i = 0; int i = 0;
// This is not a generic JSON helper. It assumes we are receiving an object with a 'properties' key // This is not a generic JSON helper. It assumes we are receiving an object with a 'properties' key
// inside of it. That key is an array of objects, and each object has a name, type, and value. // inside of it. That key is an array of objects, and each object has a name, type, and value.
FAIL_ZERO_RETURN(errctx, obj, ERR_NULLPOINTER, "NULL json obj reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL json obj reference");
FAIL_ZERO_RETURN(errctx, key, ERR_NULLPOINTER, "NULL key string"); FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
ATTEMPT { ATTEMPT {
CATCH(errctx, get_json_array_value(obj, "properties", &properties)); CATCH(errctx, akgl_get_json_array_value(obj, "properties", &properties));
for (i = 0; i < json_array_size(properties); i++) { for (i = 0; i < json_array_size(properties); i++) {
CATCH(errctx, get_json_array_index_object(properties, i, &property)); CATCH(errctx, akgl_get_json_array_index_object(properties, i, &property));
CATCH(errctx, get_json_string_value(property, "name", &tmpstr)); CATCH(errctx, akgl_get_json_string_value(property, "name", &tmpstr));
if ( strcmp(tmpstr->data, key) != 0 ) { if ( strcmp(tmpstr->data, key) != 0 ) {
CATCH(errctx, heap_release_string(tmpstr)); CATCH(errctx, akgl_heap_release_string(tmpstr));
continue; continue;
} }
CATCH(errctx, get_json_string_value(property, "type", &tmpstr)); CATCH(errctx, akgl_get_json_string_value(property, "type", &typestr));
if ( strcmp(tmpstr->data, type) != 0 ) { if ( strcmp(typestr->data, type) != 0 ) {
FAIL_BREAK(errctx, ERR_TYPE, "Character property is present but is incorrect type"); FAIL_BREAK(errctx, AKERR_TYPE, "Property %s is present but is incorrect type(expected %s got %s)", key, type, (char *)typestr->data);
} }
*dest = property; *dest = property;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
} CLEANUP { } CLEANUP {
if ( tmpstr != NULL ) { if ( tmpstr != NULL ) {
IGNORE(heap_release_string(tmpstr)); IGNORE(akgl_heap_release_string(tmpstr));
}
if ( typestr != NULL ) {
IGNORE(akgl_heap_release_string(typestr));
} }
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
FAIL_RETURN(errctx, ERR_KEY, "Property not found in properties map"); FAIL_RETURN(errctx, AKERR_KEY, "Property not found in properties map");
} }
akerr_ErrorContext *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest)
ErrorContext *get_json_properties_string(json_t *obj, char *key, string **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *property; json_t *property;
ATTEMPT { PASS(errctx, akgl_get_json_tilemap_property(obj, key, "string", &property));
CATCH(errctx, get_json_tilemap_property(obj, key, "string", &property)); PASS(errctx, akgl_heap_next_string(dest));
CATCH(errctx, heap_next_string(dest)); PASS(errctx, akgl_string_initialize(*dest, NULL));
CATCH(errctx, string_initialize(*dest, NULL)); PASS(errctx, akgl_get_json_string_value(property, "value", dest));
CATCH(errctx, get_json_string_value(property, "value", dest));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *get_json_properties_integer(json_t *obj, char *key, int *dest) akerr_ErrorContext *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *property = NULL; json_t *property = NULL;
ATTEMPT { PASS(errctx, akgl_get_json_tilemap_property(obj, key, "int", &property));
CATCH(errctx, get_json_tilemap_property(obj, key, "int", &property)); PASS(errctx, akgl_get_json_integer_value(property, "value", dest));
CATCH(errctx, get_json_integer_value(property, "value", dest));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *tilemap_load_tilesets_each(json_t *tileset, tilemap *dest, int tsidx) /**
* @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); PREPARE_ERROR(errctx);
string *tmpstr = NULL; json_t *property = NULL;
ATTEMPT { PASS(errctx, akgl_get_json_tilemap_property(obj, key, "number", &property));
CATCH(errctx, get_json_integer_value((json_t *)tileset, "columns", &dest->tilesets[tsidx].columns)); PASS(errctx, akgl_get_json_number_value(property, "value", dest));
CATCH(errctx, get_json_integer_value((json_t *)tileset, "firstgid", &dest->tilesets[tsidx].firstgid));
CATCH(errctx, get_json_integer_value((json_t *)tileset, "imageheight", &dest->tilesets[tsidx].imageheight));
CATCH(errctx, get_json_integer_value((json_t *)tileset, "imagewidth", &dest->tilesets[tsidx].imagewidth));
CATCH(errctx, get_json_integer_value((json_t *)tileset, "margin", &dest->tilesets[tsidx].margin));
CATCH(errctx, get_json_integer_value((json_t *)tileset, "spacing", &dest->tilesets[tsidx].spacing));
CATCH(errctx, get_json_integer_value((json_t *)tileset, "tilecount", &dest->tilesets[tsidx].tilecount));
CATCH(errctx, get_json_integer_value((json_t *)tileset, "tileheight", &dest->tilesets[tsidx].tileheight));
CATCH(errctx, get_json_integer_value((json_t *)tileset, "tilewidth", &dest->tilesets[tsidx].tilewidth));
CATCH(errctx, get_json_string_value((json_t *)tileset, "name", &tmpstr));
strncpy((char *)&dest->tilesets[tsidx].name,
(char *)&tmpstr->data,
TILEMAP_MAX_TILESET_NAME_SIZE
);
CATCH(errctx, get_json_string_value((json_t *)tileset, "image", &tmpstr));
snprintf((char *)&dest->tilesets[tsidx].imagefilename,
TILEMAP_MAX_TILESET_FILENAME_SIZE,
"%s%s",
SDL_GetBasePath(),
tmpstr->data
);
dest->tilesets[tsidx].texture = IMG_LoadTexture(renderer, (char *)&dest->tilesets[tsidx].imagefilename);
FAIL_ZERO_BREAK(errctx, dest->tilesets[tsidx].texture, ERR_NULLPOINTER, "Failed loading tileset image");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *tilemap_compute_tileset_offsets(tilemap *dest, int tilesetidx) /**
* @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);
json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
PASS(errctx, akgl_get_json_number_value(property, "value", dest));
SUCCEED_RETURN(errctx);
}
/**
* @brief Get json properties double.
* @param obj Object to initialize, inspect, or modify.
* @param key JSON object key or property name to locate.
* @param dest Output destination populated by the function.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_* Propagates an error reported by a delegated operation.
*/
akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, double *dest)
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
PASS(errctx, akgl_get_json_double_value(property, "value", dest));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname)
{
PREPARE_ERROR(e);
akgl_String *tmpstr = NULL;
akgl_String *tmppath = NULL;
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "columns", &dest->tilesets[tsidx].columns));
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "firstgid", &dest->tilesets[tsidx].firstgid));
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "imageheight", &dest->tilesets[tsidx].imageheight));
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "imagewidth", &dest->tilesets[tsidx].imagewidth));
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "margin", &dest->tilesets[tsidx].margin));
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "spacing", &dest->tilesets[tsidx].spacing));
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "tilecount", &dest->tilesets[tsidx].tilecount));
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "tileheight", &dest->tilesets[tsidx].tileheight));
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(&tmppath));
ATTEMPT {
strncpy((char *)&dest->tilesets[tsidx].name,
(char *)&tmpstr->data,
AKGL_TILEMAP_MAX_TILESET_NAME_SIZE
);
CATCH(e, akgl_get_json_string_value((json_t *)tileset, "image", &tmpstr));
CATCH(e, akgl_path_relative((char *)&dirname->data, (char *)&tmpstr->data, tmppath));
strncpy((char *)&dest->tilesets[tsidx].imagefilename, tmppath->data, AKGL_MAX_STRING_LENGTH);
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpstr));
IGNORE(akgl_heap_release_string(tmppath));
} PROCESS(e) {
} FINISH(e, true);
dest->tilesets[tsidx].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)&dest->tilesets[tsidx].imagefilename);
FAIL_ZERO_RETURN(e, dest->tilesets[tsidx].texture, AKERR_NULLPOINTER, "Failed loading tileset image : %s", SDL_GetError());
SUCCEED_RETURN(e);
}
akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx)
{ {
int x_offset = 0; int x_offset = 0;
int y_offset = 0; int y_offset = 0;
@@ -176,11 +235,11 @@ ErrorContext *tilemap_compute_tileset_offsets(tilemap *dest, int tilesetidx)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *tilemap_load_tilesets(tilemap *dest, json_t *root) akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "Received NULL tilemap pointer"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "Received NULL tilemap pointer");
FAIL_ZERO_RETURN(errctx, root, ERR_NULLPOINTER, "Received NULL json object pointer"); FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "Received NULL json object pointer");
json_t *tilesets = NULL; json_t *tilesets = NULL;
json_t *jstileset = NULL; json_t *jstileset = NULL;
@@ -188,11 +247,11 @@ ErrorContext *tilemap_load_tilesets(tilemap *dest, json_t *root)
dest->numtilesets = 0; dest->numtilesets = 0;
ATTEMPT { ATTEMPT {
CATCH(errctx, get_json_array_value(root, "tilesets", &tilesets)) CATCH(errctx, akgl_get_json_array_value(root, "tilesets", &tilesets))
for (i = 0; i < json_array_size((json_t *)tilesets); i++) { for (i = 0; i < json_array_size((json_t *)tilesets); i++) {
CATCH(errctx, get_json_array_index_object((json_t *)tilesets, i, &jstileset)); CATCH(errctx, akgl_get_json_array_index_object((json_t *)tilesets, i, &jstileset));
CATCH(errctx, tilemap_load_tilesets_each(jstileset, dest, i)); CATCH(errctx, akgl_tilemap_load_tilesets_each(jstileset, dest, i, dirname));
CATCH(errctx, tilemap_compute_tileset_offsets(dest, i)); CATCH(errctx, akgl_tilemap_compute_tileset_offsets(dest, i));
dest->numtilesets += 1; dest->numtilesets += 1;
} }
} CLEANUP { } CLEANUP {
@@ -202,37 +261,46 @@ ErrorContext *tilemap_load_tilesets(tilemap *dest, json_t *root)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *tilemap_load_layer_object_actor(tilemap_object *curobj, json_t *layerdatavalue, int layerid) /**
* @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); PREPARE_ERROR(errctx);
string *tmpstr = NULL; akgl_String *tmpstr = NULL;
actor *actorobj = NULL; akgl_Actor *actorobj = NULL;
curobj->type = TILEMAP_OBJECT_TYPE_ACTOR; curobj->type = AKGL_TILEMAP_OBJECT_TYPE_ACTOR;
if ( strlen((char *)&curobj->name) == 0 ) { if ( strlen((char *)&curobj->name) == 0 ) {
FAIL_RETURN(errctx, ERR_KEY, "Actor in tile object layer cannot have empty name"); FAIL_RETURN(errctx, AKERR_KEY, "Actor in tile object layer cannot have empty name");
} }
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&tmpstr)); CATCH(errctx, akgl_heap_next_string(&tmpstr));
actorobj = SDL_GetPointerProperty(REGISTRY_ACTOR, (char *)&curobj->name, NULL); actorobj = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, (char *)&curobj->name, NULL);
if ( actorobj == NULL ) { if ( actorobj == NULL ) {
CATCH(errctx, heap_next_actor(&actorobj)); CATCH(errctx, akgl_heap_next_actor(&actorobj));
CATCH(errctx, actor_initialize((actor *)actorobj, (char *)&curobj->name)); CATCH(errctx, akgl_actor_initialize((akgl_Actor *)actorobj, (char *)&curobj->name));
CATCH(errctx, get_json_properties_string((json_t *)layerdatavalue, "character", &tmpstr)); CATCH(errctx, akgl_get_json_properties_string((json_t *)layerdatavalue, "character", &tmpstr));
CATCH(errctx, CATCH(errctx,
actor_set_character( akgl_actor_set_character(
(actor *)actorobj, (akgl_Actor *)actorobj,
(char *)&tmpstr->data (char *)&tmpstr->data
) )
); );
} else { } else {
actorobj->refcount += 1; actorobj->refcount += 1;
} }
CATCH(errctx, get_json_properties_integer((json_t *)layerdatavalue, "state", &actorobj->state)); CATCH(errctx, akgl_get_json_properties_integer((json_t *)layerdatavalue, "state", &actorobj->state));
} CLEANUP { } CLEANUP {
if ( tmpstr != NULL ) { if ( tmpstr != NULL ) {
IGNORE(heap_release_string(tmpstr)); IGNORE(akgl_heap_release_string(tmpstr));
} }
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -241,72 +309,80 @@ ErrorContext *tilemap_load_layer_object_actor(tilemap_object *curobj, json_t *la
actorobj->x = curobj->x; actorobj->x = curobj->x;
actorobj->y = curobj->y; actorobj->y = curobj->y;
actorobj->visible = curobj->visible; actorobj->visible = curobj->visible;
curobj->actorptr = (actor *)actorobj; curobj->actorptr = (akgl_Actor *)actorobj;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *tilemap_load_layer_objects(tilemap *dest, json_t *root, int layerid) akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *layerdata = NULL; json_t *layerdata = NULL;
json_t *layerdatavalue = NULL; json_t *layerdatavalue = NULL;
int j; int j;
int len; int len;
tilemap_layer *curlayer = NULL; akgl_TilemapLayer *curlayer = NULL;
tilemap_object *curobj = NULL; akgl_TilemapObject *curobj = NULL;
string *tmpstr = NULL; akgl_String *tmpstr = NULL;
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "NULL destination tilemap reference"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination tilemap reference");
FAIL_ZERO_RETURN(errctx, root, ERR_NULLPOINTER, "NULL tilemap root reference"); FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");
ATTEMPT { PASS(errctx, akgl_get_json_array_value(root, "objects", &layerdata));
CATCH(errctx, get_json_array_value(root, "objects", &layerdata));
len = json_array_size((json_t *)layerdata); len = json_array_size((json_t *)layerdata);
curlayer = &dest->layers[layerid]; curlayer = &dest->layers[layerid];
for ( j = 0; j < len; j++ ) { for ( j = 0; j < len; j++ ) {
CATCH(errctx, get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue)); PASS(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
curobj = &curlayer->objects[j]; curobj = &curlayer->objects[j];
CATCH(errctx, get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr)); PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
strncpy((char *)curobj->name, tmpstr->data, ACTOR_MAX_NAME_LENGTH); strncpy((char *)curobj->name, tmpstr->data, AKGL_ACTOR_MAX_NAME_LENGTH);
CATCH(errctx, heap_release_string(tmpstr)); PASS(errctx, akgl_heap_release_string(tmpstr));
CATCH(errctx, get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x)); PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
CATCH(errctx, get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y)); PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
CATCH(errctx, get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible)); PASS(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
CATCH(errctx, get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr)); PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
if ( strcmp(tmpstr->data, "actor") == 0 ) { if ( strcmp(tmpstr->data, "actor") == 0 ) {
CATCH(errctx, tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid)); PASS(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
} else if ( strcmp(tmpstr->data, "perspective") == 0 ) {
curobj->visible = false;
if ( strcmp((char *)curobj->name, "p_foreground") == 0 ) {
dest->p_foreground_y = curobj->y;
PASS(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_foreground_h));
PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_foreground_scale));
} else if ( strcmp((char *)curobj->name, "p_vanishing") == 0 ) {
dest->p_vanishing_y = curobj->y;
PASS(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
}
} }
layerdatavalue = NULL; layerdatavalue = NULL;
} }
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *tilemap_load_layer_tile(tilemap *dest, json_t *root, int layerid) akerr_ErrorContext *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *layerdata = NULL; json_t *layerdata = NULL;
int j; int j;
int layerdatalen; int layerdatalen;
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "NULL destination tilemap reference"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination tilemap reference");
FAIL_ZERO_RETURN(errctx, root, ERR_NULLPOINTER, "NULL tilemap root reference"); FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");
FAIL_ZERO_RETURN(errctx, dirname, AKERR_NULLPOINTER, "dirname");
ATTEMPT { ATTEMPT {
CATCH(errctx, get_json_integer_value(root, "height", &dest->layers[layerid].height)); CATCH(errctx, akgl_get_json_integer_value(root, "height", &dest->layers[layerid].height));
CATCH(errctx, get_json_integer_value(root, "width", &dest->layers[layerid].width)); CATCH(errctx, akgl_get_json_integer_value(root, "width", &dest->layers[layerid].width));
CATCH(errctx, get_json_array_value(root, "data", &layerdata)); CATCH(errctx, akgl_get_json_array_value(root, "data", &layerdata));
layerdatalen = (dest->layers[layerid].width * dest->layers[layerid].height); layerdatalen = (dest->layers[layerid].width * dest->layers[layerid].height);
if ( layerdatalen >= (TILEMAP_MAX_WIDTH * TILEMAP_MAX_HEIGHT) ) { if ( layerdatalen >= (AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT) ) {
FAIL_BREAK(errctx, ERR_OUTOFBOUNDS, "Map layer exceeds the maximum size"); FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Map layer exceeds the maximum size");
} }
for ( j = 0; j < layerdatalen; j++ ) { for ( j = 0; j < layerdatalen; j++ ) {
CATCH(errctx, get_json_array_index_integer(layerdata, j, &dest->layers[layerid].data[j])); CATCH(errctx, akgl_get_json_array_index_integer(layerdata, j, &dest->layers[layerid].data[j]));
} }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
@@ -315,47 +391,95 @@ ErrorContext *tilemap_load_layer_tile(tilemap *dest, json_t *root, int layerid)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *tilemap_load_layers(tilemap *dest, json_t *root) /**
* @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); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "tilemap_load_layers received NULL tilemap pointer"); akgl_String *tmpstr;
FAIL_ZERO_RETURN(errctx, root, ERR_NULLPOINTER, "tilemap_load_layers received NULL json object pointer"); akgl_String *fpath;
json_t *layers = NULL;
json_t *layer = NULL; FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination tilemap reference");
string *tmpstr = NULL; FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");
int i; FAIL_ZERO_RETURN(errctx, dirname, AKERR_NULLPOINTER, "dirname");
int tmpint;
ATTEMPT { ATTEMPT {
CATCH(errctx, get_json_array_value(root, "layers", &layers)); CATCH(errctx, akgl_heap_next_string(&tmpstr));
CATCH(errctx, akgl_heap_next_string(&fpath));
CATCH(errctx, akgl_get_json_string_value(root, "image", &tmpstr));
DISABLE_GCC_WARNING_FORMAT_TRUNCATION
snprintf((char *)&fpath->data,
AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE,
"%s/%s",
dirname->data,
tmpstr->data
);
RESTORE_GCC_WARNINGS
dest->layers[layerid].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)fpath->data);
FAIL_ZERO_BREAK(errctx, dest->layers[layerid].texture, AKGL_ERR_SDL, "%s", SDL_GetError());
dest->layers[layerid].width = dest->layers[layerid].texture->w;
dest->layers[layerid].height = dest->layers[layerid].texture->h;
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpstr));
IGNORE(akgl_heap_release_string(fpath));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "akgl_tilemap_load_layers received NULL tilemap pointer");
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "akgl_tilemap_load_layers received NULL json object pointer");
FAIL_ZERO_RETURN(errctx, dirname, AKERR_NULLPOINTER, "dirname");
json_t *layers = NULL;
json_t *layer = NULL;
akgl_String *tmpstr = NULL;
int i;
int layerid = 0;
int tmpint = 0;
ATTEMPT {
CATCH(errctx, akgl_get_json_array_value(root, "layers", &layers));
dest->numlayers = json_array_size((json_t *)layers); dest->numlayers = json_array_size((json_t *)layers);
for ( i = 0; i < dest->numlayers; i++) { for ( i = 0; i < dest->numlayers; i++) {
if ( i >= TILEMAP_MAX_LAYERS ) { if ( i >= AKGL_TILEMAP_MAX_LAYERS ) {
FAIL_BREAK(errctx, ERR_OUTOFBOUNDS, "Map exceeds the maximum number of layers"); FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Map exceeds the maximum number of layers");
}
CATCH(errctx, get_json_array_index_object((json_t *)layers, i, &layer));
CATCH(errctx, get_json_integer_value((json_t *)layer, "id", &tmpint));
if ( (tmpint - 1) != i ) {
// TileD's map layer IDs start at 1, not 0, and are sequential but not necessarily contiguous. We may have a gap in IDs.
layer = NULL;
continue;
} }
CATCH(errctx, akgl_get_json_array_index_object((json_t *)layers, i, &layer));
CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "id", &tmpint));
CATCH(errctx, akgl_get_json_number_value((json_t *)layer, "opacity", &dest->layers[layerid].opacity));
CATCH(errctx, akgl_get_json_boolean_value((json_t *)layer, "visible", &dest->layers[layerid].visible));
CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "id", &dest->layers[layerid].id));
CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "x", &dest->layers[layerid].x));
CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "y", &dest->layers[layerid].y));
CATCH(errctx, get_json_number_value((json_t *)layer, "opacity", &dest->layers[i].opacity)); CATCH(errctx, akgl_get_json_string_value((json_t *)layer, "type", &tmpstr));
CATCH(errctx, get_json_boolean_value((json_t *)layer, "visible", &dest->layers[i].visible)); SDL_Log("Layer %d has type %s", layerid, tmpstr->data);
CATCH(errctx, get_json_integer_value((json_t *)layer, "id", &dest->layers[i].id));
CATCH(errctx, get_json_integer_value((json_t *)layer, "x", &dest->layers[i].x));
CATCH(errctx, get_json_integer_value((json_t *)layer, "y", &dest->layers[i].y));
CATCH(errctx, get_json_string_value((json_t *)layer, "type", &tmpstr));
if ( strncmp((char *)tmpstr->data, "objectgroup", strlen((char *)tmpstr->data)) == 0 ) { if ( strncmp((char *)tmpstr->data, "objectgroup", strlen((char *)tmpstr->data)) == 0 ) {
dest->layers[i].type = TILEMAP_LAYER_TYPE_OBJECTS; dest->layers[layerid].type = AKGL_TILEMAP_LAYER_TYPE_OBJECTS;
CATCH(errctx, tilemap_load_layer_objects((tilemap *)dest, (json_t *)layer, i)); CATCH(errctx, akgl_tilemap_load_layer_objects((akgl_Tilemap *)dest, (json_t *)layer, layerid, dirname));
} else if ( strncmp((char *)tmpstr->data, "tilelayer", strlen((char *)tmpstr->data)) == 0 ) { } else if ( strncmp((char *)tmpstr->data, "tilelayer", strlen((char *)tmpstr->data)) == 0 ) {
dest->layers[i].type = TILEMAP_LAYER_TYPE_TILES; dest->layers[layerid].type = AKGL_TILEMAP_LAYER_TYPE_TILES;
CATCH(errctx, tilemap_load_layer_tile((tilemap *)dest, (json_t *)layer, i)); CATCH(errctx, akgl_tilemap_load_layer_tile((akgl_Tilemap *)dest, (json_t *)layer, layerid, dirname));
} else if ( strncmp((char *)tmpstr->data, "imagelayer", strlen((char *)tmpstr->data)) == 0 ) {
dest->layers[layerid].type = AKGL_TILEMAP_LAYER_TYPE_IMAGE;
CATCH(errctx, akgl_tilemap_load_layer_image((akgl_Tilemap *)dest, (json_t *)layer, layerid, dirname));
} }
layer = NULL; layer = NULL;
layerid += 1;
} }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
@@ -363,53 +487,163 @@ ErrorContext *tilemap_load_layers(tilemap *dest, json_t *root)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *tilemap_load(char *fname, tilemap *dest) /**
* @brief Tilemap load physics.
* @param dest Output destination populated by the function.
* @param root Base directory used to resolve the path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_KEY When the corresponding validation or operation fails.
*/
akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
{
PREPARE_ERROR(e);
json_t *props = NULL;
akgl_String *tmpval = NULL;
double defzero = 0.0;
ATTEMPT {
CATCH(e, akgl_heap_next_string(&tmpval));
CATCH(e, akgl_get_json_array_value((json_t *)root, "properties", &props));
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpval));
} PROCESS(e) {
} HANDLE(e, AKERR_KEY) {
// Map has no properties, do nothing
SDL_Log("Map has no properties");
SUCCEED_RETURN(e);
} FINISH(e, true);
ATTEMPT {
CATCH(e, akgl_heap_next_string(&tmpval));
CATCH(e, akgl_get_json_properties_string(
root,
"physics.model",
&tmpval
)
);
PASS(e, akgl_physics_factory(&dest->physics, tmpval));
dest->use_own_physics = true;
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.gravity.x", &dest->physics.gravity_x
),
(void *)&defzero,
(void *)&dest->physics.gravity_x,
sizeof(double)));
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.gravity.y", &dest->physics.gravity_y
),
(void *)&defzero,
(void *)&dest->physics.gravity_y,
sizeof(double)));
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.gravity.z", &dest->physics.gravity_z
),
(void *)&defzero,
(void *)&dest->physics.gravity_z,
sizeof(double)));
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.drag.x", &dest->physics.drag_x
),
(void *)&defzero,
(void *)&dest->physics.drag_x,
sizeof(double)));
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.drag.y", &dest->physics.drag_y
),
(void *)&defzero,
(void *)&dest->physics.drag_y,
sizeof(double)));
CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.drag.z", &dest->physics.drag_z
),
(void *)&defzero,
(void *)&dest->physics.drag_z,
sizeof(double)));
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpval));
} PROCESS(e) {
} HANDLE(e, AKERR_KEY) {
SDL_Log("Map uses game physics");
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *json = NULL; json_t *json = NULL;
string *tmpstr = NULL; //akgl_String *tmpstr = NULL;
json_error_t error; json_error_t error;
akgl_String *dirnamestr = NULL;
FAIL_ZERO_RETURN(errctx, fname, ERR_NULLPOINTER, "load_tilemap received null filename"); FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "load_tilemap received null filename");
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "load_tilemap received null tilemap"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "load_tilemap received null tilemap");
memset(dest, 0x00, sizeof(tilemap));
memset(dest, 0x00, sizeof(akgl_Tilemap));
dest->p_foreground_scale = 1.0;
dest->p_vanishing_scale = 1.0;
PASS(errctx, akgl_heap_next_string(&dirnamestr));
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&tmpstr)); //CATCH(errctx, akgl_heap_next_string(&tmpstr));
CATCH(errctx, string_initialize(tmpstr, NULL)); //CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
SDL_snprintf(tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname); //SDL_snprintf(tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
json = json_load_file(tmpstr->data, 0, &error); CATCH(errctx, aksl_realpath(fname, (char *)&dirnamestr->data));
dirname((char *)&dirnamestr->data);
json = json_load_file(fname, 0, &error);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
json, json,
ERR_NULLPOINTER, AKERR_NULLPOINTER,
"Error while loading tilemap from %s on line %d: %s-", "Error while loading tilemap from %s on line %d: %s-",
tmpstr->data, fname,
error.line, error.line,
error.text error.text
); );
CATCH(errctx, get_json_integer_value((json_t *)json, "tileheight", &dest->tileheight)); CATCH(errctx, akgl_tilemap_load_physics(dest, json));
CATCH(errctx, get_json_integer_value((json_t *)json, "tilewidth", &dest->tilewidth)); CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "tileheight", &dest->tileheight));
CATCH(errctx, get_json_integer_value((json_t *)json, "height", &dest->height)); CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "tilewidth", &dest->tilewidth));
CATCH(errctx, get_json_integer_value((json_t *)json, "width", &dest->width)); CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &dest->height));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &dest->width));
dest->orientation = 0; dest->orientation = 0;
if ( (dest->width * dest->height) >= (TILEMAP_MAX_WIDTH * TILEMAP_MAX_HEIGHT) ) { if ( (dest->width * dest->height) >= (AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT) ) {
FAIL_RETURN(errctx, ERR_OUTOFBOUNDS, "Map exceeds the maximum size"); FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Map exceeds the maximum size");
} }
CATCH(errctx, tilemap_load_layers((tilemap *)dest, (json_t *)json)); CATCH(errctx, akgl_tilemap_load_layers((akgl_Tilemap *)dest, (json_t *)json, dirnamestr));
CATCH(errctx, tilemap_load_tilesets((tilemap *)dest, (json_t *)json)); CATCH(errctx, akgl_tilemap_load_tilesets((akgl_Tilemap *)dest, (json_t *)json, dirnamestr));
if ( dest->p_foreground_y && dest->p_vanishing_y ) {
// How much bigger is the foreground vs the vanishing point?
// if vanishing is height 16, and foreground is height 32, that is a 2x scale difference
/* dest->p_scale = ((float)dest->p_foreground_h / (float)dest->p_vanishing_h); */
/* SDL_Log("Map perspective scale is (%d/%d) = %f", dest->p_foreground_h, dest->p_vanishing_h, dest->p_scale); */
// Sprites are scale N (default 1.0) at the foreground, so how much do we need to
// scale them for every pixel above foreground_y before they reach vanishing_y?
// If vanishing is at 320 and foreground is at 640, that is a 320 line difference
// If our scaling rate is 2x, then our rate is (((N=1.0) / (640 - 320)) / (dest->p_scale = 2)), or
// 0.0066% scale per pixel.
dest->p_rate = ((dest->p_foreground_scale - dest->p_vanishing_scale) / (dest->p_foreground_y - dest->p_vanishing_y));
SDL_Log("Map perspective rate is %f", dest->p_rate);
}
} CLEANUP { } CLEANUP {
IGNORE(heap_release_string(tmpstr)); //IGNORE(akgl_heap_release_string(tmpstr));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *tilemap_draw(SDL_Renderer *renderer, tilemap *map, SDL_FRect *viewport, int layeridx) akerr_ErrorContext *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, int layeridx)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
SDL_FRect dest = {.x = 0, .y = 0, .w = 0, .h = 0};; SDL_FRect dest = {.x = 0, .y = 0, .w = 0, .h = 0};;
@@ -439,8 +673,8 @@ ErrorContext *tilemap_draw(SDL_Renderer *renderer, tilemap *map, SDL_FRect *view
* 0 and 8 would not be rendered. 1, 9, 6, and E would be partially rendered at their corner. * 0 and 8 would not be rendered. 1, 9, 6, and E would be partially rendered at their corner.
* 2,3,4,5 and A,B,C,D would be partially rendered with a slice from their center. * 2,3,4,5 and A,B,C,D would be partially rendered with a slice from their center.
*/ */
FAIL_ZERO_RETURN(errctx, map, ERR_NULLPOINTER, "tilemap_draw received NULL pointer to tilemap"); FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "akgl_tilemap_draw received NULL pointer to tilemap");
FAIL_ZERO_RETURN(errctx, viewport, ERR_NULLPOINTER, "tilemap_draw received NULL pointer to viewport"); FAIL_ZERO_RETURN(errctx, viewport, AKERR_NULLPOINTER, "akgl_tilemap_draw received NULL pointer to viewport");
/* Only try to render the stuff that is partially within the viewport */ /* Only try to render the stuff that is partially within the viewport */
@@ -458,6 +692,17 @@ ErrorContext *tilemap_draw(SDL_Renderer *renderer, tilemap *map, SDL_FRect *view
/*SDL_Log("Rendering map into viewport from (%d, %d) to (%d, %d)", /*SDL_Log("Rendering map into viewport from (%d, %d) to (%d, %d)",
start_x, start_y, end_x, end_y);*/ start_x, start_y, end_x, end_y);*/
if ( map->layers[layeridx].type == AKGL_TILEMAP_LAYER_TYPE_IMAGE ) {
dest.x = 0;
dest.y = 0;
src.w = map->layers[layeridx].width;
src.h = map->layers[layeridx].height;
dest.w = map->layers[layeridx].width;
dest.h = map->layers[layeridx].height;
PASS(errctx, renderer->draw_texture(renderer, map->layers[layeridx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
SUCCEED_RETURN(errctx);
}
dest.x = 0; dest.x = 0;
dest.y = 0; dest.y = 0;
dest.w = map->tilewidth; dest.w = map->tilewidth;
@@ -509,7 +754,7 @@ ErrorContext *tilemap_draw(SDL_Renderer *renderer, tilemap *map, SDL_FRect *view
dest.y, dest.y,
dest.w, dest.w,
dest.h);*/ dest.h);*/
SDL_RenderTexture(renderer, map->tilesets[tilesetidx].texture, &src, &dest); PASS(errctx, renderer->draw_texture(renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
} }
} }
dest.x += map->tilewidth; dest.x += map->tilewidth;
@@ -519,20 +764,20 @@ ErrorContext *tilemap_draw(SDL_Renderer *renderer, tilemap *map, SDL_FRect *view
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *tilemap_draw_tileset(SDL_Renderer *renderer, tilemap *map, int tilesetidx) akerr_ErrorContext *akgl_tilemap_draw_tileset(akgl_Tilemap *map, int tilesetidx)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
SDL_FRect dest; SDL_FRect dest;
SDL_FRect src; SDL_FRect src;
int tilenum = 0; int tilenum = 0;
/* /*
* Render every tile in a tileset to the given renderer * Render every tile in a tileset to the default renderer
* (this is a debugging tool that shows that the recorded tile offsets are correct, * (this is a debugging tool that shows that the recorded tile offsets are correct,
* by proving that we can reconstruct the original tileset image) * by proving that we can reconstruct the original tileset image)
*/ */
FAIL_ZERO_RETURN(errctx, map, ERR_NULLPOINTER, "tilemap_draw_tileset received NULL pointer to tilemap"); FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "akgl_tilemap_draw_tileset received NULL pointer to tilemap");
FAIL_NONZERO_RETURN(errctx, (tilesetidx >= map->numtilesets), ERR_OUTOFBOUNDS, "tilemap_draw_tileset received a tileset index out of bounds"); FAIL_NONZERO_RETURN(errctx, (tilesetidx >= map->numtilesets), AKERR_OUTOFBOUNDS, "akgl_tilemap_draw_tileset received a tileset index out of bounds");
for ( tilenum = 0; tilenum < map->tilesets[tilesetidx].tilecount; tilenum++) { for ( tilenum = 0; tilenum < map->tilesets[tilesetidx].tilecount; tilenum++) {
// Render this tile to the correct screen position // Render this tile to the correct screen position
@@ -564,7 +809,45 @@ ErrorContext *tilemap_draw_tileset(SDL_Renderer *renderer, tilemap *map, int til
dest.y, dest.y,
dest.w, dest.w,
dest.h);*/ dest.h);*/
SDL_RenderTexture(renderer, map->tilesets[tilesetidx].texture, &src, &dest); PASS(errctx, renderer->draw_texture(renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "NULL map");
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "NULL actor");
if ( actor->y <= map->p_vanishing_y ) {
actor->scale = map->p_vanishing_scale;
} else if ( actor->y >= map->p_foreground_y ) {
actor->scale = map->p_foreground_scale;
} else {
actor->scale = map->p_foreground_scale - (map->p_rate * (map->p_foreground_y - actor->y));
}
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest)
{
// Release all tileset textures
// Release all image layer textures
// Memset to zero
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "NULL map");
int i = 0;
for ( i = 0; i < AKGL_TILEMAP_MAX_TILESETS; i++ ) {
if ( dest->tilesets[i].texture != NULL ) {
SDL_DestroyTexture(dest->tilesets[i].texture);
}
}
for ( i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
if ( dest->layers[i].texture != NULL ) {
SDL_DestroyTexture(dest->tilesets[i].texture);
}
}
SUCCEED_RETURN(e);
}

View File

@@ -1,20 +1,125 @@
/**
* @file util.c
* @brief Implements the util subsystem.
*/
#include <limits.h> #include <limits.h>
#include <stdlib.h> #include <stdlib.h>
#include <errno.h>
#include <libgen.h>
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/util.h> #include <akgl/util.h>
#include <sdl3game/heap.h> #include <akgl/heap.h>
#include <sdl3game/registry.h> #include <akgl/registry.h>
#include <sdl3game/game.h> #include <akgl/game.h>
#include <akgl/staticstring.h>
ErrorContext *rectangle_points(RectanglePoints *dest, SDL_FRect *rect) #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);
akgl_String *pathbuf;
akgl_String *strbuf;
char *result;
int rootlen;
int pathlen;
int count;
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
PASS(e, akgl_heap_next_string(&strbuf));
PASS(e, akgl_heap_next_string(&pathbuf));
ATTEMPT {
// Is it relative to the root?
rootlen = strlen(root);
pathlen = strlen(path);
if ( (rootlen + pathlen) >= AKGL_MAX_STRING_LENGTH ) {
FAIL_RETURN(e, AKERR_OUTOFBOUNDS, "Total path length (%d) is greater than maximum akgl_String length (%d)", (rootlen + pathlen), AKGL_MAX_STRING_LENGTH);
}
DISABLE_GCC_WARNING_FORMAT_TRUNCATION
CATCH(e, aksl_sprintf(&count, (char *)&pathbuf->data, "%s/%s", root, path));
RESTORE_GCC_WARNINGS
CATCH(e, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data));
CATCH(e, akgl_string_copy(strbuf, dst, 0));
} CLEANUP {
IGNORE(akgl_heap_release_string(strbuf));
IGNORE(akgl_heap_release_string(pathbuf));
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
{
PREPARE_ERROR(e);
akgl_String *strbuf;
char *result;
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
PASS(e, akgl_heap_next_string(&strbuf));
ATTEMPT {
// Is path relative to our current working directory?
CATCH(e, aksl_realpath(path, (char *)&strbuf->data));
// Yes it is. strbuf->data contains the absolute path.
CATCH(e, akgl_string_copy(strbuf, dst, 0));
} CLEANUP {
IGNORE(akgl_heap_release_string(strbuf));
} PROCESS(e) {
} HANDLE(e, ENOENT) {
// Path is not relative to our current working directory
// Noop - execution proceeds after the break
return akgl_path_relative_root(root, path, dst);
} FINISH(e, true);
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;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path");
FAIL_ZERO_RETURN(e, from, AKERR_NULLPOINTER, "from");
PASS(e, akgl_heap_next_string(&dirnamestr));
PASS(e, aksl_realpath(from, (char *)&dirnamestr->data));
dirname((char *)&dirnamestr->data);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, ERR_NULLPOINTER, "NULL RectanglePoints reference"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, rect, ERR_NULLPOINTER, "NULL Rectangle reference"); FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL Rectangle reference");
dest->topleft.x = rect->x; dest->topleft.x = rect->x;
dest->topleft.y = rect->y; dest->topleft.y = rect->y;
dest->bottomleft.x = rect->x; dest->bottomleft.x = rect->x;
@@ -26,12 +131,12 @@ ErrorContext *rectangle_points(RectanglePoints *dest, SDL_FRect *rect)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *collide_point_rectangle(point *p, RectanglePoints *rp, bool *collide) akerr_ErrorContext *akgl_collide_point_rectangle(point *p, RectanglePoints *rp, bool *collide)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, p, ERR_NULLPOINTER, "NULL Point reference"); FAIL_ZERO_RETURN(errctx, p, AKERR_NULLPOINTER, "NULL Point reference");
FAIL_ZERO_RETURN(errctx, rp, ERR_NULLPOINTER, "NULL RectanglePoints reference"); FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, collide, ERR_NULLPOINTER, "NULL boolean reference"); FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL boolean reference");
if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) && if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) &&
(p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) { (p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) {
*collide = true; *collide = true;
@@ -41,49 +146,49 @@ ErrorContext *collide_point_rectangle(point *p, RectanglePoints *rp, bool *colli
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide) akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide)
{ {
RectanglePoints r1p; RectanglePoints r1p;
RectanglePoints r2p; RectanglePoints r2p;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, r1, ERR_NULLPOINTER, "NULL rectangle reference"); FAIL_ZERO_RETURN(errctx, r1, AKERR_NULLPOINTER, "NULL rectangle reference");
FAIL_ZERO_RETURN(errctx, r2, ERR_NULLPOINTER, "NULL rectangle reference"); FAIL_ZERO_RETURN(errctx, r2, AKERR_NULLPOINTER, "NULL rectangle reference");
FAIL_ZERO_RETURN(errctx, collide, ERR_NULLPOINTER, "NULL collision flag reference"); FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL collision flag reference");
ATTEMPT { ATTEMPT {
CATCH(errctx, rectangle_points(&r1p, r1)); CATCH(errctx, akgl_rectangle_points(&r1p, r1));
CATCH(errctx, rectangle_points(&r2p, r2)); CATCH(errctx, akgl_rectangle_points(&r2p, r2));
// is the upper left corner of r1 contacting r2? // is the upper left corner of r1 contacting r2?
CATCH(errctx, collide_point_rectangle(&r1p.topleft, &r2p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r1p.topleft, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the upper left corner of r2 contacting r1? // is the upper left corner of r2 contacting r1?
CATCH(errctx, collide_point_rectangle(&r2p.topleft, &r1p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r2p.topleft, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the top right corner of r1 contacting r2? // is the top right corner of r1 contacting r2?
CATCH(errctx, collide_point_rectangle(&r1p.topright, &r2p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r1p.topright, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the top right corner of r2 contacting r1? // is the top right corner of r2 contacting r1?
CATCH(errctx, collide_point_rectangle(&r2p.topright, &r1p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r2p.topright, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom left corner of r1 contacting r2? // is the bottom left corner of r1 contacting r2?
CATCH(errctx, collide_point_rectangle(&r1p.bottomleft, &r2p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomleft, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom left corner of r2 contacting r1? // is the bottom left corner of r2 contacting r1?
CATCH(errctx, collide_point_rectangle(&r2p.bottomleft, &r1p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomleft, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom right corner of r1 contacting r2? // is the bottom right corner of r1 contacting r2?
CATCH(errctx, collide_point_rectangle(&r1p.bottomright, &r2p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomright, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom right corner of r2 contacting r1? // is the bottom right corner of r2 contacting r1?
CATCH(errctx, collide_point_rectangle(&r2p.bottomright, &r1p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomright, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { SUCCEED_RETURN(errctx); }
} CLEANUP { } CLEANUP {
@@ -95,75 +200,59 @@ ErrorContext *collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide)
} }
ErrorContext *compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2) akerr_ErrorContext *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, s1, ERR_NULLPOINTER, "NULL Surface pointer"); FAIL_ZERO_RETURN(errctx, s1, AKERR_NULLPOINTER, "NULL Surface pointer");
FAIL_ZERO_RETURN(errctx, s2, ERR_NULLPOINTER, "NULL Surface pointer"); FAIL_ZERO_RETURN(errctx, s2, AKERR_NULLPOINTER, "NULL Surface pointer");
FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), ERR_VALUE, "Comparison surfaces are not equal"); FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), AKERR_VALUE, "Comparison surfaces are not equal");
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout) akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout)
{ {
SDL_Surface *s1 = NULL; SDL_Surface *s1 = NULL;
SDL_Surface *s2 = NULL; SDL_Surface *s2 = NULL;
SDL_FRect src = {.x = x, .y = y, .w = w, .h = h}; SDL_FRect src = {.x = x, .y = y, .w = w, .h = h};
SDL_FRect dest = {.x = x, .y = y, .w = w, .h = h}; SDL_FRect dest = {.x = x, .y = y, .w = w, .h = h};
SDL_Rect read = {.x = x, .y = y, .w = w, .h = h}; SDL_Rect read = {.x = x, .y = y, .w = w, .h = h};
string *tmpstring = NULL; akgl_String *tmpstring = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
FAIL_ZERO_BREAK(errctx, t1, ERR_NULLPOINTER, "NULL texture"); FAIL_ZERO_BREAK(errctx, t1, AKERR_NULLPOINTER, "NULL texture");
FAIL_ZERO_BREAK(errctx, t2, ERR_NULLPOINTER, "NULL texture"); FAIL_ZERO_BREAK(errctx, t2, AKERR_NULLPOINTER, "NULL texture");
CATCH(errctx, heap_next_string(&tmpstring)); CATCH(errctx, akgl_heap_next_string(&tmpstring));
SDL_RenderClear(renderer); SDL_RenderClear(renderer->sdl_renderer);
FAIL_ZERO_BREAK( CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
errctx, s1 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
SDL_RenderTexture( FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer");
renderer,
t1,
&src,
&dest),
ERR_SDL,
"Failed to render test texture");
s1 = SDL_RenderReadPixels(renderer, &read);
FAIL_ZERO_BREAK(errctx, s1, ERR_SDL, "Failed to read pixels from renderer");
if ( writeout != NULL ) { if ( writeout != NULL ) {
snprintf((char *)&tmpstring->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), writeout); snprintf((char *)&tmpstring->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), writeout);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
IMG_SavePNG(s1, (char *)&tmpstring->data), IMG_SavePNG(s1, (char *)&tmpstring->data),
ERR_IO, AKERR_IO,
"Unable to save %s: %s", "Unable to save %s: %s",
(char *)&tmpstring->data, (char *)&tmpstring->data,
SDL_GetError()); SDL_GetError());
} }
SDL_RenderClear(renderer); SDL_RenderClear(renderer->sdl_renderer);
FAIL_ZERO_BREAK( CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
errctx, s2 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
SDL_RenderTexture( FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer");
renderer,
t2,
&src,
&dest),
ERR_SDL,
"Failed to render test texture");
s2 = SDL_RenderReadPixels(renderer, &read);
FAIL_ZERO_BREAK(errctx, s2, ERR_SDL, "Failed to read pixels from renderer");
CATCH(errctx, compare_sdl_surfaces(s1, s2)); CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2));
} CLEANUP { } CLEANUP {
if ( s1 != NULL ) if ( s1 != NULL )
SDL_DestroySurface(s1); SDL_DestroySurface(s1);
if ( s2 != NULL ) if ( s2 != NULL )
SDL_DestroySurface(s2); SDL_DestroySurface(s2);
IGNORE(heap_release_string(tmpstring)); IGNORE(akgl_heap_release_string(tmpstring));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);

File diff suppressed because it is too large Load Diff

View 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 }]
}

View File

@@ -1,18 +1,22 @@
{ {
"name": "testcharacter", "name": "testcharacter",
"velocity_x": 0.20, "movementspeed": 1,
"velocity_y": 0.20, "speedtime": 1,
"speed_x": 0.20,
"speed_y": 0.20,
"acceleration_x": 0.20,
"acceleration_y": 0.20,
"sprite_mappings": [ "sprite_mappings": [
{ {
"state": [ "state": [
"ACTOR_STATE_ALIVE", "AKGL_ACTOR_STATE_ALIVE",
"ACTOR_STATE_FACE_LEFT" "AKGL_ACTOR_STATE_FACE_LEFT"
], ],
"sprite": "testsprite" "sprite": "testsprite"
}, },
{ {
"state": [ "state": [
"ACTOR_STATE_DEAD" "AKGL_ACTOR_STATE_DEAD"
], ],
"sprite": "testsprite2" "sprite": "testsprite2"
} }

View File

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

View File

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

View File

@@ -1,31 +1,31 @@
#include <sdl3game/game.h> #include <akgl/game.h>
#include <sdl3game/actor.h> #include <akgl/actor.h>
int main(void) int main(void)
{ {
int mask = 0; int mask = 0;
BITMASK_ADD(mask, ACTOR_STATE_ALIVE); AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_ALIVE);
if ( mask != ACTOR_STATE_ALIVE ) if ( mask != AKGL_ACTOR_STATE_ALIVE )
return 1; return 1;
BITMASK_ADD(mask, ACTOR_STATE_FACE_LEFT); AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
if ( mask != (ACTOR_STATE_ALIVE | ACTOR_STATE_FACE_LEFT) ) if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT) )
return 1; return 1;
BITMASK_DEL(mask, ACTOR_STATE_ALIVE); AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_ALIVE);
if ( mask != (ACTOR_STATE_FACE_LEFT) ) if ( mask != (AKGL_ACTOR_STATE_FACE_LEFT) )
return 1; return 1;
BITMASK_CLEAR(mask); AKGL_BITMASK_CLEAR(mask);
if ( mask != 0 ) if ( mask != 0 )
return 1; return 1;
BITMASK_ADD(mask, ACTOR_STATE_FACE_LEFT); AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
if ( !(BITMASK_HAS(mask, ACTOR_STATE_FACE_LEFT)) ) if ( !(AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_FACE_LEFT)) )
return 1; return 1;
mask = ACTOR_STATE_ALIVE | ACTOR_STATE_FACE_UP; mask = AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP;
BITMASK_DEL(mask, ACTOR_STATE_FACE_ALL); AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_FACE_ALL);
if ( mask != ACTOR_STATE_ALIVE ) if ( mask != AKGL_ACTOR_STATE_ALIVE )
return 1; return 1;
BITMASK_ADD(mask, ACTOR_STATE_MOVING_DOWN); AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_MOVING_DOWN);
BITMASK_ADD(mask, ACTOR_STATE_FACE_DOWN); AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_DOWN);
if ( mask != (ACTOR_STATE_ALIVE | ACTOR_STATE_MOVING_DOWN | ACTOR_STATE_FACE_DOWN) ) if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN) )
return 1; return 1;
return 0; return 0;
} }

View File

@@ -1,186 +1,189 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/character.h> #include <akgl/character.h>
#include <sdl3game/actor.h> #include <akgl/actor.h>
#include <sdl3game/heap.h> #include <akgl/error.h>
#include <sdl3game/registry.h> #include <akgl/heap.h>
#include <sdl3game/iterator.h> #include <akgl/registry.h>
#include <akgl/iterator.h>
SDL_Window *window; SDL_Window *window;
SDL_Renderer *renderer; SDL_Renderer *renderer;
ErrorContext *test_character_initialize() akerr_ErrorContext *test_akgl_character_initialize()
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
character *testchar = NULL; akgl_Character *testchar = NULL;
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_character(&testchar)); CATCH(errctx, akgl_heap_next_character(&testchar));
CATCH(errctx, character_initialize(testchar, "testchar")); CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
SDL_GetPointerProperty(REGISTRY_CHARACTER, "testchar", NULL), SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testchar", NULL),
ERR_KEY, AKERR_KEY,
"Character was not placed in the registry"); "Character was not placed in the registry");
FAIL_NONZERO_BREAK( FAIL_NONZERO_BREAK(
errctx, errctx,
strcmp((char *)&testchar->name, "testchar"), strcmp((char *)&testchar->name, "testchar"),
ERR_VALUE, AKERR_VALUE,
"Character was not named properly ('testchar' vs '%s')", "Character was not named properly ('testchar' vs '%s')",
(char *)&testchar->name); (char *)&testchar->name);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
testchar->state_sprites, testchar->state_sprites,
ERR_NULLPOINTER, AKERR_NULLPOINTER,
"Character state sprites map was not initialized"); "Character state sprites map was not initialized");
} CLEANUP { } CLEANUP {
IGNORE(heap_release_character(testchar)); IGNORE(akgl_heap_release_character(testchar));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_character_sprite_mgmt() akerr_ErrorContext *test_character_sprite_mgmt()
{ {
character *testchar = NULL; akgl_Character *testchar = NULL;
sprite *testsprite = NULL; akgl_Sprite *testsprite = NULL;
sprite *testsprite2 = NULL; akgl_Sprite *testsprite2 = NULL;
sprite *comparesprite = NULL; akgl_Sprite *comparesprite = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_character(&testchar)); CATCH(errctx, akgl_heap_next_character(&testchar));
CATCH(errctx, sprite_load_json("assets/testsprite.json")); CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
testsprite = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite", NULL); CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
testsprite, testsprite,
ERR_KEY, AKERR_KEY,
"Sprite loaded from json but not in registry"); "Sprite loaded from json but not in registry");
CATCH(errctx, sprite_load_json("assets/testsprite2.json")); CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
testsprite2 = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite2", NULL); testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
testsprite2, testsprite2,
ERR_KEY, AKERR_KEY,
"Sprite 2 loaded from json but not in registry"); "Sprite 2 loaded from json but not in registry");
CATCH(errctx, testchar->sprite_add(testchar, testsprite, ACTOR_STATE_ALIVE)); CATCH(errctx, testchar->sprite_add(testchar, testsprite, AKGL_ACTOR_STATE_ALIVE));
CATCH(errctx, testchar->sprite_add(testchar, testsprite2, ACTOR_STATE_DEAD)); CATCH(errctx, testchar->sprite_add(testchar, testsprite2, AKGL_ACTOR_STATE_DEAD));
CATCH(errctx, testchar->sprite_get(testchar, (ACTOR_STATE_ALIVE), &comparesprite)); CATCH(errctx, testchar->sprite_get(testchar, (AKGL_ACTOR_STATE_ALIVE), &comparesprite));
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), ERR_VALUE, "Wrong sprite for state ACTOR_STATE_ALIVE | ACTOR_STATE_FACE_LEFT"); FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT");
CATCH(errctx, testchar->sprite_get(testchar, ACTOR_STATE_DEAD, &comparesprite)); CATCH(errctx, testchar->sprite_get(testchar, AKGL_ACTOR_STATE_DEAD, &comparesprite));
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), ERR_VALUE, "Wrong sprite for state ACTOR_STATE_DEAD"); FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_DEAD");
} CLEANUP { } CLEANUP {
IGNORE(heap_release_sprite(testsprite)); IGNORE(akgl_heap_release_sprite(testsprite));
IGNORE(heap_release_sprite(testsprite2)); IGNORE(akgl_heap_release_sprite(testsprite2));
IGNORE(heap_release_character(testchar)); IGNORE(akgl_heap_release_character(testchar));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_character_iterate_state_sprites() akerr_ErrorContext *test_character_iterate_state_sprites()
{ {
character *testchar = NULL; akgl_Character *testchar = NULL;
sprite *testsprite = NULL; akgl_Sprite *testsprite = NULL;
sprite *testsprite2 = NULL; akgl_Sprite *testsprite2 = NULL;
iterator opflags = {.flags = ITERATOR_OP_RELEASE, .layerid = 0}; akgl_Iterator opflags = {.flags = AKGL_ITERATOR_OP_RELEASE, .layerid = 0};
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_character(&testchar)); CATCH(errctx, akgl_heap_next_character(&testchar));
CATCH(errctx, sprite_load_json("assets/testsprite.json")); CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
testsprite = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite", NULL); CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
testsprite, testsprite,
ERR_KEY, AKERR_KEY,
"Sprite loaded from json but not in registry"); "Sprite loaded from json but not in registry");
CATCH(errctx, sprite_load_json("assets/testsprite2.json")); CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
testsprite2 = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite2", NULL); testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
testsprite2, testsprite2,
ERR_KEY, AKERR_KEY,
"Sprite 2 loaded from json but not in registry"); "Sprite 2 loaded from json but not in registry");
CATCH(errctx, testchar->sprite_add(testchar, testsprite, ACTOR_STATE_ALIVE)); CATCH(errctx, testchar->sprite_add(testchar, testsprite, AKGL_ACTOR_STATE_ALIVE));
CATCH(errctx, testchar->sprite_add(testchar, testsprite2, ACTOR_STATE_DEAD)); CATCH(errctx, testchar->sprite_add(testchar, testsprite2, AKGL_ACTOR_STATE_DEAD));
SDL_EnumerateProperties(testchar->state_sprites, &character_state_sprites_iterate, &opflags); SDL_EnumerateProperties(testchar->state_sprites, &akgl_character_state_sprites_iterate, &opflags);
// This is called by heap_release_character so we should assume that our property map is being // This is called by akgl_heap_release_character so we should assume that our property map is being
// deleted soon after this. So we don't care if the sprites have been deleted from the sprite_states. // deleted soon after this. So we don't care if the sprites have been deleted from the sprite_states.
// We just want to know they've been released. // We just want to know they've been released.
FAIL_NONZERO_BREAK( FAIL_NONZERO_BREAK(
errctx, errctx,
(testsprite->refcount > 1), (testsprite->refcount > 1),
ERR_VALUE, AKERR_VALUE,
"heap_release_sprite not called for testsprite from iterator"); "akgl_heap_release_sprite not called for testsprite from iterator");
FAIL_NONZERO_BREAK( FAIL_NONZERO_BREAK(
errctx, errctx,
(testsprite2->refcount > 1), (testsprite2->refcount > 1),
ERR_VALUE, AKERR_VALUE,
"heap_release_sprite not called for testsprite from iterator"); "akgl_heap_release_sprite not called for testsprite from iterator");
} CLEANUP { } CLEANUP {
IGNORE(heap_release_sprite(testsprite)); IGNORE(akgl_heap_release_sprite(testsprite));
IGNORE(heap_release_sprite(testsprite2)); IGNORE(akgl_heap_release_sprite(testsprite2));
IGNORE(heap_release_character(testchar)); IGNORE(akgl_heap_release_character(testchar));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_character_load_json() akerr_ErrorContext *test_akgl_character_load_json()
{ {
character *testcharacter = NULL; akgl_Character *testcharacter = NULL;
sprite *testsprite = NULL; akgl_Sprite *testsprite = NULL;
sprite *testsprite2 = NULL; akgl_Sprite *testsprite2 = NULL;
sprite *comparesprite = NULL; akgl_Sprite *comparesprite = NULL;
int tsrc = 0; int tsrc = 0;
int tsrc2 = 0; int tsrc2 = 0;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_character(&testcharacter)); CATCH(errctx, akgl_heap_next_character(&testcharacter));
CATCH(errctx, sprite_load_json("assets/testsprite.json")); CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
testsprite = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite", NULL); testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
testsprite, testsprite,
ERR_KEY, AKERR_KEY,
"Sprite loaded from json but not in registry"); "Sprite loaded from json but not in registry");
CATCH(errctx, sprite_load_json("assets/testsprite2.json")); CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
testsprite2 = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite2", NULL); testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
testsprite2, testsprite2,
ERR_KEY, AKERR_KEY,
"Sprite 2 loaded from json but not in registry"); "Sprite 2 loaded from json but not in registry");
CATCH(errctx, character_load_json("assets/testcharacter.json")); CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));
testcharacter = SDL_GetPointerProperty(REGISTRY_CHARACTER, "testcharacter", NULL); testcharacter = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
testcharacter, testcharacter,
ERR_KEY, AKERR_KEY,
"Character loaded from json but not in registry"); "Character loaded from json but not in registry");
CATCH(errctx, testcharacter->sprite_get(testcharacter, (ACTOR_STATE_ALIVE | ACTOR_STATE_FACE_LEFT), &comparesprite)); CATCH(errctx, testcharacter->sprite_get(testcharacter, (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT), &comparesprite));
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), ERR_VALUE, "Wrong sprite for state ACTOR_STATE_ALIVE"); FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_ALIVE");
CATCH(errctx, testcharacter->sprite_get(testcharacter, ACTOR_STATE_DEAD, &comparesprite)); CATCH(errctx, testcharacter->sprite_get(testcharacter, AKGL_ACTOR_STATE_DEAD, &comparesprite));
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), ERR_VALUE, "Wrong sprite for state ACTOR_STATE_DEAD"); FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_DEAD");
FAIL_ZERO_BREAK(errctx, (testcharacter->vx != 0.200000003), ERR_VALUE, "Wrong X velocity for test character"); FAIL_ZERO_BREAK(errctx, (testcharacter->ax != 0.200000003), AKERR_VALUE, "Wrong X acceleration for test character");
FAIL_ZERO_BREAK(errctx, (testcharacter->vy != 0.200000003), ERR_VALUE, "Wrong Y velocity for test character"); FAIL_ZERO_BREAK(errctx, (testcharacter->ay != 0.200000003), AKERR_VALUE, "Wrong Y acceleration for test character");
// Release our handles on the sprites so the character's heap_release can reduce them to 0 // Release our handles on the sprites so the character's heap_release can reduce them to 0
CATCH(errctx, heap_release_sprite(testsprite)); CATCH(errctx, akgl_heap_release_sprite(testsprite));
CATCH(errctx, heap_release_sprite(testsprite2)); CATCH(errctx, akgl_heap_release_sprite(testsprite2));
tsrc = testsprite->refcount; tsrc = testsprite->refcount;
tsrc2 = testsprite2->refcount; tsrc2 = testsprite2->refcount;
CATCH(errctx, heap_release_character(testcharacter)); CATCH(errctx, akgl_heap_release_character(testcharacter));
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
((testsprite->refcount < tsrc) || (testsprite2->refcount < tsrc2)), ((testsprite->refcount < tsrc) || (testsprite2->refcount < tsrc2)),
ERR_VALUE, AKERR_VALUE,
"character did not reduce reference count of its child sprites when released"); "character did not reduce reference count of its child sprites when released");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
@@ -195,19 +198,19 @@ int main(void)
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
FAIL_BREAK(errctx, ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
} }
if (!SDL_CreateWindowAndRenderer("net/aklabs/libsdl3game/test_character", 640, 480, SDL_WINDOW_HIDDEN, &window, &renderer)) { if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_character", 640, 480, SDL_WINDOW_HIDDEN, &window, &renderer)) {
FAIL_BREAK(errctx, ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
} }
CATCH(errctx, heap_init()); CATCH(errctx, akgl_heap_init());
CATCH(errctx, registry_init()); CATCH(errctx, akgl_registry_init());
CATCH(errctx, test_character_initialize()); CATCH(errctx, test_akgl_character_initialize());
CATCH(errctx, test_character_sprite_mgmt()); CATCH(errctx, test_character_sprite_mgmt());
CATCH(errctx, test_character_iterate_state_sprites()); CATCH(errctx, test_character_iterate_state_sprites());
CATCH(errctx, test_character_load_json()); CATCH(errctx, test_akgl_character_load_json());
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);

125
tests/charviewer.c Normal file
View File

@@ -0,0 +1,125 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_properties.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <akerror.h>
#include <akgl/assets.h>
#include <akgl/iterator.h>
#include <akgl/tilemap.h>
#include <akgl/heap.h>
#include <akgl/game.h>
#include <akgl/controller.h>
#include <akgl/draw.h>
#include <akgl/sprite.h>
#include <akgl/actor.h>
#include <akgl/registry.h>
#include <akgl/error.h>
int numsprites = 8;
char *spritepaths[] = {
"assets/sprites/little_guy_walking_left.json",
"assets/sprites/little_guy_walking_right.json",
"assets/sprites/little_guy_walking_up.json",
"assets/sprites/little_guy_walking_down.json",
"assets/sprites/little_guy_facing_left.json",
"assets/sprites/little_guy_facing_right.json",
"assets/sprites/little_guy_facing_up.json",
"assets/sprites/little_guy_facing_down.json"
};
int main(void)
{
PREPARE_ERROR(errctx);
SDL3GControlMap *controlmap;
actor *actorptr = NULL;
ATTEMPT {
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
}
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
}
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
strcpy((char *)&game.name, "charviewer");
strcpy((char *)&game.version, "0.0.1");
strcpy((char *)&game.uri, "net.aklabs.libakgl.charviewer");
game.screenwidth = 640;
game.screenheight = 480;
CATCH(errctx, akgl_GAME_init());
for ( int i = 0; i < numsprites ; i++) {
CATCH(errctx, akgl_sprite_load_json(spritepaths[i]));
}
CATCH(errctx, akgl_character_load_json("assets/characters/littleguy.json"));
CATCH(errctx, akgl_heap_next_actor(&actorptr));
CATCH(errctx, akgl_actor_initialize((actor *)actorptr, "player"));
actorptr->basechar = SDL_GetPointerProperty(
AKGL_REGISTRY_CHARACTER,
"little guy",
NULL);
FAIL_ZERO_BREAK(errctx, actorptr->basechar, AKGL_ERR_REGISTRY, "Can't load character 'little guy' from the registry");
actorptr->movement_controls_face = false;
actorptr->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT);
actorptr->x = 320;
actorptr->y = 240;
actorptr->visible = true;
// set up the control map
controlmap = &GAME_ControlMaps[0];
controlmap->kbid = 0;
controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
// Move down
controlmap->controls[0].key = SDLK_DOWN;
//controlmap->controls[0].target_state_gate = AKGL_ACTOR_STATE_MOVING_DOWN;
controlmap->controls[0].target_add_state_on = AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN;
controlmap->controls[0].target_del_state_on = AKGL_ACTOR_STATE_MOVING_UP | AKGL_ACTOR_STATE_FACE_ALL;
controlmap->controls[0].target_del_state_off = AKGL_ACTOR_STATE_MOVING_DOWN;
controlmap->controls[0].event_on = SDL_EVENT_KEY_DOWN;
controlmap->controls[0].event_off = SDL_EVENT_KEY_UP;
// Move up
controlmap->controls[1].key = SDLK_UP;
//controlmap->controls[1].target_state_gate = AKGL_ACTOR_STATE_MOVING_UP;
controlmap->controls[1].target_add_state_on = AKGL_ACTOR_STATE_MOVING_UP | AKGL_ACTOR_STATE_FACE_UP;
controlmap->controls[1].target_del_state_on = AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_ALL;
controlmap->controls[1].target_del_state_off = AKGL_ACTOR_STATE_MOVING_UP;
controlmap->controls[1].event_on = SDL_EVENT_KEY_DOWN;
controlmap->controls[1].event_off = SDL_EVENT_KEY_UP;
// Move left
controlmap->controls[2].key = SDLK_LEFT;
//controlmap->controls[2].target_state_gate = AKGL_ACTOR_STATE_MOVING_LEFT;
controlmap->controls[2].target_add_state_on = AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT;
controlmap->controls[2].target_del_state_on = AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_ALL;
controlmap->controls[2].target_del_state_off = AKGL_ACTOR_STATE_MOVING_LEFT;
controlmap->controls[2].event_on = SDL_EVENT_KEY_DOWN;
controlmap->controls[2].event_off = SDL_EVENT_KEY_UP;
// Move right
controlmap->controls[3].key = SDLK_RIGHT;
//controlmap->controls[3].target_state_gate = AKGL_ACTOR_STATE_MOVING_RIGHT;
controlmap->controls[3].target_add_state_on = AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT;
controlmap->controls[3].target_del_state_on = AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_ALL;
controlmap->controls[3].target_del_state_off = AKGL_ACTOR_STATE_MOVING_RIGHT;
controlmap->controls[3].event_on = SDL_EVENT_KEY_DOWN;
controlmap->controls[3].event_off = SDL_EVENT_KEY_UP;
} CLEANUP {
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR(errctx);
return 1;
} FINISH_NORETURN(errctx);
return 0;
}

557
tests/controller.c Normal file
View 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
View 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
View 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
View 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
View 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);
}

View File

@@ -1,9 +1,9 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <stdlib.h> #include <stdlib.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/registry.h> #include <akgl/registry.h>
typedef ErrorContext *(*RegistryFuncPtr)(void); typedef akerr_ErrorContext *(*RegistryFuncPtr)(void);
void *sdl_calloc_always_fails(size_t a, size_t b) void *sdl_calloc_always_fails(size_t a, size_t b)
{ {
@@ -11,7 +11,7 @@ void *sdl_calloc_always_fails(size_t a, size_t b)
return NULL; return NULL;
} }
ErrorContext *test_registry_init(RegistryFuncPtr funcptr) akerr_ErrorContext *test_akgl_registry_init(RegistryFuncPtr funcptr)
{ {
SDL_malloc_func malloc_func; SDL_malloc_func malloc_func;
SDL_calloc_func calloc_func; SDL_calloc_func calloc_func;
@@ -43,41 +43,41 @@ ErrorContext *test_registry_init(RegistryFuncPtr funcptr)
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
FAIL_RETURN(errctx, ERR_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");
} }
ErrorContext *test_registry_init_creation_failures(void) akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, test_registry_init(&registry_init_actor)); CATCH(errctx, test_akgl_registry_init(&akgl_registry_init_actor));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n"); printf("Sucess\n");
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, test_registry_init(&registry_init_sprite)); CATCH(errctx, test_akgl_registry_init(&akgl_registry_init_sprite));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n"); printf("Sucess\n");
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, test_registry_init(&registry_init_spritesheet)); CATCH(errctx, test_akgl_registry_init(&akgl_registry_init_spritesheet));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n"); printf("Sucess\n");
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, test_registry_init(&registry_init_character)); CATCH(errctx, test_akgl_registry_init(&akgl_registry_init_character));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n"); printf("Sucess\n");
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -87,7 +87,7 @@ int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, test_registry_init_creation_failures()); CATCH(errctx, test_akgl_registry_init_creation_failures());
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);

View File

@@ -3,45 +3,46 @@
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/registry.h> #include <akgl/registry.h>
#include <sdl3game/sprite.h> #include <akgl/sprite.h>
#include <sdl3game/heap.h> #include <akgl/heap.h>
#include <sdl3game/util.h> #include <akgl/util.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/renderer.h>
SDL_Window *window;
SDL_Renderer *renderer;
ErrorContext *test_spritesheet_initialize(void) akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
spritesheet *sheet = NULL; akgl_SpriteSheet *sheet = NULL;
SDL_Texture *image = NULL; SDL_Texture *image = NULL;
string *tmpstr = NULL; akgl_String *tmpstr = NULL;
// Does the image file get loaded? // Does the image file get loaded?
// Is the image file loaded correctly? (Surface comparison) // Is the image file loaded correctly? (Surface comparison)
// Is the spritesheet in the registry? // Is the spritesheet in the registry?
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_spritesheet(&sheet)); CATCH(errctx, akgl_heap_next_spritesheet(&sheet));
CATCH(errctx, heap_next_string(&tmpstr)); CATCH(errctx, akgl_heap_next_string(&tmpstr));
snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png"); snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
CATCH(errctx, spritesheet_initialize(sheet, 48, 48, "assets/spritesheet.png")); CATCH(errctx, akgl_spritesheet_initialize(sheet, 48, 48, "assets/spritesheet.png"));
FAIL_ZERO_BREAK(errctx, sheet->texture, ERR_VALUE, "spritesheet_initialize failed to load the sprite texture"); FAIL_ZERO_BREAK(errctx, sheet->texture, AKERR_VALUE, "akgl_spritesheet_initialize failed to load the sprite texture");
FAIL_NONZERO_BREAK( FAIL_NONZERO_BREAK(
errctx, errctx,
((sheet->texture->w != 576) || (sheet->texture->h != 384)), ((sheet->texture->w != 576) || (sheet->texture->h != 384)),
ERR_VALUE, AKERR_VALUE,
"Loaded texture was not the correct size"); "Loaded texture was not the correct size");
snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png"); snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
image = IMG_LoadTexture(renderer, (char *)&tmpstr->data); image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
FAIL_ZERO_BREAK(errctx, image, ERR_SDL, "Failed to load comparison image"); FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
CATCH( CATCH(
errctx, errctx,
render_and_compare( akgl_render_and_compare(
sheet->texture, sheet->texture,
image, image,
0, 0, 576, 384, 0, 0, 576, 384,
@@ -50,13 +51,13 @@ ErrorContext *test_spritesheet_initialize(void)
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
SDL_GetPointerProperty(REGISTRY_SPRITESHEET, "assets/spritesheet.png", NULL), SDL_GetPointerProperty(AKGL_REGISTRY_SPRITESHEET, "assets/spritesheet.png", NULL),
ERR_KEY, AKERR_KEY,
"Spritesheet was not placed in the registry"); "Spritesheet was not placed in the registry");
} CLEANUP { } CLEANUP {
IGNORE(heap_release_string(tmpstr)); IGNORE(akgl_heap_release_string(tmpstr));
IGNORE(heap_release_spritesheet(sheet)); IGNORE(akgl_heap_release_spritesheet(sheet));
if ( image != NULL ) if ( image != NULL )
SDL_DestroyTexture(image); SDL_DestroyTexture(image);
} PROCESS(errctx) { } PROCESS(errctx) {
@@ -64,48 +65,48 @@ ErrorContext *test_spritesheet_initialize(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_sprite_initialize(void) akerr_ErrorContext *test_akgl_sprite_initialize(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
spritesheet *testsheet = NULL; akgl_SpriteSheet *testsheet = NULL;
sprite *testsprite = NULL; akgl_Sprite *testsprite = NULL;
string *tmpstr = NULL; akgl_String *tmpstr = NULL;
// Does the sprite get loaded? // Does the sprite get loaded?
// Do all frames of the sprite get loaded? // Do all frames of the sprite get loaded?
// Are all the frames of the sprite what we expect? (Surface comparison) // Are all the frames of the sprite what we expect? (Surface comparison)
// Is the sprite added to the registry? // Is the sprite added to the registry?
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_spritesheet(&testsheet)); CATCH(errctx, akgl_heap_next_spritesheet(&testsheet));
CATCH(errctx, heap_next_sprite(&testsprite)); CATCH(errctx, akgl_heap_next_sprite(&testsprite));
CATCH(errctx, heap_next_string(&tmpstr)); CATCH(errctx, akgl_heap_next_string(&tmpstr));
snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png"); snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
CATCH(errctx, spritesheet_initialize(testsheet, 48, 48, "assets/spritesheet.png")); CATCH(errctx, akgl_spritesheet_initialize(testsheet, 48, 48, "assets/spritesheet.png"));
FAIL_ZERO_BREAK(errctx, testsheet, ERR_VALUE, "spritesheet_initialize failed"); FAIL_ZERO_BREAK(errctx, testsheet, AKERR_VALUE, "akgl_spritesheet_initialize failed");
CATCH(errctx, sprite_initialize(testsprite, "test", testsheet)); CATCH(errctx, akgl_sprite_initialize(testsprite, "test", testsheet));
FAIL_NONZERO_BREAK(errctx, (testsprite->sheet != testsheet), ERR_VALUE, "Initialized sprite uses wrong sheet"); FAIL_NONZERO_BREAK(errctx, (testsprite->sheet != testsheet), AKERR_VALUE, "Initialized sprite uses wrong sheet");
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
SDL_GetPointerProperty(REGISTRY_SPRITE, "test", NULL), SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "test", NULL),
ERR_KEY, AKERR_KEY,
"Sprite was not placed in the registry"); "Sprite was not placed in the registry");
} CLEANUP { } CLEANUP {
IGNORE(heap_release_sprite(testsprite)); IGNORE(akgl_heap_release_sprite(testsprite));
IGNORE(heap_release_string(tmpstr)); IGNORE(akgl_heap_release_string(tmpstr));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_sprite_load_json(void) akerr_ErrorContext *test_akgl_sprite_load_json(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
sprite *testsprite = NULL; akgl_Sprite *testsprite = NULL;
sprite *testsprite2 = NULL; akgl_Sprite *testsprite2 = NULL;
string *tmpstr = NULL; akgl_String *tmpstr = NULL;
SDL_Texture *image = NULL; SDL_Texture *image = NULL;
// Does the sprite get loaded? // Does the sprite get loaded?
@@ -113,36 +114,36 @@ ErrorContext *test_sprite_load_json(void)
// Are all the frames of the sprite what we expect? (Surface comparison) // Are all the frames of the sprite what we expect? (Surface comparison)
// Is the sprite added to the registry? // Is the sprite added to the registry?
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&tmpstr)); CATCH(errctx, akgl_heap_next_string(&tmpstr));
CATCH(errctx, sprite_load_json("assets/testsprite.json")); CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
testsprite = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite", NULL); testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
testsprite, testsprite,
ERR_KEY, AKERR_KEY,
"sprite_load_json succeeds but sprite is not placed in the registry"); "akgl_sprite_load_json succeeds but sprite is not placed in the registry");
FAIL_ZERO_BREAK(errctx, (testsprite->width == 48), ERR_VALUE, "width incorrect (48 : %d)", testsprite->width); FAIL_ZERO_BREAK(errctx, (testsprite->width == 48), AKERR_VALUE, "width incorrect (48 : %d)", testsprite->width);
FAIL_ZERO_BREAK(errctx, (testsprite->height == 48), ERR_VALUE, "height incorrect (48 : %d)", testsprite->height); FAIL_ZERO_BREAK(errctx, (testsprite->height == 48), AKERR_VALUE, "height incorrect (48 : %d)", testsprite->height);
FAIL_ZERO_BREAK(errctx, (testsprite->speed == 100), ERR_VALUE, "speed incorrect (100 : %d)", testsprite->speed); FAIL_ZERO_BREAK(errctx, (testsprite->speed == 100000000), AKERR_VALUE, "speed incorrect (100 : %d)", testsprite->speed);
FAIL_ZERO_BREAK(errctx, (testsprite->loop == true), ERR_VALUE, "loop incorrect (1 : %d)", testsprite->loop); FAIL_ZERO_BREAK(errctx, (testsprite->loop == true), AKERR_VALUE, "loop incorrect (1 : %d)", testsprite->loop);
FAIL_ZERO_BREAK(errctx, (testsprite->loopReverse == true), ERR_VALUE, "loopReverse incorrect (1 : %d)", testsprite->loopReverse); FAIL_ZERO_BREAK(errctx, (testsprite->loopReverse == true), AKERR_VALUE, "loopReverse incorrect (1 : %d)", testsprite->loopReverse);
FAIL_ZERO_BREAK(errctx, (testsprite->frames == 3), ERR_VALUE, "frame count incorrect (3 : %d)", testsprite->frames); FAIL_ZERO_BREAK(errctx, (testsprite->frames == 3), AKERR_VALUE, "frame count incorrect (3 : %d)", testsprite->frames);
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[0] == 12), ERR_VALUE, "frameids[0] incorrect (12 : %d)", testsprite->frameids[0]); FAIL_ZERO_BREAK(errctx, (testsprite->frameids[0] == 12), AKERR_VALUE, "frameids[0] incorrect (12 : %d)", testsprite->frameids[0]);
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[1] == 13), ERR_VALUE, "frameids[1] incorrect (13 : %d)", testsprite->frameids[1]); FAIL_ZERO_BREAK(errctx, (testsprite->frameids[1] == 13), AKERR_VALUE, "frameids[1] incorrect (13 : %d)", testsprite->frameids[1]);
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[2] == 14), ERR_VALUE, "frameids[2] incorrect (14 : %d)", testsprite->frameids[2]); FAIL_ZERO_BREAK(errctx, (testsprite->frameids[2] == 14), AKERR_VALUE, "frameids[2] incorrect (14 : %d)", testsprite->frameids[2]);
FAIL_NONZERO_BREAK(errctx, strcmp(testsprite->name, "testsprite"), ERR_VALUE, "name incorrect (testsprite : %s)", (char *)testsprite->name); FAIL_NONZERO_BREAK(errctx, strcmp(testsprite->name, "testsprite"), AKERR_VALUE, "name incorrect (testsprite : %s)", (char *)testsprite->name);
// Is it using the right spritesheet? // Is it using the right spritesheet?
snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png"); snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
image = IMG_LoadTexture(renderer, (char *)&tmpstr->data); image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
FAIL_ZERO_BREAK(errctx, image, ERR_SDL, "Failed to load comparison image"); FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
CATCH( CATCH(
errctx, errctx,
render_and_compare( akgl_render_and_compare(
testsprite->sheet->texture, testsprite->sheet->texture,
image, image,
0, 0, 576, 384, 0, 0, 576, 384,
@@ -151,29 +152,29 @@ ErrorContext *test_sprite_load_json(void)
); );
// If we load a second sprite using the same sheet name, do they use the same sheet in memory? // If we load a second sprite using the same sheet name, do they use the same sheet in memory?
snprintf((char *)&tmpstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testsprite2.json"); snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testsprite2.json");
CATCH(errctx, sprite_load_json("assets/testsprite2.json")); CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
testsprite2 = SDL_GetPointerProperty(REGISTRY_SPRITE, "testsprite2", NULL); testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
testsprite, testsprite,
ERR_KEY, AKERR_KEY,
"sprite_load_json succeeds but second sprite is not placed in the registry"); "akgl_sprite_load_json succeeds but second sprite is not placed in the registry");
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
(testsprite->sheet == testsprite2->sheet), (testsprite->sheet == testsprite2->sheet),
ERR_VALUE, AKERR_VALUE,
"Previously loaded spritesheets are not reused"); "Previously loaded spritesheets are not reused");
} CLEANUP { } CLEANUP {
if ( testsprite != NULL ) { if ( testsprite != NULL ) {
IGNORE(heap_release_sprite(testsprite)); IGNORE(akgl_heap_release_sprite(testsprite));
} }
if ( testsprite2 != NULL ) { if ( testsprite2 != NULL ) {
IGNORE(heap_release_sprite(testsprite2)); IGNORE(akgl_heap_release_sprite(testsprite2));
} }
IGNORE(heap_release_string(tmpstr)); IGNORE(akgl_heap_release_string(tmpstr));
if ( image != NULL ) if ( image != NULL )
SDL_DestroyTexture(image); SDL_DestroyTexture(image);
} PROCESS(errctx) { } PROCESS(errctx) {
@@ -186,24 +187,26 @@ int main(void)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
renderer = &_akgl_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
FAIL_BREAK(errctx, ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
} }
if (!SDL_CreateWindowAndRenderer("net/aklabs/libsdl3game/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, ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
} }
renderer->draw_texture = &akgl_render_2d_draw_texture;
CATCH(errctx, heap_init()); CATCH(errctx, akgl_heap_init());
CATCH(errctx, registry_init_sprite()); CATCH(errctx, akgl_registry_init_sprite());
CATCH(errctx, registry_init_spritesheet()); CATCH(errctx, akgl_registry_init_spritesheet());
CATCH(errctx, test_spritesheet_initialize()); CATCH(errctx, test_akgl_spritesheet_initialize());
CATCH(errctx, test_sprite_initialize()); CATCH(errctx, test_akgl_sprite_initialize());
CATCH(errctx, test_sprite_load_json()); CATCH(errctx, test_akgl_sprite_load_json());
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);

View File

@@ -1,18 +1,19 @@
#include <string.h> #include <string.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/heap.h> #include <akgl/error.h>
#include <sdl3game/staticstring.h> #include <akgl/heap.h>
#include <akgl/staticstring.h>
void reset_string_heap(void); void reset_string_heap(void);
ErrorContext *test_fresh_heap_gives_strings(void) akerr_ErrorContext *test_fresh_heap_gives_strings(void)
{ {
string *ptr = NULL; akgl_String *ptr = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
for ( int i = 0; i < MAX_HEAP_STRING - 1; i++ ) { for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&ptr)); CATCH(errctx, akgl_heap_next_string(&ptr));
} CLEANUP { } CLEANUP {
reset_string_heap(); reset_string_heap();
} PROCESS(errctx) { } PROCESS(errctx) {
@@ -22,73 +23,73 @@ ErrorContext *test_fresh_heap_gives_strings(void)
return 0; return 0;
} }
ErrorContext *test_string_heap_error_when_no_strings_left(void) akerr_ErrorContext *test_string_heap_error_when_no_strings_left(void)
{ {
string *ptr; akgl_String *ptr;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
for ( int i = 0; i < MAX_HEAP_STRING; i++ ) { for ( int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
HEAP_STRING[i].refcount = 1; HEAP_STRING[i].refcount = 1;
} }
for ( int i = 0; i < MAX_HEAP_STRING - 1; i++ ) { for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&ptr)); CATCH(errctx, akgl_heap_next_string(&ptr));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
return 0; return 0;
} FINISH(errctx, true); } FINISH(errctx, true);
} }
FAIL_RETURN(errctx, ERR_OUTOFBOUNDS, "Expected ERR_NULLPOINTER when accessing beyond string heap bounds"); FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Expected AKERR_NULLPOINTER when accessing beyond string heap bounds");
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_string_heap_honors_refcount(void) akerr_ErrorContext *test_string_heap_honors_refcount(void)
{ {
string *firstptr = &HEAP_STRING[0]; akgl_String *firstptr = &HEAP_STRING[0];
string *secondptr = &HEAP_STRING[1]; akgl_String *secondptr = &HEAP_STRING[1];
string *testptr = NULL; akgl_String *testptr = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&testptr)); CATCH(errctx, akgl_heap_next_string(&testptr));
if ( testptr != firstptr ) { if ( testptr != firstptr ) {
FAIL_RETURN( FAIL_RETURN(
errctx, errctx,
ERR_VALUE, AKERR_VALUE,
"Expected testptr to equal (HEAP_STRING[0] = %p) but got %p", "Expected testptr to equal (HEAP_STRING[0] = %p) but got %p",
firstptr, firstptr,
testptr testptr
); );
} }
CATCH(errctx, string_initialize(testptr, NULL)); CATCH(errctx, akgl_string_initialize(testptr, NULL));
if ( testptr->refcount == 0 ) { if ( testptr->refcount == 0 ) {
FAIL_RETURN(errctx, ERR_VALUE, "Expected string reference count to be nonzero but got 0"); FAIL_RETURN(errctx, AKERR_VALUE, "Expected string reference count to be nonzero but got 0");
} }
if ( testptr != firstptr ) { if ( testptr != firstptr ) {
FAIL_RETURN( FAIL_RETURN(
errctx, errctx,
ERR_VALUE, AKERR_VALUE,
"Expected testptr to equal (HEAP_STRING[1] = %p) but got %p", "Expected testptr to equal (HEAP_STRING[1] = %p) but got %p",
secondptr, secondptr,
testptr testptr
); );
} }
CATCH(errctx, heap_next_string(&testptr)); CATCH(errctx, akgl_heap_next_string(&testptr));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_strcpy_to_all_strings_no_segfault(void) akerr_ErrorContext *test_strcpy_to_all_strings_no_segfault(void)
{ {
char copybuf[MAX_STRING_LENGTH]; char copybuf[AKGL_MAX_STRING_LENGTH];
string *ptr; akgl_String *ptr;
memset((void *)&copybuf, 'a', MAX_STRING_LENGTH); memset((void *)&copybuf, 'a', AKGL_MAX_STRING_LENGTH);
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
for ( int i = 0; i < MAX_HEAP_STRING - 1; i++ ) { for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
CATCH(errctx, heap_next_string(&ptr)); CATCH(errctx, akgl_heap_next_string(&ptr));
strncpy(ptr->data, (char *)&copybuf, MAX_STRING_LENGTH); strncpy(ptr->data, (char *)&copybuf, AKGL_MAX_STRING_LENGTH);
} }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
@@ -96,22 +97,22 @@ ErrorContext *test_strcpy_to_all_strings_no_segfault(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_string_initialize(void) akerr_ErrorContext *test_akgl_string_initialize(void)
{ {
string *ptr; akgl_String *ptr;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&ptr)); CATCH(errctx, akgl_heap_next_string(&ptr));
CATCH(errctx, string_initialize(ptr, NULL)); CATCH(errctx, akgl_string_initialize(ptr, NULL));
FAIL_NONZERO_BREAK(errctx, ptr->data[0], ERR_VALUE, "Expected empty zero length string data"); FAIL_NONZERO_BREAK(errctx, ptr->data[0], AKERR_VALUE, "Expected empty zero length string data");
CATCH(errctx, heap_release_string(ptr)); CATCH(errctx, akgl_heap_release_string(ptr));
CATCH(errctx, heap_next_string(&ptr)); CATCH(errctx, akgl_heap_next_string(&ptr));
CATCH(errctx, string_initialize(ptr, "Test value")); CATCH(errctx, akgl_string_initialize(ptr, "Test value"));
FAIL_NONZERO_BREAK(errctx, strcmp((char *)&ptr->data, "Test value"), ERR_VALUE, "Expected 'Test value', got %s", (char *)&ptr->data); FAIL_NONZERO_BREAK(errctx, strcmp((char *)&ptr->data, "Test value"), AKERR_VALUE, "Expected 'Test value', got %s", (char *)&ptr->data);
CATCH(errctx, heap_release_string(NULL)); CATCH(errctx, akgl_heap_release_string(NULL));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Failure to properly handle NULL pointer"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Failure to properly handle NULL pointer");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -122,8 +123,8 @@ ErrorContext *test_string_initialize(void)
void reset_string_heap(void) void reset_string_heap(void)
{ {
for ( int i = 0; i < MAX_HEAP_STRING; i++ ) { for ( int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
memset(&HEAP_STRING[i], 0x00, sizeof(string)); memset(&HEAP_STRING[i], 0x00, sizeof(akgl_String));
} }
} }
@@ -144,8 +145,8 @@ int main(void)
printf("test_strcpy_to_all_strings_no_segfault ...\n"); printf("test_strcpy_to_all_strings_no_segfault ...\n");
test_strcpy_to_all_strings_no_segfault(); test_strcpy_to_all_strings_no_segfault();
reset_string_heap(); reset_string_heap();
printf("test_string_initialize....\n"); printf("test_akgl_string_initialize....\n");
test_string_initialize(); test_akgl_string_initialize();
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);

99
tests/testutil.h Normal file
View 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_

View File

@@ -2,37 +2,39 @@
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <jansson.h> #include <jansson.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/util.h> #include <akgl/util.h>
#include <sdl3game/heap.h> #include <akgl/heap.h>
#include <sdl3game/registry.h> #include <akgl/registry.h>
#include <sdl3game/tilemap.h> #include <akgl/tilemap.h>
#include <sdl3game/actor.h> #include <akgl/actor.h>
#include <sdl3game/game.h> #include <akgl/game.h>
#include <sdl3game/json_helpers.h> #include <akgl/json_helpers.h>
ErrorContext *test_tilemap_get_json_tilemap_property(void) akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *jsondoc = NULL; json_t *jsondoc = NULL;
json_error_t jsonerr; json_error_t jsonerr;
string *tmpstr = NULL; akgl_String *tmpstr = NULL;
int propnum; int propnum;
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&tmpstr)); gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_heap_next_string(&tmpstr));
snprintf( snprintf(
(char *)&tmpstr->data, (char *)&tmpstr->data,
MAX_STRING_LENGTH, AKGL_MAX_STRING_LENGTH,
"%s%s", "%s%s",
SDL_GetBasePath(), SDL_GetBasePath(),
"assets/snippets/test_tilemap_get_json_tilemap_property.json" "assets/snippets/test_tilemap_get_json_tilemap_property.json"
); );
jsondoc = json_load_file((char *)&tmpstr->data, 0, (json_error_t *)&jsonerr); jsondoc = json_load_file((char *)&tmpstr->data, 0, (json_error_t *)&jsonerr);
FAIL_ZERO_BREAK(errctx, jsondoc, ERR_NULLPOINTER, "Failure loading json fixture: %s", (char *)jsonerr.text); FAIL_ZERO_BREAK(errctx, jsondoc, AKERR_NULLPOINTER, "Failure loading json fixture: %s", (char *)jsonerr.text);
CATCH( CATCH(
errctx, errctx,
get_json_properties_string( akgl_get_json_properties_string(
jsondoc, jsondoc,
"character", "character",
&tmpstr &tmpstr
@@ -41,13 +43,13 @@ ErrorContext *test_tilemap_get_json_tilemap_property(void)
FAIL_NONZERO_BREAK( FAIL_NONZERO_BREAK(
errctx, errctx,
strcmp((char *)&tmpstr->data, "testcharacter"), strcmp((char *)&tmpstr->data, "testcharacter"),
ERR_VALUE, AKERR_VALUE,
"Incorrect value loaded from property `character` (`testcharacter` vs `%s`)", "Incorrect value loaded from property `character` (`testcharacter` vs `%s`)",
(char *)&tmpstr->data (char *)&tmpstr->data
); );
CATCH( CATCH(
errctx, errctx,
get_json_properties_integer( akgl_get_json_properties_integer(
jsondoc, jsondoc,
"state", "state",
&propnum &propnum
@@ -56,13 +58,13 @@ ErrorContext *test_tilemap_get_json_tilemap_property(void)
FAIL_NONZERO_BREAK( FAIL_NONZERO_BREAK(
errctx, errctx,
(propnum != 6), (propnum != 6),
ERR_VALUE, AKERR_VALUE,
"Incorrect value loaded from property `state` (6 vs %d)", "Incorrect value loaded from property `state` (6 vs %d)",
propnum propnum
); );
} CLEANUP { } CLEANUP {
if ( tmpstr != NULL ) { if ( tmpstr != NULL ) {
IGNORE(heap_release_string(tmpstr)); IGNORE(akgl_heap_release_string(tmpstr));
} }
if ( jsondoc != NULL ) { if ( jsondoc != NULL ) {
json_decref(jsondoc); json_decref(jsondoc);
@@ -72,7 +74,7 @@ ErrorContext *test_tilemap_get_json_tilemap_property(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_tilemap_compute_tileset_offsets(void) akerr_ErrorContext *test_akgl_tilemap_compute_tileset_offsets(void)
{ {
int comparison_values[8] = { int comparison_values[8] = {
0, // Tile 0 X 0, // Tile 0 X
@@ -85,38 +87,40 @@ ErrorContext *test_tilemap_compute_tileset_offsets(void)
10 // Tile 2 Y 10 // Tile 2 Y
}; };
int *comparison_ptrs[8] = { int *comparison_ptrs[8] = {
&gamemap.tilesets[0].tile_offsets[0][0], // Tile 0 X &gamemap->tilesets[0].tile_offsets[0][0], // Tile 0 X
&gamemap.tilesets[0].tile_offsets[0][1], // Tile 0 Y &gamemap->tilesets[0].tile_offsets[0][1], // Tile 0 Y
&gamemap.tilesets[0].tile_offsets[1][0], // Tile 1 X &gamemap->tilesets[0].tile_offsets[1][0], // Tile 1 X
&gamemap.tilesets[0].tile_offsets[1][1], // Tile 0 Y &gamemap->tilesets[0].tile_offsets[1][1], // Tile 0 Y
&gamemap.tilesets[0].tile_offsets[2][0], // Tile 2 X &gamemap->tilesets[0].tile_offsets[2][0], // Tile 2 X
&gamemap.tilesets[0].tile_offsets[2][1], // Tile 2 Y &gamemap->tilesets[0].tile_offsets[2][1], // Tile 2 Y
&gamemap.tilesets[0].tile_offsets[3][0], // Tile 3 X &gamemap->tilesets[0].tile_offsets[3][0], // Tile 3 X
&gamemap.tilesets[0].tile_offsets[3][0], // Tile 3 Y &gamemap->tilesets[0].tile_offsets[3][1], // Tile 3 Y
}; };
int i = 0; int i = 0;
memset((void *)&gamemap, 0x00, sizeof(tilemap)); memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
gamemap.tilesets[0].tilecount = 4; gamemap->tilesets[0].tilecount = 4;
gamemap.tilesets[0].columns = 2; gamemap->tilesets[0].columns = 2;
gamemap.tilesets[0].firstgid = 1; gamemap->tilesets[0].firstgid = 1;
gamemap.tilesets[0].imageheight = 20; gamemap->tilesets[0].imageheight = 20;
gamemap.tilesets[0].imagewidth = 20; gamemap->tilesets[0].imagewidth = 20;
gamemap.tilesets[0].tilecount = 4; gamemap->tilesets[0].tilecount = 4;
gamemap.tilesets[0].tileheight = 10; gamemap->tilesets[0].tileheight = 10;
gamemap.tilesets[0].tilewidth = 10; gamemap->tilesets[0].tilewidth = 10;
gamemap.tilesets[0].spacing = 0; gamemap->tilesets[0].spacing = 0;
gamemap.tilesets[0].margin = 0; gamemap->tilesets[0].margin = 0;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, tilemap_compute_tileset_offsets(&gamemap, 0)); gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(gamemap, 0));
// Tile 0 X offset // Tile 0 X offset
for ( i = 0; i < 8; i++ ) { for ( i = 0; i < 8; i++ ) {
FAIL_NONZERO_BREAK( FAIL_NONZERO_BREAK(
errctx, errctx,
(*comparison_ptrs[i] != comparison_values[i]), (*comparison_ptrs[i] != comparison_values[i]),
ERR_VALUE, AKERR_VALUE,
"Tile offset incorrectly calculated for index %d (%d vs %d)", "Tile offset incorrectly calculated for index %d (%d vs %d)",
i, i,
*comparison_ptrs[i], *comparison_ptrs[i],
@@ -129,44 +133,47 @@ ErrorContext *test_tilemap_compute_tileset_offsets(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_tilemap_load_layer_objects(void) akerr_ErrorContext *test_akgl_tilemap_load_layer_objects(void)
{ {
string *pathstr; akgl_String *pathstr;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *doc = NULL; json_t *doc = NULL;
json_t *layers = NULL; json_t *layers = NULL;
json_t *objectlayer = NULL; json_t *objectlayer = NULL;
json_error_t errdata; json_error_t errdata;
actor *testactor; akgl_Actor *testactor;
memset((void *)&gamemap, 0x00, sizeof(tilemap)); memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&pathstr)); gamemap = &_akgl_gamemap;
snprintf((char *)&pathstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj"); 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); doc = json_load_file((char *)&pathstr->data, 0, &errdata);
FAIL_ZERO_BREAK(errctx, doc, ERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text); snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
CATCH(errctx, get_json_array_value(doc, "layers", &layers)); FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, get_json_array_index_object(layers, 1, &objectlayer)); CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
CATCH(errctx, tilemap_load_layer_objects(&gamemap, objectlayer, 1)); CATCH(errctx, akgl_get_json_array_index_object(layers, 1, &objectlayer));
CATCH(errctx, akgl_tilemap_load_layer_objects(gamemap, objectlayer, 1, pathstr));
testactor = SDL_GetPointerProperty(REGISTRY_ACTOR, "testactor", NULL); testactor = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
testactor, testactor,
ERR_NULLPOINTER, AKERR_NULLPOINTER,
"Test Actor was not loaded from the test map" "Test Actor was not loaded from the test map"
); );
if ( (testactor->basechar != SDL_GetPointerProperty(REGISTRY_CHARACTER, "testcharacter", NULL)) || if ( (testactor->basechar != SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL)) ||
(testactor->layer != 1) || (testactor->layer != 1) ||
(testactor->state != 6) || (testactor->state != 6) ||
(testactor->visible != true) || (testactor->visible != true) ||
(testactor->x != 16) || (testactor->x != 16) ||
(testactor->y != 16) ) { (testactor->y != 16) ) {
FAIL_BREAK(errctx, ERR_VALUE, "Test actor was loaded with incorrect values (check gdb)"); FAIL_BREAK(errctx, AKERR_VALUE, "Test actor was loaded with incorrect values (check gdb)");
} }
} CLEANUP { } CLEANUP {
if ( pathstr != NULL ) { if ( pathstr != NULL ) {
IGNORE(heap_release_string(pathstr)); IGNORE(akgl_heap_release_string(pathstr));
} }
if ( doc != NULL ) { if ( doc != NULL ) {
json_decref(doc); json_decref(doc);
@@ -176,9 +183,9 @@ ErrorContext *test_tilemap_load_layer_objects(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_tilemap_load_layer_tile(void) akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
{ {
string *pathstr; akgl_String *pathstr;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *doc = NULL; json_t *doc = NULL;
json_t *layers = NULL; json_t *layers = NULL;
@@ -186,26 +193,29 @@ ErrorContext *test_tilemap_load_layer_tile(void)
json_t *tiledata = NULL; json_t *tiledata = NULL;
json_error_t errdata; json_error_t errdata;
memset((void *)&gamemap, 0x00, sizeof(tilemap)); memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&pathstr)); gamemap = &_akgl_gamemap;
snprintf((char *)&pathstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj"); 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); doc = json_load_file((char *)&pathstr->data, 0, &errdata);
FAIL_ZERO_BREAK(errctx, doc, ERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text); snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
CATCH(errctx, get_json_array_value(doc, "layers", &layers)); FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, get_json_array_index_object(layers, 0, &tilelayer)); CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
CATCH(errctx, get_json_array_value(tilelayer, "data", &tiledata)); CATCH(errctx, akgl_get_json_array_index_object(layers, 0, &tilelayer));
CATCH(errctx, tilemap_load_layer_tile(&gamemap, tilelayer, 0)); CATCH(errctx, akgl_get_json_array_value(tilelayer, "data", &tiledata));
if ( (gamemap.layers[0].data[0] != 1) || CATCH(errctx, akgl_tilemap_load_layer_tile(gamemap, tilelayer, 0, pathstr));
(gamemap.layers[0].data[1] != 2) || if ( (gamemap->layers[0].data[0] != 1) ||
(gamemap.layers[0].data[2] != 3) || (gamemap->layers[0].data[1] != 2) ||
(gamemap.layers[0].data[3] != 4) ) { (gamemap->layers[0].data[2] != 3) ||
FAIL_BREAK(errctx, ERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)"); (gamemap->layers[0].data[3] != 4) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)");
} }
} CLEANUP { } CLEANUP {
if ( pathstr != NULL ) { if ( pathstr != NULL ) {
IGNORE(heap_release_string(pathstr)); IGNORE(akgl_heap_release_string(pathstr));
} }
if ( doc != NULL ) { if ( doc != NULL ) {
json_decref(doc); json_decref(doc);
@@ -215,73 +225,76 @@ ErrorContext *test_tilemap_load_layer_tile(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_tilemap_load_layers(void) akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
{ {
string *pathstr; akgl_String *pathstr;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *doc = NULL; json_t *doc = NULL;
json_error_t errdata; json_error_t errdata;
int i = 0; int i = 0;
memset((void *)&gamemap, 0x00, sizeof(tilemap)); memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&pathstr)); gamemap = &_akgl_gamemap;
snprintf((char *)&pathstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj"); 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); doc = json_load_file((char *)&pathstr->data, 0, &errdata);
FAIL_ZERO_BREAK(errctx, doc, ERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text); snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
CATCH(errctx, tilemap_load_layers(&gamemap, doc)); FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_tilemap_load_layers(gamemap, doc, pathstr));
FAIL_NONZERO_BREAK( FAIL_NONZERO_BREAK(
errctx, errctx,
(gamemap.numlayers != 3), (gamemap->numlayers != 3),
ERR_VALUE, AKERR_VALUE,
"Map layer count incorrect" "Map layer count incorrect"
); );
for ( i = 0; i < gamemap.numlayers; i++ ) { for ( i = 0; i < gamemap->numlayers; i++ ) {
if ( (gamemap.layers[i].opacity != 1) || if ( (gamemap->layers[i].opacity != 1) ||
(gamemap.layers[i].visible != true) || (gamemap->layers[i].visible != true) ||
(gamemap.layers[i].id != (i + 1)) || (gamemap->layers[i].id != (i + 1)) ||
(gamemap.layers[i].x != 0) || (gamemap->layers[i].x != 0) ||
(gamemap.layers[i].y != 0) ) { (gamemap->layers[i].y != 0) ) {
FAIL(errctx, ERR_VALUE, "Map layer data loaded incorrectly (see gdb)"); FAIL(errctx, AKERR_VALUE, "Map layer data loaded incorrectly (see gdb)");
goto _test_tilemap_load_layers_cleanup; goto _test_akgl_tilemap_load_layers_cleanup;
} }
} }
// Layer 2 should have 1 object loaded // Layer 2 should have 1 object loaded
if ( (gamemap.layers[1].objects[0].actorptr != SDL_GetPointerProperty(REGISTRY_ACTOR, "testactor", NULL)) || if ( (gamemap->layers[1].objects[0].actorptr != SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL)) ||
(gamemap.layers[1].objects[1].name[0] != '\0' ) || (gamemap->layers[1].objects[1].name[0] != '\0' ) ||
(gamemap.layers[1].objects[1].id != 0) ) { (gamemap->layers[1].objects[1].id != 0) ) {
FAIL_BREAK(errctx, ERR_VALUE, "Map layer 2 should have 1 loaded object (testactor) and nothing else (see gdb)"); FAIL_BREAK(errctx, AKERR_VALUE, "Map layer 2 should have 1 loaded object (testactor) and nothing else (see gdb)");
} }
// Layer 1 and 3 should have no objects // Layer 1 and 3 should have no objects
for ( i = 0; i < TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) { for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
if ( gamemap.layers[0].objects[i].id != 0 ) { if ( gamemap->layers[0].objects[i].id != 0 ) {
FAIL(errctx, ERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects"); FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
goto _test_tilemap_load_layers_cleanup; goto _test_akgl_tilemap_load_layers_cleanup;
} }
} }
for ( i = 0; i < TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) { for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
if ( gamemap.layers[2].objects[i].id != 0 ) { if ( gamemap->layers[2].objects[i].id != 0 ) {
FAIL(errctx, ERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects"); FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
goto _test_tilemap_load_layers_cleanup; goto _test_akgl_tilemap_load_layers_cleanup;
} }
} }
// Layers 1 and 3 should have tile data // Layers 1 and 3 should have tile data
if ( (gamemap.layers[0].data[0] != 1) || if ( (gamemap->layers[0].data[0] != 1) ||
(gamemap.layers[0].data[1] != 2) || (gamemap->layers[0].data[1] != 2) ||
(gamemap.layers[0].data[2] != 3) || (gamemap->layers[0].data[2] != 3) ||
(gamemap.layers[0].data[3] != 4) || (gamemap->layers[0].data[3] != 4) ||
(gamemap.layers[2].data[0] != 0) || (gamemap->layers[2].data[0] != 0) ||
(gamemap.layers[2].data[1] != 5) || (gamemap->layers[2].data[1] != 5) ||
(gamemap.layers[2].data[2] != 0) || (gamemap->layers[2].data[2] != 0) ||
(gamemap.layers[2].data[3] != 6) (gamemap->layers[2].data[3] != 6)
) { ) {
FAIL_BREAK(errctx, ERR_VALUE, "Map layers 1 and 3 should have tile data but it is incorrect"); FAIL_BREAK(errctx, AKERR_VALUE, "Map layers 1 and 3 should have tile data but it is incorrect");
} }
_test_tilemap_load_layers_cleanup: _test_akgl_tilemap_load_layers_cleanup:
} CLEANUP { } CLEANUP {
if ( pathstr != NULL ) { if ( pathstr != NULL ) {
IGNORE(heap_release_string(pathstr)); IGNORE(akgl_heap_release_string(pathstr));
} }
if ( doc != NULL ) { if ( doc != NULL ) {
json_decref(doc); json_decref(doc);
@@ -291,56 +304,59 @@ _test_tilemap_load_layers_cleanup:
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_tilemap_load_tilesets(void) akerr_ErrorContext *test_akgl_tilemap_load_tilesets(void)
{ {
string *pathstr = NULL; akgl_String *pathstr = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *doc = NULL; json_t *doc = NULL;
json_error_t errdata; json_error_t errdata;
SDL_Texture *image = NULL; SDL_Texture *image = NULL;
memset((void *)&gamemap, 0x00, sizeof(tilemap)); memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT { ATTEMPT {
CATCH(errctx, heap_next_string(&pathstr)); gamemap = &_akgl_gamemap;
snprintf((char *)&pathstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj"); 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); doc = json_load_file((char *)&pathstr->data, 0, &errdata);
FAIL_ZERO_BREAK(errctx, doc, ERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text); snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
CATCH(errctx, tilemap_load_tilesets(&gamemap, doc)); FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
FAIL_NONZERO_BREAK(errctx, (gamemap.numtilesets != 1), ERR_VALUE, "Incorrect number of tilesets loaded for map"); CATCH(errctx, akgl_tilemap_load_tilesets(gamemap, doc, pathstr));
if ( (gamemap.tilesets[0].columns != 48 ) || FAIL_NONZERO_BREAK(errctx, (gamemap->numtilesets != 1), AKERR_VALUE, "Incorrect number of tilesets loaded for map");
(gamemap.tilesets[0].firstgid != 1) || if ( (gamemap->tilesets[0].columns != 48 ) ||
(gamemap.tilesets[0].imageheight != 576) || (gamemap->tilesets[0].firstgid != 1) ||
(gamemap.tilesets[0].imagewidth != 768) || (gamemap->tilesets[0].imageheight != 576) ||
(gamemap.tilesets[0].margin != 0) || (gamemap->tilesets[0].imagewidth != 768) ||
(gamemap.tilesets[0].spacing != 0) || (gamemap->tilesets[0].margin != 0) ||
(gamemap.tilesets[0].tilecount != 1728) || (gamemap->tilesets[0].spacing != 0) ||
(gamemap.tilesets[0].tileheight != 16) || (gamemap->tilesets[0].tilecount != 1728) ||
(gamemap.tilesets[0].tilewidth != 16) ) { (gamemap->tilesets[0].tileheight != 16) ||
FAIL_BREAK(errctx, ERR_VALUE, "Tileset loaded with incorrect values"); (gamemap->tilesets[0].tilewidth != 16) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Tileset loaded with incorrect values");
} }
FAIL_NONZERO_BREAK( FAIL_NONZERO_BREAK(
errctx, errctx,
strcmp((char *)&gamemap.tilesets[0].name, "World_A1"), strcmp((char *)&gamemap->tilesets[0].name, "World_A1"),
ERR_VALUE, AKERR_VALUE,
"Tileset loaded with incorrect name"); "Tileset loaded with incorrect name");
snprintf((char *)&pathstr->data, MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/World_A1.png"); snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/World_A1.png");
image = IMG_LoadTexture(renderer, (char *)&pathstr->data); image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&pathstr->data);
FAIL_ZERO_BREAK(errctx, image, ERR_SDL, "Failed to load comparison image"); FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
CATCH( CATCH(
errctx, errctx,
render_and_compare( akgl_render_and_compare(
gamemap.tilesets[0].texture, gamemap->tilesets[0].texture,
image, image,
0, 0, 768, 576, 0, 0, 768, 576,
"test_tilemap_loaded_tileset.png") "test_akgl_tilemap_loaded_tileset.png")
); );
} CLEANUP { } CLEANUP {
if ( pathstr != NULL ) { if ( pathstr != NULL ) {
IGNORE(heap_release_string(pathstr)); IGNORE(akgl_heap_release_string(pathstr));
} }
if ( doc != NULL ) { if ( doc != NULL ) {
json_decref(doc); json_decref(doc);
@@ -350,25 +366,29 @@ ErrorContext *test_tilemap_load_tilesets(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_tilemap_load(void) akerr_ErrorContext *test_akgl_tilemap_load(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
memset((void *)&gamemap, 0x00, sizeof(tilemap)); memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT { ATTEMPT {
CATCH(errctx, tilemap_load("assets/testmap.tmj", &gamemap)); gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_tilemap_load("assets/testmap.tmj", gamemap));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_tilemap_draw(void) akerr_ErrorContext *test_akgl_tilemap_draw(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -376,11 +396,13 @@ ErrorContext *test_tilemap_draw(void)
} }
ErrorContext *test_tilemap_draw_tileset(void) akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -392,31 +414,34 @@ int main(void)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
FAIL_BREAK(errctx, ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
} }
if (!SDL_CreateWindowAndRenderer("net/aklabs/libsdl3game/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, ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
} }
renderer->draw_texture = &akgl_render_2d_draw_texture;
CATCH(errctx, registry_init()); CATCH(errctx, akgl_registry_init());
CATCH(errctx, heap_init()); CATCH(errctx, akgl_heap_init());
CATCH(errctx, sprite_load_json("assets/testsprite.json")); CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
CATCH(errctx, sprite_load_json("assets/testsprite2.json")); CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
CATCH(errctx, character_load_json("assets/testcharacter.json")); CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));
CATCH(errctx, test_tilemap_get_json_tilemap_property()); CATCH(errctx, test_tilemap_akgl_get_json_tilemap_property());
CATCH(errctx, test_tilemap_compute_tileset_offsets()); CATCH(errctx, test_akgl_tilemap_compute_tileset_offsets());
CATCH(errctx, test_tilemap_load_layer_objects()); CATCH(errctx, test_akgl_tilemap_load_layer_objects());
CATCH(errctx, test_tilemap_load_layer_tile()); CATCH(errctx, test_akgl_tilemap_load_layer_tile());
CATCH(errctx, test_tilemap_load_layers()); CATCH(errctx, test_akgl_tilemap_load_layers());
CATCH(errctx, test_tilemap_load_tilesets()); CATCH(errctx, test_akgl_tilemap_load_tilesets());
//CATCH(errctx, test_tilemap_load()); //CATCH(errctx, test_akgl_tilemap_load());
//CATCH(errctx, test_tilemap_draw_tileset()); //CATCH(errctx, test_akgl_tilemap_draw_tileset());
//CATCH(errctx, test_tilemap_draw()); //CATCH(errctx, test_akgl_tilemap_draw());
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);

View File

@@ -1,42 +1,43 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <sdlerror.h> #include <akerror.h>
#include <sdl3game/util.h> #include <akgl/error.h>
#include <akgl/util.h>
ErrorContext *test_rectangle_points_nullpointers(void) akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
{ {
RectanglePoints points; RectanglePoints points;
SDL_FRect testrect; SDL_FRect testrect;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, rectangle_points(NULL, NULL)); CATCH(errctx, akgl_rectangle_points(NULL, NULL));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "rectangle_points fails to FAIL with all NULL pointers"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with all NULL pointers");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, rectangle_points(NULL, &testrect)); CATCH(errctx, akgl_rectangle_points(NULL, &testrect));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "rectangle_points fails to FAIL with NULL SDL_FRect pointer"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL SDL_FRect pointer");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, rectangle_points(&points, NULL)); CATCH(errctx, akgl_rectangle_points(&points, NULL));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "rectangle_points fails to FAIL with NULL RectanglePoints pointer"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL RectanglePoints pointer");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, rectangle_points(&points, &testrect)); CATCH(errctx, akgl_rectangle_points(&points, &testrect));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -44,7 +45,7 @@ ErrorContext *test_rectangle_points_nullpointers(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_rectangle_points_math(void) akerr_ErrorContext *test_akgl_rectangle_points_math(void)
{ {
RectanglePoints points; RectanglePoints points;
SDL_FRect testrect = {.x = 0, .y = 0, .w = 32, .h = 32}; SDL_FRect testrect = {.x = 0, .y = 0, .w = 32, .h = 32};
@@ -52,7 +53,7 @@ ErrorContext *test_rectangle_points_math(void)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, rectangle_points(&points, &testrect)); CATCH(errctx, akgl_rectangle_points(&points, &testrect));
if ( points.topleft.x != 0 || if ( points.topleft.x != 0 ||
points.topleft.y != 0 || points.topleft.y != 0 ||
points.topright.x != 32 || points.topright.x != 32 ||
@@ -63,8 +64,8 @@ ErrorContext *test_rectangle_points_math(void)
points.bottomright.y != 32 ) { points.bottomright.y != 32 ) {
FAIL_BREAK( FAIL_BREAK(
errctx, errctx,
ERR_BEHAVIOR, AKGL_ERR_BEHAVIOR,
"rectangle_points incorrectly calculated points for {x=0, y=0, w=32, h=32} to {topleft={%d, %d}, topright={%d, %d}, bottomleft={%d, %d}, bottomright={%d, %d}}", "akgl_rectangle_points incorrectly calculated points for {x=0, y=0, w=32, h=32} to {topleft={%d, %d}, topright={%d, %d}, bottomleft={%d, %d}, bottomright={%d, %d}}",
points.topleft.x, points.topleft.y, points.topleft.x, points.topleft.y,
points.topright.x, points.topright.y, points.topright.x, points.topright.y,
points.bottomleft.x, points.bottomleft.y, points.bottomleft.x, points.bottomleft.y,
@@ -77,7 +78,7 @@ ErrorContext *test_rectangle_points_math(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_collide_point_rectangle_nullpointers(void) akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
{ {
point testpoint; point testpoint;
RectanglePoints testrectpoints; RectanglePoints testrectpoints;
@@ -86,43 +87,43 @@ ErrorContext *test_collide_point_rectangle_nullpointers(void)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, collide_point_rectangle(&testpoint, &testrectpoints, NULL)); CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, NULL));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "collide_point_rectangle(*, *, NULL) failed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, *, NULL) failed");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, collide_point_rectangle(&testpoint, NULL, &testcollide)); CATCH(errctx, akgl_collide_point_rectangle(&testpoint, NULL, &testcollide));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "collide_point_rectangle(*, NULL, *) failed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, NULL, *) failed");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, collide_point_rectangle(NULL, &testrectpoints, &testcollide)); CATCH(errctx, akgl_collide_point_rectangle(NULL, &testrectpoints, &testcollide));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "collide_point_rectangle(NULL, *, *) failed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, *, *) failed");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, collide_point_rectangle(NULL, NULL, NULL)); CATCH(errctx, akgl_collide_point_rectangle(NULL, NULL, NULL));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "collide_point_rectangle(NULL, NULL, NULL) failed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, NULL, NULL) failed");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, collide_point_rectangle(&testpoint, &testrectpoints, &testcollide)); CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -130,7 +131,7 @@ ErrorContext *test_collide_point_rectangle_nullpointers(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_collide_point_rectangle_logic(void) akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void)
{ {
point testpoint = {.x = 16, .y = 16}; point testpoint = {.x = 16, .y = 16};
SDL_FRect testrect = { .x = 0, .y = 0, .w = 32, .h = 32}; SDL_FRect testrect = { .x = 0, .y = 0, .w = 32, .h = 32};
@@ -139,17 +140,17 @@ ErrorContext *test_collide_point_rectangle_logic(void)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, rectangle_points(&testrectpoints, &testrect)); CATCH(errctx, akgl_rectangle_points(&testrectpoints, &testrect));
CATCH(errctx, collide_point_rectangle(&testpoint, &testrectpoints, &testcollide)); CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
if ( testcollide == false ) { if ( testcollide == false ) {
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Valid collision missed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
} }
testpoint.x = 48; testpoint.x = 48;
testpoint.y = 48; testpoint.y = 48;
CATCH(errctx, collide_point_rectangle(&testpoint, &testrectpoints, &testcollide)); CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
if ( testcollide == true ) { if ( testcollide == true ) {
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Invalid collision reported"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
} }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
@@ -157,7 +158,7 @@ ErrorContext *test_collide_point_rectangle_logic(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_collide_rectangles_nullpointers(void) akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
{ {
SDL_FRect testrect1; SDL_FRect testrect1;
SDL_FRect testrect2; SDL_FRect testrect2;
@@ -166,43 +167,43 @@ ErrorContext *test_collide_rectangles_nullpointers(void)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, collide_rectangles(&testrect1, &testrect2, NULL)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, NULL));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "collide_rectangles(*, *, NULL) failed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, *, NULL) failed");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, collide_rectangles(&testrect1, NULL, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, NULL, &testcollide));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "collide_rectangles(*, NULL, *) failed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, NULL, *) failed");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, collide_rectangles(NULL, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(NULL, &testrect2, &testcollide));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "collide_rectangles(NULL, *, *) failed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, *, *) failed");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, collide_rectangles(NULL, NULL, NULL)); CATCH(errctx, akgl_collide_rectangles(NULL, NULL, NULL));
FAIL_BREAK(errctx, ERR_BEHAVIOR, "collide_rectangles(NULL, NULL, NULL) failed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, NULL, NULL) failed");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, ERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, collide_rectangles(&testrect1, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -210,7 +211,7 @@ ErrorContext *test_collide_rectangles_nullpointers(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
ErrorContext *test_collide_rectangles_logic(void) akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
{ {
SDL_FRect testrect1 = { .x = 0, .y = 0, .w = 32, .h = 32}; SDL_FRect testrect1 = { .x = 0, .y = 0, .w = 32, .h = 32};
SDL_FRect testrect2 = { .x = 30, .y = 30, .w = 40, .h = 40}; SDL_FRect testrect2 = { .x = 30, .y = 30, .w = 40, .h = 40};
@@ -220,32 +221,32 @@ ErrorContext *test_collide_rectangles_logic(void)
ATTEMPT { ATTEMPT {
// Collision overlapping on the top left // Collision overlapping on the top left
CATCH(errctx, collide_rectangles(&testrect1, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) { if ( testcollide == false ) {
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Valid collision missed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
} }
// Collision overlapping on the top right // Collision overlapping on the top right
testrect1.x = 64; testrect1.x = 64;
CATCH(errctx, collide_rectangles(&testrect1, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) { if ( testcollide == false ) {
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Valid collision missed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
} }
// Collision overlapping on the bottom left // Collision overlapping on the bottom left
testrect1.x = 0; testrect1.x = 0;
testrect1.y = 32; testrect1.y = 32;
CATCH(errctx, collide_rectangles(&testrect1, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) { if ( testcollide == false ) {
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Valid collision missed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
} }
// Collision overlapping on the bottom right // Collision overlapping on the bottom right
testrect1.x = 32; testrect1.x = 32;
testrect1.y = 32; testrect1.y = 32;
CATCH(errctx, collide_rectangles(&testrect1, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) { if ( testcollide == false ) {
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Valid collision missed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
} }
// Collision overlapping the top edge // Collision overlapping the top edge
@@ -253,9 +254,9 @@ ErrorContext *test_collide_rectangles_logic(void)
testrect1.y = 0; testrect1.y = 0;
testrect1.w = 60; testrect1.w = 60;
testrect1.h = 32; testrect1.h = 32;
CATCH(errctx, collide_rectangles(&testrect1, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) { if ( testcollide == false ) {
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Valid collision missed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
} }
// Collision overlapping the left edge // Collision overlapping the left edge
@@ -263,9 +264,9 @@ ErrorContext *test_collide_rectangles_logic(void)
testrect1.y = 0; testrect1.y = 0;
testrect1.w = 35; testrect1.w = 35;
testrect1.h = 80; testrect1.h = 80;
CATCH(errctx, collide_rectangles(&testrect1, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) { if ( testcollide == false ) {
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Valid collision missed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
} }
// Collision overlapping the right edge // Collision overlapping the right edge
@@ -273,9 +274,9 @@ ErrorContext *test_collide_rectangles_logic(void)
testrect1.y = 0; testrect1.y = 0;
testrect1.w = 60; testrect1.w = 60;
testrect1.h = 80; testrect1.h = 80;
CATCH(errctx, collide_rectangles(&testrect1, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) { if ( testcollide == false ) {
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Valid collision missed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
} }
// Collision overlapping the bottom edge // Collision overlapping the bottom edge
@@ -283,9 +284,9 @@ ErrorContext *test_collide_rectangles_logic(void)
testrect1.y = 65; testrect1.y = 65;
testrect1.w = 80; testrect1.w = 80;
testrect1.h = 32; testrect1.h = 32;
CATCH(errctx, collide_rectangles(&testrect1, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) { if ( testcollide == false ) {
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Valid collision missed"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
} }
// Not colliding // Not colliding
@@ -293,9 +294,9 @@ ErrorContext *test_collide_rectangles_logic(void)
testrect1.y = 0; testrect1.y = 0;
testrect1.w = 16; testrect1.w = 16;
testrect1.h = 16; testrect1.h = 16;
CATCH(errctx, collide_rectangles(&testrect1, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == true ) { if ( testcollide == true ) {
FAIL_BREAK(errctx, ERR_BEHAVIOR, "Invalid collision reported"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
} }
} CLEANUP { } CLEANUP {
@@ -309,11 +310,11 @@ int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, test_rectangle_points_nullpointers()); CATCH(errctx, test_akgl_rectangle_points_nullpointers());
CATCH(errctx, test_rectangle_points_math()); CATCH(errctx, test_akgl_rectangle_points_math());
CATCH(errctx, test_collide_point_rectangle_nullpointers()); CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());
CATCH(errctx, test_collide_rectangles_nullpointers()); CATCH(errctx, test_akgl_collide_rectangles_nullpointers());
CATCH(errctx, test_collide_rectangles_logic()); CATCH(errctx, test_akgl_collide_rectangles_logic());
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);

BIN
util/assets/Actor1.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
util/assets/charviewer Executable file

Binary file not shown.

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "../assets/Actor1.png",
"frame_width": 48,
"frame_height": 48
},
"name": "little guy facing down",
"width": 48,
"height": 48,
"speed": 0,
"loop": false,
"loopReverse": false,
"frames": [
1
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "../assets/Actor1.png",
"frame_width": 48,
"frame_height": 48
},
"name": "little guy facing left",
"width": 48,
"height": 48,
"speed": 0,
"loop": false,
"loopReverse": false,
"frames": [
13
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "../assets/Actor1.png",
"frame_width": 48,
"frame_height": 48
},
"name": "little guy facing right",
"width": 48,
"height": 48,
"speed": 0,
"loop": false,
"loopReverse": false,
"frames": [
25
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "../assets/Actor1.png",
"frame_width": 48,
"frame_height": 48
},
"name": "little guy facing up",
"width": 48,
"height": 48,
"speed": 0,
"loop": false,
"loopReverse": false,
"frames": [
37
]
}

View File

@@ -0,0 +1,18 @@
{
"spritesheet": {
"filename": "../assets/Actor1.png",
"frame_width": 48,
"frame_height": 48
},
"name": "little guy walking down",
"width": 48,
"height": 48,
"speed": 1000,
"loop": true,
"loopReverse": true,
"frames": [
0,
1,
2
]
}

View File

@@ -0,0 +1,18 @@
{
"spritesheet": {
"filename": "../assets/Actor1.png",
"frame_width": 48,
"frame_height": 48
},
"name": "little guy walking left",
"width": 48,
"height": 48,
"speed": 1000,
"loop": true,
"loopReverse": true,
"frames": [
12,
13,
14
]
}

View File

@@ -0,0 +1,18 @@
{
"spritesheet": {
"filename": "../assets/Actor1.png",
"frame_width": 48,
"frame_height": 48
},
"name": "little guy walking right",
"width": 48,
"height": 48,
"speed": 1000,
"loop": true,
"loopReverse": true,
"frames": [
24,
25,
26
]
}

Some files were not shown because too many files have changed in this diff Show More