Fix fatal async asset load errors, remove unused event system

An async asset load failure crashed the whole game via errorThrow,
while the identical sync failure just logged and continued - a single
missing/corrupted asset could take down the process. assetUpdate now
handles the async error path the same way as sync (invoke onError,
keep running).

Also removes event.h/event.c and assetbatch, which existed only to
support multiple subscribers per asset event but had no real caller
that ever used more than one (assetbatch itself had zero callers
anywhere). Asset entries, uifullbox, and uiloading now use plain
single-callback + user-pointer fields instead of the generic
array-backed event_t.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 10:14:38 -05:00
parent 1d73b9d224
commit 717902462b
20 changed files with 116 additions and 994 deletions
-1
View File
@@ -53,7 +53,6 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
# Subdirs # Subdirs
add_subdirectory(animation) add_subdirectory(animation)
add_subdirectory(event)
add_subdirectory(assert) add_subdirectory(assert)
add_subdirectory(asset) add_subdirectory(asset)
add_subdirectory(console) add_subdirectory(console)
-1
View File
@@ -7,7 +7,6 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
asset.c asset.c
assetbatch.c
assetfile.c assetfile.c
) )
+5 -3
View File
@@ -322,7 +322,9 @@ errorret_t assetUpdate(void) {
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) { } else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
assetentry_t *loadedEntry = loading->entry; assetentry_t *loadedEntry = loading->entry;
loading->entry = NULL; loading->entry = NULL;
eventInvoke(&loadedEntry->onLoaded, loadedEntry); if(loadedEntry->onLoaded) {
loadedEntry->onLoaded(loadedEntry, loadedEntry->onLoadedUser);
}
} }
loading++; loading++;
@@ -346,8 +348,8 @@ errorret_t assetUpdate(void) {
assetentry_t *errEntry = loading->entry; assetentry_t *errEntry = loading->entry;
loading->entry = NULL; loading->entry = NULL;
threadMutexUnlock(&loading->mutex); threadMutexUnlock(&loading->mutex);
eventInvoke(&errEntry->onError, errEntry); if(errEntry->onError) errEntry->onError(errEntry, errEntry->onErrorUser);
errorThrow("Failed to load asset asynchronously."); loading++;
break; break;
} }
-165
View File
@@ -1,165 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetbatch.h"
#include "asset.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <unistd.h>
void assetBatchInit(
assetbatch_t *batch,
const uint16_t count,
const assetbatchdesc_t *descs
) {
assertNotNull(batch, "Batch cannot be NULL.");
assertNotNull(descs, "Descs cannot be NULL.");
assertTrue(count > 0, "Count must be greater than 0.");
assertTrue(
count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX."
);
memoryZero(batch, sizeof(assetbatch_t));
batch->count = count;
eventInit(
&batch->onLoaded,
batch->onLoadedCallbacks, batch->onLoadedUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryLoaded,
batch->onEntryLoadedCallbacks,
batch->onEntryLoadedUsers,
ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onError,
batch->onErrorCallbacks, batch->onErrorUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryError,
batch->onEntryErrorCallbacks,
batch->onEntryErrorUsers,
ASSET_BATCH_EVENT_MAX
);
for(uint16_t i = 0; i < count; i++) {
batch->inputs[i] = descs[i].input;
batch->entries[i] = assetLock(
descs[i].path, descs[i].type, &batch->inputs[i]
);
if(batch->entries[i]->state == ASSET_ENTRY_STATE_LOADED) {
// Already loaded (cached) - count it now, no subscription needed.
batch->loadedCount++;
} else if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) {
batch->errorCount++;
} else {
eventSubscribe(
&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb, batch
);
eventSubscribe(
&batch->entries[i]->onError, assetBatchEntryOnErrorCb, batch
);
}
}
}
void assetBatchLock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryLock(batch->entries[i]);
}
}
void assetBatchUnlock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryUnlock(batch->entries[i]);
}
}
bool_t assetBatchIsLoaded(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state != ASSET_ENTRY_STATE_LOADED) return false;
}
return true;
}
bool_t assetBatchHasError(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) return true;
}
return false;
}
errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
bool_t allDone;
do {
allDone = true;
for(uint16_t i = 0; i < batch->count; i++) {
const assetentrystate_t state = batch->entries[i]->state;
if(state == ASSET_ENTRY_STATE_ERROR) {
errorThrow("Asset '%s' failed to load.", batch->entries[i]->name);
}
if(state != ASSET_ENTRY_STATE_LOADED) {
allDone = false;
}
}
if(!allDone) {
usleep(1000);
errorChain(assetUpdate());
}
} while(!allDone);
errorOk();
}
void assetBatchDispose(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]) {
// Unsubscribe while we still hold a lock so the entry is live.
eventUnsubscribe(&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb);
eventUnsubscribe(&batch->entries[i]->onError, assetBatchEntryOnErrorCb);
assetUnlockEntry(batch->entries[i]);
}
}
memoryZero(batch, sizeof(assetbatch_t));
}
void assetBatchEntryOnLoadedCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->loadedCount++;
eventInvoke(&batch->onEntryLoaded, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
if(batch->errorCount == 0) {
eventInvoke(&batch->onLoaded, batch);
} else {
eventInvoke(&batch->onError, batch);
}
}
}
void assetBatchEntryOnErrorCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->errorCount++;
eventInvoke(&batch->onEntryError, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
eventInvoke(&batch->onError, batch);
}
}
-124
View File
@@ -1,124 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#include "event/event.h"
#define ASSET_BATCH_COUNT_MAX 64
#define ASSET_BATCH_EVENT_MAX 4
typedef struct {
const char_t *path;
assetloadertype_t type;
assetloaderinput_t input;
} assetbatchdesc_t;
typedef struct {
assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
uint16_t count;
uint16_t loadedCount;
uint16_t errorCount;
/** Fires once when every entry loaded. params = assetbatch_t * */
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry loads. params = assetentry_t * */
event_t onEntryLoaded;
eventcallback_t onEntryLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires when all entries finish (any with errors). params: assetbatch_t * */
event_t onError;
eventcallback_t onErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onErrorUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry errors. params = assetentry_t * */
event_t onEntryError;
eventcallback_t onEntryErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryErrorUsers[ASSET_BATCH_EVENT_MAX];
} assetbatch_t;
/**
* Initialises the batch from an array of descriptors. Each entry is locked
* and queued for loading immediately.
*
* @param batch Batch to initialise.
* @param descs Array of entry descriptors (need not outlive this call).
* @param count Number of descriptors (must be <= ASSET_BATCH_COUNT_MAX).
*/
void assetBatchInit(
assetbatch_t *batch,
uint16_t count,
const assetbatchdesc_t *descs
);
/**
* Acquires one additional lock on every entry in the batch.
*
* @param batch Batch to lock.
*/
void assetBatchLock(assetbatch_t *batch);
/**
* Releases one lock from every entry in the batch. When an entry's lock
* count reaches zero it will be reaped on the next assetUpdate.
*
* @param batch Batch to unlock.
*/
void assetBatchUnlock(assetbatch_t *batch);
/**
* Returns true if every entry in the batch has finished loading.
*
* @param batch Batch to query.
*/
bool_t assetBatchIsLoaded(const assetbatch_t *batch);
/**
* Returns true if any entry in the batch is in an error state.
*
* @param batch Batch to query.
*/
bool_t assetBatchHasError(const assetbatch_t *batch);
/**
* Blocks until every entry is loaded. Returns an error if any entry fails.
*
* @param batch Batch to wait on.
*/
errorret_t assetBatchRequireLoaded(assetbatch_t *batch);
/**
* Releases the batch's lock on every entry and clears the batch. After this
* call the batch struct may be reused with assetBatchInit.
*
* @param batch Batch to dispose.
*/
void assetBatchDispose(assetbatch_t *batch);
/**
* Event trampoline invoked when a batch entry finishes loading.
* Increments the loaded counter and fires batch-level events.
*
* @param params The loaded assetentry_t pointer.
* @param user The owning assetbatch_t pointer.
*/
void assetBatchEntryOnLoadedCb(void *params, void *user);
/**
* Event trampoline invoked when a batch entry fails to load.
* Increments the error counter and fires batch-level events.
*
* @param params The errored assetentry_t pointer.
* @param user The owning assetbatch_t pointer.
*/
void assetBatchEntryOnErrorCb(void *params, void *user);
+1 -17
View File
@@ -35,22 +35,6 @@ void assetEntryInit(
entry->input = NULL; entry->input = NULL;
} }
refInit(&entry->refs, entry, NULL, NULL, NULL); refInit(&entry->refs, entry, NULL, NULL, NULL);
eventInit(
&entry->onLoaded,
entry->onLoadedCallbacks, entry->onLoadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onUnloaded,
entry->onUnloadedCallbacks, entry->onUnloadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onError,
entry->onErrorCallbacks, entry->onErrorUsers,
ASSET_ENTRY_EVENT_MAX
);
} }
void assetEntryLock(assetentry_t *entry) { void assetEntryLock(assetentry_t *entry) {
@@ -97,7 +81,7 @@ errorret_t assetEntryDispose(assetentry_t *entry) {
"Asset entry still refed at dispose time." "Asset entry still refed at dispose time."
); );
eventInvoke(&entry->onUnloaded, entry); if(entry->onUnloaded) entry->onUnloaded(entry, entry->onUnloadedUser);
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry)); errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
memoryZero(entry, sizeof(assetentry_t)); memoryZero(entry, sizeof(assetentry_t));
errorOk(); errorOk();
+22 -20
View File
@@ -7,7 +7,6 @@
#pragma once #pragma once
#include "asset/loader/assetloading.h" #include "asset/loader/assetloading.h"
#include "event/event.h"
#include "util/ref.h" #include "util/ref.h"
typedef enum { typedef enum {
@@ -20,11 +19,17 @@ typedef enum {
ASSET_ENTRY_STATE_ERROR ASSET_ENTRY_STATE_ERROR
} assetentrystate_t; } assetentrystate_t;
/** Maximum number of subscribers for each per-entry event. */
#define ASSET_ENTRY_EVENT_MAX 2
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
/**
* A single asset entry callback. Each entry supports at most one subscriber
* per event - a second assignment without clearing the first is a bug.
*
* @param entry The assetentry_t the event fired on.
* @param user The user pointer passed alongside the callback.
*/
typedef void (*assetentrycallback_t)(assetentry_t *entry, void *user);
struct assetentry_s { struct assetentry_s {
char_t name[ASSET_FILE_NAME_MAX]; char_t name[ASSET_FILE_NAME_MAX];
assetloadertype_t type; assetloadertype_t type;
@@ -33,30 +38,27 @@ struct assetentry_s {
ref_t refs; ref_t refs;
assetloaderinput_t *input; assetloaderinput_t *input;
assetloaderinput_t inputData; assetloaderinput_t inputData;
/**
* Fired once when loading completes successfully (params = assetentry_t *).
* Always invoked on the main thread.
*/
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onLoadedUsers[ASSET_ENTRY_EVENT_MAX];
/** /**
* Fired once when the entry is disposed/reaped (params = assetentry_t *). * Fired once when loading completes successfully.
* The asset data is still accessible when the callback runs.
* Always invoked on the main thread. * Always invoked on the main thread.
*/ */
event_t onUnloaded; assetentrycallback_t onLoaded;
eventcallback_t onUnloadedCallbacks[ASSET_ENTRY_EVENT_MAX]; void *onLoadedUser;
void *onUnloadedUsers[ASSET_ENTRY_EVENT_MAX];
/** /**
* Fired once when loading fails (params = assetentry_t *). * Fired once when the entry is disposed/reaped. The asset data is still
* accessible when the callback runs. Always invoked on the main thread.
*/
assetentrycallback_t onUnloaded;
void *onUnloadedUser;
/**
* Fired once when loading fails.
* Always invoked on the main thread. * Always invoked on the main thread.
*/ */
event_t onError; assetentrycallback_t onError;
eventcallback_t onErrorCallbacks[ASSET_ENTRY_EVENT_MAX]; void *onErrorUser;
void *onErrorUsers[ASSET_ENTRY_EVENT_MAX];
}; };
/** /**
-9
View File
@@ -1,9 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
event.c
)
-76
View File
@@ -1,76 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "event.h"
#include "assert/assert.h"
#include "util/memory.h"
void eventInit(
event_t *event,
eventcallback_t *callbacks,
void **users,
size_t size
) {
assertNotNull(event, "event must not be NULL");
assertNotNull((void *)callbacks, "callbacks must not be NULL");
assertTrue(size > 0, "size must be greater than 0");
event->callbacks = callbacks;
event->users = users;
event->size = size;
event->count = 0;
memoryZero(callbacks, sizeof(eventcallback_t) * size);
if(users) memoryZero(users, sizeof(void *) * size);
}
void eventSubscribe(event_t *event, eventcallback_t callback, void *user) {
assertNotNull(event, "event must not be NULL");
assertNotNull(callback, "callback must not be NULL");
// Ensure callback isn't already susbcribed
for(uint32_t i = 0; i < event->count; i++) {
if(event->callbacks[i] != callback) continue;
assertUnreachable("Callback already registered, cannot subscribe twice.");
}
assertTrue(event->count < event->size, "event subscriber capacity exceeded");
event->callbacks[event->count] = callback;
if(user) {
assertNotNull(event->users, "Cannot add user pointer.");
event->users[event->count] = user;
}
event->count++;
}
void eventUnsubscribe(event_t *event, eventcallback_t callback) {
assertNotNull(event, "event must not be NULL");
assertNotNull(callback, "callback must not be NULL");
for(uint32_t i = 0; i < event->count; i++) {
if(event->callbacks[i] != callback) continue;
uint32_t last = event->count - 1;
if(i != last) {
event->callbacks[i] = event->callbacks[last];
if(event->users) event->users[i] = event->users[last];
}
event->callbacks[last] = NULL;
if(event->users) event->users[last] = NULL;
event->count--;
return;
}
}
void eventInvoke(const event_t *event, void *params) {
assertNotNull(event, "event must not be NULL");
for(uint32_t i = 0; i < event->count; i++) {
void *u = event->users ? event->users[i] : NULL;
event->callbacks[i](params, u);
}
}
-64
View File
@@ -1,64 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef void (*eventcallback_t)(void *params, void *user);
typedef struct {
eventcallback_t *callbacks;
void **users;
size_t size;
uint32_t count;
} event_t;
/**
* Initializes an event, binding it to the provided backing arrays and clearing
* all subscribers. May also be called to reset an event (re-clears subscribers
* without changing the backing arrays or size).
*
* @param event The event to initialize.
* @param callbacks Caller-owned array of at least `size` callback slots.
* @param users Array of user pointers, matching each callback, or NULL.
* @param size Capacity of both arrays, must match.
*/
void eventInit(
event_t *event,
eventcallback_t *callbacks,
void **users,
size_t size
);
/**
* Subscribes a callback to an event. The callback is invoked with params and
* the provided user pointer each time the event fires. The same (callback,
* user) pair may only be subscribed once.
*
* @param event The event to subscribe to.
* @param callback The function to call when the event fires.
* @param user Arbitrary pointer forwarded to the callback unchanged.
*/
void eventSubscribe(event_t *event, eventcallback_t callback, void *user);
/**
* Removes a previously subscribed (callback, user) pair. Does nothing if the
* pair is not currently subscribed.
*
* @param event The event to unsubscribe from.
* @param callback The callback that was passed to eventSubscribe.
*/
void eventUnsubscribe(event_t *event, eventcallback_t callback);
/**
* Invokes all subscribed callbacks, passing params and each subscriber's user
* pointer.
*
* @param event The event to invoke.
* @param params Arbitrary pointer forwarded to every callback unchanged.
*/
void eventInvoke(const event_t *event, void *params);
-1
View File
@@ -11,7 +11,6 @@
#include "util/string.h" #include "util/string.h"
#include "util/math.h" #include "util/math.h"
#include "time/time.h" #include "time/time.h"
#include "event/event.h"
input_t INPUT; input_t INPUT;
+26 -17
View File
@@ -11,7 +11,6 @@
#include "asset/asset.h" #include "asset/asset.h"
#include "asset/loader/assetloader.h" #include "asset/loader/assetloader.h"
#include "console/console.h" #include "console/console.h"
#include "event/event.h"
#include "util/string.h" #include "util/string.h"
#include "rpg/entity/global/entityglobal.h" #include "rpg/entity/global/entityglobal.h"
#include "rpg/entity/item/entityitem.h" #include "rpg/entity/item/entityitem.h"
@@ -137,8 +136,10 @@ void mapChunkUnload(chunk_t *chunk) {
memorySet(chunk->areas, 0xFF, sizeof(chunk->areas)); memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
if(chunk->dcfEntry != NULL) { if(chunk->dcfEntry != NULL) {
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded); chunk->dcfEntry->onLoaded = NULL;
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError); chunk->dcfEntry->onLoadedUser = NULL;
chunk->dcfEntry->onError = NULL;
chunk->dcfEntry->onErrorUser = NULL;
assetUnlockEntry(chunk->dcfEntry); assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
} }
@@ -159,8 +160,10 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
mapChunkLoadingSlotClear(chunk); mapChunkLoadingSlotClear(chunk);
if(chunk->dcfEntry != NULL) { if(chunk->dcfEntry != NULL) {
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded); chunk->dcfEntry->onLoaded = NULL;
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError); chunk->dcfEntry->onLoadedUser = NULL;
chunk->dcfEntry->onError = NULL;
chunk->dcfEntry->onErrorUser = NULL;
assetUnlockEntry(chunk->dcfEntry); assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
} }
@@ -244,8 +247,12 @@ void mapChunkLoadNext() {
continue; continue;
} }
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk); assertNull(entry->onLoaded, "Entry already has an onLoaded subscriber.");
eventSubscribe(&entry->onError, mapChunkLoadError, chunk); assertNull(entry->onError, "Entry already has an onError subscriber.");
entry->onLoaded = mapChunkLoaded;
entry->onLoadedUser = chunk;
entry->onError = mapChunkLoadError;
entry->onErrorUser = chunk;
} }
} }
@@ -385,10 +392,9 @@ void mapRebuildChunkOrder() {
} }
} }
void mapChunkLoadError(void *params, void *user) { void mapChunkLoadError(assetentry_t *entry, void *user) {
assertNotNull(params, "mapChunkLoadError: params cannot be NULL"); assertNotNull(entry, "mapChunkLoadError: entry cannot be NULL");
assertNotNull(user, "mapChunkLoadError: user cannot be NULL"); assertNotNull(user, "mapChunkLoadError: user cannot be NULL");
assetentry_t *entry = (assetentry_t *)params;
chunk_t *chunk = (chunk_t *)user; chunk_t *chunk = (chunk_t *)user;
if(chunk->dcfEntry != entry) return; if(chunk->dcfEntry != entry) return;
consolePrint( consolePrint(
@@ -397,8 +403,10 @@ void mapChunkLoadError(void *params, void *user) {
(int32_t)chunk->position.y, (int32_t)chunk->position.y,
(int32_t)chunk->position.z (int32_t)chunk->position.z
); );
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded); entry->onLoaded = NULL;
eventUnsubscribe(&entry->onError, mapChunkLoadError); entry->onLoadedUser = NULL;
entry->onError = NULL;
entry->onErrorUser = NULL;
assetUnlockEntry(chunk->dcfEntry); assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles)); memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
@@ -407,10 +415,9 @@ void mapChunkLoadError(void *params, void *user) {
mapChunkLoadNext(); mapChunkLoadNext();
} }
void mapChunkLoaded(void *params, void *user) { void mapChunkLoaded(assetentry_t *entry, void *user) {
assertNotNull(params, "mapChunkLoaded: params cannot be NULL"); assertNotNull(entry, "mapChunkLoaded: entry cannot be NULL");
assertNotNull(user, "mapChunkLoaded: user cannot be NULL"); assertNotNull(user, "mapChunkLoaded: user cannot be NULL");
assetentry_t *entry = (assetentry_t *)params;
chunk_t *chunk = (chunk_t *)user; chunk_t *chunk = (chunk_t *)user;
if(chunk->dcfEntry != entry) return; if(chunk->dcfEntry != entry) return;
// consolePrint( // consolePrint(
@@ -457,8 +464,10 @@ void mapChunkLoaded(void *params, void *user) {
// modelEntries must still be intact for that next reuse to copy from. // modelEntries must still be intact for that next reuse to copy from.
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m]; chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
} }
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded); entry->onLoaded = NULL;
eventUnsubscribe(&entry->onError, mapChunkLoadError); entry->onLoadedUser = NULL;
entry->onError = NULL;
entry->onErrorUser = NULL;
// Deliberately keep chunk->dcfEntry locked and set - it is what keeps the // Deliberately keep chunk->dcfEntry locked and set - it is what keeps the
// chunk asset entry (and therefore its model locks) alive for as long as // chunk asset entry (and therefore its model locks) alive for as long as
// this chunk_t is displaying it. Released in mapChunkUnload instead. // this chunk_t is displaying it. Released in mapChunkUnload instead.
+4 -4
View File
@@ -101,19 +101,19 @@ void mapChunkLoadQueueRemove(chunk_t *chunk);
* chunk tiles with TILE_SHAPE_GROUND as a fallback. * chunk tiles with TILE_SHAPE_GROUND as a fallback.
* Always invoked on the main thread. * Always invoked on the main thread.
* *
* @param params The failed assetentry_t. * @param entry The failed assetentry_t.
* @param user The chunk_t that owns the entry. * @param user The chunk_t that owns the entry.
*/ */
void mapChunkLoadError(void *params, void *user); void mapChunkLoadError(assetentry_t *entry, void *user);
/** /**
* Callback invoked when a chunk DCF asset finishes loading. * Callback invoked when a chunk DCF asset finishes loading.
* Always invoked on the main thread. * Always invoked on the main thread.
* *
* @param params The loaded assetentry_t. * @param entry The loaded assetentry_t.
* @param user The chunk_t that owns the entry. * @param user The chunk_t that owns the entry.
*/ */
void mapChunkLoaded(void *params, void *user); void mapChunkLoaded(assetentry_t *entry, void *user);
/** /**
* Rebuilds chunkOrder from the loaded chunks that fall within the * Rebuilds chunkOrder from the loaded chunks that fall within the
+3 -7
View File
@@ -20,12 +20,6 @@ uifullbox_t UI_FULLBOX_OVER;
void uiFullboxInit(uifullbox_t *fullbox) { void uiFullboxInit(uifullbox_t *fullbox) {
assertNotNull(fullbox, "fullbox must not be NULL"); assertNotNull(fullbox, "fullbox must not be NULL");
memoryZero(fullbox, sizeof(uifullbox_t)); memoryZero(fullbox, sizeof(uifullbox_t));
eventInit(
&fullbox->onTransitionEnd,
fullbox->onTransitionEndCallbacks,
fullbox->onTransitionEndUsers,
4
);
} }
void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) { void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) {
@@ -35,7 +29,9 @@ void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) {
fullbox->time += delta; fullbox->time += delta;
if(fullbox->time >= fullbox->duration) { if(fullbox->time >= fullbox->duration) {
fullbox->time = fullbox->duration; fullbox->time = fullbox->duration;
eventInvoke(&fullbox->onTransitionEnd, fullbox); if(fullbox->onTransitionEnd) {
fullbox->onTransitionEnd(fullbox, fullbox->onTransitionEndUser);
}
} }
} }
+14 -6
View File
@@ -9,18 +9,26 @@
#include "error/error.h" #include "error/error.h"
#include "display/color.h" #include "display/color.h"
#include "animation/easing.h" #include "animation/easing.h"
#include "event/event.h"
typedef struct { typedef struct uifullbox_s uifullbox_t;
/**
* Callback fired once when a fullbox transition completes.
*
* @param fullbox The uifullbox_t the transition ran on.
* @param user The user pointer passed alongside the callback.
*/
typedef void (*uifullboxcallback_t)(uifullbox_t *fullbox, void *user);
struct uifullbox_s {
color_t fromColor; color_t fromColor;
color_t toColor; color_t toColor;
float_t duration; float_t duration;
float_t time; float_t time;
easingtype_t easing; easingtype_t easing;
eventcallback_t onTransitionEndCallbacks[4]; uifullboxcallback_t onTransitionEnd;
void *onTransitionEndUsers[4]; void *onTransitionEndUser;
event_t onTransitionEnd; };
} uifullbox_t;
extern uifullbox_t UI_FULLBOX_UNDER; extern uifullbox_t UI_FULLBOX_UNDER;
extern uifullbox_t UI_FULLBOX_OVER; extern uifullbox_t UI_FULLBOX_OVER;
+9 -18
View File
@@ -20,12 +20,6 @@ uiloading_t UI_LOADING;
errorret_t uiLoadingInit(void) { errorret_t uiLoadingInit(void) {
memoryZero(&UI_LOADING, sizeof(uiloading_t)); memoryZero(&UI_LOADING, sizeof(uiloading_t));
eventInit(
&UI_LOADING.onTransitionEnd,
UI_LOADING.onTransitionEndCallbacks,
UI_LOADING.onTransitionEndUsers,
4
);
errorChain(assetLocaleGetString( errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale, &LOCALE.entry->data.locale,
@@ -43,7 +37,9 @@ errorret_t uiLoadingUpdate(void) {
UI_LOADING.time += TIME.delta; UI_LOADING.time += TIME.delta;
if(UI_LOADING.time >= UI_LOADING.duration) { if(UI_LOADING.time >= UI_LOADING.duration) {
UI_LOADING.time = UI_LOADING.duration; UI_LOADING.time = UI_LOADING.duration;
eventInvoke(&UI_LOADING.onTransitionEnd, &UI_LOADING); if(UI_LOADING.onTransitionEnd) {
UI_LOADING.onTransitionEnd(&UI_LOADING, UI_LOADING.onTransitionEndUser);
}
} }
} }
errorOk(); errorOk();
@@ -76,26 +72,21 @@ errorret_t uiLoadingDraw(void) {
return spriteBatchFlush(); return spriteBatchFlush();
} }
static void uiLoadingTransition( void uiLoadingTransition(
const float_t from, const float_t from,
const float_t to, const float_t to,
const eventcallback_t callback, const uiloadingcallback_t callback,
void *user void *user
) { ) {
UI_LOADING.fromAlpha = from; UI_LOADING.fromAlpha = from;
UI_LOADING.toAlpha = to; UI_LOADING.toAlpha = to;
UI_LOADING.duration = UI_LOADING_FADE_DURATION; UI_LOADING.duration = UI_LOADING_FADE_DURATION;
UI_LOADING.time = 0.0f; UI_LOADING.time = 0.0f;
eventInit( UI_LOADING.onTransitionEnd = callback;
&UI_LOADING.onTransitionEnd, UI_LOADING.onTransitionEndUser = user;
UI_LOADING.onTransitionEndCallbacks,
UI_LOADING.onTransitionEndUsers,
4
);
if(callback) eventSubscribe(&UI_LOADING.onTransitionEnd, callback, user);
} }
void uiLoadingShow(eventcallback_t callback, void *user) { void uiLoadingShow(uiloadingcallback_t callback, void *user) {
uiLoadingTransition(0.0f, 1.0f, callback, user); uiLoadingTransition(0.0f, 1.0f, callback, user);
uiFullboxTransition( uiFullboxTransition(
&UI_FULLBOX_OVER, &UI_FULLBOX_OVER,
@@ -106,7 +97,7 @@ void uiLoadingShow(eventcallback_t callback, void *user) {
); );
} }
void uiLoadingHide(eventcallback_t callback, void *user) { void uiLoadingHide(uiloadingcallback_t callback, void *user) {
uiLoadingTransition(1.0f, 0.0f, callback, user); uiLoadingTransition(1.0f, 0.0f, callback, user);
uiFullboxTransition( uiFullboxTransition(
&UI_FULLBOX_OVER, &UI_FULLBOX_OVER,
+32 -8
View File
@@ -7,22 +7,30 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#include "event/event.h"
#define UI_LOADING_FADE_DURATION 0.5f #define UI_LOADING_FADE_DURATION 0.5f
#define UI_LOADING_MARGIN 8.0f #define UI_LOADING_MARGIN 8.0f
#define UI_LOADING_TEXT_MAX 32 #define UI_LOADING_TEXT_MAX 32
typedef struct { typedef struct uiloading_s uiloading_t;
/**
* Callback fired once when a loading indicator fade transition completes.
*
* @param loading The uiloading_t the transition ran on.
* @param user The user pointer passed alongside the callback.
*/
typedef void (*uiloadingcallback_t)(uiloading_t *loading, void *user);
struct uiloading_s {
float_t fromAlpha; float_t fromAlpha;
float_t toAlpha; float_t toAlpha;
float_t duration; float_t duration;
float_t time; float_t time;
eventcallback_t onTransitionEndCallbacks[4]; uiloadingcallback_t onTransitionEnd;
void *onTransitionEndUsers[4]; void *onTransitionEndUser;
event_t onTransitionEnd;
char_t text[UI_LOADING_TEXT_MAX]; char_t text[UI_LOADING_TEXT_MAX];
} uiloading_t; };
extern uiloading_t UI_LOADING; extern uiloading_t UI_LOADING;
@@ -48,13 +56,29 @@ errorret_t uiLoadingUpdate(void);
*/ */
errorret_t uiLoadingDraw(void); errorret_t uiLoadingDraw(void);
/**
* Begins a loading indicator fade transition, replacing any transition
* already in progress along with its pending callback.
*
* @param from Starting alpha.
* @param to Ending alpha.
* @param callback Called when the transition completes. May be NULL.
* @param user Forwarded to the callback unchanged.
*/
void uiLoadingTransition(
const float_t from,
const float_t to,
const uiloadingcallback_t callback,
void *user
);
/** /**
* Fades the loading indicator in. Invokes callback when fully visible. * Fades the loading indicator in. Invokes callback when fully visible.
* *
* @param callback Called when the fade-in completes. May be NULL. * @param callback Called when the fade-in completes. May be NULL.
* @param user Forwarded to the callback unchanged. * @param user Forwarded to the callback unchanged.
*/ */
void uiLoadingShow(eventcallback_t callback, void *user); void uiLoadingShow(uiloadingcallback_t callback, void *user);
/** /**
* Fades the loading indicator out. Invokes callback when fully hidden. * Fades the loading indicator out. Invokes callback when fully hidden.
@@ -62,4 +86,4 @@ void uiLoadingShow(eventcallback_t callback, void *user);
* @param callback Called when the fade-out completes. May be NULL. * @param callback Called when the fade-out completes. May be NULL.
* @param user Forwarded to the callback unchanged. * @param user Forwarded to the callback unchanged.
*/ */
void uiLoadingHide(eventcallback_t callback, void *user); void uiLoadingHide(uiloadingcallback_t callback, void *user);
-1
View File
@@ -8,7 +8,6 @@ add_subdirectory(assert)
add_subdirectory(asset) add_subdirectory(asset)
add_subdirectory(console) add_subdirectory(console)
add_subdirectory(error) add_subdirectory(error)
add_subdirectory(event)
add_subdirectory(thread) add_subdirectory(thread)
add_subdirectory(display) add_subdirectory(display)
add_subdirectory(rpg) add_subdirectory(rpg)
-9
View File
@@ -1,9 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_event.c)
-443
View File
@@ -1,443 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "event/event.h"
typedef struct {
int32_t callCount;
void *lastParams;
void *lastUser;
} eventrecord_t;
// Distinct function pointers are required per-subscriber - the same callback
// pointer cannot be subscribed twice, even with a different user pointer.
static void helper_recordA(void *params, void *user) {
eventrecord_t *record = (eventrecord_t *)user;
record->callCount++;
record->lastParams = params;
record->lastUser = user;
}
static void helper_recordB(void *params, void *user) {
eventrecord_t *record = (eventrecord_t *)user;
record->callCount++;
record->lastParams = params;
record->lastUser = user;
}
static void helper_recordC(void *params, void *user) {
eventrecord_t *record = (eventrecord_t *)user;
record->callCount++;
record->lastParams = params;
record->lastUser = user;
}
// Records via `params` instead of `user` - for cases exercising a NULL user
// (e.g. no users array), where dereferencing `user` would crash.
static void helper_recordViaParams(void *params, void *user) {
eventrecord_t *record = (eventrecord_t *)params;
record->callCount++;
record->lastParams = params;
record->lastUser = user;
}
#define EVENT_CAPACITY 4
// --- eventInit ---
static void test_eventInitSetsUpBackingArrays(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
memset(callbacks, 0xAA, sizeof(callbacks));
memset(users, 0xAA, sizeof(users));
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
assert_ptr_equal(event.callbacks, callbacks);
assert_ptr_equal(event.users, users);
assert_int_equal(event.size, EVENT_CAPACITY);
assert_int_equal(event.count, 0);
for(int32_t i = 0; i < EVENT_CAPACITY; i++) {
assert_null(callbacks[i]);
assert_null(users[i]);
}
}
static void test_eventInitAllowsNullUsersArray(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
assert_null(event.users);
}
static void test_eventInitResetClearsSubscribersOnly(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t record = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &record);
assert_int_equal(event.count, 1);
// Re-init (reset) with the same backing arrays.
eventInit(&event, callbacks, users, EVENT_CAPACITY);
assert_ptr_equal(event.callbacks, callbacks);
assert_int_equal(event.count, 0);
assert_null(callbacks[0]);
assert_null(users[0]);
}
static void test_eventInitNullEventAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
expect_assert_failure(eventInit(NULL, callbacks, NULL, EVENT_CAPACITY));
}
static void test_eventInitNullCallbacksAsserts(void **state) {
event_t event;
expect_assert_failure(eventInit(&event, NULL, NULL, EVENT_CAPACITY));
}
static void test_eventInitZeroSizeAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
event_t event;
expect_assert_failure(eventInit(&event, callbacks, NULL, 0));
}
// --- eventSubscribe ---
static void test_eventSubscribeAddsCallback(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t record = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &record);
assert_int_equal(event.count, 1);
assert_ptr_equal(event.callbacks[0], helper_recordA);
assert_ptr_equal(event.users[0], &record);
}
static void test_eventSubscribeMultiple(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 }, recordC = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventSubscribe(&event, helper_recordC, &recordC);
assert_int_equal(event.count, 3);
}
static void test_eventSubscribeNullUserLeavesSlotNull(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, NULL);
assert_null(event.users[0]);
}
static void test_eventSubscribeNullUserAfterUnsubscribeStaysNull(void **state) {
// The vacated slot must actually be cleared, not left holding a stale
// user pointer from whatever previously occupied it.
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventUnsubscribe(&event, helper_recordA);
eventSubscribe(&event, helper_recordC, NULL);
for(int32_t i = 0; i < event.count; i++) {
if(event.callbacks[i] == helper_recordC) {
assert_null(event.users[i]);
return;
}
}
fail_msg("helper_recordC was not found after subscribing.");
}
static void test_eventSubscribeUserWithoutUsersArrayAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
eventrecord_t record = { 0 };
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
expect_assert_failure(eventSubscribe(&event, helper_recordA, &record));
}
static void test_eventSubscribeDuplicateCallbackAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t record = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &record);
expect_assert_failure(eventSubscribe(&event, helper_recordA, &record));
}
static void test_eventSubscribeSameCallbackDifferentUserStillAsserts(void **state) {
// Documented as "the same (callback, user) pair may only be subscribed
// once", implying a different user should be fine - but the actual
// uniqueness check only looks at the callback pointer, so this asserts
// too. Captures actual behavior; flag if the doc/implementation should
// instead match on the (callback, user) pair.
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
expect_assert_failure(eventSubscribe(&event, helper_recordA, &recordB));
}
static void test_eventSubscribeCapacityExceededAsserts(void **state) {
eventcallback_t callbacks[2];
void *users[2];
eventrecord_t recordA = { 0 }, recordB = { 0 }, recordC = { 0 };
event_t event;
eventInit(&event, callbacks, users, 2);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
expect_assert_failure(eventSubscribe(&event, helper_recordC, &recordC));
}
static void test_eventSubscribeNullEventAsserts(void **state) {
expect_assert_failure(eventSubscribe(NULL, helper_recordA, NULL));
}
static void test_eventSubscribeNullCallbackAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
expect_assert_failure(eventSubscribe(&event, NULL, NULL));
}
// --- eventUnsubscribe ---
static void test_eventUnsubscribeMiddleSwapsLastIntoPlace(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 }, recordC = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventSubscribe(&event, helper_recordC, &recordC);
eventUnsubscribe(&event, helper_recordB);
assert_int_equal(event.count, 2);
assert_ptr_equal(event.callbacks[0], helper_recordA);
// recordC (previously last) was swapped into the vacated middle slot.
assert_ptr_equal(event.callbacks[1], helper_recordC);
assert_ptr_equal(event.users[1], &recordC);
// The old tail slot is fully cleared.
assert_null(event.callbacks[2]);
assert_null(event.users[2]);
}
static void test_eventUnsubscribeLastElement(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventUnsubscribe(&event, helper_recordB);
assert_int_equal(event.count, 1);
assert_ptr_equal(event.callbacks[0], helper_recordA);
assert_null(event.callbacks[1]);
}
static void test_eventUnsubscribeNotSubscribedIsNoop(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventUnsubscribe(&event, helper_recordB);
assert_int_equal(event.count, 1);
assert_ptr_equal(event.callbacks[0], helper_recordA);
}
static void test_eventUnsubscribeThenResubscribeSucceeds(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventUnsubscribe(&event, helper_recordA);
// Would have asserted (duplicate) had the removal not actually happened.
eventSubscribe(&event, helper_recordA, &recordA);
assert_int_equal(event.count, 1);
}
static void test_eventUnsubscribeWithNullUsersArrayIsSafe(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, NULL);
eventUnsubscribe(&event, helper_recordA);
assert_int_equal(event.count, 0);
}
static void test_eventUnsubscribeNullEventAsserts(void **state) {
expect_assert_failure(eventUnsubscribe(NULL, helper_recordA));
}
static void test_eventUnsubscribeNullCallbackAsserts(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
expect_assert_failure(eventUnsubscribe(&event, NULL));
}
// --- eventInvoke ---
static void test_eventInvokeCallsAllSubscribersWithParamsAndUser(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 };
int32_t params = 123;
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventInvoke(&event, &params);
assert_int_equal(recordA.callCount, 1);
assert_ptr_equal(recordA.lastParams, &params);
assert_ptr_equal(recordA.lastUser, &recordA);
assert_int_equal(recordB.callCount, 1);
assert_ptr_equal(recordB.lastParams, &params);
assert_ptr_equal(recordB.lastUser, &recordB);
}
static void test_eventInvokeWithNoSubscribersDoesNothing(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
// No subscribers exist, so there is nothing to observe beyond "no crash".
eventInvoke(&event, NULL);
}
static void test_eventInvokeWithNullUsersArrayPassesNull(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
eventrecord_t record = { 0 };
event_t event;
eventInit(&event, callbacks, NULL, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordViaParams, NULL);
eventInvoke(&event, &record);
assert_int_equal(record.callCount, 1);
assert_ptr_equal(record.lastParams, &record);
assert_null(record.lastUser);
}
static void test_eventInvokeSkipsUnsubscribedCallback(void **state) {
eventcallback_t callbacks[EVENT_CAPACITY];
void *users[EVENT_CAPACITY];
eventrecord_t recordA = { 0 }, recordB = { 0 };
event_t event;
eventInit(&event, callbacks, users, EVENT_CAPACITY);
eventSubscribe(&event, helper_recordA, &recordA);
eventSubscribe(&event, helper_recordB, &recordB);
eventUnsubscribe(&event, helper_recordA);
eventInvoke(&event, NULL);
assert_int_equal(recordA.callCount, 0);
assert_int_equal(recordB.callCount, 1);
}
static void test_eventInvokeNullEventAsserts(void **state) {
expect_assert_failure(eventInvoke(NULL, NULL));
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_eventInitSetsUpBackingArrays),
cmocka_unit_test(test_eventInitAllowsNullUsersArray),
cmocka_unit_test(test_eventInitResetClearsSubscribersOnly),
cmocka_unit_test(test_eventInitNullEventAsserts),
cmocka_unit_test(test_eventInitNullCallbacksAsserts),
cmocka_unit_test(test_eventInitZeroSizeAsserts),
cmocka_unit_test(test_eventSubscribeAddsCallback),
cmocka_unit_test(test_eventSubscribeMultiple),
cmocka_unit_test(test_eventSubscribeNullUserLeavesSlotNull),
cmocka_unit_test(test_eventSubscribeNullUserAfterUnsubscribeStaysNull),
cmocka_unit_test(test_eventSubscribeUserWithoutUsersArrayAsserts),
cmocka_unit_test(test_eventSubscribeDuplicateCallbackAsserts),
cmocka_unit_test(test_eventSubscribeSameCallbackDifferentUserStillAsserts),
cmocka_unit_test(test_eventSubscribeCapacityExceededAsserts),
cmocka_unit_test(test_eventSubscribeNullEventAsserts),
cmocka_unit_test(test_eventSubscribeNullCallbackAsserts),
cmocka_unit_test(test_eventUnsubscribeMiddleSwapsLastIntoPlace),
cmocka_unit_test(test_eventUnsubscribeLastElement),
cmocka_unit_test(test_eventUnsubscribeNotSubscribedIsNoop),
cmocka_unit_test(test_eventUnsubscribeThenResubscribeSucceeds),
cmocka_unit_test(test_eventUnsubscribeWithNullUsersArrayIsSafe),
cmocka_unit_test(test_eventUnsubscribeNullEventAsserts),
cmocka_unit_test(test_eventUnsubscribeNullCallbackAsserts),
cmocka_unit_test(test_eventInvokeCallsAllSubscribersWithParamsAndUser),
cmocka_unit_test(test_eventInvokeWithNoSubscribersDoesNothing),
cmocka_unit_test(test_eventInvokeWithNullUsersArrayPassesNull),
cmocka_unit_test(test_eventInvokeSkipsUnsubscribedCallback),
cmocka_unit_test(test_eventInvokeNullEventAsserts),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}