/* SPDX-License-Identifier: MIT */ /* * konassets.c * * Copyright (c) 2026 KonAki * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include "konassets.h" #include #include #include #ifdef KON_MODULE_IMAGE #include "stb_image.h" #endif #ifdef KON_MODULE_AUDIO #include "miniaudio.h" #endif #define KON_ASSET_LOADERS_ 32 typedef struct kon_assetEntry_ { char *name; void *object; int type; int refs; size_t bytes; int loose; } kon_assetEntry_t; struct kon_assets { kon_pack_t *pack; char *looseDir; kon_assetLoader_t loaders[KON_ASSET_LOADERS_]; int loaderUsed[KON_ASSET_LOADERS_]; int nextCustom; kon_assetEntry_t *entries; int count, capacity; const char *error; }; /*** built in loaders ***/ static void *kon_loadBlob_(const uint8_t *data, size_t size, size_t *bytes) { kon_blob_t *blob = malloc(sizeof(kon_blob_t)); if (!blob) return NULL; blob->data = malloc(size + 1); if (!blob->data) { free(blob); return NULL; } memcpy(blob->data, data, size); blob->data[size] = 0; blob->size = size; *bytes = size; return blob; } static void kon_unloadBlob_(void *object) { kon_blob_t *blob = object; free(blob->data); free(blob); } #ifdef KON_MODULE_IMAGE static void *kon_loadImageAsset_(const uint8_t *data, size_t size, size_t *bytes) { int width, height, channels; unsigned char *pixels; kon_image *image; if (size > INT_MAX) return NULL; pixels = stbi_load_from_memory(data, (int)size, &width, &height, &channels, 4); if (!pixels) return NULL; image = kon_loadImage(pixels, width, height, konFormatRGBA8); stbi_image_free(pixels); if (image) *bytes = (size_t)width * (size_t)height * 4; return image; } static void kon_unloadImageAsset_(void *object) { kon_freeImage(object); } #endif #ifdef KON_MODULE_AUDIO #define KON_SOUND_MAX_BYTES_ ((uint64_t)1 << 30) static void *kon_loadSoundAsset_(const uint8_t *data, size_t size, size_t *bytes) { ma_decoder_config config = ma_decoder_config_init(ma_format_s16, 0, 0); ma_decoder decoder; kon_sound_t *sound; int16_t *frames = NULL; uint64_t total = 0, capacity = 0; uint32_t channels; if (ma_decoder_init_memory(data, size, &config, &decoder) != MA_SUCCESS) return NULL; channels = (uint32_t)decoder.outputChannels; if (channels == 0) { ma_decoder_uninit(&decoder); return NULL; } /* the length is not always known up front, so read until the decoder runs dry */ for (;;) { ma_uint64 got = 0; if (total == capacity) { uint64_t grown = capacity ? capacity * 2 : 16384; int16_t *more; if (grown * channels * sizeof(int16_t) > KON_SOUND_MAX_BYTES_) break; more = realloc(frames, (size_t)(grown * channels * sizeof(int16_t))); if (!more) { free(frames); ma_decoder_uninit(&decoder); return NULL; } frames = more; capacity = grown; } if (ma_decoder_read_pcm_frames(&decoder, frames + total * channels, capacity - total, &got) != MA_SUCCESS && got == 0) break; if (got == 0) break; total += got; } sound = malloc(sizeof(kon_sound_t)); if (!sound || total == 0) { free(sound); free(frames); ma_decoder_uninit(&decoder); return NULL; } sound->frames = frames; sound->frameCount = total; sound->channels = channels; sound->sampleRate = (uint32_t)decoder.outputSampleRate; *bytes = (size_t)(total * channels * sizeof(int16_t)); ma_decoder_uninit(&decoder); return sound; } static void kon_unloadSoundAsset_(void *object) { kon_sound_t *sound = object; free(sound->frames); free(sound); } #endif /*** private helper ***/ static int kon_extensionMatches_(const char *name, const char *extensions) { const char *dot = strrchr(name, '.'); const char *slash = strrchr(name, '/'); size_t length; if (!dot || (slash && dot < slash) || !extensions) return 0; length = strlen(dot); while (*extensions) { const char *end; size_t tokenLength; while (*extensions == ' ') extensions++; end = extensions; while (*end && *end != ' ') end++; tokenLength = (size_t)(end - extensions); if (tokenLength == length) { size_t i; for (i = 0; i < length; i++) { char c = dot[i]; if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); if (c != extensions[i]) break; } if (i == length) return 1; } extensions = end; } return 0; } static int kon_nameIsSafe_(const char *name) { const char *segment = name; if (!name || name[0] == '\0' || name[0] == '/') return 0; for (const char *c = name; ; c++) { if (*c == '\\' || *c == ':') return 0; if (*c == '/' || *c == '\0') { if (c - segment == 2 && segment[0] == '.' && segment[1] == '.') return 0; if (*c == '\0') break; segment = c + 1; } } return 1; } static int kon_typeForName_(const kon_assets_t *assets, const char *name) { for (int i = 1; i < KON_ASSET_LOADERS_; i++) { if (assets->loaderUsed[i] && kon_extensionMatches_(name, assets->loaders[i].extensions)) return i; } return KON_ASSET_BLOB; } static kon_assetEntry_t *kon_findEntry_(const kon_assets_t *assets, const char *name) { for (int i = 0; i < assets->count; i++) { if (strcmp(assets->entries[i].name, name) == 0) return &assets->entries[i]; } return NULL; } static uint8_t *kon_readFile_(kon_assets_t *assets, const char *name, size_t *size, int *loose) { *loose = 0; #ifdef KON_DEVELOPER if (assets->looseDir) { size_t length = strlen(assets->looseDir) + strlen(name) + 2; char *path = malloc(length); FILE *f; if (!path) { assets->error = "out of memory"; return NULL; } snprintf(path, length, "%s/%s", assets->looseDir, name); f = fopen(path, "rb"); free(path); if (f) { long end; uint8_t *data = NULL; if (fseek(f, 0, SEEK_END) == 0 && (end = ftell(f)) >= 0 && fseek(f, 0, SEEK_SET) == 0) { data = malloc((size_t)end + 1); if (data && fread(data, 1, (size_t)end, f) == (size_t)end) { data[end] = 0; *size = (size_t)end; *loose = 1; fclose(f); return data; } free(data); } fclose(f); assets->error = "could not read the loose file"; return NULL; } } #endif if (assets->pack) { int index = kon_packFind(assets->pack, name); if (index >= 0) { uint8_t *data = kon_packRead(assets->pack, index, size); if (!data) assets->error = kon_packError(); return data; } } assets->error = "file not found"; return NULL; } static void kon_unloadEntry_(kon_assets_t *assets, kon_assetEntry_t *entry) { if (assets->loaderUsed[entry->type] && assets->loaders[entry->type].unload) { assets->loaders[entry->type].unload(entry->object); } free(entry->name); } /*** assets ***/ static void kon_addLoader_(kon_assets_t *assets, int type, const char *name, const char *extensions, void *(*load)(const uint8_t *, size_t, size_t *), void (*unload)(void *)) { assets->loaders[type].name = name; assets->loaders[type].extensions = extensions; assets->loaders[type].load = load; assets->loaders[type].unload = unload; assets->loaderUsed[type] = 1; } kon_assets_t *kon_assetsCreate(kon_pack_t *pack, const char *looseDir) { kon_assets_t *assets = calloc(1, sizeof(kon_assets_t)); if (!assets) return NULL; assets->pack = pack; assets->error = "no error"; assets->nextCustom = KON_ASSET_CUSTOM; #ifdef KON_DEVELOPER if (looseDir) { assets->looseDir = malloc(strlen(looseDir) + 1); if (!assets->looseDir) { free(assets); return NULL; } strcpy(assets->looseDir, looseDir); } #else (void)looseDir; #endif kon_addLoader_(assets, KON_ASSET_BLOB, "blob", NULL, kon_loadBlob_, kon_unloadBlob_); #ifdef KON_MODULE_IMAGE kon_addLoader_(assets, KON_ASSET_IMAGE, "image", ".png .jpg .jpeg .bmp .tga .gif", kon_loadImageAsset_, kon_unloadImageAsset_); #endif #ifdef KON_MODULE_AUDIO kon_addLoader_(assets, KON_ASSET_SOUND, "sound", ".wav .mp3 .flac", kon_loadSoundAsset_, kon_unloadSoundAsset_); #endif return assets; } void kon_assetsDestroy(kon_assets_t *assets) { if (!assets) return; for (int i = 0; i < assets->count; i++) kon_unloadEntry_(assets, &assets->entries[i]); free(assets->entries); free(assets->looseDir); free(assets); } int kon_assetsRegister(kon_assets_t *assets, const kon_assetLoader_t *loader) { int type; if (!assets || !loader || !loader->load || !loader->extensions || !loader->name) return -1; if (assets->nextCustom >= KON_ASSET_LOADERS_) return -1; type = assets->nextCustom++; kon_addLoader_(assets, type, loader->name, loader->extensions, loader->load, loader->unload); return type; } void *kon_assetGet(kon_assets_t *assets, const char *name) { return kon_assetGetAs(assets, name, -1); } void *kon_assetGetAs(kon_assets_t *assets, const char *name, int type) { kon_assetEntry_t *entry; uint8_t *data; size_t size = 0, bytes = 0; int loose, found; void *object; if (!assets) return NULL; if (!kon_nameIsSafe_(name)) { assets->error = "bad asset name"; return NULL; } entry = kon_findEntry_(assets, name); if (entry) { if (type >= 0 && entry->type != type) { assets->error = "asset is a different type"; return NULL; } entry->refs++; return entry->object; } found = kon_typeForName_(assets, name); if (type >= 0 && found != type) { assets->error = "asset is a different type"; return NULL; } data = kon_readFile_(assets, name, &size, &loose); if (!data) return NULL; object = assets->loaders[found].load(data, size, &bytes); free(data); if (!object) { assets->error = "could not decode the file"; return NULL; } if (assets->count == assets->capacity) { int capacity = assets->capacity ? assets->capacity * 2 : 32; kon_assetEntry_t *grown = realloc(assets->entries, (size_t)capacity * sizeof(kon_assetEntry_t)); if (!grown) { assets->loaders[found].unload(object); assets->error = "out of memory"; return NULL; } assets->entries = grown; assets->capacity = capacity; } entry = &assets->entries[assets->count]; entry->name = malloc(strlen(name) + 1); if (!entry->name) { assets->loaders[found].unload(object); assets->error = "out of memory"; return NULL; } strcpy(entry->name, name); entry->object = object; entry->type = found; entry->refs = 1; entry->bytes = bytes; entry->loose = loose; assets->count++; return object; } void kon_assetRelease(kon_assets_t *assets, const char *name) { kon_assetEntry_t *entry; if (!assets || !name) return; entry = kon_findEntry_(assets, name); if (entry && entry->refs > 0) entry->refs--; } int kon_assetsCollect(kon_assets_t *assets) { int kept = 0, freed = 0; if (!assets) return 0; for (int i = 0; i < assets->count; i++) { if (assets->entries[i].refs > 0) { assets->entries[kept++] = assets->entries[i]; } else { kon_unloadEntry_(assets, &assets->entries[i]); freed++; } } assets->count = kept; return freed; } int kon_assetsCount(const kon_assets_t *assets) { return assets ? assets->count : 0; } int kon_assetsInfo(const kon_assets_t *assets, int index, kon_assetInfo_t *info) { const kon_assetEntry_t *entry; if (!assets || !info || index < 0 || index >= assets->count) return 0; entry = &assets->entries[index]; info->name = entry->name; info->typeName = assets->loaderUsed[entry->type] ? assets->loaders[entry->type].name : "?"; info->type = entry->type; info->refs = entry->refs; info->bytes = entry->bytes; info->loose = entry->loose; return 1; } size_t kon_assetsBytes(const kon_assets_t *assets) { size_t total = 0; if (!assets) return 0; for (int i = 0; i < assets->count; i++) total += assets->entries[i].bytes; return total; } void kon_assetsDump(const kon_assets_t *assets, FILE *out) { kon_assetInfo_t info; if (!assets || !out) return; fprintf(out, "%d assets, %lu bytes\n", assets->count, (unsigned long)kon_assetsBytes(assets)); for (int i = 0; kon_assetsInfo(assets, i, &info); i++) { fprintf(out, "%-32s %-8s refs %-3d %10lu bytes%s\n", info.name, info.typeName, info.refs, (unsigned long)info.bytes, info.loose ? " loose" : ""); } } const char *kon_assetsError(const kon_assets_t *assets) { return assets ? assets->error : "no assets"; }