changeset 552:ffd972a3d084

rpg: major rewrite of tilesets - Now tilesets can be opened using a custom allocator/loader. - A default mlk_tileset_loader_file implementation is provided. - Put a simple example-tileset example to demonstrate.
author David Demelier <markand@malikania.fr>
date Tue, 07 Mar 2023 20:45:00 +0100
parents 856c2e96189d
children cb4508f45048
files cmake/MlkBcc.cmake examples/CMakeLists.txt examples/example-tileset/CMakeLists.txt examples/example-tileset/example-tileset.c libmlk-example/CMakeLists.txt libmlk-example/mlk/example/registry.c libmlk-example/mlk/example/registry.h libmlk-rpg/CMakeLists.txt libmlk-rpg/mlk/rpg/map-file.h libmlk-rpg/mlk/rpg/tileset-file.c libmlk-rpg/mlk/rpg/tileset-file.h libmlk-rpg/mlk/rpg/tileset-loader-file.c libmlk-rpg/mlk/rpg/tileset-loader-file.h libmlk-rpg/mlk/rpg/tileset-loader.c libmlk-rpg/mlk/rpg/tileset-loader.h libmlk-rpg/mlk/rpg/tileset.c libmlk-rpg/mlk/rpg/tileset.h tests/CMakeLists.txt tests/assets/maps/error-image.tileset tests/assets/maps/error-tileheight.tileset tests/assets/maps/error-tilewidth.tileset tests/assets/maps/sample-tileset.tileset tests/test-tileset.c
diffstat 23 files changed, 1308 insertions(+), 507 deletions(-) [+]
line wrap: on
line diff
--- a/cmake/MlkBcc.cmake	Mon Mar 06 20:44:43 2023 +0100
+++ b/cmake/MlkBcc.cmake	Tue Mar 07 20:45:00 2023 +0100
@@ -46,6 +46,7 @@
 			set(args "-cs")
 		endif ()
 
+		message("===> ${output}")
 		set(outputfile ${CMAKE_CURRENT_BINARY_DIR}/${output})
 
 		add_custom_command(
--- a/examples/CMakeLists.txt	Mon Mar 06 20:44:43 2023 +0100
+++ b/examples/CMakeLists.txt	Tue Mar 07 20:45:00 2023 +0100
@@ -32,6 +32,7 @@
 	example-message
 	example-notify
 	example-sprite
+	example-tileset
 	example-trace
 	example-ui
 )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/example-tileset/CMakeLists.txt	Tue Mar 07 20:45:00 2023 +0100
@@ -0,0 +1,33 @@
+#
+# CMakeLists.txt -- CMake build system for Molko's Engine
+#
+# Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+project(example-tileset)
+
+set(
+	SOURCES
+	${example-tileset_SOURCE_DIR}/example-tileset.c
+)
+
+mlk_executable(
+	NAME example-tileset
+	FOLDER examples
+	LIBRARIES libmlk-example
+	SOURCES ${SOURCES}
+)
+
+source_group(TREE ${example-tileset_SOURCE_DIR} FILES ${SOURCES})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/example-tileset/example-tileset.c	Tue Mar 07 20:45:00 2023 +0100
@@ -0,0 +1,214 @@
+/*
+ * example-tileset.c -- example on how to use a tileset
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <mlk/core/animation.h>
+#include <mlk/core/core.h>
+#include <mlk/core/err.h>
+#include <mlk/core/event.h>
+#include <mlk/core/game.h>
+#include <mlk/core/image.h>
+#include <mlk/core/key.h>
+#include <mlk/core/painter.h>
+#include <mlk/core/panic.h>
+#include <mlk/core/state.h>
+#include <mlk/core/sys.h>
+#include <mlk/core/trace.h>
+#include <mlk/core/util.h>
+#include <mlk/core/window.h>
+
+#include <mlk/ui/label.h>
+#include <mlk/ui/ui.h>
+
+#include <mlk/rpg/tileset-loader-file.h>
+#include <mlk/rpg/tileset-loader.h>
+#include <mlk/rpg/tileset.h>
+
+#include <mlk/example/example.h>
+#include <mlk/example/registry.h>
+
+static struct mlk_tileset_loader_file loader_file;
+static struct mlk_tileset_loader loader;
+static struct mlk_tileset tileset;
+
+/*
+ * This is only to demonstrate how we can custom-allocate data, you can skip
+ * that part.
+ */
+static struct mlk_sprite sprites[16];
+static size_t spritesz;
+static struct mlk_animation animations[16];
+static size_t animationsz;
+
+static const struct {
+	const char *basename;
+	struct mlk_texture *texture;
+} table_textures[] = {
+	{ "world.png",                  &mlk_registry_textures[MLK_REGISTRY_TEXTURE_WORLD] },
+	{ "animation-water.png",        &mlk_registry_textures[MLK_REGISTRY_TEXTURE_WATER] },
+	{ NULL,                         NULL                                               }
+};
+
+static struct mlk_texture *
+init_texture(struct mlk_tileset_loader *loader, const char *ident)
+{
+	(void)loader;
+
+	char filepath[MLK_PATH_MAX], filename[FILENAME_MAX + 1];
+
+	mlk_util_strlcpy(filepath, ident, sizeof (filepath));
+	mlk_util_strlcpy(filename, mlk_util_basename(filepath), sizeof (filename));
+	mlk_tracef("Searching for texture %s", filename);
+
+	for (size_t i = 0; table_textures[i].basename != NULL; ++i)
+		if (strcmp(table_textures[i].basename, filename) == 0)
+			return table_textures[i].texture;
+
+	return NULL;
+}
+
+static struct mlk_sprite *
+init_sprite(struct mlk_tileset_loader *loader)
+{
+	(void)loader;
+
+	/* Just ensure we haven't reach the limit. */
+	assert(spritesz < MLK_UTIL_SIZE(sprites));
+
+	return &sprites[spritesz++];
+}
+
+static struct mlk_animation *
+init_animation(struct mlk_tileset_loader *loader)
+{
+	(void)loader;
+
+	/* Just ensure we haven't reach the limit. */
+	assert(animationsz < MLK_UTIL_SIZE(animations));
+
+	return &animations[animationsz++];
+}
+
+static void
+init(void)
+{
+	if (mlk_example_init("example-tileset") < 0)
+		mlk_panic();
+
+	// TODO: temporary.
+	const char *path = "/home/markand/dev/molko/build/libmlk-example/tilesets/world.tileset";
+
+	/*
+	 * Demonstrate how we can override functions to use different resources
+	 * and/or allocations.
+	 *
+	 * Images are loaded from the libmlk-example registry from RAM and
+	 * sprites animations are statically allocated.
+	 */
+	mlk_tileset_loader_file_init(&loader_file, &loader, path);
+	loader.init_texture = init_texture;
+	loader.init_sprite = init_sprite;
+	loader.init_animation = init_animation;
+
+	if (mlk_tileset_loader_open(&loader, &tileset, path) < 0)
+		mlk_panic();
+}
+
+static void
+handle(struct mlk_state *st, const union mlk_event *ev)
+{
+	(void)st;
+
+	switch (ev->type) {
+	case MLK_EVENT_QUIT:
+		mlk_game_quit();
+		break;
+	default:
+		break;
+	}
+}
+
+static void
+update(struct mlk_state *st, unsigned int ticks)
+{
+	(void)st;
+
+	mlk_tileset_update(&tileset, ticks);
+}
+
+static void
+draw(struct mlk_state *st)
+{
+	(void)st;
+
+	int nc, nr;
+
+	mlk_painter_set_color(MLK_EXAMPLE_BG);
+	mlk_painter_clear();
+
+	/*
+	 * Draw the animated tile all over the screen.
+	 */
+	nc = (MLK_EXAMPLE_W / tileset.sprite->cellw) + 1;
+	nr = (MLK_EXAMPLE_H / tileset.sprite->cellh) + 1;
+
+	for (int r = 0; r < nr; ++r) {
+		for (int c = 0; c < nc; ++c) {
+			mlk_tileset_draw(&tileset, 7, 22,
+			    c * tileset.sprite->cellw,
+			    r * tileset.sprite->cellh);
+		}
+	}
+
+	mlk_painter_present();
+}
+
+static void
+run(void)
+{
+	struct mlk_state state = {
+		.handle = handle,
+		.update = update,
+		.draw = draw
+	};
+
+	mlk_game_init();
+	mlk_game_push(&state);
+	mlk_game_loop();
+}
+
+static void
+quit(void)
+{
+	mlk_tileset_loader_file_finish(&loader_file);
+	mlk_example_finish();
+}
+
+int
+main(int argc, char **argv)
+{
+	(void)argc;
+	(void)argv;
+
+	init();
+	run();
+	quit();
+}
--- a/libmlk-example/CMakeLists.txt	Mon Mar 06 20:44:43 2023 +0100
+++ b/libmlk-example/CMakeLists.txt	Tue Mar 07 20:45:00 2023 +0100
@@ -46,6 +46,8 @@
 	${libmlk-example_SOURCE_DIR}/assets/sprites/numbers.png
 	${libmlk-example_SOURCE_DIR}/assets/sprites/people.png
 	${libmlk-example_SOURCE_DIR}/assets/sprites/ui-cursor.png
+	${libmlk-example_SOURCE_DIR}/assets/sprites/water.png
+	${libmlk-example_SOURCE_DIR}/assets/sprites/world.png
 )
 
 set(
@@ -58,14 +60,15 @@
 	${libmlk-example_SOURCE_DIR}/assets/tilesets/world.json
 )
 
+mlk_maps("${MAPS}" ${libmlk-example_BINARY_DIR}/maps maps)
+mlk_tilesets("${TILESETS}" ${libmlk-example_BINARY_DIR}/tilesets tilesets)
+
 mlk_library(
 	NAME libmlk-example
 	SOURCES ${SOURCES}
-	ASSETS ${ASSETS}
+	ASSETS ${ASSETS} ${maps} ${tilesets}
 	TYPE STATIC
 	LIBRARIES libmlk-rpg
-	MAPS ${MAPS}
-	TILESETS ${TILESETS}
 	INCLUDES
 		PUBLIC
 			$<BUILD_INTERFACE:${libmlk-example_SOURCE_DIR}>
--- a/libmlk-example/mlk/example/registry.c	Mon Mar 06 20:44:43 2023 +0100
+++ b/libmlk-example/mlk/example/registry.c	Tue Mar 07 20:45:00 2023 +0100
@@ -38,6 +38,8 @@
 #include <assets/sprites/numbers.h>
 #include <assets/sprites/people.h>
 #include <assets/sprites/ui-cursor.h>
+#include <assets/sprites/water.h>
+#include <assets/sprites/world.h>
 
 #include <assets/music/vabsounds-romance.h>
 
@@ -75,6 +77,7 @@
 } textures[] = {
 	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_CURSOR, assets_sprites_ui_cursor, 24, 24),
 	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_EXPLOSION, assets_sprites_explosion, 256, 256),
+	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_WATER, assets_sprites_water, 48, 48),
 	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_JOHN_SWORD, assets_sprites_john_sword, 256, 256),
 	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_JOHN_WALK, assets_sprites_john_walk, 256, 256),
 	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_HAUNTED_WOOD, assets_images_haunted_wood, 0, 0),
@@ -82,7 +85,8 @@
 	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_CHEST, assets_sprites_chest, 32, 32),
 	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_NUMBERS, assets_sprites_numbers, 48, 48),
 	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_SWORD, assets_images_sword, 0, 0),
-	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_PEOPLE, assets_sprites_people, 48, 48)
+	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_PEOPLE, assets_sprites_people, 48, 48),
+	MLK_REGISTRY_TEXTURE(MLK_REGISTRY_TEXTURE_WORLD, assets_sprites_world, 48, 48)
 };
 
 static const struct {
--- a/libmlk-example/mlk/example/registry.h	Mon Mar 06 20:44:43 2023 +0100
+++ b/libmlk-example/mlk/example/registry.h	Tue Mar 07 20:45:00 2023 +0100
@@ -31,6 +31,7 @@
 
 	/* Animations. */
 	MLK_REGISTRY_TEXTURE_EXPLOSION,
+	MLK_REGISTRY_TEXTURE_WATER,
 
 	/* Characters. */
 	MLK_REGISTRY_TEXTURE_JOHN_WALK,
@@ -47,6 +48,9 @@
 	/* Sword by Icongeek26 (https://www.flaticon.com). */
 	MLK_REGISTRY_TEXTURE_SWORD,
 
+	/* Tileset textures. */
+	MLK_REGISTRY_TEXTURE_WORLD,
+
 	/* Unused.*/
 	MLK_REGISTRY_TEXTURE_LAST
 };
--- a/libmlk-rpg/CMakeLists.txt	Mon Mar 06 20:44:43 2023 +0100
+++ b/libmlk-rpg/CMakeLists.txt	Tue Mar 07 20:45:00 2023 +0100
@@ -20,10 +20,6 @@
 
 set(
 	SOURCES
-	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/map-file.c
-	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/map-file.h
-	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/map.c
-	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/map.h
 	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/message.c
 	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/message.h
 	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/property.c
@@ -34,8 +30,10 @@
 	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/rpg.h
 	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/save.c
 	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/save.h
-	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/tileset-file.c
-	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/tileset-file.h
+	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/tileset-loader-file.c
+	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/tileset-loader-file.h
+	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/tileset-loader.c
+	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/tileset-loader.h
 	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/tileset.c
 	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/tileset.h
 	${libmlk-rpg_SOURCE_DIR}/mlk/rpg/walksprite.c
--- a/libmlk-rpg/mlk/rpg/map-file.h	Mon Mar 06 20:44:43 2023 +0100
+++ b/libmlk-rpg/mlk/rpg/map-file.h	Tue Mar 07 20:45:00 2023 +0100
@@ -25,7 +25,7 @@
 
 #include "map.h"
 #include "tileset.h"
-#include "tileset-file.h"
+#include "tileset-loader.h"
 
 #define MAP_FILE_TITLE_MAX 64
 
@@ -35,7 +35,7 @@
 	char title[MAP_FILE_TITLE_MAX];
 	struct map_layer layers[MAP_LAYER_TYPE_NUM];
 	struct tileset_file tileset_file;
-	struct tileset tileset;
+	struct mlk_tileset tileset;
 	struct mlk_alloc_pool blocks;
 };
 
--- a/libmlk-rpg/mlk/rpg/tileset-file.c	Mon Mar 06 20:44:43 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,343 +0,0 @@
-/*
- * tileset-file.c -- tileset file loader
- *
- * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <assert.h>
-#include <errno.h>
-#include <limits.h>
-#include <stddef.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include <mlk/util/util.h>
-
-#include <mlk/core/alloc.h>
-#include <mlk/core/animation.h>
-#include <mlk/core/image.h>
-#include <mlk/core/util.h>
-
-#include "tileset-file.h"
-#include "tileset.h"
-
-#define MAX_F(v) MAX_F_(v)
-#define MAX_F_(v) "%" #v "[^\n|]"
-
-/*
- * This is how memory for animations is allocated in the tileset_file
- * structure.
- *
- * As animations require a texture and a sprite to be present, we need to store
- * them locally in the tileset_file structure.
- *
- * tileset_file->anims[0] array (struct tileset_animation_block):
- *
- * [0]            [1]            [N]
- *  | texture      | texture      | texture
- *  | sprite       | sprite       | sprite
- *  | animation    | animation    | animation
- *
- * tileset_file->anims[1] array (struct tileset_animation):
- *
- * [0]            [1]            [N]
- *  | id           | id           | id
- *  | animation ^  | animation ^  | animation ^
- *
- * The second array is the exposed array through the tileset->anims pointer,
- * animations are referenced from the first array. This is because user may need
- * or replace the tileset by itself and as such we need to keep track of the
- * resource the tileset_file has allocated itself.
- */
-
-struct tileset_animation_block {
-	struct mlk_texture texture;
-	struct mlk_sprite sprite;
-	struct mlk_animation animation;
-};
-
-struct context {
-	struct tileset_file *tf;
-	struct tileset *tileset;
-	FILE *fp;
-
-	char basedir[MLK_PATH_MAX];
-
-	/*
-	 * The following properties aren't stored in the tileset because they
-	 * are not needed after loading.
-	 */
-	unsigned int tilewidth;
-	unsigned int tileheight;
-
-	/* Number of rows/columns in the image. */
-	unsigned int nrows;
-	unsigned int ncolumns;
-};
-
-static void
-tileset_animation_block_finish(void *data)
-{
-	struct tileset_animation_block *anim = data;
-
-	mlk_texture_finish(&anim->texture);
-}
-
-static int
-tileset_tiledef_cmp(const void *d1, const void *d2)
-{
-	const struct tileset_tiledef *mtd1 = d1;
-	const struct tileset_tiledef *mtd2 = d2;
-
-	if (mtd1->id < mtd2->id)
-		return -1;
-	if (mtd1->id > mtd2->id)
-		return 1;
-
-	return 0;
-}
-
-static int
-tileset_animation_cmp(const void *d1, const void *d2)
-{
-	const struct tileset_animation *mtd1 = d1;
-	const struct tileset_animation *mtd2 = d2;
-
-	if (mtd1->id < mtd2->id)
-		return -1;
-	if (mtd1->id > mtd2->id)
-		return 1;
-
-	return 0;
-}
-
-static int
-parse_tilewidth(struct context *ctx, const char *line)
-{
-	if (sscanf(line, "tilewidth|%u", &ctx->tilewidth) != 1 || ctx->tilewidth == 0)
-		return mlk_errf("tilewidth is null");
-
-	return 0;
-}
-
-static int
-parse_tileheight(struct context *ctx, const char *line)
-{
-	if (sscanf(line, "tileheight|%u", &ctx->tileheight) != 1 || ctx->tileheight == 0)
-		return mlk_errf("tileheight is null");
-
-	return 0;
-}
-
-static int
-parse_tiledefs(struct context *ctx, const char *line)
-{
-	(void)line;
-
-	short x, y;
-	unsigned short id, w, h;
-	struct tileset_tiledef *td;
-
-	mlk_alloc_pool_init(&ctx->tf->tiledefs, 16, sizeof (*td), NULL);
-
-	while (fscanf(ctx->fp, "%hu|%hd|%hd|%hu|%hu\n", &id, &x, &y, &w, &h) == 5) {
-		td = mlk_alloc_pool_new(&ctx->tf->tiledefs);
-		td->id = id;
-		td->x = x;
-		td->y = y;
-		td->w = w;
-		td->h = h;
-	}
-
-	/* Sort the array and expose it through the tileset->tiledefs pointer. */
-	qsort(ctx->tf->tiledefs.data, ctx->tf->tiledefs.size, ctx->tf->tiledefs.elemsize, tileset_tiledef_cmp);
-	ctx->tileset->tiledefs = ctx->tf->tiledefs.data;
-	ctx->tileset->tiledefsz = ctx->tf->tiledefs.size;
-
-	return 0;
-}
-
-static int
-parse_animations(struct context *ctx, const char *line)
-{
-	(void)line;
-
-	unsigned short id;
-	unsigned int delay;
-	char filename[FILENAME_MAX + 1];
-	struct tileset_animation_block *anim;
-
-	mlk_alloc_pool_init(&ctx->tf->anims[0], 16,
-	    sizeof (struct tileset_animation_block), tileset_animation_block_finish);
-	mlk_alloc_pool_init(&ctx->tf->anims[1], 16,
-	    sizeof (struct tileset_animation), NULL);
-
-	/*
-	 * 1. Create the first array of animation, sprite and texture that are
-	 *    owned by the tileset_file structure.
-	 */
-	while (fscanf(ctx->fp, "%hu|" MAX_F(FILENAME_MAX) "|%u", &id, filename, &delay) == 3) {
-		anim = mlk_alloc_pool_new(&ctx->tf->anims[0]);
-
-		if (mlk_image_open(&anim->texture, mlk_util_pathf("%s/%s", ctx->basedir, filename)) < 0)
-			return -1;
-
-		anim->sprite.texture = &anim->texture;
-		anim->sprite.cellw = ctx->tilewidth;
-		anim->sprite.cellh = ctx->tileheight;
-
-		anim->animation.sprite = &anim->sprite;
-		anim->animation.delay = delay;
-
-		mlk_sprite_init(&anim->sprite);
-	}
-
-	/*
-	 * 2. Create the second array that only consist of pointers to
-	 *    animations referencing the first array.
-	 */
-	for (size_t i = 0; i < ctx->tf->anims[0].size; ++i) {
-		struct tileset_animation_block *anim = mlk_alloc_pool_get(&ctx->tf->anims[0], i);
-		struct tileset_animation *ta;
-
-		if (!(ta = mlk_alloc_pool_new(&ctx->tf->anims[1])))
-			return -1;
-
-		ta->id = id;
-		ta->animation = &anim->animation;
-	}
-
-	/*
-	 * 3. Finally expose the second array through the tileset->anims pointer
-	 *    and sort it.
-	 */
-	qsort(ctx->tf->anims[1].data, ctx->tf->anims[1].size, ctx->tf->anims[1].elemsize, tileset_animation_cmp);
-	ctx->tileset->anims  = ctx->tf->anims[1].data;
-	ctx->tileset->animsz = ctx->tf->anims[1].size;
-
-	return 0;
-}
-
-static int
-parse_image(struct context *ctx, const char *line)
-{
-	char *p;
-
-	if (ctx->tilewidth == 0 || ctx->tileheight == 0)
-		return mlk_errf("missing tile dimensions before image");
-	if (!(p = strchr(line, '|')))
-		return mlk_errf("could not parse image");
-	if (mlk_image_open(&ctx->tf->image, mlk_util_pathf("%s/%s", ctx->basedir, p + 1)) < 0)
-		return -1;
-
-	ctx->tf->sprite.texture = &ctx->tf->image;
-	ctx->tf->sprite.cellw = ctx->tilewidth;
-	ctx->tf->sprite.cellh = ctx->tileheight;
-	mlk_sprite_init(&ctx->tf->sprite);
-
-	ctx->tileset->sprite = &ctx->tf->sprite;
-
-	return 0;
-}
-
-static int
-parse_line(struct context *ctx, const char *line)
-{
-	static const struct {
-		const char *property;
-		int (*read)(struct context *, const char *);
-	} props[] = {
-		{ "tilewidth",  parse_tilewidth         },
-		{ "tileheight", parse_tileheight        },
-		{ "tiledefs",   parse_tiledefs          },
-		{ "animations", parse_animations        },
-		{ "image",      parse_image             }
-	};
-
-	for (size_t i = 0; i < MLK_UTIL_SIZE(props); ++i) {
-		if (strncmp(line, props[i].property, strlen(props[i].property)) == 0)
-			return props[i].read(ctx, line);
-	}
-
-	return 0;
-}
-
-static int
-parse(struct context *ctx, const char *path)
-{
-	char line[1024], basedir[MLK_PATH_MAX];
-
-	mlk_util_strlcpy(basedir, path, sizeof (basedir));
-	mlk_util_strlcpy(ctx->basedir, mlk_util_dirname(basedir), sizeof (ctx->basedir));
-
-	while (fgets(line, sizeof (line), ctx->fp)) {
-		/* Remove \n if any */
-		line[strcspn(line, "\r\n")] = '\0';
-
-		if (parse_line(ctx, line) < 0)
-			return -1;
-	}
-
-	return 0;
-}
-
-static int
-check(const struct tileset *tileset)
-{
-	if (!tileset->sprite)
-		return mlk_errf("missing tileset image");
-
-	return 0;
-}
-
-int
-tileset_file_open(struct tileset_file *tf, struct tileset *tileset, const char *path)
-{
-	assert(tf);
-	assert(tileset);
-	assert(path);
-
-	struct context ctx = {
-		.tf = tf,
-		.tileset = tileset
-	};
-	int ret = 0;
-
-	memset(tileset, 0, sizeof (*tileset));
-
-	if (!(ctx.fp = fopen(path, "r")))
-		return -1;
-	if ((ret = parse(&ctx, path)) < 0 || (ret = check(tileset)) < 0)
-		tileset_file_finish(tf);
-
-	fclose(ctx.fp);
-
-	return ret;
-}
-
-void
-tileset_file_finish(struct tileset_file *tf)
-{
-	assert(tf);
-
-	mlk_alloc_pool_finish(&tf->tiledefs);
-	mlk_alloc_pool_finish(&tf->anims[0]);
-	mlk_alloc_pool_finish(&tf->anims[1]);
-
-	mlk_texture_finish(&tf->image);
-
-	memset(tf, 0, sizeof (*tf));
-}
--- a/libmlk-rpg/mlk/rpg/tileset-file.h	Mon Mar 06 20:44:43 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-/*
- * tileset-file.h -- tileset file loader
- *
- * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef MLK_RPG_TILESET_FILE_H
-#define MLK_RPG_TILESET_FILE_H
-
-#include <stddef.h>
-
-#include <mlk/core/alloc.h>
-#include <mlk/core/sprite.h>
-#include <mlk/core/texture.h>
-
-struct tileset;
-struct tileset_tiledef;
-
-struct tileset_file {
-	struct mlk_alloc_pool tiledefs;
-	struct mlk_alloc_pool anims[2];
-	struct mlk_texture image;
-	struct mlk_sprite sprite;
-};
-
-#if defined(__cplusplus)
-extern "C" {
-#endif
-
-int
-tileset_file_open(struct tileset_file *, struct tileset *, const char *);
-
-void
-tileset_file_finish(struct tileset_file *);
-
-#if defined(__cplusplus)
-}
-#endif
-
-#endif /* !MLK_RPG_TILESET_FILE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/mlk/rpg/tileset-loader-file.c	Tue Mar 07 20:45:00 2023 +0100
@@ -0,0 +1,197 @@
+/*
+ * tileset-file.c -- tileset file loader implementation
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <string.h>
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/animation.h>
+#include <mlk/core/image.h>
+#include <mlk/core/sprite.h>
+#include <mlk/core/texture.h>
+
+#include "tileset-loader-file.h"
+#include "tileset-loader.h"
+#include "tileset.h"
+
+static inline void *
+allocate(void ***array, size_t width)
+{
+	void **ptr, *elem;
+
+	/* Not yet allocated? Allocate a new pointer element. */
+	if (!*array)
+		ptr = mlk_alloc_new0(1, sizeof (void *));
+	else
+		ptr = mlk_alloc_expand(*array, 1);
+
+	if (!ptr)
+		return NULL;
+
+	/* Now allocate the element itself because. */
+	if (!(elem = mlk_alloc_new0(1, width)))
+		return NULL;
+
+	/* Store it into the array of elements. */
+	ptr[mlk_alloc_getn(ptr) - 1] = elem;
+	*array = ptr;
+
+	return elem;
+}
+
+static void *
+expand(void **array, size_t n, size_t w)
+{
+	void *ptr;
+
+	if (!*array)
+		ptr = mlk_alloc_new0(n, w);
+	else
+		ptr = mlk_alloc_expand(*array, n);
+
+	if (ptr)
+		*array = ptr;
+
+	return ptr;
+}
+
+static void
+finish(void ***ptr, void (*finish)(void *))
+{
+	size_t len;
+
+	/* Already cleared. */
+	if (!*ptr)
+		return;
+
+	len = mlk_alloc_getn(*ptr);
+
+	for (size_t i = 0; i < len; ++i)
+		finish((*ptr)[i]);
+
+	mlk_alloc_free(*ptr);
+	*ptr = NULL;
+}
+
+static void
+finish_texture(void *element)
+{
+	mlk_texture_finish(element);
+	mlk_alloc_free(element);
+}
+
+static struct mlk_texture *
+init_texture(struct mlk_tileset_loader *self, const char *ident)
+{
+	struct mlk_tileset_loader_file *file = self->data;
+	struct mlk_texture *texture;
+	char path[MLK_PATH_MAX];
+
+	snprintf(path, sizeof (path), "%s/%s", file->directory, ident);
+
+	/* No need to deallocate, already done in finish anyway. */
+	if (!(texture = allocate((void ***)&file->textures, sizeof (struct mlk_texture))))
+		return NULL;
+	if (mlk_image_open(texture, path) < 0)
+		return NULL;
+
+	return texture;
+}
+
+static struct mlk_sprite *
+init_sprite(struct mlk_tileset_loader *self)
+{
+	struct mlk_tileset_loader_file *file = self->data;
+
+	return allocate((void ***)&file->sprites, sizeof (struct mlk_sprite));
+}
+
+static struct mlk_animation *
+init_animation(struct mlk_tileset_loader *self)
+{
+	struct mlk_tileset_loader_file *file = self->data;
+
+	return allocate((void ***)&file->animations, sizeof (struct mlk_animation));
+}
+
+struct mlk_tileset_collision *
+expand_collisions(struct mlk_tileset_loader *self,
+                  struct mlk_tileset_collision *array,
+                  size_t arraysz)
+{
+	(void)array;
+
+	struct mlk_tileset_loader_file *file = self->data;
+
+	return expand((void **)&file->tilecollisions, arraysz, sizeof (struct mlk_tileset_collision));
+}
+
+struct mlk_tileset_animation *
+expand_animations(struct mlk_tileset_loader *self,
+                  struct mlk_tileset_animation *array,
+                  size_t arraysz)
+{
+	(void)array;
+
+	struct mlk_tileset_loader_file *file = self->data;
+
+	return expand((void **)&file->tileanimations, arraysz, sizeof (struct mlk_tileset_animation));
+}
+
+void
+mlk_tileset_loader_file_init(struct mlk_tileset_loader_file *file,
+                      struct mlk_tileset_loader *loader,
+                      const char *filename)
+{
+	assert(file);
+	assert(loader);
+	assert(filename);
+
+	char filepath[MLK_PATH_MAX];
+
+	memset(file, 0, sizeof (*file));
+	memset(loader, 0, sizeof (*loader));
+
+	/* Determine base filename base directory. */
+	mlk_util_strlcpy(filepath, filename, sizeof (filepath));
+	mlk_util_strlcpy(file->directory, mlk_util_dirname(filepath), sizeof (file->directory));
+
+	loader->data = file;
+	loader->init_texture = init_texture;
+	loader->init_sprite = init_sprite;
+	loader->init_animation = init_animation;
+	loader->expand_collisions = expand_collisions;
+	loader->expand_animations = expand_animations;
+}
+
+void
+mlk_tileset_loader_file_finish(struct mlk_tileset_loader_file *file)
+{
+	assert(file);
+
+	/* Finalize individual elements. */
+	finish((void ***)&file->textures, finish_texture);
+	finish((void ***)&file->sprites, mlk_alloc_free);
+	finish((void ***)&file->animations, mlk_alloc_free);
+
+	/* Clear array of collisions/animations .*/
+	mlk_alloc_free(file->tilecollisions);
+	mlk_alloc_free(file->tileanimations);
+
+	memset(file, 0, sizeof (*file));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/mlk/rpg/tileset-loader-file.h	Tue Mar 07 20:45:00 2023 +0100
@@ -0,0 +1,93 @@
+/*
+ * tileset-loader-file.h -- tileset file loader implementation
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_RPG_TILESET_LOADER_FILE_H
+#define MLK_RPG_TILESET_LOADER_FILE_H
+
+/**
+ * \file mlk/rpg/tileset-loader-file.h
+ * \brief Tileset file loader implementation
+ *
+ * This convenient tileset loader loads tilesets from file and its associative
+ * resources relative to the tileset file directory.
+ *
+ * It also allocate memory for individual element and as such must be kept
+ * valid until the tileset is no longer used. If this behavior is not wanted,
+ * the allocator functions can be changed.
+ */
+
+#include <mlk/util/util.h>
+
+struct mlk_animation;
+struct mlk_sprite;
+struct mlk_texture;
+struct mlk_tileset_animation;
+struct mlk_tileset_collision;
+struct mlk_tileset_loader;
+
+/**
+ * \struct mlk_tileset_loader_file
+ * \brief Tileset file loader structure
+ */
+struct mlk_tileset_loader_file {
+	/**
+	 * (read-only)
+	 *
+	 * Computed tileset file directory.
+	 */
+	char directory[MLK_PATH_MAX];
+
+	/** \cond MLK_PRIVATE_DECLS */
+	struct mlk_texture **textures;
+	struct mlk_sprite **sprites;
+	struct mlk_animation **animations;
+	struct mlk_tileset_collision *tilecollisions;
+	struct mlk_tileset_animation *tileanimations;
+	/** \endcond MLK_PRIVATE_DECLS */
+};
+
+/**
+ * Fill the abstract loader with appropriate implementation.
+ *
+ * All loader member functions will be set and ::mlk_tileset_loader::data will
+ * be set to file loader.
+ *
+ * The file and loader structure are zero'ed before being initialized.
+ *
+ * \pre file != NULL
+ * \pre loader != NULL
+ * \pre filename != NULL
+ * \param file the file loader
+ * \param loader the abstract loader interface
+ * \param filename path to the tileset file
+ */
+void
+mlk_tileset_loader_file_init(struct mlk_tileset_loader_file *file,
+                             struct mlk_tileset_loader *loader,
+                             const char *filename);
+
+/**
+ * Cleanup allocated resources by this file loader.
+ *
+ * \pre file != NULL
+ * \param file the file loader
+ */
+void
+mlk_tileset_loader_file_finish(struct mlk_tileset_loader_file *file);
+
+#endif /* !MLK_RPG_TILESET_LOADER_FILE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/mlk/rpg/tileset-loader.c	Tue Mar 07 20:45:00 2023 +0100
@@ -0,0 +1,318 @@
+/*
+ * tileset-loader.c -- abstract tileset loader
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <mlk/util/util.h>
+
+#include <mlk/core/animation.h>
+#include <mlk/core/err.h>
+#include <mlk/core/sprite.h>
+#include <mlk/core/util.h>
+
+#include "tileset-loader.h"
+#include "tileset.h"
+
+static int
+collision_cmp(const void *d1, const void *d2)
+{
+	const struct mlk_tileset_collision *c1 = d1;
+	const struct mlk_tileset_collision *c2 = d2;
+
+	if (c1->id < c2->id)
+		return -1;
+	if (c1->id > c2->id)
+		return 1;
+
+	return 0;
+}
+
+static int
+animation_cmp(const void *d1, const void *d2)
+{
+	const struct mlk_tileset_animation *a1 = d1;
+	const struct mlk_tileset_animation *a2 = d2;
+
+	if (a1->id < a2->id)
+		return -1;
+	if (a1->id > a2->id)
+		return 1;
+
+	return 0;
+}
+
+static int
+parse_tilewidth(struct mlk_tileset_loader *loader,
+                struct mlk_tileset *tileset,
+                const char *line,
+                FILE *fp)
+{
+	(void)loader;
+	(void)tileset;
+	(void)fp;
+
+	if (sscanf(line, "tilewidth|%u", &loader->tilewidth) != 1 || loader->tilewidth == 0)
+		return mlk_errf("tilewidth is null or invalid");
+
+	return 0;
+}
+
+static int
+parse_tileheight(struct mlk_tileset_loader *loader,
+                 struct mlk_tileset *tileset,
+                 const char *line,
+                 FILE *fp)
+{
+	(void)loader;
+	(void)tileset;
+	(void)fp;
+
+	if (sscanf(line, "tileheight|%u", &loader->tileheight) != 1 || loader->tileheight == 0)
+		return mlk_errf("tileheight is null or invalid");
+
+	return 0;
+}
+
+static int
+parse_collisions(struct mlk_tileset_loader *loader,
+                 struct mlk_tileset *tileset,
+                 const char *line,
+                 FILE *fp)
+{
+	(void)line;
+
+	struct mlk_tileset_collision *array, *collision, *collisions = NULL;
+	unsigned int id, w, h;
+	int x, y;
+	size_t collisionsz = 0;
+
+	while (fscanf(fp, "%u|%d|%d|%u|%u\n", &id, &x, &y, &w, &h) == 5) {
+		if (!(array = loader->expand_collisions(loader, collisions, collisionsz + 1)))
+			return -1;
+
+		collisions = array;
+		collision = &collisions[collisionsz++];
+		collision->id = id;
+		collision->x = x;
+		collision->y = y;
+		collision->w = w;
+		collision->h = h;
+	}
+
+	/*
+	 * Sort and link this array in the final tileset, user has ownership of
+	 * the data.
+	 */
+	qsort(collisions, collisionsz, sizeof (*collisions), collision_cmp);
+	tileset->collisions = collisions;
+	tileset->collisionsz = collisionsz;
+
+	return 0;
+}
+
+static int
+parse_animations(struct mlk_tileset_loader *loader,
+                 struct mlk_tileset *tileset,
+                 const char *line,
+                 FILE *fp)
+{
+	(void)line;
+
+	char fmt[64], filename[MLK_PATH_MAX];
+	unsigned int id, delay;
+	struct mlk_tileset_animation *array, *tileanimation, *tileanimations = NULL;
+	struct mlk_texture *texture;
+	struct mlk_sprite *sprite;
+	struct mlk_animation *animation;
+	size_t tileanimationsz = 0;
+
+	/* Create a format string for fscanf. */
+	snprintf(fmt, sizeof (fmt), "%%u|%%%zu[^|]|%%u", sizeof (filename));
+
+	/*
+	 * When parsing animations, we have to create three different
+	 * structures:
+	 *
+	 * 1. The texture itself.
+	 * 2. The sprite object that will use the above texture.
+	 * 3. The animation object.
+	 * 4. Link the animation to the tileset animation.
+	 */
+	while (fscanf(fp, fmt, &id, filename, &delay) == 3) {
+		if (!(texture = loader->init_texture(loader, filename)))
+			return -1;
+		if (!(sprite = loader->init_sprite(loader)))
+			return -1;
+		if (!(animation = loader->init_animation(loader)))
+			return -1;
+		if (!(array = loader->expand_animations(loader, tileanimations, tileanimationsz + 1)))
+			return -1;
+
+		/* Bind the texture to the new sprite. */
+		sprite->texture = texture;
+		sprite->cellw = loader->tilewidth;
+		sprite->cellh = loader->tileheight;
+		mlk_sprite_init(sprite);
+
+		/* Bind the sprite to the new animation. */
+		animation->sprite = sprite;
+		animation->delay = delay;
+
+		/* Add the animation to the array. */
+		tileanimations = array;
+		tileanimation = &tileanimations[tileanimationsz++];
+		tileanimation->id = id;
+		tileanimation->animation = animation;
+	}
+
+	/*
+	 * Sort and link this array in the final tileset, user has ownership of
+	 * the data.
+	 */
+	qsort(tileanimations, tileanimationsz, sizeof (*tileanimations), animation_cmp);
+	tileset->animations = tileanimations;
+	tileset->animationsz = tileanimationsz;
+
+	return 0;
+}
+
+static int
+parse_image(struct mlk_tileset_loader *loader,
+            struct mlk_tileset *tileset,
+            const char *line,
+            FILE *fp)
+{
+	(void)fp;
+
+	const char *p;
+	struct mlk_texture *texture;
+	struct mlk_sprite *sprite;
+
+	if (loader->tilewidth == 0 || loader->tileheight == 0)
+		return mlk_errf("missing tile dimensions before image");
+	if (!(p = strchr(line, '|')))
+		return mlk_errf("could not parse image");
+	if (!(texture = loader->init_texture(loader, p + 1)))
+		return -1;
+	if (!(sprite = loader->init_sprite(loader)))
+		return -1;
+
+	/* Initialize the sprite with the texture. */
+	sprite->texture = texture;
+	sprite->cellw = loader->tilewidth;
+	sprite->cellh = loader->tileheight;
+	mlk_sprite_init(sprite);
+
+	/* Link this texture to the final tileset. */
+	tileset->sprite = sprite;
+
+	return 0;
+}
+
+static int
+parse_line(struct mlk_tileset_loader *loader,
+           struct mlk_tileset *tileset,
+           const char *line,
+           FILE *fp)
+{
+	static const struct {
+		const char *property;
+		int (*read)(struct mlk_tileset_loader *, struct mlk_tileset *, const char *, FILE *);
+	} props[] = {
+		{ "tilewidth",  parse_tilewidth         },
+		{ "tileheight", parse_tileheight        },
+		{ "collisions", parse_collisions        },
+		{ "animations", parse_animations        },
+		{ "image",      parse_image             }
+	};
+
+	for (size_t i = 0; i < MLK_UTIL_SIZE(props); ++i) {
+		if (strncmp(line, props[i].property, strlen(props[i].property)) == 0)
+			return props[i].read(loader, tileset, line, fp);
+	}
+
+	return 0;
+}
+
+static int
+check(const struct mlk_tileset *tileset)
+{
+	if (!tileset->sprite)
+		return mlk_errf("missing tileset image");
+
+	return 0;
+}
+
+static int
+parse(struct mlk_tileset_loader *loader, struct mlk_tileset *tileset, FILE *fp)
+{
+	char line[128];
+
+	while (fgets(line, sizeof (line), fp)) {
+		/* Remove \n if any */
+		line[strcspn(line, "\r\n")] = 0;
+
+		if (parse_line(loader, tileset, line, fp) < 0)
+			return -1;
+	}
+
+	return check(tileset);
+}
+
+int
+mlk_tileset_loader_open(struct mlk_tileset_loader *loader,
+                        struct mlk_tileset *tileset,
+                        const char *path)
+{
+	assert(loader);
+	assert(tileset);
+	assert(path);
+
+	FILE *fp;
+
+	memset(tileset, 0, sizeof (*tileset));
+
+	if (!(fp = fopen(path, "r")))
+		return mlk_errf("%s", strerror(errno));
+
+	return parse(loader, tileset, fp);
+}
+
+int
+mlk_tileset_loader_openmem(struct mlk_tileset_loader *loader,
+                           struct mlk_tileset *tileset,
+                           const void *data,
+                           size_t datasz)
+{
+	assert(loader);
+	assert(tileset);
+	assert(data);
+
+	FILE *fp;
+
+	memset(tileset, 0, sizeof (*tileset));
+
+	if (!(fp = mlk_util_fmemopen((void *)data, datasz, "r")))
+		return -1;
+
+	return parse(loader, tileset, fp);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/mlk/rpg/tileset-loader.h	Tue Mar 07 20:45:00 2023 +0100
@@ -0,0 +1,169 @@
+/*
+ * tileset-loader.h -- abstract tileset loader
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_RPG_TILESET_LOADER_H
+#define MLK_RPG_TILESET_LOADER_H
+
+/**
+ * \file mlk/rpg/tileset-loader.h
+ * \brief Abstract tileset loader
+ *
+ * This module provides a generic way to open tilesets. It uses a callback
+ * system whenever an action has to be taken by the user. by itself, this
+ * module does not alloate nor owns any data.
+ *
+ * It is designed in mind that the loader knows how to decode a tileset data
+ * format file but has no indication on how it should allocate, arrange and
+ * find tileset images and other resources.
+ *
+ * See tileset-file.h for an implementation of this module using files.
+ */
+
+#include <stddef.h>
+
+struct mlk_animation;
+struct mlk_sprite;
+struct mlk_texture;
+struct mlk_tileset;
+struct mlk_tileset_animations;
+struct mlk_tileset_collision;
+
+/**
+ * \struct mlk_tileset_loader
+ * \brief Abstract loader structure
+ *
+ * All function pointers must be set.
+ */
+struct mlk_tileset_loader {
+	/**
+	 * (read-write, borrowed, optional)
+	 *
+	 * Arbitrary user data for callbacks.
+	 */
+	void *data;
+
+	/**
+	 * (read-write)
+	 *
+	 * Open a texture from the given ident name.
+	 *
+	 * \param self this loader
+	 * \param ident the texture name (or path)
+	 * \return a borrowed texture or NULL on failure
+	 */
+	struct mlk_texture * (*init_texture)(struct mlk_tileset_loader *self,
+	                                     const char *ident);
+
+	/**
+	 * (read-write)
+	 *
+	 * Return a sprite that the loader needs.
+	 *
+	 * \param self this loader
+	 * \return a unused sprite
+	 * \return a borrowed sprite or NULL on failure
+	 */
+	struct mlk_sprite * (*init_sprite)(struct mlk_tileset_loader *self);
+
+	/**
+	 * (read-write)
+	 *
+	 * Return an animation that the loader needs.
+	 *
+	 * \param self this loader
+	 * \return a unused animation
+	 * \return a borrowed animation or NULL on failure
+	 */
+	struct mlk_animation * (*init_animation)(struct mlk_tileset_loader *self);
+
+	/**
+	 * (read-write)
+	 *
+	 * Expand the collision array by one element.
+	 *
+	 * \param self this loader
+	 * \param array the old array (can be NULL) to reallocate
+	 * \param arraysz the new array size (usually +1 than before)
+	 * \return a unused animation
+	 */
+	struct mlk_tileset_collision * (*expand_collisions)(struct mlk_tileset_loader *self,
+	                                                    struct mlk_tileset_collision *array,
+	                                                    size_t arraysz);
+
+	/**
+	 * (read-write)
+	 *
+	 * Expand the animation array by one element.
+	 *
+	 * \param self this loader
+	 * \param array the old array (can be NULL) to reallocate
+	 * \param arraysz the new array size (usually +1 than before)
+	 * \return a unused animation
+	 */
+	struct mlk_tileset_animation * (*expand_animations)(struct mlk_tileset_loader *self,
+	                                                    struct mlk_tileset_animation *array,
+	                                                    size_t arraysz);
+
+	/** \cond MLK_PRIVATE_DECLS */
+	unsigned int tilewidth;
+	unsigned int tileheight;
+	/** \endcond MLK_PRIVATE_DECLS */
+};
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+/**
+ * Open a tileset from a filesystem path.
+ *
+ * \pre loader != NULL
+ * \param loader the loader
+ * \param tileset the tileset destination
+ * \param path the path to the tileset file
+ * \return 0 on success or an error code on failure
+ */
+int
+mlk_tileset_loader_open(struct mlk_tileset_loader *loader,
+                        struct mlk_tileset *tileset,
+                        const char *path);
+
+/**
+ * Open a tileset from a const binary data.
+ *
+ * The binary data must be kept alive until the tileset loader is no longer
+ * used.
+ *
+ * \pre loader != NULL
+ * \param loader the loader
+ * \param tileset the tileset destination
+ * \param data the tileset content
+ * \param datasz the tileset content length
+ * \return 0 on success or an error code on failure
+ */
+int
+mlk_tileset_loader_openmem(struct mlk_tileset_loader *loader,
+                           struct mlk_tileset *tileset,
+                           const void *data,
+                           size_t datasz);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_RPG_TILESET_LOADER_H */
--- a/libmlk-rpg/mlk/rpg/tileset.c	Mon Mar 06 20:44:43 2023 +0100
+++ b/libmlk-rpg/mlk/rpg/tileset.c	Tue Mar 07 20:45:00 2023 +0100
@@ -25,40 +25,44 @@
 #include "tileset.h"
 
 static inline int
-anim_cmp(const void *d1, const void *d2)
+animation_cmp(const void *d1, const void *d2)
 {
-	const struct tileset_animation *mtd1 = d1;
-	const struct tileset_animation *mtd2 = d2;
+	const struct mlk_tileset_animation *a1 = d1;
+	const struct mlk_tileset_animation *a2 = d2;
 
-	if (mtd1->id < mtd2->id)
+	if (a1->id < a2->id)
 		return -1;
-	if (mtd1->id > mtd2->id)
+	if (a1->id > a2->id)
 		return 1;
 
 	return 0;
 }
 
-static inline const struct tileset_animation *
-find(const struct tileset *ts, unsigned int r, unsigned int c)
+static inline const struct mlk_tileset_animation *
+find(const struct mlk_tileset *tileset, unsigned int r, unsigned int c)
 {
-	const struct tileset_animation key = {
-		.id = c + (r * ts->sprite->ncols)
+	const struct mlk_tileset_animation key = {
+		.id = c + (r * tileset->sprite->ncols)
 	};
 
-	return bsearch(&key, ts->anims, ts->animsz, sizeof (key), anim_cmp);
+	return bsearch(&key, tileset->animations, tileset->animationsz, sizeof (key), animation_cmp);
 }
 
 int
-tileset_ok(const struct tileset *ts)
+mlk_tileset_ok(const struct mlk_tileset *tileset)
 {
-	return ts && mlk_sprite_ok(ts->sprite);
+	return tileset && mlk_sprite_ok(tileset->sprite);
 }
 
 void
-tileset_start(struct tileset *ts)
+mlk_tileset_start(struct mlk_tileset *tileset)
 {
-	for (size_t i = 0; i < ts->animsz; ++i) {
-		struct tileset_animation *ta = &ts->anims[i];
+	assert(mlk_tileset_ok(tileset));
+
+	struct mlk_tileset_animation *ta;
+
+	for (size_t i = 0; i < tileset->animationsz; ++i) {
+		ta = &tileset->animations[i];
 
 		if (ta->animation)
 			mlk_animation_start(ta->animation);
@@ -66,28 +70,36 @@
 }
 
 void
-tileset_update(struct tileset *ts, unsigned int ticks)
+mlk_tileset_update(struct mlk_tileset *tileset, unsigned int ticks)
 {
-	for (size_t i = 0; i < ts->animsz; ++i) {
-		struct tileset_animation *ta = &ts->anims[i];
+	assert(mlk_tileset_ok(tileset));
+
+	struct mlk_tileset_animation *ta;
+
+	for (size_t i = 0; i < tileset->animationsz; ++i) {
+		ta = &tileset->animations[i];
 
 		if (!ta->animation)
 			continue;
 
+		/* Reset in case it ended, we loop animations. */
 		if (mlk_animation_update(ta->animation, ticks))
 			mlk_animation_start(ta->animation);
 	}
 }
 
-void
-tileset_draw(const struct tileset *ts, unsigned int r, unsigned int c, int x, int y)
+int
+mlk_tileset_draw(const struct mlk_tileset *tileset, unsigned int r, unsigned int c, int x, int y)
 {
-	assert(ts);
+	assert(mlk_tileset_ok(tileset));
+
+	const struct mlk_tileset_animation *ta;
+	int ret;
 
-	const struct tileset_animation *ta;
+	if ((ta = find(tileset, r, c)))
+		ret = mlk_animation_draw(ta->animation, x, y);
+	else
+		ret = mlk_sprite_draw(tileset->sprite, r, c, x, y);
 
-	if ((ta = find(ts, r, c)))
-		mlk_animation_draw(ta->animation, x, y);
-	else
-		mlk_sprite_draw(ts->sprite, r, c, x, y);
+	return ret;
 }
--- a/libmlk-rpg/mlk/rpg/tileset.h	Mon Mar 06 20:44:43 2023 +0100
+++ b/libmlk-rpg/mlk/rpg/tileset.h	Tue Mar 07 20:45:00 2023 +0100
@@ -19,28 +19,115 @@
 #ifndef MLK_RPG_TILESET_H
 #define MLK_RPG_TILESET_H
 
+/**
+ * \file mlk/rpg/tileset.h
+ * \brief Map tileset definition
+ */
+
 #include <stddef.h>
 
+struct mlk_animation;
 struct mlk_sprite;
 
-struct tileset_tiledef {
-	unsigned short id;
-	short x;
-	short y;
-	unsigned short w;
-	unsigned short h;
+/**
+ * \struct mlk_tileset_collision
+ * \brief Describe a tile collision box.
+ */
+struct mlk_tileset_collision {
+	/**
+	 * (read-write)
+	 *
+	 * The sprite cell index.
+	 */
+	unsigned int id;
+
+	/**
+	 * (read-write)
+	 *
+	 * Beginning of collision box in x.
+	 */
+	int x;
+
+	/**
+	 * (read-write)
+	 *
+	 * Beginning of collision box in y.
+	 */
+	int y;
+
+	/**
+	 * (read-write)
+	 *
+	 * Collision box width.
+	 */
+	unsigned int w;
+
+	/**
+	 * (read-write)
+	 *
+	 * Collision box height.
+	 */
+	unsigned int h;
 };
 
-struct tileset_animation {
-	unsigned short id;
+/**
+ * \struct mlk_tileset_animation
+ * \brief Animation per tile
+ */
+struct mlk_tileset_animation {
+	/**
+	 * (read-write)
+	 *
+	 * The sprite cell index.
+	 */
+	unsigned int id;
+
+	/**
+	 * (read-write, borrowed)
+	 *
+	 * Animation to used for this tile.
+	 */
 	struct mlk_animation *animation;
 };
 
-struct tileset {
-	struct tileset_tiledef *tiledefs;
-	size_t tiledefsz;
-	struct tileset_animation *anims;
-	size_t animsz;
+/**
+ * \struct mlk_tileset
+ * \brief Tileset structure
+ */
+struct mlk_tileset {
+	/**
+	 * (read-write, borrowed, optional)
+	 *
+	 * Array of collision boxes per tile that MUST be order by tile id.
+	 */
+	struct mlk_tileset_collision *collisions;
+
+	/**
+	 * (read-write)
+	 *
+	 * Number of items in the ::mlk_tileset::collisions array.
+	 */
+	size_t collisionsz;
+
+	/**
+	 * (read-write, borrowed, optional)
+	 *
+	 * Array of animations per tile that MUST be order by tile id.
+	 */
+	struct mlk_tileset_animation *animations;
+
+	/**
+	 * (read-write)
+	 *
+	 * Number of items in the ::mlk_tileset::animations array.
+	 */
+	size_t animationsz;
+
+	/**
+	 * (read-write, borrowed)
+	 *
+	 * Sprite used to render the map.
+	 */
 	struct mlk_sprite *sprite;
 };
 
@@ -48,17 +135,47 @@
 extern "C" {
 #endif
 
+/**
+ * Tells if the tileset is usable.
+ *
+ * \param tileset the tileset to check
+ * \return non-zero if the tileset structure is usable
+ */
 int
-tileset_ok(const struct tileset *);
+mlk_tileset_ok(const struct mlk_tileset *tileset);
 
+/**
+ * Start tileset animations.
+ *
+ * \pre tileset != NULL
+ * \param tileset the tileset
+ */
 void
-tileset_start(struct tileset *);
+mlk_tileset_start(struct mlk_tileset *tileset);
 
+/**
+ * Update the tileset animations.
+ *
+ * \pre tileset != NULL
+ * \param tileset the tileset
+ * \param ticks frame ticks
+ */
 void
-tileset_update(struct tileset *, unsigned int);
+mlk_tileset_update(struct mlk_tileset *tileset, unsigned int ticks);
 
-void
-tileset_draw(const struct tileset *, unsigned int, unsigned int, int, int);
+/**
+ * Draw a cell row/column into the given position.
+ *
+ * \pre tileset != NULL
+ * \param tileset the tileset
+ * \param r the cell row number
+ * \param c the cell column number
+ * \param x the x coordinate
+ * \param y the y coordinate
+ * \return 0 on success or an error code on failure
+ */
+int
+mlk_tileset_draw(const struct mlk_tileset *tileset, unsigned int r, unsigned int c, int x, int y);
 
 #if defined(__cplusplus)
 }
--- a/tests/CMakeLists.txt	Mon Mar 06 20:44:43 2023 +0100
+++ b/tests/CMakeLists.txt	Tue Mar 07 20:45:00 2023 +0100
@@ -25,7 +25,6 @@
 	alloc
 	color
 	drawable
-	map
 	save
 	save-quest
 	state
--- a/tests/assets/maps/error-image.tileset	Mon Mar 06 20:44:43 2023 +0100
+++ b/tests/assets/maps/error-image.tileset	Tue Mar 07 20:45:00 2023 +0100
@@ -1,6 +1,6 @@
 tilewidth|64
 tileheight|32
-tiledefs
+collisions
 129|8|0|56|40
 130|0|0|62|40
 132|0|0|64|40
--- a/tests/assets/maps/error-tileheight.tileset	Mon Mar 06 20:44:43 2023 +0100
+++ b/tests/assets/maps/error-tileheight.tileset	Tue Mar 07 20:45:00 2023 +0100
@@ -1,6 +1,6 @@
 tilewidth|64
 image|sample-tileset.png
-tiledefs
+collisions
 129|8|0|56|40
 130|0|0|62|40
 132|0|0|64|40
--- a/tests/assets/maps/error-tilewidth.tileset	Mon Mar 06 20:44:43 2023 +0100
+++ b/tests/assets/maps/error-tilewidth.tileset	Tue Mar 07 20:45:00 2023 +0100
@@ -1,6 +1,6 @@
 tileheight|32
 image|sample-tileset.png
-tiledefs
+collisions
 129|8|0|56|40
 130|0|0|62|40
 132|0|0|64|40
--- a/tests/assets/maps/sample-tileset.tileset	Mon Mar 06 20:44:43 2023 +0100
+++ b/tests/assets/maps/sample-tileset.tileset	Tue Mar 07 20:45:00 2023 +0100
@@ -1,7 +1,7 @@
 tilewidth|64
 tileheight|32
 image|sample-tileset.png
-tiledefs
+collisions
 129|8|0|56|40
 130|0|0|62|40
 132|0|0|64|40
--- a/tests/test-tileset.c	Mon Mar 06 20:44:43 2023 +0100
+++ b/tests/test-tileset.c	Tue Mar 07 20:45:00 2023 +0100
@@ -17,88 +17,121 @@
  */
 
 #include <mlk/core/core.h>
+#include <mlk/core/err.h>
+#include <mlk/core/sprite.h>
 #include <mlk/core/window.h>
 
-#include <mlk/rpg/tileset-file.h>
+#include <mlk/rpg/tileset-loader.h>
+#include <mlk/rpg/tileset-loader-file.h>
 #include <mlk/rpg/tileset.h>
 
 #include <dt.h>
 
-static void
-test_basics_sample(void)
-{
-	struct tileset_file loader = {0};
-	struct tileset tileset;
-
-	DT_EQ_INT(tileset_file_open(&loader, &tileset, DIRECTORY "/maps/sample-tileset.tileset"), 0);
-	DT_EQ_UINT(tileset.sprite->cellw, 64U);
-	DT_EQ_UINT(tileset.sprite->cellh, 32U);
-
-	DT_EQ_UINT(tileset.tiledefsz, 4U);
-
-	DT_EQ_UINT(tileset.tiledefs[0].id, 129);
-	DT_EQ_UINT(tileset.tiledefs[0].x, 8);
-	DT_EQ_UINT(tileset.tiledefs[0].y, 0);
-	DT_EQ_UINT(tileset.tiledefs[0].w, 56);
-	DT_EQ_UINT(tileset.tiledefs[0].h, 40);
+/*
+ * Convenient struct that pack all the required data.
+ */
+struct tileset {
+	struct mlk_tileset_loader_file file;
+	struct mlk_tileset_loader loader;
+	struct mlk_tileset tileset;
+};
 
-	DT_EQ_UINT(tileset.tiledefs[1].id, 130);
-	DT_EQ_UINT(tileset.tiledefs[1].x, 0);
-	DT_EQ_UINT(tileset.tiledefs[1].y, 0);
-	DT_EQ_UINT(tileset.tiledefs[1].w, 62);
-	DT_EQ_UINT(tileset.tiledefs[1].h, 40);
+static inline int
+tileset_open(struct tileset *ts, const char *path)
+{
+	mlk_tileset_loader_file_init(&ts->file, &ts->loader, path);
 
-	DT_EQ_UINT(tileset.tiledefs[2].id, 132);
-	DT_EQ_UINT(tileset.tiledefs[2].x, 0);
-	DT_EQ_UINT(tileset.tiledefs[2].y, 0);
-	DT_EQ_UINT(tileset.tiledefs[2].w, 64);
-	DT_EQ_UINT(tileset.tiledefs[2].h, 40);
+	return mlk_tileset_loader_open(&ts->loader, &ts->tileset, path);
+}
 
-	DT_EQ_UINT(tileset.tiledefs[3].id, 133);
-	DT_EQ_UINT(tileset.tiledefs[3].x, 0);
-	DT_EQ_UINT(tileset.tiledefs[3].y, 0);
-	DT_EQ_UINT(tileset.tiledefs[3].w, 58);
-	DT_EQ_UINT(tileset.tiledefs[3].h, 40);
-
-	tileset_file_finish(&loader);
+static inline void
+tileset_finish(struct tileset *ts)
+{
+	mlk_tileset_loader_file_finish(&ts->file);
 }
 
 static void
-test_error_tilewidth(void)
+test_basics_sample(struct tileset *ts)
 {
-	struct tileset_file loader = {0};
-	struct tileset tileset = {0};
+	DT_EQ_INT(tileset_open(ts, DIRECTORY "/maps/sample-tileset.tileset"), 0);
+	DT_EQ_UINT(ts->tileset.sprite->cellw, 64U);
+	DT_EQ_UINT(ts->tileset.sprite->cellh, 32U);
+
+	DT_ASSERT(ts->tileset.collisions);
+	DT_EQ_UINT(ts->tileset.collisionsz, 4U);
+
+	DT_EQ_UINT(ts->tileset.collisions[0].id, 129);
+	DT_EQ_UINT(ts->tileset.collisions[0].x, 8);
+	DT_EQ_UINT(ts->tileset.collisions[0].y, 0);
+	DT_EQ_UINT(ts->tileset.collisions[0].w, 56);
+	DT_EQ_UINT(ts->tileset.collisions[0].h, 40);
 
-	DT_EQ_INT(tileset_file_open(&loader, &tileset, DIRECTORY "/maps/error-tilewidth.tileset"), -1);
+	DT_EQ_UINT(ts->tileset.collisions[1].id, 130);
+	DT_EQ_UINT(ts->tileset.collisions[1].x, 0);
+	DT_EQ_UINT(ts->tileset.collisions[1].y, 0);
+	DT_EQ_UINT(ts->tileset.collisions[1].w, 62);
+	DT_EQ_UINT(ts->tileset.collisions[1].h, 40);
+
+	DT_EQ_UINT(ts->tileset.collisions[2].id, 132);
+	DT_EQ_UINT(ts->tileset.collisions[2].x, 0);
+	DT_EQ_UINT(ts->tileset.collisions[2].y, 0);
+	DT_EQ_UINT(ts->tileset.collisions[2].w, 64);
+	DT_EQ_UINT(ts->tileset.collisions[2].h, 40);
+
+	DT_EQ_UINT(ts->tileset.collisions[3].id, 133);
+	DT_EQ_UINT(ts->tileset.collisions[3].x, 0);
+	DT_EQ_UINT(ts->tileset.collisions[3].y, 0);
+	DT_EQ_UINT(ts->tileset.collisions[3].w, 58);
+	DT_EQ_UINT(ts->tileset.collisions[3].h, 40);
 }
 
 static void
-test_error_tileheight(void)
+test_error_tilewidth(struct tileset *ts)
 {
-	struct tileset_file loader = {0};
-	struct tileset tileset = {0};
+	DT_EQ_INT(tileset_open(ts, DIRECTORY "/maps/error-tilewidth.tileset"), -1);
+	DT_EQ_STR(mlk_err(), "missing tile dimensions before image");
+}
 
-	DT_EQ_INT(tileset_file_open(&loader, &tileset, DIRECTORY "/maps/error-tileheight.tileset"), -1);
+static void
+test_error_tileheight(struct tileset *ts)
+{
+	DT_EQ_INT(tileset_open(ts, DIRECTORY "/maps/error-tileheight.tileset"), -1);
+	DT_EQ_STR(mlk_err(), "missing tile dimensions before image");
 }
 
 static void
-test_error_image(void)
+test_error_image(struct tileset *ts)
 {
-	struct tileset_file loader = {0};
-	struct tileset tileset = {0};
+	DT_EQ_INT(tileset_open(ts, DIRECTORY "/maps/error-image.tileset"), -1);
+	DT_EQ_STR(mlk_err(), "missing tileset image");
+}
 
-	DT_EQ_INT(tileset_file_open(&loader, &tileset, DIRECTORY "/maps/error-image.tileset"), -1);
+static void
+setup(struct tileset *ts)
+{
+	(void)ts;
+}
+
+static void
+teardown(struct tileset *ts)
+{
+	tileset_finish(ts);
 }
 
 int
 main(void)
 {
+	struct tileset ts;
+
 	if (mlk_core_init("fr.malikania", "test") < 0 || mlk_window_open("test-tileset", 100, 100) < 0)
 		return 1;
 
-	DT_RUN(test_basics_sample);
-	DT_RUN(test_error_tilewidth);
-	DT_RUN(test_error_tileheight);
-	DT_RUN(test_error_image);
+	DT_RUN_EX(test_basics_sample, setup, teardown, &ts);
+	DT_RUN_EX(test_error_tilewidth, setup, teardown, &ts);
+	DT_RUN_EX(test_error_tileheight, setup, teardown, &ts);
+	DT_RUN_EX(test_error_image, setup, teardown, &ts);
 	DT_SUMMARY();
+
+	mlk_window_finish();
+	mlk_core_finish();
 }