summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile37
-rwxr-xr-x[-rw-r--r--]build.sh43
-rw-r--r--kon/konassets.h598
-rw-r--r--src/impl/konassets.c2
-rw-r--r--src/impl/konfont.c1
-rw-r--r--src/impl/miniaudio.c2
-rw-r--r--src/impl/stb_image.c2
7 files changed, 675 insertions, 10 deletions
diff --git a/Makefile b/Makefile
index 144cc00..f06d7d5 100644
--- a/Makefile
+++ b/Makefile
@@ -12,9 +12,16 @@ MODOBJS ?=
WARN = -std=c99 -Wall -Wextra -Wpedantic
KONFLAGS = $(WARN) $(MODEFLAGS) $(DEFS) -I$(KONDIR) -I$(EXTDIR)
+EXTFLAGS = -std=gnu99 -w $(MODEFLAGS) $(DEFS) -I$(EXTDIR)
COMPILE = @mkdir -p $(OBJDIR); echo "\# compile $@"; $(CC) $(KONFLAGS) -c
+COMPILEEXT = @mkdir -p $(OBJDIR); echo "\# compile $@"; $(CC) $(EXTFLAGS) -c
+
+ASSETDIR ?= assets
+PACKFLAGS ?=
BIN = $(OBJDIR)/$(PROGRAM)$(EXE)
+PACKER = $(OBJDIR)/konpack$(EXE)
+PACKFILE = $(OBJDIR)/data.kpk
CORE = $(OBJDIR)/engine.o $(OBJDIR)/konwinlib.o $(OBJDIR)/konsofren.o \
$(OBJDIR)/kontime.o $(OBJDIR)/koninput.o
OBJS = $(CORE) $(MODOBJS)
@@ -49,10 +56,36 @@ $(OBJDIR)/kontoml.o: $(SRCDIR)/impl/kontoml.c $(KONDIR)/kontoml.h
$(OBJDIR)/kontile.o: $(SRCDIR)/impl/kontile.c $(KONDIR)/kontile.h $(KONDIR)/konsofren.h
$(COMPILE) $(SRCDIR)/impl/kontile.c -o $@
+$(OBJDIR)/konassets.o: $(SRCDIR)/impl/konassets.c $(KONDIR)/konassets.h $(KONDIR)/konpack.h $(KONDIR)/konsofren.h $(EXTDIR)/*.h
+ $(COMPILE) $(SRCDIR)/impl/konassets.c -o $@
+
+$(OBJDIR)/konaudio.o: $(SRCDIR)/impl/konaudio.c $(KONDIR)/konaudio.h $(KONDIR)/konassets.h $(EXTDIR)/miniaudio.h
+ $(COMPILE) $(SRCDIR)/impl/konaudio.c -o $@
+
+$(OBJDIR)/stb_image.o: $(SRCDIR)/impl/stb_image.c $(EXTDIR)/stb_image.h
+ $(COMPILEEXT) $(SRCDIR)/impl/stb_image.c -o $@
+
+$(OBJDIR)/miniaudio.o: $(SRCDIR)/impl/miniaudio.c $(EXTDIR)/miniaudio.h
+ $(COMPILEEXT) $(SRCDIR)/impl/miniaudio.c -o $@
+
+$(OBJDIR)/konpack.o: $(SRCDIR)/impl/konpack.c $(KONDIR)/konpack.h
+ $(COMPILE) $(SRCDIR)/impl/konpack.c -o $@
+
+tools: $(PACKER)
+
+$(PACKER): tools/konpack.c $(KONDIR)/konpack.h
+ @mkdir -p $(OBJDIR)
+ @echo "# link $@"
+ @$(CC) $(KONFLAGS) -o $@ tools/konpack.c
+
+pack: $(PACKER)
+ @find $(ASSETDIR) -type f | sort > $(OBJDIR)/assets.lst
+ $(PACKER) -c $(PACKFILE) $(PACKFLAGS) -b $(ASSETDIR) -l $(OBJDIR)/assets.lst
+
run: $(BIN)
./$(BIN)
clean:
- rm -f $(OBJS) $(BIN)
+ rm -f $(OBJDIR)/*.o $(BIN) $(PACKER) $(PACKFILE) $(OBJDIR)/assets.lst
-.PHONY: all run clean
+.PHONY: all run clean tools pack
diff --git a/build.sh b/build.sh
index 69eda5c..766f14f 100644..100755
--- a/build.sh
+++ b/build.sh
@@ -7,20 +7,22 @@ platform=native
mode=release
objdir=obj
modules=
+developer=0
usage()
{
- echo "usage: build.sh [-d] [-p platform] [-m module,...] [-o objdir] [target ...]"
- echo "modules: font toml tile"
+ echo "usage: build.sh [-d] [-D] [-p platform] [-m module,...] [-o objdir] [target ...]"
+ echo "modules: font toml tile pack assets image audio"
echo "platforms: native linux bsd windows"
- echo "targets: all run clean"
+ echo "targets: all run clean tools pack"
exit 1
}
-while getopts dm:o:p: opt; do
+while getopts dDm:o:p: opt; do
case $opt in
d) mode=debug ;;
- m) modules=$OPTARG ;;
+ D) developer=1 ;;
+ m) modules="$modules $(echo "$OPTARG" | tr ',' ' ')" ;;
o) objdir=$OPTARG ;;
p) platform=$OPTARG ;;
*) usage ;;
@@ -56,19 +58,44 @@ esac
if [ $mode = debug ]; then
MODEFLAGS="-g -O0 -DKON_DEBUG"
+ developer=1
else
MODEFLAGS="-O2 -DNDEBUG"
fi
+[ $developer = 1 ] && MODEFLAGS="$MODEFLAGS -DKON_DEVELOPER"
MODOBJS=
-for m in $(echo "$modules" | tr ',' ' '); do
+
+addobj()
+{
+ case " $MODOBJS " in
+ *" $objdir/$1.o "*) ;;
+ *) MODOBJS="$MODOBJS $objdir/$1.o" ;;
+ esac
+}
+
+for m in $modules; do
DEFS="$DEFS -DKON_MODULE_$(echo "$m" | tr 'a-z' 'A-Z')"
case $m in
- font|toml|tile) MODOBJS="$MODOBJS $objdir/kon$m.o" ;;
+ font|toml|tile|pack) addobj "kon$m" ;;
+ assets) addobj konassets ;;
+ image) addobj stb_image ;;
+ audio)
+ addobj miniaudio
+ addobj konaudio
+ case $platform in
+ linux) LDLIBS="$LDLIBS -lpthread -ldl" ;;
+ bsd) LDLIBS="$LDLIBS -lpthread" ;;
+ esac
+ ;;
*) echo "build.sh: unknown module '$m'" >&2; usage ;;
esac
done
+case " $modules " in
+*" assets "*) addobj konpack ;;
+esac
+
OBJDIR=$objdir
export CC EXE LDLIBS MODEFLAGS DEFS OBJDIR MODOBJS
@@ -79,5 +106,5 @@ if [ "$(cat "$OBJDIR/.flags" 2>/dev/null)" != "$flags" ]; then
echo "$flags" > "$OBJDIR/.flags"
fi
-echo "===> platform $platform, $mode build, modules: ${modules:-none}"
+echo "===> platform $platform, $mode build, modules:${modules:- none}"
exec ${MAKE:-make} ${*:-all}
diff --git a/kon/konassets.h b/kon/konassets.h
new file mode 100644
index 0000000..0971166
--- /dev/null
+++ b/kon/konassets.h
@@ -0,0 +1,598 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * konassets.h
+ *
+ * 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.
+ */
+
+#ifndef KONASSETS_H
+#define KONASSETS_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdio.h>
+
+#include "konpack.h"
+#include "konsofren.h"
+
+/*** definition ***/
+
+typedef struct kon_assets kon_assets_t;
+
+/* what a loaded file turns into, picked from the file extension. anything unknown is a blob */
+typedef enum kon_assetType {
+ KON_ASSET_BLOB = 0,
+ KON_ASSET_IMAGE,
+ KON_ASSET_SOUND,
+ KON_ASSET_CUSTOM = 16
+} kon_assetType_t;
+
+typedef struct kon_blob {
+ uint8_t *data; /* has one extra 0 byte after the end, so text files can be used as strings */
+ size_t size;
+} kon_blob_t;
+
+/* 16 bit interleaved samples, played back at sampleRate */
+typedef struct kon_sound {
+ int16_t *frames;
+ uint64_t frameCount;
+ uint32_t channels;
+ uint32_t sampleRate;
+} kon_sound_t;
+
+/* load gets the raw file and returns your object, or NULL. set *bytes to what it uses for the debug view.
+ name and extensions (".lvl .map", lowercase, space separated) have to stay valid for as long as assets does */
+typedef struct kon_assetLoader {
+ const char *name;
+ const char *extensions;
+ void *(*load)(const uint8_t *data, size_t size, size_t *bytes);
+ void (*unload)(void *object);
+} kon_assetLoader_t;
+
+typedef struct kon_assetInfo {
+ const char *name;
+ const char *typeName;
+ int type;
+ int refs;
+ size_t bytes;
+ int loose; /* 1 if it came from the loose folder instead of the pack, developer builds only */
+} kon_assetInfo_t;
+
+/* pack and looseDir can each be NULL. the pack stays yours, close it after kon_assetsDestroy.
+ with KON_DEVELOPER defined files in looseDir win over the pack so you can edit without repacking,
+ without it looseDir is ignored and not even compiled in */
+kon_assets_t *kon_assetsCreate(kon_pack_t *pack, const char *looseDir);
+void kon_assetsDestroy(kon_assets_t *assets);
+
+/* returns the type id to use with kon_assetGetAs, or -1 */
+int kon_assetsRegister(kon_assets_t *assets, const kon_assetLoader_t *loader);
+
+/* loads the file the first time and counts a use every time. the object belongs to the manager, do not free it.
+ NULL if it is not found or fails to load, see kon_assetsError */
+void *kon_assetGet(kon_assets_t *assets, const char *name);
+
+/* same but NULL if the file is not of that type, without counting a use */
+void *kon_assetGetAs(kon_assets_t *assets, const char *name, int type);
+
+#define kon_assetBlob(assets, name) ((kon_blob_t *)kon_assetGetAs((assets), (name), KON_ASSET_BLOB))
+#define kon_assetImage(assets, name) ((kon_image *)kon_assetGetAs((assets), (name), KON_ASSET_IMAGE))
+#define kon_assetSound(assets, name) ((kon_sound_t *)kon_assetGetAs((assets), (name), KON_ASSET_SOUND))
+
+/* gives back one use. nothing is freed until kon_assetsCollect */
+void kon_assetRelease(kon_assets_t *assets, const char *name);
+
+/* frees everything nobody is using, returns how many it freed */
+int kon_assetsCollect(kon_assets_t *assets);
+
+/* for the debug view */
+int kon_assetsCount(const kon_assets_t *assets);
+int kon_assetsInfo(const kon_assets_t *assets, int index, kon_assetInfo_t *info);
+size_t kon_assetsBytes(const kon_assets_t *assets);
+void kon_assetsDump(const kon_assets_t *assets, FILE *out);
+
+const char *kon_assetsError(const kon_assets_t *assets);
+
+/*** implementation ***/
+
+#ifdef KONASSETS_IMPLEMENTATION
+
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+
+#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";
+}
+
+#endif /* KONASSETS_IMPLEMENTATION */
+
+#endif
diff --git a/src/impl/konassets.c b/src/impl/konassets.c
new file mode 100644
index 0000000..af67fb1
--- /dev/null
+++ b/src/impl/konassets.c
@@ -0,0 +1,2 @@
+#define KONASSETS_IMPLEMENTATION
+#include "konassets.h"
diff --git a/src/impl/konfont.c b/src/impl/konfont.c
index 04c3bd8..9c92abc 100644
--- a/src/impl/konfont.c
+++ b/src/impl/konfont.c
@@ -1,2 +1,3 @@
+#define STB_TRUETYPE_IMPLEMENTATION
#define KONFONT_IMPLEMENTATION
#include "konfont.h"
diff --git a/src/impl/miniaudio.c b/src/impl/miniaudio.c
new file mode 100644
index 0000000..afcddad
--- /dev/null
+++ b/src/impl/miniaudio.c
@@ -0,0 +1,2 @@
+#define MINIAUDIO_IMPLEMENTATION
+#include "miniaudio.h"
diff --git a/src/impl/stb_image.c b/src/impl/stb_image.c
new file mode 100644
index 0000000..8ddfd1f
--- /dev/null
+++ b/src/impl/stb_image.c
@@ -0,0 +1,2 @@
+#define STB_IMAGE_IMPLEMENTATION
+#include "stb_image.h"