changeset 84:a6c2067709ce

core: implement basic save routines, closes #2476 @2h
author David Demelier <markand@malikania.fr>
date Sat, 07 Mar 2020 17:06:01 +0100
parents f5d3e469eb93
children 34e91215ec5a
files .hgignore INSTALL.md Makefile src/core/save.c src/core/save.h src/core/sys.c src/core/sys.h tests/test-save.c
diffstat 8 files changed, 419 insertions(+), 10 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Sun Feb 16 18:27:42 2020 +0100
+++ b/.hgignore	Sat Mar 07 17:06:01 2020 +0100
@@ -17,6 +17,7 @@
 ^tests/test-error(\.exe)?$
 ^tests/test-map(\.exe)?$
 ^tests/test-script(\.exe)?$
+^tests/test-save(\.exe)?$
 ^tools/molko-map(\.exe)?$
 
 # doxygen stuff.
--- a/INSTALL.md	Sun Feb 16 18:27:42 2020 +0100
+++ b/INSTALL.md	Sat Mar 07 17:06:01 2020 +0100
@@ -6,8 +6,8 @@
 Requirements
 ------------
 
-- C99 compliant compiler,
-- POSIX system (make, ar, shell),
+- C11 compliant compiler,
+- POSIX system (make, ar, shell, some POSIX functions),
 - [Jansson][], JSON parsing library,
 - [SDL2][], Multimedia library,
 - [SDL2_image][], Image loading addon for SDL2,
--- a/Makefile	Sun Feb 16 18:27:42 2020 +0100
+++ b/Makefile	Sat Mar 07 17:06:01 2020 +0100
@@ -45,6 +45,7 @@
                 src/core/map_state.c \
                 src/core/message.c \
                 src/core/painter.c \
+                src/core/save.c \
                 src/core/script.c \
                 src/core/sprite.c \
                 src/core/sys.c \
@@ -75,6 +76,7 @@
 TESTS=          tests/test-color.c \
                 tests/test-error.c \
                 tests/test-map.c \
+                tests/test-save.c \
                 tests/test-script.c
 TESTS_INCS=     -I extern/libgreatest -I src/core ${SDL_CFLAGS}
 TESTS_LIBS=     ${LIB} ${SDL_LDFLAGS} ${LDFLAGS}
@@ -88,7 +90,7 @@
 DEFINES=        -DPREFIX=\""${PREFIX}"\" \
                 -DBINDIR=\""${BINDIR}"\" \
                 -DSHAREDIR=\""${SHAREDIR}"\"
-INCLUDES=       -I src/core -I src/adventure
+INCLUDES=       -I extern/libsqlite -I src/core -I src/adventure
 
 .SUFFIXES:
 .SUFFIXES: .c .o
@@ -101,7 +103,7 @@
 	${CC} ${DEFINES} ${INCLUDES} ${SDL_CFLAGS} ${CFLAGS} -MMD -c $< -o $@
 
 .c:
-	${CC} ${TESTS_INCS} -o $@ ${CFLAGS} $< ${TESTS_LIBS}
+	${CC} ${TESTS_INCS} -o $@ ${CFLAGS} $< ${TESTS_LIBS} ${SQLITE_LIB}
 
 ${SQLITE_OBJ}: ${SQLITE_SRC}
 	${CC} ${CFLAGS} ${SQLITE_FLAGS} -c ${SQLITE_SRC} -o $@
@@ -110,10 +112,10 @@
 	${AR} -rc $@ ${SQLITE_OBJ}
 
 ${LIB}: ${CORE_OBJS}
-	${AR} -rcs $@ ${CORE_OBJS}
+	${AR} -rc $@ ${CORE_OBJS}
 
 ${PROG}: ${LIB} ${ADV_OBJS} ${SQLITE_LIB}
-	${CC} -o $@ ${ADV_OBJS} ${LIB} ${SDL_LDFLAGS} ${LDFLAGS}
+	${CC} -o $@ ${ADV_OBJS} ${LIB} ${SQLITE_LIB} ${SDL_LDFLAGS} ${LDFLAGS}
 
 ${TESTS_OBJS}: ${LIB}
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/save.c	Sat Mar 07 17:06:01 2020 +0100
@@ -0,0 +1,214 @@
+/*
+ * 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 <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <sqlite3.h>
+
+#include "error.h"
+#include "save.h"
+#include "sys.h"
+
+static sqlite3 *db;
+
+static const char *sinit =
+	"BEGIN EXCLUSIVE TRANSACTION;"
+	""
+	"CREATE TABLE IF NOT EXISTS property("
+	"  id INTEGER PRIMARY KEY AUTOINCREMENT,"
+	"  key TEXT NOT NULL UNIQUE,"
+	"  value TEXT NOT NULL"
+	");"
+	""
+	"COMMIT"
+;
+
+static const char *sbegin =
+	"BEGIN EXCLUSIVE TRANSACTION"
+;
+
+static const char *scommit =
+	"COMMIT"
+;
+
+static const char *srollback =
+	"ROLLBACK"
+;
+
+static const char *sset_property =
+	"INSERT OR REPLACE INTO property("
+	"  key,"
+	"  value"
+	")"
+	"VALUES("
+	"  ?,"
+	"  ?"
+	");"
+;
+
+static const char *sget_property =
+	"SELECT value"
+	"  FROM property"
+	" WHERE key = ?"
+;
+
+static const char *sremove_property =
+	"DELETE"
+	"  FROM property"
+	" WHERE key = ?"
+;
+
+static bool
+exec(const char *sql)
+{
+	if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK)
+		return error_printf("%s", sqlite3_errmsg(db));
+
+	return true;
+}
+
+bool
+save_open(unsigned int idx)
+{
+	return save_open_path(sys_savepath(idx));
+}
+
+bool
+save_open_path(const char *path)
+{
+	assert(path);
+
+	if (sqlite3_open(path, &db) != SQLITE_OK)
+		return error_printf("database open error: %s", sqlite3_errmsg(db));
+	if (sqlite3_exec(db, sinit, NULL, NULL, NULL) != SQLITE_OK)
+		return error_printf("database init error: %s", sqlite3_errmsg(db));
+
+	return true;
+}
+
+bool
+save_set_property(const char *key, const char *value)
+{
+	assert(key);
+	assert(value && strlen(value) <= SAVE_PROPERTY_VALUE_MAX);
+
+	sqlite3_stmt *stmt = NULL;
+
+	if (!exec(sbegin))
+		return false;
+	if (sqlite3_prepare(db, sset_property, -1, &stmt, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+	if (sqlite3_bind_text(stmt, 1, key, -1, NULL) != SQLITE_OK ||
+	    sqlite3_bind_text(stmt, 2, value, -1, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+	if (sqlite3_step(stmt) != SQLITE_DONE)
+		goto sqlite3_err;
+
+	sqlite3_finalize(stmt);
+
+	return exec(scommit);
+
+sqlite3_err:
+	if (stmt) {
+		sqlite3_finalize(stmt);
+		exec(srollback);
+	}
+
+	return error_printf("%s", sqlite3_errmsg(db));
+}
+
+const char *
+save_get_property(const char *key)
+{
+	assert(key);
+
+	static char value[SAVE_PROPERTY_VALUE_MAX + 1];
+	const char *ret = value;
+	sqlite3_stmt *stmt = NULL;
+
+	memset(value, 0, sizeof (value));
+
+	if (sqlite3_prepare(db, sget_property, -1, &stmt, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+	if (sqlite3_bind_text(stmt, 1, key, -1, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+
+	switch (sqlite3_step(stmt)) {
+	case SQLITE_DONE:
+		/* Not found. */
+		ret = NULL;
+		break;
+	case SQLITE_ROW:
+		/* Found. */
+		snprintf(value, sizeof (value), "%s", sqlite3_column_text(stmt, 0));
+		break;
+	default:
+		/* Error. */
+		goto sqlite3_err;
+	}
+
+	sqlite3_finalize(stmt);
+
+	return ret;
+
+sqlite3_err:
+	if (stmt)
+		sqlite3_finalize(stmt);
+
+	error_printf("%s", sqlite3_errmsg(db));
+
+	return NULL;
+}
+
+bool
+save_remove_property(const char *key)
+{
+	assert(key);
+
+	sqlite3_stmt *stmt = NULL;
+
+	if (!exec(sbegin))
+		return false;
+	if (sqlite3_prepare(db, sremove_property, -1, &stmt, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+	if (sqlite3_bind_text(stmt, 1, key, -1, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+	if (sqlite3_step(stmt) != SQLITE_DONE)
+		goto sqlite3_err;
+
+	sqlite3_finalize(stmt);
+
+	return exec(scommit);
+
+sqlite3_err:
+	if (stmt)
+		sqlite3_finalize(stmt);
+
+	error_printf("%s", sqlite3_errmsg(db));
+
+	return false;
+}
+
+void
+save_finish(void)
+{
+	if (db)
+		sqlite3_close(db);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/save.h	Sat Mar 07 17:06:01 2020 +0100
@@ -0,0 +1,95 @@
+/*
+ * 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_SAVE_H
+#define MOLKO_SAVE_H
+
+/**
+ * \file save.h
+ * \brief Save functions.
+ */
+
+#include <stdbool.h>
+
+/**
+ * \brief Max property value.
+ */
+#define SAVE_PROPERTY_VALUE_MAX 1024
+
+/**
+ * Open a database by index.
+ *
+ * This function use the preferred path to store local files under the user
+ * home directory. The parameter idx specifies the save slot to use.
+ *
+ * \param idx the save slot
+ * \return false on error
+ */
+bool
+save_open(unsigned int idx);
+
+/**
+ * Open the save slot specified by path.
+ *
+ * \pre path != NULL
+ * \param path the path to the save slot
+ * \return false on error
+ */
+bool
+save_open_path(const char *path);
+
+/**
+ * Sets an arbitrary property.
+ *
+ * If the property already exists, replace it.
+ *
+ * \pre key != NULL
+ * \pre value != NULL && strlen(value) <= SAVE_PROPERTY_VALUE_MAX
+ * \param key the property key
+ * \param value the property value
+ */
+bool
+save_set_property(const char *key, const char *value);
+
+/**
+ * Get a property.
+ *
+ * \pre key != NULL
+ * \param key the property key
+ * \return the key or NULL if not found
+ */
+const char *
+save_get_property(const char *key);
+
+/**
+ * Remove a property.
+ *
+ * \pre key != NULL
+ * \param key the property key
+ * \return false on error
+ */
+bool
+save_remove_property(const char *key);
+
+/**
+ * Close the save slot.
+ */
+void
+save_finish(void);
+
+#endif /* !MOLKO_SAVE_H */
--- a/src/core/sys.c	Sun Feb 16 18:27:42 2020 +0100
+++ b/src/core/sys.c	Sat Mar 07 17:06:01 2020 +0100
@@ -19,6 +19,7 @@
 #include <assert.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <limits.h>
 
 #include <SDL.h>
 #include <SDL_image.h>
@@ -59,7 +60,7 @@
 static void
 determine(char path[], size_t pathlen)
 {
-	char localassets[FILENAME_MAX];
+	char localassets[PATH_MAX];
 	char *base = SDL_GetBasePath();
 	DIR *dp;
 
@@ -101,7 +102,8 @@
 {
 #if defined(__MINGW64__)
 	/* On MinGW buffering leads to painful debugging. */
-	setvbuf(stdout, NULL, _IONBF, 0);
+	setbuf(stderr, NULL);
+	setbuf(stdout, NULL);
 #endif
 
 	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0)
@@ -119,7 +121,7 @@
 const char *
 sys_datadir(void)
 {
-	static char path[1024];
+	static char path[PATH_MAX];
 
 	if (path[0] == '\0')
 		determine(path, sizeof (path));
@@ -143,7 +145,7 @@
 const char *
 sys_datapathv(const char *fmt, va_list ap)
 {
-	static char path[2048 + FILENAME_MAX];
+	static char path[PATH_MAX];
 	char filename[FILENAME_MAX];
 
 	vsnprintf(filename, sizeof (filename), fmt, ap);
@@ -152,6 +154,21 @@
 	return path;
 }
 
+const char *
+sys_savepath(unsigned int idx)
+{
+	static char path[PATH_MAX];
+	char *pref;
+
+	if ((pref = SDL_GetPrefPath("malikania", "molko"))) {
+		snprintf(path, sizeof (path), "%ssave-%u", idx);
+		SDL_free(pref);
+	} else
+		snprintf(path, sizeof (path), "save-%u", idx);
+
+	return path;
+}
+
 void
 sys_close(void)
 {
--- a/src/core/sys.h	Sun Feb 16 18:27:42 2020 +0100
+++ b/src/core/sys.h	Sat Mar 07 17:06:01 2020 +0100
@@ -64,6 +64,17 @@
 sys_datapathv(const char *fmt, va_list ap);
 
 /**
+ * Compute the path to the save file for the given game state.
+ *
+ * \param idx the save number
+ * \return the path to the database file
+ * \note This only compute the path, it does not check the presence of the file
+ * \post The returned value will never be NULL
+ */
+const char *
+sys_savepath(unsigned int idx);
+
+/**
  * Close the system.
  */
 void
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-save.c	Sat Mar 07 17:06:01 2020 +0100
@@ -0,0 +1,69 @@
+/*
+ * test-save.c -- test save 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 <greatest.h>
+
+#include "save.h"
+
+#define NAME "test.db"
+
+static void
+clean(void *data)
+{
+	(void)data;
+
+	save_finish();
+	remove(NAME);
+}
+
+TEST
+properties_simple(void)
+{
+	ASSERT(save_open_path(NAME));
+
+	/* Insert a new property 'initialized'. */
+	ASSERT(save_set_property("initialized", "1"));
+	ASSERT_STR_EQ(save_get_property("initialized"), "1");
+
+	/* This must replace the existing value. */
+	ASSERT(save_set_property("initialized", "2"));
+	ASSERT_STR_EQ(save_get_property("initialized"), "2");
+
+	ASSERT(save_remove_property("initialized"));
+	ASSERT(!save_get_property("initialized"));
+
+	PASS();
+}
+
+SUITE(properties)
+{
+	SET_TEARDOWN(clean, NULL);
+	RUN_TEST(properties_simple);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	RUN_SUITE(properties);
+	GREATEST_MAIN_END();
+}