changeset 281:87b8c7510717

rpg: implement load/save for characters
author David Demelier <markand@malikania.fr>
date Sun, 20 Dec 2020 10:55:53 +0100
parents 11c824a82e63
children a15f77eda9a4
files extern/libgreatest/CMakeLists.txt libmlk-adventure/CMakeLists.txt libmlk-adventure/adventure/action/chest.c libmlk-adventure/adventure/assets.c libmlk-adventure/adventure/assets.h libmlk-adventure/adventure/dialog/save.c libmlk-adventure/adventure/dialog/save.h libmlk-adventure/adventure/molko.c libmlk-adventure/adventure/state/continue.c libmlk-adventure/adventure/state/continue.h libmlk-adventure/adventure/state/mainmenu.c libmlk-core/CMakeLists.txt libmlk-core/assets/sql/init.sql libmlk-core/assets/sql/property-get.sql libmlk-core/assets/sql/property-remove.sql libmlk-core/assets/sql/property-set.sql libmlk-core/core/event.c libmlk-core/core/event.h libmlk-core/core/save.c libmlk-core/core/save.h libmlk-core/nls/fr.po libmlk-core/nls/libmlk-core.pot libmlk-data/sprites/faces.png libmlk-rpg/CMakeLists.txt libmlk-rpg/assets/sql/character-load.sql libmlk-rpg/assets/sql/character-save.sql libmlk-rpg/assets/sql/init.sql libmlk-rpg/assets/sql/property-get.sql libmlk-rpg/assets/sql/property-remove.sql libmlk-rpg/assets/sql/property-set.sql libmlk-rpg/nls/fr.po libmlk-rpg/nls/libmlk-rpg.pot libmlk-rpg/rpg/character.c libmlk-rpg/rpg/character.h libmlk-rpg/rpg/save.c libmlk-rpg/rpg/save.h libmlk-rpg/rpg/team.c libmlk-rpg/rpg/team.h tests/CMakeLists.txt tests/test-character.c tests/test-save.c
diffstat 41 files changed, 1536 insertions(+), 650 deletions(-) [+]
line wrap: on
line diff
--- a/extern/libgreatest/CMakeLists.txt	Tue Dec 15 22:07:18 2020 +0100
+++ b/extern/libgreatest/CMakeLists.txt	Sun Dec 20 10:55:53 2020 +0100
@@ -22,6 +22,7 @@
 	TARGET libgreatest
 	TYPE INTERFACE
 	SOURCES greatest.h
+	FOLDER extern
 	EXTERNAL
 	INCLUDES INTERFACE $<BUILD_INTERFACE:${libgreatest_SOURCE_DIR}>
 )
--- a/libmlk-adventure/CMakeLists.txt	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-adventure/CMakeLists.txt	Sun Dec 20 10:55:53 2020 +0100
@@ -29,6 +29,8 @@
 	${libadventure_SOURCE_DIR}/adventure/adventure_p.h
 	${libadventure_SOURCE_DIR}/adventure/assets.c
 	${libadventure_SOURCE_DIR}/adventure/assets.h
+	${libadventure_SOURCE_DIR}/adventure/dialog/save.c
+	${libadventure_SOURCE_DIR}/adventure/dialog/save.h
 	${libadventure_SOURCE_DIR}/adventure/mapscene/mapscene.c
 	${libadventure_SOURCE_DIR}/adventure/mapscene/mapscene.h
 	${libadventure_SOURCE_DIR}/adventure/molko.c
@@ -41,6 +43,8 @@
 	${libadventure_SOURCE_DIR}/adventure/state/panic.h
 	${libadventure_SOURCE_DIR}/adventure/state/splashscreen.c
 	${libadventure_SOURCE_DIR}/adventure/state/splashscreen.h
+	${libadventure_SOURCE_DIR}/adventure/state/continue.c
+	${libadventure_SOURCE_DIR}/adventure/state/continue.h
 	${libadventure_SOURCE_DIR}/adventure/trace_hud.c
 	${libadventure_SOURCE_DIR}/adventure/trace_hud.h
 )
--- a/libmlk-adventure/adventure/action/chest.c	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-adventure/adventure/action/chest.c	Sun Dec 20 10:55:53 2020 +0100
@@ -24,11 +24,11 @@
 #include <core/event.h>
 #include <core/maths.h>
 #include <core/panic.h>
-#include <core/save.h>
 #include <core/sound.h>
 #include <core/sprite.h>
 
 #include <rpg/map.h>
+#include <rpg/save.h>
 
 #include "chest.h"
 
--- a/libmlk-adventure/adventure/assets.c	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-adventure/adventure/assets.c	Sun Dec 20 10:55:53 2020 +0100
@@ -37,7 +37,8 @@
 	struct sprite sprite;
 } table_sprites[] = {
 	SPRITE(ASSETS_SPRITE_UI_CURSOR, "sprites/ui-cursor.png", 24, 24),
-	SPRITE(ASSETS_SPRITE_CHEST, "sprites/chest.png", 32, 32)
+	SPRITE(ASSETS_SPRITE_CHEST, "sprites/chest.png", 32, 32),
+	SPRITE(ASSETS_SPRITE_FACES, "sprites/faces.png", 144, 144)
 };
 
 struct sprite *assets_sprites[ASSETS_SPRITE_NUM] = {0};
--- a/libmlk-adventure/adventure/assets.h	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-adventure/adventure/assets.h	Sun Dec 20 10:55:53 2020 +0100
@@ -28,6 +28,9 @@
 	/* Actions. */
 	ASSETS_SPRITE_CHEST,
 
+	/* Team assets. */
+	ASSETS_SPRITE_FACES,
+
 	ASSETS_SPRITE_NUM
 };
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-adventure/adventure/dialog/save.c	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,302 @@
+/*
+ * save.c -- select a save slot
+ *
+ * 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 <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <compat.h>
+
+#include <core/event.h>
+#include <core/font.h>
+#include <core/maths.h>
+#include <core/painter.h>
+#include <core/sprite.h>
+#include <core/window.h>
+
+#include <ui/align.h>
+#include <ui/label.h>
+#include <ui/frame.h>
+#include <ui/theme.h>
+
+#include <adventure/adventure_p.h>
+#include <adventure/assets.h>
+
+#include "save.h"
+
+#define THEME(s)        ((s)->theme ? (s)->theme : theme_default())
+#define LINES_MAX       (3)
+
+/* TODO: require a module for this*/
+#define TEAM_MAX        (4)
+
+struct geo {
+	int x, y;
+	unsigned int w, h;
+
+	/* Per line height/padding. */
+	unsigned int lh;
+	unsigned int lp;
+
+	struct {
+		int x, y;
+		unsigned int w, h;
+
+		/* Lines start positions. */
+		int lx, ly;
+
+		struct {
+			int x, y;
+			unsigned int w, h;
+		} faces[TEAM_MAX];
+	} saves[DIALOG_SAVE_MAX];
+};
+
+static struct geo
+geometry(const struct dialog_save *dlg)
+{
+	struct geo geo = {
+		.w = window.w - THEME(dlg)->padding * 40,
+		.h = window.h - THEME(dlg)->padding * 10,
+	};
+
+	align(ALIGN_CENTER, &geo.x, &geo.y, geo.w, geo.h, 0, 0, window.w, window.h);
+
+	for (size_t i = 0; i < DIALOG_SAVE_MAX; ++i) {
+		const unsigned int padding = THEME(dlg)->padding;
+
+		geo.saves[i].w  = (geo.w -  padding * 2);
+		geo.saves[i].h  = (geo.h - (padding * (DIALOG_SAVE_MAX + 1))) / DIALOG_SAVE_MAX;
+		geo.saves[i].x  = (geo.x +  padding);
+		geo.saves[i].y  = (geo.y +  padding) + i * (padding + geo.saves[i].h);
+		geo.saves[i].lx = (geo.saves[i].x + padding);
+
+		/* Compute lines padding (we draw LINES_MAX lines). */
+		geo.lh = font_height(THEME(dlg)->fonts[THEME_FONT_INTERFACE]);
+		geo.lp = (geo.saves[i].h - (geo.lh * LINES_MAX)) / (LINES_MAX + 1);
+
+		/* Compute faces position. */
+		for (size_t f = 0; f < TEAM_MAX; ++f) {
+			geo.saves[i].faces[f].h = geo.saves[i].h - padding * 2;
+			geo.saves[i].faces[f].w = geo.saves[i].faces[f].h;
+			geo.saves[i].faces[f].x = (geo.saves[i].x + padding) + (f * (padding + geo.saves[i].faces[f].w));
+			geo.saves[i].faces[f].y = (geo.saves[i].y + padding);
+			geo.saves[i].lx += geo.saves[i].faces[f].w + padding;
+		}
+
+		geo.saves[i].ly = geo.saves[i].y + geo.lp;
+	}
+
+	return geo;
+}
+
+static void
+draw_frame(const struct geo *geo)
+{
+	struct frame f = {
+		.x = geo->x,
+		.y = geo->y,
+		.w = geo->w,
+		.h = geo->h,
+	};
+
+	frame_draw(&f);
+}
+
+static void
+draw_save_box(const struct geo *geo, size_t i)
+{
+	/* TODO: change colors at some point. */
+	painter_set_color(0x884b2bff);
+	painter_draw_rectangle(geo->saves[i].x, geo->saves[i].y, geo->saves[i].w, geo->saves[i].h);
+}
+
+static void
+draw_save_faces(const struct dialog_save *dlg, const struct geo *geo, size_t i)
+{
+	/* TODO: determine face. */
+	for (size_t f = 0; f < TEAM_MAX; ++f) {
+		sprite_scale(assets_sprites[ASSETS_SPRITE_FACES], 0, f,
+		    geo->saves[i].faces[f].x,
+		    geo->saves[i].faces[f].y,
+		    geo->saves[i].faces[f].w,
+		    geo->saves[i].faces[f].h
+		);
+	}
+}
+
+static void
+draw_save_times(const struct dialog_save *dlg, const struct geo *geo, size_t i)
+{
+	struct label label = {0};
+	char time[128], line[256];
+	struct save_property prop;
+
+	label.theme = dlg->theme;
+	label.x = geo->saves[i].lx;
+	label.y = geo->saves[i].ly;
+	label.flags = LABEL_FLAGS_SHADOW;
+	label.text = line;
+
+	/* TODO: Get map position. */
+	strlcpy(line, "World", sizeof (line));
+	label_draw(&label);
+	
+	/* Last time. */
+	strftime(time, sizeof (time), "%c", localtime(&dlg->saves[i].updated));
+	snprintf(line, sizeof (line), _("Last played: %s"), time);
+
+	label.y += geo->lp + geo->lh;
+	label_draw(&label);
+
+	/* TODO: Time played. */
+	snprintf(line, sizeof (line), _("Time played: %s"), "100 hours");
+
+	label.y += geo->lp + geo->lh;
+	label_draw(&label);
+}
+
+static void
+draw_save(const struct dialog_save *dlg, const struct geo *geo, size_t i)
+{
+	draw_save_box(geo, i);
+
+	/* Do not draw the content if save is invalid. */
+	if (!save_ok(&dlg->saves[i]))
+		return;
+
+	draw_save_faces(dlg, geo, i);
+	draw_save_times(dlg, geo, i);
+}
+
+static void
+draw_saves(const struct dialog_save *dlg, const struct geo *geo)
+{
+	for (size_t i = 0; i < DIALOG_SAVE_MAX; ++i)
+		draw_save(dlg, geo, i);
+}
+
+static void
+draw_cursor(const struct dialog_save *dlg, const struct geo *geo)
+{
+	const struct sprite *sprite =assets_sprites[ASSETS_SPRITE_UI_CURSOR];
+	const int x = geo->saves[dlg->selected].x - sprite->cellw;
+	const int y = geo->saves[dlg->selected].y;
+
+	sprite_draw(sprite, 1, 2, x, y + (geo->saves[dlg->selected].h / 2) - (sprite->cellh / 2));
+}
+
+static bool
+handle_keydown(struct dialog_save *s, const struct event_key *key)
+{
+	assert(key->type == EVENT_KEYDOWN);
+
+	switch (key->key) {
+	case KEY_UP:
+		if (s->selected == 0)
+			s->selected = DIALOG_SAVE_MAX - 1;
+		else
+			s->selected --;
+		break;
+	case KEY_DOWN:
+		if (s->selected + 1 >= DIALOG_SAVE_MAX)
+			s->selected = 0;
+		else
+			s->selected ++;
+		break;
+	case KEY_ENTER:
+		return save_ok(&s->saves[s->selected]);
+	default:
+		break;
+	}
+
+	return false;
+}
+
+static bool
+handle_clickdown(struct dialog_save *s, const struct geo *geo, const struct event_click *clk)
+{
+	assert(clk->type == EVENT_CLICKDOWN);
+
+	for (size_t i = 0; i < DIALOG_SAVE_MAX; ++i) {
+		if (maths_is_boxed(geo->saves[i].x, geo->saves[i].y,
+		                   geo->saves[i].w, geo->saves[i].h,
+		                   clk->x, clk->y)) {
+			s->selected = i;
+			break;
+		}
+	}
+
+	return clk->clicks >= 2 && save_ok(&s->saves[s->selected]);
+}
+
+void
+dialog_save_init(struct dialog_save *s)
+{
+	assert(s);
+
+	for (size_t i = 0; i < DIALOG_SAVE_MAX; ++i)
+		save_open(&s->saves[i], i, SAVE_MODE_READ);
+}
+
+bool
+dialog_save_handle(struct dialog_save *dlg, const union event *ev)
+{
+	assert(dlg);
+	assert(ev);
+
+	const struct geo geo = geometry(dlg);
+
+	switch (ev->type) {
+	case EVENT_KEYDOWN:
+		return handle_keydown(dlg, &ev->key);
+	case EVENT_CLICKDOWN:
+		return handle_clickdown(dlg, &geo, &ev->click);
+	default:
+		break;
+	}
+
+	return false;
+}
+
+void
+dialog_save_update(struct dialog_save *dlg, unsigned int ticks)
+{
+	assert(dlg);
+}
+
+void
+dialog_save_draw(const struct dialog_save *dlg)
+{
+	assert(dlg);
+
+	const struct geo geo = geometry(dlg);
+
+	draw_frame(&geo);
+	draw_saves(dlg, &geo);
+	draw_cursor(dlg, &geo);
+}
+
+void
+dialog_save_finish(struct dialog_save *dlg)
+{
+	assert(dlg);
+
+	for (size_t i = 0; i < DIALOG_SAVE_MAX; ++i)
+		save_finish(&dlg->saves[i]);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-adventure/adventure/dialog/save.h	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,53 @@
+/*
+ * save.h -- select a save slot
+ *
+ * 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_ADVENTURE_SAVE_H
+#define MOLKO_ADVENTURE_SAVE_H
+
+#include <stdbool.h>
+
+#include <rpg/save.h>
+
+#define DIALOG_SAVE_MAX (6)
+
+union event;
+
+struct theme;
+
+struct dialog_save {
+	const struct theme *theme;
+	struct save saves[DIALOG_SAVE_MAX];
+	size_t selected;
+};
+
+void
+dialog_save_init(struct dialog_save *);
+
+bool
+dialog_save_handle(struct dialog_save *, const union event *);
+
+void
+dialog_save_update(struct dialog_save *, unsigned int);
+
+void
+dialog_save_draw(const struct dialog_save *);
+
+void
+dialog_save_finish(struct dialog_save *);
+
+#endif /* !MOLKO_ADVENTURE_SAVE_H */
--- a/libmlk-adventure/adventure/molko.c	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-adventure/adventure/molko.c	Sun Dec 20 10:55:53 2020 +0100
@@ -86,7 +86,12 @@
 	assets_init();
 
 	/* Start to splash. */
+#if 0
+	// TODO: put back this.
 	game_switch(splashscreen_state_new(), true);
+#else
+	game_switch(mainmenu_state_new(), true);
+#endif
 }
 
 void
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-adventure/adventure/state/continue.c	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,109 @@
+/*
+ * continue.c -- select save to continue the game
+ *
+ * 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 <core/alloc.h>
+#include <core/event.h>
+#include <core/game.h>
+#include <core/painter.h>
+#include <core/state.h>
+
+#include <adventure/dialog/save.h>
+
+#include "mainmenu.h"
+#include "continue.h"
+
+struct self {
+	struct state state;
+	struct dialog_save dialog;
+};
+
+static void
+start(struct state *state)
+{
+	struct self *self = state->data;
+
+	dialog_save_init(&self->dialog);
+}
+
+static void
+handle(struct state *state, const union event *ev)
+{
+	struct self *self = state->data;
+	bool selected = false;
+
+	switch (ev->type) {
+	case EVENT_QUIT:
+		game_quit();
+		break;
+	case EVENT_KEYDOWN:
+		if (ev->key.key == KEY_ESCAPE)
+			game_switch(mainmenu_state_new(), false);
+		else
+			selected = dialog_save_handle(&self->dialog, ev);
+		break;
+	default:
+		selected = dialog_save_handle(&self->dialog, ev);
+		break;
+	}
+
+	if (selected)
+		game_quit();
+}
+
+static void
+update(struct state *state, unsigned int ticks)
+{
+	struct self *self = state->data;
+
+	dialog_save_update(&self->dialog, ticks);
+}
+
+static void
+draw(struct state *state)
+{
+	struct self *self = state->data;
+
+	painter_set_color(0xffffffff);
+	painter_clear();
+	dialog_save_draw(&self->dialog);
+	painter_present();
+}
+
+static void
+finish(struct state *state)
+{
+	struct self *self = state->data;
+
+	dialog_save_finish(&self->dialog);
+}
+
+struct state *
+continue_state_new(void)
+{
+	struct self *self;
+
+	self = alloc_new0(sizeof (*self));
+	self->state.data = self;
+	self->state.start = start;
+	self->state.handle = handle;
+	self->state.update = update;
+	self->state.draw = draw;
+	self->state.finish = finish;
+
+	return &self->state;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-adventure/adventure/state/continue.h	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,27 @@
+/*
+ * continue.h -- select save to continue the game
+ *
+ * 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_ADVENTURE_CONTINUE_H
+#define MOLKO_ADVENTURE_CONTINUE_H
+
+struct state;
+
+struct state *
+continue_state_new(void);
+
+#endif /* !MOLKO_ADVENTURE_CONTINUE_H */
--- a/libmlk-adventure/adventure/state/mainmenu.c	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-adventure/adventure/state/mainmenu.c	Sun Dec 20 10:55:53 2020 +0100
@@ -41,6 +41,7 @@
 #include <adventure/adventure_p.h>
 
 #include "mainmenu.h"
+#include "continue.h"
 
 struct self {
 	struct state state;
@@ -63,7 +64,7 @@
 static void
 resume(void)
 {
-	/* TODO: implement here. */
+	game_switch(continue_state_new(), false);
 }
 
 static void
--- a/libmlk-core/CMakeLists.txt	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-core/CMakeLists.txt	Sun Dec 20 10:55:53 2020 +0100
@@ -21,14 +21,6 @@
 include(CheckLibraryExists)
 
 set(
-	ASSETS
-	${libmlk-core_SOURCE_DIR}/assets/sql/init.sql
-	${libmlk-core_SOURCE_DIR}/assets/sql/property-get.sql
-	${libmlk-core_SOURCE_DIR}/assets/sql/property-remove.sql
-	${libmlk-core_SOURCE_DIR}/assets/sql/property-set.sql
-)
-
-set(
 	PO
 	${libmlk-core_SOURCE_DIR}/nls/fr.po
 )
@@ -70,8 +62,6 @@
 	${libmlk-core_SOURCE_DIR}/core/painter.h
 	${libmlk-core_SOURCE_DIR}/core/panic.c
 	${libmlk-core_SOURCE_DIR}/core/panic.h
-	${libmlk-core_SOURCE_DIR}/core/save.c
-	${libmlk-core_SOURCE_DIR}/core/save.h
 	${libmlk-core_SOURCE_DIR}/core/script.c
 	${libmlk-core_SOURCE_DIR}/core/script.h
 	${libmlk-core_SOURCE_DIR}/core/sound.c
@@ -112,7 +102,6 @@
 molko_define_library(
 	TARGET libmlk-core
 	SOURCES ${SOURCES} ${ASSETS} ${PO}
-	ASSETS ${ASSETS}
 	TRANSLATIONS fr
 	LIBRARIES
 		PUBLIC
--- a/libmlk-core/assets/sql/init.sql	Tue Dec 15 22:07:18 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
---
--- init.sql -- initialize database
---
--- 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.
---
-
-BEGIN EXCLUSIVE TRANSACTION;
-
-CREATE TABLE IF NOT EXISTS property(
-	id      INTEGER PRIMARY KEY AUTOINCREMENT,
-	key     TEXT NOT NULL UNIQUE,
-	value   TEXT NOT NULL
-);
-
-INSERT OR IGNORE INTO property(key, value) VALUES ('molko.create-date', strftime('%s','now'));
-INSERT OR IGNORE INTO property(key, value) VALUES ('molko.update-date', strftime('%s','now'));
-
-COMMIT;
--- a/libmlk-core/assets/sql/property-get.sql	Tue Dec 15 22:07:18 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
---
--- property-get.sql -- get a property
---
--- 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.
---
-
-SELECT value
-  FROM property
- WHERE key = ?
--- a/libmlk-core/assets/sql/property-remove.sql	Tue Dec 15 22:07:18 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
---
--- property-remove.sql -- remove a property
---
--- 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.
---
-
-DELETE
-  FROM property
- WHERE key = ?
--- a/libmlk-core/assets/sql/property-set.sql	Tue Dec 15 22:07:18 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
---
--- property-set.sql -- set a property
---
--- 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.
---
-
-INSERT OR REPLACE INTO property(
-	key,
-	value
-)
-VALUES(
-	?,
-	?
-)
--- a/libmlk-core/core/event.c	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-core/core/event.c	Sun Dec 20 10:55:53 2020 +0100
@@ -212,6 +212,7 @@
 	ev->click.button = MOUSE_BUTTON_NONE;
 	ev->click.x = event->button.x;
 	ev->click.y = event->button.y;
+	ev->click.clicks = event->button.clicks;
 
 	for (size_t i = 0; buttons[i].value != MOUSE_BUTTON_NONE; ++i) {
 		if (buttons[i].key == event->button.button) {
--- a/libmlk-core/core/event.h	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-core/core/event.h	Sun Dec 20 10:55:53 2020 +0100
@@ -50,6 +50,7 @@
 	enum mouse_button button;
 	int x;
 	int y;
+	unsigned int clicks;
 };
 
 union event {
--- a/libmlk-core/core/save.c	Tue Dec 15 22:07:18 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,256 +0,0 @@
-/*
- * save.c -- save functions
- *
- * 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 <compat.h>
-
-#include <assert.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include <sqlite3.h>
-
-#include <assets/sql/init.h>
-#include <assets/sql/property-get.h>
-#include <assets/sql/property-remove.h>
-#include <assets/sql/property-set.h>
-
-#include "core_p.h"
-#include "error.h"
-#include "save.h"
-#include "sys.h"
-#include "util.h"
-
-#define SQL_BEGIN       "BEGIN EXCLUSIVE TRANSACTION"
-#define SQL_COMMIT      "COMMIT"
-#define SQL_ROLLBACK    "ROLLBACK"
-
-static bool
-exec(struct save *db, const char *sql)
-{
-	if (sqlite3_exec(db->handle, sql, NULL, NULL, NULL) != SQLITE_OK)
-		return errorf("%s", sqlite3_errmsg(db->handle));
-
-	return true;
-}
-
-static const char *
-path(unsigned int idx)
-{
-	return util_pathf("%s%u.db", sys_dir(SYS_DIR_SAVE), idx);
-}
-
-static bool
-execu(struct save *db, const unsigned char *sql)
-{
-	return exec(db, (const char *)sql);
-}
-
-static bool
-verify(struct save *db)
-{
-	struct {
-		time_t *date;
-		struct save_property prop;
-	} table[] = {
-		{ .date = &db->created, { .key = "molko.create-date" } },
-		{ .date = &db->updated, { .key = "molko.update-date" } },
-	};
-
-	/* Ensure create and update dates are present. */
-	for (size_t i = 0; i < UTIL_SIZE(table); ++i) {
-		if (!save_get_property(db, &table[i].prop)) {
-			sqlite3_close(db->handle);
-			return errorf(_("database not initialized correctly"));
-		}
-
-		*table[i].date = strtoull(table[i].prop.value, NULL, 10);
-	}
-
-	return true;
-}
-
-bool
-save_open(struct save *db, unsigned int idx, enum save_mode mode)
-{
-	assert(db);
-
-	return save_open_path(db, path(idx), mode);
-}
-
-bool
-save_open_path(struct save *db, const char *path, enum save_mode mode)
-{
-	assert(db);
-	assert(path);
-
-	int flags = 0;
-
-	switch (mode) {
-	case SAVE_MODE_WRITE:
-		flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
-		break;
-	default:
-		flags = SQLITE_OPEN_READONLY;
-		break;
-	}
-
-	if (sqlite3_open_v2(path, (sqlite3**)&db->handle, flags, NULL) != SQLITE_OK)
-		goto sqlite3_err;
-
-	if (mode == SAVE_MODE_WRITE && !execu(db, sql_init))
-		goto sqlite3_err;
-
-	return verify(db);
-
-sqlite3_err:
-	errorf("%s", sqlite3_errmsg(db->handle));
-	sqlite3_close(db->handle);
-
-	memset(db, 0, sizeof (*db));
-
-	return false;
-}
-
-bool
-save_ok(const struct save *db)
-{
-	assert(db);
-
-	return db && db->handle;
-}
-
-bool
-save_set_property(struct save *db, const struct save_property *prop)
-{
-	assert(db);
-	assert(prop);
-
-	sqlite3_stmt *stmt = NULL;
-
-	if (!exec(db, SQL_BEGIN))
-		return false;
-	if (sqlite3_prepare(db->handle, (const char *)sql_property_set, -1, &stmt, NULL) != SQLITE_OK)
-		goto sqlite3_err;
-	if (sqlite3_bind_text(stmt, 1, prop->key, -1, NULL) != SQLITE_OK ||
-	    sqlite3_bind_text(stmt, 2, prop->value, -1, NULL) != SQLITE_OK)
-		goto sqlite3_err;
-	if (sqlite3_step(stmt) != SQLITE_DONE)
-		goto sqlite3_err;
-
-	sqlite3_finalize(stmt);
-
-	return exec(db, SQL_COMMIT);
-
-sqlite3_err:
-	errorf("%s", sqlite3_errmsg(db->handle));
-
-	if (stmt)
-		sqlite3_finalize(stmt);
-
-	exec(db, SQL_ROLLBACK);
-
-	return false;
-}
-
-bool
-save_get_property(struct save *db, struct save_property *prop)
-{
-	assert(db);
-	assert(prop);
-
-	sqlite3_stmt *stmt = NULL;
-	bool ret = true;
-
-	if (sqlite3_prepare(db->handle, (const char *)sql_property_get,
-	    sizeof (sql_property_get), &stmt, NULL) != SQLITE_OK)
-		goto sqlite3_err;
-	if (sqlite3_bind_text(stmt, 1, prop->key, -1, NULL) != SQLITE_OK)
-		goto sqlite3_err;
-
-	switch (sqlite3_step(stmt)) {
-	case SQLITE_DONE:
-		/* Not found. */
-		ret = errorf(_("property '%s' was not found"), prop->key);
-		break;
-	case SQLITE_ROW:
-		/* Found. */
-		strlcpy(prop->value, (const char *)sqlite3_column_text(stmt, 0),
-		    sizeof (prop->value));
-		break;
-	default:
-		/* Error. */
-		goto sqlite3_err;
-	}
-
-	sqlite3_finalize(stmt);
-
-	return ret;
-
-sqlite3_err:
-	errorf("%s", sqlite3_errmsg(db->handle));
-
-	if (stmt)
-		sqlite3_finalize(stmt);
-
-	return false;
-}
-
-bool
-save_remove_property(struct save *db, const struct save_property *prop)
-{
-	assert(db);
-	assert(prop);
-
-	sqlite3_stmt *stmt = NULL;
-
-	if (!exec(db, SQL_BEGIN))
-		return false;
-	if (sqlite3_prepare(db->handle, (const char *)sql_property_remove,
-	    sizeof (sql_property_remove), &stmt, NULL) != SQLITE_OK)
-		goto sqlite3_err;
-	if (sqlite3_bind_text(stmt, 1, prop->key, -1, NULL) != SQLITE_OK)
-		goto sqlite3_err;
-	if (sqlite3_step(stmt) != SQLITE_DONE)
-		goto sqlite3_err;
-
-	sqlite3_finalize(stmt);
-
-	return exec(db, SQL_COMMIT);
-
-sqlite3_err:
-	errorf("%s", sqlite3_errmsg(db->handle));
-
-	if (stmt)
-		sqlite3_finalize(stmt);
-
-	exec(db, SQL_ROLLBACK);
-
-	return false;
-}
-
-void
-save_finish(struct save *db)
-{
-	assert(db);
-
-	if (db->handle)
-		sqlite3_close(db->handle);
-
-	memset(db, 0, sizeof (*db));
-}
--- a/libmlk-core/core/save.h	Tue Dec 15 22:07:18 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-/*
- * save.h -- save functions
- *
- * 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_CORE_SAVE_H
-#define MOLKO_CORE_SAVE_H
-
-#include <stdbool.h>
-#include <time.h>
-
-#define SAVE_PROPERTY_KEY_MAX   (64)
-#define SAVE_PROPERTY_VALUE_MAX (1024)
-
-struct save {
-	time_t created;
-	time_t updated;
-	void *handle;
-};
-
-enum save_mode {
-	SAVE_MODE_READ,
-	SAVE_MODE_WRITE
-};
-
-struct save_property {
-	char key[SAVE_PROPERTY_KEY_MAX + 1];
-	char value[SAVE_PROPERTY_VALUE_MAX + 1];
-};
-
-bool
-save_open(struct save *db, unsigned int idx, enum save_mode mode);
-
-bool
-save_open_path(struct save *db, const char *path, enum save_mode mode);
-
-bool
-save_ok(const struct save *db);
-
-bool
-save_set_property(struct save *db, const struct save_property *prop);
-
-bool
-save_get_property(struct save *db, struct save_property *prop);
-
-bool
-save_remove_property(struct save *db, const struct save_property *prop);
-
-void
-save_finish(struct save *db);
-
-#endif /* !MOLKO_CORE_SAVE_H */
--- a/libmlk-core/nls/fr.po	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-core/nls/fr.po	Sun Dec 20 10:55:53 2020 +0100
@@ -8,7 +8,7 @@
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-11-28 22:34+0100\n"
+"POT-Creation-Date: 2020-12-20 10:24+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -27,11 +27,9 @@
 msgid "abort: panic handler returned\n"
 msgstr "fatal: la fonction de panique n'aurait pas du continuer\n"
 
-#: /Users/markand/Dev/molko/libmlk-core/core/save.c:85
-msgid "database not initialized correctly"
-msgstr "database non initialisée"
+#~ msgid "database not initialized correctly"
+#~ msgstr "database non initialisée"
 
-#: /Users/markand/Dev/molko/libmlk-core/core/save.c:175
 #, c-format
-msgid "property '%s' was not found"
-msgstr "propriété '%s' non trouvée"
+#~ msgid "property '%s' was not found"
+#~ msgstr "propriété '%s' non trouvée"
--- a/libmlk-core/nls/libmlk-core.pot	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-core/nls/libmlk-core.pot	Sun Dec 20 10:55:53 2020 +0100
@@ -8,7 +8,7 @@
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-11-30 09:42+0100\n"
+"POT-Creation-Date: 2020-12-20 10:24+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -26,12 +26,3 @@
 #, c-format
 msgid "abort: panic handler returned\n"
 msgstr ""
-
-#: /Users/markand/Dev/molko/libmlk-core/core/save.c:85
-msgid "database not initialized correctly"
-msgstr ""
-
-#: /Users/markand/Dev/molko/libmlk-core/core/save.c:175
-#, c-format
-msgid "property '%s' was not found"
-msgstr ""
Binary file libmlk-data/sprites/faces.png has changed
--- a/libmlk-rpg/CMakeLists.txt	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-rpg/CMakeLists.txt	Sun Dec 20 10:55:53 2020 +0100
@@ -19,55 +19,69 @@
 project(libmlk-rpg)
 
 set(
+	ASSETS
+	${libmlk-rpg_SOURCE_DIR}/assets/sql/character-save.sql
+	${libmlk-rpg_SOURCE_DIR}/assets/sql/init.sql
+	${libmlk-rpg_SOURCE_DIR}/assets/sql/property-get.sql
+	${libmlk-rpg_SOURCE_DIR}/assets/sql/property-remove.sql
+	${libmlk-rpg_SOURCE_DIR}/assets/sql/property-set.sql
+	${libmlk-rpg_SOURCE_DIR}/assets/sql/character-load.sql
+)
+
+set(
 	SOURCES
-	${libmlk-rpg_SOURCE_DIR}/rpg/battle.c
-	${libmlk-rpg_SOURCE_DIR}/rpg/battle.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-bar.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-bar.h
-	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity.c
-	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity.h
-	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity-state.c
-	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity-state.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity-state-attacking.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity-state-blinking.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity-state-moving.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity-state-normal.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity-state.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity-state.h
+	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-indicator.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-indicator.h
-	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state.c
-	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-ai.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-attacking.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-check.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-closing.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-lost.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-menu.c
-	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-lost.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-opening.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-selection.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-sub.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-victory.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state.h
+	${libmlk-rpg_SOURCE_DIR}/rpg/battle.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/battle.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/character.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/character.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/equipment.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/equipment.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/item.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/item.h
+	${libmlk-rpg_SOURCE_DIR}/rpg/map-file.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/map-file.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/map.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/map.h
-	${libmlk-rpg_SOURCE_DIR}/rpg/map-file.c
-	${libmlk-rpg_SOURCE_DIR}/rpg/map-file.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/message.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/message.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/rpg.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/rpg.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/rpg_p.h
+	${libmlk-rpg_SOURCE_DIR}/rpg/save.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/save.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/selection.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/spell.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/spell.h
+	${libmlk-rpg_SOURCE_DIR}/rpg/team.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/team.h
+	${libmlk-rpg_SOURCE_DIR}/rpg/tileset-file.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/tileset-file.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/tileset.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/tileset.h
-	${libmlk-rpg_SOURCE_DIR}/rpg/tileset-file.c
-	${libmlk-rpg_SOURCE_DIR}/rpg/tileset-file.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/walksprite.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/walksprite.h
 )
@@ -79,8 +93,9 @@
 
 molko_define_library(
 	TARGET libmlk-rpg
+	ASSETS ${ASSETS}
 	TRANSLATIONS fr
-	SOURCES ${SOURCES} ${PO}
+	SOURCES ${SOURCES} ${PO} ${ASSETS}
 	LIBRARIES
 		PUBLIC
 			libmlk-core
@@ -91,6 +106,7 @@
 	INCLUDES
 		PUBLIC
 			$<BUILD_INTERFACE:${libmlk-rpg_SOURCE_DIR}>
+			$<BUILD_INTERFACE:${libmlk-rpg_BINARY_DIR}>
 )
 
 source_group(TREE ${libmlk-rpg_SOURCE_DIR} FILES ${SOURCES})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/assets/sql/character-load.sql	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,30 @@
+--
+-- character-load.sql -- load a character data
+--
+-- 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.
+--
+
+SELECT hp
+     , mp
+     , level
+     , team_order
+     , bonus_hp
+     , bonus_mp
+     , bonus_atk
+     , bonus_def
+     , bonus_agt
+     , bonus_luck
+  FROM character
+ WHERE name = ?
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/assets/sql/character-save.sql	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,44 @@
+--
+-- character-save.sql -- save or replace character
+--
+-- 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.
+--
+
+INSERT OR REPLACE INTO character(
+	name,
+	hp,
+	mp,
+	level,
+	team_order,
+	bonus_hp,
+	bonus_mp,
+	bonus_atk,
+	bonus_def,
+	bonus_agt,
+	bonus_luck
+)
+VALUES(
+	?,
+	?,
+	?,
+	?,
+	?,
+	?,
+	?,
+	?,
+	?,
+	?,
+	?
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/assets/sql/init.sql	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,44 @@
+--
+-- init.sql -- initialize database
+--
+-- 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.
+--
+
+BEGIN EXCLUSIVE TRANSACTION;
+
+CREATE TABLE IF NOT EXISTS property(
+	id              INTEGER PRIMARY KEY AUTOINCREMENT,
+	key             TEXT NOT NULL UNIQUE,
+	value           TEXT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS character(
+	name            TEXT PRIMARY KEY,
+	hp              INTEGER NOT NULL,
+	mp              INTEGER NOT NULL,
+	level           INTEGER NOT NULL,
+	team_order      INTEGER DEFAULT -1,
+	bonus_hp        INTEGER DEFAULT 0,
+	bonus_mp        INTEGER DEFAULT 0,
+	bonus_atk       INTEGER DEFAULT 0,
+	bonus_def       INTEGER DEFAULT 0,
+	bonus_agt       INTEGER DEFAULT 0,
+	bonus_luck      INTEGER DEFAULT 0
+);
+
+INSERT OR IGNORE INTO property(key, value) VALUES ('molko.create-date', strftime('%s','now'));
+INSERT OR IGNORE INTO property(key, value) VALUES ('molko.update-date', strftime('%s','now'));
+
+COMMIT;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/assets/sql/property-get.sql	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,21 @@
+--
+-- property-get.sql -- get a property
+--
+-- 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.
+--
+
+SELECT value
+  FROM property
+ WHERE key = ?
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/assets/sql/property-remove.sql	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,21 @@
+--
+-- property-remove.sql -- remove a property
+--
+-- 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.
+--
+
+DELETE
+  FROM property
+ WHERE key = ?
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/assets/sql/property-set.sql	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,26 @@
+--
+-- property-set.sql -- set a property
+--
+-- 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.
+--
+
+INSERT OR REPLACE INTO property(
+	key,
+	value
+)
+VALUES(
+	?,
+	?
+)
--- a/libmlk-rpg/nls/fr.po	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-rpg/nls/fr.po	Sun Dec 20 10:55:53 2020 +0100
@@ -8,7 +8,7 @@
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-11-30 09:48+0100\n"
+"POT-Creation-Date: 2020-12-20 10:24+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -33,33 +33,37 @@
 msgid "Special"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/battle-state-victory.c:88
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/battle-state-victory.c:86
 msgid "Victory!"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/battle-state-lost.c:87
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/battle-state-lost.c:85
 msgid "You have been defeated..."
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:241
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:240
 msgid "could not parse image"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:130
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:148
 msgid "could not parse tileset"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:92
-#, c-format
-msgid "ignoring action %d,%d,%u,%u,%s"
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/save.c:80
+msgid "database not initialized correctly"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:61
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:90
+#, c-format
+msgid "ignoring action %d,%d,%u,%u,%d,%s"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:59
 #, c-format
 msgid "invalid layer type: %s"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:175
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:193
 msgid "invalid origin"
 msgstr ""
 
@@ -81,54 +85,59 @@
 msgid "message width too small: %u < %u"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:236
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:254
 msgid "missing background layer"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:238
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:256
 msgid "missing foreground layer"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:114
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:132
 msgid "missing layer type definition"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:110
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:128
 msgid "missing map dimensions before layer"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:239
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:238
 msgid "missing tile dimensions before image"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:240
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:258
 msgid "missing tileset"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:298
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:297
 msgid "missing tileset image"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:229
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:247
 msgid "missing title"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:157
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:175
 msgid "null map columns"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:166
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:184
 msgid "null map rows"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:146
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:164
 msgid "null map title"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:142
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/save.c:265
+#, c-format
+msgid "property '%s' was not found"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:141
 msgid "tileheight is null"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:133
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:132
 msgid "tilewidth is null"
 msgstr ""
--- a/libmlk-rpg/nls/libmlk-rpg.pot	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-rpg/nls/libmlk-rpg.pot	Sun Dec 20 10:55:53 2020 +0100
@@ -8,7 +8,7 @@
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-11-30 09:48+0100\n"
+"POT-Creation-Date: 2020-12-20 10:24+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -33,33 +33,37 @@
 msgid "Special"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/battle-state-victory.c:88
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/battle-state-victory.c:86
 msgid "Victory!"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/battle-state-lost.c:87
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/battle-state-lost.c:85
 msgid "You have been defeated..."
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:241
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:240
 msgid "could not parse image"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:130
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:148
 msgid "could not parse tileset"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:92
-#, c-format
-msgid "ignoring action %d,%d,%u,%u,%s"
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/save.c:80
+msgid "database not initialized correctly"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:61
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:90
+#, c-format
+msgid "ignoring action %d,%d,%u,%u,%d,%s"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:59
 #, c-format
 msgid "invalid layer type: %s"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:175
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:193
 msgid "invalid origin"
 msgstr ""
 
@@ -81,54 +85,59 @@
 msgid "message width too small: %u < %u"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:236
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:254
 msgid "missing background layer"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:238
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:256
 msgid "missing foreground layer"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:114
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:132
 msgid "missing layer type definition"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:110
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:128
 msgid "missing map dimensions before layer"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:239
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:238
 msgid "missing tile dimensions before image"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:240
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:258
 msgid "missing tileset"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:298
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:297
 msgid "missing tileset image"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:229
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:247
 msgid "missing title"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:157
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:175
 msgid "null map columns"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:166
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:184
 msgid "null map rows"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:146
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/map-file.c:164
 msgid "null map title"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:142
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/save.c:265
+#, c-format
+msgid "property '%s' was not found"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:141
 msgid "tileheight is null"
 msgstr ""
 
-#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:133
+#: /Users/markand/Dev/molko/libmlk-rpg/rpg/tileset-file.c:132
 msgid "tilewidth is null"
 msgstr ""
--- a/libmlk-rpg/rpg/character.c	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-rpg/rpg/character.c	Sun Dec 20 10:55:53 2020 +0100
@@ -20,11 +20,15 @@
 
 #include <core/sprite.h>
 
+#include <assets/sql/character-save.h>
+#include <assets/sql/character-load.h>
+
 #include "character.h"
 #include "equipment.h"
+#include "save.h"
 
 bool
-character_ok(struct character *ch)
+character_ok(const struct character *ch)
 {
 	return ch && ch->name && ch->type && ch->reset && sprite_ok(ch->sprites[CHARACTER_SPRITE_NORMAL]);
 }
@@ -65,3 +69,54 @@
 	if (ch->exec)
 		ch->exec(ch, bt);
 }
+
+bool
+character_save(const struct character *ch, struct save *s)
+{
+	assert(ch);
+	assert(save_ok(s));
+
+	return save_exec(s, (const char *)sql_character_save, "s iii i iiiiii",
+	    ch->name,
+	    ch->hp,
+	    ch->mp,
+	    ch->level,
+	    ch->team_order,
+	    ch->hpbonus,
+	    ch->mpbonus,
+	    ch->atkbonus,
+	    ch->defbonus,
+	    ch->agtbonus,
+	    ch->luckbonus
+	);
+}
+
+bool
+character_load(struct character *ch, struct save *s)
+{
+	assert(ch);
+	assert(save_ok(s));
+
+	struct save_stmt stmt;
+	bool ret;
+
+	if (!save_stmt_init(s, &stmt, (const char *)sql_character_load, "s", ch->name))
+		return false;
+
+	ret = save_stmt_next(&stmt, "iii i iiiiii",
+	    &ch->hp,
+	    &ch->mp,
+	    &ch->level,
+	    &ch->team_order,
+	    &ch->hpbonus,
+	    &ch->mpbonus,
+	    &ch->atkbonus,
+	    &ch->defbonus,
+	    &ch->agtbonus,
+	    &ch->luckbonus
+	);
+
+	save_stmt_finish(&stmt);
+
+	return ret;
+}
--- a/libmlk-rpg/rpg/character.h	Tue Dec 15 22:07:18 2020 +0100
+++ b/libmlk-rpg/rpg/character.h	Sun Dec 20 10:55:53 2020 +0100
@@ -19,175 +19,88 @@
 #ifndef MOLKO_RPG_CHARACTER_H
 #define MOLKO_RPG_CHARACTER_H
 
-/**
- * \file character.h
- * \brief Character definition.
- */
-
 #include <stdbool.h>
 
+#define CHARACTER_SPELL_MAX (64)
+
 struct battle;
+struct save;
 struct sprite;
 struct spell;
 
-/**
- * \brief Maximum number of spells in a character.
- */
-#define CHARACTER_SPELL_MAX (64)
-
-/**
- * \brief Character status
- */
 enum character_status {
-	CHARACTER_STATUS_NORMAL,                        /*!< No status. */
-	CHARACTER_STATUS_POISON         = (1 << 0)      /*!< Character is poisoned. */
+	CHARACTER_STATUS_NORMAL,
+	CHARACTER_STATUS_POISON         = (1 << 0)
 };
 
-/**
- * \brief Sprites per action.
- *
- * This enumeration should be synced with \ref equipment_type.
- */
 enum character_sprite {
-	CHARACTER_SPRITE_AXE,           /*!< Attacking with axe. */
-	CHARACTER_SPRITE_BOW,           /*!< Attacking with bow. */
-	CHARACTER_SPRITE_CROSSBOW,      /*!< Attacking with crossbow. */
-	CHARACTER_SPRITE_DAGGER,        /*!< Attacking with dagger. */
-	CHARACTER_SPRITE_HAMMER,        /*!< Attacking with hammer. */
-	CHARACTER_SPRITE_NORMAL,        /*!< Sprite for walking. */
-	CHARACTER_SPRITE_SPIKE,         /*!< Attacking with spike. */
-	CHARACTER_SPRITE_SWORD,         /*!< Attacking with sword. */
-	CHARACTER_SPRITE_WAND,          /*!< Attacking with wand. */
-	CHARACTER_SPRITE_NUM            /*!< Total number of sprites. */
-};
-
-/**
- * \brief Equipment per character.
- *
- * This enumeration should be synced with \ref equipment_type.
- */
-enum character_equipment {
-	CHARACTER_EQUIPMENT_GLOVES,     /*!< Gloves equiped. */
-	CHARACTER_EQUIPMENT_HELMET,     /*!< Helmet equiped. */
-	CHARACTER_EQUIPMENT_SHIELD,     /*!< Shield equiped. */
-	CHARACTER_EQUIPMENT_TOP,        /*!< Top equiped. */
-	CHARACTER_EQUIPMENT_TROUSERS,   /*!< Trousers equiped. */
-	CHARACTER_EQUIPMENT_WEAPON,     /*!< Weapon equiped. */
-	CHARACTER_EQUIPMENT_NUM         /*!< Total number of equipments equiped. */
+	CHARACTER_SPRITE_AXE,
+	CHARACTER_SPRITE_BOW,
+	CHARACTER_SPRITE_CROSSBOW,
+	CHARACTER_SPRITE_DAGGER,
+	CHARACTER_SPRITE_HAMMER,
+	CHARACTER_SPRITE_NORMAL,
+	CHARACTER_SPRITE_SPIKE,
+	CHARACTER_SPRITE_SWORD,
+	CHARACTER_SPRITE_WAND,
+	CHARACTER_SPRITE_NUM
 };
 
-/**
- * \brief Character object
- *
- * This structure owns the current character statistics used in battle.
- */
+enum character_equipment {
+	CHARACTER_EQUIPMENT_GLOVES,
+	CHARACTER_EQUIPMENT_HELMET,
+	CHARACTER_EQUIPMENT_SHIELD,
+	CHARACTER_EQUIPMENT_TOP,
+	CHARACTER_EQUIPMENT_TROUSERS,
+	CHARACTER_EQUIPMENT_WEAPON,
+	CHARACTER_EQUIPMENT_NUM
+};
+
 struct character {
-	const char *name;               /*!< (+) Character name. */
-	const char *type;               /*!< (+) Type or class name. */
-	unsigned int level;             /*!< (+) Character level. */
-	enum character_status status;   /*!< (+) Character status. */
-	int hp;                         /*!< (+) Heal points. */
-	unsigned int hpmax;             /*!< (+) Maximum heal points. */
-	unsigned int hpbonus;           /*!< (+) User heal points bonus. */
-	int mp;                         /*!< (+) Magic points. */
-	unsigned int mpmax;             /*!< (+) Maximum magic points. */
-	unsigned int mpbonus;           /*!< (+) User magic points bonus. */
-	int atk;                        /*!< (+) Current attack points (increase fire based spells too). */
-	unsigned int atkbonus;          /*!< (+) User attack bonus. */
-	int def;                        /*!< (+) Current defense points (increase earth based spells too). */
-	unsigned int defbonus;          /*!< (+) User defense bonus. */
-	int agt;                        /*!< (+) Current agility (increase wind based spells too). */
-	unsigned int agtbonus;          /*!< (+) User agility bonus. */
-	int luck;                       /*!< (+) Current luck points (increase */
-	unsigned int luckbonus;         /*!< (+) User luck bonus. */
+	const char *name;
+	const char *type;
+	unsigned int level;
+	enum character_status status;
+	int hp;
+	unsigned int hpmax;
+	unsigned int hpbonus;
+	int mp;
+	unsigned int mpmax;
+	unsigned int mpbonus;
+	int atk;
+	unsigned int atkbonus;
+	int def;
+	unsigned int defbonus;
+	int agt;
+	unsigned int agtbonus;
+	int luck;
+	unsigned int luckbonus;
+	unsigned int team_order;
 
-	/**
-	 * (+&) Sprites to use.
-	 */
 	struct sprite *sprites[CHARACTER_SPRITE_NUM];
-
-	/**
-	 * (+&) Equipments for this character.
-	 */
 	const struct equipment *equipments[CHARACTER_EQUIPMENT_NUM];
-
-	/**
-	 * (+&?) List of spells for this character.
-	 */
 	const struct spell *spells[CHARACTER_SPELL_MAX];
 
-	/**
-	 * (+) Reset statistics from this character class.
-	 *
-	 * This function must reset the following member variables according
-	 * to the class characteristics:
-	 *
-	 * - hpmax
-	 * - mpmax
-	 * - atk
-	 * - def
-	 * - agt
-	 * - luck
-	 *
-	 * \param owner this owner
-	 */
 	void (*reset)(struct character *owner);
-
-	/**
-	 * (+?) Execute an action.
-	 *
-	 * This function should be present for AI enemies in a battle, it should
-	 * be kept NULL for team players unless they have automatic actions
-	 * which in that case would skip user input.
-	 *
-	 * \param owner this owner
-	 * \param bt the battle object
-	 */
 	void (*exec)(struct character *owner, struct battle *bt);
 };
 
-/**
- * Check if this is a valid character object.
- *
- * \pre ch != NULL
- * \param ch the character object
- */
 bool
-character_ok(struct character *ch);
+character_ok(const struct character *ch);
 
-/**
- * Get a string name for the given status.
- *
- * Since status is a bitmask you have to select only one status.
- *
- * \pre status must be valid
- * \param status the status
- * \return A const string.
- */
 const char *
 character_status_string(enum character_status status);
 
-/**
- * Shortcut for ch->reset.
- *
- * This function is usually called after an equipment change, a level change
- * or and of a battle.
- *
- * \pre ch != NULL
- * \param ch the character object
- */
 void
 character_reset(struct character *ch);
 
-/**
- * Shortcut for ch->exec (if not NULL)
- *
- * \pre character_ok(ch)
- * \pre bt != NULL
- * \param ch the character
- * \param bt the battle object
- */
 void
 character_exec(struct character *ch, struct battle *bt);
 
+bool
+character_save(const struct character *ch, struct save *s);
+
+bool
+character_load(struct character *, struct save *);
+
 #endif /* !MOLKO_RPG_CHARACTER_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/rpg/save.c	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,408 @@
+/*
+ * save.c -- save functions
+ *
+ * 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 <compat.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sqlite3.h>
+
+#include <core/error.h>
+#include <core/sys.h>
+#include <core/util.h>
+
+#include <assets/sql/init.h>
+#include <assets/sql/property-get.h>
+#include <assets/sql/property-remove.h>
+#include <assets/sql/property-set.h>
+
+#include "rpg_p.h"
+#include "save.h"
+
+#define SQL_BEGIN       "BEGIN EXCLUSIVE TRANSACTION"
+#define SQL_COMMIT      "COMMIT"
+#define SQL_ROLLBACK    "ROLLBACK"
+
+static bool
+exec(struct save *db, const char *sql)
+{
+	if (sqlite3_exec(db->handle, sql, NULL, NULL, NULL) != SQLITE_OK)
+		return errorf("%s", sqlite3_errmsg(db->handle));
+
+	return true;
+}
+
+static const char *
+path(unsigned int idx)
+{
+	return util_pathf("%s%u.db", sys_dir(SYS_DIR_SAVE), idx);
+}
+
+static bool
+execu(struct save *db, const unsigned char *sql)
+{
+	return exec(db, (const char *)sql);
+}
+
+static bool
+verify(struct save *db)
+{
+	struct {
+		time_t *date;
+		struct save_property prop;
+	} table[] = {
+		{ .date = &db->created, { .key = "molko.create-date" } },
+		{ .date = &db->updated, { .key = "molko.update-date" } },
+	};
+
+	/* Ensure create and update dates are present. */
+	for (size_t i = 0; i < UTIL_SIZE(table); ++i) {
+		if (!save_get_property(db, &table[i].prop)) {
+			sqlite3_close(db->handle);
+			return errorf(_("database not initialized correctly"));
+		}
+
+		*table[i].date = strtoull(table[i].prop.value, NULL, 10);
+	}
+
+	return true;
+}
+
+static bool
+prepare(struct save *s, struct save_stmt *stmt, const char *sql, const char *args, va_list ap)
+{
+	stmt->parent = s;
+	stmt->handle = NULL;
+
+	if (sqlite3_prepare(s->handle, sql, -1, (sqlite3_stmt **)&stmt->handle, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+
+	for (int i = 1; args && *args; ++args) {
+		switch (*args) {
+		case 'i':
+		case 'u':
+			if (sqlite3_bind_int(stmt->handle, i++, va_arg(ap, int)) != SQLITE_OK)
+				return false;
+			break;
+		case 's':
+			if (sqlite3_bind_text(stmt->handle, i++, va_arg(ap, const char *), -1, NULL) != SQLITE_OK)
+				return false;
+			break;
+		case 't':
+			if (sqlite3_bind_int64(stmt->handle, i++, va_arg(ap, time_t)) != SQLITE_OK)
+				return false;
+			break;
+		case ' ':
+			break;
+		default:
+			return errorf("invalid format: %c", *args);
+		}
+	}
+
+	return true;
+
+sqlite3_err:
+	return errorf("%s", sqlite3_errmsg(s->handle));
+}
+
+static bool
+extract(struct save_stmt *stmt, const char *args, va_list ap)
+{
+	const int ncols = sqlite3_column_count(stmt->handle);
+
+	for (int c = 0; args && *args; ++args) {
+		if (c >= ncols)
+			return errorf("too many arguments");
+
+		/* TODO: type check. */
+		switch (*args) {
+		case 'i':
+		case 'u':
+			*va_arg(ap, int *) = sqlite3_column_int(stmt->handle, c++);
+			break;
+		case 's': {
+			char *str = va_arg(ap, char *);
+			size_t max = va_arg(ap, size_t);
+
+			strlcpy(str, (const char *)sqlite3_column_text(stmt->handle, c++), max);
+			break;
+		}
+		case 't':
+			*va_arg(ap, time_t *) = sqlite3_column_int64(stmt->handle, c++);
+			break;
+		case ' ':
+			break;
+		default:
+			return errorf("invalid format: %c", *args);
+		}
+	}
+
+	return true;
+
+sqlite3_err:
+	return errorf("%s", sqlite3_errmsg(stmt->parent->handle));
+}
+
+bool
+save_open(struct save *db, unsigned int idx, enum save_mode mode)
+{
+	assert(db);
+
+	return save_open_path(db, path(idx), mode);
+}
+
+bool
+save_open_path(struct save *db, const char *path, enum save_mode mode)
+{
+	assert(db);
+	assert(path);
+
+	int flags = 0;
+
+	switch (mode) {
+	case SAVE_MODE_WRITE:
+		flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
+		break;
+	default:
+		flags = SQLITE_OPEN_READONLY;
+		break;
+	}
+
+	if (sqlite3_open_v2(path, (sqlite3**)&db->handle, flags, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+
+	if (mode == SAVE_MODE_WRITE && !execu(db, sql_init))
+		goto sqlite3_err;
+
+	return verify(db);
+
+sqlite3_err:
+	errorf("%s", sqlite3_errmsg(db->handle));
+	sqlite3_close(db->handle);
+
+	memset(db, 0, sizeof (*db));
+
+	return false;
+}
+
+bool
+save_ok(const struct save *db)
+{
+	assert(db);
+
+	return db && db->handle;
+}
+
+bool
+save_set_property(struct save *db, const struct save_property *prop)
+{
+	assert(db);
+	assert(prop);
+
+	sqlite3_stmt *stmt = NULL;
+
+	if (!exec(db, SQL_BEGIN))
+		return false;
+	if (sqlite3_prepare(db->handle, (const char *)sql_property_set, -1, &stmt, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+	if (sqlite3_bind_text(stmt, 1, prop->key, -1, NULL) != SQLITE_OK ||
+	    sqlite3_bind_text(stmt, 2, prop->value, -1, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+	if (sqlite3_step(stmt) != SQLITE_DONE)
+		goto sqlite3_err;
+
+	sqlite3_finalize(stmt);
+
+	return exec(db, SQL_COMMIT);
+
+sqlite3_err:
+	errorf("%s", sqlite3_errmsg(db->handle));
+
+	if (stmt)
+		sqlite3_finalize(stmt);
+
+	exec(db, SQL_ROLLBACK);
+
+	return false;
+}
+
+bool
+save_get_property(struct save *db, struct save_property *prop)
+{
+	assert(db);
+	assert(prop);
+
+	sqlite3_stmt *stmt = NULL;
+	bool ret = true;
+
+	if (sqlite3_prepare(db->handle, (const char *)sql_property_get,
+	    sizeof (sql_property_get), &stmt, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+	if (sqlite3_bind_text(stmt, 1, prop->key, -1, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+
+	switch (sqlite3_step(stmt)) {
+	case SQLITE_DONE:
+		/* Not found. */
+		ret = errorf(_("property '%s' was not found"), prop->key);
+		break;
+	case SQLITE_ROW:
+		/* Found. */
+		strlcpy(prop->value, (const char *)sqlite3_column_text(stmt, 0),
+		    sizeof (prop->value));
+		break;
+	default:
+		/* Error. */
+		goto sqlite3_err;
+	}
+
+	sqlite3_finalize(stmt);
+
+	return ret;
+
+sqlite3_err:
+	errorf("%s", sqlite3_errmsg(db->handle));
+
+	if (stmt)
+		sqlite3_finalize(stmt);
+
+	return false;
+}
+
+bool
+save_remove_property(struct save *db, const struct save_property *prop)
+{
+	assert(db);
+	assert(prop);
+
+	sqlite3_stmt *stmt = NULL;
+
+	if (!exec(db, SQL_BEGIN))
+		return false;
+	if (sqlite3_prepare(db->handle, (const char *)sql_property_remove,
+	    sizeof (sql_property_remove), &stmt, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+	if (sqlite3_bind_text(stmt, 1, prop->key, -1, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+	if (sqlite3_step(stmt) != SQLITE_DONE)
+		goto sqlite3_err;
+
+	sqlite3_finalize(stmt);
+
+	return exec(db, SQL_COMMIT);
+
+sqlite3_err:
+	errorf("%s", sqlite3_errmsg(db->handle));
+
+	if (stmt)
+		sqlite3_finalize(stmt);
+
+	exec(db, SQL_ROLLBACK);
+
+	return false;
+}
+
+bool
+save_exec(struct save *db, const char *sql, const char *args, ...)
+{
+	assert(save_ok(db));
+	assert(sql && args);
+
+	struct save_stmt stmt;
+	bool ret;
+	va_list ap;
+
+	va_start(ap, args);
+	ret = prepare(db, &stmt, sql, args, ap);
+	va_end(ap);
+
+	if (!ret)
+		return false;
+
+	ret = save_stmt_next(&stmt, NULL) == 0;
+	save_stmt_finish(&stmt);
+
+	return ret;
+}
+
+void
+save_finish(struct save *db)
+{
+	assert(db);
+
+	if (db->handle)
+		sqlite3_close(db->handle);
+
+	memset(db, 0, sizeof (*db));
+}
+
+bool
+save_stmt_init(struct save *db, struct save_stmt *stmt, const char *sql, const char *args, ...)
+{
+	assert(save_ok(db));
+	assert(stmt);
+	assert(args);
+
+	va_list ap;
+	bool ret;
+
+	va_start(ap, args);
+	ret = prepare(db, stmt, sql, args, ap);
+	va_end(ap);
+
+	return ret;
+}
+
+int
+save_stmt_next(struct save_stmt *stmt, const char *args, ...)
+{
+	assert(stmt);
+
+	va_list ap;
+	bool ret = -1;
+
+	switch (sqlite3_step(stmt->handle)) {
+	case SQLITE_ROW:
+		va_start(ap, args);
+
+		if (extract(stmt, args, ap))
+			ret = 1;
+
+		va_end(ap);
+		break;
+	case SQLITE_DONE:
+		ret = 0;
+		break;
+	default:
+		break;
+	}
+
+	return ret;
+}
+
+void
+save_stmt_finish(struct save_stmt *stmt)
+{
+	assert(stmt);
+
+	sqlite3_finalize(stmt->handle);
+	memset(stmt, 0, sizeof (*stmt));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/rpg/save.h	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,83 @@
+/*
+ * save.h -- save functions
+ *
+ * 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_RPG_SAVE_H
+#define MOLKO_RPG_SAVE_H
+
+#include <stdbool.h>
+#include <time.h>
+
+#define SAVE_PROPERTY_KEY_MAX   (64)
+#define SAVE_PROPERTY_VALUE_MAX (1024)
+
+struct save {
+	time_t created;
+	time_t updated;
+	void *handle;
+};
+
+enum save_mode {
+	SAVE_MODE_READ,
+	SAVE_MODE_WRITE
+};
+
+struct save_property {
+	char key[SAVE_PROPERTY_KEY_MAX + 1];
+	char value[SAVE_PROPERTY_VALUE_MAX + 1];
+};
+
+struct save_stmt {
+	struct save *parent;
+	void *handle;
+};
+
+bool
+save_open(struct save *db, unsigned int idx, enum save_mode mode);
+
+bool
+save_open_path(struct save *db, const char *path, enum save_mode mode);
+
+bool
+save_ok(const struct save *db);
+
+bool
+save_set_property(struct save *db, const struct save_property *prop);
+
+bool
+save_get_property(struct save *db, struct save_property *prop);
+
+bool
+save_remove_property(struct save *db, const struct save_property *prop);
+
+bool
+save_exec(struct save *db, const char *sql, const char *args, ...);
+
+void
+save_finish(struct save *db);
+
+/* Prepared statements. */
+bool
+save_stmt_init(struct save *db, struct save_stmt *stmt, const char *sql, const char *args, ...);
+
+int
+save_stmt_next(struct save_stmt *stmt, const char *args, ...);
+
+void
+save_stmt_finish(struct save_stmt *stmt);
+
+#endif /* !MOLKO_RPG_SAVE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/rpg/team.c	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,19 @@
+/*
+ * team.c -- team storage
+ *
+ * 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.
+ */
+
+/* Nothing yet. */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/rpg/team.h	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,30 @@
+/*
+ * team.h -- team storage
+ *
+ * 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_RPG_TEAM_H
+#define MOLKO_RPG_TEAM_H
+
+#define TEAM_MEMBER_MAX (4)
+
+struct character;
+
+struct team {
+	struct character *members[TEAM_MEMBER_MAX];
+};
+
+#endif /* MOLKO_RPG_TEAM_H */
--- a/tests/CMakeLists.txt	Tue Dec 15 22:07:18 2020 +0100
+++ b/tests/CMakeLists.txt	Sun Dec 20 10:55:53 2020 +0100
@@ -21,6 +21,7 @@
 molko_define_test(TARGET action SOURCES test-action.c)
 molko_define_test(TARGET action-script SOURCES test-action-script.c)
 molko_define_test(TARGET alloc SOURCES test-alloc.c)
+molko_define_test(TARGET character SOURCES test-character.c)
 molko_define_test(TARGET color SOURCES test-color.c)
 molko_define_test(TARGET error SOURCES test-error.c)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-character.c	Sun Dec 20 10:55:53 2020 +0100
@@ -0,0 +1,90 @@
+/*
+ * test-character.c -- test character routines
+ *
+ * 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 <string.h>
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+#include <rpg/character.h>
+#include <rpg/save.h>
+
+static void
+clean(void *data)
+{
+	(void)data;
+
+	remove("test.db");
+}
+
+GREATEST_TEST
+test_save_simple(void)
+{
+	struct save db;
+	struct character ch = {
+		.name = "david",
+		.hp = 1989,
+		.mp = 1,
+		.level = 18,
+		.team_order = 1,
+		.hpbonus = 500,
+		.mpbonus = 50,
+		.atkbonus = 1001,
+		.defbonus = 1002,
+		.agtbonus = 1003,
+		.luckbonus = 1004
+	};
+
+	GREATEST_ASSERT(save_open_path(&db, "test.db", SAVE_MODE_WRITE));
+	GREATEST_ASSERT(character_save(&ch, &db));
+
+	/* Restore. */
+	memset(&ch, 0, sizeof (ch));
+	ch.name = "david";
+
+	GREATEST_ASSERT(character_load(&ch, &db));
+	GREATEST_ASSERT_EQ(1989, ch.hp);
+	GREATEST_ASSERT_EQ(1, ch.mp);
+	GREATEST_ASSERT_EQ(18, ch.level);
+	GREATEST_ASSERT_EQ(1, ch.team_order);
+	GREATEST_ASSERT_EQ(500, ch.hpbonus);
+	GREATEST_ASSERT_EQ(50, ch.mpbonus);
+	GREATEST_ASSERT_EQ(1001, ch.atkbonus);
+	GREATEST_ASSERT_EQ(1002, ch.defbonus);
+	GREATEST_ASSERT_EQ(1003, ch.agtbonus);
+	GREATEST_ASSERT_EQ(1004, ch.luckbonus);
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_save)
+{
+	GREATEST_SET_SETUP_CB(clean, NULL);
+	GREATEST_SET_TEARDOWN_CB(clean, NULL);
+	GREATEST_RUN_TEST(test_save_simple);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_save);
+	GREATEST_MAIN_END();
+}
--- a/tests/test-save.c	Tue Dec 15 22:07:18 2020 +0100
+++ b/tests/test-save.c	Sun Dec 20 10:55:53 2020 +0100
@@ -21,7 +21,7 @@
 #define GREATEST_USE_ABBREVS 0
 #include <greatest.h>
 
-#include <core/save.h>
+#include <rpg/save.h>
 
 static void
 clean(void *data)