An assistant-written stretch of code had drifted from the conventions the older hand-written files establish. Sweeps the whole of src/ back into line. Systematic: - @returns -> @return (163 occurrences, 84 files). Concentrated in ui/ and rpg/cutscene/; the rest of the tree already used @return 646 times. - Lowercase "null" -> "NULL" in assert/error message strings (170 occurrences, 28 files), matching the dominant 441-use spelling. Covers "cannot be null", "must not be null" and adjectival uses. Localized: - sort.c: drop a stray #include <stdlib.h> that sat *above* the copyright header, leaving it the only file in the repo without a leading header block. Also drops the same redundant include from random.c and npcturn.c (dusk.h already pulls in stdlib.h). - Convert 7 files' license headers from // lines to the /** */ block form. - cutscenesystem.c: memset -> memoryZero, matching the identical call ~20 lines further down and the rest of the codebase. - Struct tags suffixed _t -> _s: threadlock_t -> threadmutex_s (which also makes the tag match its typedef) and chunkpos_t -> chunkpos_s. - Convert 20 inline /* */ block comments to // across the mesh builders, assetfile.c and assetlocaleloader.c. - Pointer truthiness if(!ptr) -> if(ptr == NULL) in 16 places, matching the 267 existing explicit comparisons. Boolean !x checks are left alone. - entityanim.h: drop a JSDoc-style type annotation from an @return. - Document easing.h's 16 undeclared easing functions and assetjsonloader.h's 3 loader callbacks. No behavioral change. Builds clean with no new warnings; ctest shows the same 6 pre-existing failures as HEAD, verified against a pristine worktree. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
465 lines
13 KiB
C
465 lines
13 KiB
C
/**
|
|
* Copyright (c) 2026 Dominic Masters
|
|
*
|
|
* This software is released under the MIT License.
|
|
* https://opensource.org/licenses/MIT
|
|
*/
|
|
|
|
#include "asset.h"
|
|
#include "util/memory.h"
|
|
#include "util/string.h"
|
|
#include "assert/assert.h"
|
|
#include "engine/engine.h"
|
|
#include "util/string.h"
|
|
#include "console/console.h"
|
|
#include <unistd.h>
|
|
|
|
asset_t ASSET;
|
|
|
|
errorret_t assetInit(void) {
|
|
memoryZero(&ASSET, sizeof(asset_t));
|
|
|
|
threadMutexInit(&ASSET.zipLock);
|
|
|
|
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
|
|
threadMutexInit(&ASSET.loading[i].mutex);
|
|
}
|
|
|
|
// assetInitPlatform must either define both ASSET.zip/ASSET.zipStored or
|
|
// throw an error.
|
|
errorChain(assetInitPlatform());
|
|
assertNotNull(ASSET.zip, "Asset zip NULL without error.");
|
|
assertNotNull(ASSET.zipStored, "Asset stored zip NULL without error.");
|
|
threadInit(&ASSET.loadThread, assetUpdateAsync);
|
|
threadStart(&ASSET.loadThread);
|
|
|
|
errorOk();
|
|
}
|
|
|
|
bool_t assetFileExists(const char_t *filename) {
|
|
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
|
|
|
|
threadMutexLock(&ASSET.zipLock);
|
|
bool_t found =
|
|
zip_name_locate(ASSET.zip, filename, 0) >= 0 ||
|
|
zip_name_locate(ASSET.zipStored, filename, 0) >= 0;
|
|
threadMutexUnlock(&ASSET.zipLock);
|
|
return found;
|
|
}
|
|
|
|
assetentry_t * assetGetEntry(
|
|
const char_t *name,
|
|
const assetloadertype_t type,
|
|
assetloaderinput_t *input
|
|
) {
|
|
// Is there an existing asset?
|
|
assetentry_t *entry = ASSET.entries;
|
|
do {
|
|
if(entry->type == ASSET_LOADER_TYPE_NULL) {
|
|
entry++;
|
|
continue;
|
|
}
|
|
if(stringEquals(entry->name, name)) {
|
|
assertTrue(entry->type == type, "Asset entry type mismatch.");
|
|
return entry;
|
|
}
|
|
entry++;
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
|
|
// We did not find one existing. Find first available slot, reaping
|
|
// zero-ref entries to make room if none are immediately available.
|
|
bool_t reaped = false;
|
|
for(;;) {
|
|
entry = ASSET.entries;
|
|
do {
|
|
if(entry->type != ASSET_LOADER_TYPE_NULL) {
|
|
entry++;
|
|
continue;
|
|
}
|
|
|
|
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
|
|
assetEntryInit(entry, name, type, input);
|
|
return entry;
|
|
}
|
|
entry++;
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
|
|
if(reaped) break;
|
|
reaped = true;
|
|
errorCatch(assetReapUnused());
|
|
}
|
|
|
|
assertUnreachable("No available asset entry slots.");
|
|
return NULL;
|
|
}
|
|
|
|
uint32_t assetGetEntriesOfType(
|
|
assetentry_t **outEntries,
|
|
const assetloadertype_t type
|
|
) {
|
|
assertNotNull(outEntries, "Output entries cannot be NULL.");
|
|
|
|
uint32_t count = 0;
|
|
assetentry_t *entry = ASSET.entries;
|
|
do {
|
|
if(entry->type == type) {
|
|
outEntries[count++] = entry;
|
|
}
|
|
entry++;
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
|
|
return count;
|
|
}
|
|
|
|
errorret_t assetRequireLoaded(assetentry_t *entry) {
|
|
assertNotNull(entry, "Entry cannot be NULL.");
|
|
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
|
|
assertIsMainThread("Currently only works on main thread.");
|
|
|
|
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
|
|
errorOk();
|
|
}
|
|
|
|
// Lock to prevent the reaper from collecting the entry mid-spin.
|
|
assetEntryLock(entry);
|
|
|
|
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
|
|
// A failed load transitions to ERROR, not LOADED - without this check
|
|
// this loop spins forever on any load failure (assetUpdate() itself
|
|
// still returns OK, since a single asset failing isn't meant to halt
|
|
// the whole update loop - see its ASSET_ENTRY_STATE_ERROR handling).
|
|
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
|
|
assetEntryUnlock(entry);
|
|
errorThrow("Failed to load asset: %s", entry->name);
|
|
}
|
|
|
|
usleep(1000);
|
|
errorret_t ret = assetUpdate();
|
|
if(errorIsNotOk(ret)) {
|
|
assetEntryUnlock(entry);
|
|
errorChain(ret);
|
|
}
|
|
}
|
|
|
|
assetEntryUnlock(entry);
|
|
errorOk();
|
|
}
|
|
|
|
errorret_t assetRequireDisposed(assetentry_t *entry) {
|
|
assertNotNull(entry, "Entry cannot be NULL.");
|
|
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
|
|
assertIsMainThread("Currently only works on main thread.");
|
|
|
|
if(entry->type == ASSET_LOADER_TYPE_NULL) {
|
|
errorOk();
|
|
}
|
|
|
|
if(
|
|
entry->state == ASSET_ENTRY_STATE_NOT_STARTED ||
|
|
entry->state == ASSET_ENTRY_STATE_ERROR
|
|
) {
|
|
errorOk();
|
|
}
|
|
|
|
assertTrue(
|
|
entry->refs.count == 0,
|
|
"Cannot require disposal of an entry with active references."
|
|
);
|
|
|
|
// Lock to prevent the reaper from collecting the entry mid-spin.
|
|
assetEntryLock(entry);
|
|
|
|
while(entry->type != ASSET_LOADER_TYPE_NULL) {
|
|
usleep(1000);
|
|
errorret_t ret = assetUpdate();
|
|
if(errorIsNotOk(ret)) {
|
|
assetEntryUnlock(entry);
|
|
errorChain(ret);
|
|
}
|
|
}
|
|
|
|
assetEntryUnlock(entry);
|
|
errorOk();
|
|
}
|
|
|
|
assetentry_t * assetLock(
|
|
const char_t *name,
|
|
const assetloadertype_t type,
|
|
assetloaderinput_t *input
|
|
) {
|
|
assetentry_t *entry = assetGetEntry(name, type, input);
|
|
assetEntryLock(entry);
|
|
return entry;
|
|
}
|
|
|
|
void assetUnlock(const char_t *name) {
|
|
assertNotNull(name, "Name cannot be NULL.");
|
|
|
|
assetentry_t *entry = ASSET.entries;
|
|
do {
|
|
if(
|
|
entry->type != ASSET_LOADER_TYPE_NULL &&
|
|
stringEquals(entry->name, name)
|
|
) {
|
|
assetEntryUnlock(entry);
|
|
return;
|
|
}
|
|
entry++;
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
|
|
assertUnreachable("Asset entry not found for unlock.");
|
|
}
|
|
|
|
void assetUnlockEntry(assetentry_t *entry) {
|
|
assertNotNull(entry, "Entry cannot be NULL.");
|
|
assetEntryUnlock(entry);
|
|
}
|
|
|
|
errorret_t assetReapUnused(void) {
|
|
assertIsMainThread("assetReapUnused must be called from the main thread.");
|
|
|
|
// Repeatedly find and dispose zero-ref LOADED entries until none remain.
|
|
// This handles dependency chains where an entry (e.g. a model) holds refs
|
|
// on child entries (mesh, texture): dispose parents first so child ref
|
|
// counts drop to zero, then pick up the children on the next pass. Without
|
|
// this, a forward-only scan fails when a shared child entry appears before
|
|
// a parent that still holds a ref to it.
|
|
bool_t any;
|
|
do {
|
|
any = false;
|
|
assetentry_t *entry = ASSET.entries;
|
|
do {
|
|
if(entry->type == ASSET_LOADER_TYPE_NULL) { entry++; continue; }
|
|
if(entry->state != ASSET_ENTRY_STATE_LOADED) { entry++; continue; }
|
|
if(entry->refs.count > 0) { entry++; continue; }
|
|
errorChain(assetEntryDispose(entry));
|
|
any = true;
|
|
entry++;
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
} while(any);
|
|
|
|
errorOk();
|
|
}
|
|
|
|
errorret_t assetUpdate(void) {
|
|
assertIsMainThread("assetUpdate must be called from the main thread.");
|
|
|
|
// Determine how many available loading slots we have.
|
|
assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX];
|
|
uint8_t availableLoadingCount = 0;
|
|
assetloading_t *loading = ASSET.loading;
|
|
assetentry_t *entry;
|
|
|
|
|
|
do {
|
|
// We only care about NULL entry references. Nothing async touches this so
|
|
// it's fine to use raw here.
|
|
if(loading->entry != NULL) {
|
|
loading++;
|
|
continue;
|
|
}
|
|
availableLoading[availableLoadingCount++] = loading;
|
|
loading++;
|
|
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
|
|
|
|
|
|
// Now we can check for pending asset entries, we can't do anything if there
|
|
// is no available slots though.
|
|
if(availableLoadingCount > 0) {
|
|
entry = ASSET.entries;
|
|
do {
|
|
// Is this asset "ready to start loading" ?
|
|
if(entry->type == ASSET_LOADER_TYPE_NULL) {
|
|
entry++;
|
|
continue;
|
|
}
|
|
|
|
// We only care about assets not started.
|
|
if(entry->state != ASSET_ENTRY_STATE_NOT_STARTED) {
|
|
entry++;
|
|
continue;
|
|
}
|
|
|
|
// Pop a loading slot for this asset entry.
|
|
loading = availableLoading[--availableLoadingCount];
|
|
|
|
// Start loading this asset.
|
|
assetEntryStartLoading(entry, loading);
|
|
entry++;
|
|
|
|
// Did we run out of loading slots?
|
|
if(availableLoadingCount == 0) {
|
|
break;
|
|
}
|
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
}
|
|
|
|
// Now walk over all the loading slots and see what needs to be done.
|
|
loading = ASSET.loading;
|
|
do {
|
|
// Is the loading slot in use? Entry can only be modified synchronously.
|
|
if(loading->entry == NULL) {
|
|
loading++;
|
|
continue;
|
|
}
|
|
|
|
// Lock the loading slot. This will prevent any async modifications.
|
|
threadMutexLock(&loading->mutex);
|
|
|
|
// Check the state of the entry.
|
|
switch(loading->entry->state) {
|
|
// This thing is pending synchronous loading.
|
|
case ASSET_ENTRY_STATE_PENDING_SYNC:
|
|
loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC;
|
|
// Unlock before calling loadSync. The sync loader may re-enter
|
|
// assetUpdate (e.g. a script loading another asset), and the async
|
|
// thread never touches LOADING_SYNC entries, so this is safe.
|
|
threadMutexUnlock(&loading->mutex);
|
|
|
|
errorret_t ret = (
|
|
ASSET_LOADER_CALLBACKS[loading->type].loadSync(loading)
|
|
);
|
|
|
|
// After a sync load, these are the only valid states.
|
|
assertTrue(
|
|
loading->entry->state == ASSET_ENTRY_STATE_LOADED ||
|
|
loading->entry->state == ASSET_ENTRY_STATE_ERROR ||
|
|
loading->entry->state == ASSET_ENTRY_STATE_PENDING_SYNC ||
|
|
loading->entry->state == ASSET_ENTRY_STATE_PENDING_ASYNC,
|
|
"Loader did not set entry state to loaded or error on finished load."
|
|
);
|
|
|
|
if(errorIsNotOk(ret)) {
|
|
errorCatch(errorPrint(ret));
|
|
assertTrue(
|
|
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
|
|
"Loader did not set entry state to error on failed load."
|
|
);
|
|
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
|
|
assetentry_t *loadedEntry = loading->entry;
|
|
loading->entry = NULL;
|
|
if(loadedEntry->onLoaded) {
|
|
loadedEntry->onLoaded(loadedEntry, loadedEntry->onLoadedUser);
|
|
}
|
|
}
|
|
|
|
loading++;
|
|
break;
|
|
|
|
case ASSET_ENTRY_STATE_LOADING_SYNC:
|
|
// A re-entrant assetUpdate call (e.g. from a script loading another
|
|
// asset) will see this entry mid-sync-load. Skip it.
|
|
threadMutexUnlock(&loading->mutex);
|
|
loading++;
|
|
continue;
|
|
|
|
// Done loading, we can just free it up.
|
|
case ASSET_ENTRY_STATE_LOADED:
|
|
loading->entry = NULL;
|
|
threadMutexUnlock(&loading->mutex);
|
|
loading++;
|
|
break;
|
|
|
|
case ASSET_ENTRY_STATE_ERROR: {
|
|
assetentry_t *errEntry = loading->entry;
|
|
loading->entry = NULL;
|
|
threadMutexUnlock(&loading->mutex);
|
|
if(errEntry->onError) {
|
|
errEntry->onError(errEntry, errEntry->onErrorUser);
|
|
}
|
|
loading++;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
threadMutexUnlock(&loading->mutex);
|
|
loading++;
|
|
continue;
|
|
}
|
|
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
|
|
|
|
errorOk();
|
|
}
|
|
|
|
void assetUpdateAsync(thread_t *thread) {
|
|
assertNotMainThread("assetUpdateAsync must not run on the main thread.");
|
|
|
|
while(!threadShouldStop(thread)) {
|
|
// Walk over each asset
|
|
assetloading_t *loading;
|
|
loading = ASSET.loading;
|
|
|
|
do {
|
|
threadMutexLock(&loading->mutex);
|
|
|
|
if(loading->entry == NULL) {
|
|
threadMutexUnlock(&loading->mutex);
|
|
loading++;
|
|
continue;
|
|
}
|
|
|
|
switch(loading->entry->state) {
|
|
case ASSET_ENTRY_STATE_PENDING_ASYNC:
|
|
loading->entry->state = ASSET_ENTRY_STATE_LOADING_ASYNC;
|
|
assertNotNull(
|
|
ASSET_LOADER_CALLBACKS[loading->type].loadAsync,
|
|
"Loader does not support async loading."
|
|
);
|
|
errorret_t ret = (
|
|
ASSET_LOADER_CALLBACKS[loading->type].loadAsync(loading)
|
|
);
|
|
|
|
if(errorIsNotOk(ret)) {
|
|
errorCatch(errorPrint(ret));
|
|
assertTrue(
|
|
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
|
|
"Loader did not set entry state to error on failed load."
|
|
);
|
|
}
|
|
|
|
threadMutexUnlock(&loading->mutex);
|
|
loading++;
|
|
break;
|
|
|
|
case ASSET_ENTRY_STATE_LOADING_ASYNC:
|
|
assertUnreachable(
|
|
"Entry is in a pending async state still?"
|
|
);
|
|
break;
|
|
|
|
default:
|
|
threadMutexUnlock(&loading->mutex);
|
|
loading++;
|
|
continue;
|
|
}
|
|
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
|
|
|
|
if(threadShouldStop(thread)) break;
|
|
usleep(1000);
|
|
}
|
|
}
|
|
|
|
errorret_t assetDispose(void) {
|
|
assertIsMainThread("Must be called from the main thread.");
|
|
threadStop(&ASSET.loadThread);
|
|
|
|
errorChain(assetReapUnused());
|
|
|
|
// Cleanup zip files.
|
|
if(ASSET.zip != NULL) {
|
|
if(zip_close(ASSET.zip) != 0) {
|
|
errorThrow("Failed to close compressed asset zip archive.");
|
|
}
|
|
ASSET.zip = NULL;
|
|
}
|
|
if(ASSET.zipStored != NULL) {
|
|
if(zip_close(ASSET.zipStored) != 0) {
|
|
errorThrow("Failed to close stored asset zip archive.");
|
|
}
|
|
ASSET.zipStored = NULL;
|
|
}
|
|
|
|
errorChain(assetDisposePlatform());
|
|
threadMutexDispose(&ASSET.zipLock);
|
|
errorOk();
|
|
} |