20 Commits

Author SHA1 Message Date
b899f09fc8 Note that defect 26 is fixed under the per-axis scale item
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 20s
libakgl CI Build / performance (push) Failing after 18s
libakgl CI Build / memory_check (push) Failing after 14s
libakgl CI Build / mutation_test (push) Failing after 16s
Upstream filed the per-axis scale item while the drawn-height-from-width bug
was still open, and recorded akbasic working around both together. That half is
fixed, so a non-square sprite now draws at its own proportions through the
library's own renderfunc. The uniform-scale half stands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:37:16 -04:00
9ec0b41460 Write down the rule the 0.5.0 defect work kept re-learning
A "Fixing Defects" section in AGENTS.md: read what the code does before
deciding what it should do. Six rules, every one of them broken in this
repository during the 0.5.0 work, and every one cheap to check and expensive to
assume.

The one that prompted it: the savegame fix grew four name-width aliases to let
the on-disk format diverge from the object model, and akgl_game_load already
refuses a save from another build sixteen lines above the tables being edited.
The abstraction defended a case that cannot arise.

The rest are the same mistake in other clothes -- two tests that passed against
the unfixed library, a bitmask case that gives the same answer under both
parses, three CI exclusions resting on a premise nobody had re-checked, and
eleven TODO entries describing code that had already changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:34:52 -04:00
58b99e0477 Drop the savegame name-width aliases
They were four #defines, one per table, each expanding to exactly one existing
constant and used exactly once per side. That is indirection with no second
consumer, and this repository's own rule is to abstract when the second
consumer is real and not before.

The reasoning behind them does not survive contact either. They were meant to
let the on-disk format's field widths diverge from the object model's -- but
raising AKGL_ACTOR_MAX_NAME_LENGTH is an ABI change, which bumps the version,
and akgl_game_load refuses a save whose libversion does not match before it
reads a single name table. The divergence they anticipated cannot happen
quietly.

The reader now names the same constant the writer does, per table, which is all
the fix ever needed to be. What actually catches a disagreement is the EOF
check at the end of akgl_game_load, and that works however the widths are
spelled: reverting the spritesheet reader to the wrong constant still fails the
roundtrip test with AKERR_IO.

25/25 pass, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:34:51 -04:00
28fa929f6a Make savegames, optional array elements, empty text and coverage work
Closes Known-and-still-open items 7 and 13, and Defects items 18, 25 and 27.

The savegame name tables carry no length prefix, so writer and reader have to
agree on a field width exactly. The writer used each object's own maximum name
length -- 512 for a spritesheet, a filename -- and the reader used
AKGL_ACTOR_MAX_NAME_LENGTH for all four. Four AKGL_GAME_SAVE_*_NAME_WIDTH
constants drive both sides now.

The failure turned out to be worse than "cannot be read back": a reader
stepping the wrong width does not run off anything, it finds a run of zeros
inside an entry, stops early, and reports success with silently wrong maps. A
test asserting only that the load succeeded passed against the broken reader.
So akgl_game_load checks it is at EOF once the tables are read, which turns a
width disagreement into AKERR_IO instead of a corruption. That is the assertion
the new roundtrip test -- the first with all four registries populated -- hangs
on.

akgl_get_json_with_default gains a third HANDLE_GROUP for AKERR_OUTOFBOUNDS,
which is what the array index accessors report, so "this element is optional"
works for an array element and not only for an object member. The arm goes
above the one holding the memcpy: HANDLE_GROUP emits no break and every arm
falls into that body.

That test needed a second attempt. with_default returns *the context it was
given* when it does not handle the status, so TEST_EXPECT_OK -- which releases
whatever the statement returns -- double-released it against a CLEANUP block
that released it too, and a double-released context corrupts the failure rather
than reporting it. The first draft passed against the unfixed library.

akgl_text_rendertextat returns success without rasterizing for the empty
string, matching akgl_text_measure, which has always accepted it. The check
sits after the font and backend guards, so drawing nothing still refuses what
drawing something refuses. tests/text.c had this case written and unasserted
waiting for the two halves of the header to agree.

character_load_json_state_int_from_strings guards dest rather than testing
states twice. Not asserted: the function is static with one call site that
passes a real pointer, so the guard cannot fire, and reaching it from a test
would mean giving it external linkage purely for that.

Both gcovr invocations take the build tree as an explicit positional search
path. gcovr searches --root when given none, which is the source directory,
where build trees live; --object-directory does not narrow it. Verified by
building two instrumented trees with a source edit between them: the old
invocation fails with "Got function write_exact on multiple lines: 46, 48" and
exits 64, the new one exits 0 and the full coverage run passes with the stale
tree still present.

25/25 pass, reindent --check, check_api_surface and check_error_protocol clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:34:51 -04:00
947bc03601 Re-verdict performance targets 16 and 17
Both cited defects that are fixed. 17 is met outright; 16 is qualified, because
the tilemap cycle is the one that was leaking and the one now asserted, and
nothing yet asserts the same of a sprite, spritesheet or character cycle -- a
gap in the tests rather than a known leak.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:34:51 -04:00
b0a4e36ded Regenerate the controller database with the fixed generator
A deliberate standalone commit, as AGENTS.md asks for this file.

Content is byte-identical -- 2255 mappings, not one changed -- which is the
point: it demonstrates the rewritten mkcontrollermappings.sh produces exactly
what the old one did on the success path. The only differences are the $(date)
stamp and the include guard, which is _AKGL_SDL_GAMECONTROLLERDB_H_ now so it
matches every other header in include/akgl/.

That rename was made in the generator rather than by hand, per the rule about
generated files, and the tracked copy carries it so the next regeneration shows
no spurious diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:34:50 -04:00
d9f0187ecf Stop a failed controller-DB fetch from destroying the tracked fallback
Closes Defects -> Known and still open item 12, both halves.

include/akgl/SDL_GameControllerDB.h is tracked on purpose: it is the offline
fallback that keeps the library buildable when upstream is unreachable. The
script that writes it had no set -e and never checked curl, so a failed fetch
wrote AKGL_SDL_GAMECONTROLLER_DB_LEN 0 and an empty array over the good copy
and exited 0. The safety net destroyed itself, and because CMake re-ran the
generator on every build, an offline build was enough to do it.

The script now runs under set -euo pipefail, fetches into a temporary
directory, and moves the result into place only after checking curl's exit
status (with --fail, so an HTTP error is a status rather than an error page in
the body), a plausible minimum mapping count, and that no mapping carries a
quote or backslash that would break the C string literal it becomes. Any of
those failing leaves the tracked header exactly as it was and exits non-zero.

Verified against all five failure modes -- unresolvable host, 404, truncated
response, empty-but-successful response (the original failure exactly), and a
response containing a quote. Each refuses, and the header's checksum is
unchanged after every one. AKGL_CONTROLLERDB_URL and
AKGL_CONTROLLERDB_MIN_LINES are environment-overridable, which is how.

The build no longer runs it at all. The add_custom_command declared a relative
OUTPUT, which CMake resolves against the binary directory while the script
writes to the source directory, so the declared output never appeared and every
build re-ran the command -- needing network access and leaving the tree dirty.
It is an explicit `controllerdb` target now, and the header is no longer listed
as a library source, which is all it was there for.

The empty-initializer problem goes with it: an empty array initializer is a
constraint violation in ISO C that compiles only as a GCC extension, and the
minimum-count check guarantees at least one entry.

Also here, because it rested on a premise that turned out to be false: the
character suite is no longer excluded from CI's coverage and memory-check jobs,
nor from the mutation harness by default. It was excluded on the grounds that
it failed deliberately. It did not -- it was reporting success while running
one of its four tests.

25/25 pass, memcheck clean, shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:34:28 -04:00
913834a3af Draw actors at their sprite's height, and update each one once a frame
Closes Defects item 26 and Performance item 32.

akgl_actor_render set dest.h from curSprite->width, so every actor was drawn
square and a non-square sprite was stretched or squashed. Invisible in the
fixtures because they are all square, so tests/actor.c gets a 48x24 sprite and
a render backend whose draw_texture records the rectangle it is handed instead
of drawing it. That recording backend is the first coverage akgl_actor_render
has had at all -- every other test in the file stubs renderfunc out.

akgl_game_update looped over AKGL_TILEMAP_MAX_LAYERS with the actor sweep
nested inside it and never compared an actor's layer to the layer it was on, so
every live actor's updatefunc ran sixteen times a frame. The sweep is hoisted
out: updating an actor is not a per-layer operation, and
akgl_render_2d_draw_world already walks the layers for the half that is.
AKGL_ITERATOR_OP_LAYERMASK is honoured rather than ignored now, so a caller who
wants one layer can still ask, and gets each of those actors once.

Counting is the assertion, deliberately. The defect was invisible in the frame
total because the tilemap blits are three orders of magnitude larger, so a
timing test would have measured the rasterizer. tests/game.c counts calls into
a stub updatefunc and reports 16 against the old code.

PERFORMANCE.md records the timing side as what it honestly is: the gap between
the akgl_game_update and draw_world rows of the same run, 92 us before and
noise in both directions after. The absolute table is not re-taken -- a later
run on this machine read every row about 15% high, including rows nothing has
touched.

25/25 pass, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:34:27 -04:00
3ee8c60491 Record the fixes whose TODO.md edits were dropped
Eleven entries still described the old code: known-and-still-open items 1, 2,
5, 9 and 11, error-handling items 16, 17 and 18, and defects 20, 29 and 30. The
code and the tests were right; the document was not.

The edits were lost the same way each time. The scripts that made them built
the whole file in memory and wrote it once at the end, so an assertion failure
partway through -- on an entry whose text had already been reworded -- threw
away that script's earlier, correct replacements along with the failed one.
Written one entry at a time now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:34:27 -04:00
3f37d38f2d Re-record coverage after the 0.5.0 defect work
83.9% line and 91.2% function, from 79.6% / 87.2%. src/tilemap.c moved the
most, 47% to 62%, because bounding the loaders and proving the pool accounting
needed them driven rather than merely called.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:34:27 -04:00
9c2f80bcb9 Give back every pooled string and sprite reference the loaders take
Closes Defects items 20, 22, 23 and 29, and the residual of item 38.

The tilemap load leak was five pooled strings per load; the property-lookup
fix in an earlier commit took it to two, and the last two were each a claim
with no matching release -- the string every layer's `type` was read into, and
the dirname the map's relative paths resolve against. tests/tilemap.c asserts
the pool is exactly where it started after one load/release cycle and after 64.

Finding those two was a matter of dumping the contents of every still-claimed
slot after a cycle rather than reading the code again; 'tilelayer' and an
assets directory named themselves immediately.

Same file, same class, fixed with it: akgl_tilemap_load_layer_objects released
its scratch string after reading each object's name and then kept using the
slot, because akgl_get_json_string_value reuses a non-NULL destination without
taking another reference. The slot was free while still live, so any other
claim could have been handed it.

akgl_character_sprite_add wrote over an existing binding without releasing the
sprite it displaced, so a character that rebinds a state while alive leaked a
sprite slot per rebind -- teardown only gives back what the map holds at the
end. The new reference is taken before the write and given back if the write
fails, so there is no window where a sprite is bound with nothing behind it.
The write was unchecked too.

Three failure-path leaks moved into CLEANUP blocks: akgl_render_2d_init's two
pooled strings, akgl_controller_open_gamepads' enumeration array, and
akgl_text_rendertextat's surface and texture -- the last being a leak per frame
on a HUD line.

akgl_text_unloadallfonts() closes every font in the registry and destroys it,
which is what item 38 left open. Deliberately not a whole akgl_game_shutdown:
tearing down the mixer, SDL_ttf and SDL in the right order is a design
question, and this is the part that was simply missing. It is a new public
symbol, which 0.5.0 already covers -- this release has not shipped.

Every fix has a test that fails against the old code.

25/25 pass, memcheck clean, reindent --check, check_api_surface and
check_error_protocol all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:55 -04:00
8a90cbf275 Drop the perf suite's lowfpsfunc workaround, and record item 31
akgl_game_update_fps installs the default itself now, so the benchmark no
longer has to. Leaving the hook NULL there is what keeps that true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:38 -04:00
21d69192e3 Report the failures that used to be crashes
Closes Defects items 30 and 31 and Known-and-still-open items 1, 2, 5, 9 and 11.

Both string accessors in json_helpers.c ended their ATTEMPT block with
FINISH(errctx, false), which swallows the failure, and then strncpy'd through
the pointer akgl_heap_next_string never set. So the one condition the pool
exists to report -- it is full, which in practice means something is not
releasing -- arrived as a segfault somewhere else entirely. It is
FINISH(errctx, true) now, and tests/json_helpers.c claims every slot and
asserts AKGL_ERR_HEAP comes back out of both. That test segfaults against the
old code, which is also how the tilemap leak test in the previous commit
confirmed this one.

akgl_tilemap_release tested layers[i].texture and destroyed
tilesets[i].texture, so every tileset texture was freed twice on one release
and no image layer's texture was freed at all. Pointers are cleared as they go,
so a second release is safe instead of a use-after-free.

akgl_game_update_fps called game.lowfpsfunc() unguarded, on a path taken on
frame one because fps is 0 for the first second. Only akgl_game_init installs
it, and renderer.h documents the other path deliberately -- a host that owns
its window binds a backend instead. It installs the default when it finds NULL.

akgl_controller_pushmap and akgl_controller_default checked only the upper
bound, so a negative id indexed before akgl_controlmaps.

The two test-harness helpers were quietly worthless. akgl_render_and_compare
drew t1 on both passes, so it always reported a match and every image assertion
built on it asserted nothing; and akgl_compare_sdl_surfaces memcmp'd
s1->pitch * s1->h bytes out of both surfaces without checking that the second
was the same size, so a smaller one was read past its end. Both fixed, both
tested. tests/util.c also now calls the collide-point test it has defined and
never run.

25/25 pass, memcheck clean, reindent --check and check_error_protocol clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:37 -04:00
230278d303 Bound every array a data file can index
Closes Defects items 16 and 17 and Known-and-still-open item 6. All three let
an asset file, or a caller's argument, write past a fixed array.

akgl_sprite_load_json took its frame count straight from the document and wrote
that many entries into a 16-byte frameids -- through a uint32_t * cast of a
uint8_t *, so each write touched four bytes and the overrun reached four bytes
past the array, into the rest of akgl_Sprite and then the next pool slot. The
count is checked first now, each id is read into an int and narrowed
deliberately, and a frame number too large for a uint8_t is refused rather than
truncated into an index for a different tile.

The tilemap loader had the same shape twice: objects[j] with no check against
AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER and tilesets[i] with none against
AKGL_TILEMAP_MAX_TILESETS. akgl_tilemap_load_layers already bounded its own
loop, so the pattern was in the same file. The object one is the reachable
half -- 128 objects is not a large object layer.

akgl_string_initialize zeroed sizeof(akgl_String) starting at `data`, which
begins after the refcount in front of it, so it ran four bytes past the end of
the object and onto the *next* slot's refcount -- the field the allocator reads
to decide whether a slot is free. Same file, same class, fixed with it:
akgl_string_copy accepted a count larger than the buffers, reading past one
pool slot and writing past another, which the header documented as behaviour.

Every case has a test that fails against the old code, with five new fixtures.
Exactly-the-maximum is asserted alongside one-past in each, so the bound cannot
be fixed by making the limit off by one.

25/25 pass, memcheck clean, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:37 -04:00
6b1cf437d3 Record that akgl_registry_init now brings up the properties registry
Missed in the previous commit, which made the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:37 -04:00
19530f6a97 Fix the type, macro and state-table defects, and the leftover debris
Closes internal-consistency items 19 through 36, 38 and 41. Item 37, the ~180
redundant casts, is deliberately left open with its reasoning in TODO.md: the
benefit only arrives once the build turns on the warnings those casts suppress,
and doing it before that is churn across the two files with the most
outstanding functional defects.

The two that were real bugs are in the actor state table.
AKGL_ACTOR_STATE_STRING_NAMES was declared [AKGL_ACTOR_MAX_STATES+1] and
defined [32], so a consumer trusting the declared bound read past the object;
and indices 11 and 12 were named UNDEFINED_11 and UNDEFINED_12 where actor.h
has MOVING_IN and MOVING_OUT, so no character JSON could bind a sprite to
either state. tests/registry.c now walks the whole table -- every entry
non-NULL, every entry resolving to its own bit, no two entries sharing a name.

The bitmask macros are parenthesized and AKGL_BITMASK_CLEAR has lost the
semicolon inside its body. Writing tests/bitmasks.c for that turned up
something worth knowing: the obvious test does not catch it. For a bit that is
set, the misparse `!(mask & bit) == bit` gives the same answer as the correct
one. It only diverges for an unset bit whose value is not 1, and that is the
shape the suite uses now.

akgl_draw_background was the last public function outside the error protocol.
It takes a backend like everything else in draw.h, restores the draw colour it
found, and is tested -- TODO.md had it filed under "needs the offscreen
renderer harness", which was never true; what it needed was to stop reading the
global.

All eight registry initializers go through one helper, so the seven that leaked
an SDL_PropertiesID on every call after the first no longer do. Fixed in the
same place because it is the same function: akgl_registry_init never called
akgl_registry_init_properties, which made akgl_set_property a silent no-op for
anyone not going through akgl_game_init -- Defects, Known and still open item 3.

Also: AKGL_COLLIDE_RECTANGLES (three open parens, two closes) and akgl_Frame
deleted, float32_t/float64_t used consistently, the developer-specific debug
logging removed from the controller inner loop, the abandoned SDL_GetBasePath
comments removed, nine unused locals removed, and dst renamed to dest.

akgl_game_update's default flags no longer OR the same bit twice. That changes
nothing today, and the reason is Performance item 32: the loop never reads
either bit, which is why every actor is updated sixteen times a frame. Still
open.

25/25 pass, memcheck clean, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:36 -04:00
a60220d39b Name every local error context errctx
Closes internal-consistency item 18. The convention was settled -- AGENTS.md
records errctx winning 92 sites to 45 -- but the other 45 were still there, and
four files favoured `e` throughout while six favoured errctx, sometimes within
one file.

Its own commit because it is a rename and nothing else. All 333 changed lines
are a single identifier substitution: applying \be\b -> errctx to each removed
line reproduces the added line exactly, and the counts match at 333 either way.

`e` keeps its meaning where the convention actually wants it -- an incoming
error context being inspected, as in akgl_get_json_with_default(e, ...) -- which
is why that function was left alone.

25/25 pass, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:36 -04:00
76ea04205c Stop returning past CLEANUP, and validate the arguments every sibling validates
Closes internal-consistency items 16 and 17.

Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and
skip every release in it. The one that mattered was the success path of
akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries
on every lookup that *found* what it was asked for, and a map load does that
several times per layer. tests/tilemap.c now runs each of its three paths --
found, absent, wrong type -- twice the pool size and asserts the pool is where
it started. Against the old code that test does not merely fail, it segfaults,
which is Defects item 30 seen from the outside: pool exhaustion arriving as a
NULL strncpy rather than as AKGL_ERR_HEAP.

Two of the conversions needed more than swapping the macro. In
akgl_get_json_tilemap_property a plain break would have fallen through to the
"property not found" FAIL_RETURN after FINISH, reporting a miss for something
found, so the success path sets a flag. In akgl_collide_rectangles the eight
early exits are followed by `*collide = false;`, which would have overwritten
the hit that broke out; each corner test writes the flag itself, so that line
is gone rather than moved. The same function also released its scratch string
once per loop iteration while continuing to use it, so the slot was free while
still live -- one claim now covers the whole scan.

akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was
the last statement in the ATTEMPT block, so the path that falls out of FINISH
reached the closing brace of a non-void function.

scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside
ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test.
Neither produces a compiler diagnostic and neither fails a test run until the
pool it drains is empty, which is why both have already shipped once.

For item 17: the eight typed JSON accessors that validated their container and
then wrote through dest unconditionally now check key and dest as the two
string accessors always did; the null physics backend checks its actors like
the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check
self, which the first two read straight through. tests/renderer.c calls all
three with NULL, which segfaulted before.

25/25 pass, memcheck clean, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:35 -04:00
3a262bee54 Give every exported function a declaration, and check that it stays that way
Closes internal-consistency items 7 through 15. Nineteen non-static functions
were in the ABI with no declaration anywhere, so no consumer could call them
and any consumer could collide with them.

The four gamepad_handle_* functions are the ones that mattered: controller.h
declared akgl_controller_handle_button_down and three siblings that did not
exist, so anything compiled against the header alone failed to link. The
definitions carry the declared names now, which also closes Defects -> Known
and still open item 10, and their documentation moved to the header.

The rest are either declared under a "part of the internal API" block --
akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to
declare for itself, plus six tilemap loader helpers the untested-loader work
wants to reach -- or static, which is what the four save iterators and
load_objectnamemap should always have been. akgl_path_relative_from is deleted:
declared nowhere, called from nowhere, never wrote its output, and leaked a
pooled string on every call, so it closes Known and still open item 4 and item
40 by ceasing to exist.

scripts/check_api_surface.sh keeps it closed. It reads the built library's
dynamic symbol table and every public header with comments stripped, and fails
on an exported akgl_* symbol that is declared nowhere. Stripping comments is
the whole point -- four of these were mentioned in controller.h prose, which is
how they went unnoticed.

The pool-size ceilings are defined once, in heap.h, so the #ifndef override
hook fires for the first time; actor.h, sprite.h and character.h were defining
the same four unconditionally from headers heap.h includes above its own guard.
tests/header_pool_override.c fails the compile if that regresses.

Also here: (void) rather than () on the twelve no-argument entry points,
AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix
dropped, and the six parameter-name mismatches. akgl_get_json_with_default had
its two contexts swapped rather than merely misspelled -- the incoming one was
`err` and its own was `e`, which is the name reserved for an incoming one.

24/24 pass, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:35 -04:00
9924d74dcc Namespace every exported symbol, and bump to 0.5.0
Closes internal-consistency items 1 through 6, 12 and 13. Every include guard
is _AKGL_<FILE>_H_, every in-project header include is angled, and every
exported function, type and global carries the akgl_ prefix. This is an ABI
break; the soname goes to libakgl.so.0.5. TODO.md carries the full rename
table.

The renames were driven by renaming each declaration and letting the compiler
find the uses, not by pattern substitution: renderer, physics and camera are
also parameter and struct-member names, and a sed would have rewritten
map->physics and every akgl_RenderBackend *renderer parameter without a word.

Item 4 turned out not to be cosmetic. The library exported a global called
renderer and tests/character.c defined an SDL_Renderer *renderer of its own;
the executable's definition preempted the library's, akgl_sprite_load_json
read a SDL_Renderer * through an akgl_RenderBackend *, and every texture load
in that suite failed. The suite reported success anyway, because libakerror's
unhandled-error handler ends in exit(errctx->status), exit keeps only the low
byte, and AKGL_ERR_SDL is exactly 256. So character had been green while
running one of its four tests, and every suite in the tree was unable to fail
on the most common status in a library built on SDL.

Both are fixed. tests/testutil.h gains TEST_TRAP_UNHANDLED_ERRORS(), which
collapses any status a byte cannot carry onto 1, and every suite installs it.
character binds a real backend with akgl_render_2d_bind. Its fourth test then
runs for the first time and fails on a defect it has asserted all along, so
akgl_heap_release_character now walks state_sprites with
AKGL_ITERATOR_OP_RELEASE and destroys the property set before zeroing the slot
-- TODO.md Defects item 21 and half of Carried over item 1.

AKGL_TIME_ONESEC_MS said "one second in milliseconds" and held 1000000, so
akgl_game_state_lock waited roughly sixteen minutes rather than one second. It
is AKGL_TIME_ONEMS_NS now, the budget is its own named constant, and
tests/game.c holds the mutex from a second thread to assert the wait -- the
contended path had no coverage at all.

Headers are self-contained and it is enforced: AKGL_PUBLIC_HEADERS drives both
install() and a generated translation unit per header, so a header that ships
is a header that is checked. Writing that found registry.h, which used
SDL_PropertiesID in eight declarations and included no SDL header.

23/23 suites pass, memcheck is clean, reindent --check is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:35 -04:00
80 changed files with 8889 additions and 2602 deletions

View File

@@ -42,7 +42,6 @@ jobs:
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
@@ -161,11 +160,12 @@ jobs:
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build --parallel
# The character suite is excluded here for the same reason as in the
# coverage job: it fails deliberately, and a failing suite would be
# reported as "valgrind found nothing but ctest exited non-zero".
# Every suite, character included. It was excluded from both this job
# and the coverage job on the grounds that it failed deliberately -- it
# did not. It was reporting success while running one of its four tests;
# see TODO.md, "Test suites that could not fail".
- name: Memory check
run: scripts/memcheck.sh -E '^character$'
run: scripts/memcheck.sh
- name: Upload valgrind logs
if: always()
uses: actions/upload-artifact@v4

140
AGENTS.md
View File

@@ -19,26 +19,28 @@ Working rules:
- **Never hand-edit it.** It is machine-written; changes belong in
`mkcontrollermappings.sh`.
- **Do not commit incidental regeneration churn.** The build regenerates the
header on every run, and the script stamps `$(date)` into a comment, so an
ordinary build leaves the file modified with nothing of substance changed.
Revert that before committing: `git checkout -- include/akgl/SDL_GameControllerDB.h`.
- **An ordinary build does not touch it.** Regeneration is
`cmake --build build --target controllerdb`, and nothing else runs the script.
Until 0.5.0 an `add_custom_command` re-ran it on every build, which made every
build need network access and left the file modified with a fresh `$(date)`
stamp and nothing of substance changed.
- **Update it as a deliberate, standalone commit** when you actually want newer
mappings, so the diff is reviewable and bisectable.
- **Never commit a copy with `AKGL_SDL_GAMECONTROLLER_DB_LEN 0`.** That is the
signature of a failed fetch, not an empty upstream — see the defect note
below. Check the constant before staging this file.
- **A failed fetch cannot reach this file.** The script assembles the header in
a temporary directory and moves it into place only after checking curl's exit
status, a plausible minimum mapping count, and that no mapping contains a
character that would break a C string literal. Any of those failing leaves the
tracked copy untouched and exits non-zero.
- Formatting tooling ignores it: `scripts/reindent.sh` and the pre-commit hook
both skip it.
> **Known defect (tracked in `TODO.md`).** The safety net is currently
> self-defeating. `mkcontrollermappings.sh` has no `set -e` and does not check
> `curl`'s exit status, so when the fetch fails it writes a header with
> `AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty array **over the good tracked
> copy, and exits 0**. Because CMake re-runs the generator on every build, an
> offline build silently destroys the very fallback the file exists to provide.
> Until that is fixed, treat a dirty `SDL_GameControllerDB.h` after a build as
> suspect and check the length constant before committing it.
> **Fixed in 0.5.0.** The safety net used to be self-defeating:
> `mkcontrollermappings.sh` had no `set -e` and never checked `curl`, so a
> failed fetch wrote `AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty array
> **over the good tracked copy, and exited 0** — and because CMake re-ran the
> generator on every build, an offline build silently destroyed the very
> fallback the file exists to provide. Both halves are closed: the script
> refuses rather than overwrites, and a build does not run it.
## Build, Test, and Development Commands
@@ -75,7 +77,7 @@ because the perf suites scale themselves down under valgrind. Suppressions for
third-party findings live in `scripts/valgrind.supp`; add one only when the
finding genuinely cannot be fixed from this repository.
Run mutation testing with `cmake --build build --target mutation`. For a quick smoke run, use `scripts/mutation_test.py --target src/tilemap.c --max-mutants 10`; the harness mutates only a scratch copy and excludes the intentionally failing character test.
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 runs every suite.
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.
@@ -91,8 +93,11 @@ because the others cannot do its work:
| `memory_check` | RelWithDebInfo | `scripts/memcheck.sh`, every suite under valgrind. Gates: a definite leak, an invalid access or a branch on uninitialised memory fails the build. Keeps the valgrind logs as an artifact. |
| `mutation_test` | Debug | One focused source file, so CI stays bounded. |
The `character` suite is excluded wherever a whole run is selected: it fails
deliberately, and pinning current-but-wrong behavior is not what it is for.
Every suite runs in every job. The `character` suite used to be excluded from
the coverage and memory-check jobs on the grounds that it failed deliberately.
It did not: it was reporting success while running one of its four tests, for
two independent reasons, and both are fixed. See TODO.md, "Test suites that
could not fail".
## Coding Style
@@ -187,12 +192,9 @@ For reference, `cc-mode` defines the style as:
- `case` labels sit at the same column as their `switch` (`label . 0`).
- Continuation lines of a wrapped expression indent one level (`statement-cont . +`).
Most of `src/` already conforms. The known non-conforming files are
`src/json_helpers.c`, `src/util.c` (from `akgl_rectangle_points` onward),
`src/assets.c`, `src/staticstring.c`, parts of `src/actor.c`, and the headers
`include/akgl/util.h` and `include/akgl/staticstring.h` — all of which use a
2-column offset. Convert a file to the canonical style in its own commit, not
mixed into a behavioral change.
The whole tree conforms: `scripts/reindent.sh --check` is clean, and the
pre-commit hook keeps it that way. Convert a file to the canonical style in its
own commit, not mixed into a behavioral change.
### Braces
@@ -256,8 +258,9 @@ style drifted in the first place.
embed a type name (`akgl_Actor_cmhf_left_on` is wrong; it should be
`akgl_actor_cmhf_left_on`).
- **Public types**: `akgl_TypeName` — `akgl_Actor`, `akgl_SpriteSheet`,
`akgl_PhysicsBackend`. Every type exported from a header takes the prefix;
bare names like `point` and `RectanglePoints` are defects, not precedent.
`akgl_PhysicsBackend`. Every type exported from a header takes the prefix.
`point` and `RectanglePoints` shipped bare until 0.5.0; they are
`akgl_Point` and `akgl_RectanglePoints` now, and neither is precedent.
- **Constants and macros**: `AKGL_UPPER_SNAKE_CASE`. A constant belongs to the
subsystem it describes: a character limit is `AKGL_CHARACTER_MAX_*`, not
`AKGL_SPRITE_MAX_CHARACTER_*`. Name a constant for what its value *is* — a
@@ -266,6 +269,15 @@ style drifted in the first place.
not add bare names (`renderer`, `camera`, `window`), leading-underscore names
(reserved at file scope), or SCREAMING_SNAKE names for mutable objects —
`AKGL_UPPER_SNAKE_CASE` is for constants.
This one is not cosmetic and there is a scar to prove it. Until 0.5.0 the
library exported `renderer`, and `tests/character.c` defined a
`SDL_Renderer *renderer` of its own. Both had external linkage and the same
spelling, so the executable's definition preempted the shared library's: the
library read a `SDL_Renderer *` through an `akgl_RenderBackend *`, every
texture load in that suite failed, and the suite reported success anyway. A
consuming game with a variable called `renderer` would have hit the same
thing with no test to notice.
- **`static` helpers drop the `akgl_` prefix**, which exists only to avoid
external collisions.
- **Include guards**: `_AKGL_<FILE>_H_`, matching the file name. Guard names
@@ -321,8 +333,74 @@ Headers must be self-contained: include what you use, so that a translation unit
including a single `akgl` header compiles without a particular include order.
Within the project use the angled form, `#include <akgl/sibling.h>`.
This is enforced. `AKGL_PUBLIC_HEADERS` in `CMakeLists.txt` lists every header
installed into `include/akgl/`, and drives two things from that one list: what
`install()` ships, and a generated translation unit per header — each including
exactly that header and nothing before it — linked into the `headers` suite. A
new header is covered by adding it to that list. One header per translation
unit is the only shape that proves anything: a second `#include` after the
first proves nothing about the second, because by then the first has dragged
its dependencies in.
Declare no-argument functions as `(void)`, not `()`.
## Fixing Defects
**Read what the code does before deciding what it should do.** Every rule below
is here because it was broken in this repository, and every one of them was
cheap to check and expensive to assume.
- **Check the surrounding code before designing around a hazard.** The savegame
name-width fix first grew four `AKGL_GAME_SAVE_*_NAME_WIDTH` aliases, so the
on-disk format's field widths could diverge from the object model's. They were
deleted: `akgl_game_load` compares the save's `libversion` against
`AKGL_VERSION` and refuses a mismatch **in the same function, sixteen lines
above the tables being edited**, so the divergence they defended against
cannot happen quietly. An
abstraction defending an impossible case is worse than none, because the next
reader has to work out what it is for.
- **Abstract when the second consumer is real, not before.** A `#define` with
one use, a wrapper with one caller, a parameter only ever passed one value:
delete it and inline the thing. Those four aliases had exactly one use per
side and each expanded to exactly one existing constant.
- **A test that has not failed has not been tested.** Break the fix, watch the
new test go red, put the fix back. Two tests in the 0.5.0 defect work passed
against the unfixed library on the first attempt:
- the savegame roundtrip asserted the load *succeeded*, and a reader stepping
the wrong field width does not fail — it finds a run of zeros mid-entry,
stops early, and reports success with silently wrong maps;
- the `akgl_get_json_with_default` test used `TEST_EXPECT_OK`, which releases
whatever context the statement returns — and that function hands *back* the
context it was given when it does not handle the status, so the `CLEANUP`
block released it a second time and the double release corrupted the
failure instead of reporting it.
Neither was discovered by reasoning. Both were discovered by reverting the fix
and being surprised.
- **Pick the test case that distinguishes the two behaviours, not the obvious
one.** `!AKGL_BITMASK_HAS(mask, bit)` misparses to `!(mask & bit) == bit`
without the parentheses — but for a bit that *is* set that is `0 == bit`,
false, which is the same answer the correct parse gives. It only diverges for
a bit that is **not** set whose value is **not** 1. The obvious test passes
either way.
- **Do not trust a comment, a TODO entry, or a CI exclusion that states a
premise.** Verify it. `tests/character.c` was excluded from two CI jobs and
from the mutation harness because it "fails deliberately". It did not: it was
exiting 0 while running one of its four tests. `TODO.md` carried eleven
entries describing code that had already changed. A premise nobody has
re-checked is where the next defect is hiding.
- **When a measurement moves, say what you measured.** Absolute benchmark
numbers drift with the machine. The `akgl_game_update` fix is recorded as the
*gap* between two rows of the same run — 92 µs before, noise after — because
a later run read every row 15% high, including rows nothing had touched. Do
not re-baseline a whole table to record one change.
## Rules
- Add yourself (agent program name, model name and version) as a co-author on every commit message
@@ -330,7 +408,15 @@ Declare no-argument functions as `(void)`, not `()`.
## 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.
Tests use simple executable return codes and are registered through CTest in `CMakeLists.txt`; there is no declared coverage threshold.
**Every suite's `main()` must call `TEST_TRAP_UNHANDLED_ERRORS()` immediately
after `akgl_error_init()`.** libakerror's default unhandled-error handler ends
in `exit(errctx->status)`, `exit` keeps only the low byte, and libakgl's status
band starts at 256 — so a suite that failed with `AKGL_ERR_SDL` exited 0 and
CTest recorded a pass. That is not hypothetical: `tests/character.c` aborted at
its second of four tests on a bad renderer and was green until 0.5.0. The trap
in `tests/testutil.h` collapses any status a byte cannot carry onto 1. Add focused tests as `tests/<feature>.c`, create a matching `test_<feature>` target, and register it with `add_test`. Put reusable fixtures in `tests/assets/` and keep paths compatible with tests launched from the build tree. Coverage mode wraps the suite in a CTest fixture so counters are reset before tests and reports are generated afterward.
### Performance suites

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10)
# The single source of truth for the library version. It drives the generated
# include/akgl/version.h, the shared library's VERSION and SOVERSION, and the
# Version field in akgl.pc. Bump it here and nowhere else.
project(akgl VERSION 0.4.0 LANGUAGES C)
project(akgl VERSION 0.5.0 LANGUAGES C)
# Memory checking reuses the suites that already exist -- `ctest -T memcheck`
# runs every registered test under valgrind -- rather than adding programs of its
@@ -154,6 +154,34 @@ if(NOT TARGET jansson::jansson)
find_package(jansson)
endif()
# Every header installed into include/akgl/, by base name. This list is the
# single source of truth for two things that must not drift apart: what gets
# installed, and what the `headers` suite proves is self-contained. Adding a
# header here is all a new one needs. SDL_GameControllerDB.h and the generated
# version.h are installed separately -- neither is an API header.
set(AKGL_PUBLIC_HEADERS
actor
assets
audio
character
controller
draw
error
game
heap
iterator
json_helpers
physics
registry
renderer
sprite
staticstring
text
tilemap
types
util
)
set(GAMECONTROLLERDB_H "include/akgl/SDL_GameControllerDB.h")
set(prefix ${CMAKE_INSTALL_PREFIX})
set(exec_prefix "\${prefix}")
@@ -168,10 +196,27 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/include/akgl/version.h.in
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/tests/assets"
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
add_custom_command(
OUTPUT ${GAMECONTROLLERDB_H}
# Regenerating the controller database is a deliberate act, not part of a build.
#
# This used to be an add_custom_command whose OUTPUT was the *relative* path
# "include/akgl/SDL_GameControllerDB.h", which CMake resolves against the binary
# directory while the script writes to the source directory. The declared output
# therefore never appeared, the command was permanently out of date, and every
# build re-ran it -- so every build needed network access and left the tracked
# header dirty with a fresh $(date) stamp.
#
# `cmake --build build --target controllerdb` when newer mappings are actually
# wanted. The result is its own commit, per AGENTS.md.
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKGL_CONTROLLERDB_TARGET controllerdb)
else()
set(AKGL_CONTROLLERDB_TARGET akgl_controllerdb)
endif()
add_custom_target(${AKGL_CONTROLLERDB_TARGET}
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/mkcontrollermappings.sh ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Generating controller mappings ..."
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
USES_TERMINAL
COMMENT "Fetching controller mappings and rewriting ${GAMECONTROLLERDB_H} ..."
)
# Add include directories
@@ -198,7 +243,6 @@ add_library(akgl SHARED
src/tilemap.c
src/util.c
src/version.c
${GAMECONTROLLERDB_H}
)
# While the major version is 0 the ABI is not stable across minor releases, so
@@ -274,6 +318,64 @@ endforeach()
add_test(NAME semver_unit COMMAND akgl_test_semver_unit)
# Not a C program, so it is not in AKGL_TEST_SUITES: it reads the built library's
# dynamic symbol table and every public header, and fails when a function with
# external linkage is declared nowhere. That is an ABI question rather than a
# behavioural one, and nineteen symbols had drifted out of the headers before
# anything was checking.
add_test(
NAME api_surface
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/scripts/check_api_surface.sh
$<TARGET_FILE:akgl>
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_BINARY_DIR}/include
)
set_tests_properties(api_surface PROPERTIES SKIP_RETURN_CODE 2)
# The two akerror control-flow rules whose failure mode is silent: a *_RETURN
# inside an ATTEMPT block, and a return out of a HANDLE block. Neither produces
# a compiler diagnostic and neither fails a test run until the pool it drains is
# empty, which is why both have already shipped.
#
# The mutation target below wants the same interpreter; find_package caches, so
# asking here as well costs nothing and keeps this block self-contained.
find_package(Python3 COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(
NAME error_protocol
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/scripts/check_error_protocol.py
${CMAKE_CURRENT_SOURCE_DIR}/src
)
endif()
# One translation unit per public header, each including exactly that header and
# nothing before it. This is the only shape that proves a header is
# self-contained: a second #include in the same file proves nothing about the
# second, because by then the first has dragged its dependencies in. Generated
# rather than written by hand so the check cannot fall behind
# AKGL_PUBLIC_HEADERS -- the failure being guarded against is a header nobody
# remembered to cover.
set(AKGL_HEADER_SELFTEST_SOURCES "")
foreach(header IN LISTS AKGL_PUBLIC_HEADERS)
set(generated "${CMAKE_CURRENT_BINARY_DIR}/tests/header_${header}.c")
file(WRITE "${generated}"
"/* Generated by CMakeLists.txt from AKGL_PUBLIC_HEADERS. Do not edit. */\n\
#include <akgl/${header}.h>\n\
\n\
/* ISO C forbids an empty translation unit; the compile is the assertion. */\n\
int akgl_header_selftest_${header}(void);\n\
int akgl_header_selftest_${header}(void)\n\
{\n\
return 0;\n\
}\n")
list(APPEND AKGL_HEADER_SELFTEST_SOURCES "${generated}")
endforeach()
target_sources(akgl_test_headers PRIVATE ${AKGL_HEADER_SELFTEST_SOURCES})
# Hand-written rather than generated: it checks a header's behaviour under a
# caller-supplied override, which is not something the generated shape covers.
target_sources(akgl_test_headers PRIVATE tests/header_pool_override.c)
# TIMEOUT is generous because CTest applies the same property to `ctest -T
# memcheck`, where every suite runs under valgrind at something like twenty
# times its normal cost. The unit suites finish in well under a second each
@@ -307,12 +409,29 @@ if(AKGL_COVERAGE)
set(AKGL_COVERAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/coverage")
file(MAKE_DIRECTORY "${AKGL_COVERAGE_DIR}")
# Both gcovr invocations take the build tree as an explicit positional search
# path, and neither passes --object-directory.
#
# gcovr looks for .gcda/.gcno under its search paths, and with none given it
# searches --root -- the source directory, which is where developers keep
# their build trees. --object-directory does *not* narrow that: per gcovr's
# own help it only identifies "the path between gcda files and the directory
# where the compiler was originally run". So every instrumented tree left
# inside the source directory was folded into the report alongside the one
# being measured, and with two trees describing different line numbers for the
# same function, coverage_reset failed before any test ran:
#
# AssertionError: Got function akgl_game_lowfps on multiple lines: 45, 46
#
# The `build*/` entry in .gitignore hides those trees from git status, which
# makes the state easy to get into and hard to notice.
add_test(
NAME coverage_reset
COMMAND ${GCOVR_EXECUTABLE}
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
--delete
"${CMAKE_CURRENT_BINARY_DIR}"
)
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP akgl_coverage)
# Any suite missing from this list runs outside the fixture and has its
@@ -328,11 +447,11 @@ if(AKGL_COVERAGE)
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"
"${CMAKE_CURRENT_BINARY_DIR}"
)
set_tests_properties(
coverage_report
@@ -455,24 +574,7 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h DESTINATION "include/akgl/")
install(TARGETS akgl DESTINATION "lib/")
install(FILES "deps/semver/semver.h" DESTINATION "include/")
install(FILES "include/akgl/actor.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/types.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/text.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/assets.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/audio.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/character.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/error.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/draw.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/game.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/controller.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/heap.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/iterator.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/json_helpers.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/renderer.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/physics.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/registry.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/sprite.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/staticstring.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/tilemap.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/util.h" DESTINATION "include/akgl/")
foreach(header IN LISTS AKGL_PUBLIC_HEADERS)
install(FILES "include/akgl/${header}.h" DESTINATION "include/akgl/")
endforeach()
install(FILES ${GAMECONTROLLERDB_H} DESTINATION "include/akgl/")

View File

@@ -84,11 +84,11 @@ start whether the game uses one slot or all of them.
| Pool | Slots | Bytes each | Total |
|---|---:|---:|---:|
| `HEAP_ACTOR` | 64 | 400 | 25,600 |
| `akgl_heap_actors` | 64 | 400 | 25,600 |
| `HEAP_SPRITE` | 1024 | 176 | 180,224 |
| `HEAP_SPRITESHEET` | 1024 | 536 | 548,864 |
| `HEAP_CHARACTER` | 256 | 184 | 47,104 |
| `HEAP_STRING` | 256 | 4,100 | 1,049,600 |
| `akgl_heap_strings` | 256 | 4,100 | 1,049,600 |
| **pools, total** | | | **1,851,392** |
| `akgl_Tilemap` (one, as `_akgl_gamemap`) | 1 | 26,388,008 | 26,388,008 |
| — of which layers | 16 | 1,120,296 | 17,924,736 |
@@ -167,6 +167,14 @@ rows are raw `SDL_RenderTexture` loops with no libakgl in the path.
| `draw_world`, 1200 tiles + 64 actors | frame | 16,484,493.6 | 61 |
| `akgl_game_update`, full frame | frame | 16,576,902.2 | 60 |
The `akgl_game_update` row is from before the sixteen-times-per-actor defect was
fixed in 0.5.0; see below. It sat about 92 µs above `draw_world` in the same
run, and now sits level with it. The absolute figures in this table have not
been re-taken -- a later run on this machine read every row about 15% high,
including rows nothing has touched, so the table is a record of one machine
state and the honest measurement of that fix is the *gap* between two rows of
the same run, not a new absolute.
## The frame budget
At 60 fps a frame is 16.67 ms. Here is where it goes for a 640x480 game with a
@@ -247,16 +255,30 @@ The design conclusion is not "make errors cheaper". It is that a *routine*
condition should not be reported as an error. A character that has no sprite for
a state should answer that question with a boolean.
### `akgl_game_update` updates every actor sixteen times
### `akgl_game_update` updated every actor sixteen times — fixed in 0.5.0
`src/game.c:617` loops over `AKGL_TILEMAP_MAX_LAYERS`, and the actor sweep
nested inside it does not filter by layer. Every live actor's `updatefunc` runs
**16 times per frame**. At 68.4 ns per update and 64 actors that is 70 µs of
work to do 4.4 µs of work.
`akgl_game_update` looped over `AKGL_TILEMAP_MAX_LAYERS` with the actor sweep
nested inside it, and never compared an actor's `layer` to the layer it was on.
Every live actor's `updatefunc` ran **16 times per frame**: at 68.4 ns per
update and 64 actors, 70 µs of work to do 4.4 µs of it — and, more to the
point, every piece of a game's own per-frame actor logic running sixteen times.
It is invisible in the frame total here because the tilemap blits are three
orders of magnitude larger. On a GPU backend, where the frame might be 2 ms, it
is 3.5% of the frame doing nothing. Filed in `TODO.md`.
The sweep is hoisted out of the layer loop. Updating an actor is not a per-layer
operation; drawing is, and `akgl_render_2d_draw_world` walks the layers itself.
`AKGL_ITERATOR_OP_LAYERMASK` is honoured now rather than ignored, so a caller
that genuinely wants one layer can still ask for it and gets each of those
actors once.
**How it was measured.** `tests/game.c` counts calls into a stub `updatefunc`
and asserts exactly one per live actor per `akgl_game_update`; against the old
code it reports 16. The timing side is the gap between the `akgl_game_update`
and `draw_world` rows of the *same* benchmark run: 92 µs before, and across
three runs afterwards -414 µs, -376 µs and +19 µs — noise in both directions.
It was invisible in the frame total because the tilemap blits are three orders
of magnitude larger, which is exactly why it needed a counting test rather than
a stopwatch. On a GPU backend, where a frame might be 2 ms, it was 3.5% of the
frame doing nothing.
### Text has no cache at all

View File

@@ -24,8 +24,8 @@ 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));
PASS(e, akgl_render_2d_init(akgl_renderer));
PASS(e, akgl_physics_init_arcade(akgl_physics));
```
Unlock the game state
@@ -55,10 +55,10 @@ In your game loop (or in your `SDL_AppIterate` method), lock the game state, cal
```c
PASS(e, akgl_game_state_lock());
PASS(e, renderer->frame_start(renderer));
SDL_RenderClear(renderer->sdl_renderer);
PASS(e, akgl_renderer->frame_start(akgl_renderer));
SDL_RenderClear(akgl_renderer->sdl_renderer);
PASS(e, akgl_game_update(NULL));
PASS(e, renderer->frame_end(renderer));
PASS(e, akgl_renderer->frame_end(akgl_renderer));
PASS(e, akgl_game_state_unlock());
```
@@ -190,7 +190,7 @@ Character files:
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));
PASS(e, akgl_tilemap_load(PATHSTRING, akgl_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.
@@ -225,8 +225,8 @@ Actors will be automatically populated from objects in the tilemap object layers
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;
if ( akgl_gamemap->use_own_physics == true ) {
akgl_physics = &akgl_gamemap->physics;
}
```
@@ -251,13 +251,13 @@ Tilemap physics specification follows. A map can specify its own physics propert
}],
```
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.)
The global `akgl_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 simplest way is to check the global `akgl_camera` object's `akgl_camera->w` and `akgl_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.

1295
TODO.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
#ifndef _SDL_GAMECONTROLLERDB_H_
#define _SDL_GAMECONTROLLERDB_H_
#ifndef _AKGL_SDL_GAMECONTROLLERDB_H_
#define _AKGL_SDL_GAMECONTROLLERDB_H_
// Taken from https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt on Fri Jul 31 01:11:07 PM EDT 2026
// Taken from https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt on Sat Aug 1 07:34:43 AM EDT 2026
#define AKGL_SDL_GAMECONTROLLER_DB_LEN 2253
#define AKGL_SDL_GAMECONTROLLER_DB_LEN 2255
const char *SDL_GAMECONTROLLER_DB[] = {
"03000000300f00000a01000000000000,3 In 1 Conversion Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b8,x:b3,y:b0,platform:Windows",
@@ -1994,6 +1994,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"66306331643531333230306437353936,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b15,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android",
"050000002028000009000000ffff3f00,8BitDo SNES30,a:b1,b:b0,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android",
"050000003512000020ab000000780f00,8BitDo SNES30,a:b21,b:b20,back:b30,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b26,rightshoulder:b27,start:b31,x:b24,y:b23,platform:Android",
"05000000c82d00001b30000000000000,8BitDo Ultimate 2C Wireless,a:b10,b:b1,x:b3,y:b6,back:b16,guide:b19,start:b15,leftstick:b17,rightstick:b18,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b8,righttrigger:b9,platform:Android",
"33666663316164653937326237613331,8BitDo Zero,a:b0,b:b1,back:b15,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b2,y:b3,platform:Android",
"38426974646f205a65726f2047616d65,8BitDo Zero,a:b0,b:b1,back:b15,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b2,y:b3,platform:Android",
"05000000c82d000018900000ffff0f00,8BitDo Zero 2,a:b1,b:b0,back:b4,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b3,y:b2,platform:Android",
@@ -2015,6 +2016,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"30363230653635633863366338623265,Evo VR,a:b0,b:b1,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftx:a0,lefty:a1,x:b2,y:b3,platform:Android",
"05000000b404000011240000dfff3f00,Flydigi Vader 2,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,paddle1:b17,paddle2:b18,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android",
"05000000bc20000000550000ffff3f00,GameSir G3w,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android",
"05000000373500001611000000000000,GameSir X5 Lite,a:b0,b:b1,x:b3,y:b6,leftshoulder:b4,rightshoulder:b5,lefttrigger:b8,righttrigger:b9,back:b16,start:b15,guide:b19,leftstick:b17,rightstick:b18,leftx:a0,lefty:a1,rightx:a2,righty:a3,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,platform:Android",
"34323662653333636330306631326233,Google Nexus,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android",
"35383633353935396534393230616564,Google Stadia Controller,a:b0,b:b1,back:b15,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android",
"476f6f676c65204c4c43205374616469,Google Stadia Controller,a:b0,b:b1,back:b15,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android",
@@ -2260,4 +2262,4 @@ const char *SDL_GAMECONTROLLER_DB[] = {
"050000005e040000130b0000df870001,Xbox Series X Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b10,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:iOS",
"050000005e040000130b0000ff870001,Xbox Series X Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,platform:iOS"
};
#endif // _SDL_GAMECONTROLLERDB_H_
#endif // _AKGL_SDL_GAMECONTROLLERDB_H_

View File

@@ -33,53 +33,54 @@
#define _AKGL_ACTOR_H_
#include <stdint.h>
#include "types.h"
#include "character.h"
#include <akgl/types.h>
#include <akgl/character.h>
// ---- LOW WORD STATUSES ----
//
// The trailing column is the bit pattern within its own 16-bit half -- the
// low half here, the high half below -- not the full 32-bit value. Read as a
// whole word it looks like bit 16 restarts at bit 0, which it does not.
#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
#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_STATE_UNDEFINED_16 (1 << 16) // 65536 0000 0000 0000 0001
#define AKGL_ACTOR_STATE_UNDEFINED_17 (1 << 17) // 131072 0000 0000 0000 0010
#define AKGL_ACTOR_STATE_UNDEFINED_18 (1 << 18) // 262144 0000 0000 0000 0100
#define AKGL_ACTOR_STATE_UNDEFINED_19 (1 << 19) // 524288 0000 0000 0000 1000
#define AKGL_ACTOR_STATE_UNDEFINED_20 (1 << 20) // 1048576 0000 0000 0001 0000
#define AKGL_ACTOR_STATE_UNDEFINED_21 (1 << 21) // 2097152 0000 0000 0010 0000
#define AKGL_ACTOR_STATE_UNDEFINED_22 (1 << 22) // 4194304 0000 0000 0100 0000
#define AKGL_ACTOR_STATE_UNDEFINED_23 (1 << 23) // 8388608 0000 0000 1000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_24 (1 << 24) // 16777216 0000 0001 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_25 (1 << 25) // 33554432 0000 0010 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_26 (1 << 26) // 67108864 0000 0100 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_27 (1 << 27) // 134217728 0000 1000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_28 (1 << 28) // 268435456 0001 0000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_29 (1 << 29) // 536870912 0010 0000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_30 (1 << 30) // 1073741824 0100 0000 0000 0000
#define AKGL_ACTOR_STATE_UNDEFINED_31 (1 << 31) // 2147483648 1000 0000 0000 0000
/** @brief Bits in an actor's state word. Fixed by the width of `int32_t state`. */
#define AKGL_ACTOR_MAX_STATES 32
// This is an array of strings equal to actor states from 1-32.
// This is built by a utility script and not kept in git, see
// the Makefile for lib_src/actor_state_string_names.c
/**
* @brief Bit position -> state name, as text. Index `i` names the bit `1 << i`.
*
@@ -87,17 +88,14 @@
* #AKGL_REGISTRY_ACTOR_STATE_STRINGS out of this, which is what lets character
* JSON write `"AKGL_ACTOR_STATE_FACE_LEFT"` instead of `2`. A bit whose name
* here does not match its `#define` above cannot be referred to from JSON at
* all.
* all -- which was the case for `MOVING_IN` and `MOVING_OUT` until 0.5.0.
*
* @warning Three known defects, all tracked in TODO.md items 24-26: the
* definition in `src/actor_state_string_names.c` is 32 entries while
* this declares 33, so reading index 32 reads past the object; bits 11
* and 12 are named `UNDEFINED_11`/`UNDEFINED_12` rather than
* `MOVING_IN`/`MOVING_OUT`; and the comment above about a generator
* script is stale -- there is no such script, and the file is tracked
* in git and maintained by hand.
* Maintained by hand in `src/actor_state_string_names.c`, and exactly
* #AKGL_ACTOR_MAX_STATES entries long: it was declared one longer than it was
* defined until 0.5.0, so a consumer trusting the declared bound read past the
* object. `tests/registry.c` checks every entry against its bit.
*/
extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1];
extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES];
/** @brief Every facing bit. Clear this before setting one, so an actor faces exactly one way. */
#define AKGL_ACTOR_STATE_FACE_ALL (AKGL_ACTOR_STATE_FACE_DOWN | AKGL_ACTOR_STATE_FACE_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_FACE_UP)
@@ -109,8 +107,6 @@ extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1];
/** @brief Children one actor can carry. A child moves with its parent rather than simulating. */
#define AKGL_ACTOR_MAX_CHILDREN 8
/** @brief Actors in the pool. Override before including heap.h to change it; see heap.h. */
#define AKGL_MAX_HEAP_ACTOR 64
/** @brief Represents a live actor, including state, motion, hierarchy, and behavior callbacks. */
typedef struct akgl_Actor {
@@ -225,7 +221,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_set_character(akgl_Actor *obj, cha
* @return `NULL` on success -- including every skipped case above -- otherwise
* an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
* @throws AKERR_* Whatever akgl_sprite_sheet_coords_for_frame or the renderer's
* @throws AKERR_* Whatever akgl_spritesheet_coords_for_frame or the renderer's
* `draw_texture` raises.
*
* @note The destination height is computed from the sprite's *width*, so a
@@ -368,7 +364,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_add_child(akgl_Actor *obj, akgl_Ac
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event);
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Stop the actor moving left, zeroing everything on the x axis.
* @param obj The actor to stop. Required. `basechar` is not needed here.
@@ -378,7 +374,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_
* @note It clears the x axis whichever way the actor was going, so releasing
* left while holding right also stops the rightward movement.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event);
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Start the actor moving right, and turn it to face right.
* @param obj The actor to move. Required, along with its `basechar`.
@@ -386,7 +382,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event);
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Stop the actor moving right, zeroing everything on the x axis.
* @param obj The actor to stop. Required.
@@ -394,7 +390,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event);
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Start the actor moving up the screen, and turn it to face up.
* @param obj The actor to move. Required, along with its `basechar`, whose y
@@ -403,7 +399,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SD
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event);
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Stop the actor moving up, zeroing everything on the y axis.
* @param obj The actor to stop. Required.
@@ -413,7 +409,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Ev
* @note This also zeroes `ey`, so an actor under gravity has its accumulated
* fall cancelled by releasing a movement key.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event);
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Start the actor moving down the screen, and turn it to face down.
* @param obj The actor to move. Required, along with its `basechar`.
@@ -421,16 +417,16 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_E
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event);
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Stop the actor moving down, zeroing everything on the y axis.
* @param obj The actor to stop. Required.
* @param event The event that triggered this. Required, but not read.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
* @note Zeroes `ey` as well; see akgl_Actor_cmhf_up_off.
* @note Zeroes `ey` as well; see akgl_actor_cmhf_up_off.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event);
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event);
/**
* @brief `SDL_EnumerateProperties` callback that applies an akgl_Iterator to one registered actor.

View File

@@ -3,8 +3,8 @@
* @brief Loads the game's startup assets into the global mixer and track table.
*/
#ifndef _ASSETS_H_
#define _ASSETS_H_
#ifndef _AKGL_ASSETS_H_
#define _AKGL_ASSETS_H_
#include <akerror.h>
@@ -41,4 +41,4 @@
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_load_start_bgm(char *fname);
#endif //_ASSETS_H_
#endif //_AKGL_ASSETS_H_

View File

@@ -16,18 +16,17 @@
#define _AKGL_CHARACTER_H_
#include <SDL3/SDL_properties.h>
#include "types.h"
#include "sprite.h"
#include <akgl/types.h>
#include <akgl/sprite.h>
#define AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH 128
#define AKGL_MAX_HEAP_CHARACTER 256
#define AKGL_CHARACTER_MAX_NAME_LENGTH 128
/** @brief Defines reusable movement parameters and actor-state sprite bindings. */
typedef struct akgl_Character {
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. */
char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH]; /**< Registry key. Truncated, not rejected, if the source name is longer. */
char name[AKGL_CHARACTER_MAX_NAME_LENGTH]; /**< Registry key. Truncated, not rejected, if the source name is longer. */
SDL_PropertiesID state_sprites; /**< State bitmask (decimal, as a string) -> akgl_Sprite *. */
uint64_t speedtime; /**< Nanoseconds one sprite frame is held before advancing. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONESEC_MS, which despite its name is nanoseconds-per-millisecond. TODO.md item 6. */
uint64_t speedtime; /**< Nanoseconds one sprite frame is held before advancing. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONEMS_NS. */
float32_t ax; /**< Acceleration along x, world units per second squared. Copied into an actor by akgl_actor_set_character. */
float32_t ay; /**< Acceleration along y. */
float32_t az; /**< Acceleration along z. Not read from JSON; stays 0 unless set by hand. */
@@ -52,7 +51,7 @@ typedef struct akgl_Character {
* akgl_heap_next_character. Required. Any previous contents are
* discarded without releasing the sprites they referenced.
* @param name Registry key, NUL-terminated. Required. Truncated at
* #AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH. A name already in the
* #AKGL_CHARACTER_MAX_NAME_LENGTH. A name already in the
* registry is silently replaced, and the character it displaced
* becomes unreachable rather than being released.
* @return `NULL` on success, otherwise an error context owned by the caller.
@@ -62,7 +61,7 @@ typedef struct akgl_Character {
* #AKGL_REGISTRY_CHARACTER -- in practice, because akgl_registry_init
* has not run and the registry id is still 0.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *basechar, char *name);
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *obj, char *name);
/**
* @brief Bind a sprite to one exact combination of actor-state bits.
*

View File

@@ -24,12 +24,12 @@
* akgl_controller_poll_keystroke().
*/
#ifndef _CONTROLLER_H_
#define _CONTROLLER_H_
#ifndef _AKGL_CONTROLLER_H_
#define _AKGL_CONTROLLER_H_
#include <SDL3/SDL.h>
#include <akerror.h>
#include "types.h"
#include <akgl/types.h>
// The binding handlers below take an akgl_Actor *, so this header cannot be
// included before akgl/actor.h without one. akgl_Actor is a typedef of a named
// struct, and repeating a typedef is not C99, so it is included rather than
@@ -94,7 +94,7 @@ typedef struct {
} akgl_ControlMap;
/** @brief Every control map. Zeroed by akgl_game_init; index it by the same id the functions below take. */
extern akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
extern akgl_ControlMap akgl_controlmaps[AKGL_MAX_CONTROL_MAPS];
/**
* @brief Log every attached keyboard and its SDL id.
@@ -138,49 +138,77 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void);
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_event(void *appstate, SDL_Event *event);
/**
* @brief Declared but not defined under this name. Do not call.
* @brief Set the movement and facing bits on the "player" actor for a D-pad or arrow press.
*
* The implementation exists as `gamepad_handle_button_down` in `src/controller.c`
* and is `static`-in-spirit -- it is not declared anywhere -- so a caller that
* uses this declaration compiles and then fails to link. TODO.md, "Known and
* still open" item 10.
* A whole-application shortcut rather than a control-map binding: it looks the
* actor up by the literal name `"player"` in #AKGL_REGISTRY_ACTOR instead of
* being told which actor to act on, so it only ever drives one. The
* akgl_actor_cmhf_* handlers are the general form.
*
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @return Nothing; it cannot be called.
* Facing follows movement unless the actor sets `movement_controls_face`, which
* is how an actor that aims independently of the direction it walks opts out.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only;
* never read.
* @param event The button or key event. Required. Both the gamepad and
* keyboard unions are read on every call, so the arm that does
* not correspond to the event type is read as garbage -- which
* works only because no real button and keycode pair collides.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_down(void *appstate, SDL_Event *event);
/**
* @brief Declared but not defined under this name. Do not call.
* @brief Clear the movement bit on the "player" actor for a D-pad or arrow release, and reset its animation.
*
* See akgl_controller_handle_button_down. The implementation is
* `gamepad_handle_button_up`.
* The counterpart to akgl_controller_handle_button_down. It clears only the movement
* bit, leaving the facing bits alone so the actor keeps looking the way it was
* walking, and rewinds `curSpriteFrameId` to 0 so the next step starts from the
* first frame of the walk cycle rather than wherever it stopped.
*
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @return Nothing; it cannot be called.
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The button or key release event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_up(void *appstate, SDL_Event *event);
/**
* @brief Declared but not defined under this name. Do not call.
* @brief Open a gamepad that has just been plugged in, and log its mapping.
*
* See akgl_controller_handle_button_down. The implementation is
* `gamepad_handle_added`, which opens a newly plugged-in gamepad.
* SDL delivers no button events from an unopened gamepad, so a device that
* appears mid-session has to be opened here the way akgl_controller_open_gamepads
* opens the ones present at startup. A gamepad SDL has already opened is logged
* and left alone.
*
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @return Nothing; it cannot be called.
* The mapping is logged because a controller with no entry in the database
* produces no button events at all, and that is otherwise indistinguishable
* from a broken binding.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_ADDED event. Required. The joystick id
* is read out of the `gbutton` arm rather than `gdevice`, which
* works because the two share their `which` field.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*
* @note A failed `SDL_OpenGamepad` is logged rather than reported: this returns
* success and the device stays silent.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_added(void *appstate, SDL_Event *event);
/**
* @brief Declared but not defined under this name. Do not call.
* @brief Close a gamepad that has been unplugged.
*
* See akgl_controller_handle_button_down. The implementation is
* `gamepad_handle_removed`, which closes an unplugged gamepad.
* An unplugged device SDL still has open is a leaked handle, so this closes it.
* A removal for a device that was never opened is logged and otherwise ignored.
* Any control map still holding that `jsid` simply stops matching -- the map is
* not torn down.
*
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @return Nothing; it cannot be called.
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_REMOVED event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate, SDL_Event *event);
@@ -190,7 +218,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate
* The binding is copied, so the caller's `akgl_Control` can be a stack local
* reused across pushes -- which is exactly what akgl_controller_default does.
* Bindings can only be appended; there is no remove, and no way to reset a map
* short of zeroing it in ::GAME_ControlMaps directly.
* short of zeroing it in ::akgl_controlmaps directly.
*
* @param controlmapid Which map to append to, 0 through
* #AKGL_MAX_CONTROL_MAPS - 1.
@@ -203,7 +231,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate
*
* @warning A **negative** @p controlmapid is not rejected -- only the upper
* bound is checked -- and indexes before the start of
* ::GAME_ControlMaps. TODO.md, "Known and still open" item 11.
* ::akgl_controlmaps. TODO.md, "Known and still open" item 11.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control);
@@ -212,7 +240,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akg
*
* Points the map at the named actor and pushes eight bindings: the four arrow
* keys and the four D-pad directions, each wired to the matching
* `akgl_Actor_cmhf_*_on`/`_off` pair. It is the "just give me something that
* `akgl_actor_cmhf_*_on`/`_off` pair. It is the "just give me something that
* works" path -- a game wanting different keys builds its own bindings with
* akgl_controller_pushmap.
*
@@ -334,4 +362,4 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke
* @return `NULL`. There is no failure path -- it resets two counters.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void);
#endif // _CONTROLLER_H_
#endif // _AKGL_CONTROLLER_H_

View File

@@ -16,8 +16,8 @@
* what the next SDL_RenderClear() paints.
*/
#ifndef _DRAW_H_
#define _DRAW_H_
#ifndef _AKGL_DRAW_H_
#define _AKGL_DRAW_H_
#include <SDL3/SDL.h>
@@ -39,18 +39,27 @@
/**
* @brief Paint an 8x8 grey checkerboard over a region, the way an image editor shows transparency.
*
* A diagnostic backdrop, not a general primitive -- `charviewer` uses it so a
* sprite's transparent pixels are visible rather than blending into black. It
* is the one function in this file that does not follow the file's conventions:
* it draws through the *global* `renderer` rather than a backend the caller
* passes in, it leaves the renderer's draw colour changed, and it reports
* nothing.
* A diagnostic backdrop rather than a general primitive -- `charviewer` uses it
* so a sprite's transparent pixels are visible instead of blending into black.
*
* @param w Width of the region to cover, in pixels, starting at x = 0.
* @param h Height of the region, starting at y = 0. Both round up to whole 8px
* cells, so a 12-pixel height paints 16.
* Until 0.5.0 this was the one function in the library outside the error
* protocol: it returned `void`, drew through the *global* `akgl_renderer`
* without checking it, and left the renderer's draw colour changed. It now
* takes a backend like every other entry point here and restores the colour it
* found, which is also what makes it testable without a world.
*
* @param self The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param w Width of the region to cover, in pixels, starting at x = 0. Zero
* or negative paints nothing and is not reported.
* @param h Height of the region, starting at y = 0. Both round up to whole
* 8px cells, so a 12-pixel height paints 16.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or a cell
* cannot be filled.
*/
void akgl_draw_background(int w, int h);
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_background(akgl_RenderBackend *self, int w, int h);
/**
* @brief Plot a single pixel.
@@ -233,4 +242,4 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *sel
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y);
#endif //_DRAW_H_
#endif //_AKGL_DRAW_H_

View File

@@ -3,8 +3,8 @@
* @brief Declares the public error API.
*/
#ifndef _ERROR_H_
#define _ERROR_H_
#ifndef _AKGL_ERROR_H_
#define _AKGL_ERROR_H_
#include <akerror.h>
@@ -75,4 +75,4 @@
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_error_init(void);
#endif // _ERROR_H_
#endif // _AKGL_ERROR_H_

View File

@@ -6,24 +6,24 @@
* registries and the audio and font engines; `akgl_game_update` is one frame --
* update every actor, step the physics, draw the world.
*
* There is exactly one of everything. `renderer`, `physics`, `camera`, and
* `gamemap` are globals pointing at the `_akgl_*` storage below them, so a
* There is exactly one of everything. `akgl_renderer`, `akgl_physics`, `akgl_camera`, and
* `akgl_gamemap` are globals pointing at the `akgl_default_*` storage below them, so a
* program can swap in its own instance by reassigning the pointer without the
* rest of the library knowing. That is the whole extent of the indirection:
* there is no notion of two worlds at once.
*
* The startup order that actually works:
*
* 1. fill in `game.name`, `game.version`, and `game.uri` -- akgl_game_init
* 1. fill in `akgl_game.name`, `akgl_game.version`, and `akgl_game.uri` -- akgl_game_init
* refuses to run without them;
* 2. akgl_game_init();
* 3. akgl_registry_load_properties() or akgl_set_property(), to configure
* screen size, physics constants and so on;
* 4. akgl_render_init2d(renderer) and akgl_physics_factory(physics, ...), both
* 4. akgl_render_2d_init(renderer) and akgl_physics_factory(physics, ...), both
* of which read that configuration;
* 5. load assets, then loop on akgl_game_update().
*
* @warning None of this is thread-safe beyond the `game.state` mutex, and that
* @warning None of this is thread-safe beyond the `akgl_game.state` mutex, and that
* mutex protects the state flags, not the pools or the registries.
*/
@@ -32,10 +32,10 @@
#include <stdint.h>
#include <SDL3_mixer/SDL_mixer.h>
#include "types.h"
#include "tilemap.h"
#include "renderer.h"
#include "physics.h"
#include <akgl/types.h>
#include <akgl/tilemap.h>
#include <akgl/renderer.h>
#include <akgl/physics.h>
// AKGL_VERSION used to be defined here by hand, which is how akgl.pc came to
// ship an empty Version field: nothing tied the two together.
#include <akgl/version.h>
@@ -48,25 +48,32 @@
/** @brief Nanoseconds in one second. The unit `SDL_GetTicksNS` reports in. */
#define AKGL_TIME_ONESEC_NS 1000000000
/**
* @brief Misnamed: this is nanoseconds per **millisecond**, not milliseconds per second.
* @brief Nanoseconds in one millisecond. The scale factor from JSON durations to internal ones.
*
* One second in milliseconds is 1000. 1000000 is one millisecond in
* nanoseconds, which is how sprite and character load actually use it -- as a
* milliseconds-to-nanoseconds scale factor -- and is not how
* akgl_game_state_lock uses it. TODO.md item 6 proposes renaming it
* `AKGL_TIME_ONEMS_NS`.
* Sprite and character definitions state frame durations in milliseconds
* because that is what a human writing one wants to type; everything that
* compares them against `SDL_GetTicksNS` needs nanoseconds.
*
* This was called `AKGL_TIME_ONESEC_MS` until 0.5.0, which said "one second in
* milliseconds" and held 1000000 -- a name and a value that described two
* different quantities. Both callers that meant the scale factor were right;
* the one that read it as a one-second budget was wrong by a factor of a
* thousand. See akgl_game_state_lock.
*/
#define AKGL_TIME_ONESEC_MS 1000000
#define AKGL_TIME_ONEMS_NS 1000000
/**
* @brief How long akgl_game_state_lock keeps trying for the state mutex, in milliseconds.
*
* A deliberate ceiling rather than a blocking wait: a deadlock here reports an
* error the caller can act on instead of hanging the process.
*/
#define AKGL_GAME_STATE_LOCK_BUDGET_MS 1000
/** @brief How long akgl_game_state_lock sleeps between attempts, in milliseconds. */
#define AKGL_GAME_STATE_LOCK_RETRY_MS 100
/* ==================== GAME STATE VARIABLES =================== */
/** @brief Describes a renderable frame. Declared but not used by anything in the library. */
typedef struct {
float32_t w; /**< Width in pixels. */
float32_t h; /**< Height in pixels. */
SDL_Texture *texture; /**< The frame's texture. */
} akgl_Frame;
/** @brief Stores application-defined game-state flags. */
typedef struct {
int32_t flags; /**< Meaning is entirely the application's; the library never reads it. Guard changes with akgl_game_state_lock. */
@@ -82,55 +89,65 @@ typedef struct {
SDL_Mutex *statelock; /**< Guards `state`. Created by akgl_game_init. */
int16_t fps; /**< Frames drawn during the last completed second. Recomputed once per second, not per frame. */
SDL_Time gameStartTime; /**< `SDL_GetTicksNS()` at akgl_game_init. */
SDL_Time lastIterTime; /**< Timestamp of the most recent akgl_game_updateFPS call. */
SDL_Time lastIterTime; /**< Timestamp of the most recent akgl_game_update_fps call. */
SDL_Time lastFPSTime; /**< When `fps` was last recomputed. */
int16_t framesSinceUpdate; /**< Frames counted so far in the current second. */
void (*lowfpsfunc)(void); /**< Called every frame while `fps` is under 30. Defaults to akgl_game_lowfps; replace it to do something more useful than log. */
void (*lowfpsfunc)(void); /**< Called every frame while `fps` is under 30. akgl_game_init installs akgl_game_lowfps, and akgl_game_update_fps installs it too if it finds this NULL -- an embedder that binds its own renderer rather than calling akgl_game_init used to crash here on frame one. Replace it to do something more useful than log. */
} akgl_Game;
/** @brief The SDL window, created by akgl_render_init2d. `NULL` until then. */
extern SDL_Window *window;
/** @brief The SDL window, created by akgl_render_2d_init. `NULL` until then. */
extern SDL_Window *akgl_window;
/** @brief The background music, loaded by akgl_load_start_bgm. `NULL` until then. */
extern MIX_Audio *bgm;
extern MIX_Audio *akgl_bgm;
/** @brief The mixer device, created by akgl_game_init. Everything audio goes through it. */
extern MIX_Mixer *akgl_mixer;
/** @brief Playback tracks by slot. #AKGL_GAME_AUDIO_TRACK_BGM is the music track; the rest are the application's to assign. */
extern MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
/** @brief Storage behind the default `camera`. Point `camera` elsewhere rather than reaching for this. */
extern SDL_FRect _akgl_camera;
/** @brief Storage behind the default `akgl_camera`. Point `akgl_camera` elsewhere rather than reaching for this. */
extern SDL_FRect akgl_default_camera;
/** @brief The one game object: metadata, timing, and FPS accounting. */
extern akgl_Game game;
/** @brief Storage behind the default `renderer`. */
extern akgl_RenderBackend _akgl_renderer;
/** @brief Storage behind the default `physics`. */
extern akgl_PhysicsBackend _akgl_physics;
/** @brief Storage behind the default `gamemap`. */
extern akgl_Tilemap _akgl_gamemap;
extern akgl_Game akgl_game;
/** @brief Storage behind the default `akgl_renderer`. */
extern akgl_RenderBackend akgl_default_renderer;
/** @brief Storage behind the default `akgl_physics`. */
extern akgl_PhysicsBackend akgl_default_physics;
/** @brief Storage behind the default `akgl_gamemap`. */
extern akgl_Tilemap akgl_default_gamemap;
/** @brief Currently active tilemap. */
extern akgl_Tilemap *gamemap;
extern akgl_Tilemap *akgl_gamemap;
/** @brief Currently active renderer. */
extern akgl_RenderBackend *renderer;
extern akgl_RenderBackend *akgl_renderer;
/** @brief Currently active physics backend. */
extern akgl_PhysicsBackend *physics;
extern akgl_PhysicsBackend *akgl_physics;
/** @brief Currently active camera. */
extern SDL_FRect *camera;
extern SDL_FRect *akgl_camera;
/**
* @brief True when every bit of `y` is set in `x`. Not "any of them" -- all of them.
* @warning Unparenthesized: it expands to `(x & y) == y`, so `!AKGL_BITMASK_HAS(a, b)`
* parses as `!(a & b) == b`. Use #AKGL_BITMASK_HASNOT rather than
* negating this. TODO.md item 21.
*
* Fully parenthesized, so it composes: `!AKGL_BITMASK_HAS(a, b)` and
* `AKGL_BITMASK_HAS(a, b) && cond` both mean what they read as. Until 0.5.0 it
* expanded to a bare `(x & y) == y`, so a negation bound to the `&` and parsed
* as `!(a & b) == b`. Nothing in the tree negated it -- #AKGL_BITMASK_HASNOT
* exists for that -- which is the only reason it was latent rather than live.
*/
#define AKGL_BITMASK_HAS(x, y) (x & y) == y
/** @brief True when at least one bit of `y` is missing from `x`. Same parenthesization caveat as #AKGL_BITMASK_HAS. */
#define AKGL_BITMASK_HASNOT(x, y) (x & y) != y
#define AKGL_BITMASK_HAS(x, y) ((((x) & (y)) == (y)))
/** @brief True when at least one bit of `y` is missing from `x`. */
#define AKGL_BITMASK_HASNOT(x, y) ((((x) & (y)) != (y)))
/** @brief Set every bit of `y` in `x`. Modifies `x`. */
#define AKGL_BITMASK_ADD(x, y) x |= y
#define AKGL_BITMASK_ADD(x, y) ((x) |= (y))
/** @brief Clear every bit of `y` in `x`. Modifies `x`. */
#define AKGL_BITMASK_DEL(x, y) x &= ~(y)
/** @brief Clear every bit of `x`. Carries its own trailing semicolon, so do not add another. */
#define AKGL_BITMASK_CLEAR(x) x = 0;
#define AKGL_BITMASK_DEL(x, y) ((x) &= ~(y))
/**
* @brief Clear every bit of `x`. Modifies `x`.
*
* Carries no trailing semicolon: write `AKGL_BITMASK_CLEAR(flags);` like any
* other statement. It used to include one, so every ordinary use produced an
* empty statement after it and a use as the whole body of an unbraced `if`
* would have taken the following statement with it.
*/
#define AKGL_BITMASK_CLEAR(x) ((x) = 0)
/**
* @brief Bring the whole library up: error codes, pools, registries, SDL, audio, fonts, gamepads.
@@ -141,14 +158,14 @@ extern SDL_FRect *camera;
* pools, create the registries, hand SDL the app metadata, clear the control
* maps, `SDL_Init` video/gamepad/audio, load the bundled controller database,
* open any attached gamepads, start SDL_mixer and SDL_ttf, and finally point
* `renderer`, `physics`, `camera`, and `gamemap` at their default storage.
* `akgl_renderer`, `akgl_physics`, `akgl_camera`, and `akgl_gamemap` at their default storage.
*
* What it does *not* do: create the window, choose a physics backend, or load
* any configuration. Those read properties, so they come after the caller has
* set them. See the sequence at the top of this file.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If `game.name`, `game.version`, or `game.uri` is
* @throws AKERR_NULLPOINTER If `akgl_game.name`, `akgl_game.version`, or `akgl_game.uri` is
* empty. All three are required and there are no defaults -- the
* window title, SDL's app metadata, and the savegame compatibility
* check are all built from them.
@@ -163,28 +180,18 @@ extern SDL_FRect *camera;
* @note It takes the state lock on the way in and releases it on the way out, so
* a failure part-way through leaves the mutex held.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init();
/**
* @brief Declared but never defined. Do not call.
*
* There is no definition anywhere in the library, so a translation unit that
* calls this compiles and then fails to link. Screen setup is akgl_render_init2d.
* Tracked in TODO.md under header/implementation surface drift.
*
* @return Nothing; it cannot be called.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init_screen();
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init(void);
/**
* @brief Count this frame, and recompute the frame rate once a second has passed.
*
* Called at the top of akgl_game_update, so a caller running its own loop needs
* to call it itself. `game.fps` is only refreshed when a full second has
* to call it itself. `akgl_game.fps` is only refreshed when a full second has
* elapsed, so it is a completed-second average rather than an instantaneous
* figure -- and it reads 0 for the first second of the process, which is under
* the low-FPS threshold and so fires `lowfpsfunc` on every frame until the first
* second is up.
*/
void akgl_game_updateFPS();
void akgl_game_update_fps(void);
/**
* @brief Write the game state and the name-to-pointer tables to a save file.
*
@@ -236,9 +243,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath);
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath);
/**
* @brief The default `game.lowfpsfunc`: log the current frame rate.
* @brief The default `akgl_game.lowfpsfunc`: log the current frame rate.
*
* Called from akgl_game_updateFPS on every frame where `game.fps` is under 30.
* Called from akgl_game_update_fps on every frame where `akgl_game.fps` is under 30.
* It is a placeholder -- the point of the hook is that a game can replace it
* with something that actually sheds work.
*/
@@ -251,15 +258,16 @@ void akgl_game_lowfps(void);
*
* @return `NULL` once the lock is held, otherwise an error context owned by the
* caller.
* @throws AKGL_ERR_SDL If the budget runs out with the lock still held
* elsewhere. The message carries `SDL_GetError()`, which after a failed
* `SDL_TryLockMutex` is usually stale or empty -- the status is the
* signal, not the text.
* @throws AKGL_ERR_SDL If #AKGL_GAME_STATE_LOCK_BUDGET_MS elapses with the lock
* still held elsewhere. `SDL_GetError()` is appended for whatever it is
* worth, but after a failed `SDL_TryLockMutex` it is usually stale or
* empty -- contention is reported by the return value, not by an error
* string. The status is the signal, not the text.
*
* @note The budget is meant to be one second but is not: the loop counts against
* #AKGL_TIME_ONESEC_MS, which is nanoseconds-per-millisecond (1000000)
* rather than milliseconds-per-second, so it retries 10,000 times at 100 ms
* and gives up after roughly 16 minutes. TODO.md item 6.
* @note Before 0.5.0 the loop counted against a constant named
* `AKGL_TIME_ONESEC_MS` that held 1000000, so it retried 10,000 times at
* 100 ms and blocked for roughly sixteen minutes rather than the one
* second documented here.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void);
/**
@@ -274,17 +282,24 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
/**
* @brief One frame: update every actor layer by layer, step the physics, draw the world.
*
* Takes the state lock, counts the frame, then walks layers 0 through
* #AKGL_TILEMAP_MAX_LAYERS calling each live actor's `updatefunc`, optionally
* rescaling it to the tilemap first. Then it steps `physics` and draws through
* `renderer`, and releases the lock.
* Takes the state lock, counts the frame, then calls each live actor's
* `updatefunc` **exactly once**, optionally rescaling it to the tilemap first.
* Then it steps `akgl_physics` and draws through `akgl_renderer`, and releases
* the lock.
*
* @param opflags Iterator flags. Optional -- `NULL` selects a default set that
* sweeps one layer at a time, in which case `layerid` is advanced
* by the loop. Only #AKGL_ITERATOR_OP_TILEMAPSCALE is read here;
* with it clear every actor's `scale` is forced to 1.0. Note that
* the flags are *not* forwarded to the physics or render calls,
* both of which are passed `NULL`.
* Until 0.5.0 the sweep sat inside a walk over #AKGL_TILEMAP_MAX_LAYERS that
* never compared an actor's `layer` to the layer it was on, so every actor
* updated sixteen times a frame. Updating an actor is not a per-layer
* operation; drawing is, and akgl_render_2d_draw_world walks the layers itself.
*
* @param opflags Iterator flags. Optional -- `NULL` selects
* #AKGL_ITERATOR_OP_UPDATE alone, which is every live actor
* once. Two bits are read here:
* #AKGL_ITERATOR_OP_TILEMAPSCALE, without which every actor's
* `scale` is forced to 1.0; and #AKGL_ITERATOR_OP_LAYERMASK,
* which restricts the sweep to the actors whose `layer` matches
* `layerid`. Note that the flags are *not* forwarded to the
* physics or render calls, both of which are passed `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL If the state lock cannot be taken.
* @throws AKERR_* Whatever an actor's `updatefunc`, akgl_tilemap_scale_actor,
@@ -296,4 +311,51 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags);
/*
* 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 Write all four name-to-address tables, each with its terminator.
*
* Actors, sprites, spritesheets, characters, in that order -- which is the order
* akgl_game_load reads them back in. Each table is the registry enumerated one
* entry at a time, followed by a zeroed name field and a zeroed pointer that the
* reader stops on.
*
* @param fp The open save-game stream. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fp is `NULL`.
* @throws AKERR_IO On a stream error or a short write while emitting a
* terminator.
*
* @note Only the terminators are written through the error-reporting path. The
* entries themselves go through `SDL_EnumerateProperties`, whose callbacks
* cannot report failure upward and instead terminate the process -- so a
* write error mid-table never reaches this function's return value.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp);
/**
* @brief Refuse a save file unless its version matches the running one exactly.
*
* Both strings are parsed as semver and compared with `"="` -- exact equality,
* not compatibility. That is deliberate and strict: a save file is a raw memory
* image of `akgl_Game` and the object tables, so any change to a struct layout
* invalidates it, and semver has no way to say "the layout did not move".
*
* @param versiontype What is being compared -- `"library"` or `"game"`. Used
* only to build the message, but required, since a bare
* "incompatible version" error would not say which.
* @param newversion The version read out of the save file. Required.
* @param curversion The version of the running program or library. Required.
* @return `NULL` when the two match, otherwise an error context owned by the
* caller.
* @throws AKERR_NULLPOINTER If any of the three arguments is `NULL`.
* @throws AKERR_VALUE If either string is not valid semver. The message says
* which side it came from.
* @throws AKERR_API If both parse but are not equal. The message quotes both.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion);
#endif //_AKGL_GAME_H_

View File

@@ -29,10 +29,10 @@
#ifndef _AKGL_HEAP_H_
#define _AKGL_HEAP_H_
#include "sprite.h"
#include "actor.h"
#include "character.h"
#include "staticstring.h"
#include <akgl/sprite.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/staticstring.h>
#include <akerror.h>
#ifndef AKGL_MAX_HEAP_ACTOR
@@ -52,15 +52,15 @@
#endif
/** @brief The actor pool. Public so the render and physics sweeps can walk it directly instead of going through the registry. */
extern akgl_Actor HEAP_ACTOR[AKGL_MAX_HEAP_ACTOR];
extern akgl_Actor akgl_heap_actors[AKGL_MAX_HEAP_ACTOR];
/** @brief The sprite pool. 16 per actor, on the assumption of one sprite per state combination. */
extern akgl_Sprite HEAP_SPRITE[AKGL_MAX_HEAP_SPRITE];
extern akgl_Sprite akgl_heap_sprites[AKGL_MAX_HEAP_SPRITE];
/** @brief The spritesheet pool. Sized like the sprite pool, though sharing means far fewer are used in practice. */
extern akgl_SpriteSheet HEAP_SPRITESHEET[AKGL_MAX_HEAP_SPRITESHEET];
extern akgl_SpriteSheet akgl_heap_spritesheets[AKGL_MAX_HEAP_SPRITESHEET];
/** @brief The character pool. */
extern akgl_Character HEAP_CHARACTER[AKGL_MAX_HEAP_CHARACTER];
extern akgl_Character akgl_heap_characters[AKGL_MAX_HEAP_CHARACTER];
/** @brief The string pool. Every entry is PATH_MAX bytes, so this is the largest of the five by a wide margin. */
extern akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING];
extern akgl_String akgl_heap_strings[AKGL_MAX_HEAP_STRING];
/**
* @brief Zero every pool, marking every slot free.
@@ -74,7 +74,7 @@ extern akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING];
* -- but check it anyway; the signature exists so a layer that needs
* real setup has somewhere to report from.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init();
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init(void);
/**
* @brief Zero the actor pool only.
*
@@ -85,7 +85,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init();
*
* @return `NULL`. No failure path today; see akgl_heap_init.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor();
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor(void);
/**
* @brief Claim a free actor slot.
* @param dest Receives a pointer to the free slot. Required, and **not**
@@ -195,11 +195,12 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_spritesheet(akgl_SpriteShee
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*
* @note It does not walk the state-to-sprite map, so the references the
* character took in akgl_character_sprite_add are never given back, and
* the SDL property set holding the map is leaked. Release the sprites
* first with akgl_character_state_sprites_iterate and
* #AKGL_ITERATOR_OP_RELEASE if you need them back.
* @note At zero it also walks the state-to-sprite map with
* akgl_character_state_sprites_iterate and #AKGL_ITERATOR_OP_RELEASE,
* giving back every reference akgl_character_sprite_add took, and then
* destroys the `SDL_PropertiesID` holding the map. Before 0.5.0 it did
* neither, so loading and releasing characters level by level exhausted
* the sprite pool and leaked a property set per character.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_character(akgl_Character *ptr);
/**

View File

@@ -20,44 +20,46 @@
#ifndef _AKGL_ITERATOR_H_
#define _AKGL_ITERATOR_H_
#include <stdint.h>
/** @brief Selects operations and an optional layer for actor traversal. */
typedef struct {
uint32_t flags; /**< Bitwise OR of the `AKGL_ITERATOR_OP_*` values below. */
uint8_t layerid; /**< Layer to restrict the sweep to. Read only when #AKGL_ITERATOR_OP_LAYERMASK is set. */
} akgl_Iterator;
#define AKGL_ITERATOR_OP_UPDATE 1 // 1 Call the actor's updatefunc
#define AKGL_ITERATOR_OP_RENDER 1 << 1 // 2 Call the actor's renderfunc
#define AKGL_ITERATOR_OP_RELEASE 1 << 2 // 4 Release the object back to its heap layer
#define AKGL_ITERATOR_OP_LAYERMASK 1 << 3 // 8 Skip anything whose layer != layerid
#define AKGL_ITERATOR_OP_TILEMAPSCALE 1 << 4 // 16 Scale actors to the tilemap; otherwise force scale 1.0
#define AKGL_ITERATOR_OP_UNDEFINED_5 1 << 5 // 32
#define AKGL_ITERATOR_OP_UNDEFINED_6 1 << 6 // 64
#define AKGL_ITERATOR_OP_UNDEFINED_7 1 << 7 // 128
#define AKGL_ITERATOR_OP_UNDEFINED_8 1 << 8 // 256
#define AKGL_ITERATOR_OP_UNDEFINED_9 1 << 9 // 512
#define AKGL_ITERATOR_OP_UNDEFINED_10 1 << 10 // 1024
#define AKGL_ITERATOR_OP_UNDEFINED_11 1 << 11 // 2048
#define AKGL_ITERATOR_OP_UNDEFINED_12 1 << 12 // 4096
#define AKGL_ITERATOR_OP_UNDEFINED_13 1 << 13 // 8192
#define AKGL_ITERATOR_OP_UNDEFINED_14 1 << 14 // 16384
#define AKGL_ITERATOR_OP_UNDEFINED_15 1 << 15 // 32768
#define AKGL_ITERATOR_OP_UNDEFINED_16 1 << 16 // 65536
#define AKGL_ITERATOR_OP_UNDEFINED_17 1 << 17 // 131072
#define AKGL_ITERATOR_OP_UNDEFINED_18 1 << 18 // 262144
#define AKGL_ITERATOR_OP_UNDEFINED_19 1 << 19 // 524288
#define AKGL_ITERATOR_OP_UNDEFINED_20 1 << 20 // 1048576
#define AKGL_ITERATOR_OP_UNDEFINED_21 1 << 21 // 2097152
#define AKGL_ITERATOR_OP_UNDEFINED_22 1 << 22 // 4194304
#define AKGL_ITERATOR_OP_UNDEFINED_23 1 << 23 // 8388608
#define AKGL_ITERATOR_OP_UNDEFINED_24 1 << 24 // 16777216
#define AKGL_ITERATOR_OP_UNDEFINED_25 1 << 25 // 33554432
#define AKGL_ITERATOR_OP_UNDEFINED_26 1 << 26 // 67108864
#define AKGL_ITERATOR_OP_UNDEFINED_27 1 << 27 // 134217728
#define AKGL_ITERATOR_OP_UNDEFINED_28 1 << 28 // 268435456
#define AKGL_ITERATOR_OP_UNDEFINED_29 1 << 29 // 536870912
#define AKGL_ITERATOR_OP_UNDEFINED_30 1 << 30 // 1073741824
#define AKGL_ITERATOR_OP_UNDEFINED_31 1 << 31 // 2147483648
#define AKGL_ITERATOR_OP_UPDATE (1 << 0) // 1 Call the actor's updatefunc
#define AKGL_ITERATOR_OP_RENDER (1 << 1) // 2 Call the actor's renderfunc
#define AKGL_ITERATOR_OP_RELEASE (1 << 2) // 4 Release the object back to its heap layer
#define AKGL_ITERATOR_OP_LAYERMASK (1 << 3) // 8 Skip anything whose layer != layerid
#define AKGL_ITERATOR_OP_TILEMAPSCALE (1 << 4) // 16 Scale actors to the tilemap; otherwise force scale 1.0
#define AKGL_ITERATOR_OP_UNDEFINED_5 (1 << 5) // 32
#define AKGL_ITERATOR_OP_UNDEFINED_6 (1 << 6) // 64
#define AKGL_ITERATOR_OP_UNDEFINED_7 (1 << 7) // 128
#define AKGL_ITERATOR_OP_UNDEFINED_8 (1 << 8) // 256
#define AKGL_ITERATOR_OP_UNDEFINED_9 (1 << 9) // 512
#define AKGL_ITERATOR_OP_UNDEFINED_10 (1 << 10) // 1024
#define AKGL_ITERATOR_OP_UNDEFINED_11 (1 << 11) // 2048
#define AKGL_ITERATOR_OP_UNDEFINED_12 (1 << 12) // 4096
#define AKGL_ITERATOR_OP_UNDEFINED_13 (1 << 13) // 8192
#define AKGL_ITERATOR_OP_UNDEFINED_14 (1 << 14) // 16384
#define AKGL_ITERATOR_OP_UNDEFINED_15 (1 << 15) // 32768
#define AKGL_ITERATOR_OP_UNDEFINED_16 (1 << 16) // 65536
#define AKGL_ITERATOR_OP_UNDEFINED_17 (1 << 17) // 131072
#define AKGL_ITERATOR_OP_UNDEFINED_18 (1 << 18) // 262144
#define AKGL_ITERATOR_OP_UNDEFINED_19 (1 << 19) // 524288
#define AKGL_ITERATOR_OP_UNDEFINED_20 (1 << 20) // 1048576
#define AKGL_ITERATOR_OP_UNDEFINED_21 (1 << 21) // 2097152
#define AKGL_ITERATOR_OP_UNDEFINED_22 (1 << 22) // 4194304
#define AKGL_ITERATOR_OP_UNDEFINED_23 (1 << 23) // 8388608
#define AKGL_ITERATOR_OP_UNDEFINED_24 (1 << 24) // 16777216
#define AKGL_ITERATOR_OP_UNDEFINED_25 (1 << 25) // 33554432
#define AKGL_ITERATOR_OP_UNDEFINED_26 (1 << 26) // 67108864
#define AKGL_ITERATOR_OP_UNDEFINED_27 (1 << 27) // 134217728
#define AKGL_ITERATOR_OP_UNDEFINED_28 (1 << 28) // 268435456
#define AKGL_ITERATOR_OP_UNDEFINED_29 (1 << 29) // 536870912
#define AKGL_ITERATOR_OP_UNDEFINED_30 (1 << 30) // 1073741824
#define AKGL_ITERATOR_OP_UNDEFINED_31 (1 << 31) // 2147483648
#endif // _AKGL_ITERATOR_H_

View File

@@ -26,11 +26,13 @@
* - **The document itself is never modified.**
*/
#ifndef _JSON_HELPERS_H_
#define _JSON_HELPERS_H_
#ifndef _AKGL_JSON_HELPERS_H_
#define _AKGL_JSON_HELPERS_H_
#include <akerror.h>
#include "staticstring.h"
#include <akgl/types.h>
#include <jansson.h>
#include <akgl/staticstring.h>
/**
* @brief Read a nested object out of a JSON object.
@@ -84,7 +86,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_integer_value(json_t *obj, char
* @throws AKERR_TYPE If @p key is present but is not a number. Integers and
* reals are both accepted, so `1` and `1.0` behave alike.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char *key, float *dest);
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char *key, float32_t *dest);
/**
* @brief Read a number out of a JSON object as a `double`.
*
@@ -100,7 +102,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not a number.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest);
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, float64_t *dest);
/**
* @brief Read a string out of a JSON object into a pooled akgl_String.
*
@@ -233,4 +235,4 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_string(json_t *arra
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize);
#endif // _JSON_HELPERS_H_
#endif // _AKGL_JSON_HELPERS_H_

View File

@@ -25,8 +25,8 @@
* falling one.
*/
#ifndef _PHYSICS_H_
#define _PHYSICS_H_
#ifndef _AKGL_PHYSICS_H_
#define _AKGL_PHYSICS_H_
#include <SDL3/SDL.h>
#include <akerror.h>
@@ -41,12 +41,12 @@ typedef struct akgl_PhysicsBackend {
akerr_ErrorContext AKERR_NOIGNORE *(*collide)(struct akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2); /**< Resolve a collision between two actors. Not called by the simulation loop yet. */
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /**< Commit one actor's velocity to its position. */
double drag_x; /**< Fraction of environmental velocity shed per second along x. 0 disables. From `physics.drag.x`. */
double drag_y; /**< Drag along y. From `physics.drag.y`. */
double drag_z; /**< Drag along z. From `physics.drag.z`. */
double gravity_x; /**< Acceleration along x, world units per second squared. Applied toward screen left. From `physics.gravity.x`. */
double gravity_y; /**< Acceleration along y. Applied down the screen, since y grows downward. From `physics.gravity.y`. */
double gravity_z; /**< Acceleration along z. Applied away from the camera. From `physics.gravity.z`. */
float64_t drag_x; /**< Fraction of environmental velocity shed per second along x. 0 disables. From `physics.drag.x`. */
float64_t drag_y; /**< Drag along y. From `physics.drag.y`. */
float64_t drag_z; /**< Drag along z. From `physics.drag.z`. */
float64_t gravity_x; /**< Acceleration along x, world units per second squared. Applied toward screen left. From `physics.gravity.x`. */
float64_t gravity_y; /**< Acceleration along y. Applied down the screen, since y grows downward. From `physics.gravity.y`. */
float64_t gravity_z; /**< Acceleration along z. Applied away from the camera. From `physics.gravity.z`. */
SDL_Time gravity_time; /**< Timestamp of the previous step, in nanoseconds. The simulation's `dt` is measured from this. */
SDL_Time timer_gravity; /**< Unused. Nothing in the library reads or writes it. */
} akgl_PhysicsBackend;
@@ -235,4 +235,4 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *sel
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags);
#endif // _PHYSICS_H_
#endif // _AKGL_PHYSICS_H_

View File

@@ -21,14 +21,15 @@
* separate call, made by akgl_game_init but by nothing else. A program
* that builds its own startup path and skips it gets a silently
* no-op akgl_set_property and an akgl_get_property that always hands
* back the caller's default, which in turn means akgl_render_init2d
* back the caller's default, which in turn means akgl_render_2d_init
* and akgl_physics_init_arcade quietly ignore their configuration.
* TODO.md, "Known and still open" item 3.
*/
#ifndef _REGISTRY_H_
#define _REGISTRY_H_
#ifndef _AKGL_REGISTRY_H_
#define _AKGL_REGISTRY_H_
#include <SDL3/SDL.h>
#include <akgl/error.h>
#include <akgl/staticstring.h>
@@ -63,7 +64,7 @@ extern SDL_PropertiesID AKGL_REGISTRY_PROPERTIES;
* @warning This does not create #AKGL_REGISTRY_PROPERTIES -- see the warning on
* this file.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init();
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init(void);
/**
* @brief Create the music registry.
* @return `NULL` on success, otherwise an error context owned by the caller.
@@ -71,7 +72,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init();
* @note Overwrites the existing id without destroying the old set, so calling it
* twice leaks the first one along with everything registered in it.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music();
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music(void);
/**
* @brief Create the font registry.
* @return `NULL` on success, otherwise an error context owned by the caller.
@@ -79,7 +80,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music();
* @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font();
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font(void);
/**
* @brief Create the actor registry, destroying any previous one first.
*
@@ -93,7 +94,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font();
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set. The old one
* has already been destroyed at that point, so the registry is left at 0.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor();
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor(void);
/**
* @brief Create the sprite registry.
* @return `NULL` on success, otherwise an error context owned by the caller.
@@ -101,7 +102,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor();
* @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite();
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite(void);
/**
* @brief Create the spritesheet registry.
* @return `NULL` on success, otherwise an error context owned by the caller.
@@ -109,7 +110,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite();
* @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet();
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet(void);
/**
* @brief Create the character registry.
* @return `NULL` on success, otherwise an error context owned by the caller.
@@ -117,14 +118,14 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet();
* @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character();
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character(void);
/**
* @brief Create the configuration-property registry.
*
* Deliberately separate from akgl_registry_init, because configuration has to be
* in place before the subsystems that read it start up. akgl_game_init creates
* it; filling it in -- with akgl_registry_load_properties or akgl_set_property --
* is the caller's job, and has to happen before akgl_render_init2d or
* is the caller's job, and has to happen before akgl_render_2d_init or
* akgl_physics_init_arcade run.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
@@ -132,13 +133,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character();
* @note Overwrites the existing id without destroying the old set, so every
* property set before a second call is lost.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_properties();
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_properties(void);
/**
* @brief Load a JSON configuration file into the property registry.
*
* Expects a document with a top-level `properties` object whose members are all
* strings; each becomes one entry. Numbers are configured as strings here and
* parsed by whoever reads them -- `game.screenwidth` is `"800"`, not `800`.
* parsed by whoever reads them -- `akgl_game.screenwidth` is `"800"`, not `800`.
*
* @param fname Path to the JSON document. Required. Used verbatim.
* @return `NULL` on success, otherwise an error context owned by the caller.
@@ -171,7 +172,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname);
* and `MOVING_OUT`, so those two states cannot be named from JSON at all.
* TODO.md items 24-26.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor_state_strings();
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor_state_strings(void);
/**
* @brief Set one configuration property.
*
@@ -215,4 +216,4 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *value);
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def);
#endif //_REGISTRY_H_
#endif //_AKGL_REGISTRY_H_

View File

@@ -2,27 +2,27 @@
* @file renderer.h
* @brief The pluggable rendering backend: a record of function pointers plus an initializer.
*
* There is one backend shipped, the 2D SDL one, and `akgl_render_init2d` is its
* There is one backend shipped, the 2D SDL one, and `akgl_render_2d_init` is its
* initializer -- it creates the window and the `SDL_Renderer` and then fills in
* the six `akgl_render_2d_*` entry points. A different renderer is a different
* initializer populating the same struct, not a branch inside these functions.
*
* Those are two separable jobs, and a host that already owns an `SDL_Renderer`
* -- an embedded interpreter, which must not create the window -- wants only the
* second. akgl_render_bind2d() is that half on its own; akgl_render_init2d()
* second. akgl_render_2d_bind() is that half on its own; akgl_render_2d_init()
* makes the window and then calls it.
*
* Callers do not normally name the `akgl_render_2d_*` functions directly; they
* go through the pointers on the backend (`renderer->frame_start(renderer)`),
* which is what makes the swap possible. The global `renderer` in game.h is the
* which is what makes the swap possible. The global `akgl_renderer` in game.h is the
* instance the rest of the library draws through.
*
* A frame is `frame_start` (clear), any number of `draw_*` calls, then
* `frame_end` (present).
*/
#ifndef _RENDERER_H_
#define _RENDERER_H_
#ifndef _AKGL_RENDERER_H_
#define _AKGL_RENDERER_H_
#include <SDL3/SDL.h>
@@ -61,7 +61,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *s
* it is checked -- a `NULL` @p self is a crash, not an error.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If `self->sdl_renderer` is `NULL`, which means the
* backend was never run through akgl_render_init2d.
* backend was never run through akgl_render_2d_init.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self);
/**
@@ -70,7 +70,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend
* it is checked -- a `NULL` @p self is a crash, not an error.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If `self->sdl_renderer` is `NULL`, which means the
* backend was never run through akgl_render_init2d.
* backend was never run through akgl_render_2d_init.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self);
/**
@@ -116,12 +116,12 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *
* @brief Draw the whole scene: every tilemap layer, and the actors standing on it.
*
* Walks layers from 0 to #AKGL_TILEMAP_MAX_LAYERS. For each, it draws that layer
* of the global `gamemap` through `camera` (if the map has that many layers),
* of the global `akgl_gamemap` through `akgl_camera` (if the map has that many layers),
* then sweeps the actor pool and calls `renderfunc` on every live actor whose
* `layer` matches. Sweeping in layer order is what puts actors in front of the
* scenery they stand on and behind the scenery they walk under.
*
* It reads the globals `gamemap`, `camera`, and `HEAP_ACTOR` directly rather
* It reads the globals `akgl_gamemap`, `akgl_camera`, and `akgl_heap_actors` directly rather
* than taking them as arguments, so there is exactly one world to draw.
*
* @param self The backend to draw through. Required.
@@ -137,7 +137,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *
* @throws AKERR_* Whatever akgl_tilemap_draw or an actor's own `renderfunc`
* raises; the first failure aborts the frame and propagates unchanged.
*
* @warning Neither the global `gamemap` nor a live actor's `renderfunc` is
* @warning Neither the global `akgl_gamemap` nor a live actor's `renderfunc` is
* checked before use, so drawing before akgl_tilemap_load, or with a
* hand-built actor that was never run through akgl_actor_initialize,
* is a crash rather than an error context.
@@ -147,7 +147,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend
/**
* @brief Install the 2D backend's methods on a backend the caller owns.
*
* The vtable half of akgl_render_init2d(), with no window, no renderer, and no
* The vtable half of akgl_render_2d_init(), with no window, no renderer, and no
* property registry involved: it points @p self's six function pointers at the
* `akgl_render_2d_*` entry points and returns. `sdl_renderer` is not touched,
* so a caller who has already put its own `SDL_Renderer` there keeps it, and a
@@ -156,7 +156,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend
*
* This is the entry point for a host that owns its own window -- it can drive
* akgl_actor_render(), akgl_tilemap_draw() and the rest through a backend
* libakgl never created. akgl_render_init2d() is the same thing with a window
* libakgl never created. akgl_render_2d_init() is the same thing with a window
* in front of it.
*
* @param self The backend to bind. Required. Only the method pointers are
@@ -164,15 +164,15 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_bind2d(akgl_RenderBackend *self);
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_bind(akgl_RenderBackend *self);
/**
* @brief Create the window and SDL renderer, and bind the 2D backend's methods.
*
* Reads `game.screenwidth` and `game.screenheight` from the property registry
* Reads `akgl_game.screenwidth` and `akgl_game.screenheight` from the property registry
* (both defaulting to the string "0", which asks SDL for a zero-sized window),
* creates the window and renderer with `game.uri` as the title, points `camera`
* at the full screen rectangle, and then calls akgl_render_bind2d() to install
* creates the window and renderer with `akgl_game.uri` as the title, points `akgl_camera`
* at the full screen rectangle, and then calls akgl_render_2d_bind() to install
* the six `akgl_render_2d_*` function pointers on @p self.
*
* Because the dimensions come from the registry, akgl_registry_init_properties
@@ -184,7 +184,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_bind2d(akgl_RenderBackend *self);
* pointers are overwritten; anything it held before is not released.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_VALUE If `game.screenwidth` or `game.screenheight` is set to
* @throws AKERR_VALUE If `akgl_game.screenwidth` or `akgl_game.screenheight` is set to
* something that is not a base-10 integer.
* @throws ERANGE If either dimension does not fit in an `int`.
* @throws AKGL_ERR_SDL If the window and renderer cannot be created. The message
@@ -195,6 +195,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_bind2d(akgl_RenderBackend *self);
* @note The two pooled strings holding the dimensions are only released on the
* success path, so each failed initialization leaks two string slots.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self);
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_init(akgl_RenderBackend *self);
#endif // _RENDERER_H_
#endif // _AKGL_RENDERER_H_

View File

@@ -12,7 +12,7 @@
* both publish themselves in a registry under their name, which is how
* akgl_character_sprite_add and the tilemap loader find them. Because loading a
* sheet uploads a texture, the renderer has to exist first -- so
* akgl_render_init2d, or akgl_game_init, before any of this.
* akgl_render_2d_init, or akgl_game_init, before any of this.
*/
#ifndef _AKGL_SPRITE_H_
@@ -28,8 +28,6 @@
#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 {
@@ -47,7 +45,7 @@ typedef struct {
uint32_t frames; /**< How many entries of frameids are in use. */
uint32_t width; /**< Frame width in pixels; also the horizontal stride used to find a frame on the sheet. */
uint32_t height; /**< Frame height in pixels; the vertical stride once frames wrap to the next row. */
uint32_t speed; /**< Nanoseconds one frame is held. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONESEC_MS, which despite its name is nanoseconds-per-millisecond. TODO.md item 6. */
uint32_t speed; /**< Nanoseconds one frame is held. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONEMS_NS. */
bool loop; /**< Restart from frame 0 when the last frame is reached, instead of holding it. */
bool loopReverse; /**< Play back down to frame 0 instead of jumping to it -- a ping-pong loop. */
char name[AKGL_SPRITE_MAX_NAME_LENGTH]; /**< Registry key, from the JSON `name` field. */
@@ -82,7 +80,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_initialize(akgl_Sprite *spr, char
/**
* @brief Load an image file as a texture and publish it as a spritesheet.
*
* Uploads @p filename through SDL_image using the global `renderer`, so the
* Uploads @p filename through SDL_image using the global `akgl_renderer`, so the
* renderer must already be initialized. The sheet is registered in
* #AKGL_REGISTRY_SPRITESHEET under @p filename, which is the key
* akgl_sprite_load_json looks it up by to avoid loading the same image twice.
@@ -166,6 +164,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_load_json(char *filename);
* `NULL`. Note that `sheet->texture` is dereferenced without a check,
* so a registered-but-unloaded sheet is a crash rather than an error.
*/
akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid);
akerr_ErrorContext *akgl_spritesheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid);
#endif //_AKGL_SPRITE_H_

View File

@@ -8,10 +8,10 @@
* these functions truncate rather than grow, and truncation is silent.
*/
#ifndef _STRING_H_
#define _STRING_H_
#ifndef _AKGL_STATICSTRING_H_
#define _AKGL_STATICSTRING_H_
#include "string.h"
#include <string.h>
#include <akerror.h>
#include <limits.h>
@@ -40,32 +40,35 @@ typedef struct
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
*
* @note Known defect: the `NULL` @p init path zeroes `sizeof(akgl_String)`
* bytes starting at `data`, which is four bytes past the end of the
* buffer -- `refcount` sits in front of it. TODO.md, "Known and still
* open" item 6.
* @note Until 0.5.0 the `NULL` @p init path zeroed `sizeof(akgl_String)` bytes
* starting at `data`, which is four bytes past the end of the buffer --
* `refcount` sits in front of it, so the overrun landed on the next pool
* slot's reference count.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_initialize(akgl_String *obj, char *init);
/**
* @brief Copy the contents of one pooled string into another.
*
* A bounded `strncpy` between two already-claimed pool slots. It copies bytes
* only: `refcount` is left alone, so @p dst keeps whatever pool state it had.
* only: `refcount` is left alone, so @p dest keeps whatever pool state it had.
*
* @param src Source string. Required. Read up to @p count bytes.
* @param dst Destination string. Required. Overwritten in place; the pool
* @param dest Destination string. Required. Overwritten in place; the pool
* slot must already have been claimed.
* @param count Maximum bytes to copy. 0 selects #AKGL_MAX_STRING_LENGTH, the
* whole buffer. A @p count shorter than the source truncates
* without writing a terminator; a @p count longer than the source
* zero-pads the remainder, per `strncpy`. Values above
* #AKGL_MAX_STRING_LENGTH overrun both buffers and are not
* rejected.
* zero-pads the remainder, per `strncpy`. A negative @p count, or
* one above #AKGL_MAX_STRING_LENGTH, is refused -- both buffers
* are exactly that long, so a larger count walked off the end of
* two pool slots at once.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p src or @p dst is `NULL`.
* @throws AKERR_NULLPOINTER If @p src or @p dest is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p count is negative or above
* #AKGL_MAX_STRING_LENGTH.
* @throws errno Whatever `errno` holds if `strncpy` returns something other
* than @p dst. In practice `strncpy` always returns its destination, so
* than @p dest. In practice `strncpy` always returns its destination, so
* this path is unreachable rather than merely rare.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_copy(akgl_String *src, akgl_String *dst, int count);
#endif //_STRING_H_
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_copy(akgl_String *src, akgl_String *dest, int count);
#endif //_AKGL_STATICSTRING_H_

View File

@@ -13,8 +13,8 @@
* static text redrawn every frame.
*/
#ifndef _TEXT_H_
#define _TEXT_H_
#ifndef _AKGL_TEXT_H_
#define _AKGL_TEXT_H_
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
@@ -69,11 +69,29 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath
* left with a dangling pointer. Fonts are not reference counted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name);
/**
* @brief Close every font in #AKGL_REGISTRY_FONT and destroy the registry.
*
* A font, once loaded, was reachable only by name -- so a game that exited
* without unloading each one by hand left them open, and `SDL_Quit` destroying
* the property registry took the last reference to them with it. That is
* bounded by how many fonts a game loads rather than unbounded, but there was
* no way to do anything about it at all.
*
* Call this during shutdown, **before** `TTF_Quit` or `SDL_Quit`. Afterwards
* #AKGL_REGISTRY_FONT is 0 and akgl_text_loadfont needs
* akgl_registry_init_font() again before it can register anything.
*
* @return `NULL`. Closing a font reports nothing, and an uninitialized registry
* is success rather than an error -- shutdown paths run after partial
* startups.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadallfonts(void);
/**
* @brief Rasterize a string and blit it at a screen position, in one call.
*
* Renders blended (anti-aliased, alpha-blended) through SDL_ttf, uploads the
* result to a texture, draws it through the global `renderer`, and destroys both
* result to a texture, draws it through the global `akgl_renderer`, and destroys both
* the texture and the surface before returning. The text is drawn at its natural
* size -- @p x and @p y are the top-left corner, not a centre.
*
@@ -91,9 +109,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name);
* @param y Top edge of the text, in screen pixels.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p font or @p text is `NULL`; if the global
* `renderer`, its `sdl_renderer`, or its `draw_texture` is `NULL` --
* `akgl_renderer`, its `sdl_renderer`, or its `draw_texture` is `NULL` --
* that last one is the state a backend is in between being allocated
* and being run through akgl_render_bind2d(); if SDL_ttf cannot
* and being run through akgl_render_2d_bind(); if SDL_ttf cannot
* rasterize the string; or if the surface cannot be uploaded as a
* texture. The last two carry `SDL_GetError()` and are a reused status
* rather than a pointer problem.
@@ -155,4 +173,4 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text,
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h);
#endif // _TEXT_H_
#endif // _AKGL_TEXT_H_

View File

@@ -27,12 +27,12 @@
*
* Note the size of akgl_Tilemap: a layer's tile grid alone is 512x512 ints, and
* a tileset's offset table is 65536 pairs. It is a megabytes-large object and
* belongs in static storage, which is where `_akgl_gamemap` puts it. Do not put
* belongs in static storage, which is where `akgl_default_gamemap` puts it. Do not put
* one on the stack.
*/
#ifndef _TILEMAP_H_
#define _TILEMAP_H_
#ifndef _AKGL_TILEMAP_H_
#define _AKGL_TILEMAP_H_
#include <limits.h>
#include <akgl/actor.h>
@@ -71,8 +71,8 @@
/** @brief One object placed in a Tiled object layer. */
typedef struct {
float x; /**< Position in map pixels, from the Tiled object. Copied onto the actor it spawns. */
float y; /**< Position in map pixels. */
float32_t x; /**< Position in map pixels, from the Tiled object. Copied onto the actor it spawns. */
float32_t y; /**< Position in map pixels. */
int gid; /**< Global tile id, for a tile object. Not read by the loader. */
int id; /**< Tiled's object id. Not read by the loader. */
int height; /**< Height in map pixels. Read into the map's perspective bands, not into this field. */
@@ -87,7 +87,7 @@ typedef struct {
/** @brief One layer of a tilemap: a tile grid, an image, or a group of objects. */
typedef struct {
short type; /**< Which of the `AKGL_TILEMAP_LAYER_TYPE_*` kinds this is; decides which of the members below mean anything. */
float opacity; /**< 0.0 to 1.0, from Tiled. Recorded but not yet applied at draw time. */
float32_t opacity; /**< 0.0 to 1.0, from Tiled. Recorded but not yet applied at draw time. */
bool visible; /**< From Tiled. Recorded but not yet consulted at draw time. */
int height; /**< Tile layer: height in tiles. Image layer: the texture's height in pixels. */
int width; /**< Tile layer: width in tiles. Image layer: the texture's width in pixels. */
@@ -143,15 +143,15 @@ typedef struct {
int p_vanishing_y; /**< Y of the `p_vanishing` marker: the row at which actors are smallest. 0 disables perspective. */
int p_foreground_h; /**< Height of the `p_foreground` marker object. Recorded; not used in the current rate calculation. */
int p_vanishing_h; /**< Height of the `p_vanishing` marker object. Recorded, likewise. */
float p_foreground_scale; /**< Actor scale at `p_foreground_y`. Defaults to 1.0. */
float p_vanishing_scale; /**< Actor scale at `p_vanishing_y`. Defaults to 1.0. */
float p_scale; /**< Unused. Left from an earlier formulation of the perspective maths. */
float p_rate; /**< Scale change per pixel of y between the two markers. Derived at load; what akgl_tilemap_scale_actor interpolates with. */
float32_t p_foreground_scale; /**< Actor scale at `p_foreground_y`. Defaults to 1.0. */
float32_t p_vanishing_scale; /**< Actor scale at `p_vanishing_y`. Defaults to 1.0. */
float32_t p_scale; /**< Unused. Left from an earlier formulation of the perspective maths. */
float32_t p_rate; /**< Scale change per pixel of y between the two markers. Derived at load; what akgl_tilemap_scale_actor interpolates with. */
akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS]; /**< The tilesets, in the order Tiled listed them. */
akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS]; /**< The layers, in draw order: index 0 is furthest back. */
// Different levels may have different physics.
bool use_own_physics; /**< Set when the map declared a `physics.model` property. A caller that honours it simulates through `physics` instead of the global backend. */
bool use_own_physics; /**< Set when the map declared a `physics.model` property. A caller that honours it simulates through `akgl_physics` instead of the global backend. */
akgl_PhysicsBackend physics; /**< This map's own backend, valid only when `use_own_physics` is set. */
} akgl_Tilemap;
@@ -204,21 +204,20 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load(char *fname, akgl_Tilemap *
* tile grid. An image layer ignores the viewport entirely and is drawn once at
* the origin.
*
* Drawing goes through the global `renderer`, not through a backend passed in.
* Drawing goes through the global `akgl_renderer`, not through a backend passed in.
*
* @param dest The map to draw from. Required. Not an output parameter
* despite the name.
* @param map The map to draw from. Required.
* @param viewport The rectangle of the map, in map pixels, that is on screen.
* Required. Usually the global `camera`.
* Required. Usually the global `akgl_camera`.
* @param layeridx Which layer to draw, 0 to `numlayers - 1`. **Not**
* bounds-checked: an index past the end reads a neighbouring
* layer, or past the array.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p viewport is `NULL`.
* @throws AKERR_NULLPOINTER If @p map or @p viewport is `NULL`.
* @throws AKERR_* Whatever the renderer's `draw_texture` raises. The first
* failure abandons the rest of the layer.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRect *viewport, int layeridx);
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, int layeridx);
/**
* @brief Draw a whole tileset to the screen, tile by tile, from its offset table.
*
@@ -227,11 +226,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRe
* matches the source image the offset table is right -- and if tiles are shifted
* or duplicated, it is not. The `charviewer` utility uses it.
*
* @param dest The map holding the tileset. Required. Not an output
* parameter despite the name.
* @param map The map holding the tileset. Required.
* @param tilesetidx Which tileset to draw, 0 to `numtilesets - 1`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
* @throws AKERR_NULLPOINTER If @p map is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p tilesetidx is at or above `numtilesets`. A
* negative index is not rejected.
* @throws AKERR_* Whatever the renderer's `draw_texture` raises.
@@ -240,7 +238,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRe
* tileset's, so a tileset whose tiles are a different size from the map's
* is reconstructed at the wrong pitch.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest, int tilesetidx);
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *map, int tilesetidx);
/*
* These functions are part of the internal API and should not be called by the user.
@@ -490,4 +488,152 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest);
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor);
#endif //_TILEMAP_H_
/**
* @brief Read a Tiled custom property declared as `number`.
*
* Tiled writes `float` for a floating-point property, so this matches nothing
* Tiled produces today -- akgl_get_json_properties_float is the one the loader
* uses. Kept because a hand-written or older map may still say `number`.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value as a `float`. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `number`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_number(json_t *obj, char *key, float32_t *dest);
/**
* @brief Read a Tiled custom property declared as `float`.
*
* The spelling Tiled actually writes for a floating-point property. This is how
* the `scale` on a perspective marker is read.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_float(json_t *obj, char *key, float32_t *dest);
/**
* @brief Read a Tiled custom property declared as `float`, at full precision.
*
* Same declared type as akgl_get_json_properties_float, but writing a `double`.
* The map's physics constants are `double`, which is the reason both exist.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* akgl_tilemap_load_physics passes this status to
* akgl_get_json_with_default, which turns it into a default of 0.0.
* @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_double(json_t *obj, char *key, float64_t *dest);
/**
* @brief Turn one Tiled object into a live actor, creating it if it is not already registered.
*
* The object's name is the actor's registry key, which is what lets the same
* actor be placed by more than one object: the first placement creates it and
* binds the character named in the object's `character` property, and every
* later one just takes another reference. Either way the object's position,
* visibility, and layer are copied onto the actor and the object keeps a
* pointer to it.
*
* Not declared in tilemap.h -- reachable only through
* akgl_tilemap_load_layer_objects.
*
* @param curobj The tilemap object, with its `name`, `x`, `y`, and
* `visible` already read from JSON. Required, unchecked,
* dereferenced at once.
* @param layerdatavalue The object's JSON, for the custom properties --
* `character` and `state` -- that are not part of Tiled's
* own object fields. Required.
* @param layerid The layer this object sits on, copied onto the actor.
* @param dirname Directory to resolve paths against. Accepted for
* signature consistency; nothing here uses it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_KEY If the object's name is empty -- an actor has to be
* addressable -- if the `character` or `state` property is absent, or if
* the named character is not in #AKGL_REGISTRY_CHARACTER.
* @throws AKERR_TYPE If `character` is not declared `string` or `state` not
* declared `int`.
* @throws AKERR_NULLPOINTER If akgl_actor_initialize refuses the name.
* @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
*
* @note An existing actor's `character` and position are overwritten by each
* later placement, so two objects naming one actor leave it wherever the
* last one put it rather than producing two.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname);
/**
* @brief Load one image layer: upload its image as a single texture.
*
* An image layer is one picture rather than a grid, which is what a parallax
* backdrop wants. The layer's `width` and `height` are taken from the loaded
* texture rather than from the JSON, so they are always the true pixel size.
*
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load_layers.
*
* @param dest The map to load into. Required.
* @param root The layer's JSON object. Required.
* @param layerid Which layer slot to fill. Not bounds-checked.
* @param dirname Directory to resolve the layer's `image` path against.
* Required. Joined with a `/` rather than run through
* akgl_path_relative, so unlike a tileset image this path is not
* canonicalized and an absolute one will not work.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
* @throws AKERR_KEY If the layer has no `image`.
* @throws AKERR_TYPE If `image` is not a string.
* @throws AKGL_ERR_SDL If the image cannot be loaded. The message carries
* `SDL_GetError()`.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @note The joined path is truncated at
* #AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE without complaint, so an
* over-long path fails as a missing file rather than as a length error.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
/**
* @brief Give the map its own physics backend, if it asked for one.
*
* A map with a `physics.model` custom property gets its own backend, configured
* from `physics.gravity.x`/`.y`/`.z` and `physics.drag.x`/`.y`/`.z`, and
* `use_own_physics` is set so a caller knows to simulate through it. Each
* constant defaults to 0.0 when absent, so a map can name a model and override
* only what it cares about.
*
* Absence is the normal case at every level: a map with no properties at all, or
* with properties but no `physics.model`, uses the game's global backend and
* this returns success. That is why the AKERR_KEY handlers here are not error
* paths -- they are the "map did not ask" path.
*
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load.
*
* @param dest The map to configure. Required in practice; not checked here,
* since akgl_tilemap_load has already done so.
* @param root The map's root JSON object. Required, likewise.
* @return `NULL` on success -- including when the map declared no physics at
* all -- otherwise an error context owned by the caller.
* @throws AKERR_KEY If `physics.model` names a backend that does not exist.
* @throws AKERR_TYPE If `physics.model` is not declared `string`, or one of the
* six constants is not declared `float`.
* @throws AKERR_NULLPOINTER If akgl_physics_factory refuses its arguments.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root);
#endif //_AKGL_TILEMAP_H_

View File

@@ -17,7 +17,7 @@
/** @brief Single-precision float. Positions, velocities, and scale factors. */
typedef float float32_t;
/** @brief Double-precision float. Declared for symmetry; unused so far. */
/** @brief Double-precision float. The physics backend keeps its drag and gravity constants at this width. */
typedef double float64_t;
#endif // _AKGL_TYPES_H_

View File

@@ -12,18 +12,20 @@
* separating-axis test.
*/
#ifndef _UTIL_H_
#define _UTIL_H_
#ifndef _AKGL_UTIL_H_
#define _AKGL_UTIL_H_
#include <SDL3/SDL.h>
#include <akerror.h>
#include <stdbool.h>
#include <akgl/staticstring.h>
/** @brief An integer point. Carries a `z` the collision routines do not use. */
typedef struct point {
typedef struct akgl_Point {
int x; /**< Horizontal position, in whatever space the caller is working in. */
int y; /**< Vertical position. */
int z; /**< Depth. Never written by akgl_rectangle_points and never read by the collision tests. */
} point;
} akgl_Point;
/**
* @brief The four corners of an axis-aligned rectangle, precomputed.
@@ -32,19 +34,12 @@ typedef struct point {
* spans, so it wants the corners as points. akgl_rectangle_points derives one of
* these from an `SDL_FRect`.
*/
typedef struct RectanglePoints {
point topleft; /**< (x, y). */
point topright; /**< (x + w, y). */
point bottomleft; /**< (x, y + h). */
point bottomright; /**< (x + w, y + h). */
} RectanglePoints;
/**
* @brief Do not use. Three open parentheses, two closes -- any expansion is a
* syntax error. It duplicates akgl_collide_rectangles(), has no callers,
* and is slated for deletion. TODO.md item 20.
*/
#define AKGL_COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x)
typedef struct akgl_RectanglePoints {
akgl_Point topleft; /**< (x, y). */
akgl_Point topright; /**< (x + w, y). */
akgl_Point bottomleft; /**< (x, y + h). */
akgl_Point bottomright; /**< (x + w, y + h). */
} akgl_RectanglePoints;
/**
* @brief Expand a rectangle into its four corner points.
@@ -62,7 +57,7 @@ typedef struct RectanglePoints {
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p rect is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect);
akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(akgl_RectanglePoints *dest, SDL_FRect *rect);
/**
* @brief Test whether a point falls inside a rectangle, edges included.
*
@@ -78,7 +73,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(RectanglePoints *dest,
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p p, @p r, or @p collide is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(point *p, RectanglePoints *r, bool *collide);
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(akgl_Point *p, akgl_RectanglePoints *r, bool *collide);
/**
* @brief Test whether two rectangles overlap, edges included.
*
@@ -114,11 +109,11 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FR
* @param root Directory to fall back to, normally `dirname` of the file that
* contained @p path. Required, even when unused.
* @param path The path to resolve, relative or absolute. Required.
* @param dst Receives the resolved absolute path. Required, and must already be
* @param dest Receives the resolved absolute path. Required, and must already be
* a claimed pool string -- this writes into it, it does not claim
* one for you.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dst is `NULL`.
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dest is `NULL`.
* @throws AKERR_OUTOFBOUNDS If `root + "/" + path` would not fit in
* #AKGL_MAX_STRING_LENGTH.
* @throws ENOENT If neither spelling names an existing file. Any other `errno`
@@ -132,7 +127,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FR
* error context it was handling. Each such call consumes one slot of
* libakerror's fixed 128-entry context array for the life of the process.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, akgl_String *dst);
akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, akgl_String *dest);
// These are REALLY slow routines that are only useful in testing harnesses
/**
@@ -147,12 +142,15 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, ak
* @param s2 Second surface. Required.
* @return `NULL` when the pixels match, otherwise an error context owned by the
* caller. "Not equal" is reported as an error, not as an out-param.
* @throws AKERR_NULLPOINTER If @p s1 or @p s2 is `NULL`.
* @throws AKERR_VALUE If the pixels differ.
* @throws AKERR_NULLPOINTER If @p s1 or @p s2 is `NULL`, or either has no
* pixel buffer.
* @throws AKERR_VALUE If the surfaces differ in size, pitch, or pixel format,
* or if their pixels differ.
*
* @warning The surfaces' dimensions, pitch, and format are not compared, so a
* smaller @p s2 is read past its end rather than reported as a
* mismatch. TODO.md, "Known and still open" item 5.
* Dimensions, pitch and pixel format are compared first, and a difference in
* any of them is reported as a mismatch. Until 0.5.0 they were not, so a
* smaller @p s2 was read past its end instead -- benign in practice and
* immediately fatal under a memory checker.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2);
/**
@@ -182,10 +180,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SD
* @throws AKERR_VALUE If the two renders differ.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @warning Known defect: both passes draw @p t1 -- @p t2 is never rendered -- so
* this currently always reports a match. TODO.md, "Known and still
* open" item 1.
* @note Until 0.5.0 both passes drew @p t1, so this always reported a match and
* every image assertion built on it asserted nothing. It draws @p t2 on
* the second pass now.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout);
#endif // _UTIL_H_
#endif // _AKGL_UTIL_H_

View File

@@ -1,32 +1,108 @@
#!/bin/bash
#
# Regenerate include/akgl/SDL_GameControllerDB.h from the community controller
# database.
#
# That header is tracked on purpose: it is the offline fallback that keeps the
# library buildable when upstream is renamed, rate-limited, taken down, or
# simply unreachable. This script therefore has one rule above all others --
# **it must never replace a good header with a worse one**. Until 0.5.0 it did
# exactly that: no `set -e`, no check on curl's exit status, so a failed fetch
# wrote an empty mappings file, overwrote the good tracked header with
# `AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty initializer, and exited 0.
# Verified by pointing it at an unresolvable host.
#
# Everything is assembled in a temporary directory and moved into place only
# once it has been checked. A failure leaves the existing header untouched and
# exits non-zero.
#
# This is no longer run by an ordinary build. `cmake --build build --target
# controllerdb` is the deliberate way to update the mappings, so a build needs
# no network access and does not leave the working tree dirty.
#
# Usage: mkcontrollermappings.sh <repository-root>
rootdir=$1
set -euo pipefail
curl https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt | grep -v '^#' | grep -v '^$' | sed s/',$'//g > mappings.txt
AKGL_CONTROLLERDB_URL="${AKGL_CONTROLLERDB_URL:-https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt}"
# A floor, not a target. The database has carried well over two thousand entries
# for years; a few hundred means something truncated the response, which is a
# fetch that "succeeded" and still must not be committed.
AKGL_CONTROLLERDB_MIN_LINES="${AKGL_CONTROLLERDB_MIN_LINES:-1000}"
filelen=$(wc -l mappings.txt | cut -d ' ' -f 1)
function die()
{
echo "mkcontrollermappings: $*" >&2
exit 1
}
cat > ${rootdir}/include/akgl/SDL_GameControllerDB.h <<EOF
#ifndef _SDL_GAMECONTROLLERDB_H_
#define _SDL_GAMECONTROLLERDB_H_
if [[ $# -lt 1 ]]; then
die "usage: $0 <repository-root>"
fi
// Taken from https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt on $(date)
rootdir="$1"
target="${rootdir}/include/akgl/SDL_GameControllerDB.h"
#define AKGL_SDL_GAMECONTROLLER_DB_LEN ${filelen}
if [[ ! -d "${rootdir}/include/akgl" ]]; then
die "no such directory: ${rootdir}/include/akgl"
fi
command -v curl >/dev/null 2>&1 || die "curl is not available"
const char *SDL_GAMECONTROLLER_DB[] = {
EOF
workdir="$(mktemp -d)"
counter=0
function cleanup()
{
rm -rf "${workdir}"
}
trap cleanup EXIT
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
raw="${workdir}/raw.txt"
mappings="${workdir}/mappings.txt"
staged="${workdir}/SDL_GameControllerDB.h"
printf "\n};\n" >> ${rootdir}/include/akgl/SDL_GameControllerDB.h
printf "#endif // _SDL_GAMECONTROLLERDB_H_\n" >> ${rootdir}/include/akgl/SDL_GameControllerDB.h
# --fail so an HTTP error status is an exit status rather than an error page in
# the body. Not a pipeline: the exit status being checked is curl's, and a
# pipeline reports the last command's.
if ! curl --fail --silent --show-error --location "${AKGL_CONTROLLERDB_URL}" --output "${raw}"; then
die "fetch failed from ${AKGL_CONTROLLERDB_URL}; ${target} left as it was"
fi
grep -v '^#' "${raw}" | grep -v '^$' | sed s/',$'//g > "${mappings}" || true
filelen=$(wc -l < "${mappings}" | tr -d ' ')
if [[ "${filelen}" -lt "${AKGL_CONTROLLERDB_MIN_LINES}" ]]; then
die "fetched only ${filelen} mappings, expected at least ${AKGL_CONTROLLERDB_MIN_LINES}; ${target} left as it was"
fi
# Every mapping becomes a C string literal, so a quote or a backslash would
# produce a header that does not compile -- or, worse, one that does.
if grep -q '["\\]' "${mappings}"; then
die "fetched mappings contain a quote or a backslash; ${target} left as it was"
fi
{
echo "#ifndef _AKGL_SDL_GAMECONTROLLERDB_H_"
echo "#define _AKGL_SDL_GAMECONTROLLERDB_H_"
echo ""
echo "// Taken from ${AKGL_CONTROLLERDB_URL} on $(date)"
echo ""
echo "#define AKGL_SDL_GAMECONTROLLER_DB_LEN ${filelen}"
echo ""
echo "const char *SDL_GAMECONTROLLER_DB[] = {"
# One pass: wrap each mapping as a quoted, indented, comma-terminated entry,
# then drop the trailing comma from the last. An empty initializer is a
# constraint violation in ISO C that compiles only as a GCC extension, and
# the length check above is what guarantees there is at least one entry.
sed -e 's/.*/ "&",/' -e '$ s/,$//' "${mappings}"
echo "};"
echo "#endif // _AKGL_SDL_GAMECONTROLLERDB_H_"
} > "${staged}"
# Checked before it is allowed anywhere near the tracked copy.
if ! grep -q "^#define AKGL_SDL_GAMECONTROLLER_DB_LEN *${filelen}\$" "${staged}"; then
die "staged header did not come out as expected; ${target} left as it was"
fi
mv "${staged}" "${target}"
echo "mkcontrollermappings: wrote ${filelen} mappings to ${target}"

86
scripts/check_api_surface.sh Executable file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
#
# Every function with external linkage must be declared in a header.
#
# A non-static function that appears in no header is still in the ABI but is
# unreachable by callers, and it collides with any consumer that happens to pick
# the same name. libakgl shipped nineteen of those. The rule is in AGENTS.md
# under "API Surface"; this is what enforces it.
#
# Usage: check_api_surface.sh <libakgl.so> <header-dir> [<header-dir> ...]
#
# Exits 0 when every exported akgl_* symbol is declared, 1 otherwise, and 2 when
# it cannot do its job -- a missing library or no nm.
set -u
if [ "$#" -lt 2 ]; then
echo "usage: $0 <libakgl.so> <header-dir> [<header-dir> ...]" >&2
exit 2
fi
LIBRARY="$1"
shift
if ! command -v nm >/dev/null 2>&1; then
echo "check_api_surface: nm is not available; skipping" >&2
exit 2
fi
if [ ! -f "${LIBRARY}" ]; then
echo "check_api_surface: no such library: ${LIBRARY}" >&2
exit 2
fi
DECLARATIONS="$(mktemp)"
trap 'rm -f "${DECLARATIONS}"' EXIT
# Comments are stripped first. Several of these names appear in prose -- the
# whole point of the check is that being *mentioned* in a header is not the same
# as being declared in one, and that distinction is exactly what went wrong with
# the gamepad_handle_* handlers.
for dir in "$@"; do
[ -d "${dir}" ] || continue
find "${dir}" -name '*.h' -print0 |
xargs -0 --no-run-if-empty cat |
sed -e 's://.*::' |
awk 'BEGIN { incomment = 0 }
{
line = $0
while ( 1 ) {
if ( incomment ) {
idx = index(line, "*/")
if ( idx == 0 ) { line = ""; break }
line = substr(line, idx + 2); incomment = 0
} else {
idx = index(line, "/*")
if ( idx == 0 ) { break }
printf "%s", substr(line, 1, idx - 1)
line = substr(line, idx + 2); incomment = 1
}
}
print line
}' >> "${DECLARATIONS}"
done
UNDECLARED=""
for symbol in $(nm -D --defined-only "${LIBRARY}" | awk '$2 == "T" { print $3 }' | grep '^akgl_' | sort); do
if ! grep -qE "(^|[^A-Za-z0-9_])${symbol}[^A-Za-z0-9_]" "${DECLARATIONS}"; then
UNDECLARED="${UNDECLARED} ${symbol}"
fi
done
if [ -n "${UNDECLARED}" ]; then
echo "check_api_surface: exported but declared in no header:" >&2
for symbol in ${UNDECLARED}; do
echo " ${symbol}" >&2
done
echo "" >&2
echo "Declare each one in the header for its subsystem, or make it static." >&2
echo "A function exposed only so tests can reach it goes under the existing" >&2
echo "\"part of the internal API\" comment block." >&2
exit 1
fi
echo "check_api_surface: every exported akgl_* symbol is declared"
exit 0

145
scripts/check_error_protocol.py Executable file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Enforce the two akerror control-flow rules whose failure mode is silent.
AGENTS.md states both, and both have already cost this library a leak:
1. Never use a ``*_RETURN`` macro inside an ``ATTEMPT`` block. It returns past
``CLEANUP``, so every release, ``fclose`` and free in ``CLEANUP`` is
skipped. ``akgl_get_json_tilemap_property`` leaked two pooled strings on
every *successful* lookup this way.
2. Never ``return`` from inside a ``HANDLE`` block. ``FINISH`` is what carries
``RELEASE_ERROR``, so leaving early never gives the handled context back to
``AKERR_ARRAY_ERROR``. That cost ``akgl_path_relative`` one slot per call
and killed the process on the 129th.
Neither produces a compiler diagnostic, and neither shows up in a passing test
run until the pool it drains is empty. Hence a linter.
Usage: check_error_protocol.py <file-or-directory> [...]
Exits 0 when clean, 1 on a violation.
"""
import os
import re
import sys
# A ``*_RETURN`` macro invocation: SUCCEED_RETURN, FAIL_RETURN, FAIL_ZERO_RETURN,
# FAIL_NONZERO_RETURN, and anything later shaped like them.
RETURN_MACRO = re.compile(r"\b[A-Z][A-Z0-9_]*_RETURN\s*\(")
BARE_RETURN = re.compile(r"\breturn\b")
ATTEMPT_OPEN = re.compile(r"\bATTEMPT\s*\{")
CLEANUP_OPEN = re.compile(r"\}\s*CLEANUP\s*\{")
HANDLE_OPEN = re.compile(r"\}\s*HANDLE(?:_GROUP)?\s*\(")
FINISH_ANY = re.compile(r"\}\s*FINISH(?:_NORETURN)?\s*\(")
def strip_comments(text):
"""Blank out comments, keeping line numbering intact.
Comments are removed rather than skipped because several of these keywords
appear in prose -- "which should never return AKERR_KEY" is a sentence, not
a violation.
"""
out = []
i = 0
n = len(text)
while i < n:
ch = text[i]
if ch == '"' or ch == "'":
quote = ch
out.append(ch)
i += 1
while i < n:
if text[i] == "\\" and (i + 1) < n:
out.append(" ")
i += 2
continue
out.append(text[i])
if text[i] == quote:
i += 1
break
i += 1
continue
if text.startswith("/*", i):
j = text.find("*/", i + 2)
j = n if j == -1 else (j + 2)
out.append("".join(c if c == "\n" else " " for c in text[i:j]))
i = j
continue
if text.startswith("//", i):
j = text.find("\n", i)
j = n if j == -1 else j
out.append(" " * (j - i))
i = j
continue
out.append(ch)
i += 1
return "".join(out)
def check(path):
findings = []
lines = strip_comments(open(path, encoding="utf-8", errors="replace").read()).split("\n")
in_attempt = False
in_handle = False
for number, line in enumerate(lines, 1):
if ATTEMPT_OPEN.search(line):
in_attempt = True
continue
if CLEANUP_OPEN.search(line):
in_attempt = False
if HANDLE_OPEN.search(line):
in_handle = True
continue
if FINISH_ANY.search(line):
in_handle = False
continue
if in_attempt and RETURN_MACRO.search(line):
findings.append(
(number, "a *_RETURN macro inside an ATTEMPT block returns past CLEANUP; "
"use the matching *_BREAK variant"))
if in_handle and BARE_RETURN.search(line):
findings.append(
(number, "returning from inside a HANDLE block skips the RELEASE_ERROR that "
"FINISH carries; set a flag and act on it after FINISH"))
return findings
def sources(targets):
for target in targets:
if os.path.isdir(target):
for root, _dirs, files in os.walk(target):
for name in sorted(files):
if name.endswith(".c") and not name.endswith("~"):
yield os.path.join(root, name)
else:
yield target
def main(argv):
if len(argv) < 2:
print(__doc__, file=sys.stderr)
return 1
total = 0
for path in sources(argv[1:]):
for number, message in check(path):
print("%s:%d: %s" % (path, number, message), file=sys.stderr)
total += 1
if total:
print("", file=sys.stderr)
print("check_error_protocol: %d violation(s). See the Error-Handling Protocol "
"section of AGENTS.md." % total, file=sys.stderr)
return 1
print("check_error_protocol: clean")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))

View File

@@ -24,7 +24,7 @@ Usage:
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$)
--exclude-test REGEX CTest regex to exclude (default: none)
--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)
@@ -327,8 +327,10 @@ def main():
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$)")
# No default exclusion. This was "^character$" while that suite was
# believed to fail deliberately; it does not, and did not.
ap.add_argument("--exclude-test", default="",
help="CTest regex to exclude (default: none)")
ap.add_argument("--threshold", type=float, default=0.0)
ap.add_argument("--junit", default=None,
help="write a JUnit XML report to this path")

View File

@@ -234,7 +234,7 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
ATTEMPT {
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
CATCH(errctx, actor_visible(obj, camera, &visible));
CATCH(errctx, actor_visible(obj, akgl_camera, &visible));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_KEY) {
@@ -256,7 +256,7 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
ATTEMPT {
CATCH(errctx,
akgl_sprite_sheet_coords_for_frame(
akgl_spritesheet_coords_for_frame(
curSprite,
&src,
obj->curSpriteFrameId)
@@ -266,16 +266,16 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
} FINISH(errctx, true);
if ( obj->parent != NULL ) {
dest.x = (obj->parent->x + obj->x - camera->x);
dest.y = (obj->parent->y + obj->y - camera->y);
dest.x = (obj->parent->x + obj->x - akgl_camera->x);
dest.y = (obj->parent->y + obj->y - akgl_camera->y);
} else {
dest.x = (obj->x - camera->x);
dest.y = (obj->y - camera->y);
dest.x = (obj->x - akgl_camera->x);
dest.y = (obj->y - akgl_camera->y);
}
dest.w = curSprite->width * obj->scale;
dest.h = curSprite->width * obj->scale;
dest.h = curSprite->height * obj->scale;
PASS(errctx, renderer->draw_texture(renderer, curSprite->sheet->texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, curSprite->sheet->texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
SUCCEED_RETURN(errctx);
}
@@ -316,7 +316,7 @@ void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, cons
CATCH(errctx, obj->updatefunc(obj));
}
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE)) {
CATCH(errctx, akgl_tilemap_scale_actor(gamemap, obj));
CATCH(errctx, akgl_tilemap_scale_actor(akgl_gamemap, obj));
} else {
obj->scale = 1.0;
}
@@ -328,7 +328,7 @@ void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, cons
} FINISH_NORETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event)
akerr_ErrorContext *akgl_actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "actor");
@@ -342,7 +342,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event)
akerr_ErrorContext *akgl_actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -357,7 +357,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event)
akerr_ErrorContext *akgl_actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -371,7 +371,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event)
akerr_ErrorContext *akgl_actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -386,7 +386,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SD
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event)
akerr_ErrorContext *akgl_actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -400,7 +400,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Ev
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event)
akerr_ErrorContext *akgl_actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -415,7 +415,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_E
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event)
akerr_ErrorContext *akgl_actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -429,7 +429,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event)
akerr_ErrorContext *akgl_actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");

View File

@@ -1,39 +1,59 @@
/**
* @file actor_state_string_names.c
* @brief Implements the actor state string names subsystem.
* @brief Bit position -> state name, as text. Maintained by hand.
*
* An earlier comment in `actor.h` said this file was "built by a utility script
* and not kept in git, see the Makefile for lib_src/actor_state_string_names.c".
* None of that is true: there is no Makefile (the project is CMake), no
* `lib_src/`, no such script under `scripts/` or `util/`, and this file is
* tracked. It is maintained by hand, and the two things that keeps costing are
* worth stating here rather than rediscovering.
*
* **Entry `i` must name the bit `1 << i` in `actor.h`.** Bits 11 and 12 read
* `UNDEFINED_11` and `UNDEFINED_12` here for as long as `actor.h` has called
* them `MOVING_IN` and `MOVING_OUT`, which meant a character JSON could not
* bind a sprite to either state -- the name it would have to write did not
* exist in the registry akgl_registry_init_actor_state_strings builds out of
* this array.
*
* **The array is sized by #AKGL_ACTOR_MAX_STATES**, not by a literal. It was
* `[32]` here against `[AKGL_ACTOR_MAX_STATES+1]` in the header, so a consumer
* trusting the declared bound read one entry past the object.
*/
char *AKGL_ACTOR_STATE_STRING_NAMES[32] = {
"AKGL_ACTOR_STATE_FACE_DOWN",
"AKGL_ACTOR_STATE_FACE_LEFT",
"AKGL_ACTOR_STATE_FACE_RIGHT",
"AKGL_ACTOR_STATE_FACE_UP",
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_DYING",
"AKGL_ACTOR_STATE_DEAD",
"AKGL_ACTOR_STATE_MOVING_LEFT",
"AKGL_ACTOR_STATE_MOVING_RIGHT",
"AKGL_ACTOR_STATE_MOVING_UP",
"AKGL_ACTOR_STATE_MOVING_DOWN",
"AKGL_ACTOR_STATE_UNDEFINED_11",
"AKGL_ACTOR_STATE_UNDEFINED_12",
"AKGL_ACTOR_STATE_UNDEFINED_13",
"AKGL_ACTOR_STATE_UNDEFINED_14",
"AKGL_ACTOR_STATE_UNDEFINED_15",
"AKGL_ACTOR_STATE_UNDEFINED_16",
"AKGL_ACTOR_STATE_UNDEFINED_17",
"AKGL_ACTOR_STATE_UNDEFINED_18",
"AKGL_ACTOR_STATE_UNDEFINED_19",
"AKGL_ACTOR_STATE_UNDEFINED_20",
"AKGL_ACTOR_STATE_UNDEFINED_21",
"AKGL_ACTOR_STATE_UNDEFINED_22",
"AKGL_ACTOR_STATE_UNDEFINED_23",
"AKGL_ACTOR_STATE_UNDEFINED_24",
"AKGL_ACTOR_STATE_UNDEFINED_25",
"AKGL_ACTOR_STATE_UNDEFINED_26",
"AKGL_ACTOR_STATE_UNDEFINED_27",
"AKGL_ACTOR_STATE_UNDEFINED_28",
"AKGL_ACTOR_STATE_UNDEFINED_29",
"AKGL_ACTOR_STATE_UNDEFINED_30",
"AKGL_ACTOR_STATE_UNDEFINED_31",
#include <akgl/actor.h>
char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES] = {
"AKGL_ACTOR_STATE_FACE_DOWN", // 1 << 0
"AKGL_ACTOR_STATE_FACE_LEFT", // 1 << 1
"AKGL_ACTOR_STATE_FACE_RIGHT", // 1 << 2
"AKGL_ACTOR_STATE_FACE_UP", // 1 << 3
"AKGL_ACTOR_STATE_ALIVE", // 1 << 4
"AKGL_ACTOR_STATE_DYING", // 1 << 5
"AKGL_ACTOR_STATE_DEAD", // 1 << 6
"AKGL_ACTOR_STATE_MOVING_LEFT", // 1 << 7
"AKGL_ACTOR_STATE_MOVING_RIGHT", // 1 << 8
"AKGL_ACTOR_STATE_MOVING_UP", // 1 << 9
"AKGL_ACTOR_STATE_MOVING_DOWN", // 1 << 10
"AKGL_ACTOR_STATE_MOVING_IN", // 1 << 11
"AKGL_ACTOR_STATE_MOVING_OUT", // 1 << 12
"AKGL_ACTOR_STATE_UNDEFINED_13", // 1 << 13
"AKGL_ACTOR_STATE_UNDEFINED_14", // 1 << 14
"AKGL_ACTOR_STATE_UNDEFINED_15", // 1 << 15
"AKGL_ACTOR_STATE_UNDEFINED_16", // 1 << 16
"AKGL_ACTOR_STATE_UNDEFINED_17", // 1 << 17
"AKGL_ACTOR_STATE_UNDEFINED_18", // 1 << 18
"AKGL_ACTOR_STATE_UNDEFINED_19", // 1 << 19
"AKGL_ACTOR_STATE_UNDEFINED_20", // 1 << 20
"AKGL_ACTOR_STATE_UNDEFINED_21", // 1 << 21
"AKGL_ACTOR_STATE_UNDEFINED_22", // 1 << 22
"AKGL_ACTOR_STATE_UNDEFINED_23", // 1 << 23
"AKGL_ACTOR_STATE_UNDEFINED_24", // 1 << 24
"AKGL_ACTOR_STATE_UNDEFINED_25", // 1 << 25
"AKGL_ACTOR_STATE_UNDEFINED_26", // 1 << 26
"AKGL_ACTOR_STATE_UNDEFINED_27", // 1 << 27
"AKGL_ACTOR_STATE_UNDEFINED_28", // 1 << 28
"AKGL_ACTOR_STATE_UNDEFINED_29", // 1 << 29
"AKGL_ACTOR_STATE_UNDEFINED_30", // 1 << 30
"AKGL_ACTOR_STATE_UNDEFINED_31", // 1 << 31
};

View File

@@ -24,10 +24,9 @@ akerr_ErrorContext *akgl_load_start_bgm(char *fname)
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
//SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
SDL_Log("Loading music asset from %s", fname);
bgm = MIX_LoadAudio(akgl_mixer, fname, true);
FAIL_ZERO_BREAK(errctx, bgm, AKERR_NULLPOINTER, "Failed to load music asset %s : %s", fname, SDL_GetError());
akgl_bgm = MIX_LoadAudio(akgl_mixer, fname, true);
FAIL_ZERO_BREAK(errctx, akgl_bgm, AKERR_NULLPOINTER, "Failed to load music asset %s : %s", fname, SDL_GetError());
bgmtrack = MIX_CreateTrack(akgl_mixer);
FAIL_ZERO_BREAK(errctx, bgmtrack, AKERR_NULLPOINTER, "Failed to create audio track for background music: %s", SDL_GetError());
@@ -36,7 +35,7 @@ akerr_ErrorContext *akgl_load_start_bgm(char *fname)
FAIL_ZERO_BREAK(
errctx,
MIX_SetTrackAudio(bgmtrack, bgm),
MIX_SetTrackAudio(bgmtrack, akgl_bgm),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
@@ -49,8 +48,8 @@ akerr_ErrorContext *akgl_load_start_bgm(char *fname)
} CLEANUP {
//IGNORE(akgl_heap_release_string(tmpstr));
if ( errctx != NULL ) {
if ( errctx->status != 0 && bgm != NULL) {
MIX_DestroyAudio(bgm);
if ( errctx->status != 0 && akgl_bgm != NULL) {
MIX_DestroyAudio(akgl_bgm);
}
}
} PROCESS(errctx) {

View File

@@ -283,7 +283,7 @@ static void SDLCALL audio_stream_callback(void *userdata, SDL_AudioStream *strea
}
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void)
akerr_ErrorContext *akgl_audio_init(void)
{
SDL_AudioSpec spec;
@@ -322,7 +322,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void)
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void)
akerr_ErrorContext *akgl_audio_shutdown(void)
{
SDL_AudioStream *closing = audiostream;
@@ -337,7 +337,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void)
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms)
akerr_ErrorContext *akgl_audio_tone(int voice, float32_t hz, uint32_t ms)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
@@ -350,7 +350,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
akerr_ErrorContext *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
@@ -368,7 +368,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_sweep(int voice, float32_t from_hz
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice)
akerr_ErrorContext *akgl_audio_stop(int voice)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
@@ -382,7 +382,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice)
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform)
akerr_ErrorContext *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
@@ -401,7 +401,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWave
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release)
akerr_ErrorContext *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
@@ -422,7 +422,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attac
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level)
akerr_ErrorContext *akgl_audio_volume(float32_t level)
{
PREPARE_ERROR(errctx);
FAIL_NONZERO_RETURN(
@@ -439,7 +439,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level)
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active)
akerr_ErrorContext *akgl_audio_voice_active(int voice, bool *active)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
@@ -452,7 +452,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *acti
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames)
akerr_ErrorContext *akgl_audio_mix(float32_t *dest, int frames)
{
akgl_AudioVoice *voice = NULL;
float32_t sum = 0.0f;

View File

@@ -24,7 +24,7 @@ akerr_ErrorContext *akgl_character_initialize(akgl_Character *obj, char *name)
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL akgl_Character reference");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL name string pointer");
memset(obj, 0x00, sizeof(akgl_Character));
strncpy(obj->name, name, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH);
strncpy(obj->name, name, AKGL_CHARACTER_MAX_NAME_LENGTH);
obj->state_sprites = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, obj->state_sprites, AKERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map");
@@ -44,19 +44,42 @@ akerr_ErrorContext *akgl_character_sprite_add(akgl_Character *basechar, akgl_Spr
{
PREPARE_ERROR(errctx);
char stateval[32];
akgl_Sprite *displaced = NULL;
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
FAIL_ZERO_RETURN(errctx, ref, AKERR_NULLPOINTER, "NULL sprite reference");
memset(&stateval, 0x00, 32);
SDL_itoa(state, (char *)&stateval, 10);
SDL_SetPointerProperty(basechar->state_sprites, (char *)&stateval, ref);
SDL_Log("Added sprite %s to character %s for state %b", (char *)&ref->name, (char *)&basechar->name, state);
// Whatever this state was bound to is losing its binding, and the reference
// taken for that binding with it. Without this a character that rebinds a
// state while alive leaks one sprite slot per rebind -- releasing at
// teardown only gives back whatever the map happens to hold at the end.
displaced = (akgl_Sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL);
// Take the new reference before anything can fail, so a failed write cannot
// leave the sprite bound with no reference behind it.
ref->refcount += 1;
if ( SDL_SetPointerProperty(basechar->state_sprites, (char *)&stateval, ref) == false ) {
ref->refcount -= 1;
FAIL_RETURN(
errctx,
AKERR_KEY,
"Unable to bind sprite %s to character %s for state %d: %s",
(char *)&ref->name,
(char *)&basechar->name,
state,
SDL_GetError());
}
if ( (displaced != NULL) && (displaced != ref) ) {
PASS(errctx, akgl_heap_release_sprite(displaced));
}
SDL_Log("Added sprite %s to character %s for state %b", (char *)&ref->name, (char *)&basechar->name, state);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest)
{
akgl_Sprite *target = NULL;
PREPARE_ERROR(errctx);
char stateval[32];
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer to sprite pointer (**dest)");
@@ -65,14 +88,12 @@ akerr_ErrorContext *akgl_character_sprite_get(akgl_Character *basechar, int stat
SDL_itoa(state, (char *)&stateval, 10);
*dest = (akgl_Sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL);
FAIL_ZERO_RETURN(errctx, *dest, AKERR_KEY, "Sprite for state %d (%b) not found in the character's registry", state, state);
target = *dest;
//SDL_Log("Sprite state %d (%s) has character %s", state, (char *)&stateval, target->name);
SUCCEED_RETURN(errctx);
}
// SDL iterator so we can't return error information here, void only
// this means we don't have anywhere to send exceptions up to, so if we hit an error, we log and exit(1) here
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID registry, const char *name)
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name)
{
PREPARE_ERROR(errctx);
akgl_Sprite *spriteptr;
@@ -80,7 +101,7 @@ void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID regis
ATTEMPT {
FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "Character state sprite iterator received null iterator op pointer");
FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "Character state sprite iterator received null sprite name");
spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(registry, name, NULL);
spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(props, name, NULL);
FAIL_ZERO_BREAK(errctx, spriteptr, AKERR_NULLPOINTER, "Character state sprite for %s not found", name);
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RELEASE) ) {
CATCH(errctx, akgl_heap_release_sprite(spriteptr));
@@ -114,14 +135,14 @@ void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID regis
* @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_t *states, int *dest)
static akerr_ErrorContext *character_load_json_state_int_from_strings(json_t *states, int *dest)
{
int i = 0;
long newstate = 0;
akgl_String *tmpstring = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL states array");
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL destination integer");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination integer");
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpstring));
@@ -161,7 +182,7 @@ static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_
* @throws AKERR_OUTOFBOUNDS If an array is indexed past its end.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Character *obj)
static akerr_ErrorContext *character_load_json_inner(json_t *json, akgl_Character *obj)
{
PREPARE_ERROR(errctx);
json_t *mappings = NULL;
@@ -189,7 +210,7 @@ static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Cha
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr2));
CATCH(errctx, akgl_get_json_array_value((json_t *)curmapping, "state", &statearray));
CATCH(errctx, akgl_character_load_json_state_int_from_strings(statearray, &stateval));
CATCH(errctx, character_load_json_state_int_from_strings(statearray, &stateval));
CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
FAIL_ZERO_BREAK(
@@ -228,7 +249,6 @@ akerr_ErrorContext *akgl_character_load_json(char *filename)
CATCH(errctx, akgl_heap_next_character(&obj));
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
//SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
json = (json_t *)json_load_file(filename, 0, &error);
FAIL_ZERO_BREAK(
errctx,
@@ -236,9 +256,9 @@ akerr_ErrorContext *akgl_character_load_json(char *filename)
AKERR_NULLPOINTER,
"Error while loading character from %s on line %d: %s", filename, error.line, error.text
);
CATCH(errctx, akgl_character_load_json_inner(json, obj));
CATCH(errctx, character_load_json_inner(json, obj));
CATCH(errctx, akgl_get_json_integer_value(json, "speedtime", (int *)&obj->speedtime));
obj->speedtime = obj->speedtime * AKGL_TIME_ONESEC_MS;
obj->speedtime = obj->speedtime * AKGL_TIME_ONEMS_NS;
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));

View File

@@ -10,7 +10,7 @@
#include <akgl/game.h>
#include <akgl/controller.h>
akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
akgl_ControlMap akgl_controlmaps[AKGL_MAX_CONTROL_MAPS];
/*
* Keystrokes waiting for akgl_controller_poll_key() and
@@ -182,13 +182,13 @@ static bool keybuffer_take(akgl_Keystroke *dest)
return true;
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
akerr_ErrorContext *akgl_controller_list_keyboards(void)
{
int count;
SDL_KeyboardID *keyboards = SDL_GetKeyboards(&count);
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(e, keyboards, AKERR_NULLPOINTER, "%s", SDL_GetError());
FAIL_ZERO_RETURN(errctx, keyboards, AKERR_NULLPOINTER, "%s", SDL_GetError());
// The array is SDL's to allocate and ours to free -- that is the contract on
// every SDL_Get*s() enumeration. Released in CLEANUP so the loop can fail
@@ -201,36 +201,58 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
} CLEANUP {
SDL_free(keyboards);
keyboards = NULL;
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void)
akerr_ErrorContext *akgl_controller_open_gamepads(void)
{
int count = 0;
int i = 0;
SDL_JoystickID *gamepads = NULL;
SDL_Gamepad *gamepad = NULL;
bool openfailed = false;
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
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);
// SDL_free in CLEANUP, not after the loop. A gamepad that failed to
// open used to return straight out of here and take the enumeration
// array with it.
ATTEMPT {
FAIL_ZERO_BREAK(errctx, gamepads, AKERR_NULLPOINTER, "%s", SDL_GetError());
for ( i = 0; i < count ; i++ ) {
gamepad = SDL_OpenGamepad(gamepads[i]);
if ( gamepad == NULL ) {
openfailed = true;
break;
}
SDL_Log("Gamepad %d is %s", i, SDL_GetGamepadNameForID(gamepads[i]));
}
// Reported after the loop rather than inside it: a
// FAIL_ZERO_BREAK in there would break the loop, not the block.
FAIL_NONZERO_BREAK(errctx, openfailed, AKERR_NULLPOINTER, "%s", SDL_GetError());
} CLEANUP {
if ( gamepads != NULL ) {
SDL_free(gamepads);
gamepads = NULL;
}
} PROCESS(errctx) {
} FINISH(errctx, true);
} else {
// SDL_GetGamepads allocates even when it reports none.
if ( gamepads != NULL ) {
SDL_free(gamepads);
gamepads = NULL;
}
SDL_Log("No gamepads enumerated");
}
} else {
SDL_Log("No gamepads connected");
}
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *event)
@@ -258,7 +280,7 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
ATTEMPT {
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
curmap = &GAME_ControlMaps[i];
curmap = &akgl_controlmaps[i];
if ( curmap->target == NULL ) {
continue;
}
@@ -279,13 +301,6 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
event->key.which == curmap->kbid &&
event->key.key == curcontrol->key)
);
if ( event->type == 768 && event->key.which == 11 && event->key.key == 13 ) {
SDL_Log("Event type=%d, keyboard=%d, key=%d", event->type, event->key.which, event->key.key);
SDL_Log("ControlMap[%d].Controls[%d] keyboard=%d, key=%d", i, j, curmap->kbid, curcontrol->key);
SDL_Log("event %d -> control on %d off %d", event->type, curcontrol->event_on, curcontrol->event_off);
SDL_Log("eventButtonComboMatch for controlmap %d id %d = %d",
i, j, eventButtonComboMatch);
}
if ( event->type == curcontrol->event_on && eventButtonComboMatch) {
CATCH(errctx, curcontrol->handler_on(curmap->target, event));
goto _akgl_controller_handle_event_success;
@@ -302,32 +317,7 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
SUCCEED_RETURN(errctx);
}
/**
* @brief Set the movement and facing bits on the "player" actor for a D-pad or arrow press.
*
* A whole-application shortcut rather than a control-map binding: it looks the
* actor up by the literal name `"player"` in #AKGL_REGISTRY_ACTOR instead of
* being told which actor to act on, so it only ever drives one. The
* akgl_Actor_cmhf_* handlers are the general form.
*
* Facing follows movement unless the actor sets `movement_controls_face`, which
* is how an actor that aims independently of the direction it walks opts out.
*
* Declared nowhere -- `controller.h` advertises this as
* `akgl_controller_handle_button_down`, which does not exist. TODO.md, "Known
* and still open" item 10.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only;
* never read.
* @param event The button or key event. Required. Both the gamepad and
* keyboard unions are read on every call, so the arm that does
* not correspond to the event type is read as garbage -- which
* works only because no real button and keycode pair collides.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
akerr_ErrorContext *akgl_controller_handle_button_down(void *appstate, SDL_Event *event)
{
akgl_Actor *player = NULL;
@@ -378,23 +368,7 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
SUCCEED_RETURN(errctx);
}
/**
* @brief Clear the movement bit on the "player" actor for a D-pad or arrow release, and reset its animation.
*
* The counterpart to gamepad_handle_button_down. It clears only the movement
* bit, leaving the facing bits alone so the actor keeps looking the way it was
* walking, and rewinds `curSpriteFrameId` to 0 so the next step starts from the
* first frame of the walk cycle rather than wherever it stopped.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The button or key release event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
akerr_ErrorContext *akgl_controller_handle_button_up(void *appstate, SDL_Event *event)
{
akgl_Actor *player = NULL;
@@ -433,31 +407,7 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
SUCCEED_RETURN(errctx);
}
/**
* @brief Open a gamepad that has just been plugged in, and log its mapping.
*
* SDL delivers no button events from an unopened gamepad, so a device that
* appears mid-session has to be opened here the way akgl_controller_open_gamepads
* opens the ones present at startup. A gamepad SDL has already opened is logged
* and left alone.
*
* The mapping is logged because a controller with no entry in the database
* produces no button events at all, and that is otherwise indistinguishable
* from a broken binding.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_ADDED event. Required. The joystick id
* is read out of the `gbutton` arm rather than `gdevice`, which
* works because the two share their `which` field.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*
* @note A failed `SDL_OpenGamepad` is logged rather than reported: this returns
* success and the device stays silent.
*/
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
akerr_ErrorContext *akgl_controller_handle_added(void *appstate, SDL_Event *event)
{
SDL_JoystickID which;
SDL_Gamepad *gamepad = NULL;
@@ -487,22 +437,7 @@ akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
SUCCEED_RETURN(errctx);
}
/**
* @brief Close a gamepad that has been unplugged.
*
* An unplugged device SDL still has open is a leaked handle, so this closes it.
* A removal for a device that was never opened is logged and otherwise ignored.
* Any control map still holding that `jsid` simply stops matching -- the map is
* not torn down.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_REMOVED event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*/
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
akerr_ErrorContext *akgl_controller_handle_removed(void *appstate, SDL_Event *event)
{
SDL_JoystickID which;
SDL_Gamepad *gamepad = NULL;
@@ -521,24 +456,24 @@ akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control)
akerr_ErrorContext *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;
FAIL_ZERO_BREAK(errctx, control, AKERR_NULLPOINTER, "NULL Control");
FAIL_NONZERO_BREAK(errctx, ((controlmapid < 0) || (controlmapid >= AKGL_MAX_CONTROL_MAPS)), AKERR_OUTOFBOUNDS, "Control map id %d is outside 0..%d", controlmapid, (AKGL_MAX_CONTROL_MAPS - 1));
newmapid = akgl_controlmaps[controlmapid].nextMap;
FAIL_ZERO_BREAK(errctx, (AKGL_MAX_CONTROLS - newmapid), AKERR_OUTOFBOUNDS, "Control map ID %d is full", controlmapid);
memcpy((void *)&akgl_controlmaps[controlmapid].controls[newmapid], control, sizeof(akgl_Control));
akgl_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)
akerr_ErrorContext *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid)
{
akgl_ControlMap *controlmap;
akgl_Control control;
@@ -546,9 +481,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
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);
FAIL_NONZERO_BREAK(errctx, ((controlmapid < 0) || (controlmapid >= AKGL_MAX_CONTROL_MAPS)), AKERR_OUTOFBOUNDS, "Control map id %d is outside 0..%d", controlmapid, (AKGL_MAX_CONTROL_MAPS - 1));
memset((void *)&control, 0x00, sizeof(akgl_Control));
controlmap = &GAME_ControlMaps[controlmapid];
controlmap = &akgl_controlmaps[controlmapid];
controlmap->kbid = kbid;
controlmap->jsid = jsid;
@@ -561,32 +496,32 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
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;
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;
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;
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;
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;
@@ -595,42 +530,43 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
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;
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;
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;
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;
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);
// The SUCCEED_RETURN used to sit at the end of the ATTEMPT block, which
// returned past CLEANUP and left this function with no return statement at
// all on the path that falls out of FINISH.
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available)
akerr_ErrorContext *akgl_controller_poll_key(int *keycode, bool *available)
{
akgl_Keystroke keystroke;
@@ -655,7 +591,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available)
akerr_ErrorContext *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL keystroke destination");
@@ -666,7 +602,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void)
akerr_ErrorContext *akgl_controller_flush_keys(void)
{
PREPARE_ERROR(errctx);
keybuffer_head = 0;

View File

@@ -28,32 +28,6 @@ typedef struct {
*/
static FloodSpan floodspans[AKGL_DRAW_MAX_FLOOD_SPANS];
/* Draw a Gimpish background pattern to show transparency in the image */
void akgl_draw_background(int w, int h)
{
SDL_Color col[2] = {
{ 0x66, 0x66, 0x66, 0xff },
{ 0x99, 0x99, 0x99, 0xff },
};
int i, x, y;
SDL_FRect rect;
const int dx = 8, dy = 8;
rect.w = (float)dx;
rect.h = (float)dy;
for (y = 0; y < h; y += dy) {
for (x = 0; x < w; x += dx) {
/* use an 8x8 checkerboard pattern */
i = (((x ^ y) >> 3) & 1);
SDL_SetRenderDrawColor(renderer->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a);
rect.x = (float)x;
rect.y = (float)y;
SDL_RenderFillRect(renderer->sdl_renderer, &rect);
}
}
}
/**
* @brief Remember the renderer's draw color and replace it with @p color.
*
@@ -116,6 +90,63 @@ static akerr_ErrorContext *pop_draw_color(akgl_RenderBackend *self, SDL_Color *p
SUCCEED_RETURN(errctx);
}
/* Draw a Gimpish background pattern to show transparency in the image */
akerr_ErrorContext *akgl_draw_background(akgl_RenderBackend *self, int w, int h)
{
PREPARE_ERROR(errctx);
SDL_Color col[2] = {
{ 0x66, 0x66, 0x66, 0xff },
{ 0x99, 0x99, 0x99, 0xff },
};
SDL_Color previous;
SDL_FRect rect;
const int dx = 8, dy = 8;
bool pushed = false;
bool drawfailed = false;
int i = 0;
int x = 0;
int y = 0;
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
rect.w = (float)dx;
rect.h = (float)dy;
ATTEMPT {
CATCH(errctx, push_draw_color(self, col[0], &previous));
pushed = true;
// The two SDL calls are checked through a flag rather than with
// FAIL_ZERO_BREAK. Inside these loops a `break` would leave the loop,
// not the ATTEMPT block, and the fill would carry on with the failure
// unnoticed -- the hazard AGENTS.md describes for CATCH inside a loop.
for ( y = 0; (y < h) && (drawfailed == false); y += dy ) {
for ( x = 0; x < w; x += dx ) {
/* use an 8x8 checkerboard pattern */
i = (((x ^ y) >> 3) & 1);
if ( SDL_SetRenderDrawColor(self->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a) == false ) {
drawfailed = true;
break;
}
rect.x = (float)x;
rect.y = (float)y;
if ( SDL_RenderFillRect(self->sdl_renderer, &rect) == false ) {
drawfailed = true;
break;
}
}
}
FAIL_NONZERO_BREAK(errctx, drawfailed, AKGL_ERR_SDL, "%s", SDL_GetError());
} CLEANUP {
if ( pushed == true ) {
IGNORE(pop_draw_color(self, &previous));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Fill the four-connected region of @p oldpixel around a seed pixel.
*
@@ -236,7 +267,7 @@ static akerr_ErrorContext *flood_region(SDL_Surface *surface, int x, int y, uint
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color)
akerr_ErrorContext *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color)
{
SDL_Color previous;
@@ -258,7 +289,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, flo
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color)
akerr_ErrorContext *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color)
{
SDL_Color previous;
@@ -280,7 +311,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, floa
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
akerr_ErrorContext *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
{
SDL_Color previous;
@@ -303,7 +334,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
akerr_ErrorContext *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
{
SDL_Color previous;
@@ -326,7 +357,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *sel
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color)
akerr_ErrorContext *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color)
{
SDL_Color previous;
SDL_FPoint octants[8];
@@ -393,7 +424,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, fl
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color)
akerr_ErrorContext *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color)
{
SDL_Surface *target = NULL;
SDL_Surface *rgba = NULL;
@@ -481,7 +512,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest)
akerr_ErrorContext *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest)
{
SDL_Surface *saved = NULL;
int width = 0;
@@ -551,7 +582,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *sel
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y)
akerr_ErrorContext *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y)
{
SDL_Texture *patch = NULL;
SDL_FRect dest;

View File

@@ -13,6 +13,7 @@
#include <akstdlib.h>
#include <akgl/game.h>
#include <akgl/controller.h>
#include <akgl/tilemap.h>
#include <akgl/sprite.h>
@@ -24,24 +25,24 @@
#include <akgl/error.h>
#include <akgl/SDL_GameControllerDB.h>
SDL_Window *window = NULL;
SDL_Window *akgl_window = NULL;
// Currently active objects
akgl_RenderBackend *renderer;
akgl_PhysicsBackend *physics;
SDL_FRect *camera;
akgl_Tilemap *gamemap;
akgl_RenderBackend *akgl_renderer;
akgl_PhysicsBackend *akgl_physics;
SDL_FRect *akgl_camera;
akgl_Tilemap *akgl_gamemap;
// Default objects
akgl_RenderBackend _akgl_renderer;
akgl_PhysicsBackend _akgl_physics;
SDL_FRect _akgl_camera;
akgl_Tilemap _akgl_gamemap;
akgl_RenderBackend akgl_default_renderer;
akgl_PhysicsBackend akgl_default_physics;
SDL_FRect akgl_default_camera;
akgl_Tilemap akgl_default_gamemap;
MIX_Audio *bgm = NULL;
MIX_Audio *akgl_bgm = NULL;
MIX_Mixer *akgl_mixer = NULL;
MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
akgl_Game game;
akgl_Game akgl_game;
static akerr_ErrorContext *write_exact(const void *ptr, size_t size, size_t nmemb, FILE *fp)
{
@@ -50,6 +51,20 @@ static akerr_ErrorContext *write_exact(const void *ptr, size_t size, size_t nmem
return aksl_fwrite(ptr, size, nmemb, fp, &transferred);
}
/*
* The four savegame name tables are written and read at each object's own
* maximum name length -- AKGL_ACTOR_MAX_NAME_LENGTH for actors,
* AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH for spritesheets, and so on.
*
* **The writer and the reader have to name the same constant per table.** The
* tables carry no length prefix, so the reader finds each next entry by
* stepping that many bytes. They disagreed until 0.5.0: the writer used each
* object's own length and the reader used AKGL_ACTOR_MAX_NAME_LENGTH for all
* four. The other three are 128 as well, so only the spritesheet table was
* wrong -- and that was enough. See the EOF check at the end of akgl_game_load,
* which is what turns a disagreement into an error instead of a corruption.
*/
/**
* @brief Widest name field any of the save tables writes.
*
@@ -69,7 +84,7 @@ typedef char akgl_game_save_name_field_fits[
((AKGL_ACTOR_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
(AKGL_SPRITE_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
(AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
(AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD)) ? 1 : -1];
(AKGL_CHARACTER_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD)) ? 1 : -1];
/**
* @brief Write a registry key as a fixed-width, zero-padded field.
@@ -99,11 +114,11 @@ static akerr_ErrorContext *write_name_field(const char *name, size_t width, FILE
{
char field[AKGL_GAME_SAVE_MAX_NAME_FIELD];
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL name");
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL name");
FAIL_ZERO_RETURN(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
FAIL_NONZERO_RETURN(
e,
errctx,
(width > sizeof(field)),
AKERR_OUTOFBOUNDS,
"Name field width %zu exceeds the staging buffer (%zu)",
@@ -112,8 +127,8 @@ static akerr_ErrorContext *write_name_field(const char *name, size_t width, FILE
memset(&field, 0x00, sizeof(field));
strncpy((char *)&field, name, (width - 1));
PASS(e, write_exact((char *)&field, 1, width, fp));
SUCCEED_RETURN(e);
PASS(errctx, write_exact((char *)&field, 1, width, fp));
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *read_exact(void *ptr, size_t size, size_t nmemb, FILE *fp)
@@ -125,50 +140,47 @@ static akerr_ErrorContext *read_exact(void *ptr, size_t size, size_t nmemb, FILE
void akgl_game_lowfps(void)
{
SDL_Log("Low FPS! %d", game.fps);
SDL_Log("Low FPS! %d", akgl_game.fps);
return;
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
akerr_ErrorContext *akgl_game_init(void)
{
int screenwidth = 0;
int screenheight = 0;
int i = 0;
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
// First, before anything that can raise: everything below reports through
// AKGL_ERR_* codes, and a code raised before its name is registered prints
// as "Unknown Error" in the stack trace the caller is left holding.
PASS(e, akgl_error_init());
strncpy((char *)&game.libversion, AKGL_VERSION, 32);
game.gameStartTime = SDL_GetTicksNS();
game.lastIterTime = game.gameStartTime;
game.lastFPSTime = game.gameStartTime;
game.lowfpsfunc = &akgl_game_lowfps;
game.statelock = SDL_CreateMutex();
FAIL_ZERO_RETURN(e, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
PASS(e, akgl_game_state_lock());
FAIL_ZERO_RETURN(e, strlen((char *)&game.name), AKERR_NULLPOINTER, "Must provide game name");
FAIL_ZERO_RETURN(e, strlen((char *)&game.version), AKERR_NULLPOINTER, "Must provide game version");
FAIL_ZERO_RETURN(e, strlen((char *)&game.uri), AKERR_NULLPOINTER, "Must provide game uri");
PASS(e, akgl_heap_init());
PASS(e, akgl_registry_init_actor());
PASS(e, akgl_registry_init_sprite());
PASS(e, akgl_registry_init_spritesheet());
PASS(e, akgl_registry_init_character());
PASS(e, akgl_registry_init_font());
PASS(e, akgl_registry_init_music());
PASS(e, akgl_registry_init_properties());
PASS(e, akgl_registry_init_actor_state_strings());
PASS(errctx, akgl_error_init());
strncpy((char *)&akgl_game.libversion, AKGL_VERSION, 32);
akgl_game.gameStartTime = SDL_GetTicksNS();
akgl_game.lastIterTime = akgl_game.gameStartTime;
akgl_game.lastFPSTime = akgl_game.gameStartTime;
akgl_game.lowfpsfunc = &akgl_game_lowfps;
akgl_game.statelock = SDL_CreateMutex();
FAIL_ZERO_RETURN(errctx, akgl_game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
PASS(errctx, akgl_game_state_lock());
FAIL_ZERO_RETURN(errctx, strlen((char *)&akgl_game.name), AKERR_NULLPOINTER, "Must provide game name");
FAIL_ZERO_RETURN(errctx, strlen((char *)&akgl_game.version), AKERR_NULLPOINTER, "Must provide game version");
FAIL_ZERO_RETURN(errctx, strlen((char *)&akgl_game.uri), AKERR_NULLPOINTER, "Must provide game uri");
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_registry_init_actor());
PASS(errctx, akgl_registry_init_sprite());
PASS(errctx, akgl_registry_init_spritesheet());
PASS(errctx, akgl_registry_init_character());
PASS(errctx, akgl_registry_init_font());
PASS(errctx, akgl_registry_init_music());
PASS(errctx, akgl_registry_init_properties());
PASS(errctx, akgl_registry_init_actor_state_strings());
SDL_SetAppMetadata(game.name, game.version, game.uri);
SDL_SetAppMetadata(akgl_game.name, akgl_game.version, akgl_game.uri);
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap));
memset(&akgl_controlmaps[i], 0x00, sizeof(akgl_ControlMap));
}
FAIL_ZERO_RETURN(
e,
errctx,
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO),
AKGL_ERR_SDL,
"Couldn't initialize SDL: %s",
@@ -178,77 +190,97 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
for ( i = 0; i < AKGL_SDL_GAMECONTROLLER_DB_LEN ; i++ ) {
if ( SDL_AddGamepadMapping(SDL_GAMECONTROLLER_DB[i]) == -1 ) {
FAIL_ZERO_RETURN(e, 0, AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_RETURN(errctx, 0, AKGL_ERR_SDL, "%s", SDL_GetError());
}
}
PASS(e, akgl_controller_open_gamepads());
PASS(errctx, akgl_controller_open_gamepads());
FAIL_ZERO_RETURN(
e,
errctx,
MIX_Init(),
AKGL_ERR_SDL,
"Couldn't initialize audio: %s",
SDL_GetError());
akgl_mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, 0);
FAIL_ZERO_RETURN(
e,
errctx,
akgl_mixer,
AKGL_ERR_SDL,
"Unable to create mixer device: %s",
SDL_GetError());
FAIL_ZERO_RETURN(
e,
errctx,
TTF_Init(),
AKGL_ERR_SDL,
"Couldn't initialize front engine: %s",
SDL_GetError());
camera = &_akgl_camera;
renderer = &_akgl_renderer;
physics = &_akgl_physics;
gamemap = &_akgl_gamemap;
akgl_camera = &akgl_default_camera;
akgl_renderer = &akgl_default_renderer;
akgl_physics = &akgl_default_physics;
akgl_gamemap = &akgl_default_gamemap;
PASS(e, akgl_game_state_unlock());
SUCCEED_RETURN(e);
PASS(errctx, akgl_game_state_unlock());
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void)
akerr_ErrorContext *akgl_game_state_lock(void)
{
PREPARE_ERROR(e);
SDL_Time totaltime = 0;
PREPARE_ERROR(errctx);
// Milliseconds, which is what SDL_Delay takes. This used to count against
// AKGL_TIME_ONESEC_MS, a constant whose name said "one second in
// milliseconds" and whose value was a thousand times that, so the budget
// was roughly sixteen minutes rather than the one second documented.
Uint32 waitedms = 0;
while ( totaltime < AKGL_TIME_ONESEC_MS ) {
if ( SDL_TryLockMutex(game.statelock) == true ) {
SUCCEED_RETURN(e);
while ( waitedms < AKGL_GAME_STATE_LOCK_BUDGET_MS ) {
if ( SDL_TryLockMutex(akgl_game.statelock) == true ) {
SUCCEED_RETURN(errctx);
}
totaltime += 100;
SDL_Delay(100);
waitedms += AKGL_GAME_STATE_LOCK_RETRY_MS;
SDL_Delay(AKGL_GAME_STATE_LOCK_RETRY_MS);
}
FAIL_RETURN(e, AKGL_ERR_SDL, "%s", SDL_GetError());
// SDL_GetError() after a failed SDL_TryLockMutex is stale or empty -- the
// call reports contention by returning false, not by setting an error -- so
// say what actually happened and keep SDL's text as trailing context.
FAIL_RETURN(
errctx,
AKGL_ERR_SDL,
"Gave up waiting %d ms for the game state lock: %s",
AKGL_GAME_STATE_LOCK_BUDGET_MS,
SDL_GetError());
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void)
akerr_ErrorContext *akgl_game_state_unlock(void)
{
PREPARE_ERROR(e);
SDL_UnlockMutex(game.statelock);
SUCCEED_RETURN(e);
PREPARE_ERROR(errctx);
SDL_UnlockMutex(akgl_game.statelock);
SUCCEED_RETURN(errctx);
}
void akgl_game_updateFPS()
void akgl_game_update_fps(void)
{
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 ( (curTime - akgl_game.lastFPSTime) > AKGL_TIME_ONESEC_NS ) {
akgl_game.fps = akgl_game.framesSinceUpdate;
akgl_game.framesSinceUpdate = 0;
akgl_game.lastFPSTime = curTime;
}
if ( game.fps < 30 ) {
game.lowfpsfunc();
if ( akgl_game.fps < 30 ) {
// Install the default if nobody has. Only akgl_game_init sets this, and
// renderer.h documents the other path on purpose -- a host that owns
// its own window calls akgl_render_2d_bind instead. Such an embedder
// used to crash on frame one, because fps is 0 for the first second and
// 0 is under the threshold.
if ( akgl_game.lowfpsfunc == NULL ) {
akgl_game.lowfpsfunc = &akgl_game_lowfps;
}
akgl_game.lowfpsfunc();
}
game.framesSinceUpdate += 1;
game.lastIterTime = curTime;
akgl_game.framesSinceUpdate += 1;
akgl_game.lastIterTime = curTime;
}
/*
@@ -275,25 +307,25 @@ void akgl_game_updateFPS()
* **exits the process**. A failed write during a save terminates the
* game rather than returning an error.
*/
void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
static void save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
FILE *fp = (FILE *)userdata;
akgl_Actor *actor = NULL;
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
CATCH(e, write_name_field(name, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
CATCH(errctx, write_name_field(name, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
actor = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, write_exact(&actor, 1, sizeof(akgl_Actor *), fp));
CATCH(errctx, write_exact(&actor, 1, sizeof(akgl_Actor *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}
/**
* @brief Write one sprite's name and save-time address to the name table.
*
* As akgl_game_save_actorname_iterator, but the name field is
* As save_actorname_iterator, but the name field is
* #AKGL_SPRITE_MAX_NAME_LENGTH wide.
*
* @param userdata The open output stream, as a `FILE *`. Required.
@@ -301,27 +333,27 @@ void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, c
* @param name The sprite's registry key, written at fixed width.
*
* @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator.
* save_actorname_iterator.
*/
void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
static void save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
FILE *fp = (FILE *)userdata;
akgl_Sprite *sprite = NULL;
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
sprite = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, write_name_field(name, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
CATCH(e, write_exact(&sprite, 1, sizeof(akgl_Sprite *), fp));
CATCH(errctx, write_name_field(name, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
CATCH(errctx, write_exact(&sprite, 1, sizeof(akgl_Sprite *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}
/**
* @brief Write one spritesheet's name and save-time address to the name table.
*
* As akgl_game_save_actorname_iterator, but the name field is
* As save_actorname_iterator, but the name field is
* #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH wide -- the widest of the four, since
* a spritesheet is keyed by its resolved path.
*
@@ -330,78 +362,59 @@ void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props,
* @param name The spritesheet's registry key, written at fixed width.
*
* @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator.
* save_actorname_iterator.
* @note This is the table the loader disagrees with: it reads every table at
* #AKGL_ACTOR_MAX_NAME_LENGTH, so a save containing any spritesheet cannot
* be read back. TODO.md, "Known and still open" item 7.
*/
void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
static void save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
FILE *fp = (FILE *)userdata;
akgl_SpriteSheet *spritesheet = NULL;
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
spritesheet = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, write_name_field(name, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
CATCH(e, write_exact(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp));
CATCH(errctx, write_name_field(name, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
CATCH(errctx, write_exact(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}
/**
* @brief Write one character's name and save-time address to the name table.
*
* As akgl_game_save_actorname_iterator, but the name field is
* #AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH wide.
* As save_actorname_iterator, but the name field is
* #AKGL_CHARACTER_MAX_NAME_LENGTH wide.
*
* @param userdata The open output stream, as a `FILE *`. Required.
* @param props #AKGL_REGISTRY_CHARACTER, supplied by SDL.
* @param name The character's registry key, written at fixed width.
*
* @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator.
* save_actorname_iterator.
*/
void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name)
static void save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
FILE *fp = (FILE *)userdata;
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
akgl_Character *character = NULL;
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
character = SDL_GetPointerProperty(props, name, NULL);
CATCH(e, write_name_field(name, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
CATCH(e, write_exact(&character, 1, sizeof(akgl_Character *), fp));
CATCH(errctx, write_name_field(name, AKGL_CHARACTER_MAX_NAME_LENGTH, fp));
CATCH(errctx, write_exact(&character, 1, sizeof(akgl_Character *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH_NORETURN(e);
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}
/**
* @brief Write all four name-to-address tables, each with its terminator.
*
* Actors, sprites, spritesheets, characters, in that order -- which is the order
* akgl_game_load reads them back in. Each table is the registry enumerated one
* entry at a time, followed by a zeroed name field and a zeroed pointer that the
* reader stops on.
*
* @param fp The open save-game stream. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fp is `NULL`.
* @throws AKERR_IO On a stream error or a short write while emitting a
* terminator.
*
* @note Only the terminators are written through the error-reporting path. The
* entries themselves go through `SDL_EnumerateProperties`, whose callbacks
* cannot report failure upward and instead terminate the process -- so a
* write error mid-table never reaches this function's return value.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
akerr_ErrorContext *akgl_game_save_actors(FILE *fp)
{
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
// 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
// is what 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
@@ -412,51 +425,51 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
memset((void *)&nullbuf, 0x00, sizeof(nullbuf));
ATTEMPT {
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
// write the actor name pointer table
SDL_EnumerateProperties(
AKGL_REGISTRY_ACTOR,
&akgl_game_save_actorname_iterator,
&save_actorname_iterator,
(void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Actor *), fp));
CATCH(errctx, write_exact((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
CATCH(errctx, write_exact((void *)&nullbuf, 1, sizeof(akgl_Actor *), fp));
// write the sprite name pointer table
SDL_EnumerateProperties(
AKGL_REGISTRY_SPRITE,
&akgl_game_save_spritename_iterator,
&save_spritename_iterator,
(void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Sprite *), fp));
CATCH(errctx, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
CATCH(errctx, write_exact((void *)&nullbuf, 1, sizeof(akgl_Sprite *), fp));
// write the spritesheet name pointer table
SDL_EnumerateProperties(
AKGL_REGISTRY_SPRITESHEET,
&akgl_game_save_spritesheetname_iterator,
&save_spritesheetname_iterator,
(void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_SpriteSheet *), fp));
CATCH(errctx, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
CATCH(errctx, write_exact((void *)&nullbuf, 1, sizeof(akgl_SpriteSheet *), fp));
// write the character name pointer table
SDL_EnumerateProperties(
AKGL_REGISTRY_CHARACTER,
&akgl_game_save_charactername_iterator,
&save_charactername_iterator,
(void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Character *), fp));
CATCH(errctx, write_exact((void *)&nullbuf, 1, AKGL_CHARACTER_MAX_NAME_LENGTH, fp));
CATCH(errctx, write_exact((void *)&nullbuf, 1, sizeof(akgl_Character *), fp));
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
akerr_ErrorContext *akgl_game_save(char *fpath)
{
FILE *fp = NULL;
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
ATTEMPT {
FAIL_ZERO_BREAK(e, fpath, AKERR_NULLPOINTER, "NULL file path");
CATCH(e, aksl_fopen(fpath, "wb", &fp));
CATCH(e, write_exact(&game, 1, sizeof(akgl_Game), fp));
CATCH(e, akgl_game_save_actors(fp));
FAIL_ZERO_BREAK(errctx, fpath, AKERR_NULLPOINTER, "NULL file path");
CATCH(errctx, aksl_fopen(fpath, "wb", &fp));
CATCH(errctx, write_exact(&akgl_game, 1, sizeof(akgl_Game), fp));
CATCH(errctx, 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
@@ -464,9 +477,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
// closed its stream.
if ( fp != NULL )
fclose(fp);
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e); // SUCCEED_NORETURN if in main().
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx); // SUCCEED_NORETURN if in main().
}
/**
@@ -491,7 +504,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
* @param namelength Width of the table's name field, which must match what the
* corresponding save iterator wrote. It also sizes a
* variable-length array on the stack.
* @param ptrlength Width of the table's pointer field, i.e. `sizeof` the
* @param ptrlength Width of the table's pointer field, i.errctx. `sizeof` the
* pointer type that table holds.
* @param registry The registry to resolve names against -- #AKGL_REGISTRY_ACTOR
* for the actor table, and so on.
@@ -505,7 +518,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
* rather than being reported: the entry is written with whatever
* `SDL_GetPointerProperty` returned.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry)
static akerr_ErrorContext *load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry)
{
void *ptr = NULL;
char ptrstring[32];
@@ -513,7 +526,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_Pr
int retval = 0;
bool done = false;
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
// 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
@@ -521,8 +534,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_Pr
// corrupt name table as a successful load.
while ( done == false ) {
ATTEMPT {
CATCH(e, read_exact((void *)&objname, 1, namelength, fp));
CATCH(e, read_exact((void *)&ptr, 1, ptrlength, fp));
CATCH(errctx, read_exact((void *)&objname, 1, namelength, fp));
CATCH(errctx, read_exact((void *)&ptr, 1, ptrlength, fp));
// End of the map
if ( ptr == 0x00 && objname[0] == 0x00 ) {
done = true;
@@ -536,66 +549,46 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_Pr
// 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));
CATCH(errctx, 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);
} PROCESS(errctx) {
} FINISH(errctx, true);
};
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
/**
* @brief Refuse a save file unless its version matches the running one exactly.
*
* Both strings are parsed as semver and compared with `"="` -- exact equality,
* not compatibility. That is deliberate and strict: a save file is a raw memory
* image of `akgl_Game` and the object tables, so any change to a struct layout
* invalidates it, and semver has no way to say "the layout did not move".
*
* @param versiontype What is being compared -- `"library"` or `"game"`. Used
* only to build the message, but required, since a bare
* "incompatible version" error would not say which.
* @param newversion The version read out of the save file. Required.
* @param curversion The version of the running program or library. Required.
* @return `NULL` when the two match, otherwise an error context owned by the
* caller.
* @throws AKERR_NULLPOINTER If any of the three arguments is `NULL`.
* @throws AKERR_VALUE If either string is not valid semver. The message says
* which side it came from.
* @throws AKERR_API If both parse but are not equal. The message quotes both.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion)
akerr_ErrorContext *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");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, versiontype, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(errctx, curversion, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(errctx, newversion, AKERR_NULLPOINTER, "NULL argument");
ATTEMPT {
// Check save game library version
FAIL_NONZERO_BREAK(
e,
errctx,
semver_parse((const char *)curversion, &current_version),
AKERR_VALUE,
"Invalid semantic %s version in current game: %s",
versiontype,
(char *)curversion);
FAIL_NONZERO_BREAK(
e,
errctx,
semver_parse((const char *)newversion, &compare_version),
AKERR_VALUE,
"Invalid semantic %s version in save game: %s",
versiontype,
(char *)&newversion);
FAIL_ZERO_BREAK(
e,
errctx,
semver_satisfies(compare_version, current_version, "="),
AKERR_API,
"Incompatible save game %s version (%s != %s)",
@@ -605,12 +598,12 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype,
} CLEANUP {
semver_free(&current_version);
semver_free(&compare_version);
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
akerr_ErrorContext *akgl_game_load(char *fpath)
{
akgl_Game savegame;
SDL_PropertiesID actormap;
@@ -619,86 +612,113 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
SDL_PropertiesID charactermap;
FILE *fp = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fpath, AKERR_NULLPOINTER, "NULL file path");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, fpath, AKERR_NULLPOINTER, "NULL file path");
ATTEMPT {
CATCH(e, aksl_fopen(fpath, "rb", &fp));
CATCH(e, read_exact((void *)&savegame, 1, sizeof(akgl_Game), fp));
CATCH(e, akgl_game_load_versioncmp("library", (char *)&savegame.libversion, (char *)AKGL_VERSION));
CATCH(e, akgl_game_load_versioncmp("game", (char *)&savegame.version, (char *)&game.version));
FAIL_NONZERO_RETURN(
e,
strncmp((char *)&savegame.name, (char *)&game.name, 256),
CATCH(errctx, aksl_fopen(fpath, "rb", &fp));
CATCH(errctx, read_exact((void *)&savegame, 1, sizeof(akgl_Game), fp));
CATCH(errctx, akgl_game_load_versioncmp("library", (char *)&savegame.libversion, (char *)AKGL_VERSION));
CATCH(errctx, akgl_game_load_versioncmp("game", (char *)&savegame.version, (char *)&akgl_game.version));
FAIL_NONZERO_BREAK(
errctx,
strncmp((char *)&savegame.name, (char *)&akgl_game.name, 256),
AKERR_API,
"Savegame is not compatible with this game");
FAIL_NONZERO_RETURN(
e,
strncmp((char *)&savegame.uri, (char *)&game.uri, 256),
FAIL_NONZERO_BREAK(
errctx,
strncmp((char *)&savegame.uri, (char *)&akgl_game.uri, 256),
AKERR_API,
"Savegame is not compatible with this game");
memcpy((void *)&game, (void *)&savegame, sizeof(akgl_Game));
memcpy((void *)&akgl_game, (void *)&savegame, sizeof(akgl_Game));
// Load actor name map
actormap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
CATCH(errctx, load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
// Load sprite name map
spritemap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, spritemap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
CATCH(errctx, load_objectnamemap(fp, spritemap, AKGL_SPRITE_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
// Load spritesheet name map
spritesheetmap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, spritesheetmap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
CATCH(errctx, load_objectnamemap(fp, spritesheetmap, AKGL_SPRITE_SHEET_MAX_FILENAME_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));
CATCH(errctx, load_objectnamemap(fp, charactermap, AKGL_CHARACTER_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER));
// The four tables are the whole file, so the stream has to be at its end
// now. Nothing else checks: the tables carry no length prefix and end at
// a zeroed sentinel, so a reader whose field width disagrees with the
// writer's does not run off anything -- it finds a run of zeros
// somewhere inside an entry, stops early, and reports success with
// silently wrong maps. This is what makes that disagreement an error
// rather than a corruption, and it is the check that would have caught
// the spritesheet width being wrong for however long it was.
//
// It also has to move once the objects themselves are written; the
// comment below is the marker for that.
FAIL_NONZERO_BREAK(
errctx,
(fgetc(fp) != EOF),
AKERR_IO,
"Savegame has data left after its name tables; a name field width does not match the writer's");
// 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);
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags)
akerr_ErrorContext *akgl_game_update(akgl_Iterator *opflags)
{
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
// No AKGL_ITERATOR_OP_LAYERMASK by default: updating every actor once is
// the whole job, and restricting the sweep to one layer is the caller
// deliberately asking for less.
akgl_Iterator defflags = {
.flags = (AKGL_ITERATOR_OP_LAYERMASK | AKGL_ITERATOR_OP_LAYERMASK),
.flags = AKGL_ITERATOR_OP_UPDATE,
.layerid = 0
};
SDL_Time curTime = SDL_GetTicksNS();
akgl_Actor *actor = NULL;
if ( opflags == NULL ) {
opflags = &defflags;
}
PASS(e, akgl_game_state_lock());
PASS(errctx, akgl_game_state_lock());
akgl_game_updateFPS();
akgl_game_update_fps();
for ( int i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
if ( opflags == &defflags ) {
opflags->layerid = i;
// One sweep, not one per layer. This loop used to sit inside a walk over
// AKGL_TILEMAP_MAX_LAYERS that never compared actor->layer to the layer it
// was on, so every live actor's updatefunc ran sixteen times a frame --
// measured at 68.4 ns per update and 64 actors, 70 us of work to do 4.4 us
// of it. Updating an actor is not a per-layer operation. Drawing is, and
// akgl_render_2d_draw_world walks the layers itself.
for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
actor = &akgl_heap_actors[j];
if ( actor->refcount == 0 ) {
continue;
}
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));
// The flag is honoured now rather than ignored. A caller that sets it
// gets exactly the actors on opflags->layerid, once each.
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) &&
(actor->layer != opflags->layerid) ) {
continue;
}
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE) ) {
PASS(errctx, akgl_tilemap_scale_actor(akgl_gamemap, actor));
} else {
actor->scale = 1.0;
}
PASS(errctx, 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);
PASS(errctx, akgl_physics->simulate(akgl_physics, NULL));
PASS(errctx, akgl_renderer->draw_world(akgl_renderer, NULL));
PASS(errctx, akgl_game_state_unlock());
SUCCEED_RETURN(errctx);
}

View File

@@ -14,49 +14,49 @@
#include <akgl/iterator.h>
#include <akgl/error.h>
akgl_Actor HEAP_ACTOR[AKGL_MAX_HEAP_ACTOR];
akgl_Sprite HEAP_SPRITE[AKGL_MAX_HEAP_SPRITE];
akgl_SpriteSheet HEAP_SPRITESHEET[AKGL_MAX_HEAP_SPRITESHEET];
akgl_Character HEAP_CHARACTER[AKGL_MAX_HEAP_CHARACTER];
akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING];
akgl_Actor akgl_heap_actors[AKGL_MAX_HEAP_ACTOR];
akgl_Sprite akgl_heap_sprites[AKGL_MAX_HEAP_SPRITE];
akgl_SpriteSheet akgl_heap_spritesheets[AKGL_MAX_HEAP_SPRITESHEET];
akgl_Character akgl_heap_characters[AKGL_MAX_HEAP_CHARACTER];
akgl_String akgl_heap_strings[AKGL_MAX_HEAP_STRING];
akerr_ErrorContext *akgl_heap_init()
akerr_ErrorContext *akgl_heap_init(void)
{
PREPARE_ERROR(errctx);
int i = 0;
PASS(errctx, akgl_heap_init_actor());
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) {
memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite));
memset(&akgl_heap_sprites[i], 0x00, sizeof(akgl_Sprite));
}
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++) {
memset(&HEAP_SPRITESHEET[i], 0x00, sizeof(akgl_SpriteSheet));
memset(&akgl_heap_spritesheets[i], 0x00, sizeof(akgl_SpriteSheet));
}
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++) {
memset(&HEAP_CHARACTER[i], 0x00, sizeof(akgl_Character));
memset(&akgl_heap_characters[i], 0x00, sizeof(akgl_Character));
}
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++) {
memset(&HEAP_STRING[i], 0x00, sizeof(akgl_String));
memset(&akgl_heap_strings[i], 0x00, sizeof(akgl_String));
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_heap_init_actor(void)
{
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++) {
memset(&HEAP_ACTOR[i], 0x00, sizeof(akgl_Actor));
memset(&akgl_heap_actors[i], 0x00, sizeof(akgl_Actor));
}
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_heap_next_actor(akgl_Actor **dest)
{
PREPARE_ERROR(errctx);
for (int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
if ( HEAP_ACTOR[i].refcount != 0 ) {
if ( akgl_heap_actors[i].refcount != 0 ) {
continue;
}
*dest = &HEAP_ACTOR[i];
*dest = &akgl_heap_actors[i];
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused actor on the heap");
@@ -66,10 +66,10 @@ akerr_ErrorContext *akgl_heap_next_sprite(akgl_Sprite **dest)
{
PREPARE_ERROR(errctx);
for (int i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
if ( HEAP_SPRITE[i].refcount != 0 ) {
if ( akgl_heap_sprites[i].refcount != 0 ) {
continue;
}
*dest = &HEAP_SPRITE[i];
*dest = &akgl_heap_sprites[i];
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused sprite on the heap");
@@ -79,10 +79,10 @@ akerr_ErrorContext *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest)
{
PREPARE_ERROR(errctx);
for (int i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
if ( HEAP_SPRITESHEET[i].refcount != 0 ) {
if ( akgl_heap_spritesheets[i].refcount != 0 ) {
continue;
}
*dest = &HEAP_SPRITESHEET[i];
*dest = &akgl_heap_spritesheets[i];
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused spritesheet on the heap");
@@ -92,10 +92,10 @@ akerr_ErrorContext *akgl_heap_next_character(akgl_Character **dest)
{
PREPARE_ERROR(errctx);
for (int i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
if ( HEAP_CHARACTER[i].refcount != 0 ) {
if ( akgl_heap_characters[i].refcount != 0 ) {
continue;
}
*dest = &HEAP_CHARACTER[i];
*dest = &akgl_heap_characters[i];
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused character on the heap");
@@ -105,11 +105,11 @@ akerr_ErrorContext *akgl_heap_next_string(akgl_String **dest)
{
PREPARE_ERROR(errctx);
for (int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
if ( HEAP_STRING[i].refcount != 0 ) {
if ( akgl_heap_strings[i].refcount != 0 ) {
continue;
}
*dest = &HEAP_STRING[i];
HEAP_STRING[i].refcount += 1;
*dest = &akgl_heap_strings[i];
akgl_heap_strings[i].refcount += 1;
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused string on the heap");
@@ -135,19 +135,35 @@ akerr_ErrorContext *akgl_heap_release_actor(akgl_Actor *ptr)
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_heap_release_character(akgl_Character *basechar)
akerr_ErrorContext *akgl_heap_release_character(akgl_Character *ptr)
{
PREPARE_ERROR(errctx);
akgl_Iterator opflags;
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "NULL character reference");
AKGL_BITMASK_CLEAR(opflags.flags);
if ( basechar->refcount > 0 ) {
basechar->refcount -= 1;
if ( ptr->refcount > 0 ) {
ptr->refcount -= 1;
}
if ( basechar->refcount == 0 ) {
SDL_ClearProperty(AKGL_REGISTRY_CHARACTER, (char *)&basechar->name);
memset(basechar, 0x00, sizeof(akgl_Character));
if ( ptr->refcount == 0 ) {
// Give back every sprite reference akgl_character_sprite_add took, and
// the property set holding the map, before the slot is zeroed. Without
// this a game that loads and releases characters level by level
// exhausts the sprite pool and leaks one SDL_PropertiesID per
// character. akgl_character_state_sprites_iterate exists for exactly
// this walk and simply was never called from here.
if ( ptr->state_sprites != 0 ) {
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_RELEASE);
SDL_EnumerateProperties(
ptr->state_sprites,
&akgl_character_state_sprites_iterate,
(void *)&opflags
);
SDL_DestroyProperties(ptr->state_sprites);
ptr->state_sprites = 0;
}
SDL_ClearProperty(AKGL_REGISTRY_CHARACTER, (char *)&ptr->name);
memset(ptr, 0x00, sizeof(akgl_Character));
}
SUCCEED_RETURN(errctx);
}

View File

@@ -17,6 +17,8 @@ akerr_ErrorContext *akgl_get_json_object_value(json_t *obj, char *key, json_t **
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -28,6 +30,8 @@ akerr_ErrorContext *akgl_get_json_boolean_value(json_t *obj, char *key, bool *de
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -39,6 +43,8 @@ akerr_ErrorContext *akgl_get_json_integer_value(json_t *obj, char *key, int *des
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -46,10 +52,12 @@ akerr_ErrorContext *akgl_get_json_integer_value(json_t *obj, char *key, int *des
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float *dest)
akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float32_t *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -57,10 +65,12 @@ akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float *de
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest)
akerr_ErrorContext *akgl_get_json_double_value(json_t *obj, char *key, float64_t *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -73,8 +83,8 @@ akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_Stri
json_t *value = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
@@ -84,9 +94,13 @@ akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_Stri
CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL));
}
// FINISH(errctx, true), not false. With `false` a failed
// akgl_heap_next_string was swallowed and the strncpy below ran through
// the pointer it never set, so an exhausted string pool arrived as a
// segfault inside strncpy rather than as AKGL_ERR_HEAP.
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, false);
} FINISH(errctx, true);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx);
@@ -96,6 +110,8 @@ akerr_ErrorContext *akgl_get_json_array_value(json_t *obj, char *key, json_t **d
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -107,6 +123,7 @@ akerr_ErrorContext *akgl_get_json_array_index_object(json_t *array, int index, j
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
@@ -118,6 +135,7 @@ akerr_ErrorContext *akgl_get_json_array_index_integer(json_t *array, int index,
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
@@ -129,7 +147,7 @@ akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, a
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
@@ -138,33 +156,50 @@ akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, a
CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL));
}
// FINISH(errctx, true), not false. With `false` a failed
// akgl_heap_next_string was swallowed and the strncpy below ran through
// the pointer it never set, so an exhausted string pool arrived as a
// segfault inside strncpy rather than as AKGL_ERR_HEAP.
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, false);
} FINISH(errctx, true);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *err, void *defval, void *dest, uint32_t defsize)
akerr_ErrorContext *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize)
{
PREPARE_ERROR(e);
if ( err == NULL ) {
SUCCEED_RETURN(e);
// Two contexts, and the names now say which is which: `e` is the incoming
// one being inspected, `errctx` is this function's own. They were `err` and
// `e`, which put the incoming context under the name the convention
// reserves for exactly that and the local one under a name that reads like
// it.
PREPARE_ERROR(errctx);
if ( e == NULL ) {
SUCCEED_RETURN(errctx);
}
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");
FAIL_ZERO_RETURN(errctx, defval, AKERR_NULLPOINTER, "defval");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
ATTEMPT {
} CLEANUP {
} PROCESS(err) {
} HANDLE_GROUP(err, AKERR_KEY) {
} HANDLE_GROUP(err, AKERR_INDEX) {
} PROCESS(e) {
// All three arms fall into the memcpy: HANDLE_GROUP emits no `break`,
// so a new status has to be added *above* the arm holding the body, not
// below it.
//
// AKERR_OUTOFBOUNDS is what akgl_get_json_array_index_object, _integer
// and _string report for a short array. Without it here, "this element
// is optional" worked for an object member and silently did not work
// for an array element -- the error propagated instead of being
// replaced by the default.
} HANDLE_GROUP(e, AKERR_KEY) {
} HANDLE_GROUP(e, AKERR_OUTOFBOUNDS) {
} HANDLE_GROUP(e, AKERR_INDEX) {
memcpy(dest, defval, defsize);
} FINISH(err, true);
} FINISH(e, true);
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}

View File

@@ -12,46 +12,50 @@
#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)
akerr_ErrorContext *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);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2)
akerr_ErrorContext *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);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, a1, AKERR_NULLPOINTER, "a1");
FAIL_ZERO_RETURN(errctx, a2, AKERR_NULLPOINTER, "a2");
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
akerr_ErrorContext *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
SUCCEED_RETURN(e);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *self)
akerr_ErrorContext *akgl_physics_init_null(akgl_PhysicsBackend *self)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, 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);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
akerr_ErrorContext *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");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
if ( self->gravity_x != 0 ) {
// Assume the X origin is - (screen left)
@@ -66,34 +70,34 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBacke
actor->ez -= (self->gravity_z * dt);
}
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2)
akerr_ErrorContext *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);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_RETURN(errctx, AKERR_API, "Not implemented");
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
akerr_ErrorContext *akgl_physics_arcade_move(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");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
actor->x += actor->vx * dt;
actor->y += actor->vy * dt;
actor->z += actor->vz * dt;
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self)
akerr_ErrorContext *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));
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
PASS(errctx, akgl_heap_next_string(&tmp));
self->gravity = akgl_physics_arcade_gravity;
self->collide = akgl_physics_arcade_collide;
@@ -101,29 +105,29 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend
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));
CATCH(errctx, akgl_get_property("physics.gravity.x", &tmp, "0.0"));
CATCH(errctx, aksl_atof(tmp->data, &self->gravity_x));
CATCH(errctx, akgl_get_property("physics.gravity.y", &tmp, "0.0"));
CATCH(errctx, aksl_atof(tmp->data, &self->gravity_y));
CATCH(errctx, akgl_get_property("physics.gravity.z", &tmp, "0.0"));
CATCH(errctx, aksl_atof(tmp->data, &self->gravity_z));
CATCH(errctx, akgl_get_property("physics.drag.x", &tmp, "0.0"));
CATCH(errctx, aksl_atof(tmp->data, &self->drag_x));
CATCH(errctx, akgl_get_property("physics.drag.y", &tmp, "0.0"));
CATCH(errctx, aksl_atof(tmp->data, &self->drag_y));
CATCH(errctx, akgl_get_property("physics.drag.z", &tmp, "0.0"));
CATCH(errctx, aksl_atof(tmp->data, &self->drag_z));
} CLEANUP {
IGNORE(akgl_heap_release_string(tmp));
} PROCESS(e) {
} FINISH(e, true);
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags)
akerr_ErrorContext *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags)
{
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
akgl_Iterator defflags = {
.flags = 0,
.layerid = 0
@@ -132,8 +136,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
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");
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->move, AKERR_NULLPOINTER, "self->move");
// Reading the elapsed time requires self, so it cannot be hoisted above
// the null check.
@@ -146,7 +150,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
actor = &HEAP_ACTOR[i];
actor = &akgl_heap_actors[i];
if ( actor->refcount == 0 ) {
continue;
}
@@ -198,8 +202,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
}
}
ATTEMPT {
CATCH(e, actor->movementlogicfunc(actor, dt));
PASS(e, self->gravity(self, actor, dt));
CATCH(errctx, actor->movementlogicfunc(actor, dt));
PASS(errctx, self->gravity(self, actor, dt));
// Counteract velocity with atmospheric drag
if ( self->drag_x != 0 ) {
@@ -216,31 +220,31 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
actor->vy = actor->ey + actor->ty;
actor->vz = actor->ez + actor->tz;
PASS(e, self->move(self, actor, dt));
PASS(errctx, self->move(self, actor, dt));
} CLEANUP {
} PROCESS(e) {
} HANDLE(e, AKGL_ERR_LOGICINTERRUPT) {
} PROCESS(errctx) {
} HANDLE(errctx, AKGL_ERR_LOGICINTERRUPT) {
// noop
} FINISH(e, true);
} FINISH(errctx, true);
}
self->gravity_time = curtime;
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type)
akerr_ErrorContext *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");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, type, AKERR_NULLPOINTER, "type");
if ( strncmp(type->data, "null", 4) == 0) {
PASS(e, akgl_physics_init_null(self));
SUCCEED_RETURN(e);
PASS(errctx, akgl_physics_init_null(self));
SUCCEED_RETURN(errctx);
}
if ( strncmp(type->data, "arcade", 6) == 0) {
PASS(e, akgl_physics_init_arcade(self));
SUCCEED_RETURN(e);
PASS(errctx, akgl_physics_init_arcade(self));
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(e, AKERR_KEY, "Invalid physics engine %s", type->data);
FAIL_RETURN(errctx, AKERR_KEY, "Invalid physics engine %s", type->data);
}

View File

@@ -25,7 +25,35 @@ SDL_PropertiesID AKGL_REGISTRY_MUSIC = 0;
SDL_PropertiesID AKGL_REGISTRY_FONT = 0;
SDL_PropertiesID AKGL_REGISTRY_PROPERTIES = 0;
akerr_ErrorContext *akgl_registry_init()
/**
* @brief Create one registry, replacing whatever set was held under it.
*
* All eight initializers go through this so they behave the same way on a
* second call. Only akgl_registry_init_actor used to destroy the old set --
* akgl_heap_init_actor's counterpart, so resetting actors between levels was
* the one path that did not leak -- and the other seven abandoned their
* SDL_PropertiesID every time they were called again.
*
* @param dest The registry handle to (re)create. Required.
* @param what What it holds, for the failure message. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest is `NULL`, or if SDL cannot create the
* property set.
*/
static akerr_ErrorContext *registry_create(SDL_PropertiesID *dest, const char *what)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
if ( *dest != 0 ) {
SDL_DestroyProperties(*dest);
*dest = 0;
}
*dest = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, *dest, AKERR_NULLPOINTER, "Error initializing %s registry", what);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_registry_init(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
@@ -36,54 +64,52 @@ akerr_ErrorContext *akgl_registry_init()
CATCH(errctx, akgl_registry_init_actor_state_strings());
CATCH(errctx, akgl_registry_init_font());
CATCH(errctx, akgl_registry_init_music());
// Missing until 0.5.0. Without it AKGL_REGISTRY_PROPERTIES stayed 0,
// akgl_set_property was a silent no-op, akgl_get_property always
// returned the caller's default, and akgl_physics_init_arcade and
// akgl_render_2d_init quietly ignored their configuration. Only callers
// going through akgl_game_init, which calls it separately, escaped that.
CATCH(errctx, akgl_registry_init_properties());
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_registry_init_actor()
akerr_ErrorContext *akgl_registry_init_actor(void)
{
PREPARE_ERROR(errctx);
if ( AKGL_REGISTRY_ACTOR != 0 ) {
SDL_DestroyProperties(AKGL_REGISTRY_ACTOR);
}
AKGL_REGISTRY_ACTOR = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_ACTOR, AKERR_NULLPOINTER, "Error initializing actor registry");
PASS(errctx, registry_create(&AKGL_REGISTRY_ACTOR, "actor"));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font()
akerr_ErrorContext *akgl_registry_init_font(void)
{
PREPARE_ERROR(errctx);
AKGL_REGISTRY_FONT = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_FONT, AKERR_NULLPOINTER, "Error initializing font registry");
PASS(errctx, registry_create(&AKGL_REGISTRY_FONT, "font"));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_registry_init_music()
akerr_ErrorContext *akgl_registry_init_music(void)
{
PREPARE_ERROR(errctx);
AKGL_REGISTRY_MUSIC = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_MUSIC, AKERR_NULLPOINTER, "Error initializing music registry");
PASS(errctx, registry_create(&AKGL_REGISTRY_MUSIC, "music"));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_registry_init_properties()
akerr_ErrorContext *akgl_registry_init_properties(void)
{
PREPARE_ERROR(errctx);
AKGL_REGISTRY_PROPERTIES = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_PROPERTIES, AKERR_NULLPOINTER, "Error initializing properties registry");
PASS(errctx, registry_create(&AKGL_REGISTRY_PROPERTIES, "properties"));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_registry_init_actor_state_strings()
akerr_ErrorContext *akgl_registry_init_actor_state_strings(void)
{
int i = 0;
int flag = 0;
PREPARE_ERROR(errctx);
AKGL_REGISTRY_ACTOR_STATE_STRINGS = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_ACTOR_STATE_STRINGS, AKERR_NULLPOINTER, "Error initializing actor state strings registry");
PASS(errctx, registry_create(&AKGL_REGISTRY_ACTOR_STATE_STRINGS, "actor state strings"));
for ( i = 0 ; i < AKGL_ACTOR_MAX_STATES; i++ ) {
flag = (1 << i);
SDL_SetNumberProperty(
@@ -94,31 +120,28 @@ akerr_ErrorContext *akgl_registry_init_actor_state_strings()
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_registry_init_sprite()
akerr_ErrorContext *akgl_registry_init_sprite(void)
{
PREPARE_ERROR(errctx);
AKGL_REGISTRY_SPRITE = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_SPRITE, AKERR_NULLPOINTER, "Error initializing sprite registry");
PASS(errctx, registry_create(&AKGL_REGISTRY_SPRITE, "sprite"));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_registry_init_spritesheet()
akerr_ErrorContext *akgl_registry_init_spritesheet(void)
{
PREPARE_ERROR(errctx);
AKGL_REGISTRY_SPRITESHEET = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_SPRITESHEET, AKERR_NULLPOINTER, "Error initializing spritesheet registry");
PASS(errctx, registry_create(&AKGL_REGISTRY_SPRITESHEET, "spritesheet"));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_registry_init_character()
akerr_ErrorContext *akgl_registry_init_character(void)
{
PREPARE_ERROR(errctx);
AKGL_REGISTRY_CHARACTER = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_CHARACTER, AKERR_NULLPOINTER, "Error initializing character registry");
PASS(errctx, registry_create(&AKGL_REGISTRY_CHARACTER, "character"));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname)
akerr_ErrorContext *akgl_registry_load_properties(char *fname)
{
json_t *json = NULL;
json_t *props = NULL;
@@ -175,26 +198,26 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname)
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *src)
akerr_ErrorContext *akgl_set_property(char *name, char *value)
{
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);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL char *");
FAIL_ZERO_RETURN(errctx, value, AKERR_NULLPOINTER, "NULL char *");
SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, name, value);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def)
akerr_ErrorContext *akgl_get_property(char *name, akgl_String **dest, char *def)
{
const char *value = NULL;
size_t valuelen = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL char *");
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "NULL akgl_String *");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL char *");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL akgl_String *");
ATTEMPT {
if ( *dest == NULL ) {
CATCH(e, akgl_heap_next_string(dest));
CATCH(errctx, akgl_heap_next_string(dest));
}
// Copy the value's own length, not the destination's capacity. What
// SDL hands back is its strdup of the value -- four bytes for "0.0" --
@@ -207,23 +230,23 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **d
// which is what the header has always promised. It used to arrive from
// inside aksl_memcpy; now it is raised here, before anything is measured.
FAIL_ZERO_BREAK(
e,
errctx,
value,
AKERR_NULLPOINTER,
"Property %s is not set and no default was given",
name);
valuelen = strlen(value);
FAIL_NONZERO_BREAK(
e,
errctx,
(valuelen >= AKGL_MAX_STRING_LENGTH),
AKERR_OUTOFBOUNDS,
"Property %s is %zu bytes, which does not fit an akgl_String (%d)",
name,
valuelen,
AKGL_MAX_STRING_LENGTH);
CATCH(e, aksl_memcpy((*dest)->data, (void *)value, (valuelen + 1)));
CATCH(errctx, aksl_memcpy((*dest)->data, (void *)value, (valuelen + 1)));
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}

View File

@@ -14,43 +14,56 @@
#include <akerror.h>
#include <akstdlib.h>
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self)
akerr_ErrorContext *akgl_render_2d_init(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");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, 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));
// The two pooled strings come back in CLEANUP. Released after both parses,
// as they used to be, a non-numeric game.screenwidth returned past them and
// leaked two of the pool's 256 entries.
ATTEMPT {
CATCH(errctx, akgl_get_property("game.screenwidth", &width, "0"));
CATCH(errctx, akgl_get_property("game.screenheight", &height, "0"));
CATCH(errctx, aksl_atoi(width->data, &screenwidth));
CATCH(errctx, aksl_atoi(height->data, &screenheight));
SDL_Log("Initializing screen (%sx%s = %dx%d)", width->data, height->data, screenwidth, screenheight);
} CLEANUP {
if ( width != NULL ) {
IGNORE(akgl_heap_release_string(width));
width = NULL;
}
if ( height != NULL ) {
IGNORE(akgl_heap_release_string(height));
height = NULL;
}
} PROCESS(errctx) {
} FINISH(errctx, true);
FAIL_ZERO_RETURN(
e,
SDL_CreateWindowAndRenderer(game.uri, screenwidth, screenheight, 0, &window, &self->sdl_renderer),
errctx,
SDL_CreateWindowAndRenderer(akgl_game.uri, screenwidth, screenheight, 0, &akgl_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;
akgl_camera->x = 0;
akgl_camera->y = 0;
akgl_camera->w = screenwidth;
akgl_camera->h = screenheight;
PASS(e, akgl_render_bind2d(self));
SUCCEED_RETURN(e);
PASS(errctx, akgl_render_2d_bind(self));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_bind2d(akgl_RenderBackend *self)
akerr_ErrorContext *akgl_render_2d_bind(akgl_RenderBackend *self)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
// Deliberately does not touch self->sdl_renderer: a host that owns its own
// window has already put one there, and this is the only way it gets a
@@ -61,93 +74,94 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_bind2d(akgl_RenderBackend *self)
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);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self)
akerr_ErrorContext *akgl_render_2d_shutdown(akgl_RenderBackend *self)
{
PREPARE_ERROR(e);
SUCCEED_RETURN(e);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self)
akerr_ErrorContext *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");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
SDL_SetRenderDrawColor(self->sdl_renderer, 0, 0, 0, 255);
SDL_RenderClear(self->sdl_renderer);
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self)
akerr_ErrorContext *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");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
SDL_RenderPresent(self->sdl_renderer);
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_texture(akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip)
akerr_ErrorContext *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");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, texture, AKERR_NULLPOINTER, "texture");
//FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
//FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
if ( angle != 0 ) {
FAIL_ZERO_RETURN(e, center, AKERR_NULLPOINTER, "center");
FAIL_ZERO_RETURN(errctx, center, AKERR_NULLPOINTER, "center");
FAIL_ZERO_RETURN(
e,
errctx,
SDL_RenderTextureRotated(self->sdl_renderer, texture, src, dest, angle, center, flip),
AKERR_NULLPOINTER, "%s", SDL_GetError()
);
} else {
FAIL_ZERO_RETURN(
e,
errctx,
SDL_RenderTexture(self->sdl_renderer, texture, src, dest),
AKERR_NULLPOINTER, "%s", SDL_GetError()
);
}
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self)
akerr_ErrorContext *akgl_render_2d_draw_mesh(akgl_RenderBackend *self)
{
PREPARE_ERROR(e);
FAIL_RETURN(e, AKERR_API, "Not implemented");
PREPARE_ERROR(errctx);
FAIL_RETURN(errctx, AKERR_API, "Not implemented");
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags)
akerr_ErrorContext *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags)
{
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
akgl_Iterator defflags;
SDL_Time curTime = SDL_GetTicksNS();
akgl_Actor *actor = NULL;
int j = 0;
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
if ( opflags == NULL ) {
opflags = &defflags;
PASS(e, aksl_memset((void *)opflags, 0x00, sizeof(akgl_Iterator)));
PASS(errctx, aksl_memset((void *)opflags, 0x00, sizeof(akgl_Iterator)));
}
for ( int i = 0; i < AKGL_TILEMAP_MAX_LAYERS ; i++ ) {
if ( i < gamemap->numlayers ) {
PASS(e, akgl_tilemap_draw(gamemap, camera, i));
if ( i < akgl_gamemap->numlayers ) {
PASS(errctx, akgl_tilemap_draw(akgl_gamemap, akgl_camera, i));
}
for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR ; j++ ) {
actor = &HEAP_ACTOR[j];
actor = &akgl_heap_actors[j];
if ( actor->refcount == 0 ) {
continue;
}
if ( actor->layer != i ) {
continue;
}
PASS(e, actor->renderfunc(actor));
PASS(errctx, actor->renderfunc(actor));
}
}
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}

View File

@@ -19,13 +19,13 @@
#include <akgl/iterator.h>
#include <akgl/util.h>
akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid)
akerr_ErrorContext *akgl_spritesheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid)
{
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
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");
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL sprite");
FAIL_ZERO_RETURN(errctx, srccoords, AKERR_NULLPOINTER, "NULL SDL_Rect");
FAIL_ZERO_RETURN(errctx, self->sheet, AKERR_NULLPOINTER, "NULL spritesheet");
srccoords->x = self->width * self->frameids[frameid];
if ( srccoords->x >= self->sheet->texture->w ) {
@@ -37,7 +37,7 @@ akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FR
srccoords->w = self->width;
srccoords->h = self->height;
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
/**
@@ -67,7 +67,7 @@ akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FR
* @throws AKGL_ERR_SDL If the image cannot be decoded.
* @throws AKGL_ERR_HEAP If the spritesheet or string pool is exhausted.
*/
static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
static akerr_ErrorContext *sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
{
PREPARE_ERROR(errctx);
json_t *spritesheet_json = NULL;
@@ -119,6 +119,8 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
akgl_String *spritename = NULL;
akgl_String *filename_copy = NULL;
int i = 0;
int framecount = 0;
int frameid = 0;
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
ATTEMPT {
@@ -136,7 +138,6 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
CATCH(errctx, akgl_heap_next_string(&spritename));
CATCH(errctx, akgl_string_initialize(spritename, NULL));
//SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
json = (json_t *)json_load_file(filename, 0, &error);
FAIL_ZERO_BREAK(
errctx,
@@ -145,7 +146,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
"Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
);
CATCH(errctx, akgl_sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data)));
CATCH(errctx, sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data)));
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
CATCH(errctx,
akgl_sprite_initialize(
@@ -157,14 +158,45 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &obj->width));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &obj->height));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &obj->speed));
obj->speed = obj->speed * AKGL_TIME_ONESEC_MS;
obj->speed = obj->speed * AKGL_TIME_ONEMS_NS;
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loop", &obj->loop));
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));
CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames));
obj->frames = json_array_size((json_t *)frames);
for ( i = 0 ; i < obj->frames; i++ ) {
CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, (uint32_t *)&obj->frameids[i]));
// Bounded before anything is written. frameids is
// AKGL_SPRITE_MAX_FRAMES bytes, and this loop used to take its count
// straight from the document -- so a definition with seventeen frames
// wrote past the array, past the rest of akgl_Sprite, and into the
// neighbouring pool slot.
framecount = (int)json_array_size((json_t *)frames);
FAIL_NONZERO_BREAK(
errctx,
(framecount > AKGL_SPRITE_MAX_FRAMES),
AKERR_OUTOFBOUNDS,
"Sprite %s declares %d frames; the maximum is %d",
(char *)&obj->name,
framecount,
AKGL_SPRITE_MAX_FRAMES
);
obj->frames = framecount;
for ( i = 0 ; i < framecount; i++ ) {
// Read into an int and narrow deliberately. The old form wrote
// through a uint32_t * cast of a uint8_t *, so every element write
// touched four bytes; it only appeared to work because the next
// iteration overwrote the spill and the last one landed in the
// struct's alignment padding.
CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, &frameid));
FAIL_NONZERO_BREAK(
errctx,
((frameid < 0) || (frameid > UINT8_MAX)),
AKERR_OUTOFBOUNDS,
"Sprite %s frame %d is %d; frame numbers are 0..%d",
(char *)&obj->name,
i,
frameid,
UINT8_MAX
);
obj->frameids[i] = (uint8_t)frameid;
}
} CLEANUP {
// The sprite copies every field it wants out of the document and the
@@ -221,8 +253,7 @@ akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int spr
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH);
//snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
sheet->texture = IMG_LoadTexture(renderer->sdl_renderer, filename);
sheet->texture = IMG_LoadTexture(akgl_renderer->sdl_renderer, filename);
FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());
FAIL_ZERO_BREAK(

View File

@@ -14,22 +14,37 @@ akerr_ErrorContext *akgl_string_initialize(akgl_String *obj, char *init)
if ( init != NULL ) {
strncpy((char *)&obj->data, init, AKGL_MAX_STRING_LENGTH);
} else {
memset(&obj->data, 0x00, sizeof(akgl_String));
// sizeof(obj->data), not sizeof(akgl_String). `data` starts after the
// `refcount` in front of it, so zeroing the size of the whole struct
// from the start of the buffer ran four bytes past the end of the
// object -- into the next pool slot's refcount, which is what makes a
// free slot look claimed or a claimed one look free.
memset(&obj->data, 0x00, sizeof(obj->data));
}
obj->refcount = 1;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_string_copy(akgl_String *src, akgl_String *dst, int count)
akerr_ErrorContext *akgl_string_copy(akgl_String *src, akgl_String *dest, int count)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(errctx, dest, 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");
// Both buffers are exactly AKGL_MAX_STRING_LENGTH bytes, so a larger count
// walks off the end of two pool slots at once. Refused rather than
// documented, which is what it used to be.
FAIL_NONZERO_RETURN(
errctx,
((count < 0) || (count > AKGL_MAX_STRING_LENGTH)),
AKERR_OUTOFBOUNDS,
"Copy count %d is outside 0..%d",
count,
AKGL_MAX_STRING_LENGTH);
if ( (char *)dest->data != strncpy((char *)&dest->data, (char *)&src->data, count) ) {
FAIL_RETURN(errctx, errno, "strncpy");
}
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}

View File

@@ -10,7 +10,7 @@
#include <akgl/registry.h>
#include <akgl/game.h>
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size)
akerr_ErrorContext *akgl_text_loadfont(char *name, char *filepath, int size)
{
TTF_Font *font = NULL;
@@ -37,7 +37,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name)
akerr_ErrorContext *akgl_text_unloadfont(char *name)
{
TTF_Font *font = NULL;
@@ -54,7 +54,39 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name)
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y)
/**
* @brief SDL_EnumerateProperties callback: close one registered font.
*
* The properties set cannot be modified while it is being enumerated, so this
* only closes; akgl_text_unloadallfonts clears the whole set afterwards.
*/
static void unload_font_iterate(void *userdata, SDL_PropertiesID props, const char *name)
{
TTF_Font *font = NULL;
if ( name == NULL ) {
return;
}
font = (TTF_Font *)SDL_GetPointerProperty(props, name, NULL);
if ( font != NULL ) {
TTF_CloseFont(font);
}
}
akerr_ErrorContext *akgl_text_unloadallfonts(void)
{
PREPARE_ERROR(errctx);
if ( AKGL_REGISTRY_FONT == 0 ) {
SUCCEED_RETURN(errctx);
}
SDL_EnumerateProperties(AKGL_REGISTRY_FONT, &unload_font_iterate, NULL);
SDL_DestroyProperties(AKGL_REGISTRY_FONT);
AKGL_REGISTRY_FONT = 0;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *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;
@@ -65,11 +97,20 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
// Checked before anything is rasterized, and checked at all because a
// backend that has an SDL_Renderer but was never run through
// akgl_render_bind2d has a NULL draw_texture -- which is exactly the state
// akgl_render_init2d used to be the only escape from.
FAIL_ZERO_RETURN(errctx, renderer, AKERR_NULLPOINTER, "No renderer backend");
FAIL_ZERO_RETURN(errctx, renderer->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, renderer->draw_texture, AKERR_NULLPOINTER, "Renderer backend has no draw_texture");
// akgl_render_2d_bind has a NULL draw_texture -- which is exactly the state
// akgl_render_2d_init used to be the only escape from.
FAIL_ZERO_RETURN(errctx, akgl_renderer, AKERR_NULLPOINTER, "No renderer backend");
FAIL_ZERO_RETURN(errctx, akgl_renderer->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, akgl_renderer->draw_texture, AKERR_NULLPOINTER, "Renderer backend has no draw_texture");
// Drawing nothing is not a failure. SDL_ttf returns NULL with "Text has
// zero width" from both rasterizers for "", so this used to report
// AKERR_NULLPOINTER for what a caller means as "draw an empty line" --
// while akgl_text_measure("") is documented as legal and returns 0 wide by
// one line high. Two halves of one header disagreeing about one string.
if ( text[0] == '\0' ) {
SUCCEED_RETURN(errctx);
}
if ( wraplength > 0 ) {
textsurf = TTF_RenderText_Blended_Wrapped(
font,
@@ -85,18 +126,30 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *
color);
}
FAIL_ZERO_RETURN(errctx, textsurf, AKERR_NULLPOINTER, "%s", SDL_GetError());
texture = SDL_CreateTextureFromSurface(renderer->sdl_renderer, textsurf);
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);
// Both were destroyed only after the draw succeeded, so a failed texture
// upload leaked the surface and a failed draw leaked both -- on a HUD line
// redrawn every frame, that is a leak per frame.
ATTEMPT {
texture = SDL_CreateTextureFromSurface(akgl_renderer->sdl_renderer, textsurf);
FAIL_ZERO_BREAK(errctx, texture, AKERR_NULLPOINTER, "%s", SDL_GetError());
dest.x = x;
dest.y = y;
SDL_GetTextureSize(texture, &dest.w, &dest.h);
CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE));
} CLEANUP {
if ( texture != NULL ) {
SDL_DestroyTexture(texture);
texture = NULL;
}
SDL_DestroySurface(textsurf);
textsurf = NULL;
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h)
akerr_ErrorContext *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
@@ -114,7 +167,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text,
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h)
akerr_ErrorContext *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");

View File

@@ -30,6 +30,7 @@ akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char
json_t *property = NULL;
akgl_String *tmpstr = NULL;
akgl_String *typestr = NULL;
bool found = false;
int i = 0;
// 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.
@@ -39,9 +40,13 @@ akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char
CATCH(errctx, akgl_get_json_array_value(obj, "properties", &properties));
for (i = 0; i < json_array_size(properties); i++) {
CATCH(errctx, akgl_get_json_array_index_object(properties, i, &property));
// One scratch string for the whole scan. akgl_get_json_string_value
// reuses a non-NULL *dest without taking another reference, so
// releasing it per iteration -- which this used to do -- dropped the
// refcount to zero while the slot was still in use, and the next
// claim anywhere could have been handed the same one.
CATCH(errctx, akgl_get_json_string_value(property, "name", &tmpstr));
if ( strcmp(tmpstr->data, key) != 0 ) {
CATCH(errctx, akgl_heap_release_string(tmpstr));
continue;
}
CATCH(errctx, akgl_get_json_string_value(property, "type", &typestr));
@@ -49,7 +54,8 @@ akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char
FAIL_BREAK(errctx, AKERR_TYPE, "Property %s is present but is incorrect type(expected %s got %s)", key, type, (char *)typestr->data);
}
*dest = property;
SUCCEED_RETURN(errctx);
found = true;
break;
}
} CLEANUP {
if ( tmpstr != NULL ) {
@@ -61,6 +67,13 @@ akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char
} PROCESS(errctx) {
} FINISH(errctx, true);
// The success path used to be a SUCCEED_RETURN from inside the ATTEMPT
// block, which returned past CLEANUP and leaked both scratch strings on
// every successful lookup. The pool is 256 entries and a map load does this
// many times per layer.
if ( found == true ) {
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKERR_KEY, "Property not found in properties map");
}
@@ -87,24 +100,7 @@ akerr_ErrorContext *akgl_get_json_properties_integer(json_t *obj, char *key, int
SUCCEED_RETURN(errctx);
}
/**
* @brief Read a Tiled custom property declared as `number`.
*
* Tiled writes `float` for a floating-point property, so this matches nothing
* Tiled produces today -- akgl_get_json_properties_float is the one the loader
* uses. Kept because a hand-written or older map may still say `number`.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value as a `float`. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `number`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, float *dest)
akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, float32_t *dest)
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
@@ -114,23 +110,7 @@ akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, floa
SUCCEED_RETURN(errctx);
}
/**
* @brief Read a Tiled custom property declared as `float`.
*
* The spelling Tiled actually writes for a floating-point property. This is how
* the `scale` on a perspective marker is read.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float *dest)
akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float32_t *dest)
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
@@ -140,25 +120,7 @@ akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float
SUCCEED_RETURN(errctx);
}
/**
* @brief Read a Tiled custom property declared as `float`, at full precision.
*
* Same declared type as akgl_get_json_properties_float, but writing a `double`.
* The map's physics constants are `double`, which is the reason both exist.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* akgl_tilemap_load_physics passes this status to
* akgl_get_json_with_default, which turns it into a default of 0.0.
* @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, double *dest)
akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, float64_t *dest)
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
@@ -170,41 +132,41 @@ akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, doub
akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname)
{
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
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(errctx, akgl_get_json_integer_value((json_t *)tileset, "columns", &dest->tilesets[tsidx].columns));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "firstgid", &dest->tilesets[tsidx].firstgid));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "imageheight", &dest->tilesets[tsidx].imageheight));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "imagewidth", &dest->tilesets[tsidx].imagewidth));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "margin", &dest->tilesets[tsidx].margin));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "spacing", &dest->tilesets[tsidx].spacing));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "tilecount", &dest->tilesets[tsidx].tilecount));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "tileheight", &dest->tilesets[tsidx].tileheight));
PASS(errctx, 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));
PASS(errctx, akgl_get_json_string_value((json_t *)tileset, "name", &tmpstr));
PASS(errctx, 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));
CATCH(errctx, akgl_get_json_string_value((json_t *)tileset, "image", &tmpstr));
CATCH(errctx, 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);
} PROCESS(errctx) {
} FINISH(errctx, 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());
dest->tilesets[tsidx].texture = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&dest->tilesets[tsidx].imagefilename);
FAIL_ZERO_RETURN(errctx, dest->tilesets[tsidx].texture, AKERR_NULLPOINTER, "Failed loading tileset image : %s", SDL_GetError());
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx)
@@ -276,6 +238,17 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
ATTEMPT {
CATCH(errctx, akgl_get_json_array_value(root, "tilesets", &tilesets))
for (i = 0; i < json_array_size((json_t *)tilesets); i++) {
// The bound goes at the top of the body, matching
// akgl_tilemap_load_layers. Without it a map with seventeen
// tilesets wrote past the fixed table -- and unlike the layer
// count, nothing anywhere else was checking this one.
FAIL_NONZERO_BREAK(
errctx,
(i >= AKGL_TILEMAP_MAX_TILESETS),
AKERR_OUTOFBOUNDS,
"Map declares more than %d tilesets",
AKGL_TILEMAP_MAX_TILESETS
);
CATCH(errctx, akgl_get_json_array_index_object((json_t *)tilesets, i, &jstileset));
CATCH(errctx, akgl_tilemap_load_tilesets_each(jstileset, dest, i, dirname));
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(dest, i));
@@ -288,41 +261,6 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
SUCCEED_RETURN(errctx);
}
/**
* @brief Turn one Tiled object into a live actor, creating it if it is not already registered.
*
* The object's name is the actor's registry key, which is what lets the same
* actor be placed by more than one object: the first placement creates it and
* binds the character named in the object's `character` property, and every
* later one just takes another reference. Either way the object's position,
* visibility, and layer are copied onto the actor and the object keeps a
* pointer to it.
*
* Not declared in tilemap.h -- reachable only through
* akgl_tilemap_load_layer_objects.
*
* @param curobj The tilemap object, with its `name`, `x`, `y`, and
* `visible` already read from JSON. Required, unchecked,
* dereferenced at once.
* @param layerdatavalue The object's JSON, for the custom properties --
* `character` and `state` -- that are not part of Tiled's
* own object fields. Required.
* @param layerid The layer this object sits on, copied onto the actor.
* @param dirname Directory to resolve paths against. Accepted for
* signature consistency; nothing here uses it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_KEY If the object's name is empty -- an actor has to be
* addressable -- if the `character` or `state` property is absent, or if
* the named character is not in #AKGL_REGISTRY_CHARACTER.
* @throws AKERR_TYPE If `character` is not declared `string` or `state` not
* declared `int`.
* @throws AKERR_NULLPOINTER If akgl_actor_initialize refuses the name.
* @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
*
* @note An existing actor's `character` and position are overwritten by each
* later placement, so two objects naming one actor leave it wherever the
* last one put it rather than producing two.
*/
akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
@@ -384,33 +322,58 @@ akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *
PASS(errctx, akgl_get_json_array_value(root, "objects", &layerdata));
len = json_array_size((json_t *)layerdata);
curlayer = &dest->layers[layerid];
for ( j = 0; j < len; j++ ) {
PASS(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
curobj = &curlayer->objects[j];
PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
strncpy((char *)curobj->name, tmpstr->data, AKGL_ACTOR_MAX_NAME_LENGTH);
PASS(errctx, akgl_heap_release_string(tmpstr));
PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
PASS(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
if ( strcmp(tmpstr->data, "actor") == 0 ) {
PASS(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
} else if ( strcmp(tmpstr->data, "perspective") == 0 ) {
curobj->visible = false;
if ( strcmp((char *)curobj->name, "p_foreground") == 0 ) {
dest->p_foreground_y = curobj->y;
PASS(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_foreground_h));
PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_foreground_scale));
} else if ( strcmp((char *)curobj->name, "p_vanishing") == 0 ) {
dest->p_vanishing_y = curobj->y;
PASS(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
// The loop is the whole ATTEMPT body on purpose. A CATCH inside a loop
// breaks the loop rather than the block, which is only safe because there
// is nothing after it -- CLEANUP, PROCESS and FINISH still run and still
// propagate. Anything added after this loop has to account for that.
ATTEMPT {
for ( j = 0; j < len; j++ ) {
FAIL_NONZERO_BREAK(
errctx,
(j >= AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER),
AKERR_OUTOFBOUNDS,
"Object layer %d has more than %d objects",
layerid,
AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER
);
CATCH(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
curobj = &curlayer->objects[j];
// One scratch string for the whole walk, released once in CLEANUP.
// Releasing it after the name read -- which is what this used to do
// -- dropped its refcount to zero while the very next line went on
// using the same slot, because akgl_get_json_string_value reuses a
// non-NULL destination without taking another reference. Any other
// claim in between would have been handed the same string.
CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
strncpy((char *)curobj->name, tmpstr->data, AKGL_ACTOR_MAX_NAME_LENGTH);
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
CATCH(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
if ( strcmp(tmpstr->data, "actor") == 0 ) {
CATCH(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
} else if ( strcmp(tmpstr->data, "perspective") == 0 ) {
curobj->visible = false;
if ( strcmp((char *)curobj->name, "p_foreground") == 0 ) {
dest->p_foreground_y = curobj->y;
CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_foreground_h));
CATCH(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_foreground_scale));
} else if ( strcmp((char *)curobj->name, "p_vanishing") == 0 ) {
dest->p_vanishing_y = curobj->y;
CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
CATCH(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
}
}
layerdatavalue = NULL;
}
layerdatavalue = NULL;
}
} CLEANUP {
if ( tmpstr != NULL ) {
IGNORE(akgl_heap_release_string(tmpstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
@@ -444,34 +407,6 @@ akerr_ErrorContext *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *roo
SUCCEED_RETURN(errctx);
}
/**
* @brief Load one image layer: upload its image as a single texture.
*
* An image layer is one picture rather than a grid, which is what a parallax
* backdrop wants. The layer's `width` and `height` are taken from the loaded
* texture rather than from the JSON, so they are always the true pixel size.
*
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load_layers.
*
* @param dest The map to load into. Required.
* @param root The layer's JSON object. Required.
* @param layerid Which layer slot to fill. Not bounds-checked.
* @param dirname Directory to resolve the layer's `image` path against.
* Required. Joined with a `/` rather than run through
* akgl_path_relative, so unlike a tileset image this path is not
* canonicalized and an absolute one will not work.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
* @throws AKERR_KEY If the layer has no `image`.
* @throws AKERR_TYPE If `image` is not a string.
* @throws AKGL_ERR_SDL If the image cannot be loaded. The message carries
* `SDL_GetError()`.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @note The joined path is truncated at
* #AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE without complaint, so an
* over-long path fails as a missing file rather than as a length error.
*/
akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
@@ -496,7 +431,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *ro
);
RESTORE_GCC_WARNINGS
dest->layers[layerid].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)fpath->data);
dest->layers[layerid].texture = IMG_LoadTexture(akgl_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;
@@ -553,104 +488,83 @@ akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, a
layerid += 1;
}
} CLEANUP {
// One scratch string for every layer's type, given back once. It was
// claimed by the first accessor call and never released, which is one
// of the two pool strings a map load used to keep for good.
if ( tmpstr != NULL ) {
IGNORE(akgl_heap_release_string(tmpstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Give the map its own physics backend, if it asked for one.
*
* A map with a `physics.model` custom property gets its own backend, configured
* from `physics.gravity.x`/`.y`/`.z` and `physics.drag.x`/`.y`/`.z`, and
* `use_own_physics` is set so a caller knows to simulate through it. Each
* constant defaults to 0.0 when absent, so a map can name a model and override
* only what it cares about.
*
* Absence is the normal case at every level: a map with no properties at all, or
* with properties but no `physics.model`, uses the game's global backend and
* this returns success. That is why the AKERR_KEY handlers here are not error
* paths -- they are the "map did not ask" path.
*
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load.
*
* @param dest The map to configure. Required in practice; not checked here,
* since akgl_tilemap_load has already done so.
* @param root The map's root JSON object. Required, likewise.
* @return `NULL` on success -- including when the map declared no physics at
* all -- otherwise an error context owned by the caller.
* @throws AKERR_KEY If `physics.model` names a backend that does not exist.
* @throws AKERR_TYPE If `physics.model` is not declared `string`, or one of the
* six constants is not declared `float`.
* @throws AKERR_NULLPOINTER If akgl_physics_factory refuses its arguments.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
{
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
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));
CATCH(errctx, akgl_heap_next_string(&tmpval));
CATCH(errctx, akgl_get_json_array_value((json_t *)root, "properties", &props));
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpval));
} PROCESS(e) {
} HANDLE(e, AKERR_KEY) {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_KEY) {
// Map has no properties, do nothing
SDL_Log("Map has no properties");
SUCCEED_RETURN(e);
} FINISH(e, true);
SUCCEED_RETURN(errctx);
} FINISH(errctx, true);
ATTEMPT {
CATCH(e, akgl_heap_next_string(&tmpval));
CATCH(e, akgl_get_json_properties_string(
CATCH(errctx, akgl_heap_next_string(&tmpval));
CATCH(errctx, akgl_get_json_properties_string(
root,
"physics.model",
&tmpval
)
);
PASS(e, akgl_physics_factory(&dest->physics, tmpval));
PASS(errctx, akgl_physics_factory(&dest->physics, tmpval));
dest->use_own_physics = true;
CATCH(e, akgl_get_json_with_default(
CATCH(errctx, 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(
CATCH(errctx, 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(
CATCH(errctx, 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(
CATCH(errctx, 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(
CATCH(errctx, 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(
CATCH(errctx, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.drag.z", &dest->physics.drag_z
),
@@ -659,12 +573,12 @@ akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
sizeof(double)));
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpval));
} PROCESS(e) {
} HANDLE(e, AKERR_KEY) {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_KEY) {
SDL_Log("Map uses game physics");
} FINISH(e, true);
} FINISH(errctx, true);
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
@@ -686,7 +600,6 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
ATTEMPT {
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
//SDL_snprintf(tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
CATCH(errctx, aksl_realpath(fname, (char *)&dirnamestr->data, sizeof(dirnamestr->data)));
dirname((char *)&dirnamestr->data);
@@ -708,7 +621,7 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
dest->orientation = 0;
if ( (dest->width * dest->height) >= (AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT) ) {
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Map exceeds the maximum size");
FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Map exceeds the maximum size");
}
CATCH(errctx, akgl_tilemap_load_layers((akgl_Tilemap *)dest, (json_t *)json, dirnamestr));
@@ -728,7 +641,6 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
SDL_Log("Map perspective rate is %f", dest->p_rate);
}
} CLEANUP {
//IGNORE(akgl_heap_release_string(tmpstr));
// The map is built entirely out of copies -- layer data, tileset
// geometry, object names -- so the document is dead the moment the
// loaders above return, whether they succeeded or not. It is also the
@@ -737,6 +649,13 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
json_decref(json);
json = NULL;
}
// And the directory the map's relative paths resolve against, which
// was claimed above and never released -- the other of the two pool
// strings a map load kept.
if ( dirnamestr != NULL ) {
IGNORE(akgl_heap_release_string(dirnamestr));
dirnamestr = NULL;
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
@@ -798,7 +717,7 @@ akerr_ErrorContext *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, in
src.h = map->layers[layeridx].height;
dest.w = map->layers[layeridx].width;
dest.h = map->layers[layeridx].height;
PASS(errctx, renderer->draw_texture(renderer, map->layers[layeridx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, map->layers[layeridx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
SUCCEED_RETURN(errctx);
}
@@ -853,7 +772,7 @@ akerr_ErrorContext *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, in
dest.y,
dest.w,
dest.h);*/
PASS(errctx, renderer->draw_texture(renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
}
}
dest.x += map->tilewidth;
@@ -908,17 +827,17 @@ akerr_ErrorContext *akgl_tilemap_draw_tileset(akgl_Tilemap *map, int tilesetidx)
dest.y,
dest.w,
dest.h);*/
PASS(errctx, renderer->draw_texture(renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor)
akerr_ErrorContext *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor)
{
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "NULL map");
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "NULL map");
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "NULL actor");
if ( actor->y <= map->p_vanishing_y ) {
actor->scale = map->p_vanishing_scale;
@@ -927,26 +846,32 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, a
} else {
actor->scale = map->p_foreground_scale - (map->p_rate * (map->p_foreground_y - actor->y));
}
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest)
akerr_ErrorContext *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");
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL map");
int i = 0;
// Each pointer is cleared as it goes. Without that a second release
// destroys textures SDL has already freed, and the second loop used to
// destroy `tilesets[i]` while testing `layers[i]` -- so every tileset
// texture was freed twice and no image layer's texture was freed at all.
for ( i = 0; i < AKGL_TILEMAP_MAX_TILESETS; i++ ) {
if ( dest->tilesets[i].texture != NULL ) {
SDL_DestroyTexture(dest->tilesets[i].texture);
dest->tilesets[i].texture = NULL;
}
}
for ( i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
if ( dest->layers[i].texture != NULL ) {
SDL_DestroyTexture(dest->tilesets[i].texture);
SDL_DestroyTexture(dest->layers[i].texture);
dest->layers[i].texture = NULL;
}
}
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}

View File

@@ -21,13 +21,14 @@
#include <akstdlib.h>
/**
* @brief Resolve @p path against @p root and write the absolute result to @p dst.
* @brief Resolve @p path against @p root and write the absolute result to @p dest.
*
* The second half of akgl_path_relative: joins the two with a `/` and resolves
* the join, so symlinks and `..` are folded out and the result is absolute. It
* is the fallback, reached only once resolving @p path on its own has failed.
*
* Not declared in util.h -- it is reachable only through akgl_path_relative.
* `static`: it is reachable only through akgl_path_relative, which is the only
* caller and the only sensible one.
*
* @param root Directory to resolve against, normally `dirname` of the file that
* named @p path. Required. A trailing `/` is harmless; the join adds
@@ -35,10 +36,10 @@
* @param path Relative path to resolve. Required. An absolute @p path still gets
* @p root pasted in front of it, which will not exist -- so callers
* must not send absolute paths down this branch.
* @param dst Receives the resolved absolute path. Required, and must already be
* @param dest Receives the resolved absolute path. Required, and must already be
* a claimed pool string.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dst is `NULL`.
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dest is `NULL`.
* @throws AKERR_OUTOFBOUNDS If `root + path` is at least #AKGL_MAX_STRING_LENGTH
* bytes. The message reports both the combined length and the limit.
* @throws ENOENT If the joined path does not exist. Any other `errno`
@@ -50,32 +51,31 @@
* scratch strings are never released. This is exactly the hazard AGENTS.md
* warns about; it wants a `FAIL_BREAK`.
*/
akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String *dst)
static akerr_ErrorContext *path_relative_root(char *root, char *path, akgl_String *dest)
{
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
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");
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL argument");
PASS(e, akgl_heap_next_string(&strbuf));
PASS(e, akgl_heap_next_string(&pathbuf));
PASS(errctx, akgl_heap_next_string(&strbuf));
PASS(errctx, 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);
FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Total path length (%d) is greater than maximum akgl_String length (%d)", (rootlen + pathlen), AKGL_MAX_STRING_LENGTH);
}
DISABLE_GCC_WARNING_FORMAT_TRUNCATION
CATCH(e, aksl_snprintf(
CATCH(errctx, aksl_snprintf(
&count,
(char *)&pathbuf->data,
sizeof(pathbuf->data),
@@ -84,38 +84,37 @@ akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String
path
));
RESTORE_GCC_WARNINGS
CATCH(e, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data, sizeof(strbuf->data)));
CATCH(e, akgl_string_copy(strbuf, dst, 0));
CATCH(errctx, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data, sizeof(strbuf->data)));
CATCH(errctx, akgl_string_copy(strbuf, dest, 0));
} CLEANUP {
IGNORE(akgl_heap_release_string(strbuf));
IGNORE(akgl_heap_release_string(pathbuf));
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dest)
{
PREPARE_ERROR(e);
PREPARE_ERROR(errctx);
akgl_String *strbuf;
char *result;
bool relative_to_root = false;
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL argument");
PASS(e, akgl_heap_next_string(&strbuf));
PASS(errctx, akgl_heap_next_string(&strbuf));
ATTEMPT {
// Is path relative to our current working directory?
CATCH(e, aksl_realpath(path, (char *)&strbuf->data, sizeof(strbuf->data)));
CATCH(errctx, aksl_realpath(path, (char *)&strbuf->data, sizeof(strbuf->data)));
// Yes it is. strbuf->data contains the absolute path.
CATCH(e, akgl_string_copy(strbuf, dst, 0));
CATCH(errctx, akgl_string_copy(strbuf, dest, 0));
} CLEANUP {
IGNORE(akgl_heap_release_string(strbuf));
} PROCESS(e) {
} HANDLE(e, ENOENT) {
} PROCESS(errctx) {
} HANDLE(errctx, ENOENT) {
// Path is not relative to our current working directory. Resolve it
// against root instead -- but after FINISH, not from in here. Returning
// from inside a HANDLE block skips the RELEASE_ERROR that FINISH ends
@@ -125,54 +124,18 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
// context from the array!". Every map load resolves several paths this
// way.
relative_to_root = true;
} FINISH(e, true);
} FINISH(errctx, true);
if ( relative_to_root == true ) {
PASS(e, akgl_path_relative_root(root, path, dst));
PASS(errctx, path_relative_root(root, path, dest));
}
SUCCEED_RETURN(e);
SUCCEED_RETURN(errctx);
}
/**
* @brief Intended to resolve @p path against the directory holding @p from. Unfinished.
*
* The shape is there -- resolve @p from, take its `dirname` -- but the body
* stops before it does anything with @p path, and `*dst` is never written. Not
* declared in util.h, and nothing in the tree calls it.
*
* @param path The path to resolve. Required, and currently ignored past the
* `NULL` check.
* @param from A file whose directory @p path should be taken as relative to.
* Required. Must exist -- it is resolved with `realpath(3)`.
* @param dst Intended to receive the resolved path. Never written. Note the
* double indirection, which disagrees with the rest of the
* `akgl_path_relative*` family; see TODO.md item 40.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p path or @p from is `NULL`.
* @throws ENOENT If @p from does not exist, along with any other `errno`
* `realpath(3)` raises.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @warning Known defect: the scratch string it claims is never released, so 256
* calls exhaust the string pool. TODO.md, "Known and still open" item 4.
*/
akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String **dst)
{
akgl_String *dirnamestr;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path");
FAIL_ZERO_RETURN(e, from, AKERR_NULLPOINTER, "from");
PASS(e, akgl_heap_next_string(&dirnamestr));
PASS(e, aksl_realpath(from, (char *)&dirnamestr->data, sizeof(dirnamestr->data)));
dirname((char *)&dirnamestr->data);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect)
akerr_ErrorContext *akgl_rectangle_points(akgl_RectanglePoints *dest, SDL_FRect *rect)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL akgl_RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL Rectangle reference");
dest->topleft.x = rect->x;
dest->topleft.y = rect->y;
@@ -185,11 +148,11 @@ akerr_ErrorContext *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_collide_point_rectangle(point *p, RectanglePoints *rp, bool *collide)
akerr_ErrorContext *akgl_collide_point_rectangle(akgl_Point *p, akgl_RectanglePoints *rp, bool *collide)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, p, AKERR_NULLPOINTER, "NULL Point reference");
FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL akgl_RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL boolean reference");
if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) &&
(p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) {
@@ -202,8 +165,8 @@ akerr_ErrorContext *akgl_collide_point_rectangle(point *p, RectanglePoints *rp,
akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide)
{
RectanglePoints r1p;
RectanglePoints r2p;
akgl_RectanglePoints r1p;
akgl_RectanglePoints r2p;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, r1, AKERR_NULLPOINTER, "NULL rectangle reference");
FAIL_ZERO_RETURN(errctx, r2, AKERR_NULLPOINTER, "NULL rectangle reference");
@@ -215,41 +178,43 @@ akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *
// is the upper left corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topleft, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
if ( *collide == true ) { break; }
// is the upper left corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topleft, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
if ( *collide == true ) { break; }
// is the top right corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topright, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
if ( *collide == true ) { break; }
// is the top right corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topright, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
if ( *collide == true ) { break; }
// is the bottom left corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomleft, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
if ( *collide == true ) { break; }
// is the bottom left corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomleft, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
if ( *collide == true ) { break; }
// is the bottom right corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomright, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
if ( *collide == true ) { break; }
// is the bottom right corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomright, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
if ( *collide == true ) { break; }
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
*collide = false;
// No trailing `*collide = false;` here. Each corner test writes the flag
// itself, so the eighth one leaves it false when nothing hit; assigning
// after FINISH would overwrite the hit that broke out of the block early.
SUCCEED_RETURN(errctx);
}
@@ -259,6 +224,30 @@ akerr_ErrorContext *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2)
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, s1, AKERR_NULLPOINTER, "NULL Surface pointer");
FAIL_ZERO_RETURN(errctx, s2, AKERR_NULLPOINTER, "NULL Surface pointer");
// Geometry first. The memcmp reads s1->pitch * s1->h bytes out of *both*,
// so a smaller s2 was read past its end rather than reported as a
// mismatch -- and a differing pitch or format made the comparison
// meaningless even when it did not run off.
FAIL_NONZERO_RETURN(
errctx,
((s1->w != s2->w) || (s1->h != s2->h)),
AKERR_VALUE,
"Comparison surfaces differ in size: %dx%d against %dx%d",
s1->w, s1->h, s2->w, s2->h);
FAIL_NONZERO_RETURN(
errctx,
(s1->pitch != s2->pitch),
AKERR_VALUE,
"Comparison surfaces differ in pitch: %d against %d",
s1->pitch, s2->pitch);
FAIL_NONZERO_RETURN(
errctx,
(s1->format != s2->format),
AKERR_VALUE,
"Comparison surfaces differ in pixel format: %s against %s",
SDL_GetPixelFormatName(s1->format), SDL_GetPixelFormatName(s2->format));
FAIL_ZERO_RETURN(errctx, s1->pixels, AKERR_NULLPOINTER, "First surface has no pixels");
FAIL_ZERO_RETURN(errctx, s2->pixels, AKERR_NULLPOINTER, "Second surface has no pixels");
FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), AKERR_VALUE, "Comparison surfaces are not equal");
SUCCEED_RETURN(errctx);
}
@@ -278,9 +267,9 @@ akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, in
FAIL_ZERO_BREAK(errctx, t2, AKERR_NULLPOINTER, "NULL texture");
CATCH(errctx, akgl_heap_next_string(&tmpstring));
SDL_RenderClear(renderer->sdl_renderer);
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
s1 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
SDL_RenderClear(akgl_renderer->sdl_renderer);
CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
s1 = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, &read);
FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer");
if ( writeout != NULL ) {
@@ -294,10 +283,10 @@ akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, in
SDL_GetError());
}
SDL_RenderClear(renderer->sdl_renderer);
SDL_RenderClear(akgl_renderer->sdl_renderer);
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
s2 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, t2, &src, &dest, 0, NULL, SDL_FLIP_NONE));
s2 = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, &read);
FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer");
CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2));

View File

@@ -17,6 +17,7 @@
#include <akgl/sprite.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/renderer.h>
#include "testutil.h"
@@ -266,10 +267,10 @@ akerr_ErrorContext *test_actor_manage_children(void)
CATCH(errctx, akgl_heap_release_actor(parent));
// All actor objects on the heap should be empty now
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++) {
FAIL_NONZERO_BREAK(errctx, HEAP_ACTOR[i].refcount, AKERR_VALUE, "Actor not properly cleared");
FAIL_NONZERO_BREAK(errctx, HEAP_ACTOR[i].parent, AKERR_VALUE, "Actor not properly cleared");
FAIL_NONZERO_BREAK(errctx, akgl_heap_actors[i].refcount, AKERR_VALUE, "Actor not properly cleared");
FAIL_NONZERO_BREAK(errctx, akgl_heap_actors[i].parent, AKERR_VALUE, "Actor not properly cleared");
for ( j = 0 ; j < AKGL_ACTOR_MAX_CHILDREN; j++) {
if ( HEAP_ACTOR[i].children[j] != NULL ) {
if ( akgl_heap_actors[i].children[j] != NULL ) {
FAIL(errctx, AKERR_VALUE, "Actor not properly cleared");
goto _test_actor_addchild_heaprelease_cleanup;
}
@@ -375,7 +376,7 @@ akerr_ErrorContext *test_actor_control_handlers_on(void)
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
actor.state = (AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_ALIVE);
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_on(&actor, &event), "left on");
TEST_EXPECT_OK(e, akgl_actor_cmhf_left_on(&actor, &event), "left on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
"left on did not set MOVING_LEFT (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
@@ -391,7 +392,7 @@ akerr_ErrorContext *test_actor_control_handlers_on(void)
// Right: same, with the sign preserved.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_on(&actor, &event), "right on");
TEST_EXPECT_OK(e, akgl_actor_cmhf_right_on(&actor, &event), "right on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"right on did not set MOVING_RIGHT (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
@@ -401,7 +402,7 @@ akerr_ErrorContext *test_actor_control_handlers_on(void)
// Up reverses the Y acceleration, because Y grows downward.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_up_on(&actor, &event), "up on");
TEST_EXPECT_OK(e, akgl_actor_cmhf_up_on(&actor, &event), "up on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
"up on did not set MOVING_UP (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_UP),
@@ -410,7 +411,7 @@ akerr_ErrorContext *test_actor_control_handlers_on(void)
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_down_on(&actor, &event), "down on");
TEST_EXPECT_OK(e, akgl_actor_cmhf_down_on(&actor, &event), "down on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
"down on did not set MOVING_DOWN (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_DOWN),
@@ -420,8 +421,8 @@ akerr_ErrorContext *test_actor_control_handlers_on(void)
// Each direction is exclusive: turning right after left leaves only right.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_on(&actor, &event), "left on before turning");
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_on(&actor, &event), "right on after left");
TEST_EXPECT_OK(e, akgl_actor_cmhf_left_on(&actor, &event), "left on before turning");
TEST_EXPECT_OK(e, akgl_actor_cmhf_right_on(&actor, &event), "right on after left");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
"turning right left MOVING_LEFT set (state %d)", actor.state);
} CLEANUP {
@@ -450,7 +451,7 @@ akerr_ErrorContext *test_actor_control_handlers_off(void)
actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT);
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_off(&actor, &event), "left off");
TEST_EXPECT_OK(e, akgl_actor_cmhf_left_off(&actor, &event), "left off");
TEST_ASSERT_FEQ(e, actor.ax, 0.0f, "left off left ax at %f", actor.ax);
TEST_ASSERT_FEQ(e, actor.ex, 0.0f, "left off left ex at %f", actor.ex);
TEST_ASSERT_FEQ(e, actor.tx, 0.0f, "left off left tx at %f", actor.tx);
@@ -466,7 +467,7 @@ akerr_ErrorContext *test_actor_control_handlers_off(void)
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_off(&actor, &event), "right off");
TEST_EXPECT_OK(e, akgl_actor_cmhf_right_off(&actor, &event), "right off");
TEST_ASSERT_FEQ(e, actor.vx, 0.0f, "right off left vx at %f", actor.vx);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"right off did not clear MOVING_RIGHT (state %d)", actor.state);
@@ -474,7 +475,7 @@ akerr_ErrorContext *test_actor_control_handlers_off(void)
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_up_off(&actor, &event), "up off");
TEST_EXPECT_OK(e, akgl_actor_cmhf_up_off(&actor, &event), "up off");
TEST_ASSERT_FEQ(e, actor.vy, 0.0f, "up off left vy at %f", actor.vy);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
"up off did not clear MOVING_UP (state %d)", actor.state);
@@ -482,7 +483,7 @@ akerr_ErrorContext *test_actor_control_handlers_off(void)
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_down_off(&actor, &event), "down off");
TEST_EXPECT_OK(e, akgl_actor_cmhf_down_off(&actor, &event), "down off");
TEST_ASSERT_FEQ(e, actor.vy, 0.0f, "down off left vy at %f", actor.vy);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
"down off did not clear MOVING_DOWN (state %d)", actor.state);
@@ -502,33 +503,33 @@ akerr_ErrorContext *test_actor_control_handlers_nullpointers(void)
memset(&event, 0x00, sizeof(SDL_Event));
memset(&actor, 0x00, sizeof(akgl_Actor));
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(NULL, &event), "left on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_off(NULL, &event), "left off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(NULL, &event), "right on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_off(NULL, &event), "right off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(NULL, &event), "up on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_off(NULL, &event), "up off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(NULL, &event), "down on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_off(NULL, &event), "down off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_on(NULL, &event), "left on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_off(NULL, &event), "left off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_on(NULL, &event), "right on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_off(NULL, &event), "right off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_on(NULL, &event), "up on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_off(NULL, &event), "up off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_on(NULL, &event), "down on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_off(NULL, &event), "down off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(&actor, NULL), "left on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_off(&actor, NULL), "left off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(&actor, NULL), "right on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_off(&actor, NULL), "right off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(&actor, NULL), "up on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_off(&actor, NULL), "up off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(&actor, NULL), "down on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_off(&actor, NULL), "down off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_on(&actor, NULL), "left on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_off(&actor, NULL), "left off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_on(&actor, NULL), "right on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_off(&actor, NULL), "right off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_on(&actor, NULL), "up on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_off(&actor, NULL), "up off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_on(&actor, NULL), "down on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_off(&actor, NULL), "down off, NULL event");
// A movement handler reads acceleration off the base character, so an
// actor without one has to be reported rather than dereferenced.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(&actor, &event),
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_on(&actor, &event),
"left on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(&actor, &event),
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_on(&actor, &event),
"right on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(&actor, &event),
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_on(&actor, &event),
"up on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(&actor, &event),
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_on(&actor, &event),
"down on, actor with no base character");
} CLEANUP {
} PROCESS(e) {
@@ -879,12 +880,12 @@ akerr_ErrorContext *test_actor_sprite_sheet_coords(void)
sprite.width = 32;
sprite.height = 32;
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(NULL, &coords, 0),
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_spritesheet_coords_for_frame(NULL, &coords, 0),
"sheet coords with a NULL sprite");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(&sprite, NULL, 0),
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_spritesheet_coords_for_frame(&sprite, NULL, 0),
"sheet coords with a NULL rectangle");
// The sprite has no sheet attached, so there is no texture to measure against.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(&sprite, &coords, 0),
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_spritesheet_coords_for_frame(&sprite, &coords, 0),
"sheet coords with a NULL spritesheet");
} CLEANUP {
} PROCESS(e) {
@@ -892,6 +893,122 @@ akerr_ErrorContext *test_actor_sprite_sheet_coords(void)
SUCCEED_RETURN(e);
}
/*
* A render backend whose draw_texture records what it was asked to draw instead
* of drawing it. akgl_actor_render's whole output is the rectangle it hands to
* draw_texture, so that rectangle is the only thing there is to assert on.
*/
static akgl_RenderBackend testbackend;
static SDL_FRect lastdrawdest;
static SDL_FRect lastdrawsrc;
static int drawcalls = 0;
static akerr_ErrorContext *record_draw_texture(
akgl_RenderBackend *self,
SDL_Texture *texture,
SDL_FRect *src,
SDL_FRect *dest,
double angle,
SDL_FPoint *center,
SDL_FlipMode flip)
{
PREPARE_ERROR(errctx);
if ( src != NULL ) {
lastdrawsrc = *src;
}
if ( dest != NULL ) {
lastdrawdest = *dest;
}
drawcalls += 1;
SUCCEED_RETURN(errctx);
}
/**
* @brief akgl_actor_render must take the drawn height from the sprite's height.
*
* It read `curSprite->width` for both, so every actor was drawn square and a
* non-square sprite was stretched or squashed. Invisible in the fixtures
* because they are square, which is exactly why this test uses a sprite that
* is not: 48 wide by 24 high, so a regression puts 48 in both.
*
* This is also the first test of akgl_actor_render at all -- everything else in
* this file stubs renderfunc out.
*/
akerr_ErrorContext *test_actor_render_uses_sprite_height(void)
{
PREPARE_ERROR(e);
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
akgl_Sprite *sprite = NULL;
SDL_Texture *sheettexture = NULL;
ATTEMPT {
CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "renderactor", AKGL_ACTOR_STATE_ALIVE));
// akgl_spritesheet_coords_for_frame reads sheet->texture->w, so the
// sheet needs a real one even though nothing is rasterized.
sheettexture = SDL_CreateTexture(
testbackend.sdl_renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
96, 48);
FAIL_ZERO_BREAK(e, sheettexture, AKGL_ERR_SDL, "%s", SDL_GetError());
sprite->sheet->texture = sheettexture;
sprite->width = 48;
sprite->height = 24;
sprite->frames = 2;
actor->curSpriteFrameId = 0;
actor->visible = true;
actor->scale = 1.0f;
actor->x = 0;
actor->y = 0;
akgl_camera->x = 0;
akgl_camera->y = 0;
akgl_camera->w = 640;
akgl_camera->h = 480;
drawcalls = 0;
TEST_EXPECT_OK(e, akgl_actor_render(actor), "rendering a non-square actor");
TEST_ASSERT(e, drawcalls == 1, "actor render made %d draw calls, expected 1", drawcalls);
TEST_ASSERT_FEQ(e, lastdrawdest.w, 48.0f,
"drawn width is %f, expected the sprite's 48", lastdrawdest.w);
TEST_ASSERT_FEQ(e, lastdrawdest.h, 24.0f,
"drawn height is %f, expected the sprite's 24 -- it used to take the width",
lastdrawdest.h);
// The source rectangle has always come from the sprite properly; assert
// it too, so a regression cannot move the bug one line up.
TEST_ASSERT_FEQ(e, lastdrawsrc.w, 48.0f, "source width is %f, expected 48", lastdrawsrc.w);
TEST_ASSERT_FEQ(e, lastdrawsrc.h, 24.0f, "source height is %f, expected 24", lastdrawsrc.h);
// Scale multiplies both, and must not collapse them back together.
actor->scale = 2.0f;
drawcalls = 0;
TEST_EXPECT_OK(e, akgl_actor_render(actor), "rendering a scaled non-square actor");
TEST_ASSERT_FEQ(e, lastdrawdest.w, 96.0f, "scaled width is %f, expected 96", lastdrawdest.w);
TEST_ASSERT_FEQ(e, lastdrawdest.h, 48.0f, "scaled height is %f, expected 48", lastdrawdest.h);
} CLEANUP {
if ( sprite != NULL && sprite->sheet != NULL ) {
sprite->sheet->texture = NULL;
}
if ( sheettexture != NULL ) {
SDL_DestroyTexture(sheettexture);
}
if ( actor != NULL ) {
IGNORE(akgl_heap_release_actor(actor));
}
if ( basechar != NULL ) {
IGNORE(akgl_heap_release_character(basechar));
}
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
int main(void)
{
akgl_actor_updated = 0;
@@ -904,11 +1021,28 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_registry_init_actor());
CATCH(errctx, akgl_registry_init_sprite());
CATCH(errctx, akgl_registry_init_spritesheet());
CATCH(errctx, akgl_registry_init_character());
// A renderer, so akgl_actor_render can be driven at all. The backend is
// bound and then has its draw_texture replaced, which is the point:
// what the function computes is only visible in what it hands over.
if ( !SDL_Init(SDL_INIT_VIDEO) ) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
}
memset(&testbackend, 0x00, sizeof(akgl_RenderBackend));
if ( !SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_actor", 640, 480,
SDL_WINDOW_HIDDEN, &akgl_window, &testbackend.sdl_renderer) ) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
}
CATCH(errctx, akgl_render_2d_bind(&testbackend));
testbackend.draw_texture = &record_draw_texture;
akgl_renderer = &testbackend;
akgl_camera = &akgl_default_camera;
CATCH(errctx, test_registry_actor_iterator_nullpointers());
CATCH(errctx, test_registry_actor_iterator_missingactor());
CATCH(errctx, test_registry_actor_iterator_updaterender());
@@ -924,6 +1058,7 @@ int main(void)
CATCH(errctx, test_actor_update());
CATCH(errctx, test_actor_character_sprite_binding());
CATCH(errctx, test_actor_sprite_sheet_coords());
CATCH(errctx, test_actor_render_uses_sprite_height());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,225 @@
{
"tilesets": [
{
"columns": 48,
"firstgid": 1,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_0",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 1729,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_1",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 3457,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_2",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 5185,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_3",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 6913,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_4",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 8641,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_5",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 10369,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_6",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 12097,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_7",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 13825,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_8",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 15553,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_9",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 17281,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_10",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 19009,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_11",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 20737,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_12",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 22465,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_13",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 24193,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_14",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 25921,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_15",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
},
{
"columns": 48,
"firstgid": 27649,
"image": "assets/World_A1.png",
"imageheight": 576,
"imagewidth": 768,
"margin": 0,
"name": "World_A1_16",
"spacing": 0,
"tilecount": 1728,
"tileheight": 16,
"tilewidth": 16
}
]
}

View File

@@ -0,0 +1,31 @@
{
"spritesheet": {
"filename": "spritesheet.png",
"frame_width": 48,
"frame_height": 48
},
"name": "testsprite_maxframes",
"width": 48,
"height": 48,
"speed": 100,
"loop": true,
"loopReverse": true,
"frames": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15
]
}

View File

@@ -0,0 +1,32 @@
{
"spritesheet": {
"filename": "spritesheet.png",
"frame_width": 48,
"frame_height": 48
},
"name": "testsprite_toomanyframes",
"width": 48,
"height": 48,
"speed": 100,
"loop": true,
"loopReverse": true,
"frames": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16
]
}

View File

@@ -0,0 +1,17 @@
{
"spritesheet": {
"filename": "spritesheet.png",
"frame_width": 48,
"frame_height": 48
},
"name": "testsprite_widecount",
"width": 48,
"height": 48,
"speed": 100,
"loop": true,
"loopReverse": true,
"frames": [
0,
256
]
}

View File

@@ -602,6 +602,7 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
FAIL_ZERO_BREAK(
errctx,
SDL_Init(SDL_INIT_AUDIO),

View File

@@ -1,9 +1,21 @@
/**
* @file bitmasks.c
* @brief The AKGL_BITMASK_* macros, including the ways an unparenthesized one breaks.
*
* These are macros, so the only thing that checks them is a call site. Most of
* this file is the ordinary arithmetic; the composition cases at the bottom are
* the ones that matter, because until 0.5.0 the macros expanded to bare
* expressions and every one of them was a silent misparse waiting for a caller.
*/
#include <akgl/game.h>
#include <akgl/actor.h>
int main(void)
{
int mask = 0;
int counter = 0;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_ALIVE);
if ( mask != AKGL_ACTOR_STATE_ALIVE )
return 1;
@@ -27,5 +39,70 @@ int main(void)
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_DOWN);
if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN) )
return 1;
// ---- composition, which is what the parentheses are for ----
// Negation. Unparenthesized, AKGL_BITMASK_HAS expanded to a bare
// `(x & y) == y`, so `!` bound to the `&` and the whole thing read as
// `!(mask & bit) == bit`.
//
// Note which case catches that and which does not. For a bit that *is*
// set, `!(nonzero)` is 0 and `0 == bit` is false -- the same answer the
// correct parse gives, so a test written that way passes either way. It
// only diverges for a bit that is *not* set and whose value is not 1:
// `!(0)` is 1, and `1 == 64` is false where the answer should be true.
// That is the shape below, and it is why this was worth a real case rather
// than an obvious one.
mask = AKGL_ACTOR_STATE_ALIVE;
if ( !AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_ALIVE) )
return 1;
if ( AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_DEAD) )
return 1;
if ( !AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_DEAD) ) {
counter = 1;
} else {
return 1;
}
if ( counter != 1 )
return 1;
// The same divergence through AKGL_BITMASK_HASNOT, which was written the
// same way: `!(0) != 64` is true where the answer should be false.
if ( !AKGL_BITMASK_HASNOT(mask, AKGL_ACTOR_STATE_DEAD) )
return 1;
if ( AKGL_BITMASK_HASNOT(mask, AKGL_ACTOR_STATE_ALIVE) )
return 1;
// As an operand of a lower-precedence operator.
if ( AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_ALIVE) == false )
return 1;
if ( (AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_ALIVE) & 1) != 1 )
return 1;
// The mutating macros as expressions rather than statements, and with an
// argument that is itself an expression: `x |= y` unparenthesized cannot be
// used in either position.
mask = 0;
counter = (AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_DYING));
if ( counter != AKGL_ACTOR_STATE_DYING )
return 1;
if ( (AKGL_BITMASK_CLEAR(mask)) != 0 )
return 1;
if ( mask != 0 )
return 1;
// AKGL_BITMASK_CLEAR carries no trailing semicolon, so it is a statement
// like any other and does not swallow the one after it when it is the whole
// body of an unbraced `if`.
mask = AKGL_ACTOR_STATE_ALIVE;
counter = 0;
if ( mask != 0 )
AKGL_BITMASK_CLEAR(mask);
counter = 1;
if ( mask != 0 )
return 1;
if ( counter != 1 )
return 1;
return 0;
}

View File

@@ -7,9 +7,21 @@
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/iterator.h>
#include <akgl/game.h>
#include <akgl/renderer.h>
SDL_Window *window;
SDL_Renderer *renderer;
#include "testutil.h"
/*
* This suite used to declare its own file-scope `window` and `renderer`, back
* when the library's globals carried those bare names too. Both definitions had
* external linkage and the same spelling, so the executable's preempted the
* shared library's: akgl_sprite_load_json read the test's `SDL_Renderer *`
* through an `akgl_RenderBackend *` and happened not to crash. That is the
* collision the akgl_ prefix on exported globals exists to prevent, and it was
* invisible until they were renamed. The suite now binds a real backend, the
* way tests/sprite.c and tests/text.c do.
*/
akerr_ErrorContext *test_akgl_character_initialize()
{
@@ -191,20 +203,102 @@ akerr_ErrorContext *test_akgl_character_load_json()
SUCCEED_RETURN(errctx);
}
/**
* @brief Rebinding a state must release the sprite it displaces.
*
* akgl_character_sprite_add took a reference for every binding and wrote over
* any existing entry without releasing the one it replaced, so a character that
* rebinds a state while alive leaked a sprite slot per rebind. Releasing at
* teardown does not cover it: the map only holds the last binding by then, so
* the earlier ones are unreachable.
*
* The write itself was also unchecked, so a failure to record the binding was
* reported as success -- with the reference already taken.
*/
akerr_ErrorContext *test_character_sprite_rebind_releases_displaced(void)
{
PREPARE_ERROR(errctx);
akgl_Character *testchar = NULL;
akgl_Sprite *first = NULL;
akgl_Sprite *second = NULL;
akgl_Sprite *found = NULL;
int firstbefore = 0;
int secondbefore = 0;
int i = 0;
ATTEMPT {
CATCH(errctx, akgl_heap_next_character(&testchar));
CATCH(errctx, akgl_character_initialize(testchar, "rebindchar"));
// Real sprites out of the fixtures: akgl_sprite_initialize refuses a
// NULL spritesheet, and a hand-built sprite would not be a sprite.
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
first = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
second = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
FAIL_ZERO_BREAK(errctx, first, AKERR_KEY, "testsprite is not in the registry");
FAIL_ZERO_BREAK(errctx, second, AKERR_KEY, "testsprite2 is not in the registry");
firstbefore = first->refcount;
secondbefore = second->refcount;
CATCH(errctx, akgl_character_sprite_add(testchar, first, AKGL_ACTOR_STATE_ALIVE));
TEST_ASSERT(errctx, first->refcount == (firstbefore + 1),
"binding a sprite did not take a reference (%d, expected %d)",
first->refcount, firstbefore + 1);
// Rebind the same state to a different sprite.
CATCH(errctx, akgl_character_sprite_add(testchar, second, AKGL_ACTOR_STATE_ALIVE));
TEST_ASSERT(errctx, second->refcount == (secondbefore + 1),
"rebinding did not take a reference on the new sprite (%d, expected %d)",
second->refcount, secondbefore + 1);
TEST_ASSERT(errctx, first->refcount == firstbefore,
"rebinding did not release the displaced sprite (%d, expected %d)",
first->refcount, firstbefore);
CATCH(errctx, testchar->sprite_get(testchar, AKGL_ACTOR_STATE_ALIVE, &found));
TEST_ASSERT(errctx, found == second, "the state is not bound to the new sprite");
// Rebinding a state to the sprite already there must be a no-op on the
// count rather than a release followed by a claim of the same slot.
CATCH(errctx, akgl_character_sprite_add(testchar, second, AKGL_ACTOR_STATE_ALIVE));
TEST_ASSERT(errctx, second->refcount == (secondbefore + 2),
"rebinding a state to the sprite it already held mishandled the count (%d)",
second->refcount);
// And a long run of rebinds must not drift.
for ( i = 0; i < 200; i++ ) {
CATCH(errctx, akgl_character_sprite_add(testchar, first, AKGL_ACTOR_STATE_DEAD));
CATCH(errctx, akgl_character_sprite_add(testchar, second, AKGL_ACTOR_STATE_DEAD));
}
TEST_ASSERT(errctx, first->refcount <= (firstbefore + 1),
"200 rebinds left the first sprite at refcount %d", first->refcount);
} CLEANUP {
if ( testchar != NULL ) {
IGNORE(akgl_heap_release_character(testchar));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
akgl_renderer = &akgl_default_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
}
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_character", 640, 480, SDL_WINDOW_HIDDEN, &window, &renderer)) {
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_character", 640, 480, SDL_WINDOW_HIDDEN, &akgl_window, &akgl_renderer->sdl_renderer)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
}
CATCH(errctx, akgl_render_2d_bind(akgl_renderer));
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
@@ -212,6 +306,7 @@ int main(void)
CATCH(errctx, test_character_sprite_mgmt());
CATCH(errctx, test_character_iterate_state_sprites());
CATCH(errctx, test_akgl_character_load_json());
CATCH(errctx, test_character_sprite_rebind_releases_displaced());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);

View File

@@ -76,7 +76,7 @@ int main(void)
actorptr->visible = true;
// set up the control map
controlmap = &GAME_ControlMaps[0];
controlmap = &akgl_controlmaps[0];
controlmap->kbid = 0;
controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
// Move down

View File

@@ -13,7 +13,7 @@
#include <akerror.h>
#include <akgl/error.h>
// For the akgl_Actor_cmhf_* handlers these tests bind. akgl/controller.h pulls
// For the akgl_actor_cmhf_* handlers these tests bind. akgl/controller.h pulls
// actor.h in for itself now; tests/headers.c is what keeps it doing so.
#include <akgl/actor.h>
#include <akgl/character.h>
@@ -24,16 +24,6 @@
#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. */
@@ -52,7 +42,7 @@ 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));
memset(&akgl_controlmaps[i], 0x00, sizeof(akgl_ControlMap));
}
}
@@ -129,30 +119,30 @@ akerr_ErrorContext *test_controller_pushmap(void)
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;
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,
TEST_ASSERT(e, akgl_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,
akgl_controlmaps[0].nextMap);
TEST_ASSERT(e, akgl_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,
TEST_ASSERT(e, akgl_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,
TEST_ASSERT(e, akgl_controlmaps[0].nextMap == 2,
"pushing a second control left nextMap at %d, expected 2",
GAME_ControlMaps[0].nextMap);
akgl_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);
TEST_ASSERT(e, akgl_controlmaps[3].nextMap == 1,
"map 3 nextMap is %d, expected 1", akgl_controlmaps[3].nextMap);
TEST_ASSERT(e, akgl_controlmaps[0].nextMap == 2,
"pushing into map 3 disturbed map 0 (nextMap %d)", akgl_controlmaps[0].nextMap);
// Filling a map exactly to capacity still succeeds.
reset_control_maps();
@@ -164,9 +154,9 @@ akerr_ErrorContext *test_controller_pushmap(void)
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,
TEST_ASSERT(e, akgl_controlmaps[0].nextMap == AKGL_MAX_CONTROLS,
"a full map reports nextMap %d, expected %d",
GAME_ControlMaps[0].nextMap, AKGL_MAX_CONTROLS);
akgl_controlmaps[0].nextMap, AKGL_MAX_CONTROLS);
// One past capacity is refused.
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_controller_pushmap(0, &control),
@@ -180,6 +170,18 @@ akerr_ErrorContext *test_controller_pushmap(void)
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS + 5, &control),
"pushing into a control map id past the limit");
// Negative ids index *before* akgl_controlmaps. Both entry points
// checked only the upper bound until 0.5.0.
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
akgl_controller_pushmap(-1, &control),
"pushing into a negative control map id");
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
akgl_controller_pushmap(-4096, &control),
"pushing into a far negative control map id");
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
akgl_controller_default(-1, "player", TEST_KBID, TEST_JSID),
"defaulting a negative control map id");
} CLEANUP {
reset_control_maps();
} PROCESS(e) {
@@ -198,38 +200,38 @@ akerr_ErrorContext *test_controller_default_bindings(void)
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,
TEST_ASSERT(e, akgl_controlmaps[0].target == player,
"the default map did not target the player actor");
TEST_ASSERT(e, GAME_ControlMaps[0].kbid == TEST_KBID,
TEST_ASSERT(e, akgl_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,
akgl_controlmaps[0].kbid, TEST_KBID);
TEST_ASSERT(e, akgl_controlmaps[0].jsid == TEST_JSID,
"the default map recorded gamepad %d, expected %d",
GAME_ControlMaps[0].jsid, TEST_JSID);
akgl_controlmaps[0].jsid, TEST_JSID);
// Four keyboard bindings then four gamepad bindings.
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 8,
TEST_ASSERT(e, akgl_controlmaps[0].nextMap == 8,
"the default map installed %d controls, expected 8",
GAME_ControlMaps[0].nextMap);
akgl_controlmaps[0].nextMap);
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].key == SDLK_DOWN,
TEST_ASSERT(e, akgl_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,
TEST_ASSERT(e, akgl_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,
TEST_ASSERT(e, akgl_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,
TEST_ASSERT(e, akgl_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,
TEST_ASSERT(e, akgl_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,
TEST_ASSERT(e, akgl_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,
TEST_ASSERT(e, akgl_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,
TEST_ASSERT(e, akgl_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.
@@ -424,7 +426,7 @@ akerr_ErrorContext *test_controller_gamepad_button_handlers(void)
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),
TEST_EXPECT_OK(e, akgl_controller_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);
@@ -432,7 +434,7 @@ akerr_ErrorContext *test_controller_gamepad_button_handlers(void)
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),
TEST_EXPECT_OK(e, akgl_controller_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);
@@ -441,55 +443,55 @@ akerr_ErrorContext *test_controller_gamepad_button_handlers(void)
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_EXPECT_OK(e, akgl_controller_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");
TEST_EXPECT_OK(e, akgl_controller_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_EXPECT_OK(e, akgl_controller_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");
TEST_EXPECT_OK(e, akgl_controller_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_EXPECT_OK(e, akgl_controller_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");
TEST_EXPECT_OK(e, akgl_controller_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),
TEST_EXPECT_OK(e, akgl_controller_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),
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_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),
akgl_controller_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),
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_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),
akgl_controller_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),
akgl_controller_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),
akgl_controller_handle_button_up(&appstate_placeholder, &event),
"dpad release handler with no player actor registered");
} CLEANUP {
} PROCESS(e) {
@@ -506,22 +508,22 @@ akerr_ErrorContext *test_controller_device_events(void)
// 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),
TEST_EXPECT_OK(e, akgl_controller_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),
TEST_EXPECT_OK(e, akgl_controller_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),
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_added(NULL, &event),
"add handler with a NULL appstate");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_added(&appstate_placeholder, NULL),
akgl_controller_handle_added(&appstate_placeholder, NULL),
"add handler with a NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_removed(NULL, &event),
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_removed(NULL, &event),
"remove handler with a NULL appstate");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_removed(&appstate_placeholder, NULL),
akgl_controller_handle_removed(&appstate_placeholder, NULL),
"remove handler with a NULL event");
} CLEANUP {
} PROCESS(e) {
@@ -856,6 +858,7 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
FAIL_ZERO_BREAK(
errctx,
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD),

View File

@@ -38,13 +38,13 @@ static akerr_ErrorContext *clear_target(void)
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(
errctx,
SDL_SetRenderDrawColor(renderer->sdl_renderer, 0x00, 0x00, 0x00, 0xff),
SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x00, 0x00, 0x00, 0xff),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
FAIL_ZERO_RETURN(
errctx,
SDL_RenderClear(renderer->sdl_renderer),
SDL_RenderClear(akgl_renderer->sdl_renderer),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
@@ -80,10 +80,10 @@ akerr_ErrorContext *test_draw_point(void)
ATTEMPT {
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_point(renderer, 10.0f, 20.0f, testred),
TEST_EXPECT_OK(errctx, akgl_draw_point(akgl_renderer, 10.0f, 20.0f, testred),
"plotting one pixel");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 10, 20, testred),
"the plotted pixel at 10,20 is not the color it was drawn with");
@@ -115,10 +115,10 @@ akerr_ErrorContext *test_draw_line(void)
CATCH(errctx, clear_target());
// A vertical line, so every pixel of it is known without reasoning
// about how SDL rasterises a diagonal.
TEST_EXPECT_OK(errctx, akgl_draw_line(renderer, 5.0f, 4.0f, 5.0f, 12.0f, testred),
TEST_EXPECT_OK(errctx, akgl_draw_line(akgl_renderer, 5.0f, 4.0f, 5.0f, 12.0f, testred),
"drawing a vertical line");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
for ( i = 4; i <= 12; i++ ) {
TEST_ASSERT_FLAG(onthe_line, pixel_is(shot, 5, i, testred));
@@ -158,8 +158,8 @@ akerr_ErrorContext *test_draw_rects(void)
// The outline touches the border and leaves the middle alone.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_rect(renderer, &box, testred), "outlining a rectangle");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
TEST_EXPECT_OK(errctx, akgl_draw_rect(akgl_renderer, &box, testred), "outlining a rectangle");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the outline is missing its top left corner");
TEST_ASSERT(errctx, pixel_is(shot, 23, 23, testred),
@@ -171,8 +171,8 @@ akerr_ErrorContext *test_draw_rects(void)
// The filled form covers the interior as well.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(renderer, &box, testred), "filling a rectangle");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred), "filling a rectangle");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testred), "the fill left its interior empty");
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the fill missed its top left corner");
@@ -181,11 +181,11 @@ akerr_ErrorContext *test_draw_rects(void)
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(NULL, &box, testred),
"outlining through a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(renderer, NULL, testred),
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(akgl_renderer, NULL, testred),
"outlining a NULL rectangle");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(NULL, &box, testred),
"filling through a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(renderer, NULL, testred),
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(akgl_renderer, NULL, testred),
"filling a NULL rectangle");
} CLEANUP {
if ( shot != NULL ) {
@@ -206,10 +206,10 @@ akerr_ErrorContext *test_draw_circle(void)
ATTEMPT {
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_circle(renderer, 32.0f, 32.0f, 10.0f, testred),
TEST_EXPECT_OK(errctx, akgl_draw_circle(akgl_renderer, 32.0f, 32.0f, 10.0f, testred),
"drawing a circle");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
// The four axis points are exact for any correct midpoint circle.
TEST_ASSERT(errctx, pixel_is(shot, 42, 32, testred), "the circle is missing its rightmost pixel");
@@ -243,15 +243,15 @@ akerr_ErrorContext *test_draw_circle(void)
// A zero radius is the degenerate case, not an error: one pixel.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_circle(renderer, 5.0f, 5.0f, 0.0f, testred),
TEST_EXPECT_OK(errctx, akgl_draw_circle(akgl_renderer, 5.0f, 5.0f, 0.0f, testred),
"drawing a circle of radius zero");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 5, 5, testred),
"a circle of radius zero did not plot its center");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_circle(renderer, 5.0f, 5.0f, -1.0f, testred),
akgl_draw_circle(akgl_renderer, 5.0f, 5.0f, -1.0f, testred),
"drawing a circle of negative radius");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_circle(NULL, 5.0f, 5.0f, 4.0f, testred),
@@ -279,12 +279,12 @@ akerr_ErrorContext *test_draw_flood_fill(void)
box.w = 20.0f;
box.h = 20.0f;
CATCH(errctx, clear_target());
CATCH(errctx, akgl_draw_rect(renderer, &box, testred));
CATCH(errctx, akgl_draw_rect(akgl_renderer, &box, testred));
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 20, 20, testgreen),
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 20, 20, testgreen),
"flooding the inside of a box");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen), "the seed pixel was not filled");
TEST_ASSERT(errctx, pixel_is(shot, 11, 11, testgreen),
@@ -301,9 +301,9 @@ akerr_ErrorContext *test_draw_flood_fill(void)
// Filling a region that is already the requested color changes nothing
// and is not an error.
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 20, 20, testgreen),
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 20, 20, testgreen),
"flooding a region that is already that color");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen),
"refilling a region disturbed it");
@@ -313,9 +313,9 @@ akerr_ErrorContext *test_draw_flood_fill(void)
shot = NULL;
// Flooding the outside reaches every pixel that is not the box.
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 0, 0, testgreen),
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 0, 0, testgreen),
"flooding the area around a box");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testgreen), "the seed pixel was not filled");
TEST_ASSERT(errctx, pixel_is(shot, TEST_TARGET_SIZE - 1, TEST_TARGET_SIZE - 1, testgreen),
@@ -324,10 +324,10 @@ akerr_ErrorContext *test_draw_flood_fill(void)
"the fill from outside overwrote the boundary");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_flood_fill(renderer, -1, 0, testred),
akgl_draw_flood_fill(akgl_renderer, -1, 0, testred),
"flooding from a seed left of the target");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_flood_fill(renderer, 0, TEST_TARGET_SIZE, testred),
akgl_draw_flood_fill(akgl_renderer, 0, TEST_TARGET_SIZE, testred),
"flooding from a seed below the target");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_flood_fill(NULL, 0, 0, testred),
@@ -357,13 +357,13 @@ akerr_ErrorContext *test_draw_copy_and_paste_region(void)
box.w = 8.0f;
box.h = 8.0f;
CATCH(errctx, clear_target());
CATCH(errctx, akgl_draw_filled_rect(renderer, &box, testred));
CATCH(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred));
region.x = 0;
region.y = 0;
region.w = 8;
region.h = 8;
TEST_EXPECT_OK(errctx, akgl_draw_copy_region(renderer, &region, &saved),
TEST_EXPECT_OK(errctx, akgl_draw_copy_region(akgl_renderer, &region, &saved),
"saving a region of the target");
TEST_ASSERT(errctx, saved != NULL, "akgl_draw_copy_region did not allocate a surface");
TEST_ASSERT(errctx, saved->w == 8 && saved->h == 8,
@@ -371,9 +371,9 @@ akerr_ErrorContext *test_draw_copy_and_paste_region(void)
// Wipe the screen and put it back somewhere else.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_paste_region(renderer, saved, 32.0f, 32.0f),
TEST_EXPECT_OK(errctx, akgl_draw_paste_region(akgl_renderer, saved, 32.0f, 32.0f),
"pasting a saved region");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testred),
"the pasted region did not land at its destination");
@@ -392,7 +392,7 @@ akerr_ErrorContext *test_draw_copy_and_paste_region(void)
FAIL_ZERO_BREAK(errctx, reused, AKGL_ERR_SDL, "%s", SDL_GetError());
region.x = 32;
region.y = 32;
TEST_EXPECT_OK(errctx, akgl_draw_copy_region(renderer, &region, &reused),
TEST_EXPECT_OK(errctx, akgl_draw_copy_region(akgl_renderer, &region, &reused),
"saving into a caller-owned surface");
TEST_ASSERT(errctx, pixel_is(reused, 0, 0, testred),
"the caller-owned surface did not receive the region");
@@ -401,35 +401,35 @@ akerr_ErrorContext *test_draw_copy_and_paste_region(void)
// refused rather than silently clipped.
region.w = 4;
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_copy_region(renderer, &region, &reused),
akgl_draw_copy_region(akgl_renderer, &region, &reused),
"saving into a destination of the wrong size");
region.x = TEST_TARGET_SIZE - 4;
region.y = 0;
region.w = 8;
region.h = 8;
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_copy_region(renderer, &region, &reused),
akgl_draw_copy_region(akgl_renderer, &region, &reused),
"saving a region that runs off the right edge");
region.x = 0;
region.w = 0;
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_copy_region(renderer, &region, &reused),
akgl_draw_copy_region(akgl_renderer, &region, &reused),
"saving a region with no area");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_copy_region(NULL, &region, &reused),
"saving through a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_copy_region(renderer, NULL, &reused),
akgl_draw_copy_region(akgl_renderer, NULL, &reused),
"saving a NULL region");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_copy_region(renderer, &region, NULL),
akgl_draw_copy_region(akgl_renderer, &region, NULL),
"saving into a NULL destination");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_paste_region(NULL, saved, 0.0f, 0.0f),
"pasting through a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_paste_region(renderer, NULL, 0.0f, 0.0f),
akgl_draw_paste_region(akgl_renderer, NULL, 0.0f, 0.0f),
"pasting a NULL surface");
} CLEANUP {
if ( shot != NULL ) {
@@ -465,19 +465,19 @@ akerr_ErrorContext *test_draw_preserves_render_draw_color(void)
// with, or the host's next SDL_RenderClear() paints the wrong color.
FAIL_ZERO_BREAK(
errctx,
SDL_SetRenderDrawColor(renderer->sdl_renderer, 0x11, 0x22, 0x33, 0x44),
SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x11, 0x22, 0x33, 0x44),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
CATCH(errctx, akgl_draw_point(renderer, 1.0f, 1.0f, testred));
CATCH(errctx, akgl_draw_line(renderer, 0.0f, 0.0f, 3.0f, 3.0f, testred));
CATCH(errctx, akgl_draw_rect(renderer, &box, testred));
CATCH(errctx, akgl_draw_filled_rect(renderer, &box, testred));
CATCH(errctx, akgl_draw_circle(renderer, 20.0f, 20.0f, 4.0f, testred));
CATCH(errctx, akgl_draw_point(akgl_renderer, 1.0f, 1.0f, testred));
CATCH(errctx, akgl_draw_line(akgl_renderer, 0.0f, 0.0f, 3.0f, 3.0f, testred));
CATCH(errctx, akgl_draw_rect(akgl_renderer, &box, testred));
CATCH(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred));
CATCH(errctx, akgl_draw_circle(akgl_renderer, 20.0f, 20.0f, 4.0f, testred));
FAIL_ZERO_BREAK(
errctx,
SDL_GetRenderDrawColor(renderer->sdl_renderer, &r, &g, &b, &a),
SDL_GetRenderDrawColor(akgl_renderer->sdl_renderer, &r, &g, &b, &a),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
@@ -554,6 +554,80 @@ akerr_ErrorContext *test_draw_backend_without_a_renderer(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief akgl_draw_background paints its checkerboard and restores the draw colour.
*
* Until 0.5.0 this function returned `void`, drew through the global renderer
* with no check on it, and left the draw colour changed. TODO.md listed it
* under "needs the offscreen renderer harness" purely because of that global;
* taking a backend is what makes it testable here alongside everything else in
* this file.
*/
akerr_ErrorContext *test_draw_background(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
SDL_Color after;
SDL_Color light = { 0x99, 0x99, 0x99, 0xff };
SDL_Color dark = { 0x66, 0x66, 0x66, 0xff };
ATTEMPT {
CATCH(errctx, clear_target());
// A deliberately odd colour, so "restored" cannot be confused with
// "happened to already be black".
FAIL_ZERO_BREAK(
errctx,
SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x12, 0x34, 0x56, 0x78),
AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_EXPECT_OK(errctx,
akgl_draw_background(akgl_renderer, TEST_TARGET_SIZE, TEST_TARGET_SIZE),
"painting the transparency checkerboard");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
// The pattern is 8x8 cells alternating on ((x ^ y) >> 3) & 1, so (0,0)
// and (8,8) share a colour and (8,0) is the other one.
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, dark), "cell (0,0) is not the first checker colour");
TEST_ASSERT(errctx, pixel_is(shot, 7, 7, dark), "cell (0,0) is not filled to its edge");
TEST_ASSERT(errctx, pixel_is(shot, 8, 0, light), "cell (1,0) is not the second checker colour");
TEST_ASSERT(errctx, pixel_is(shot, 0, 8, light), "cell (0,1) is not the second checker colour");
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, dark), "cell (1,1) is not the first checker colour");
FAIL_ZERO_BREAK(
errctx,
SDL_GetRenderDrawColor(akgl_renderer->sdl_renderer, &after.r, &after.g, &after.b, &after.a),
AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx,
(after.r == 0x12) && (after.g == 0x34) && (after.b == 0x56) && (after.a == 0x78),
"the draw colour was left at %02x%02x%02x%02x rather than restored",
after.r, after.g, after.b, after.a);
// Degenerate sizes paint nothing and are not an error.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_background(akgl_renderer, 0, 0),
"painting a zero-sized background");
TEST_EXPECT_OK(errctx, akgl_draw_background(akgl_renderer, -8, -8),
"painting a negative-sized background");
SDL_DestroySurface(shot);
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testblack),
"a zero or negative sized background painted something");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_background(NULL, 8, 8),
"painting a background through a NULL backend");
} CLEANUP {
if ( shot != NULL ) {
SDL_DestroySurface(shot);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
@@ -564,7 +638,8 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
renderer = &_akgl_renderer;
TEST_TRAP_UNHANDLED_ERRORS();
akgl_renderer = &akgl_default_renderer;
FAIL_ZERO_BREAK(
errctx,
@@ -579,8 +654,8 @@ int main(void)
TEST_TARGET_SIZE,
TEST_TARGET_SIZE,
0,
&window,
&renderer->sdl_renderer),
&akgl_window,
&akgl_renderer->sdl_renderer),
AKGL_ERR_SDL,
"Couldn't create window/renderer: %s",
SDL_GetError());
@@ -593,6 +668,7 @@ int main(void)
CATCH(errctx, test_draw_copy_and_paste_region());
CATCH(errctx, test_draw_preserves_render_draw_color());
CATCH(errctx, test_draw_backend_without_a_renderer());
CATCH(errctx, test_draw_background());
} CLEANUP {
SDL_Quit();
} PROCESS(errctx) {

View File

@@ -94,7 +94,13 @@ int main(void)
PREPARE_ERROR(errctx);
ATTEMPT {
// Unlike every other suite, this one has no akgl_error_init() in
// main() -- the first test is what brings the subsystem up. The trap
// therefore goes after it: libakerror installs its own default handler
// from the lazy akerr_init() inside that first call, and would
// overwrite anything set beforehand.
CATCH(errctx, test_error_init_owns_the_status_band());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, test_error_init_is_idempotent());
} CLEANUP {
} PROCESS(errctx) {

View File

@@ -21,17 +21,12 @@
#include <akgl/registry.h>
#include <akgl/sprite.h>
#include <akgl/staticstring.h>
#include <akgl/renderer.h>
#include <akgl/physics.h>
#include <akgl/iterator.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. */
@@ -40,11 +35,11 @@ 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);
memset(&akgl_game, 0x00, sizeof(akgl_Game));
strncpy((char *)&akgl_game.libversion, AKGL_VERSION, 31);
strncpy((char *)&akgl_game.version, "1.2.3", 31);
strncpy((char *)&akgl_game.name, "libakgl test game", 255);
strncpy((char *)&akgl_game.uri, "https://example.invalid/akgl-test", 255);
}
akerr_ErrorContext *test_game_load_versioncmp_matching(void)
@@ -134,23 +129,23 @@ akerr_ErrorContext *test_game_save_roundtrip(void)
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));
akgl_game.fps = 60;
akgl_game.framesSinceUpdate = 7;
memcpy(&expected, &akgl_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;
akgl_game.fps = 0;
akgl_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,
TEST_ASSERT(e, akgl_game.fps == 60, "fps restored as %d, expected 60", akgl_game.fps);
TEST_ASSERT(e, akgl_game.framesSinceUpdate == 7,
"framesSinceUpdate restored as %d, expected 7", akgl_game.framesSinceUpdate);
TEST_ASSERT(e, strncmp((char *)&akgl_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,
TEST_ASSERT(e, strncmp((char *)&akgl_game.version, (char *)&expected.version, 32) == 0,
"the game version was not preserved across a save and load");
} CLEANUP {
unlink((char *)&savepath);
@@ -170,7 +165,7 @@ akerr_ErrorContext *test_game_load_rejects_foreign_saves(void)
// 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);
strncpy((char *)&akgl_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);
@@ -178,14 +173,14 @@ akerr_ErrorContext *test_game_load_rejects_foreign_saves(void)
// Same for a differing URI.
set_game_identity();
CATCH(e, akgl_game_save((char *)&savepath));
strncpy((char *)&game.uri, "https://example.invalid/other", 255);
strncpy((char *)&akgl_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);
strncpy((char *)&akgl_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),
@@ -194,7 +189,7 @@ akerr_ErrorContext *test_game_load_rejects_foreign_saves(void)
// And one written against a different game version.
set_game_identity();
strncpy((char *)&game.version, "4.5.6", 31);
strncpy((char *)&akgl_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),
@@ -246,7 +241,7 @@ akerr_ErrorContext *test_game_load_truncated_table(void)
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,
FAIL_ZERO_BREAK(e, fwrite(&akgl_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");
@@ -296,7 +291,7 @@ akerr_ErrorContext *test_game_save_writes_name_tables(void)
+ (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 *));
+ (long)(AKGL_CHARACTER_MAX_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",
@@ -317,8 +312,8 @@ akerr_ErrorContext *test_game_state_lock(void)
ATTEMPT {
set_game_identity();
game.statelock = SDL_CreateMutex();
FAIL_ZERO_BREAK(e, game.statelock, AKGL_ERR_SDL, "unable to create the state mutex");
akgl_game.statelock = SDL_CreateMutex();
FAIL_ZERO_BREAK(e, akgl_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");
@@ -327,9 +322,133 @@ akerr_ErrorContext *test_game_state_lock(void)
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;
if ( akgl_game.statelock != NULL ) {
SDL_DestroyMutex(akgl_game.statelock);
akgl_game.statelock = NULL;
}
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/**
* @brief akgl_game_update_fps must not call a lowfpsfunc nobody installed.
*
* `game.fps` is 0 for the first second of the process, which is under the
* threshold, so this fires on frame one. Only akgl_game_init installs the
* default -- and renderer.h documents the other path deliberately: a host that
* owns its own window calls akgl_render_2d_bind instead. Such an embedder
* crashed here on its first frame, through a NULL function pointer.
*/
akerr_ErrorContext *test_game_updateFPS_without_a_lowfps_handler(void)
{
PREPARE_ERROR(e);
ATTEMPT {
set_game_identity();
// Exactly the state a host that never called akgl_game_init is in.
akgl_game.lowfpsfunc = NULL;
akgl_game.fps = 0;
akgl_game.framesSinceUpdate = 0;
akgl_game.lastFPSTime = SDL_GetTicksNS();
akgl_game_update_fps();
TEST_ASSERT(e, akgl_game.lowfpsfunc != NULL,
"akgl_game_update_fps left lowfpsfunc NULL");
TEST_ASSERT(e, akgl_game.lowfpsfunc == &akgl_game_lowfps,
"akgl_game_update_fps installed something other than the default");
// And it keeps working on the frames after.
akgl_game_update_fps();
TEST_ASSERT(e, akgl_game.framesSinceUpdate == 2,
"frames counted as %d over two updates, expected 2",
akgl_game.framesSinceUpdate);
} CLEANUP {
akgl_game.lowfpsfunc = &akgl_game_lowfps;
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/** @brief Cleared while the helper thread should keep holding the state mutex. */
static SDL_AtomicInt lockholder_release;
/** @brief Takes the state mutex and sits on it until lockholder_release is set. */
static int SDLCALL lockholder_thread(void *userdata)
{
SDL_Mutex *statelock = (SDL_Mutex *)userdata;
SDL_LockMutex(statelock);
while ( SDL_GetAtomicInt(&lockholder_release) == 0 ) {
SDL_Delay(10);
}
SDL_UnlockMutex(statelock);
return 0;
}
/**
* @brief akgl_game_state_lock must give up on a contended mutex in about a second.
*
* The uncontended path above never reaches the retry loop, which is where the
* budget lives and where the defect was: the loop counted against a constant
* named "one second in milliseconds" that held 1000000, so it retried 10,000
* times at 100 ms and blocked for roughly sixteen minutes before reporting
* failure. The upper bound below is the assertion that matters. The lower bound
* is there so a build that gave up immediately -- reporting failure without
* waiting at all -- cannot pass either.
*/
akerr_ErrorContext *test_game_state_lock_budget(void)
{
PREPARE_ERROR(e);
SDL_Thread *holder = NULL;
Uint64 started = 0;
Uint64 elapsed = 0;
ATTEMPT {
set_game_identity();
akgl_game.statelock = SDL_CreateMutex();
FAIL_ZERO_BREAK(e, akgl_game.statelock, AKGL_ERR_SDL, "unable to create the state mutex");
SDL_SetAtomicInt(&lockholder_release, 0);
holder = SDL_CreateThread(lockholder_thread, "akgl_test_lockholder", (void *)akgl_game.statelock);
FAIL_ZERO_BREAK(e, holder, AKGL_ERR_SDL, "unable to start the lock-holding thread");
// Wait until the helper actually owns the mutex. Without this the
// measurement races the thread start and the lock is taken on the first
// try, which measures nothing.
while ( SDL_TryLockMutex(akgl_game.statelock) == true ) {
SDL_UnlockMutex(akgl_game.statelock);
SDL_Delay(1);
}
started = SDL_GetTicksNS();
TEST_EXPECT_STATUS(
e,
AKGL_ERR_SDL,
akgl_game_state_lock(),
"taking a state lock another thread is holding");
elapsed = SDL_GetTicksNS() - started;
TEST_ASSERT(
e,
elapsed >= (AKGL_TIME_ONESEC_NS / 2),
"state lock gave up after %" SDL_PRIu64 " ns without waiting out its budget",
elapsed);
TEST_ASSERT(
e,
elapsed < (5 * (Uint64)AKGL_TIME_ONESEC_NS),
"state lock waited %" SDL_PRIu64 " ns on a %d ms budget",
elapsed,
AKGL_GAME_STATE_LOCK_BUDGET_MS);
} CLEANUP {
SDL_SetAtomicInt(&lockholder_release, 1);
if ( holder != NULL ) {
SDL_WaitThread(holder, NULL);
}
if ( akgl_game.statelock != NULL ) {
SDL_DestroyMutex(akgl_game.statelock);
akgl_game.statelock = NULL;
}
} PROCESS(e) {
} FINISH(e, true);
@@ -352,41 +471,41 @@ akerr_ErrorContext *test_game_updateFPS(void)
ATTEMPT {
set_game_identity();
game.lowfpsfunc = &stub_lowfps;
akgl_game.lowfpsfunc = &stub_lowfps;
// Below the 30 FPS floor, every update notifies the callback.
game.fps = 10;
game.lastFPSTime = SDL_GetTicksNS();
akgl_game.fps = 10;
akgl_game.lastFPSTime = SDL_GetTicksNS();
lowfps_calls = 0;
framesbefore = game.framesSinceUpdate;
akgl_game_updateFPS();
framesbefore = akgl_game.framesSinceUpdate;
akgl_game_update_fps();
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),
TEST_ASSERT(e, akgl_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");
akgl_game.framesSinceUpdate, framesbefore + 1);
TEST_ASSERT(e, akgl_game.lastIterTime != 0, "updateFPS did not stamp lastIterTime");
// At or above the floor, the callback stays quiet.
game.fps = 60;
game.lastFPSTime = SDL_GetTicksNS();
akgl_game.fps = 60;
akgl_game.lastFPSTime = SDL_GetTicksNS();
lowfps_calls = 0;
akgl_game_updateFPS();
akgl_game_update_fps();
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);
akgl_game.fps = 60;
akgl_game.framesSinceUpdate = 45;
akgl_game.lastFPSTime = SDL_GetTicksNS() - (2 * (SDL_Time)AKGL_TIME_ONESEC_NS);
akgl_game_update_fps();
TEST_ASSERT(e, akgl_game.fps == 45,
"after a second elapsed, fps rolled over as %d, expected 45", akgl_game.fps);
TEST_ASSERT(e, akgl_game.framesSinceUpdate == 1,
"the frame counter restarted at %d, expected 1", akgl_game.framesSinceUpdate);
// The shipped default callback only logs, so it just has to not crash.
game.fps = 1;
akgl_game.fps = 1;
akgl_game_lowfps();
} CLEANUP {
} PROCESS(e) {
@@ -394,6 +513,212 @@ akerr_ErrorContext *test_game_updateFPS(void)
SUCCEED_RETURN(e);
}
/**
* @brief A save with a registered spritesheet must read back.
*
* The four name tables carry no length prefix, so the reader finds each entry
* by stepping a fixed width. The writer used each object's own maximum name
* length -- 512 for a spritesheet, which is a filename -- and the reader used
* AKGL_ACTOR_MAX_NAME_LENGTH for all four. The other three are 128 as well, so
* only the spritesheet table was wrong, and that was enough: every entry after
* it was read out of the middle of its neighbour.
*
* The existing roundtrip test passed because empty registries write nothing but
* the zeroed sentinel. This one puts a name in each of the four registries, and
* a long one in the spritesheet registry, so the widths actually have to agree.
*
* The registry values are placeholder pointers rather than real objects: the
* save tables record name-to-address pairs and the loader looks each name up in
* the live registry, so what the pointers point at never matters here.
*/
akerr_ErrorContext *test_game_save_roundtrip_with_a_spritesheet(void)
{
PREPARE_ERROR(e);
char longsheetname[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
akgl_Actor placeholder_actor;
akgl_Sprite placeholder_sprite;
akgl_SpriteSheet placeholder_sheet;
akgl_Character placeholder_character;
int i = 0;
ATTEMPT {
CATCH(e, akgl_registry_init());
CATCH(e, akgl_heap_init());
set_game_identity();
// A spritesheet name that does not fit the width the reader used to
// assume. Filled to just under the field so the terminator still fits.
memset(&longsheetname, 0x00, sizeof(longsheetname));
for ( i = 0; i < (AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH - 1); i++ ) {
longsheetname[i] = 'a' + (i % 26);
}
FAIL_ZERO_BREAK(e, SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, "roundtrip_actor",
(void *)&placeholder_actor),
AKERR_KEY, "could not register the actor");
FAIL_ZERO_BREAK(e, SDL_SetPointerProperty(AKGL_REGISTRY_SPRITE, "roundtrip_sprite",
(void *)&placeholder_sprite),
AKERR_KEY, "could not register the sprite");
FAIL_ZERO_BREAK(e, SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, (char *)&longsheetname,
(void *)&placeholder_sheet),
AKERR_KEY, "could not register the spritesheet");
FAIL_ZERO_BREAK(e, SDL_SetPointerProperty(AKGL_REGISTRY_CHARACTER, "roundtrip_character",
(void *)&placeholder_character),
AKERR_KEY, "could not register the character");
TEST_EXPECT_OK(e, akgl_game_save((char *)&savepath),
"saving a game with all four registries populated");
// The load is what walks the four tables in order. If any width
// disagrees with the writer's, the table after it starts mid-entry and
// the read runs off the end of the file.
TEST_EXPECT_OK(e, akgl_game_load((char *)&savepath),
"loading back a save with a registered spritesheet");
TEST_ASSERT(e, strncmp((char *)&akgl_game.name, "libakgl test game", 256) == 0,
"the game identity did not survive the roundtrip");
} CLEANUP {
unlink((char *)&savepath);
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/** @brief Counts akgl_game_update calls into each actor's updatefunc, by actor index. */
static int updatecounts[AKGL_MAX_HEAP_ACTOR];
/** @brief updatefunc stub: record that this actor was updated. */
static akerr_ErrorContext *counting_updatefunc(akgl_Actor *obj)
{
PREPARE_ERROR(e);
int i = 0;
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
if ( &akgl_heap_actors[i] == obj ) {
updatecounts[i] += 1;
break;
}
}
SUCCEED_RETURN(e);
}
/** @brief Physics backend stub: akgl_game_update calls simulate, and it must not matter here. */
static akerr_ErrorContext *stub_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags)
{
PREPARE_ERROR(e);
SUCCEED_RETURN(e);
}
/** @brief Render backend stub: akgl_game_update calls draw_world; drawing is not under test. */
static akerr_ErrorContext *stub_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags)
{
PREPARE_ERROR(e);
SUCCEED_RETURN(e);
}
/**
* @brief akgl_game_update must call each live actor's updatefunc exactly once.
*
* The sweep used to sit inside a walk over AKGL_TILEMAP_MAX_LAYERS and never
* compared actor->layer to the layer it was on, so every live actor updated
* sixteen times a frame -- 70 microseconds of work to do 4.4 of it, and every
* bit of per-frame actor logic running sixteen times over. It hid behind a
* software rasterizer and would not have hidden behind a GPU backend.
*
* Counting is the assertion. A timing test would measure the machine.
*/
akerr_ErrorContext *test_game_update_visits_each_actor_once(void)
{
PREPARE_ERROR(e);
akgl_PhysicsBackend stubphysics;
akgl_RenderBackend stubrenderer;
akgl_Iterator opflags;
akgl_Actor *actors[3] = { NULL, NULL, NULL };
int layers[3] = { 0, 1, 1 };
int i = 0;
int live = 0;
ATTEMPT {
set_game_identity();
akgl_game.statelock = SDL_CreateMutex();
FAIL_ZERO_BREAK(e, akgl_game.statelock, AKGL_ERR_SDL, "unable to create the state mutex");
akgl_game.lowfpsfunc = &akgl_game_lowfps;
CATCH(e, akgl_heap_init());
CATCH(e, akgl_registry_init_actor());
memset(&stubphysics, 0x00, sizeof(akgl_PhysicsBackend));
memset(&stubrenderer, 0x00, sizeof(akgl_RenderBackend));
stubphysics.simulate = &stub_simulate;
stubrenderer.draw_world = &stub_draw_world;
akgl_physics = &stubphysics;
akgl_renderer = &stubrenderer;
akgl_gamemap = &akgl_default_gamemap;
for ( i = 0; i < 3; i++ ) {
char name[32];
snprintf((char *)&name, sizeof(name), "sweepactor%d", i);
CATCH(e, akgl_heap_next_actor(&actors[i]));
CATCH(e, akgl_actor_initialize(actors[i], (char *)&name));
actors[i]->updatefunc = &counting_updatefunc;
actors[i]->layer = layers[i];
}
// The default sweep: every live actor, once.
memset(&updatecounts, 0x00, sizeof(updatecounts));
TEST_EXPECT_OK(e, akgl_game_update(NULL), "one default game update");
live = 0;
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
if ( akgl_heap_actors[i].refcount == 0 ) {
TEST_ASSERT(e, updatecounts[i] == 0,
"a free actor slot %d was updated %d times", i, updatecounts[i]);
continue;
}
live += 1;
TEST_ASSERT(e, updatecounts[i] == 1,
"actor %d updated %d times in one frame, expected 1",
i, updatecounts[i]);
}
TEST_ASSERT(e, live == 3, "%d live actors, expected 3", live);
// Two frames means two updates, not thirty-two.
memset(&updatecounts, 0x00, sizeof(updatecounts));
TEST_EXPECT_OK(e, akgl_game_update(NULL), "the second game update");
TEST_EXPECT_OK(e, akgl_game_update(NULL), "the third game update");
for ( i = 0; i < 3; i++ ) {
TEST_ASSERT(e, updatecounts[i] == 2,
"actor %d updated %d times over two frames, expected 2",
i, updatecounts[i]);
}
// AKGL_ITERATOR_OP_LAYERMASK now means what it says: only layer 1.
memset(&updatecounts, 0x00, sizeof(updatecounts));
AKGL_BITMASK_CLEAR(opflags.flags);
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_UPDATE);
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_LAYERMASK);
opflags.layerid = 1;
TEST_EXPECT_OK(e, akgl_game_update(&opflags), "a layer-masked game update");
TEST_ASSERT(e, updatecounts[0] == 0,
"the layer 0 actor updated %d times under a layer 1 mask", updatecounts[0]);
TEST_ASSERT(e, updatecounts[1] == 1,
"the first layer 1 actor updated %d times, expected 1", updatecounts[1]);
TEST_ASSERT(e, updatecounts[2] == 1,
"the second layer 1 actor updated %d times, expected 1", updatecounts[2]);
} CLEANUP {
for ( i = 0; i < 3; i++ ) {
if ( actors[i] != NULL ) {
IGNORE(akgl_heap_release_actor(actors[i]));
}
}
if ( akgl_game.statelock != NULL ) {
SDL_DestroyMutex(akgl_game.statelock);
akgl_game.statelock = NULL;
}
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
int main(void)
{
PREPARE_ERROR(errctx);
@@ -403,6 +728,7 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
@@ -414,8 +740,12 @@ int main(void)
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_save_roundtrip_with_a_spritesheet());
CATCH(errctx, test_game_state_lock());
CATCH(errctx, test_game_state_lock_budget());
CATCH(errctx, test_game_updateFPS());
CATCH(errctx, test_game_updateFPS_without_a_lowfps_handler());
CATCH(errctx, test_game_update_visits_each_actor_once());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);

View File

@@ -0,0 +1,42 @@
/**
* @file header_pool_override.c
* @brief Checks that the compile-time pool-size overrides in heap.h actually fire.
*
* `heap.h` wraps every `AKGL_MAX_HEAP_*` ceiling in an `#ifndef` so a game that
* needs more actors can define its own before including it. That mechanism was
* dead until 0.5.0: `actor.h`, `sprite.h` and `character.h` defined the same
* four unconditionally, and `heap.h` includes all three *above* its own
* `#ifndef` block, so whichever landed first won and the guard never fired. The
* override was documented, and did nothing.
*
* The assertion is the compile. Nothing here links against the pools -- it
* cannot, because the library was built with the default ceilings and this
* translation unit deliberately disagrees with it. That disagreement is the
* documented consequence of overriding a ceiling, not something this file is
* working around: the arrays are sized at compile time, so the library and
* everything linking it have to agree.
*/
#define AKGL_MAX_HEAP_ACTOR 128
#define AKGL_MAX_HEAP_STRING 512
#include <akgl/heap.h>
#if AKGL_MAX_HEAP_ACTOR != 128
#error "heap.h overrode a caller's AKGL_MAX_HEAP_ACTOR; the #ifndef guard is dead again"
#endif
#if AKGL_MAX_HEAP_STRING != 512
#error "heap.h overrode a caller's AKGL_MAX_HEAP_STRING; the #ifndef guard is dead again"
#endif
/* The derived ceilings have to follow the override rather than a stale literal. */
#if AKGL_MAX_HEAP_SPRITE != (128 * 16)
#error "AKGL_MAX_HEAP_SPRITE no longer derives from AKGL_MAX_HEAP_ACTOR"
#endif
int akgl_header_selftest_pool_override(void);
int akgl_header_selftest_pool_override(void)
{
return 0;
}

View File

@@ -10,7 +10,13 @@
*
* One header per file, deliberately. A second `#include` after the first proves
* nothing about the second: by then the first has already dragged its
* dependencies in. Covering another header means another file like this one.
* dependencies in. So every other public header gets its own generated
* translation unit -- `CMakeLists.txt` writes one per entry in
* `AKGL_PUBLIC_HEADERS` and links them all into this program. That list also
* drives `install()`, so a header that ships is a header that is checked.
*
* This file stays hand-written because it asserts something the generated ones
* cannot: that `akgl_Actor` is *complete* here rather than merely declared.
*/
#include <akgl/controller.h>
@@ -26,11 +32,11 @@ int main(void)
// one by value.
memset(&control, 0x00, sizeof(akgl_Control));
control.key = SDLK_LEFT;
control.handler_on = &akgl_Actor_cmhf_left_on;
control.handler_on = &akgl_actor_cmhf_left_on;
if ( control.handler_on == NULL ) {
return 1;
}
if ( sizeof(GAME_ControlMaps) != (sizeof(akgl_ControlMap) * AKGL_MAX_CONTROL_MAPS) ) {
if ( sizeof(akgl_controlmaps) != (sizeof(akgl_ControlMap) * AKGL_MAX_CONTROL_MAPS) ) {
return 1;
}
return 0;

View File

@@ -50,57 +50,57 @@ akerr_ErrorContext *test_heap_init_clears_every_pool(void)
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;
akgl_heap_actors[i].refcount = 5;
}
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
HEAP_SPRITE[i].refcount = 5;
akgl_heap_sprites[i].refcount = 5;
}
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
HEAP_SPRITESHEET[i].refcount = 5;
akgl_heap_spritesheets[i].refcount = 5;
}
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
HEAP_CHARACTER[i].refcount = 5;
akgl_heap_characters[i].refcount = 5;
}
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
HEAP_STRING[i].refcount = 5;
akgl_heap_strings[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_FLAG(clean, akgl_heap_actors[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_FLAG(clean, akgl_heap_sprites[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_FLAG(clean, akgl_heap_spritesheets[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_FLAG(clean, akgl_heap_characters[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_FLAG(clean, akgl_heap_strings[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;
akgl_heap_actors[0].refcount = 9;
akgl_heap_sprites[0].refcount = 9;
CATCH(e, akgl_heap_init_actor());
TEST_ASSERT(e, HEAP_ACTOR[0].refcount == 0,
TEST_ASSERT(e, akgl_heap_actors[0].refcount == 0,
"akgl_heap_init_actor did not clear the actor pool");
TEST_ASSERT(e, HEAP_SPRITE[0].refcount == 9,
TEST_ASSERT(e, akgl_heap_sprites[0].refcount == 9,
"akgl_heap_init_actor cleared the sprite pool as well");
HEAP_SPRITE[0].refcount = 0;
akgl_heap_sprites[0].refcount = 0;
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
@@ -151,28 +151,28 @@ akerr_ErrorContext *test_heap_exhaustion(void)
// 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;
akgl_heap_actors[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;
akgl_heap_sprites[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;
akgl_heap_spritesheets[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;
akgl_heap_characters[i].refcount = 1;
}
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_character(&basechar),
"akgl_heap_next_character with every slot claimed");
@@ -186,10 +186,10 @@ akerr_ErrorContext *test_heap_exhaustion(void)
"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]));
CATCH(e, akgl_heap_release_string(&akgl_heap_strings[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],
TEST_ASSERT(e, str == &akgl_heap_strings[0],
"the reclaimed string was not the slot that was released");
CATCH(e, reset_all_heaps());
@@ -264,7 +264,7 @@ akerr_ErrorContext *test_heap_release_actor_children(void)
// 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_FLAG(released, akgl_heap_actors[i].refcount == 1);
}
TEST_ASSERT(e, released,
"releasing a parent did not decrement every child exactly once");
@@ -362,6 +362,7 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());

View File

@@ -48,6 +48,138 @@ static akerr_ErrorContext *load_fixture(void)
SUCCEED_RETURN(e);
}
/**
* @brief Every accessor must refuse a NULL key or destination, not dereference it.
*
* Only the two string accessors did. The other eight validated the container
* and then wrote through @p dest unconditionally, so which arguments a caller
* could safely get wrong depended on which typed accessor they happened to
* call -- and the ones that crashed were the ones used most.
*/
/**
* @brief An exhausted string pool must report AKGL_ERR_HEAP, not crash.
*
* Both string accessors ended their ATTEMPT block with `FINISH(errctx, false)`,
* which swallows the failure instead of passing it up, and then ran
* `strncpy(&(*dest)->data, ...)` through the pointer akgl_heap_next_string
* never set. So the one condition the pool is designed to report -- it is full,
* usually because something is not releasing -- arrived as a segfault inside
* strncpy.
*
* This is what TODO.md Performance item 29 looked like from the outside: not
* "the tilemap loader leaks five strings a load", but a crash somewhere else
* entirely, fifty levels later.
*/
akerr_ErrorContext *test_json_string_accessor_reports_pool_exhaustion(void)
{
PREPARE_ERROR(e);
akgl_String *claimed[AKGL_MAX_HEAP_STRING];
akgl_String *dest = NULL;
json_t *strings = NULL;
int held = 0;
int i = 0;
memset(&claimed, 0x00, sizeof(claimed));
ATTEMPT {
CATCH(e, akgl_get_json_array_value(fixture, "strings", &strings));
// Take every slot the pool has. Whatever else is holding one already
// simply means this stops sooner.
while ( held < AKGL_MAX_HEAP_STRING ) {
akerr_ErrorContext *claim = akgl_heap_next_string(&claimed[held]);
if ( claim != NULL ) {
claim->handled = true;
claim = akerr_release_error(claim);
claimed[held] = NULL;
break;
}
held += 1;
}
TEST_ASSERT(e, held > 0, "could not claim any pool strings");
TEST_ASSERT(e, test_string_pool_used() == AKGL_MAX_HEAP_STRING,
"the string pool is not full: %d of %d claimed",
test_string_pool_used(), AKGL_MAX_HEAP_STRING);
// dest is NULL, so both accessors have to claim -- and cannot.
dest = NULL;
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP,
akgl_get_json_string_value(fixture, "name", &dest),
"reading a string with the pool exhausted");
TEST_ASSERT(e, dest == NULL,
"a refused string accessor wrote to its destination");
dest = NULL;
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP,
akgl_get_json_array_index_string(strings, 0, &dest),
"reading an array string with the pool exhausted");
TEST_ASSERT(e, dest == NULL,
"a refused array string accessor wrote to its destination");
} CLEANUP {
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
if ( claimed[i] != NULL ) {
IGNORE(akgl_heap_release_string(claimed[i]));
}
}
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_json_accessor_null_arguments(void)
{
PREPARE_ERROR(e);
int intval = 0;
float floatval = 0;
double dblval = 0;
bool boolval = false;
json_t *jsonval = NULL;
akgl_String *strval = NULL;
ATTEMPT {
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_object_value(fixture, NULL, &jsonval),
"object accessor with a NULL key");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_object_value(fixture, "nested", NULL),
"object accessor with a NULL destination");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_boolean_value(fixture, NULL, &boolval),
"boolean accessor with a NULL key");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_boolean_value(fixture, "enabled", NULL),
"boolean accessor with a NULL destination");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_integer_value(fixture, NULL, &intval),
"integer accessor with a NULL key");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_integer_value(fixture, "count", NULL),
"integer accessor with a NULL destination");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_number_value(fixture, NULL, &floatval),
"number accessor with a NULL key");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_number_value(fixture, "ratio", NULL),
"number accessor with a NULL destination");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_double_value(fixture, NULL, &dblval),
"double accessor with a NULL key");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_double_value(fixture, "ratio", NULL),
"double accessor with a NULL destination");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_value(fixture, NULL, &jsonval),
"array accessor with a NULL key");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_value(fixture, "integers", NULL),
"array accessor with a NULL destination");
// The index accessors take no key, but they take a destination.
CATCH(e, akgl_get_json_array_value(fixture, "integers", &jsonval));
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_integer(jsonval, 0, NULL),
"array index integer accessor with a NULL destination");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_object(jsonval, 0, NULL),
"array index object accessor with a NULL destination");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_string(jsonval, 0, NULL),
"array index string accessor with a NULL destination");
// Nothing above should have claimed a pool string on the way to
// refusing, which is the other half of "refuse rather than proceed".
TEST_ASSERT(e, strval == NULL, "a refused accessor wrote to its destination anyway");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_json_scalar_accessors(void)
{
PREPARE_ERROR(e);
@@ -286,6 +418,9 @@ akerr_ErrorContext *test_json_with_default(void)
int defval = 99;
akerr_ErrorContext *keyerr = NULL;
akerr_ErrorContext *typeerr = NULL;
akerr_ErrorContext *defaulted = NULL;
json_t *shortarray = NULL;
json_t *junkobj = NULL;
int junk = 0;
ATTEMPT {
@@ -314,6 +449,51 @@ akerr_ErrorContext *test_json_with_default(void)
TEST_ASSERT(e, dest == 1, "with_default applied the default for an unrelated error");
typeerr = NULL;
// A short array. This is the one that did not work: the three array
// index accessors report AKERR_OUTOFBOUNDS, which with_default never
// handled, so "this element is optional" worked for an object member
// and silently did not for an array element. Nothing in tree passed an
// array accessor's error here, which is why it went unnoticed.
dest = 1;
CATCH(e, akgl_get_json_array_value(fixture, "integers", &shortarray));
keyerr = akgl_get_json_array_index_integer(shortarray, 99, &junk);
TEST_ASSERT(e, keyerr != NULL, "reading past the end of an array unexpectedly succeeded");
TEST_ASSERT(e, keyerr->status == AKERR_OUTOFBOUNDS,
"a short array reported %d, expected AKERR_OUTOFBOUNDS", keyerr->status);
// Not TEST_EXPECT_OK here. That macro releases whatever context the
// statement returns, and akgl_get_json_with_default returns *the context
// it was given* when it does not handle it -- so on the failing path the
// CLEANUP block below would release the same context a second time, and
// a double-released context corrupts the failure rather than reporting
// it. Ownership passes to with_default either way: it releases through
// FINISH when it handles the status, and hands the context back when it
// does not.
defaulted = akgl_get_json_with_default(keyerr, (void *)&defval, (void *)&dest, sizeof(int));
keyerr = NULL;
if ( defaulted != NULL ) {
defaulted->handled = true;
defaulted = akerr_release_error(defaulted);
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR,
"with_default propagated AKERR_OUTOFBOUNDS instead of applying the default");
}
TEST_ASSERT(e, dest == 99,
"with_default did not apply the default for a short array (dest is %d)", dest);
// The same through the object index accessor, so the fix is pinned to
// the status rather than to one call site.
dest = 1;
keyerr = akgl_get_json_array_index_object(shortarray, 99, &junkobj);
TEST_ASSERT(e, keyerr != NULL, "reading an object past the end unexpectedly succeeded");
defaulted = akgl_get_json_with_default(keyerr, (void *)&defval, (void *)&dest, sizeof(int));
keyerr = NULL;
if ( defaulted != NULL ) {
defaulted->handled = true;
defaulted = akerr_release_error(defaulted);
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR,
"with_default propagated a short object array instead of applying the default");
}
TEST_ASSERT(e, dest == 99, "with_default did not apply the default (dest is %d)", dest);
// 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,
@@ -348,16 +528,19 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, load_fixture());
CATCH(errctx, test_json_accessor_null_arguments());
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());
CATCH(errctx, test_json_string_accessor_reports_pool_exhaustion());
} CLEANUP {
if ( fixture != NULL ) {
json_decref(fixture);

View File

@@ -69,21 +69,21 @@ static void bench_report_footprint(void)
{
size_t total = 0;
total = sizeof(HEAP_ACTOR) + sizeof(HEAP_SPRITE) + sizeof(HEAP_SPRITESHEET) +
sizeof(HEAP_CHARACTER) + sizeof(HEAP_STRING);
total = sizeof(akgl_heap_actors) + sizeof(akgl_heap_sprites) + sizeof(akgl_heap_spritesheets) +
sizeof(akgl_heap_characters) + sizeof(akgl_heap_strings);
printf("\n");
printf("static footprint, fixed at compile time:\n");
printf(" %-24s %5d x %7zu = %10zu bytes\n",
"HEAP_ACTOR", AKGL_MAX_HEAP_ACTOR, sizeof(akgl_Actor), sizeof(HEAP_ACTOR));
"akgl_heap_actors", AKGL_MAX_HEAP_ACTOR, sizeof(akgl_Actor), sizeof(akgl_heap_actors));
printf(" %-24s %5d x %7zu = %10zu bytes\n",
"HEAP_SPRITE", AKGL_MAX_HEAP_SPRITE, sizeof(akgl_Sprite), sizeof(HEAP_SPRITE));
"akgl_heap_sprites", AKGL_MAX_HEAP_SPRITE, sizeof(akgl_Sprite), sizeof(akgl_heap_sprites));
printf(" %-24s %5d x %7zu = %10zu bytes\n",
"HEAP_SPRITESHEET", AKGL_MAX_HEAP_SPRITESHEET, sizeof(akgl_SpriteSheet), sizeof(HEAP_SPRITESHEET));
"akgl_heap_spritesheets", AKGL_MAX_HEAP_SPRITESHEET, sizeof(akgl_SpriteSheet), sizeof(akgl_heap_spritesheets));
printf(" %-24s %5d x %7zu = %10zu bytes\n",
"HEAP_CHARACTER", AKGL_MAX_HEAP_CHARACTER, sizeof(akgl_Character), sizeof(HEAP_CHARACTER));
"akgl_heap_characters", AKGL_MAX_HEAP_CHARACTER, sizeof(akgl_Character), sizeof(akgl_heap_characters));
printf(" %-24s %5d x %7zu = %10zu bytes\n",
"HEAP_STRING", AKGL_MAX_HEAP_STRING, sizeof(akgl_String), sizeof(HEAP_STRING));
"akgl_heap_strings", AKGL_MAX_HEAP_STRING, sizeof(akgl_String), sizeof(akgl_heap_strings));
printf(" %-24s %5s %7s %10zu bytes\n", "pools, total", "", "", total);
printf(" %-24s %5d x %7s = %10zu bytes\n",
"akgl_Tilemap", 1, "", sizeof(akgl_Tilemap));
@@ -263,7 +263,7 @@ static akerr_ErrorContext *bench_heap_actor_claim(void)
PASS(errctx, inner);
for ( i = 0; i < (AKGL_MAX_HEAP_ACTOR - 1); i++ ) {
HEAP_ACTOR[i].refcount = 1;
akgl_heap_actors[i].refcount = 1;
}
bench_start("heap_next_actor, one slot left", "call", 400.0);
BENCH_LOOP(inner, i, count, akgl_heap_next_actor(&actor));
@@ -328,7 +328,7 @@ static akerr_ErrorContext *bench_heap_string(void)
// The same claim with 255 of the 256 slots taken: a megabyte walked, one
// reference count read per 4 KiB page.
for ( i = 0; i < (AKGL_MAX_HEAP_STRING - 1); i++ ) {
HEAP_STRING[i].refcount = 1;
akgl_heap_strings[i].refcount = 1;
}
bench_start("heap_next_string, one slot left", "call", 2500.0);
for ( i = 0; i < count; i++ ) {
@@ -470,7 +470,7 @@ static akerr_ErrorContext *bench_registry_lookup(void)
PASS(errctx, akgl_registry_init());
PASS(errctx, bench_make_character(&basechar, "benchlookupchar", 0));
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
actor = &HEAP_ACTOR[0];
actor = &akgl_heap_actors[0];
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("actor_set_character, registry lookup", "call", 400.0);
@@ -569,7 +569,7 @@ static akerr_ErrorContext *bench_actor_update(void)
PASS(errctx, akgl_registry_init());
PASS(errctx, bench_make_character(&basechar, "benchupdatechar", 0));
PASS(errctx, bench_fill_actor_pool(basechar, 1));
actor = &HEAP_ACTOR[0];
actor = &akgl_heap_actors[0];
AKGL_BITMASK_CLEAR(actor->state);
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
@@ -674,7 +674,7 @@ static akerr_ErrorContext *bench_logic_frame(void)
bench_start("logic frame, 64 actors updated + simulated", "frame", 60000.0);
for ( i = 0; i < count; i++ ) {
for ( j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
actor = &HEAP_ACTOR[j];
actor = &akgl_heap_actors[j];
if ( actor->refcount == 0 ) {
continue;
}
@@ -709,7 +709,7 @@ static akerr_ErrorContext *bench_geometry(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
RectanglePoints points;
akgl_RectanglePoints points;
SDL_FRect rects[BENCH_ACTOR_COUNT];
SDL_FRect overlapping = { .x = 8.0, .y = 8.0, .w = 32.0, .h = 32.0 };
SDL_FRect disjoint = { .x = 900.0, .y = 900.0, .w = 32.0, .h = 32.0 };
@@ -901,6 +901,7 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_registry_init_properties());

View File

@@ -91,7 +91,7 @@ static SDL_Texture *benchtiles = NULL;
*/
#define BENCH_FLUSH_STOP(e, ops) \
{ \
bool __bench_flushed = SDL_FlushRenderer(renderer->sdl_renderer); \
bool __bench_flushed = SDL_FlushRenderer(akgl_renderer->sdl_renderer); \
bench_stop(ops); \
FAIL_ZERO_RETURN(e, __bench_flushed, AKGL_ERR_SDL, "SDL_FlushRenderer: %s", SDL_GetError()); \
}
@@ -233,11 +233,11 @@ static akerr_ErrorContext *bench_frame(void)
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("frame_start + frame_end, 640x480", "frame", 230000.0);
for ( i = 0; i < count; i++ ) {
inner = renderer->frame_start(renderer);
inner = akgl_renderer->frame_start(akgl_renderer);
if ( inner != NULL ) {
break;
}
inner = renderer->frame_end(renderer);
inner = akgl_renderer->frame_end(akgl_renderer);
if ( inner != NULL ) {
break;
}
@@ -268,30 +268,30 @@ static akerr_ErrorContext *bench_primitives(void)
int i = 0;
int rep = 0;
PASS(errctx, renderer->frame_start(renderer));
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("draw_point", "call", 1400.0);
BENCH_LOOP(inner, i, count, akgl_draw_point(renderer, 320.0, 240.0, red));
BENCH_LOOP(inner, i, count, akgl_draw_point(akgl_renderer, 320.0, 240.0, red));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
bench_start("draw_line, screen diagonal", "call", 6200.0);
BENCH_LOOP(inner, i, count, akgl_draw_line(renderer, 0.0, 0.0, 639.0, 479.0, red));
BENCH_LOOP(inner, i, count, akgl_draw_line(akgl_renderer, 0.0, 0.0, 639.0, 479.0, red));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
bench_start("draw_rect, 200x150 outline", "call", 5000.0);
BENCH_LOOP(inner, i, count, akgl_draw_rect(renderer, &rect, red));
BENCH_LOOP(inner, i, count, akgl_draw_rect(akgl_renderer, &rect, red));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
bench_start("draw_filled_rect, 200x150", "call", 440000.0);
BENCH_LOOP(inner, i, filled, akgl_draw_filled_rect(renderer, &rect, red));
BENCH_LOOP(inner, i, filled, akgl_draw_filled_rect(akgl_renderer, &rect, red));
BENCH_FLUSH_STOP(errctx, filled);
PASS(errctx, inner);
bench_start("draw_circle, radius 64", "call", 25000.0);
BENCH_LOOP(inner, i, circles, akgl_draw_circle(renderer, 320.0, 240.0, 64.0, red));
BENCH_LOOP(inner, i, circles, akgl_draw_circle(akgl_renderer, 320.0, 240.0, 64.0, red));
BENCH_FLUSH_STOP(errctx, circles);
PASS(errctx, inner);
}
@@ -325,14 +325,14 @@ static akerr_ErrorContext *bench_framebuffer(void)
int i = 0;
int rep = 0;
PASS(errctx, renderer->frame_start(renderer));
PASS(errctx, akgl_draw_copy_region(renderer, &region, &saved));
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
PASS(errctx, akgl_draw_copy_region(akgl_renderer, &region, &saved));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("draw_copy_region, 64x64 readback", "call", 10000.0);
for ( i = 0; i < count; i++ ) {
SDL_Surface *scratch = NULL;
inner = akgl_draw_copy_region(renderer, &region, &scratch);
inner = akgl_draw_copy_region(akgl_renderer, &region, &scratch);
if ( inner != NULL ) {
break;
}
@@ -342,20 +342,20 @@ static akerr_ErrorContext *bench_framebuffer(void)
PASS(errctx, inner);
bench_start("draw_paste_region, 64x64 upload", "call", 55000.0);
BENCH_LOOP(inner, i, count, akgl_draw_paste_region(renderer, saved, 64.0, 64.0));
BENCH_LOOP(inner, i, count, akgl_draw_paste_region(akgl_renderer, saved, 64.0, 64.0));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
bench_start("draw_flood_fill, full 640x480 target", "call", 20000000.0);
for ( i = 0; i < fills; i++ ) {
inner = akgl_draw_flood_fill(renderer, 320, 240, ( (i % 2) == 0 ) ? red : blue);
inner = akgl_draw_flood_fill(akgl_renderer, 320, 240, ( (i % 2) == 0 ) ? red : blue);
if ( inner != NULL ) {
break;
}
}
BENCH_FLUSH_STOP(errctx, fills);
PASS(errctx, inner);
PASS(errctx, renderer->frame_start(renderer));
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
}
SDL_DestroySurface(saved);
SUCCEED_RETURN(errctx);
@@ -381,7 +381,7 @@ static akerr_ErrorContext *bench_text(void)
int i = 0;
int rep = 0;
PASS(errctx, renderer->frame_start(renderer));
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("text_measure, 15 characters", "call", 400.0);
BENCH_LOOP(inner, i, count, akgl_text_measure(benchfont, BENCH_TEXT, &w, &h));
@@ -486,13 +486,13 @@ static akerr_ErrorContext *bench_asset_load(void)
// pointer it never set. Both defects are filed in TODO.md; this loop
// works around the first so it never reaches the second.
for ( j = 0; j < AKGL_MAX_HEAP_STRING; j++ ) {
HEAP_STRING[j].refcount = 0;
akgl_heap_strings[j].refcount = 0;
}
inner = akgl_tilemap_load("assets/testmap.tmj", gamemap);
inner = akgl_tilemap_load("assets/testmap.tmj", akgl_gamemap);
if ( inner != NULL ) {
break;
}
inner = akgl_tilemap_release(gamemap);
inner = akgl_tilemap_release(akgl_gamemap);
if ( inner != NULL ) {
break;
}
@@ -523,7 +523,7 @@ static akerr_ErrorContext *bench_map_zero(void)
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("zeroing one akgl_Tilemap", "call", 16000000.0);
for ( i = 0; i < count; i++ ) {
memset(gamemap, 0x00, sizeof(akgl_Tilemap));
memset(akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
}
BENCH_FLUSH_STOP(errctx, count);
}
@@ -546,10 +546,10 @@ static akerr_ErrorContext *bench_tileset_offsets(void)
int i = 0;
int rep = 0;
PASS(errctx, bench_build_map(gamemap, 1));
PASS(errctx, bench_build_map(akgl_gamemap, 1));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("tilemap_compute_tileset_offsets, 1728 tiles", "call", 23000.0);
BENCH_LOOP(inner, i, count, akgl_tilemap_compute_tileset_offsets(gamemap, 0));
BENCH_LOOP(inner, i, count, akgl_tilemap_compute_tileset_offsets(akgl_gamemap, 0));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
}
@@ -573,19 +573,19 @@ static akerr_ErrorContext *bench_tilemap_draw(void)
int i = 0;
int rep = 0;
PASS(errctx, renderer->frame_start(renderer));
PASS(errctx, bench_build_map(gamemap, 1));
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
PASS(errctx, bench_build_map(akgl_gamemap, 1));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("tilemap_draw, 40x30 tiles, 1 tileset", "frame", 170000000.0);
BENCH_LOOP(inner, i, count, akgl_tilemap_draw(gamemap, camera, 0));
BENCH_LOOP(inner, i, count, akgl_tilemap_draw(akgl_gamemap, akgl_camera, 0));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
}
PASS(errctx, bench_build_map(gamemap, BENCH_MAP_TILESETS));
PASS(errctx, bench_build_map(akgl_gamemap, BENCH_MAP_TILESETS));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("tilemap_draw, 40x30 tiles, 8 tilesets", "frame", 170000000.0);
BENCH_LOOP(inner, i, count, akgl_tilemap_draw(gamemap, camera, 0));
BENCH_LOOP(inner, i, count, akgl_tilemap_draw(akgl_gamemap, akgl_camera, 0));
BENCH_FLUSH_STOP(errctx, count);
PASS(errctx, inner);
}
@@ -627,7 +627,7 @@ static akerr_ErrorContext *bench_raw_blit_control(void)
columns = benchtiles->w / BENCH_TILE_SIZE;
tilecount = columns * (benchtiles->h / BENCH_TILE_SIZE);
PASS(errctx, renderer->frame_start(renderer));
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
// One source tile, blitted 1200 times. The floor: 1200 SDL calls and
// 1200 x 256 pixels of copying, with the source rectangle staying in
@@ -640,7 +640,7 @@ static akerr_ErrorContext *bench_raw_blit_control(void)
for ( x = 0; x < across; x++ ) {
dest.x = (float32_t)(x * BENCH_TILE_SIZE);
dest.y = (float32_t)(y * BENCH_TILE_SIZE);
SDL_RenderTexture(renderer->sdl_renderer, benchtiles, &src, &dest);
SDL_RenderTexture(akgl_renderer->sdl_renderer, benchtiles, &src, &dest);
}
}
}
@@ -662,7 +662,7 @@ static akerr_ErrorContext *bench_raw_blit_control(void)
src.y = (float32_t)((tile / columns) * BENCH_TILE_SIZE);
dest.x = (float32_t)(x * BENCH_TILE_SIZE);
dest.y = (float32_t)(y * BENCH_TILE_SIZE);
SDL_RenderTexture(renderer->sdl_renderer, benchtiles, &src, &dest);
SDL_RenderTexture(akgl_renderer->sdl_renderer, benchtiles, &src, &dest);
}
}
}
@@ -690,9 +690,9 @@ static akerr_ErrorContext *bench_scene(void)
int rep = 0;
PASS(errctx, bench_populate_scene());
PASS(errctx, bench_build_map(gamemap, 1));
PASS(errctx, renderer->frame_start(renderer));
actor = &HEAP_ACTOR[0];
PASS(errctx, bench_build_map(akgl_gamemap, 1));
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
actor = &akgl_heap_actors[0];
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("actor_render, on camera", "actor", 31000.0);
@@ -701,7 +701,7 @@ static akerr_ErrorContext *bench_scene(void)
PASS(errctx, inner);
bench_start("draw_world, 1200 tiles + 64 actors", "frame", 170000000.0);
BENCH_LOOP(inner, i, frames, renderer->draw_world(renderer, NULL));
BENCH_LOOP(inner, i, frames, akgl_renderer->draw_world(akgl_renderer, NULL));
BENCH_FLUSH_STOP(errctx, frames);
PASS(errctx, inner);
}
@@ -712,11 +712,13 @@ static akerr_ErrorContext *bench_scene(void)
* @brief Time a whole frame the way akgl_game_update runs one.
*
* This is the number a host actually gets, and it is not the sum of the parts.
* akgl_game_update's update sweep is nested inside a loop over
* #AKGL_TILEMAP_MAX_LAYERS and does not filter by layer, so every live actor is
* updated sixteen times per frame rather than once. The gap between this
* benchmark and the scene benchmark plus the logic frame in tests/perf.c is that
* multiplier, and TODO.md carries it as a defect.
*
* It used to be measurably worse than that: the update sweep was nested inside
* a loop over #AKGL_TILEMAP_MAX_LAYERS that never filtered by layer, so every
* live actor updated sixteen times a frame, and this benchmark sat about 92 us
* above the scene benchmark below it. That gap *was* the multiplier. Since
* 0.5.0 the two are equal within run-to-run noise, which is the shape of the
* fix rather than a number worth quoting on its own.
*/
static akerr_ErrorContext *bench_game_update(void)
{
@@ -727,8 +729,8 @@ static akerr_ErrorContext *bench_game_update(void)
int rep = 0;
PASS(errctx, bench_populate_scene());
PASS(errctx, bench_build_map(gamemap, 1));
PASS(errctx, akgl_physics_init_null(physics));
PASS(errctx, bench_build_map(akgl_gamemap, 1));
PASS(errctx, akgl_physics_init_null(akgl_physics));
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("game_update, full frame", "frame", 170000000.0);
@@ -750,10 +752,11 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
renderer = &_akgl_renderer;
physics = &_akgl_physics;
camera = &_akgl_camera;
gamemap = &_akgl_gamemap;
TEST_TRAP_UNHANDLED_ERRORS();
akgl_renderer = &akgl_default_renderer;
akgl_physics = &akgl_default_physics;
akgl_camera = &akgl_default_camera;
akgl_gamemap = &akgl_default_gamemap;
FAIL_ZERO_BREAK(
errctx,
@@ -774,36 +777,35 @@ int main(void)
BENCH_SCREEN_W,
BENCH_SCREEN_H,
0,
&window,
&renderer->sdl_renderer),
&akgl_window,
&akgl_renderer->sdl_renderer),
AKGL_ERR_SDL,
"Couldn't create window/renderer: %s",
SDL_GetError());
CATCH(errctx, akgl_render_bind2d(renderer));
CATCH(errctx, akgl_render_2d_bind(akgl_renderer));
camera->x = 0.0;
camera->y = 0.0;
camera->w = BENCH_SCREEN_W;
camera->h = BENCH_SCREEN_H;
akgl_camera->x = 0.0;
akgl_camera->y = 0.0;
akgl_camera->w = BENCH_SCREEN_W;
akgl_camera->h = BENCH_SCREEN_H;
// akgl_game_update takes this before it does anything else. Normally
// akgl_game_init creates it; this suite brings the library up piece by
// piece so it can stay headless, so it creates the mutex itself.
game.statelock = SDL_CreateMutex();
FAIL_ZERO_BREAK(errctx, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
akgl_game.statelock = SDL_CreateMutex();
FAIL_ZERO_BREAK(errctx, akgl_game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
// And this one is not optional either. akgl_game_updateFPS calls
// game.lowfpsfunc through the pointer, unguarded, on every frame under
// 30 fps -- which includes the first frame, before there is a frame rate
// to compare. A host that brings the library up the way renderer.h
// documents for an embedder, akgl_render_bind2d over its own window,
// segfaults on its first akgl_game_update. Filed in TODO.md; installing
// the default by hand is the workaround.
game.lowfpsfunc = &akgl_game_lowfps;
// akgl_game.lowfpsfunc is deliberately left NULL. It used to have to be
// installed by hand here, because akgl_game_update_fps called it
// unguarded on every frame under 30 fps -- which includes the first,
// before there is a frame rate to compare against -- and a host that
// binds its own renderer the way renderer.h documents never went through
// akgl_game_init to get one. It installs the default itself now, and
// leaving it NULL here is what keeps that true.
benchfont = TTF_OpenFont(BENCH_FONT_PATH, BENCH_FONT_SIZE);
FAIL_ZERO_BREAK(errctx, benchfont, AKGL_ERR_SDL, "Couldn't open %s: %s", BENCH_FONT_PATH, SDL_GetError());
benchtiles = IMG_LoadTexture(renderer->sdl_renderer, BENCH_TILESET_IMAGE);
benchtiles = IMG_LoadTexture(akgl_renderer->sdl_renderer, BENCH_TILESET_IMAGE);
FAIL_ZERO_BREAK(errctx, benchtiles, AKGL_ERR_SDL, "Couldn't load %s: %s", BENCH_TILESET_IMAGE, SDL_GetError());
CATCH(errctx, akgl_heap_init());

View File

@@ -3,7 +3,7 @@
* @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
* akgl_heap_actors directly, so each test rebuilds the actor heap rather than relying
* on state left behind by an earlier one.
*/
@@ -145,6 +145,18 @@ akerr_ErrorContext *test_physics_null_backend_is_inert(void)
"null move with NULL self");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_collide(NULL, &actor, &reference),
"null collide with NULL self");
// The actor arguments too. The arcade backend has always checked these;
// the null one checked only `self`, so which pointers a caller could get
// away with passing depended on which backend it happened to have.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_gravity(&backend, NULL, 1.0f),
"null gravity with a NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_move(&backend, NULL, 1.0f),
"null move with a NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_collide(&backend, NULL, &reference),
"null collide with a NULL first actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_collide(&backend, &actor, NULL),
"null collide with a NULL second actor");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
@@ -723,6 +735,7 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_registry_init_properties());

View File

@@ -5,6 +5,7 @@
#include <akgl/error.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/actor.h>
#include <akgl/staticstring.h>
#include "testutil.h"
@@ -158,13 +159,142 @@ akerr_ErrorContext *test_akgl_get_property_copies_only_the_value(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief Every actor-state bit must be nameable, and every name must map to its own bit.
*
* akgl_registry_init_actor_state_strings walks AKGL_ACTOR_STATE_STRING_NAMES
* and registers entry `i` under the value `1 << i`. Character JSON resolves
* state names through that registry, so an entry that disagrees with the
* `#define` in actor.h is a state no character can ever bind a sprite to. Bits
* 11 and 12 were `UNDEFINED_11` and `UNDEFINED_12` here while actor.h called
* them MOVING_IN and MOVING_OUT, so both were unreachable by name.
*
* The loop also pins the array's length: it reads exactly
* AKGL_ACTOR_MAX_STATES entries, which is what the header declares and what
* the definition is now sized by. It was declared one longer than defined.
*/
akerr_ErrorContext *test_actor_state_string_names(void)
{
PREPARE_ERROR(errctx);
int i = 0;
int j = 0;
Sint64 registered = 0;
ATTEMPT {
CATCH(errctx, akgl_registry_init_actor_state_strings());
for ( i = 0; i < AKGL_ACTOR_MAX_STATES; i++ ) {
TEST_ASSERT(errctx, AKGL_ACTOR_STATE_STRING_NAMES[i] != NULL,
"actor state bit %d has no name", i);
registered = SDL_GetNumberProperty(
AKGL_REGISTRY_ACTOR_STATE_STRINGS,
AKGL_ACTOR_STATE_STRING_NAMES[i],
0);
TEST_ASSERT(errctx, registered == (Sint64)(1 << i),
"state name %s resolves to %lld, expected %d",
AKGL_ACTOR_STATE_STRING_NAMES[i],
(long long)registered,
(1 << i));
// No two entries may share a name, or the later one silently
// overwrites the earlier in the registry and one bit becomes
// unreachable without anything looking wrong.
for ( j = 0; j < i; j++ ) {
TEST_ASSERT(errctx,
strcmp(AKGL_ACTOR_STATE_STRING_NAMES[i],
AKGL_ACTOR_STATE_STRING_NAMES[j]) != 0,
"actor state bits %d and %d share the name %s",
j, i, AKGL_ACTOR_STATE_STRING_NAMES[i]);
}
}
// The two that were unreachable, named explicitly so a regression reads
// as what it is rather than as an index.
TEST_ASSERT(errctx,
SDL_GetNumberProperty(AKGL_REGISTRY_ACTOR_STATE_STRINGS,
"AKGL_ACTOR_STATE_MOVING_IN", 0)
== (Sint64)AKGL_ACTOR_STATE_MOVING_IN,
"AKGL_ACTOR_STATE_MOVING_IN is not resolvable by name");
TEST_ASSERT(errctx,
SDL_GetNumberProperty(AKGL_REGISTRY_ACTOR_STATE_STRINGS,
"AKGL_ACTOR_STATE_MOVING_OUT", 0)
== (Sint64)AKGL_ACTOR_STATE_MOVING_OUT,
"AKGL_ACTOR_STATE_MOVING_OUT is not resolvable by name");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Re-initializing a registry must replace the old set, not abandon it.
*
* Only akgl_registry_init_actor destroyed the set it was replacing; the other
* seven leaked an SDL_PropertiesID on every call after the first. A game that
* resets between levels calls these repeatedly.
*
* Also checks that akgl_registry_init() brings up the properties registry.
* It did not until 0.5.0, so akgl_set_property was a silent no-op for any
* caller that had not also gone through akgl_game_init.
*/
akerr_ErrorContext *test_akgl_registry_init_is_repeatable(void)
{
PREPARE_ERROR(errctx);
SDL_PropertiesID first = 0;
akgl_String *readback = NULL;
ATTEMPT {
CATCH(errctx, akgl_registry_init());
TEST_ASSERT(errctx, AKGL_REGISTRY_PROPERTIES != 0,
"akgl_registry_init did not create the properties registry");
// Whether the old set was *destroyed* is not observable from here --
// SDL hands out no liveness query, and it reuses ids -- so that half is
// the memcheck run's job, and `cmake --build build --target memcheck`
// gates on it. What is observable is that re-initializing really does
// replace: a key written before the call must be gone after it.
first = AKGL_REGISTRY_SPRITE;
SDL_SetNumberProperty(AKGL_REGISTRY_SPRITE, "akgl_test_sentinel", 1);
TEST_ASSERT(errctx,
SDL_GetNumberProperty(AKGL_REGISTRY_SPRITE, "akgl_test_sentinel", 0) == 1,
"could not write a sentinel into the sprite registry");
CATCH(errctx, akgl_registry_init_sprite());
TEST_ASSERT(errctx, AKGL_REGISTRY_SPRITE != 0,
"re-initializing the sprite registry left it unset");
TEST_ASSERT(errctx,
SDL_GetNumberProperty(AKGL_REGISTRY_SPRITE, "akgl_test_sentinel", 0) == 0,
"re-initializing the sprite registry kept the old contents");
// Configuration set through the registry has to survive and read back,
// which is the behaviour the missing initializer was breaking.
CATCH(errctx, akgl_registry_init());
TEST_EXPECT_OK(errctx, akgl_set_property("test.property", "1234"),
"setting a property after akgl_registry_init");
TEST_EXPECT_OK(errctx, akgl_get_property("test.property", &readback, NULL),
"reading a property back after akgl_registry_init");
TEST_ASSERT(errctx, strcmp((char *)&readback->data, "1234") == 0,
"property read back as '%s', expected '1234'",
(char *)&readback->data);
} CLEANUP {
if ( readback != NULL ) {
IGNORE(akgl_heap_release_string(readback));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, test_akgl_registry_init_creation_failures());
CATCH(errctx, test_akgl_get_property_copies_only_the_value());
CATCH(errctx, test_actor_state_string_names());
CATCH(errctx, test_akgl_registry_init_is_repeatable());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);

View File

@@ -2,13 +2,13 @@
* @file renderer.c
* @brief Unit tests for the 2D render backend's vtable and its entry points.
*
* None of this needs a display. akgl_render_bind2d() installs the backend's
* None of this needs a display. akgl_render_2d_bind() installs the backend's
* methods and nothing else, so it can be checked against a backend that has no
* SDL_Renderer at all; the entry points that do draw are checked against a
* software renderer under the dummy video driver, the same way tests/draw.c
* does it.
*
* akgl_render_init2d() is not covered here: it creates a window from the
* akgl_render_2d_init() is not covered here: it creates a window from the
* property registry and writes the `camera` global, which is what the offscreen
* harness described in TODO.md exists to make testable.
*/
@@ -20,6 +20,7 @@
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/renderer.h>
#include <akgl/registry.h>
#include "testutil.h"
@@ -42,7 +43,7 @@ akerr_ErrorContext *test_render_bind2d(void)
memset(&backend, 0x00, sizeof(akgl_RenderBackend));
backend.sdl_renderer = (SDL_Renderer *)&bound;
TEST_EXPECT_OK(errctx, akgl_render_bind2d(&backend), "binding the 2D backend");
TEST_EXPECT_OK(errctx, akgl_render_2d_bind(&backend), "binding the 2D backend");
TEST_ASSERT(errctx, backend.sdl_renderer == (SDL_Renderer *)&bound,
"binding replaced the caller's SDL_Renderer");
@@ -63,12 +64,12 @@ akerr_ErrorContext *test_render_bind2d(void)
// are separable in both directions -- and leaves it NULL rather than
// inventing one.
memset(&backend, 0x00, sizeof(akgl_RenderBackend));
TEST_EXPECT_OK(errctx, akgl_render_bind2d(&backend),
TEST_EXPECT_OK(errctx, akgl_render_2d_bind(&backend),
"binding a backend with no SDL renderer");
TEST_ASSERT(errctx, backend.sdl_renderer == NULL,
"binding invented an SDL_Renderer");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_bind2d(NULL),
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_bind(NULL),
"binding a NULL backend");
} CLEANUP {
} PROCESS(errctx) {
@@ -85,13 +86,23 @@ akerr_ErrorContext *test_render_backend_without_a_renderer(void)
// Bound but never given an SDL_Renderer. Every entry point that draws
// has to report that rather than dereference it.
memset(&empty, 0x00, sizeof(akgl_RenderBackend));
CATCH(errctx, akgl_render_bind2d(&empty));
CATCH(errctx, akgl_render_2d_bind(&empty));
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_start(&empty),
"starting a frame on a backend with no renderer");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_end(&empty),
"ending a frame on a backend with no renderer");
// And a NULL backend outright. frame_start and frame_end used to read
// self->sdl_renderer with no check on self at all, which is a segfault
// rather than an error, while draw_texture beside them checked it.
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_frame_start(NULL),
"starting a frame on a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_frame_end(NULL),
"ending a frame on a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_shutdown(NULL),
"shutting down a NULL backend");
// Shutdown is the one that has nothing to release yet, so it succeeds.
TEST_EXPECT_OK(errctx, empty.shutdown(&empty), "shutting down an unused backend");
} CLEANUP {
@@ -177,6 +188,58 @@ akerr_ErrorContext *test_render_unimplemented_and_guards(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief akgl_render_2d_init must give back its pooled strings when a parse fails.
*
* It reads game.screenwidth and game.screenheight into two pool strings and
* used to release them only after both had parsed, so a non-numeric value
* returned past both and leaked two of the pool's 256 entries -- every time a
* host started with a bad configuration and retried.
*
* The window creation after the parse is what makes the success path need a
* display, so this only drives the failure path. That is the one that leaked.
*/
akerr_ErrorContext *test_render_2d_init_releases_strings_on_failure(void)
{
PREPARE_ERROR(errctx);
akgl_RenderBackend backend;
int baseline = 0;
int i = 0;
ATTEMPT {
CATCH(errctx, akgl_registry_init_properties());
memset(&backend, 0x00, sizeof(akgl_RenderBackend));
CATCH(errctx, akgl_set_property("game.screenwidth", "not-a-number"));
CATCH(errctx, akgl_set_property("game.screenheight", "480"));
baseline = test_string_pool_used();
for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
TEST_EXPECT_ANY_ERROR(errctx, akgl_render_2d_init(&backend),
"initializing a 2D renderer from an unparseable width");
}
TEST_ASSERT(errctx, test_string_pool_used() == baseline,
"%d failed initializations left %d pool strings claimed, expected %d",
(AKGL_MAX_HEAP_STRING * 2), test_string_pool_used(), baseline);
// The second property is the one read after the first parse, so fail on
// it too and check the other order.
CATCH(errctx, akgl_set_property("game.screenwidth", "640"));
CATCH(errctx, akgl_set_property("game.screenheight", "also-not-a-number"));
baseline = test_string_pool_used();
for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
TEST_EXPECT_ANY_ERROR(errctx, akgl_render_2d_init(&backend),
"initializing a 2D renderer from an unparseable height");
}
TEST_ASSERT(errctx, test_string_pool_used() == baseline,
"%d failed initializations left %d pool strings claimed, expected %d",
(AKGL_MAX_HEAP_STRING * 2), test_string_pool_used(), baseline);
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
@@ -187,6 +250,7 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
memset(&bound, 0x00, sizeof(akgl_RenderBackend));
FAIL_ZERO_BREAK(
@@ -202,19 +266,20 @@ int main(void)
TEST_TARGET_SIZE,
TEST_TARGET_SIZE,
0,
&window,
&akgl_window,
&bound.sdl_renderer),
AKGL_ERR_SDL,
"Couldn't create window/renderer: %s",
SDL_GetError());
// The host owns the window; libakgl only ever binds to it. This is the
// arrangement akgl_render_bind2d exists for.
CATCH(errctx, akgl_render_bind2d(&bound));
// arrangement akgl_render_2d_bind exists for.
CATCH(errctx, akgl_render_2d_bind(&bound));
CATCH(errctx, test_render_bind2d());
CATCH(errctx, test_render_backend_without_a_renderer());
CATCH(errctx, test_render_frames_and_textures());
CATCH(errctx, test_render_unimplemented_and_guards());
CATCH(errctx, test_render_2d_init_releases_strings_on_failure());
} CLEANUP {
SDL_Quit();
} PROCESS(errctx) {

View File

@@ -13,6 +13,8 @@
#include <akgl/game.h>
#include <akgl/renderer.h>
#include "testutil.h"
akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
{
@@ -37,7 +39,7 @@ akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
"Loaded texture was not the correct size");
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
image = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&tmpstr->data);
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
CATCH(
@@ -138,7 +140,7 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void)
// Is it using the right spritesheet?
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
image = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&tmpstr->data);
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
CATCH(
@@ -182,13 +184,66 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief A sprite definition must not be able to write past `frameids`.
*
* `frameids` is AKGL_SPRITE_MAX_FRAMES bytes and the loader took its count
* straight from the document, so a definition with seventeen frames wrote past
* the array, past the rest of akgl_Sprite, and into the next pool slot. It also
* wrote through a `uint32_t *` cast of a `uint8_t *`, four bytes at a time,
* which is why the overrun reached four bytes past the array rather than one.
*
* The neighbouring slot is claimed and stamped first, so the test fails on the
* corruption rather than on whatever the corruption happens to do later.
*/
akerr_ErrorContext *test_akgl_sprite_load_json_bounds_frames(void)
{
PREPARE_ERROR(errctx);
akgl_Sprite *loaded = NULL;
int i = 0;
ATTEMPT {
// Exactly the maximum is legal and must still load.
TEST_EXPECT_OK(errctx, akgl_sprite_load_json("assets/testsprite_maxframes.json"),
"loading a sprite with exactly AKGL_SPRITE_MAX_FRAMES frames");
loaded = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite_maxframes", NULL);
FAIL_ZERO_BREAK(errctx, loaded, AKERR_KEY, "the max-frames sprite is not in the registry");
TEST_ASSERT(errctx, loaded->frames == AKGL_SPRITE_MAX_FRAMES,
"max-frames sprite loaded %d frames, expected %d",
loaded->frames, AKGL_SPRITE_MAX_FRAMES);
for ( i = 0; i < AKGL_SPRITE_MAX_FRAMES; i++ ) {
TEST_ASSERT(errctx, loaded->frameids[i] == (uint8_t)i,
"max-frames sprite frame %d is %d, expected %d",
i, loaded->frameids[i], i);
}
// One more than the maximum is refused, and nothing is registered.
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_sprite_load_json("assets/testsprite_toomanyframes.json"),
"loading a sprite with more frames than the array holds");
TEST_ASSERT(errctx,
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite_toomanyframes", NULL) == NULL,
"a sprite with too many frames was registered anyway");
// A frame number a uint8_t cannot hold is refused rather than truncated
// to something that indexes a different tile.
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_sprite_load_json("assets/testsprite_widecount.json"),
"loading a sprite whose frame number does not fit a uint8_t");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
renderer = &_akgl_renderer;
TEST_TRAP_UNHANDLED_ERRORS();
akgl_renderer = &akgl_default_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
@@ -196,10 +251,10 @@ int main(void)
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
}
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer->sdl_renderer)) {
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &akgl_window, &akgl_renderer->sdl_renderer)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
}
renderer->draw_texture = &akgl_render_2d_draw_texture;
akgl_renderer->draw_texture = &akgl_render_2d_draw_texture;
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init_sprite());
@@ -208,6 +263,7 @@ int main(void)
CATCH(errctx, test_akgl_spritesheet_initialize());
CATCH(errctx, test_akgl_sprite_initialize());
CATCH(errctx, test_akgl_sprite_load_json());
CATCH(errctx, test_akgl_sprite_load_json_bounds_frames());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);

View File

@@ -4,6 +4,8 @@
#include <akgl/heap.h>
#include <akgl/staticstring.h>
#include "testutil.h"
void reset_string_heap(void);
akerr_ErrorContext *test_fresh_heap_gives_strings(void)
@@ -28,7 +30,7 @@ akerr_ErrorContext *test_string_heap_error_when_no_strings_left(void)
akgl_String *ptr;
PREPARE_ERROR(errctx);
for ( int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
HEAP_STRING[i].refcount = 1;
akgl_heap_strings[i].refcount = 1;
}
for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
ATTEMPT {
@@ -45,8 +47,8 @@ akerr_ErrorContext *test_string_heap_error_when_no_strings_left(void)
akerr_ErrorContext *test_string_heap_honors_refcount(void)
{
akgl_String *firstptr = &HEAP_STRING[0];
akgl_String *secondptr = &HEAP_STRING[1];
akgl_String *firstptr = &akgl_heap_strings[0];
akgl_String *secondptr = &akgl_heap_strings[1];
akgl_String *testptr = NULL;
PREPARE_ERROR(errctx);
ATTEMPT {
@@ -55,7 +57,7 @@ akerr_ErrorContext *test_string_heap_honors_refcount(void)
FAIL_RETURN(
errctx,
AKERR_VALUE,
"Expected testptr to equal (HEAP_STRING[0] = %p) but got %p",
"Expected testptr to equal (akgl_heap_strings[0] = %p) but got %p",
firstptr,
testptr
);
@@ -68,7 +70,7 @@ akerr_ErrorContext *test_string_heap_honors_refcount(void)
FAIL_RETURN(
errctx,
AKERR_VALUE,
"Expected testptr to equal (HEAP_STRING[1] = %p) but got %p",
"Expected testptr to equal (akgl_heap_strings[1] = %p) but got %p",
secondptr,
testptr
);
@@ -124,16 +126,122 @@ akerr_ErrorContext *test_akgl_string_initialize(void)
void reset_string_heap(void)
{
for ( int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
memset(&HEAP_STRING[i], 0x00, sizeof(akgl_String));
memset(&akgl_heap_strings[i], 0x00, sizeof(akgl_String));
}
}
/**
* @brief akgl_string_initialize must not write past the buffer it is zeroing.
*
* The `NULL` init path zeroed `sizeof(akgl_String)` bytes starting at `data`.
* `data` begins after the `int refcount` in front of it, so that ran four bytes
* past the end of the object -- straight onto the *next* pool slot's refcount,
* which is the field the allocator uses to decide whether a slot is free.
*
* Claiming two adjacent slots and initializing the first is enough to catch it:
* the second's refcount goes to zero and the pool believes it is free while the
* caller is still holding it.
*/
akerr_ErrorContext *test_akgl_string_initialize_stays_in_bounds(void)
{
PREPARE_ERROR(errctx);
akgl_String *first = NULL;
akgl_String *second = NULL;
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&first));
CATCH(errctx, akgl_heap_next_string(&second));
TEST_ASSERT(errctx, second == (first + 1),
"the pool did not hand out adjacent slots; this test needs them");
// A sentinel the overrun would land on. akgl_heap_next_string has
// already set it to 1; make it something an accidental write cannot
// coincide with.
second->refcount = 0x5A;
CATCH(errctx, akgl_string_initialize(first, NULL));
TEST_ASSERT(errctx, second->refcount == 0x5A,
"initializing a string wrote past its buffer: the next slot's "
"refcount is %d, expected %d",
second->refcount, 0x5A);
TEST_ASSERT(errctx, first->refcount == 1,
"initializing a string left its own refcount at %d, expected 1",
first->refcount);
TEST_ASSERT(errctx, first->data[0] == '\0',
"initializing a string with NULL did not zero its buffer");
TEST_ASSERT(errctx, first->data[AKGL_MAX_STRING_LENGTH - 1] == '\0',
"initializing a string with NULL did not zero its last byte");
} CLEANUP {
if ( second != NULL ) {
second->refcount = 1;
IGNORE(akgl_heap_release_string(second));
}
if ( first != NULL ) {
IGNORE(akgl_heap_release_string(first));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief akgl_string_copy must refuse a count that would leave both buffers.
*
* Both slots are exactly AKGL_MAX_STRING_LENGTH bytes, so a larger count read
* past the end of one and wrote past the end of the other. The header used to
* document that as behaviour.
*/
akerr_ErrorContext *test_akgl_string_copy_bounds_its_count(void)
{
PREPARE_ERROR(errctx);
akgl_String *src = NULL;
akgl_String *dest = NULL;
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&src));
CATCH(errctx, akgl_heap_next_string(&dest));
CATCH(errctx, akgl_string_initialize(src, "bounded"));
CATCH(errctx, akgl_string_initialize(dest, NULL));
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_string_copy(src, dest, AKGL_MAX_STRING_LENGTH + 1),
"copying one byte more than a pool string holds");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_string_copy(src, dest, -1),
"copying a negative number of bytes");
// The boundary itself is legal: it is exactly the buffer.
TEST_EXPECT_OK(errctx, akgl_string_copy(src, dest, AKGL_MAX_STRING_LENGTH),
"copying exactly a pool string's length");
TEST_ASSERT(errctx, strcmp((char *)&dest->data, "bounded") == 0,
"a full-length copy did not transfer the contents");
// 0 still means "the whole buffer" rather than "nothing".
CATCH(errctx, akgl_string_initialize(dest, NULL));
TEST_EXPECT_OK(errctx, akgl_string_copy(src, dest, 0),
"copying with a count of zero");
TEST_ASSERT(errctx, strcmp((char *)&dest->data, "bounded") == 0,
"a zero-count copy did not transfer the whole buffer");
} CLEANUP {
if ( dest != NULL ) {
IGNORE(akgl_heap_release_string(dest));
}
if ( src != NULL ) {
IGNORE(akgl_heap_release_string(src));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
printf("test_fresh_heap_gives_string ....\n");
test_fresh_heap_gives_strings();
reset_string_heap();
@@ -148,6 +256,12 @@ int main(void)
reset_string_heap();
printf("test_akgl_string_initialize....\n");
test_akgl_string_initialize();
reset_string_heap();
printf("test_akgl_string_initialize_stays_in_bounds ...\n");
CATCH(errctx, test_akgl_string_initialize_stays_in_bounds());
reset_string_heap();
printf("test_akgl_string_copy_bounds_its_count ...\n");
CATCH(errctx, test_akgl_string_copy_bounds_its_count());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);

View File

@@ -16,9 +16,66 @@
#define _AKGL_TESTUTIL_H_
#include <math.h>
#include <stdlib.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/heap.h>
/**
* @brief How many pool string slots are currently claimed.
*
* The string pool is 256 entries and every loader borrows scratch strings from
* it, so "does this function give back what it took" is a question a test can
* answer directly by counting either side of a call. A leak that would take
* fifty map loads to show up in production shows up here on the first one.
*/
static inline int test_string_pool_used(void)
{
int used = 0;
int i = 0;
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
if ( akgl_heap_strings[i].refcount != 0 ) {
used += 1;
}
}
return used;
}
/**
* @brief Exit with a status the shell can actually see.
*
* libakerror's default unhandled-error handler ends in `exit(errctx->status)`.
* `exit` keeps only the low byte of what it is given, and libakgl's own
* statuses start at `AKERR_FIRST_CONSUMER_STATUS`, which is 256 -- so
* #AKGL_ERR_SDL exits 0, and CTest records a pass. Every suite in this
* directory that failed for the most common reason in this library therefore
* reported success. `tests/character.c` did exactly that: it aborted at its
* second of four tests on a bad renderer and was green for months.
*
* This collapses any status that a byte cannot carry onto 1. It does not log --
* `FINISH_NORETURN` has already logged the context and its stack by the time it
* calls the handler.
*/
static inline void test_handler_unhandled_error(akerr_ErrorContext *errctx)
{
int status = ( errctx == NULL ) ? 1 : (errctx->status & 0xFF);
if ( status == 0 ) {
status = 1;
}
exit(status);
}
/**
* @brief Install test_handler_unhandled_error(). Call once, after akgl_error_init().
*
* akgl_error_init() reaches akerr_init(), which installs libakerror's default
* handler, so this has to come after it rather than before.
*/
#define TEST_TRAP_UNHANDLED_ERRORS() \
(akerr_handler_unhandled_error = &test_handler_unhandled_error)
/** @brief Fail the enclosing ATTEMPT block unless @p cond holds. */
#define TEST_ASSERT(e, cond, ...) \

View File

@@ -256,7 +256,7 @@ akerr_ErrorContext *test_text_render_backend_guards(void)
ATTEMPT {
// Nothing initialized the renderer at all, which is where the global
// starts and where a host that has not called akgl_game_init leaves it.
renderer = NULL;
akgl_renderer = NULL;
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
"drawing text with no renderer backend");
@@ -264,13 +264,13 @@ akerr_ErrorContext *test_text_render_backend_guards(void)
// A backend that exists but was never given an SDL_Renderer -- the
// state a host is in between allocating one and initializing it.
memset(&backend, 0x00, sizeof(akgl_RenderBackend));
renderer = &backend;
akgl_renderer = &backend;
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
"drawing text through a backend with no SDL renderer");
// A live SDL_Renderer but no methods: a host that created its own
// renderer and has not called akgl_render_bind2d() yet. This is the one
// renderer and has not called akgl_render_2d_bind() yet. This is the one
// that used to be a segfault rather than an error -- with a real
// renderer behind it the call gets all the way to a NULL function
// pointer, which is why the check has to be here and not left to SDL.
@@ -279,7 +279,7 @@ akerr_ErrorContext *test_text_render_backend_guards(void)
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
"drawing text through a backend that was never bound");
renderer = &testbackend;
akgl_renderer = &testbackend;
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_rendertextat(NULL, "hello", white, 0, 0, 0),
"drawing text with a NULL font");
@@ -287,7 +287,7 @@ akerr_ErrorContext *test_text_render_backend_guards(void)
akgl_text_rendertextat(testfont, NULL, white, 0, 0, 0),
"drawing a NULL string");
} CLEANUP {
renderer = &testbackend;
akgl_renderer = &testbackend;
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
@@ -302,25 +302,86 @@ akerr_ErrorContext *test_text_rendertextat(void)
// A bound backend over a software renderer is enough to draw through:
// no display, no window shown, and the whole path from rasterizing to
// blitting runs.
renderer = &testbackend;
TEST_EXPECT_OK(errctx, renderer->frame_start(renderer), "starting a frame");
akgl_renderer = &testbackend;
TEST_EXPECT_OK(errctx, akgl_renderer->frame_start(akgl_renderer), "starting a frame");
TEST_EXPECT_OK(errctx, akgl_text_rendertextat(testfont, "hello", white, 0, 4, 4),
"drawing a line of text");
// Wrapping takes the other rasterizing path.
TEST_EXPECT_OK(errctx,
akgl_text_rendertextat(testfont, "one two three", white, TEST_TARGET_SIZE / 2, 0, 0),
"drawing wrapped text");
// Not asserted here: the empty string, which SDL_ttf refuses with "Text
// has zero width" on both paths, so drawing an empty line reports a
// failure rather than drawing nothing. That is filed rather than pinned
// -- see TODO.md, "Known and still open".
TEST_EXPECT_OK(errctx, renderer->frame_end(renderer), "ending the frame");
// The empty string. SDL_ttf refuses it with "Text has zero width" from
// both rasterizers, so this used to report AKERR_NULLPOINTER for what a
// caller means as "draw nothing" -- while akgl_text_measure("") is
// documented as legal. This case was written and deliberately left
// unasserted until the two halves of the header agreed.
TEST_EXPECT_OK(errctx, akgl_text_rendertextat(testfont, "", white, 0, 0, 0),
"drawing an empty line of text");
TEST_EXPECT_OK(errctx, akgl_text_rendertextat(testfont, "", white, TEST_TARGET_SIZE / 2, 0, 0),
"drawing an empty line of wrapped text");
// Drawing nothing must still refuse the things drawing something
// refuses, rather than short-circuiting past the checks.
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_rendertextat(NULL, "", white, 0, 0, 0),
"drawing an empty line with no font");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_rendertextat(testfont, NULL, white, 0, 0, 0),
"drawing a NULL string");
TEST_EXPECT_OK(errctx, akgl_renderer->frame_end(akgl_renderer), "ending the frame");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief akgl_text_unloadallfonts closes every registered font and clears the registry.
*
* A font was reachable only by name, so a game that exited without unloading
* each one by hand left them open -- and SDL_Quit destroying the property
* registry took the last reference with it. Roughly 10 KB per font, most of it
* FreeType's. Bounded by how many fonts a game loads, but there was nothing a
* game could do about it.
*/
akerr_ErrorContext *test_text_unloadallfonts(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_registry_init_font());
CATCH(errctx, akgl_text_loadfont("unloadall_a", TEST_FONT_PATH, 16));
CATCH(errctx, akgl_text_loadfont("unloadall_b", TEST_FONT_PATH, 24));
TEST_ASSERT(errctx,
SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "unloadall_a", NULL) != NULL,
"the first font is not in the registry");
TEST_ASSERT(errctx,
SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "unloadall_b", NULL) != NULL,
"the second font is not in the registry");
TEST_EXPECT_OK(errctx, akgl_text_unloadallfonts(), "unloading every font");
TEST_ASSERT(errctx, AKGL_REGISTRY_FONT == 0,
"unloading every font left the registry behind");
// Shutdown paths run after partial startups, so a second call and a
// call against no registry at all are both success.
TEST_EXPECT_OK(errctx, akgl_text_unloadallfonts(), "unloading every font again");
// And the subsystem comes back with a fresh registry.
CATCH(errctx, akgl_registry_init_font());
TEST_EXPECT_OK(errctx, akgl_text_loadfont("unloadall_c", TEST_FONT_PATH, 16),
"loading a font after unloading them all");
TEST_EXPECT_OK(errctx, akgl_text_unloadallfonts(), "unloading that one too");
} CLEANUP {
if ( AKGL_REGISTRY_FONT == 0 ) {
IGNORE(akgl_registry_init_font());
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
@@ -331,6 +392,7 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
memset(&testbackend, 0x00, sizeof(akgl_RenderBackend));
FAIL_ZERO_BREAK(
errctx,
@@ -355,7 +417,7 @@ int main(void)
SDL_GetError());
// The host owns the window and libakgl binds to it, which is the
// arrangement akgl_render_bind2d exists for and the one an embedded
// arrangement akgl_render_2d_bind exists for and the one an embedded
// interpreter drawing text is in.
FAIL_ZERO_BREAK(
errctx,
@@ -364,18 +426,19 @@ int main(void)
TEST_TARGET_SIZE,
TEST_TARGET_SIZE,
0,
&window,
&akgl_window,
&testbackend.sdl_renderer),
AKGL_ERR_SDL,
"Couldn't create window/renderer: %s",
SDL_GetError());
CATCH(errctx, akgl_render_bind2d(&testbackend));
CATCH(errctx, akgl_render_2d_bind(&testbackend));
CATCH(errctx, test_text_loadfont());
CATCH(errctx, test_text_measure());
CATCH(errctx, test_text_measure_wrapped());
CATCH(errctx, test_text_render_backend_guards());
CATCH(errctx, test_text_rendertextat());
CATCH(errctx, test_text_unloadallfonts());
} CLEANUP {
if ( testfont != NULL ) {
TTF_CloseFont(testfont);

View File

@@ -11,6 +11,102 @@
#include <akgl/game.h>
#include <akgl/json_helpers.h>
#include "testutil.h"
/**
* @brief akgl_get_json_tilemap_property must give back its scratch strings on every path.
*
* It claims two pool strings per call -- one for the property name it is
* comparing, one for the declared type -- and used to leave the block through a
* `SUCCEED_RETURN` from inside `ATTEMPT` on the path where it *found* what it
* was asked for. That returns past `CLEANUP`, so the ordinary, successful
* lookup was the leaking one, at two of the pool's 256 entries a time. A map
* load does this several times per layer.
*
* Three paths, run more times than the pool has entries: found, absent, and
* present-but-wrong-type. Against the old code the found loop exhausts the pool
* and comes back AKGL_ERR_HEAP long before it finishes.
*/
akerr_ErrorContext *test_tilemap_property_lookup_releases_strings(void)
{
PREPARE_ERROR(errctx);
json_t *jsondoc = NULL;
json_error_t jsonerr;
akgl_String *pathstr = NULL;
int baseline = 0;
int propnum = 0;
int i = 0;
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf(
(char *)&pathstr->data,
AKGL_MAX_STRING_LENGTH,
"%s%s",
SDL_GetBasePath(),
"assets/snippets/test_tilemap_get_json_tilemap_property.json"
);
jsondoc = json_load_file((char *)&pathstr->data, 0, (json_error_t *)&jsonerr);
FAIL_ZERO_BREAK(errctx, jsondoc, AKERR_NULLPOINTER, "Failure loading json fixture: %s", (char *)jsonerr.text);
baseline = test_string_pool_used();
// The success path. Twice the pool size, so a leak of even one entry
// per call cannot finish.
for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
CATCH(errctx, akgl_get_json_properties_integer(jsondoc, "state", &propnum));
}
TEST_ASSERT(
errctx,
test_string_pool_used() == baseline,
"%d successful property lookups left %d pool strings claimed, expected %d",
(AKGL_MAX_HEAP_STRING * 2),
test_string_pool_used(),
baseline);
// The absent path, which reports AKERR_KEY from after FINISH.
for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
TEST_EXPECT_STATUS(
errctx,
AKERR_KEY,
akgl_get_json_properties_integer(jsondoc, "no_such_property", &propnum),
"looking up a property the object does not have");
}
TEST_ASSERT(
errctx,
test_string_pool_used() == baseline,
"absent-property lookups left %d pool strings claimed, expected %d",
test_string_pool_used(),
baseline);
// The wrong-type path, which fails from inside the ATTEMPT block with
// both scratch strings already claimed.
for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
TEST_EXPECT_STATUS(
errctx,
AKERR_TYPE,
akgl_get_json_properties_integer(jsondoc, "character", &propnum),
"reading a string property as an int");
}
TEST_ASSERT(
errctx,
test_string_pool_used() == baseline,
"wrong-type lookups left %d pool strings claimed, expected %d",
test_string_pool_used(),
baseline);
} CLEANUP {
if ( jsondoc != NULL ) {
json_decref(jsondoc);
jsondoc = NULL;
}
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void)
{
PREPARE_ERROR(errctx);
@@ -20,8 +116,8 @@ akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void)
int propnum;
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&tmpstr));
snprintf(
(char *)&tmpstr->data,
@@ -87,34 +183,34 @@ akerr_ErrorContext *test_akgl_tilemap_compute_tileset_offsets(void)
10 // Tile 2 Y
};
int *comparison_ptrs[8] = {
&gamemap->tilesets[0].tile_offsets[0][0], // Tile 0 X
&gamemap->tilesets[0].tile_offsets[0][1], // Tile 0 Y
&gamemap->tilesets[0].tile_offsets[1][0], // Tile 1 X
&gamemap->tilesets[0].tile_offsets[1][1], // Tile 0 Y
&gamemap->tilesets[0].tile_offsets[2][0], // Tile 2 X
&gamemap->tilesets[0].tile_offsets[2][1], // Tile 2 Y
&gamemap->tilesets[0].tile_offsets[3][0], // Tile 3 X
&gamemap->tilesets[0].tile_offsets[3][1], // Tile 3 Y
&akgl_gamemap->tilesets[0].tile_offsets[0][0], // Tile 0 X
&akgl_gamemap->tilesets[0].tile_offsets[0][1], // Tile 0 Y
&akgl_gamemap->tilesets[0].tile_offsets[1][0], // Tile 1 X
&akgl_gamemap->tilesets[0].tile_offsets[1][1], // Tile 0 Y
&akgl_gamemap->tilesets[0].tile_offsets[2][0], // Tile 2 X
&akgl_gamemap->tilesets[0].tile_offsets[2][1], // Tile 2 Y
&akgl_gamemap->tilesets[0].tile_offsets[3][0], // Tile 3 X
&akgl_gamemap->tilesets[0].tile_offsets[3][1], // Tile 3 Y
};
int i = 0;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
gamemap->tilesets[0].tilecount = 4;
gamemap->tilesets[0].columns = 2;
gamemap->tilesets[0].firstgid = 1;
gamemap->tilesets[0].imageheight = 20;
gamemap->tilesets[0].imagewidth = 20;
gamemap->tilesets[0].tilecount = 4;
gamemap->tilesets[0].tileheight = 10;
gamemap->tilesets[0].tilewidth = 10;
gamemap->tilesets[0].spacing = 0;
gamemap->tilesets[0].margin = 0;
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
akgl_gamemap->tilesets[0].tilecount = 4;
akgl_gamemap->tilesets[0].columns = 2;
akgl_gamemap->tilesets[0].firstgid = 1;
akgl_gamemap->tilesets[0].imageheight = 20;
akgl_gamemap->tilesets[0].imagewidth = 20;
akgl_gamemap->tilesets[0].tilecount = 4;
akgl_gamemap->tilesets[0].tileheight = 10;
akgl_gamemap->tilesets[0].tilewidth = 10;
akgl_gamemap->tilesets[0].spacing = 0;
akgl_gamemap->tilesets[0].margin = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(gamemap, 0));
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(akgl_gamemap, 0));
// Tile 0 X offset
for ( i = 0; i < 8; i++ ) {
FAIL_NONZERO_BREAK(
@@ -133,6 +229,190 @@ akerr_ErrorContext *test_akgl_tilemap_compute_tileset_offsets(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief The loader must refuse a map that would overrun its fixed tables.
*
* Two loops indexed straight from the document. `curlayer->objects[j]` had no
* check against AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER, and `dest->tilesets[i]`
* none against AKGL_TILEMAP_MAX_TILESETS. The object one is the reachable one:
* 128 objects is not a large object layer, and akgl_TilemapObject is big, so
* the 129th wrote well past the end of the layer.
*
* akgl_tilemap_load_layers already bounded its own loop and raised
* AKERR_OUTOFBOUNDS, so the shape to copy was in the same file.
*/
/**
* @brief Releasing a map twice must not free a texture twice.
*
* The layers loop tested `dest->layers[i].texture` and then destroyed
* `dest->tilesets[i].texture` -- already destroyed by the loop above it. So
* every tileset texture was freed twice on a single release, and no image
* layer's texture was freed at all. Neither pointer was cleared either, so a
* second release was a use-after-free on top of that.
*/
/**
* @brief A load/release cycle must give back every pooled string it took.
*
* Measured before the fix by counting non-zero HEAP_STRING refcounts across
* cycles: five per cycle, exactly, and akgl_tilemap_release gave none of them
* back. The pool is 256 entries, so the 52nd map load in a process found it
* empty -- and until the accessors were fixed, "empty" arrived as a segfault
* rather than as AKGL_ERR_HEAP.
*
* Blast radius is every level transition, and a game that reloads a level on
* death hits it sooner.
*/
akerr_ErrorContext *test_akgl_tilemap_load_releases_strings(void)
{
akgl_String *pathstr = NULL;
PREPARE_ERROR(errctx);
int baseline = 0;
int afterfirst = 0;
int i = 0;
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
SDL_GetBasePath(), "assets/testmap.tmj");
baseline = test_string_pool_used();
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
CATCH(errctx, akgl_tilemap_load((char *)&pathstr->data, akgl_gamemap));
CATCH(errctx, akgl_tilemap_release(akgl_gamemap));
afterfirst = test_string_pool_used();
TEST_ASSERT(errctx, afterfirst == baseline,
"one load/release cycle left %d pool strings claimed, expected %d",
afterfirst, baseline);
// Then enough cycles that a leak of even one string per load would run
// the pool dry, so this cannot pass by rounding.
for ( i = 0; i < 64; i++ ) {
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
CATCH(errctx, akgl_tilemap_load((char *)&pathstr->data, akgl_gamemap));
CATCH(errctx, akgl_tilemap_release(akgl_gamemap));
}
TEST_ASSERT(errctx, test_string_pool_used() == baseline,
"64 load/release cycles left %d pool strings claimed, expected %d",
test_string_pool_used(), baseline);
} CLEANUP {
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_release_is_idempotent(void)
{
akgl_String *pathstr = NULL;
PREPARE_ERROR(errctx);
int i = 0;
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
SDL_GetBasePath(), "assets/testmap.tmj");
CATCH(errctx, akgl_tilemap_load((char *)&pathstr->data, akgl_gamemap));
TEST_ASSERT(errctx, akgl_gamemap->numtilesets > 0,
"the fixture map loaded no tilesets, so this test proves nothing");
TEST_ASSERT(errctx, akgl_gamemap->tilesets[0].texture != NULL,
"the fixture map's first tileset has no texture");
TEST_EXPECT_OK(errctx, akgl_tilemap_release(akgl_gamemap), "releasing a loaded map");
for ( i = 0; i < AKGL_TILEMAP_MAX_TILESETS; i++ ) {
TEST_ASSERT(errctx, akgl_gamemap->tilesets[i].texture == NULL,
"tileset %d's texture pointer survived release", i);
}
for ( i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
TEST_ASSERT(errctx, akgl_gamemap->layers[i].texture == NULL,
"layer %d's texture pointer survived release", i);
}
// The one that used to be a use-after-free.
TEST_EXPECT_OK(errctx, akgl_tilemap_release(akgl_gamemap), "releasing the same map again");
TEST_EXPECT_OK(errctx, akgl_tilemap_release(akgl_gamemap), "releasing it a third time");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_tilemap_release(NULL),
"releasing a NULL map");
} CLEANUP {
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_load_bounds_fixed_tables(void)
{
akgl_String *pathstr = NULL;
PREPARE_ERROR(errctx);
json_t *doc = NULL;
json_error_t errdata;
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
// Exactly the maximum must still load.
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
SDL_GetBasePath(), "assets/snippets/test_tilemap_max_objects.json");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "max-objects fixture: %s", (char *)&errdata.text);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
TEST_EXPECT_OK(errctx,
akgl_tilemap_load_layer_objects(akgl_gamemap, doc, 0, pathstr),
"loading an object layer with exactly the maximum objects");
json_decref(doc);
doc = NULL;
// One more must be refused rather than written.
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
SDL_GetBasePath(), "assets/snippets/test_tilemap_too_many_objects.json");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "too-many-objects fixture: %s", (char *)&errdata.text);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_tilemap_load_layer_objects(akgl_gamemap, doc, 0, pathstr),
"loading an object layer with one object too many");
json_decref(doc);
doc = NULL;
// And the tileset table.
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
SDL_GetBasePath(), "assets/snippets/test_tilemap_too_many_tilesets.json");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "too-many-tilesets fixture: %s", (char *)&errdata.text);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_tilemap_load_tilesets(akgl_gamemap, doc, pathstr),
"loading a map with one tileset too many");
TEST_ASSERT(errctx, akgl_gamemap->numtilesets <= AKGL_TILEMAP_MAX_TILESETS,
"numtilesets reached %d, past the %d the table holds",
akgl_gamemap->numtilesets, AKGL_TILEMAP_MAX_TILESETS);
} CLEANUP {
if ( doc != NULL ) {
json_decref(doc);
}
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_load_layer_objects(void)
{
akgl_String *pathstr;
@@ -143,10 +423,10 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_objects(void)
json_error_t errdata;
akgl_Actor *testactor;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_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);
@@ -154,7 +434,7 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_objects(void)
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
CATCH(errctx, akgl_get_json_array_index_object(layers, 1, &objectlayer));
CATCH(errctx, akgl_tilemap_load_layer_objects(gamemap, objectlayer, 1, pathstr));
CATCH(errctx, akgl_tilemap_load_layer_objects(akgl_gamemap, objectlayer, 1, pathstr));
testactor = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL);
FAIL_ZERO_BREAK(
@@ -193,11 +473,11 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
json_t *tiledata = NULL;
json_error_t errdata;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_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);
@@ -206,11 +486,11 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
CATCH(errctx, akgl_get_json_array_index_object(layers, 0, &tilelayer));
CATCH(errctx, akgl_get_json_array_value(tilelayer, "data", &tiledata));
CATCH(errctx, akgl_tilemap_load_layer_tile(gamemap, tilelayer, 0, pathstr));
if ( (gamemap->layers[0].data[0] != 1) ||
(gamemap->layers[0].data[1] != 2) ||
(gamemap->layers[0].data[2] != 3) ||
(gamemap->layers[0].data[3] != 4) ) {
CATCH(errctx, akgl_tilemap_load_layer_tile(akgl_gamemap, tilelayer, 0, pathstr));
if ( (akgl_gamemap->layers[0].data[0] != 1) ||
(akgl_gamemap->layers[0].data[1] != 2) ||
(akgl_gamemap->layers[0].data[2] != 3) ||
(akgl_gamemap->layers[0].data[3] != 4) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)");
}
} CLEANUP {
@@ -233,61 +513,61 @@ akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
json_error_t errdata;
int i = 0;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_tilemap_load_layers(gamemap, doc, pathstr));
CATCH(errctx, akgl_tilemap_load_layers(akgl_gamemap, doc, pathstr));
FAIL_NONZERO_BREAK(
errctx,
(gamemap->numlayers != 3),
(akgl_gamemap->numlayers != 3),
AKERR_VALUE,
"Map layer count incorrect"
);
for ( i = 0; i < gamemap->numlayers; i++ ) {
if ( (gamemap->layers[i].opacity != 1) ||
(gamemap->layers[i].visible != true) ||
(gamemap->layers[i].id != (i + 1)) ||
(gamemap->layers[i].x != 0) ||
(gamemap->layers[i].y != 0) ) {
for ( i = 0; i < akgl_gamemap->numlayers; i++ ) {
if ( (akgl_gamemap->layers[i].opacity != 1) ||
(akgl_gamemap->layers[i].visible != true) ||
(akgl_gamemap->layers[i].id != (i + 1)) ||
(akgl_gamemap->layers[i].x != 0) ||
(akgl_gamemap->layers[i].y != 0) ) {
FAIL(errctx, AKERR_VALUE, "Map layer data loaded incorrectly (see gdb)");
goto _test_akgl_tilemap_load_layers_cleanup;
}
}
// Layer 2 should have 1 object loaded
if ( (gamemap->layers[1].objects[0].actorptr != SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL)) ||
(gamemap->layers[1].objects[1].name[0] != '\0' ) ||
(gamemap->layers[1].objects[1].id != 0) ) {
if ( (akgl_gamemap->layers[1].objects[0].actorptr != SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL)) ||
(akgl_gamemap->layers[1].objects[1].name[0] != '\0' ) ||
(akgl_gamemap->layers[1].objects[1].id != 0) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Map layer 2 should have 1 loaded object (testactor) and nothing else (see gdb)");
}
// Layer 1 and 3 should have no objects
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
if ( gamemap->layers[0].objects[i].id != 0 ) {
if ( akgl_gamemap->layers[0].objects[i].id != 0 ) {
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
goto _test_akgl_tilemap_load_layers_cleanup;
}
}
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
if ( gamemap->layers[2].objects[i].id != 0 ) {
if ( akgl_gamemap->layers[2].objects[i].id != 0 ) {
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
goto _test_akgl_tilemap_load_layers_cleanup;
}
}
// Layers 1 and 3 should have tile data
if ( (gamemap->layers[0].data[0] != 1) ||
(gamemap->layers[0].data[1] != 2) ||
(gamemap->layers[0].data[2] != 3) ||
(gamemap->layers[0].data[3] != 4) ||
(gamemap->layers[2].data[0] != 0) ||
(gamemap->layers[2].data[1] != 5) ||
(gamemap->layers[2].data[2] != 0) ||
(gamemap->layers[2].data[3] != 6)
if ( (akgl_gamemap->layers[0].data[0] != 1) ||
(akgl_gamemap->layers[0].data[1] != 2) ||
(akgl_gamemap->layers[0].data[2] != 3) ||
(akgl_gamemap->layers[0].data[3] != 4) ||
(akgl_gamemap->layers[2].data[0] != 0) ||
(akgl_gamemap->layers[2].data[1] != 5) ||
(akgl_gamemap->layers[2].data[2] != 0) ||
(akgl_gamemap->layers[2].data[3] != 6)
) {
FAIL_BREAK(errctx, AKERR_VALUE, "Map layers 1 and 3 should have tile data but it is incorrect");
}
@@ -312,43 +592,43 @@ akerr_ErrorContext *test_akgl_tilemap_load_tilesets(void)
json_error_t errdata;
SDL_Texture *image = NULL;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_tilemap_load_tilesets(gamemap, doc, pathstr));
FAIL_NONZERO_BREAK(errctx, (gamemap->numtilesets != 1), AKERR_VALUE, "Incorrect number of tilesets loaded for map");
if ( (gamemap->tilesets[0].columns != 48 ) ||
(gamemap->tilesets[0].firstgid != 1) ||
(gamemap->tilesets[0].imageheight != 576) ||
(gamemap->tilesets[0].imagewidth != 768) ||
(gamemap->tilesets[0].margin != 0) ||
(gamemap->tilesets[0].spacing != 0) ||
(gamemap->tilesets[0].tilecount != 1728) ||
(gamemap->tilesets[0].tileheight != 16) ||
(gamemap->tilesets[0].tilewidth != 16) ) {
CATCH(errctx, akgl_tilemap_load_tilesets(akgl_gamemap, doc, pathstr));
FAIL_NONZERO_BREAK(errctx, (akgl_gamemap->numtilesets != 1), AKERR_VALUE, "Incorrect number of tilesets loaded for map");
if ( (akgl_gamemap->tilesets[0].columns != 48 ) ||
(akgl_gamemap->tilesets[0].firstgid != 1) ||
(akgl_gamemap->tilesets[0].imageheight != 576) ||
(akgl_gamemap->tilesets[0].imagewidth != 768) ||
(akgl_gamemap->tilesets[0].margin != 0) ||
(akgl_gamemap->tilesets[0].spacing != 0) ||
(akgl_gamemap->tilesets[0].tilecount != 1728) ||
(akgl_gamemap->tilesets[0].tileheight != 16) ||
(akgl_gamemap->tilesets[0].tilewidth != 16) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Tileset loaded with incorrect values");
}
FAIL_NONZERO_BREAK(
errctx,
strcmp((char *)&gamemap->tilesets[0].name, "World_A1"),
strcmp((char *)&akgl_gamemap->tilesets[0].name, "World_A1"),
AKERR_VALUE,
"Tileset loaded with incorrect name");
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/World_A1.png");
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&pathstr->data);
image = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&pathstr->data);
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
CATCH(
errctx,
akgl_render_and_compare(
gamemap->tilesets[0].texture,
akgl_gamemap->tilesets[0].texture,
image,
0, 0, 768, 576,
"test_akgl_tilemap_loaded_tileset.png")
@@ -370,12 +650,12 @@ akerr_ErrorContext *test_akgl_tilemap_load(void)
{
PREPARE_ERROR(errctx);
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
CATCH(errctx, akgl_tilemap_load("assets/testmap.tmj", gamemap));
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_tilemap_load("assets/testmap.tmj", akgl_gamemap));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
@@ -387,8 +667,8 @@ akerr_ErrorContext *test_akgl_tilemap_draw(void)
PREPARE_ERROR(errctx);
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
@@ -401,8 +681,8 @@ akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void)
PREPARE_ERROR(errctx);
ATTEMPT {
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
@@ -415,18 +695,19 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer;
TEST_TRAP_UNHANDLED_ERRORS();
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
}
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 768, 576, 0, &window, &renderer->sdl_renderer)) {
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 768, 576, 0, &akgl_window, &akgl_renderer->sdl_renderer)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
}
renderer->draw_texture = &akgl_render_2d_draw_texture;
akgl_renderer->draw_texture = &akgl_render_2d_draw_texture;
CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_heap_init());
@@ -435,11 +716,15 @@ int main(void)
CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));
CATCH(errctx, test_tilemap_akgl_get_json_tilemap_property());
CATCH(errctx, test_tilemap_property_lookup_releases_strings());
CATCH(errctx, test_akgl_tilemap_compute_tileset_offsets());
CATCH(errctx, test_akgl_tilemap_load_layer_objects());
CATCH(errctx, test_akgl_tilemap_load_layer_tile());
CATCH(errctx, test_akgl_tilemap_load_layers());
CATCH(errctx, test_akgl_tilemap_load_tilesets());
CATCH(errctx, test_akgl_tilemap_load_bounds_fixed_tables());
CATCH(errctx, test_akgl_tilemap_release_is_idempotent());
CATCH(errctx, test_akgl_tilemap_load_releases_strings());
//CATCH(errctx, test_akgl_tilemap_load());
//CATCH(errctx, test_akgl_tilemap_draw_tileset());
//CATCH(errctx, test_akgl_tilemap_draw());

View File

@@ -6,6 +6,8 @@
#include <akgl/staticstring.h>
#include <akgl/util.h>
#include "testutil.h"
/**
* @brief How many entries of AKERR_ARRAY_ERROR are currently held by somebody.
*
@@ -28,14 +30,14 @@ static int live_error_contexts(void)
akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
{
RectanglePoints points;
akgl_RectanglePoints points;
// Zeroed for the same reason as the fixtures in
// test_akgl_collide_point_rectangle_nullpointers: the last case here is a
// real call, and feeding it stack garbage is noise under `memcheck`.
SDL_FRect testrect = {.x = 0, .y = 0, .w = 0, .h = 0};
PREPARE_ERROR(errctx);
memset((void *)&points, 0x00, sizeof(RectanglePoints));
memset((void *)&points, 0x00, sizeof(akgl_RectanglePoints));
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(NULL, NULL));
@@ -57,7 +59,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&points, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL RectanglePoints pointer");
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL akgl_RectanglePoints pointer");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
@@ -75,9 +77,9 @@ akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
akerr_ErrorContext *test_akgl_rectangle_points_math(void)
{
RectanglePoints points;
akgl_RectanglePoints points;
SDL_FRect testrect = {.x = 0, .y = 0, .w = 32, .h = 32};
memset((void *)&points, 0x00, sizeof(RectanglePoints));
memset((void *)&points, 0x00, sizeof(akgl_RectanglePoints));
PREPARE_ERROR(errctx);
ATTEMPT {
@@ -112,13 +114,13 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
// function is a real call with real arguments, and reading uninitialised
// floats out of it is sixteen findings under `memcheck` for a test that is
// not about coordinates at all.
point testpoint = { .x = 0, .y = 0 };
RectanglePoints testrectpoints;
akgl_Point testpoint = { .x = 0, .y = 0 };
akgl_RectanglePoints testrectpoints;
bool testcollide = false;
PREPARE_ERROR(errctx);
memset(&testrectpoints, 0x00, sizeof(RectanglePoints));
memset(&testrectpoints, 0x00, sizeof(akgl_RectanglePoints));
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, NULL));
@@ -167,9 +169,9 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void)
{
point testpoint = {.x = 16, .y = 16};
akgl_Point testpoint = {.x = 16, .y = 16};
SDL_FRect testrect = { .x = 0, .y = 0, .w = 32, .h = 32};
RectanglePoints testrectpoints;
akgl_RectanglePoints testrectpoints;
bool testcollide = false;
PREPARE_ERROR(errctx);
@@ -354,6 +356,69 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
* The loop runs well past AKERR_MAX_ARRAY_ERROR on purpose: at the old
* behaviour this test does not fail, it terminates the suite.
*/
/**
* @brief akgl_compare_sdl_surfaces must check geometry before it memcmps.
*
* It compared `s1->pitch * s1->h` bytes out of both surfaces without looking at
* the second one's dimensions, so a smaller s2 was read past its end rather
* than reported as a mismatch. Benign in practice and immediately fatal under a
* memory checker, which is the reason to fix it rather than leave it.
*/
akerr_ErrorContext *test_akgl_compare_sdl_surfaces_checks_geometry(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *big = NULL;
SDL_Surface *small = NULL;
SDL_Surface *twin = NULL;
ATTEMPT {
big = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGBA8888);
twin = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGBA8888);
small = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA8888);
FAIL_ZERO_BREAK(errctx, big, AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, twin, AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, small, AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(big, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(twin, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(small, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_EXPECT_OK(errctx, akgl_compare_sdl_surfaces(big, twin),
"comparing two identical surfaces");
// The one that used to read 4 KiB past the end of an 8x8 surface.
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(big, small),
"comparing a 32x32 surface against an 8x8 one");
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(small, big),
"comparing an 8x8 surface against a 32x32 one");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_compare_sdl_surfaces(NULL, big),
"comparing a NULL first surface");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_compare_sdl_surfaces(big, NULL),
"comparing a NULL second surface");
// Same dimensions, different format: the pixels are not comparable even
// though the byte count might be.
SDL_DestroySurface(small);
small = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGB24);
FAIL_ZERO_BREAK(errctx, small, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(big, small),
"comparing two surfaces of different pixel formats");
} CLEANUP {
if ( big != NULL ) {
SDL_DestroySurface(big);
}
if ( twin != NULL ) {
SDL_DestroySurface(twin);
}
if ( small != NULL ) {
SDL_DestroySurface(small);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_path_relative_releases_contexts(void)
{
PREPARE_ERROR(errctx);
@@ -392,11 +457,16 @@ int main(void)
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, test_akgl_rectangle_points_nullpointers());
CATCH(errctx, test_akgl_rectangle_points_math());
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());
// Defined since forever and never called until 0.5.0. TODO.md, "Known
// and still open" item 9.
CATCH(errctx, test_akgl_collide_point_rectangle_logic());
CATCH(errctx, test_akgl_collide_rectangles_nullpointers());
CATCH(errctx, test_akgl_collide_rectangles_logic());
CATCH(errctx, test_akgl_compare_sdl_surfaces_checks_geometry());
CATCH(errctx, test_akgl_path_relative_releases_contexts());
} CLEANUP {
} PROCESS(errctx) {

View File

@@ -99,6 +99,7 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, test_version_string_matches_components());
CATCH(errctx, test_version_linked_matches_compiled());
CATCH(errctx, test_version_at_least_boundaries());

View File

@@ -35,7 +35,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
return SDL_APP_FAILURE;
}
*appstate = (void *)&game.state;
*appstate = (void *)&akgl_game.state;
characterjson = argv[1];
memset((char *)&pathbuf, 0x00, 4096);
memset((char *)&cwdbuf, 0x00, 1024);
@@ -45,14 +45,14 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
FAIL_ZERO_BREAK(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate pointer");
FAIL_ZERO_BREAK(errctx, getcwd((char *)&cwdbuf, 1024), AKERR_NULLPOINTER, "Couldn't get current working directory");
strcpy((char *)&game.name, "charviewer");
strcpy((char *)&game.version, "0.0.1");
strcpy((char *)&game.uri, "net.aklabs.libakgl.charviewer");
strcpy((char *)&akgl_game.name, "charviewer");
strcpy((char *)&akgl_game.version, "0.0.1");
strcpy((char *)&akgl_game.uri, "net.aklabs.libakgl.charviewer");
CATCH(errctx, akgl_game_init());
CATCH(errctx, akgl_set_property("game.screenwidth", "640"));
CATCH(errctx, akgl_set_property("game.screenheight", "480"));
CATCH(errctx, akgl_render_init2d(renderer));
CATCH(errctx, akgl_physics_init_null(physics));
CATCH(errctx, akgl_render_2d_init(akgl_renderer));
CATCH(errctx, akgl_physics_init_null(akgl_physics));
} CLEANUP {
} PROCESS(errctx) {
@@ -144,9 +144,9 @@ SDL_AppResult SDL_AppIterate(void *appstate)
AKGL_BITMASK_CLEAR(opflags.flags);
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_UPDATE);
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_RENDER);
akgl_draw_background((int)camera->w, (int)camera->h);
ATTEMPT {
CATCH(errctx, akgl_tilemap_draw(gamemap, camera, i));
CATCH(errctx, akgl_draw_background(akgl_renderer, (int)akgl_camera->w, (int)akgl_camera->h));
CATCH(errctx, akgl_tilemap_draw(akgl_gamemap, akgl_camera, i));
SDL_EnumerateProperties(AKGL_REGISTRY_ACTOR, &akgl_registry_iterate_actor, (void *)&opflags);
} CLEANUP {
} PROCESS(errctx) {
@@ -154,7 +154,7 @@ SDL_AppResult SDL_AppIterate(void *appstate)
LOG_ERROR(errctx);
return SDL_APP_FAILURE;
} FINISH_NORETURN(errctx);
SDL_RenderPresent(renderer->sdl_renderer);
SDL_RenderPresent(akgl_renderer->sdl_renderer);
return SDL_APP_CONTINUE;
}