changeset 197:852d0b7817ce

rpg: map, extreme cleanup, closes #2508 @4h
author David Demelier <markand@malikania.fr>
date Mon, 09 Nov 2020 10:37:36 +0100
parents 658ee50b8bcb
children d6f217a5e4b1
files doxygen/page-faq.c libadventure/CMakeLists.txt libadventure/adventure/assets/maps/overworld.map libadventure/adventure/assets/maps/overworld.png libadventure/adventure/assets/tilesets/world.png libadventure/adventure/molko.c libadventure/adventure/molko.h libadventure/adventure/state/mainmenu.c libadventure/adventure/state/panic.c libadventure/adventure/state/splashscreen.c librpg/CMakeLists.txt librpg/rpg/map-file.c librpg/rpg/map-file.h librpg/rpg/map.c librpg/rpg/map.h librpg/rpg/map_state.c librpg/rpg/map_state.h librpg/rpg/walksprite.c librpg/rpg/walksprite.h molko/main.c tests/CMakeLists.txt tests/assets/maps/sample-map.png tests/assets/tilesets/sample-map.png tests/test-map.c
diffstat 24 files changed, 1078 insertions(+), 803 deletions(-) [+]
line wrap: on
line diff
--- a/doxygen/page-faq.c	Mon Nov 09 10:36:39 2020 +0100
+++ b/doxygen/page-faq.c	Mon Nov 09 10:37:36 2020 +0100
@@ -64,15 +64,4 @@
  * synchronisation and many other stuff than a local game does not require.
  *
  * There are no plans to create a network oriented core API anytime soon.
- *
- * # Can I write an action RPG like Zelda?
- *
- * Probably.
- *
- * Do not use the battle system provided and then depending on your game you
- * may:
- *
- * - Edit the \ref map_state.h module to your needs (you may also simply define
- *   your own input/update handler instead),
- * - Create a dedicated state and use \ref map.h if you want.
  */
--- a/libadventure/CMakeLists.txt	Mon Nov 09 10:36:39 2020 +0100
+++ b/libadventure/CMakeLists.txt	Mon Nov 09 10:37:36 2020 +0100
@@ -30,6 +30,8 @@
 
 set(
 	SOURCES
+	${libadventure_SOURCE_DIR}/adventure/molko.c
+	${libadventure_SOURCE_DIR}/adventure/molko.h
 	${libadventure_SOURCE_DIR}/adventure/trace_hud.c
 	${libadventure_SOURCE_DIR}/adventure/trace_hud.h
 )
@@ -40,9 +42,7 @@
 	${libadventure_SOURCE_DIR}/adventure/assets/fonts/lato.ttf
 	${libadventure_SOURCE_DIR}/adventure/assets/fonts/teutonic.ttf
 	${libadventure_SOURCE_DIR}/adventure/assets/fonts/pirata-one.ttf
-	${libadventure_SOURCE_DIR}/adventure/assets/maps/overworld.map
 	${libadventure_SOURCE_DIR}/adventure/assets/sprites/john.png
-	${libadventure_SOURCE_DIR}/adventure/assets/tilesets/world.png
 )
 
 molko_define_library(
@@ -52,8 +52,11 @@
 		${SOURCES}
 	ASSETS ${ASSETS}
 	LIBRARIES libcore libui librpg
+	PRIVATE_INCLUDES
+		${libadventure_SOURCE_DIR}/adventure
 	PUBLIC_INCLUDES
 		$<BUILD_INTERFACE:${libadventure_SOURCE_DIR}>
+	PRIVATE_FLAGS DIRECTORY="${libadventure_SOURCE_DIR}/adventure/assets"
 )
 
 source_group(adventure FILES ${SOURCES})
--- a/libadventure/adventure/assets/maps/overworld.map	Mon Nov 09 10:36:39 2020 +0100
+++ b/libadventure/adventure/assets/maps/overworld.map	Mon Nov 09 10:37:36 2020 +0100
@@ -4,6 +4,7 @@
 height|100
 tilewidth|32
 tileheight|32
+tileset|overworld.png
 layer|background
 21
 21
@@ -20006,4 +20007,3 @@
 0
 0
 0
-tileset|test.png
Binary file libadventure/adventure/assets/maps/overworld.png has changed
Binary file libadventure/adventure/assets/tilesets/world.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libadventure/adventure/molko.c	Mon Nov 09 10:37:36 2020 +0100
@@ -0,0 +1,132 @@
+/*
+ * molko.c -- main structure for Molko's Adventure
+ *
+ * Copyright (c) 2020 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 <stddef.h>
+#include <setjmp.h>
+
+#include <core/clock.h>
+#include <core/core.h>
+#include <core/event.h>
+#include <core/panic.h>
+#include <core/util.h>
+#include <core/window.h>
+
+#include <ui/ui.h>
+
+#include <rpg/rpg.h>
+
+#include <adventure/state/panic.h>
+#include <adventure/state/splashscreen.h>
+#include <adventure/state/mainmenu.h>
+
+#include "molko.h"
+
+#define WINDOW_WIDTH    1280
+#define WINDOW_HEIGHT   720
+
+static jmp_buf panic_buf;
+
+struct molko molko;
+
+static noreturn void
+crash(void)
+{
+	longjmp(panic_buf, 1);
+}
+
+static void
+loop(void)
+{
+	struct clock clock = {0};
+
+	while (game.state) {
+		unsigned int elapsed = clock_elapsed(&clock);
+
+		clock_start(&clock);
+
+		for (union event ev; event_poll(&ev); ) {
+			switch (ev.type) {
+			case EVENT_QUIT:
+				return;
+			default:
+				game_handle(&ev);
+				break;
+			}
+		}
+
+		game_update(elapsed);
+		game_draw();
+
+		if ((elapsed = clock_elapsed(&clock)) < 20)
+			delay(20 - elapsed);
+	}
+}
+
+void
+molko_init(void)
+{
+	if (!core_init() || !ui_init() || !rpg_init())
+		panic();
+	if (!window_open("Molko's Adventure", WINDOW_WIDTH, WINDOW_HEIGHT))
+		panic();
+
+	/*
+	 * From here, we can setup our panic state which requires a window
+	 * to be running.
+	 */
+
+	/* Init unrecoverable panic state. */
+	panic_state(&molko.states[MOLKO_STATE_PANIC]);
+	panic_handler = crash;
+
+	/* Init states. */
+	splashscreen_state(&molko.states[MOLKO_STATE_SPLASH], &molko.states[MOLKO_STATE_MAINMENU]);
+	mainmenu_state(&molko.states[MOLKO_STATE_MAINMENU]);
+	map_state(&molko.map, &molko.states[MOLKO_STATE_MAP]);
+
+	/* Start to splash. */
+	game_switch(&molko.states[MOLKO_STATE_SPLASH], true);
+}
+
+void
+molko_run(void)
+{
+	if (setjmp(panic_buf) == 0) {
+		/* Initial game run. */
+		loop();
+	} else {
+		/* Clear event queue to avoid accidental key presses. */
+		for (union event ev; event_poll(&ev); )
+			continue;
+
+		game_switch(&molko.states[MOLKO_STATE_PANIC], true);
+		loop();
+	}
+}
+
+void
+molko_finish(void)
+{
+	for (size_t i = 0; i < MOLKO_STATE_NUM; ++i)
+		state_finish(&molko.states[i]);
+
+	window_finish();
+	rpg_finish();
+	ui_finish();
+	core_finish();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libadventure/adventure/molko.h	Mon Nov 09 10:37:36 2020 +0100
@@ -0,0 +1,60 @@
+/*
+ * molko.h -- main structure for Molko's Adventure
+ *
+ * Copyright (c) 2020 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 MOLKO_H
+#define MOLKO_H
+
+#include <core/game.h>
+#include <core/texture.h>
+#include <core/sprite.h>
+#include <core/state.h>
+
+#include <rpg/map-file.h>
+#include <rpg/map.h>
+
+enum molko_state {
+	MOLKO_STATE_SPLASH,
+	MOLKO_STATE_MAINMENU,
+	MOLKO_STATE_PANIC,
+	MOLKO_STATE_MAP,
+	MOLKO_STATE_NUM
+};
+
+struct molko {
+	struct game engine;
+	struct state states[MOLKO_STATE_NUM];
+
+	/* MOLKO_STATE_MAP. */
+	struct texture map_player_texture;
+	struct sprite map_player_sprite;
+	struct map_file map_file;
+	struct map map;
+};
+
+extern struct molko molko;
+
+void
+molko_init(void);
+
+void
+molko_run(void);
+
+void
+molko_finish(void);
+
+#endif /* !MOLKO_H */
--- a/libadventure/adventure/state/mainmenu.c	Mon Nov 09 10:36:39 2020 +0100
+++ b/libadventure/adventure/state/mainmenu.c	Mon Nov 09 10:37:36 2020 +0100
@@ -24,6 +24,7 @@
 #include <core/event.h>
 #include <core/font.h>
 #include <core/game.h>
+#include <core/image.h>
 #include <core/painter.h>
 #include <core/panic.h>
 #include <core/state.h>
@@ -39,6 +40,7 @@
 #include <adventure/assets/fonts/pirata-one.h>
 
 #include "mainmenu.h"
+#include "molko.h"
 
 struct mainmenu {
 	struct {
@@ -54,6 +56,20 @@
 new(void)
 {
 	/* TODO: implement here. */
+	if (!map_file_open(&molko.map_file, DIRECTORY "/maps/overworld.map", &molko.map))
+		panic();
+
+	/* Put a sprite. */
+	if (!image_open(&molko.map_player_texture, DIRECTORY "/sprites/john.png"))
+		panic();
+
+	sprite_init(&molko.map_player_sprite, &molko.map_player_texture, 48, 48);
+	molko.map.player_sprite = &molko.map_player_sprite;
+
+	if (!map_init(&molko.map))
+		panic();
+
+	game_switch(&molko.states[MOLKO_STATE_MAP], false);
 }
 
 static void
--- a/libadventure/adventure/state/panic.c	Mon Nov 09 10:36:39 2020 +0100
+++ b/libadventure/adventure/state/panic.c	Mon Nov 09 10:37:36 2020 +0100
@@ -37,8 +37,6 @@
 #include <ui/align.h>
 #include <ui/theme.h>
 
-#include <rpg/map_state.h>
-
 #include "panic.h"
 
 #define BACKGROUND 0x4f5070ff
--- a/libadventure/adventure/state/splashscreen.c	Mon Nov 09 10:36:39 2020 +0100
+++ b/libadventure/adventure/state/splashscreen.c	Mon Nov 09 10:37:36 2020 +0100
@@ -98,9 +98,6 @@
 {
 	struct splashscreen *splash = state->data;
 
-	if (!splash)
-		return;
-
 	texture_finish(&splash->tex);
 
 	free(splash);
--- a/librpg/CMakeLists.txt	Mon Nov 09 10:36:39 2020 +0100
+++ b/librpg/CMakeLists.txt	Mon Nov 09 10:37:36 2020 +0100
@@ -54,8 +54,8 @@
 	${librpg_SOURCE_DIR}/rpg/item.h
 	${librpg_SOURCE_DIR}/rpg/map.c
 	${librpg_SOURCE_DIR}/rpg/map.h
-	${librpg_SOURCE_DIR}/rpg/map_state.c
-	${librpg_SOURCE_DIR}/rpg/map_state.h
+	${librpg_SOURCE_DIR}/rpg/map-file.c
+	${librpg_SOURCE_DIR}/rpg/map-file.h
 	${librpg_SOURCE_DIR}/rpg/message.c
 	${librpg_SOURCE_DIR}/rpg/message.h
 	${librpg_SOURCE_DIR}/rpg/rpg.c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/librpg/rpg/map-file.c	Mon Nov 09 10:37:36 2020 +0100
@@ -0,0 +1,290 @@
+/*
+ * map-file.c -- map file loader
+ *
+ * Copyright (c) 2020 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.
+ */
+
+#define _XOPEN_SOURCE 700
+#include <assert.h>
+#include <errno.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <core/alloc.h>
+#include <core/error.h>
+#include <core/image.h>
+
+#include "map-file.h"
+
+/* Create %<v>c string literal for scanf */
+#define MAX_F(v) MAX_F_(v)
+#define MAX_F_(v) "%" #v "c"
+
+struct parser {
+	struct map_file *mf;            /* Map loader. */
+	struct map *map;                /* Map object to fill. */
+	FILE *fp;                       /* Map file pointer. */
+	char basedir[PATH_MAX];         /* Parent map directory */
+};
+
+static bool
+parse_layer(struct parser *ps, const char *line)
+{
+	char layer_name[32 + 1] = {0};
+	enum map_layer_type layer_type;
+	size_t amount, current;
+
+	/* Check if weight/height has been specified. */
+	if (ps->map->w == 0 || ps->map->h == 0)
+		return errorf("missing map dimensions before layer");
+
+	/* Determine layer type. */
+	if (sscanf(line, "layer|%32s", layer_name) <= 0)
+		return errorf("missing layer type definition");
+
+	if (strcmp(layer_name, "background") == 0)
+		layer_type = MAP_LAYER_TYPE_BACKGROUND;
+	else if (strcmp(layer_name, "foreground") == 0)
+		layer_type = MAP_LAYER_TYPE_FOREGROUND;
+	else
+		return errorf("invalid layer type: %s", layer_name);
+
+	amount = ps->map->w * ps->map->h;
+	current = 0;
+
+	/*
+	 * The next line after a layer declaration is a list of plain integer
+	 * that fill the layer tiles.
+	 */
+	if (!(ps->mf->layers[layer_type].tiles = alloc_zero(amount, sizeof (unsigned short))))
+		return false;
+
+	for (int tile; fscanf(ps->fp, "%d", &tile) && current < amount; ++current)
+		ps->mf->layers[layer_type].tiles[current] = tile;
+
+	ps->map->layers[layer_type].tiles = ps->mf->layers[layer_type].tiles;
+
+	return true;
+}
+
+static bool
+parse_tileset(struct parser *ps, const char *line)
+{
+	char filename[FILENAME_MAX + 1] = {0};
+	char filepath[PATH_MAX];
+	int ret;
+
+	if (ps->map->tile_w == 0 || ps->map->tile_h == 0)
+		return errorf("missing map dimensions before tileset");
+
+	if ((ret = sscanf(line, "tileset|" MAX_F(FILENAME_MAX), filename)) == 1) {
+		snprintf(filepath, sizeof (filepath), "%s/%s", ps->basedir, filename);
+
+		if (!image_open(&ps->mf->tileset, filepath))
+			return false;
+	}
+
+	/* Initialize sprite. */
+	sprite_init(&ps->mf->sprite, &ps->mf->tileset, ps->map->tile_w, ps->map->tile_h);
+	ps->map->tileset = &ps->mf->sprite;
+
+	return true;
+}
+
+static bool
+parse_title(struct parser *ps, const char *line)
+{
+	if (sscanf(line, "title|" MAX_F(MAP_FILE_TITLE_MAX), ps->mf->title) != 1 || strlen(ps->mf->title) == 0)
+		return errorf("null map title");
+
+	ps->map->title = ps->mf->title;
+
+	return true;
+}
+
+static bool
+parse_width(struct parser *ps, const char *line)
+{
+	if (sscanf(line, "width|%u", &ps->map->w) != 1 || ps->map->w == 0)
+		return errorf("null map width");
+
+	return true;
+}
+
+static bool
+parse_height(struct parser *ps, const char *line)
+{
+	if (sscanf(line, "height|%u", &ps->map->h) != 1 || ps->map->h == 0)
+		return errorf("null map height");
+
+	return true;
+}
+
+static bool
+parse_tilewidth(struct parser *ps, const char *line)
+{
+	if (sscanf(line, "tilewidth|%hu", &ps->map->tile_w) != 1 || ps->map->tile_w == 0)
+		return errorf("null map tile width");
+	if (ps->map->w == 0)
+		return errorf("missing map width before tilewidth");
+
+	ps->map->real_w = ps->map->w * ps->map->tile_w;
+
+	return true;
+}
+
+static bool
+parse_tileheight(struct parser *ps, const char *line)
+{
+	if (sscanf(line, "tileheight|%hu", &ps->map->tile_h) != 1 || ps->map->tile_h == 0)
+		return errorf("null map tile height");
+	if (ps->map->h == 0)
+		return errorf("missing map height before tileheight");
+
+	ps->map->real_h = ps->map->h * ps->map->tile_h;
+
+	return true;
+}
+
+static bool
+parse_origin(struct parser *ps, const char *line)
+{
+	if (sscanf(line, "origin|%d|%d", &ps->map->origin_x, &ps->map->origin_y) != 2)
+		return errorf("invalid origin");
+
+	/*
+	 * We adjust the player position here because it should not be done in
+	 * the map_init function. This is because the player should not move
+	 * magically each time we re-use the map (saving position).
+	 */
+	ps->map->player_x = ps->map->origin_x;
+	ps->map->player_y = ps->map->origin_y;
+
+	return true;
+}
+
+static bool
+parse_line(struct parser *ps, const char *line)
+{
+	static const struct {
+		const char *property;
+		bool (*read)(struct parser *, const char *);
+	} props[] = {
+		{ "title",      parse_title             },
+		{ "width",      parse_width             },
+		{ "height",     parse_height            },
+		{ "tilewidth",  parse_tilewidth         },
+		{ "tileheight", parse_tileheight        },
+		{ "tileset",    parse_tileset           },
+		{ "origin",     parse_origin            },
+		{ "layer",      parse_layer             }
+	};
+
+	for (size_t i = 0; i < NELEM(props); ++i)
+		if (strncmp(line, props[i].property, strlen(props[i].property)) == 0)
+			return props[i].read(ps, line);
+
+	return true;
+}
+
+static bool
+parse(struct map_file *loader, const char *path, struct map *map, FILE *fp)
+{
+	char line[1024];
+	struct parser ps = {
+		.mf = loader,
+		.map = map,
+		.fp = fp
+	};
+
+	/*
+	 * Even though dirname(3) usually not modify the path as argument it may
+	 * do according to POSIX specification, as such we still need a
+	 * temporary buffer.
+	 */
+	snprintf(ps.basedir, sizeof (ps.basedir), "%s", path);
+	snprintf(ps.basedir, sizeof (ps.basedir), "%s", dirname(ps.basedir));
+
+	while (fgets(line, sizeof (line), fp)) {
+		/* Remove \n if any */
+		line[strcspn(line, "\n")] = '\0';
+
+		if (!parse_line(&ps, line))
+			return false;
+	}
+
+	return true;
+}
+
+static bool
+check(struct map *map)
+{
+	/*
+	 * Check that we have parsed every required components.
+	 */
+	if (!map->title)
+		return errorf("missing title");
+
+	/*
+	 * We don't need to check width/height because parsing layers and
+	 * tilesets already check for their presence, so only check layers.
+	 */
+	if (!map->layers[0].tiles)
+		return errorf("missing background layer");
+	if (!map->layers[1].tiles)
+		return errorf("missing foreground layer");
+	if (!sprite_ok(map->tileset))
+		return errorf("missing tileset");
+
+	return true;
+}
+
+bool
+map_file_open(struct map_file *file, const char *path, struct map *map)
+{
+	assert(file);
+	assert(path);
+	assert(map);
+
+	FILE *fp;
+	bool ret = true;
+
+	memset(file, 0, sizeof (*file));
+	memset(map, 0, sizeof (*map));
+
+	if (!(fp = fopen(path, "r")))
+		return errorf("%s", strerror(errno));
+
+	if (!(ret = parse(file, path, map, fp)) || !(ret = check(map))) {
+		map_finish(map);
+		map_file_finish(file);
+	}
+
+	fclose(fp);
+
+	return ret;
+}
+
+void
+map_file_finish(struct map_file *file)
+{
+	assert(file);
+
+	texture_finish(&file->tileset);
+	memset(file, 0, sizeof (*file));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/librpg/rpg/map-file.h	Mon Nov 09 10:37:36 2020 +0100
@@ -0,0 +1,102 @@
+/*
+ * map-file.h -- map file loader
+ *
+ * Copyright (c) 2020 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 MOLKO_MAP_FILE_H
+#define MOLKO_MAP_FILE_H
+
+/**
+ * \file map-file.h
+ * \brief Map file loader.
+ *
+ * Because a map is a complicated object that require several components to be
+ * loaded, the way it is loaded from a source is implemented separately to keep
+ * the \ref map.h module as simple as possible.
+ *
+ * This module load map files generated from mlk-map tool and expect some
+ * convention from the user:
+ *
+ * - A tileset image must be present in the same directory as the map itself.
+ *
+ * This module allocates some dynamic data that are stored in the map object as
+ * such the map_file structure must be kept until the map is no longer used
+ * itself.
+ */
+
+#include <stdbool.h>
+
+#include <core/plat.h>
+#include <core/sprite.h>
+#include <core/texture.h>
+
+#include "map.h"
+
+/**
+ * \brief Maximum title map length in file.
+ */
+#define MAP_FILE_TITLE_MAX 64
+
+/**
+ * \brief Context for loading maps.
+ *
+ * Member fields should not be modified directly but must be modified in the
+ * underlying associated map object once loaded.
+ *
+ * Since the map object does not own resources, this map file will dynamically
+ * load and store them from the file and thus must not be destroyed until the
+ * map is no longer use itself.
+ */
+struct map_file {
+	/**
+	 * (*) Map title loaded from file.
+	 */
+	char title[MAP_FILE_TITLE_MAX];
+
+	/**
+	 * (*) Map layers stored dynamically here.
+	 */
+	struct map_layer layers[MAP_LAYER_TYPE_NUM];
+
+	struct texture tileset; /*!< (*) Tileset image. */
+	struct sprite sprite;   /*!< (*) Tileset sprite. */
+};
+
+/**
+ * Try to open a map from a file path.
+ *
+ * \pre file != NULL
+ * \pre path != NULL
+ * \pre map != NULL
+ * \param file the loader to use
+ * \param path the path to the map file
+ * \param map the map to set
+ * \warning Keep file object until map is no longer used.
+ */
+bool
+map_file_open(struct map_file *file, const char *path, struct map *map) PLAT_NODISCARD;
+
+/**
+ * Close resources from the loader.
+ *
+ * \pre file != NULL
+ * \param file the file to loader to destroy
+ * \warning Destroy the map itself before calling this function.
+ */
+void
+map_file_finish(struct map_file *file);
+
+#endif /* !MOLKO_MAP_FILE_H */
--- a/librpg/rpg/map.c	Mon Nov 09 10:36:39 2020 +0100
+++ b/librpg/rpg/map.c	Mon Nov 09 10:37:36 2020 +0100
@@ -22,84 +22,254 @@
 #include <string.h>
 
 #include <core/error.h>
+#include <core/event.h>
 #include <core/image.h>
 #include <core/painter.h>
 #include <core/sprite.h>
+#include <core/state.h>
 #include <core/sys.h>
 #include <core/texture.h>
 #include <core/window.h>
 
+#include <ui/debug.h>
+
 #include "map.h"
 
-/* Create %<v>c string literal for scanf */
-#define MAX_F(v) MAX_F_(v)
-#define MAX_F_(v) "%" #v "c"
+/*
+ * This is the speed the player moves on the map.
+ *
+ * SPEED represents the number of pixels it must move per SEC.
+ * SEC simply represends the number of milliseconds in one second.
+ */
+#define SPEED 200
+#define SEC   1000
+
+/*
+ * Those are margins within the edge of the screen. The camera always try to
+ * keep those padding between the player and the screen.
+ */
+#define MARGIN_WIDTH    80
+#define MARGIN_HEIGHT   80
+
+/*
+ * This structure defines the possible movement of the player as flags since
+ * it's possible to make diagonal movements.
+ */
+enum movement {
+	MOVING_UP       = 1 << 0,
+	MOVING_RIGHT    = 1 << 1,
+	MOVING_DOWN     = 1 << 2,
+	MOVING_LEFT     = 1 << 3
+};
+
+/*
+ * A bit of explanation within this array. The structure walksprite requires
+ * an orientation between 0-7 depending on the user direction.
+ *
+ * Since keys for moving the character may be pressed at the same time, we need
+ * a conversion table from "key pressed" to "orientation".
+ *
+ * When an orientation is impossible, it is set to -1. Example, when both left
+ * and right are pressed.
+ *
+ * MOVING_UP    = 0001 = 0x1
+ * MOVING_RIGHT = 0010 = 0x2
+ * MOVING_DOWN  = 0100 = 0x3
+ * MOVING_LEFT  = 1000 = 0x4
+ */
+static unsigned int orientations[16] = {
+	[0x1] = 0,
+	[0x2] = 2,
+	[0x3] = 1,
+	[0x4] = 4,
+	[0x6] = 3,
+	[0x8] = 6,
+	[0x9] = 7,
+	[0xC] = 5
+};
 
 static void
-parse_layer(struct map_data *data, const char *line, FILE *fp)
+center(struct map *map)
 {
-	char layer_name[32 + 1] = { 0 };
-	struct map_layer *layer;
-	size_t amount, current;
+	map->view_x = map->player_x - (int)(map->view_w / 2);
+	map->view_y = map->player_y - (int)(map->view_h / 2);
+
+	if (map->view_x < 0)
+		map->view_x = 0;
+	else if ((unsigned int)map->view_x > map->real_w - map->view_w)
+		map->view_x = map->real_w - map->view_w;
+
+	if (map->view_y < 0)
+		map->view_y = 0;
+	else if ((unsigned int)map->view_y > map->real_h - map->view_h)
+		map->view_y = map->real_h - map->view_h;
+}
+
+static void
+start(struct map *map)
+{
+	map_repaint(map);
+
+	/* Adjust view. */
+	map->view_w = window.w;
+	map->view_h = window.h;
+
+	/* Adjust margin. */
+	map->margin_w = map->view_w - (MARGIN_WIDTH * 2);
+	map->margin_h = map->view_h - (MARGIN_HEIGHT * 2);
 
-	/* Determine layer. */
-	if (sscanf(line, "layer|%32s", layer_name) <= 0)
-		return;
-	if (strcmp(layer_name, "background") == 0)
-		layer = &data->layers[0];
-	else if (strcmp(layer_name, "foreground") == 0)
-		layer = &data->layers[1];
-	else
-		return;
+	/* Center the view by default. */
+	center(map);
+
+	/* Final bits. */
+	walksprite_init(&map->player_ws, map->player_sprite, 150);
+}
 
-	/* Check if weight/height has been specified. */
-	if (data->w == 0 || data->h == 0)
-		return;
+static void
+handle_keydown(struct map *map, const union event *event)
+{
+	switch (event->key.key) {
+	case KEY_UP:
+		map->player_movement |= MOVING_UP;
+		break;
+	case KEY_RIGHT:
+		map->player_movement |= MOVING_RIGHT;
+		break;
+	case KEY_DOWN:
+		map->player_movement |= MOVING_DOWN;
+		break;
+	case KEY_LEFT:
+		map->player_movement |= MOVING_LEFT;
+		break;
+	default:
+		break;
+	}
 
-	amount = data->w * data->h;
-	current = 0;
-
-	if (!(layer->tiles = calloc(amount, sizeof (unsigned short))))
-		return;
-
-	for (int tile; fscanf(fp, "%d", &tile) && current < amount; ++current)
-		layer->tiles[current] = tile;
+	map->player_angle = orientations[map->player_movement];
 }
 
 static void
-parse(struct map_data *data, const char *line, FILE *fp)
+handle_keyup(struct map *map, const union event *event)
+{
+	switch (event->key.key) {
+	case KEY_UP:
+		map->player_movement &= ~(MOVING_UP);
+		break;
+	case KEY_RIGHT:
+		map->player_movement &= ~(MOVING_RIGHT);
+		break;
+	case KEY_DOWN:
+		map->player_movement &= ~(MOVING_DOWN);
+		break;
+	case KEY_LEFT:
+		map->player_movement &= ~(MOVING_LEFT);
+		break;
+	default:
+		break;
+	}
+}
+
+static void
+move_up(struct map *map, int delta)
 {
-	if (strncmp(line, "title", 5) == 0)
-		sscanf(line, "title|" MAX_F(MAP_TITLE_MAX), data->title);
-	else if (strncmp(line, "width", 5) == 0)
-		sscanf(line, "width|%u", &data->w);
-	else if (strncmp(line, "height", 6) == 0)
-		sscanf(line, "height|%u", &data->h);
-	else if (strncmp(line, "tilewidth", 9) == 0)
-		sscanf(line, "tilewidth|%hu", &data->tile_w);
-	else if (strncmp(line, "tileheight", 10) == 0)
-		sscanf(line, "tileheight|%hu", &data->tile_h);
-	else if (strncmp(line, "origin", 6) == 0)
-		sscanf(line, "origin|%d|%d", &data->origin_x, &data->origin_y);
-	else if (strncmp(line, "tileset", 7) == 0)
-		sscanf(line, "tileset|" MAX_F(MAP_TILESET_MAX), data->tileset);
-	else if (strncmp(line, "layer", 5) == 0)
-		parse_layer(data, line, fp);
+	map->player_y -= delta;
+
+	if (map->player_y < map->margin_y) {
+		map->view_y = map->player_y - MARGIN_HEIGHT;
+
+		if (map->view_y < 0)
+			map->view_y = 0;
+	}
+
+	if (map->player_y < 0)
+		map->player_y = 0;
+}
+
+static void
+move_right(struct map *map, int delta)
+{
+	if (map->player_x + map->player_sprite->cellw + delta > map->real_w)
+		map->player_x = map->real_w - map->player_sprite->cellw;
+	else
+		map->player_x += delta;
+
+	if (map->player_x + map->player_sprite->cellw > map->margin_x + map->margin_w) {
+		map->view_x += delta;
+
+		if (map->view_x + map->view_w > map->real_w)
+			map->view_x = map->real_w - map->view_w;
+	}
 }
 
-static bool
-check(struct map_data *data)
+static void
+move_down(struct map *map, int delta)
+{
+	if (map->player_y + map->player_sprite->cellh + delta > map->real_h)
+		map->player_y = map->real_h - map->player_sprite->cellh;
+	else
+		map->player_y += delta;
+
+	if (map->player_y + map->player_sprite->cellh > map->margin_y + map->margin_h) {
+		map->view_y += delta;
+
+		if (map->view_y + map->view_h > map->real_h)
+			map->view_y = map->real_h - map->view_h;
+	}
+}
+
+static void
+move_left(struct map *map, int delta)
+{
+	if (map->player_x < delta)
+		map->player_x = 0;
+	else
+		map->player_x -= delta;
+
+	if (map->player_x < map->margin_x) {
+		if (map->view_x < delta)
+			map->view_x = 0;
+		else
+			map->view_x -= delta;
+	}
+}
+
+static void
+move(struct map *map, unsigned int ticks)
 {
-	if (strlen(data->title) == 0)
-		return errorf("data has no title");
-	if (data->w == 0 || data->h == 0)
-		return errorf("data has null sizes");
-	if (data->tile_w == 0 || data->tile_h == 0)
-		return errorf("data has null tile sizes");
-	if (!data->layers[0].tiles || !data->layers[1].tiles)
-		return errorf("could not allocate data");
+	/* This is the amount of pixels the player must move. */
+	const int delta = SPEED * ticks / SEC;
+
+	/* This is the rectangle within the view where users must be. */
+	map->margin_x = map->view_x + MARGIN_WIDTH;
+	map->margin_y = map->view_y + MARGIN_HEIGHT;
+
+	int dx = 0;
+	int dy = 0;
+
+	if (map->player_movement == 0)
+		return;
 
-	return true;
+	if (map->player_movement & MOVING_UP)
+		dy = -1;
+	if (map->player_movement & MOVING_DOWN)
+		dy = 1;
+	if (map->player_movement & MOVING_LEFT)
+		dx = -1;
+	if (map->player_movement & MOVING_RIGHT)
+		dx = 1;
+
+	/* Move the player and adjust view if needed. */
+	if (dx > 0)
+		move_right(map, delta);
+	else if (dx < 0)
+		move_left(map, delta);
+
+	if (dy > 0)
+		move_down(map, delta);
+	else if (dy < 0)
+		move_up(map, delta);
+
+	walksprite_update(&map->player_ws, ticks);
 }
 
 static void
@@ -108,124 +278,130 @@
 	assert(map);
 	assert(layer);
 
-	struct sprite sprite;
 	int x = 0, y = 0;
 
-	sprite_init(&sprite, map->tileset, map->data->tile_w, map->data->tile_h);
-
-	for (unsigned int r = 0; r < map->data->w; ++r) {
-		for (unsigned int c = 0; c < map->data->h; ++c) {
-			unsigned int si = r * map->data->w + c;
-			unsigned int sr = (layer->tiles[si] - 1) / sprite.ncols;
-			unsigned int sc = (layer->tiles[si] - 1) % sprite.nrows;
+	for (unsigned int r = 0; r < map->w; ++r) {
+		for (unsigned int c = 0; c < map->h; ++c) {
+			unsigned int si = r * map->w + c;
+			unsigned int sr = (layer->tiles[si] - 1) / map->tileset->ncols;
+			unsigned int sc = (layer->tiles[si] - 1) % map->tileset->nrows;
 
 			if (layer->tiles[si] != 0)
-				sprite_draw(&sprite, sr, sc, x, y);
+				sprite_draw(map->tileset, sr, sc, x, y);
 
-			x += map->data->tile_w;
+			x += map->tile_w;
 		}
 
 		x = 0;
-		y += map->data->tile_h;
+		y += map->tile_h;
 	}
 }
 
+static void
+handle(struct state *state, const union event *ev)
+{
+	map_handle(state->data, ev);
+}
+
+static void
+update(struct state *state, unsigned int ticks)
+{
+	map_update(state->data, ticks);
+}
+
+static void
+draw(struct state *state)
+{
+	return map_draw(state->data);
+}
+
 bool
-map_data_open_fp(struct map_data *data, FILE *fp)
+map_init(struct map *map)
 {
-	assert(data);
+	assert(map);
 
-	char line[1024];
-
-	if (!fp)
+	if (!texture_new(&map->picture, map->real_w, map->real_h))
 		return false;
 
-	memset(data, 0, sizeof (*data));
-
-	while (fgets(line, sizeof (line), fp)) {
-		/* Remove \n if any */
-		line[strcspn(line, "\n")] = '\0';
-		parse(data, line, fp);
-	}
-
-	fclose(fp);
-
-	if (!check(data)) {
-		map_data_finish(data);
-		return false;
-	}
-
-	/* Compute real size. */
-	data->real_w = data->w * data->tile_w;
-	data->real_h = data->h * data->tile_h;
-
-	return true;
-}
-
-bool
-map_data_open(struct map_data *data, const char *path)
-{
-	assert(data);
-	assert(path);
-
-	return map_data_open_fp(data, fopen(path, "r"));
-}
-
-bool
-map_data_openmem(struct map_data *data, const void *buf, size_t bufsz)
-{
-	assert(data);
-	assert(buf);
-
-	return map_data_open_fp(data, fmemopen((void *)buf, bufsz, "r"));
-}
-
-void
-map_data_finish(struct map_data *data)
-{
-	assert(data);
-
-	free(data->layers[0].tiles);
-	free(data->layers[1].tiles);
-
-	memset(data, 0, sizeof (*data));
-}
-
-bool
-map_init(struct map *map, struct map_data *data, struct texture *tileset)
-{
-	assert(map);
-	assert(data);
-	assert(tileset && texture_ok(tileset));
-
-	if (!(texture_new(&map->picture, data->real_w, data->real_h)))
-		return false;
-
-	map->data = data;
-	map->tileset = tileset;
-
-	map_repaint(map);
+	start(map);
 
 	return true;
 }
 
 void
-map_draw(struct map *map, int srcx, int srcy)
+map_handle(struct map *map, const union event *ev)
+{
+	assert(map);
+	assert(ev);
+
+	switch (ev->type) {
+	case EVENT_KEYDOWN:
+		handle_keydown(map, ev);
+		break;
+	case EVENT_KEYUP:
+		handle_keyup(map, ev);
+		break;
+	default:
+		break;
+	}
+}
+
+void
+map_update(struct map *map, unsigned int ticks)
 {
-	texture_scale(&map->picture, srcx, srcy, window.w, window.h,
+	assert(map);
+
+	move(map, ticks);
+}
+
+void
+map_draw(const struct map *map)
+{
+	assert(map);
+
+	struct debug_report report = {0};
+
+	/* Draw the texture about background/foreground. */
+	texture_scale(&map->picture, map->view_x, map->view_y, window.w, window.h,
 	    0, 0, window.w, window.h, 0.0);
+
+	walksprite_draw(
+		&map->player_ws,
+		map->player_angle,
+		map->player_x - map->view_x,
+		map->player_y - map->view_y);
+
+	debugf(&report, "position: %d, %d", map->player_x,
+	    map->player_y);
+	debugf(&report, "view: %d, %d", map->view_x,
+	    map->view_y);
 }
 
 void
 map_repaint(struct map *map)
 {
+	assert(map);
+
 	PAINTER_BEGIN(&map->picture);
-	draw_layer(map, &map->data->layers[0]);
-	draw_layer(map, &map->data->layers[1]);
+	draw_layer(map, &map->layers[MAP_LAYER_TYPE_BACKGROUND]);
+	draw_layer(map, &map->layers[MAP_LAYER_TYPE_FOREGROUND]);
 	PAINTER_END();
 }
 
 void
+map_state(struct map *map, struct state *state)
+{
+	assert(map);
+	assert(state);
+
+	memset(state, 0, sizeof (*state));
+	state->data = map;
+	state->draw = draw;
+	state->handle = handle;
+	state->update = update;
+}
+
+void
 map_finish(struct map *map)
 {
 	assert(map);
@@ -234,3 +410,4 @@
 
 	memset(map, 0, sizeof (*map));
 }
+
--- a/librpg/rpg/map.h	Mon Nov 09 10:36:39 2020 +0100
+++ b/librpg/rpg/map.h	Mon Nov 09 10:37:36 2020 +0100
@@ -24,37 +24,43 @@
  * \brief Game map.
  */
 
-#include <stdbool.h>
-#include <stdio.h>
+#include <core/texture.h>
+
+#include "walksprite.h"
 
-#include <core/texture.h>
+struct sprite;
+struct state;
+
+union event;
 
 /**
- * \brief Max title length for a map.
+ * \brief Map layer type.
  */
-#define MAP_TITLE_MAX   32
-
-/**
- * \brief Max filename for tilesets.
- */
-#define MAP_TILESET_MAX FILENAME_MAX
+enum map_layer_type {
+	MAP_LAYER_TYPE_BACKGROUND,      /*!< Background layer. */
+	MAP_LAYER_TYPE_FOREGROUND,      /*!< Foreground layer. */
+	MAP_LAYER_TYPE_ABOVE,           /*!< Above foreground layer. */
+	MAP_LAYER_TYPE_NUM              /*!< Number of layers. */
+};
 
 /**
  * \brief Map layer.
  */
 struct map_layer {
-	unsigned short *tiles;          /*!< (+) Array of tiles, depending on the map size. */
+	unsigned short *tiles;          /*!< (+&) Array of tiles, depending on the map size. */
 };
 
 /**
- * \brief Map definition structure.
+ * \brief Map object.
+ *
+ * The map object is used to move a player within the map according to the
+ * tilesets and collisions masks.
  *
- * This structure only defines the map characteristics. It does not have any
- * logic and is left for game state.
+ * By itself, a map does not know how to be loaded from a file and must be done
+ * from an helper like \ref map_file.
  */
-struct map_data {
-	char title[MAP_TITLE_MAX];      /*!< (+) The map title. */
-	char tileset[MAP_TILESET_MAX];  /*!< (+) Name of tileset to use. */
+struct map {
+	const char *title;              /*!< (+) Map title name. */
 	int origin_x;                   /*!< (+) Where the player starts in X. */
 	int origin_y;                   /*!< (+) Where the player starts in Y. */
 	unsigned int real_w;            /*!< (-) Real width in pixels. */
@@ -63,79 +69,76 @@
 	unsigned int h;                 /*!< (-) Map height in cells. */
 	unsigned short tile_w;          /*!< (-) Pixels per cell (width). */
 	unsigned short tile_h;          /*!< (-) Pixels per cell (height). */
-	struct map_layer layers[2];     /*!< (+) Layers (background, foreground). */
-};
+	struct sprite *tileset;         /*!< (+&) Tileset to use. */
+	struct texture picture;         /*!< (-) Map drawn into a texture. */
+
+	/* Player. */
+	struct sprite *player_sprite;   /*!< (+) The sprite to use */
+	struct walksprite player_ws;    /*!< (-) Walking sprite for moving the player. */
+	int player_x;                   /*!< (+) Player position in x */
+	int player_y;                   /*!< (+) Player position in y */
+	int player_angle;               /*!< (+) Player angle (see walksprite) */
+	unsigned int player_movement;   /*!< (*) Current player movements. */
 
-/**
- * \brief High level map object.
- *
- * This structure reference a map and perform drawing operations.
- */
-struct map {
-	struct map_data *data;          /*!< (+&) Map data. */
-	struct texture *tileset;        /*!< (+&) Tileset to use. */
-	struct texture picture;         /*!< (-) Map drawn into a picture. */
+	/* View to zoom/locate. */
+	int view_x;                     /*!< (+) Position in x */
+	int view_y;                     /*!< (+) Position in y */
+	unsigned int view_w;            /*!< (+) View width */
+	unsigned int view_h;            /*!< (+) View height */
+
+	/* View margin. */
+	int margin_x;                   /*!< (+) View margin in x. */
+	int margin_y;                   /*!< (+) View margin in y. */
+	unsigned int margin_w;          /*!< (+) Margin width. */
+	unsigned int margin_h;          /*!< (+) Margin height. */
+
+	/**
+	 * Different tile layers.
+	 */
+	struct map_layer layers[MAP_LAYER_TYPE_NUM];
 };
 
 /**
- * Open a map defintion.
+ * Initialize the map.
  *
- * \pre data != NULL
- * \pre path != NULL
- * \param data the map defintion to fill
- * \param path the path to the map
- * \return True if successfully loaded.
+ * This function will re-generate the terrain and center the view to the player
+ * position.
+ *
+ * \pre map != NULL
+ * \param map the map to initialize
  */
 bool
-map_data_open(struct map_data *data, const char *path);
-
-/**
- * Open map data definition from memory.
- *
- *\pre data != NULL
- *\pre buf != NULL
- *\param data the map definition to fill
- *\param buf the source buffer
- *\param bufsz the source buffer size
- */
-bool
-map_data_openmem(struct map_data *data, const void *buf, size_t bufsz);
+map_init(struct map *map);
 
 /**
- * Dispose the map definition data.
- *
- * \pre data != NULL
- * \param data the map definition
- */
-void
-map_data_finish(struct map_data *data);
-
-/**
- * Initialize this map.
+ * Handle an event.
  *
  * \pre map != NULL
- * \pre data != NULL
- * \pre tileset != NULL && texture_ok(tileset)
- * \param map the map to initialize
- * \param data the definition to reference
- * \param tileset the tileset to use
- * \return False on errors.
+ * \pre ev != NULL
+ * \param map the map
+ * \param ev the event to handle
  */
-bool
-map_init(struct map *map,
-         struct map_data *data,
-         struct texture *tileset);
+void
+map_handle(struct map *map, const union event *ev);
+
+/**
+ * Update the map.
+ *
+ * \pre map != NULL
+ * \param map the map
+ * \param ticks ellapsed milliseconds since last frame
+ */
+void
+map_update(struct map *map, unsigned int ticks);
 
 /**
  * Render a map.
  *
  * \pre map != NULL
  * \param map the map to render
- * \param srcx the x coordinate region
- * \param srcy the y coordinate region
  */
 void
-map_draw(struct map *map, int srcx, int srcy);
+map_draw(const struct map *map);
 
 /**
  * Force map repaint on its texture.
@@ -148,6 +151,23 @@
 map_repaint(struct map *map);
 
 /**
+ * Convert the map into a game state.
+ *
+ * Both objects must exist until the state is no longer used.
+ *
+ * \pre map != NULL
+ * \pre state != NULL
+ * \param map the map to use
+ * \param state the state to fill
+ * \post state->data is set to map
+ * \post state->handle is set
+ * \post state->update is set
+ * \post state->draw is set
+ */
+void
+map_state(struct map *map, struct state *state);
+
+/**
  * Dispose map resources.
  *
  * \pre map != NULL
--- a/librpg/rpg/map_state.c	Mon Nov 09 10:36:39 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,354 +0,0 @@
-/*
- * map_state.c -- state when player is on a map
- *
- * Copyright (c) 2020 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 <stdio.h>
-
-#include <core/event.h>
-#include <core/game.h>
-#include <core/painter.h>
-#include <core/state.h>
-#include <core/texture.h>
-#include <core/window.h>
-
-#include <ui/debug.h>
-
-#include "map.h"
-#include "map_state.h"
-#include "walksprite.h"
-
-/*
- * This is the speed the player moves on the map.
- *
- * SPEED represents the number of pixels it must move per SEC.
- * SEC simply represends the number of milliseconds in one second.
- */
-#define SPEED 100
-#define SEC   1000
-
-/*
- * Those are margins within the edge of the screen. The camera always try to
- * keep those padding between the player and the screen.
- */
-#define MARGIN_WIDTH    80
-#define MARGIN_HEIGHT   80
-
-/*
- * Convenient macros to access the state data.
- */
-#define MAP()           (&map_state_data.map)
-#define PLAYER()        (&map_state_data.player)
-#define VIEW()          (&map_state_data.view)
-
-/*
- * This structure defines the possible movement of the player as flags since
- * it's possible to make diagonal movements.
- */
-enum movement {
-	MOVING_UP       = 1 << 0,
-	MOVING_RIGHT    = 1 << 1,
-	MOVING_DOWN     = 1 << 2,
-	MOVING_LEFT     = 1 << 3
-};
-
-/*
- * A bit of explanation within this array. The structure walksprite requires
- * an orientation between 0-7 depending on the user direction.
- *
- * Since keys for moving the character may be pressed at the same time, we need
- * a conversion table from "key pressed" to "orientation".
- *
- * When an orientation is impossible, it is set to -1. Example, when both left
- * and right are pressed.
- *
- * MOVING_UP    = 0001 = 0x1
- * MOVING_RIGHT = 0010 = 0x2
- * MOVING_DOWN  = 0100 = 0x3
- * MOVING_LEFT  = 1000 = 0x4
- */
-static unsigned int orientations[16] = {
-	[0x1] = 0,
-	[0x2] = 2,
-	[0x3] = 1,
-	[0x4] = 4,
-	[0x6] = 3,
-	[0x8] = 6,
-	[0x9] = 7,
-	[0xC] = 5
-};
-
-/*
- * Additional data that is not necessary to expose in map_state_data.
- */
-static struct {
-	struct {
-		enum movement moving;
-		struct walksprite ws;
-	} player;
-
-	struct {
-		int x;
-		int y;
-		unsigned int w;
-		unsigned int h;
-	} margin;
-} cache;
-
-static void
-center(void)
-{
-	VIEW()->x = PLAYER()->x - (VIEW()->w / 2);
-	VIEW()->y = PLAYER()->y - (VIEW()->h / 2);
-
-	if (VIEW()->x < 0)
-		VIEW()->x = 0;
-	else if ((unsigned int)VIEW()->x > MAP()->data.real_w - VIEW()->w)
-		VIEW()->x = MAP()->data.real_w - VIEW()->w;
-
-	if (VIEW()->y < 0)
-		VIEW()->y = 0;
-	else if ((unsigned int)VIEW()->y > MAP()->data.real_h - VIEW()->h)
-		VIEW()->y = MAP()->data.real_h - VIEW()->h;
-}
-
-static void
-start(struct state *st)
-{
-	(void)st;
-
-	/* Adjust map properties. */
-	struct map *m = &map_state_data.map.map;
-
-	map_repaint(m);
-	MAP()->data.real_w = m->picture.w;
-	MAP()->data.real_h = m->picture.h;
-
-	/* Adjust view. */
-	VIEW()->w = window.w;
-	VIEW()->h = window.h;
-
-	/* Adjust margin. */
-	cache.margin.w = VIEW()->w - (MARGIN_WIDTH * 2);
-	cache.margin.h = VIEW()->h - (MARGIN_HEIGHT * 2);
-
-	/* Center the view by default. */
-	center();
-
-	/* Final bits. */
-	walksprite_init(&cache.player.ws, &PLAYER()->sprite, 300);
-}
-
-static void
-handle_keydown(const union event *event)
-{
-	switch (event->key.key) {
-	case KEY_UP:
-		cache.player.moving |= MOVING_UP;
-		break;
-	case KEY_RIGHT:
-		cache.player.moving |= MOVING_RIGHT;
-		break;
-	case KEY_DOWN:
-		cache.player.moving |= MOVING_DOWN;
-		break;
-	case KEY_LEFT:
-		cache.player.moving |= MOVING_LEFT;
-		break;
-	default:
-		break;
-	}
-
-	PLAYER()->angle = orientations[cache.player.moving];
-}
-
-static void
-handle_keyup(const union event *event)
-{
-	switch (event->key.key) {
-	case KEY_UP:
-		cache.player.moving &= ~(MOVING_UP);
-		break;
-	case KEY_RIGHT:
-		cache.player.moving &= ~(MOVING_RIGHT);
-		break;
-	case KEY_DOWN:
-		cache.player.moving &= ~(MOVING_DOWN);
-		break;
-	case KEY_LEFT:
-		cache.player.moving &= ~(MOVING_LEFT);
-		break;
-	default:
-		break;
-	}
-}
-
-static void
-move_right(unsigned int delta)
-{
-	PLAYER()->x += delta;
-
-	if (PLAYER()->x > (int)(cache.margin.x + cache.margin.w)) {
-		VIEW()->x = (PLAYER()->x - VIEW()->w) + MARGIN_WIDTH;
-
-		if (VIEW()->x >= (int)(MAP()->data.real_w - VIEW()->w))
-			VIEW()->x = MAP()->data.real_w - VIEW()->w;
-	}
-
-	if (PLAYER()->x > (int)MAP()->data.real_w - 48)
-		PLAYER()->x = MAP()->data.real_w - 48;
-}
-
-static void
-move_left(unsigned int delta)
-{
-	PLAYER()->x -= delta;
-
-	if (PLAYER()->x < cache.margin.x) {
-		VIEW()->x = PLAYER()->x - MARGIN_WIDTH;
-
-		if (VIEW()->x < 0)
-			VIEW()->x = 0;
-	}
-
-	if (PLAYER()->x < 0)
-		PLAYER()->x = 0;
-}
-
-static void
-move_down(unsigned int delta)
-{
-	PLAYER()->y += delta;
-
-	if (PLAYER()->y > (int)(cache.margin.y + cache.margin.h)) {
-		VIEW()->y = (PLAYER()->y - VIEW()->h) + MARGIN_HEIGHT;
-
-		if (VIEW()->y >= (int)(MAP()->data.real_h - VIEW()->h))
-			VIEW()->y = MAP()->data.real_h - VIEW()->h;
-	}
-
-	if (PLAYER()->y > (int)MAP()->data.real_h - 48)
-		PLAYER()->y = MAP()->data.real_h - 48;
-}
-
-static void
-move_up(unsigned int delta)
-{
-	PLAYER()->y -= delta;
-
-	if (PLAYER()->y < cache.margin.y) {
-		VIEW()->y = PLAYER()->y - MARGIN_HEIGHT;
-
-		if (VIEW()->y < 0)
-			VIEW()->y = 0;
-	}
-
-	if (PLAYER()->y < 0)
-		PLAYER()->y = 0;
-}
-
-static void
-move(unsigned int ticks)
-{
-	/* This is the amount of pixels the player must move. */
-	const int delta = SPEED * ticks / SEC;
-
-	/* This is the rectangle within the view where users must be. */
-	cache.margin.x = VIEW()->x + MARGIN_WIDTH;
-	cache.margin.y = VIEW()->y + MARGIN_HEIGHT;
-
-	int dx = 0;
-	int dy = 0;
-
-	if (cache.player.moving == 0)
-		return;
-
-	if (cache.player.moving & MOVING_UP)
-		dy = -1;
-	if (cache.player.moving & MOVING_DOWN)
-		dy = 1;
-	if (cache.player.moving & MOVING_LEFT)
-		dx = -1;
-	if (cache.player.moving & MOVING_RIGHT)
-		dx = 1;
-
-	/* Move the player and adjust view if needed. */
-	if (dx > 0)
-		move_right(delta);
-	else if (dx < 0)
-		move_left(delta);
-
-	if (dy > 0)
-		move_down(delta);
-	else if (dy < 0)
-		move_up(delta);
-
-	walksprite_update(&cache.player.ws, ticks);
-}
-
-static void
-handle(struct state *st, const union event *event)
-{
-	(void)st;
-
-	switch (event->type) {
-	case EVENT_KEYDOWN:
-		handle_keydown(event);
-		break;
-	case EVENT_KEYUP:
-		handle_keyup(event);
-		break;
-	default:
-		break;
-	}
-}
-
-static void
-update(struct state *st, unsigned int ticks)
-{
-	(void)st;
-
-	move(ticks);
-}
-
-static void
-draw(struct state *st)
-{
-	(void)st;
-
-	struct debug_report report = {0};
-
-	map_draw(&map_state_data.map.map, VIEW()->x, VIEW()->y);
-	walksprite_draw(
-		&cache.player.ws,
-		PLAYER()->angle,
-		PLAYER()->x - VIEW()->x,
-		PLAYER()->y - VIEW()->y);
-
-	debugf(&report, "position: %d, %d", PLAYER()->x,
-	    PLAYER()->y);
-	debugf(&report, "view: %d, %d", VIEW()->x,
-	    VIEW()->y);
-}
-
-struct map_state_data map_state_data;
-
-struct state map_state = {
-	.start = start,
-	.update = update,
-	.handle = handle,
-	.draw = draw
-};
--- a/librpg/rpg/map_state.h	Mon Nov 09 10:36:39 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-/*
- * map_state.h -- state when player is on a map
- *
- * Copyright (c) 2020 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 MOLKO_MAP_STATE_H
-#define MOLKO_MAP_STATE_H
-
-/**
- * \file map_state.h
- * \brief State when player is on a map.
- * \ingroup states
- */
-
-#include <core/sprite.h>
-
-#include "map.h"
-
-/**
- * \brief Data for the state.
- *
- * Update this structure before switching to this state.
- */
-extern struct map_state_data {
-	/**
-	 * Map properties.
-	 */
-	struct {
-		struct map_data data;   /*!< (+) Map data. */
-		struct map map;         /*!< (+) Map object. */
-	} map;
-
-	/**
-	 * Player position.
-	 *
-	 * If you adjust this structure, it is strictly encouraged to update
-	 * the view as well.
-	 */
-	struct {
-		struct sprite sprite;   /*!< (+) The sprite to use */
-		int x;                  /*!< (+) Player position in x */
-		int y;                  /*!< (+) Player position in y */
-		int angle;              /*!< (+) Player angle (see walksprite) */
-	} player;
-
-	/**
-	 * Position and size of the view.
-	 */
-	struct {
-		int x;                  /*!< (+) Position in x */
-		int y;                  /*!< (+) Position in y */
-		unsigned int w;         /*!< (+) View width */
-		unsigned int h;         /*!< (+) View height */
-	} view;
-} map_state_data; /*!< Access to data. */
-
-/**
- * \brief State when player is on a map.
- */
-extern struct state map_state;
-
-#endif /* !MOLKO_MAP_STATE_H */
--- a/librpg/rpg/walksprite.c	Mon Nov 09 10:36:39 2020 +0100
+++ b/librpg/rpg/walksprite.c	Mon Nov 09 10:37:36 2020 +0100
@@ -52,7 +52,7 @@
 }
 
 void
-walksprite_draw(struct walksprite *ws, unsigned int orientation, int x, int y)
+walksprite_draw(const struct walksprite *ws, unsigned int orientation, int x, int y)
 {
 	assert(ws);
 	assert(orientation < 8);
--- a/librpg/rpg/walksprite.h	Mon Nov 09 10:36:39 2020 +0100
+++ b/librpg/rpg/walksprite.h	Mon Nov 09 10:37:36 2020 +0100
@@ -98,6 +98,6 @@
  * \param y the y coordinate
  */
 void
-walksprite_draw(struct walksprite *ws, unsigned int orientation, int x, int y);
+walksprite_draw(const struct walksprite *ws, unsigned int orientation, int x, int y);
 
 #endif /* !MOLKO_WALKSPRITE_H */
--- a/molko/main.c	Mon Nov 09 10:36:39 2020 +0100
+++ b/molko/main.c	Mon Nov 09 10:37:36 2020 +0100
@@ -16,105 +16,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <setjmp.h>
-#include <stdnoreturn.h>
-
-#include <core/clock.h>
-#include <core/core.h>
-#include <core/event.h>
-#include <core/game.h>
-#include <core/panic.h>
-#include <core/state.h>
-#include <core/sys.h>
-#include <core/util.h>
-#include <core/window.h>
-
-#include <ui/theme.h>
-#include <ui/ui.h>
-
-#include <rpg/rpg.h>
-
-#include <adventure/state/panic.h>
-#include <adventure/state/splashscreen.h>
-#include <adventure/state/mainmenu.h>
-
-#define WINDOW_WIDTH 1280
-#define WINDOW_HEIGHT 720
-
-static struct {
-	struct state splash;
-	struct state mainmenu;
-	struct state panic;
-} states;
-
-static jmp_buf panic_buf;
-
-static noreturn void
-unrecoverable(void)
-{
-	longjmp(panic_buf, 1);
-}
-
-static void
-init(void)
-{
-	if (!core_init() || !ui_init() || !rpg_init())
-		panic();
-	if (!window_open("Molko's Adventure", WINDOW_WIDTH, WINDOW_HEIGHT))
-		panic();
-
-	/*
-	 * From here, we can setup our panic state which requires a window
-	 * to be running.
-	 */
-
-	/* Init unrecoverable panic state. */
-	panic_state(&states.panic);
-	panic_handler = unrecoverable;
-
-	/* Init states. */
-	splashscreen_state(&states.splash, &states.mainmenu);
-	mainmenu_state(&states.mainmenu);
-
-	game_switch(&states.splash, true);
-}
-
-static void
-run(void)
-{
-	struct clock clock = { 0 };
-
-	while (game.state) {
-		unsigned int elapsed = clock_elapsed(&clock);
-
-		clock_start(&clock);
-
-		for (union event ev; event_poll(&ev); ) {
-			switch (ev.type) {
-			case EVENT_QUIT:
-				return;
-			default:
-				game_handle(&ev);
-				break;
-			}
-		}
-
-		game_update(elapsed);
-		game_draw();
-
-		if ((elapsed = clock_elapsed(&clock)) < 20)
-			delay(20 - elapsed);
-	}
-}
-
-static void
-close(void)
-{
-	window_finish();
-	rpg_finish();
-	ui_finish();
-	core_finish();
-}
+#include <adventure/molko.h>
 
 int
 main(int argc, char **argv)
@@ -122,20 +24,9 @@
 	(void)argc;
 	(void)argv;
 
-	if (setjmp(panic_buf) == 0) {
-		/* Initial game run. */
-		init();
-		run();
-	} else {
-		/* Clear event queue to avoid accidental key presses. */
-		for (union event ev; event_poll(&ev); )
-			continue;
-
-		game_switch(&states.panic, true);
-		run();
-	}
-
-	close();
+	molko_init();
+	molko_run();
+	molko_finish();
 
 	return 0;
 }
--- a/tests/CMakeLists.txt	Mon Nov 09 10:36:39 2020 +0100
+++ b/tests/CMakeLists.txt	Mon Nov 09 10:37:36 2020 +0100
@@ -26,13 +26,7 @@
 molko_define_test(
 	TARGET map
 	SOURCES test-map.c
-	ASSETS
-		${tests_SOURCE_DIR}/assets/maps/error-height.map
-		${tests_SOURCE_DIR}/assets/maps/error-tileheight.map
-		${tests_SOURCE_DIR}/assets/maps/error-tilewidth.map
-		${tests_SOURCE_DIR}/assets/maps/error-title.map
-		${tests_SOURCE_DIR}/assets/maps/error-width.map
-		${tests_SOURCE_DIR}/assets/maps/sample-map.map
+	FLAGS DIRECTORY="${tests_SOURCE_DIR}/assets/maps/"
 )
 molko_define_test(TARGET save SOURCES test-save.c)
 molko_define_test(TARGET state SOURCES test-state.c)
Binary file tests/assets/maps/sample-map.png has changed
Binary file tests/assets/tilesets/sample-map.png has changed
--- a/tests/test-map.c	Mon Nov 09 10:36:39 2020 +0100
+++ b/tests/test-map.c	Mon Nov 09 10:37:36 2020 +0100
@@ -19,26 +19,22 @@
 #define GREATEST_USE_ABBREVS 0
 #include <greatest.h>
 
+#include <core/core.h>
 #include <core/error.h>
 #include <core/panic.h>
 #include <core/sys.h>
 #include <core/window.h>
 
+#include <rpg/map-file.h>
 #include <rpg/map.h>
 
-#include <assets/maps/sample-map.h>
-#include <assets/maps/error-title.h>
-#include <assets/maps/error-width.h>
-#include <assets/maps/error-height.h>
-#include <assets/maps/error-tilewidth.h>
-#include <assets/maps/error-tileheight.h>
-
 GREATEST_TEST
 test_sample(void)
 {
-	struct map_data map;
+	struct map_file loader = {0};
+	struct map map = {0};
 
-	GREATEST_ASSERT(map_data_openmem(&map, maps_sample_map, sizeof (maps_sample_map)));
+	GREATEST_ASSERT(map_file_open(&loader, DIRECTORY "sample-map.map", &map));
 	GREATEST_ASSERT_STR_EQ("This is a test map", map.title);
 	GREATEST_ASSERT_EQ(2, map.w);
 	GREATEST_ASSERT_EQ(2, map.h);
@@ -52,51 +48,80 @@
 	GREATEST_ASSERT_EQ(5, map.layers[1].tiles[1]);
 	GREATEST_ASSERT_EQ(6, map.layers[1].tiles[2]);
 	GREATEST_ASSERT_EQ(7, map.layers[1].tiles[3]);
+
+	map_finish(&map);
+	map_file_finish(&loader);
+
 	GREATEST_PASS();
 }
 
 GREATEST_TEST
 test_error_title(void)
 {
-	struct map_data map;
+	struct map_file loader = {0};
+	struct map map = {0};
 
-	GREATEST_ASSERT(!map_data_openmem(&map, maps_error_title, sizeof (maps_error_title)));
+	GREATEST_ASSERT(!map_file_open(&loader, DIRECTORY "error-title.map", &map));
+
+	map_finish(&map);
+	map_file_finish(&loader);
+
 	GREATEST_PASS();
 }
 
 GREATEST_TEST
 test_error_width(void)
 {
-	struct map_data map;
+	struct map_file loader = {0};
+	struct map map = {0};
 
-	GREATEST_ASSERT(!map_data_openmem(&map, maps_error_width, sizeof (maps_error_width)));
+	GREATEST_ASSERT(!map_file_open(&loader, DIRECTORY "error-width.map", &map));
+
+	map_finish(&map);
+	map_file_finish(&loader);
+
 	GREATEST_PASS();
 }
 
 GREATEST_TEST
 test_error_height(void)
 {
-	struct map_data map;
+	struct map_file loader = {0};
+	struct map map = {0};
 
-	GREATEST_ASSERT(!map_data_openmem(&map, maps_error_height, sizeof (maps_error_height)));
+	GREATEST_ASSERT(!map_file_open(&loader, DIRECTORY "error-height.map", &map));
+
+	map_finish(&map);
+	map_file_finish(&loader);
+
 	GREATEST_PASS();
 }
 
 GREATEST_TEST
 test_error_tilewidth(void)
 {
-	struct map_data map;
+	struct map_file loader = {0};
+	struct map map = {0};
 
-	GREATEST_ASSERT(!map_data_openmem(&map, maps_error_tilewidth, sizeof (maps_error_tilewidth)));
+	GREATEST_ASSERT(!map_file_open(&loader, DIRECTORY "error-tilewidth.map", &map));
+
+	map_finish(&map);
+	map_file_finish(&loader);
+
 	GREATEST_PASS();
 }
 
 GREATEST_TEST
 test_error_tileheight(void)
 {
-	struct map_data map;
+	struct map_file loader = {0};
+	struct map map = {0};
 
-	GREATEST_ASSERT(!map_data_openmem(&map, maps_error_tileheight, sizeof (maps_error_tileheight)));
+	GREATEST_ASSERT(!map_file_open(&loader, DIRECTORY "error-tileheight.map", &map));
+
+	map_finish(&map);
+	map_file_finish(&loader);
+
 	GREATEST_PASS();
 }
 
@@ -120,8 +145,18 @@
 main(int argc, char **argv)
 {
 	GREATEST_MAIN_BEGIN();
-	GREATEST_RUN_SUITE(basics);
-	GREATEST_RUN_SUITE(errors);
+
+	/*
+	 * This test opens graphical images and therefore need to initialize a
+	 * window and all of the API. As tests sometime run on headless machine
+	 * we will skip if it fails to initialize.
+	 */
+
+	if (core_init() && window_open("test-map", 100, 100)) {
+		GREATEST_RUN_SUITE(basics);
+		GREATEST_RUN_SUITE(errors);
+	}
+
 	GREATEST_MAIN_END();
 
 	return 0;