changeset 136:30b68089ae70

core: rework actions and a bit of drawables, closes #2492 In the effort of having as less as possible memory allocation in libcore, the usage of actions and drawable no longer copy the original source parameter to let user pass a heap allocated variable or a static storage one. Update predefined drawable and actions to match these new needs.
author David Demelier <markand@malikania.fr>
date Tue, 13 Oct 2020 09:38:44 +0200
parents eadfed7674ac
children 90bfbd160fb7
files doxygen/CMakeLists.txt examples/CMakeLists.txt examples/assets/sprites/chest.png examples/assets/sprites/people.png examples/example-action.c examples/example-drawable.c libcore/CMakeLists.txt libcore/core/action.c libcore/core/action.h libcore/core/animation.c libcore/core/animation.h libcore/core/drawable.c libcore/core/drawable.h libcore/core/game.c libcore/core/message.c libcore/core/message.h libcore/core/script.c libcore/core/script.h libcore/core/wait.c libcore/core/wait.h tests/CMakeLists.txt tests/test-action-script.c tests/test-action.c tests/test-script.c
diffstat 24 files changed, 1929 insertions(+), 472 deletions(-) [+]
line wrap: on
line diff
--- a/doxygen/CMakeLists.txt	Sun Oct 11 15:01:26 2020 +0200
+++ b/doxygen/CMakeLists.txt	Tue Oct 13 09:38:44 2020 +0200
@@ -31,6 +31,13 @@
 		libadventure
 		libcore
 	)
+
+	set(
+		DOXYGEN_STRIP_FROM_PATH
+		${CMAKE_SOURCE_DIR}/libadventure
+		${CMAKE_SOURCE_DIR}/libcore
+	)
+
 	set(DOXYGEN_ALLOW_UNICODE_NAMES YES)
 	set(DOXYGEN_AUTOLINK_SUPPORT NO)
 	set(DOXYGEN_ENABLE_PREPROCESSING YES)
@@ -38,7 +45,7 @@
 	set(DOXYGEN_EXCLUDE_PATTERNS *_p.h)
 	set(DOXYGEN_FILE_PATTERNS *.h)
 	set(DOXYGEN_GENERATE_LATEX NO)
-	set(DOXYGEN_GENERATE_MAN YES)
+	set(DOXYGEN_GENERATE_MAN NO)
 	set(DOXYGEN_INPUT_ENCODING UTF-8)
 	set(DOXYGEN_MAX_INITIALIZER_LINES 0)
 	set(DOXYGEN_OPTIMIZE_OUTPUT_FOR_C YES)
@@ -48,7 +55,6 @@
 	set(DOXYGEN_PROJECT_NAME "Molko's Adventure")
 	set(DOXYGEN_QUIET YES)
 	set(DOXYGEN_RECURSIVE YES)
-	set(DOXYGEN_STRIP_FROM_PATH src)
 	set(DOXYGEN_TAB_SIZE 8)
 	set(DOXYGEN_WARNINGS YES)
 
--- a/examples/CMakeLists.txt	Sun Oct 11 15:01:26 2020 +0200
+++ b/examples/CMakeLists.txt	Tue Oct 13 09:38:44 2020 +0200
@@ -19,6 +19,16 @@
 project(examples)
 
 molko_define_executable(
+	TARGET example-action
+	SOURCES example-action.c
+	FOLDER examples
+	ASSETS
+		${examples_SOURCE_DIR}/assets/sprites/chest.png
+		${examples_SOURCE_DIR}/assets/sprites/people.png
+	LIBRARIES libcore
+)
+
+molko_define_executable(
 	TARGET example-inventory
 	SOURCES example-inventory
 	ASSETS
Binary file examples/assets/sprites/chest.png has changed
Binary file examples/assets/sprites/people.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/example-action.c	Tue Oct 13 09:38:44 2020 +0200
@@ -0,0 +1,350 @@
+/*
+ * example-action.c -- example on how to use automatic drawables
+ *
+ * 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 <core/action.h>
+#include <core/clock.h>
+#include <core/event.h>
+#include <core/image.h>
+#include <core/maths.h>
+#include <core/message.h>
+#include <core/painter.h>
+#include <core/panic.h>
+#include <core/script.h>
+#include <core/sprite.h>
+#include <core/sys.h>
+#include <core/texture.h>
+#include <core/theme.h>
+#include <core/util.h>
+#include <core/window.h>
+
+#include <assets/sprites/chest.h>
+#include <assets/sprites/people.h>
+
+#define W 1280
+#define H 720
+
+static struct action_stack stack;
+
+/*
+ * Event object for the chest to click on.
+ */
+static struct {
+	struct message msg;
+	struct action msg_act;
+	int x;
+	int y;
+	bool opened;
+	struct texture image;
+	struct sprite sprite;
+	struct action event;
+} chest = {
+	.msg = {
+		.text = {
+			"100000 pièces.",
+			"Te voilà riche sale file de crevette."
+		}
+	}
+};
+
+/*
+ * Event object for the guide who ask dialog with you.
+ *
+ * It is a script spawned upon click like this.
+ *
+ * [guide.event]
+ *       |           spawn
+ *   (on click)------------------->[message 1]
+ *                                      v
+ *                                 [message 2]
+ *                                      v
+ *                                 [question 3]
+ *                   spawn              v
+ * [response]<---------------------[check result]
+ *     v
+ * [message 4/5]
+ */
+static struct {
+	struct {
+		struct message msg;
+		struct action act;
+	} msgs[5];
+
+	/* This is the sprite and action to click on. */
+	int x;
+	int y;
+	struct texture image;
+	struct sprite sprite;
+	struct action event;
+
+	/* This is the event for the response. */
+	struct action response;
+
+	struct script script;
+	struct action script_act;
+} guide = {
+	.msgs = {
+		{
+			.msg = {
+				.text = {
+					"Bienvenue dans ce monde Molko."
+				}
+			},
+		},
+		{
+			.msg = {
+				.text = {
+					"Penses tu vraiment pouvoir me battre ?"
+				}
+			}
+		},
+		{
+			.msg = {
+				.flags = MESSAGE_QUESTION,
+				.text = {
+					"Non j'ai la trouille.",
+					"Bien sûr, je suis la légende."
+				}
+			}
+		},
+
+		/* In case of NO. */
+		{
+			.msg = {
+				.text = {
+					"Poule mouillée."
+				}
+			}
+		},
+
+		/* In case of YES. */
+		{
+			.msg = {
+				.text = {
+					"Prépare toi à souffrir."
+				}
+			}
+		}
+	}
+};
+
+static bool
+guide_response_update(struct action *act, unsigned int ticks)
+{
+	/* Immediately return to get access to end. */
+	(void)act;
+	(void)ticks;
+
+	return true;
+}
+
+static void
+guide_response_end(struct action *act)
+{
+	(void)act;
+
+	/* We add a final response depending on the result. */
+	const int index = guide.msgs[2].msg.index + 3;
+
+	message_action(&guide.msgs[index].msg, &guide.msgs[index].act);
+	action_stack_add(&stack, &guide.msgs[index].act);
+}
+
+static bool
+guide_running(void)
+{
+	return guide.script.actionsz > 0;
+}
+
+static void
+guide_popup(void)
+{
+	/* Prepare the script with first 3 messages. */
+	for (size_t i = 0; i < 3; ++i) {
+		message_action(&guide.msgs[i].msg, &guide.msgs[i].act);
+		script_append(&guide.script, &guide.msgs[i].act);
+	}
+
+	/* Reset the message index. */
+	guide.msgs[2].msg.index = 0;
+
+	/* This is final action that analyze user input. */
+	guide.response.update = guide_response_update;
+	guide.response.end = guide_response_end;
+	script_append(&guide.script, &guide.response);
+
+	script_action(&guide.script, &guide.script_act);
+	action_stack_add(&stack, &guide.script_act);
+}
+
+static void
+guide_handle(struct action *act, const union event *ev)
+{
+	(void)act;
+
+	if (guide_running())
+		return;
+
+	switch (ev->type) {
+	case EVENT_CLICKDOWN:
+		if (maths_is_boxed(guide.x, guide.y,
+		    guide.sprite.cellw, guide.sprite.cellh,
+		    ev->click.x, ev->click.y))
+			guide_popup();
+		break;
+	default:
+		break;
+	}
+}
+
+static void
+guide_draw(struct action *act)
+{
+	(void)act;
+
+	sprite_draw(&guide.sprite, 0, 0, guide.x, guide.y);
+}
+
+static void
+guide_init(void)
+{
+	if (!image_openmem(&guide.image, sprites_people, sizeof (sprites_people)))
+		panic();
+
+	sprite_init(&guide.sprite, &guide.image, 48, 48);
+
+	/* This is the global guide action. */
+	guide.event.handle = guide_handle;
+	guide.event.draw = guide_draw;
+	guide.x = 200;
+	guide.y = 200;
+}
+
+static void
+chest_handle(struct action *act, const union event *ev)
+{
+	(void)act;
+
+	if (chest.opened)
+		return;
+
+	switch (ev->type) {
+	case EVENT_CLICKDOWN:
+		if (maths_is_boxed(chest.x, chest.y, chest.sprite.cellw, chest.sprite.cellh,
+		    ev->click.x, ev->click.y)) {
+			chest.opened = true;
+			message_action(&chest.msg, &chest.msg_act);
+			action_stack_add(&stack, &chest.msg_act);
+		}
+	default:
+		break;
+	}
+}
+
+static void
+chest_draw(struct action *act)
+{
+	(void)act;
+
+	const int row = chest.opened ? 3 : 0;
+
+	sprite_draw(&chest.sprite, row, 0, chest.x, chest.y);
+}
+
+static void
+chest_init(void)
+{
+	if (!image_openmem(&chest.image, sprites_chest, sizeof (sprites_chest)))
+		panic();
+
+	sprite_init(&chest.sprite, &chest.image, 32, 32);
+
+	chest.x = 100;
+	chest.y = 100;
+	chest.event.handle = chest_handle;
+	chest.event.draw = chest_draw;
+}
+
+static void
+init(void)
+{
+	if (!sys_init() ||
+	    !window_init("Example - Action", W, H) ||
+	    !theme_init())
+		panic();
+
+	guide_init();
+	chest_init();
+}
+
+static void
+run(void)
+{
+	struct clock clock = {0};
+
+	clock_start(&clock);
+	action_stack_add(&stack, &chest.event);
+	action_stack_add(&stack, &guide.event);
+
+	for (;;) {
+		unsigned int elapsed = clock_elapsed(&clock);
+
+		clock_start(&clock);
+
+		for (union event ev; event_poll(&ev); ) {
+			switch (ev.type) {
+			case EVENT_QUIT:
+				return;
+			default:
+				action_stack_handle(&stack, &ev);
+				break;
+			}
+		}
+
+		painter_set_color(0xffffffff);
+		painter_clear();
+		action_stack_update(&stack, elapsed);
+		action_stack_draw(&stack);
+		painter_present();
+
+		if ((elapsed = clock_elapsed(&clock)) < 20)
+			delay(20 - elapsed);
+	}
+}
+
+static void
+quit(void)
+{
+	theme_finish();
+	window_finish();
+	sys_finish();
+}
+
+int
+main(int argc, char **argv)
+{
+	(void)argc;
+	(void)argv;
+
+	init();
+	run();
+	quit();
+
+	return 0;
+}
--- a/examples/example-drawable.c	Sun Oct 11 15:01:26 2020 +0200
+++ b/examples/example-drawable.c	Tue Oct 13 09:38:44 2020 +0200
@@ -16,6 +16,8 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <stdlib.h>
+
 #include <core/animation.h>
 #include <core/clock.h>
 #include <core/event.h>
@@ -72,7 +74,11 @@
 /* 0: Explosion animation. */
 static struct texture explosion_tex;
 static struct sprite explosion_sprite;
-static struct animation explosion_animation;
+
+struct explosion {
+	struct animation anim;
+	struct drawable dw;
+};
 
 static void
 init(void)
@@ -87,16 +93,32 @@
 		panic();
 
 	sprite_init(&explosion_sprite, &explosion_tex, 256, 256);
-	animation_init(&explosion_animation, &explosion_sprite, 100);
+}
+
+static void
+explosion_finish(struct drawable *dw)
+{
+	free(dw->data);
 }
 
 static void
 spawn(int x, int y)
 {
-	struct drawable dw;
+	struct explosion *expl = emalloc(sizeof (struct explosion));
+
+	animation_init(&expl->anim, &explosion_sprite, 100);
+	animation_drawable(&expl->anim, &expl->dw, x, y);
 
-	drawable_from_animation(&dw, &explosion_animation, x, y);
-	drawable_stack_add(&stack, &dw);
+	/*
+	 * This work because the drawable->data field expects a struct animation
+	 * pointer which is the first member of struct explosion.
+	 *
+	 * Thus this "poor man inheritance" trick works perfectly in our case
+	 * and we simply need to free the whole explosion struct afterwards.
+	 */
+	expl->dw.finish = explosion_finish;
+
+	drawable_stack_add(&stack, &expl->dw);
 }
 
 static void
--- a/libcore/CMakeLists.txt	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/CMakeLists.txt	Tue Oct 13 09:38:44 2020 +0200
@@ -27,6 +27,7 @@
 
 set(
 	SOURCES
+	${libcore_SOURCE_DIR}/core/action.c
 	${libcore_SOURCE_DIR}/core/action.h
 	${libcore_SOURCE_DIR}/core/animation.c
 	${libcore_SOURCE_DIR}/core/animation.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libcore/core/action.c	Tue Oct 13 09:38:44 2020 +0200
@@ -0,0 +1,168 @@
+/*
+ * action.c -- action states
+ *
+ * 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 <stddef.h>
+#include <string.h>
+
+#include "action.h"
+
+#define ACTION_FOREACH(st, iter) \
+	for (size_t i = 0; i < ACTION_STACK_MAX && ((iter) = (st)->actions[i], 1); ++i)
+
+void
+action_handle(struct action *act, const union event *ev)
+{
+	assert(act);
+	assert(ev);
+
+	if (act->handle)
+		act->handle(act, ev);
+}
+
+bool
+action_update(struct action *act, unsigned int ticks)
+{
+	assert(act);
+
+	if (act->update)
+		return act->update(act, ticks);
+
+	return false;
+}
+
+void
+action_draw(struct action *act)
+{
+	assert(act);
+
+	if (act->draw)
+		act->draw(act);
+}
+
+void
+action_end(struct action *act)
+{
+	assert(act);
+
+	if (act->end)
+		act->end(act);
+}
+
+void
+action_finish(struct action *act)
+{
+	assert(act);
+
+	if (act->finish)
+		act->finish(act);
+}
+
+void
+action_stack_init(struct action_stack *st)
+{
+	assert(st);
+
+	memset(st, 0, sizeof (*st));
+}
+
+bool
+action_stack_add(struct action_stack *st, struct action *act)
+{
+	assert(st);
+	assert(act);
+
+	for (size_t i = 0; i < ACTION_STACK_MAX; ++i) {
+		if (!st->actions[i]) {
+			st->actions[i] = act;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+void
+action_stack_handle(struct action_stack *st, const union event *ev)
+{
+	assert(st);
+	assert(ev);
+
+	struct action *act;
+
+	ACTION_FOREACH(st, act)
+		if (act)
+			action_handle(act, ev);
+}
+
+bool
+action_stack_update(struct action_stack *st, unsigned int ticks)
+{
+	assert(st);
+
+	struct action *act;
+
+	for (size_t i = 0; i < ACTION_STACK_MAX; ++i) {
+		act = st->actions[i];
+
+		if (act && action_update(act, ticks)) {
+			action_end(act);
+			action_finish(act);
+			st->actions[i] = NULL;
+		}
+	}
+
+	/*
+	 * We process all actions again in case the user modified the stack
+	 * within their update function.
+	 */
+	ACTION_FOREACH(st, act)
+		if (act)
+			return false;
+
+	return true;
+}
+
+void
+action_stack_draw(struct action_stack *st)
+{
+	assert(st);
+
+	struct action *act;
+
+	ACTION_FOREACH(st, act)
+		if (act)
+			action_draw(act);
+}
+
+void
+action_stack_finish(struct action_stack *st)
+{
+	assert(st);
+	
+	struct action *act;
+
+	ACTION_FOREACH(st, act) {
+		if (act) {
+			action_end(act);
+			action_finish(act);
+		}
+	}
+
+	memset(st, 0, sizeof (*st));
+}
--- a/libcore/core/action.h	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/action.h	Tue Oct 13 09:38:44 2020 +0200
@@ -16,8 +16,8 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#ifndef ACTION_H
-#define ACTION_H
+#ifndef MOLKO_ACTION_H
+#define MOLKO_ACTION_H
 
 /**
  * \file action.h
@@ -27,56 +27,62 @@
 
 #include <stdbool.h>
 
-union event;
-
 /**
- * \brief Action flags.
+ * \brief Maximum number of actions in stack.
  */
-enum action_flags {
-	ACTION_NONE,                            /*!< No flags */
-	ACTION_AUTO_LEAVE       = (1 << 0)      /*!< Action is removed on state change */
-};
+#define ACTION_STACK_MAX        128
+
+union event;
 
 /**
  * \brief Action structure.
+ *
+ * Use this structure to create an action that reacts to user events.
+ *
+ * The purpose of actions is to simplify user interaction within a specific
+ * state or a dedicated user routine. With the help of the companion
+ * \ref action_stack it is easy to manage actions in a specific game logic
+ * state.
+ *
+ * All members can be NULL.
  */
 struct action {
 	/**
-	 * (RW)
-	 *
-	 * Optional flags.
-	 */
-	enum action_flags flags;
-
-	/**
-	 * (RW)
+	 * (RW, optional)
 	 *
 	 * Arbitrary user data.
 	 */
 	void *data;
 
 	/**
-	 * (RW)
+	 * (RW, optional)
 	 *
 	 * Handle event.
+	 *
+	 * \param act this action
+	 * \param ev the event
 	 */
-	void (*handle)(struct action *, const union event *event);
+	void (*handle)(struct action *act, const union event *ev);
+
+	/**
+	 * (RW, optional)
+	 *
+	 * Update the action.
+	 *
+	 * \param act this action
+	 * \param ticks the number of milliseconds since last frame
+	 * \return true if action has terminated
+	 */
+	bool (*update)(struct action *act, unsigned int ticks);
 
 	/**
 	 * (RW)
 	 *
-	 * Update the action.
+	 * Draw the action.
 	 *
-	 * If returns true, the action is removed.
+	 * \param act this action
 	 */
-	bool (*update)(struct action *, unsigned int);
-
-	/**
-	 * (RW)
-	 *
-	 * Draw the aciton.
-	 */
-	void (*draw)(struct action *);
+	void (*draw)(struct action *act);
 
 	/**
 	 * (RW)
@@ -86,15 +92,146 @@
 	 * This callback is mostly provided to allow the user doing something
 	 * else once an action is complete. Predefined actions should not use
 	 * this callback by themselves.
+	 *
+	 * \param act this action
 	 */
-	void (*end)(struct action *a);
+	void (*end)(struct action *act);
 
 	/**
 	 * (RW)
 	 *
-	 * Close the action before removal.
+	 * Close the action before removal. This function should be used to
+	 * deallocate memory if necessary.
+	 *
+	 * \param act this action
 	 */
-	void (*finish)(struct action *);
+	void (*finish)(struct action *act);
 };
 
-#endif /* !ACTION_H */
+/**
+ * Shortcut for act->handle (if not NULL).
+ *
+ * \pre act != NULL
+ * \pre ev != NULL
+ * \param act the action
+ * \param ev the event
+ */
+void
+action_handle(struct action *act, const union event *ev);
+
+/**
+ * Shortcut for act->update (if not NULL).
+ *
+ * \pre act != NULL
+ * \param act the action
+ * \param ticks the number of milliseconds since last frame
+ */
+bool
+action_update(struct action *act, unsigned int ticks);
+
+/**
+ * Shortcut for act->draw (if not NULL).
+ *
+ * \pre act != NULL
+ * \param act the action
+ */
+void
+action_draw(struct action *act);
+
+/**
+ * Shortcut for act->end (if not NULL).
+ *
+ * \pre act != NULL
+ * \param act the action
+ */
+void
+action_end(struct action *act);
+
+/**
+ * Shortcut for act->finish (if not NULL).
+ *
+ * \pre act != NULL
+ * \param act the action
+ */
+void
+action_finish(struct action *act);
+
+/**
+ * \brief Stack of actions.
+ *
+ * The purpose of this structure is to help managing several actions at once.
+ * Actions are automatically removed from the stack if the corresponding update
+ * member function returns true after completion.
+ *
+ * This structure contains pointers to actions that must be kept until the stack
+ * is destroyed. User is responsible of deallocating them if they were allocated
+ * from the heap.
+ */
+struct action_stack {
+	struct action *actions[ACTION_STACK_MAX];        /*!< (RW) Actions */
+};
+
+/**
+ * Initalize the action stack.
+ *
+ * It is unnecessary if the object was zero'ed.
+ *
+ * \pre st != NULL
+ * \param st the stack
+ */
+void
+action_stack_init(struct action_stack *st);
+
+/**
+ * Add an action to the stack.
+ *
+ * \pre st != NULL
+ * \pre act != NULL
+ * \param st the stack
+ * \param act the action
+ * \note The pointer must be kept alive.
+ */
+bool
+action_stack_add(struct action_stack *st, struct action *act);
+
+/**
+ * Handle an event for all actions.
+ *
+ * \pre st != NULL
+ * \pre ev != NULL
+ * \param st the stack
+ * \param ev the event
+ */
+void
+action_stack_handle(struct action_stack *st, const union event *ev);
+
+/**
+ * Update all actions.
+ *
+ * \pre st != NULL
+ * \param st the stack
+ * \param ticks the number of milliseconds since last frame
+ * \return true if all actions completed
+ */
+bool
+action_stack_update(struct action_stack *st, unsigned int ticks);
+
+/**
+ * Draw all actions.
+ *
+ * \pre st != NULL
+ * \param st the stack
+ */
+void
+action_stack_draw(struct action_stack *st);
+
+/**
+ * Terminate all actions and clear the stack.
+ *
+ * \pre st != NULL
+ * \param st the stack
+ */
+void
+action_stack_finish(struct action_stack *st);
+
+#endif /* !MOLKO_ACTION_H */
--- a/libcore/core/animation.c	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/animation.c	Tue Oct 13 09:38:44 2020 +0200
@@ -17,10 +17,24 @@
  */
 
 #include <assert.h>
+#include <string.h>
 
+#include "drawable.h"
 #include "animation.h"
 #include "sprite.h"
 
+static bool
+update(struct drawable *dw, unsigned int ticks)
+{
+	return animation_update(dw->data, ticks);
+}
+
+static void
+draw(struct drawable *dw)
+{
+	return animation_draw(dw->data, dw->x, dw->y);
+}
+
 void
 animation_init(struct animation *an, struct sprite *sprite, unsigned int delay)
 {
@@ -34,16 +48,6 @@
 	an->elapsed = 0;
 }
 
-bool
-animation_is_complete(const struct animation *an)
-{
-	assert(an);
-
-	return an->row == an->sprite->nrows &&
-	       an->column == an->sprite->ncols &&
-	       an->elapsed >= an->delay;
-}
-
 void
 animation_start(struct animation *an)
 {
@@ -86,3 +90,20 @@
 {
 	sprite_draw(an->sprite, an->row, an->column, x, y);
 }
+
+void
+animation_drawable(struct animation *an, struct drawable *dw, int x, int y)
+{
+	assert(an);
+	assert(dw);
+
+	memset(dw, 0, sizeof (*dw));
+	
+	dw->data = an;
+	dw->x = x - (an->sprite->cellw / 2);
+	dw->y = y - (an->sprite->cellh / 2);
+	dw->update = update;
+	dw->draw = draw;
+
+	animation_start(an);
+}
--- a/libcore/core/animation.h	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/animation.h	Tue Oct 13 09:38:44 2020 +0200
@@ -27,6 +27,7 @@
 
 #include <stdbool.h>
 
+struct drawable;
 struct sprite;
 
 /**
@@ -89,4 +90,28 @@
 void
 animation_draw(struct animation *an, int x, int y);
 
+/**
+ * Create a drawable from an animation.
+ *
+ * The animation must be kept alive until the drawable is used.
+ *
+ * The dw->data member will be set to the animation pointer and can be
+ * safely used by the user for custom drawable operation if needed.
+ *
+ * The dw->finish member isn't used and can be re-used by the user.
+ *
+ * \pre an != NULL
+ * \pre dw != NULL
+ * \param an the animation
+ * \param dw the drawable
+ * \param x x position on screen
+ * \param y y position on screen
+ * \post dw->data contains an
+ * \post dw->update contains an update function
+ * \post dw->draw contains a drawing function
+ * \post dw->finish is NULL
+ */
+void
+animation_drawable(struct animation *an, struct drawable *dw, int x, int y);
+
 #endif /* !MOLKO_ANIMATION_H */
--- a/libcore/core/drawable.c	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/drawable.c	Tue Oct 13 09:38:44 2020 +0200
@@ -26,6 +26,9 @@
 #include "util.h"
 #include "sprite.h"
 
+#define DRAWABLE_FOREACH(st, iter) \
+	for (size_t i = 0; i < DRAWABLE_STACK_MAX && ((iter) = (st)->objects[i], 1); ++i)
+
 bool
 drawable_update(struct drawable *dw, unsigned int ticks)
 {
@@ -49,43 +52,6 @@
 		dw->finish(dw);
 }
 
-static bool
-drawable_animation_update(struct drawable *dw, unsigned int ticks)
-{
-	return animation_update(dw->data, ticks);
-}
-
-static void
-drawable_animation_draw(struct drawable *dw)
-{
-	return animation_draw(dw->data, dw->x, dw->y);
-}
-
-static void
-drawable_animation_finish(struct drawable *dw)
-{
-	free(dw->data);
-}
-
-void
-drawable_from_animation(struct drawable *dw,
-			const struct animation *an,
-			int x,
-			int y)
-{
-	assert(dw);
-	assert(an);
-
-	memset(dw, 0, sizeof (*dw));
-	
-	dw->data = ememdup(an, sizeof (*an));
-	dw->x = x - (an->sprite->cellw / 2);
-	dw->y = y - (an->sprite->cellh / 2);
-	dw->update = drawable_animation_update;
-	dw->draw = drawable_animation_draw;
-	dw->finish = drawable_animation_finish;
-}
-
 void
 drawable_stack_init(struct drawable_stack *st)
 {
@@ -95,27 +61,18 @@
 }
 
 bool
-drawable_stack_add(struct drawable_stack *st, const struct drawable *dw)
+drawable_stack_add(struct drawable_stack *st, struct drawable *dw)
 {
-	/* Find an empty slot. */
-	struct drawable *slot = NULL;
+	assert(st);
+	assert(dw);
 
 	for (size_t i = 0; i < DRAWABLE_STACK_MAX; ++i) {
-		struct drawable *dtmp = &st->objects[i];
-
-		dtmp = &st->objects[i];
-
-		if (!dtmp->update && !dtmp->draw) {
-			slot = dtmp;
-			break;
+		if (!st->objects[i]) {
+			st->objects[i] = dw;
+			return true;
 		}
 	}
 
-	if (slot) {
-		memcpy(slot, dw, sizeof (*dw));
-		return true;
-	}
-	
 	return false;
 }
 
@@ -125,10 +82,12 @@
 	assert(st);
 
 	for (size_t i = 0; i < DRAWABLE_STACK_MAX; ++i) {
-		struct drawable *dw = &st->objects[i];
+		struct drawable *dw = st->objects[i];
 
-		if (dw->update && dw->update(dw, ticks))
-			memset(dw, 0, sizeof (*dw));
+		if (dw && drawable_update(dw, ticks)) {
+			drawable_finish(dw);
+			st->objects[i] = NULL;
+		}
 	}
 }
 
@@ -137,21 +96,20 @@
 {
 	assert(st);
 
-	for (size_t i = 0; i < DRAWABLE_STACK_MAX; ++i) {
-		struct drawable *dw = &st->objects[i];
+	struct drawable *dw;
 
-		if (dw->draw)
-			dw->draw(dw);
-	}
+	DRAWABLE_FOREACH(st, dw)
+		if (dw)
+			drawable_draw(dw);
 }
 
 void
 drawable_stack_finish(struct drawable_stack *st)
 {
 	for (size_t i = 0; i < DRAWABLE_STACK_MAX; ++i) {
-		struct drawable *dw = &st->objects[i];
+		struct drawable *dw = st->objects[i];
 
-		if (dw->finish)
+		if (dw && dw->finish)
 			dw->finish(dw);
 	}
 
--- a/libcore/core/drawable.h	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/drawable.h	Tue Oct 13 09:38:44 2020 +0200
@@ -19,6 +19,11 @@
 #ifndef MOLKO_DRAWABLE_H
 #define MOLKO_DRAWABLE_H
 
+/**
+ * \file drawable.h
+ * \brief Automatic drawable objects.
+ */
+
 #include <stdbool.h>
 
 /**
@@ -26,8 +31,6 @@
  */
 #define DRAWABLE_STACK_MAX      128
 
-struct animation;
-
 /**
  * \brief Abstract drawable object.
  *
@@ -64,7 +67,7 @@
 };
 
 /**
- * Update the object
+ * Shortcut for dw->update (if not NULL).
  *
  * \pre dw != NULL
  * \param dw the drawable object
@@ -75,7 +78,7 @@
 drawable_update(struct drawable *dw, unsigned int ticks);
 
 /**
- * Draw the drawable.
+ * Shortcut for dw->draw (if not NULL).
  *
  * \pre dw != NULL
  * \param dw the drawable object
@@ -84,7 +87,7 @@
 drawable_draw(struct drawable *dw);
 
 /**
- * Dispose internal resources if necessary.
+ * Shortcut for dw->finish (if not NULL).
  *
  * \pre dw != NULL
  * \param dw the drawable object
@@ -93,34 +96,15 @@
 drawable_finish(struct drawable *dw);
 
 /**
- * Create a drawable from an animation.
- *
- * The animation is copied verbatim (as such internal resources must be kept
- * valid).
- *
- * \pre dw != NULL
- * \pre an the animation
- * \param dw the drawable
- * \param an the animation
- * \param x x position on screen
- * \param y y position on screen
- */
-void
-drawable_from_animation(struct drawable *dw,
-                        const struct animation *an,
-                        int x,
-                        int y);
-
-/**
  * \brief Stack of drawable objects.
  *
  * This stack of drawable object can be used to store drawable objects within
  * a specific transition (state, battle, menu, etc).
  *
- * You can add, clear, and update and draw them.
+ * You can add, clear, update and draw them.
  */
 struct drawable_stack {
-	struct drawable objects[DRAWABLE_STACK_MAX];    /*!< (RW) Drawables. */
+	struct drawable *objects[DRAWABLE_STACK_MAX];   /*!< (RW) Drawables. */
 };
 
 /**
@@ -135,17 +119,17 @@
 /**
  * Add a drawable object into the stack.
  *
- * The drawable object internals are copied verbatim into the stack.
+ * The drawable object must be kept alive until the stack uses it.
  *
  * \pre st != NULL
  * \pre dw != NULL
  * \param st the stack
- * \param dw the drawable to copy from
+ * \param dw the drawable to reference
  * \return true if the drawable was placed correctly and false if there wasn't
  *         enough room.
  */
 bool
-drawable_stack_add(struct drawable_stack *st, const struct drawable *dw);
+drawable_stack_add(struct drawable_stack *st, struct drawable *dw);
 
 /**
  * Update all drawable objects.
--- a/libcore/core/game.c	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/game.c	Tue Oct 13 09:38:44 2020 +0200
@@ -26,6 +26,8 @@
 
 struct game game;
 
+#if 0
+
 static struct action *
 find_empty_action(void)
 {
@@ -88,6 +90,8 @@
 			game.actions[i].draw(&game.actions[i]);
 }
 
+#endif
+
 void
 game_switch(struct state *state, bool quick)
 {
@@ -109,7 +113,9 @@
 	if (game.state && !(game.inhibit & INHIBIT_STATE_INPUT))
 		game.state->handle(event);
 
+#if 0
 	handle_actions(event);
+#endif
 }
 
 void
@@ -126,15 +132,19 @@
 			game.state->enter();
 			game.state_next = NULL;
 
+#if 0
 			/* Remove any actions that must be deleted. */
 			clear_actions();
+#endif
 		}
 
 		if (game.state)
 			game.state->update(ticks);
 	}
 
+#if 0
 	update_actions(ticks);
+#endif
 }
 
 void
@@ -143,10 +153,13 @@
 	if (game.state && !(game.inhibit & INHIBIT_STATE_DRAW))
 		game.state->draw();
 
+#if 0
 	draw_actions();
+#endif
 	painter_present();
 }
 
+#if 0
 void
 game_add_action(const struct action *action)
 {
@@ -157,6 +170,7 @@
 	if ((pos = find_empty_action()))
 		memcpy(pos, action, sizeof (struct action));
 }
+#endif
 
 void
 game_quit(void)
--- a/libcore/core/message.c	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/message.c	Tue Oct 13 09:38:44 2020 +0200
@@ -43,7 +43,7 @@
 #define THEME(msg) (msg->theme ? msg->theme : theme_default())
 
 static void
-action_handle(struct action *action, const union event *ev)
+handle(struct action *action, const union event *ev)
 {
 	assert(action);
 	assert(ev);
@@ -52,7 +52,7 @@
 }
 
 static bool
-action_update(struct action *action, unsigned int ticks)
+update(struct action *action, unsigned int ticks)
 {
 	assert(action);
 
@@ -60,21 +60,13 @@
 }
 
 static void
-action_draw(struct action *action)
+draw(struct action *action)
 {
 	assert(action);
 
 	message_draw(action->data);
 }
 
-static void
-action_finish(struct action *action)
-{
-	assert(action);
-
-	free(action->data);
-}
-
 void
 message_start(struct message *msg)
 {
@@ -247,15 +239,16 @@
 }
 
 void
-message_action(const struct message *msg, struct action *action)
+message_action(struct message *msg, struct action *action)
 {
 	assert(msg);
 	assert(action);
 
 	memset(action, 0, sizeof (struct action));
-	action->data = ememdup(msg, sizeof (struct message));
-	action->handle = action_handle;
-	action->update = action_update;
-	action->draw = action_draw;
-	action->finish = action_finish;
+	action->data = msg;
+	action->handle = handle;
+	action->update = update;
+	action->draw = draw;
+
+	message_start(msg);
 }
--- a/libcore/core/message.h	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/message.h	Tue Oct 13 09:38:44 2020 +0200
@@ -170,10 +170,10 @@
  *
  * \pre msg != NULL
  * \pre action != NULL
- * \param msg the message to copy from
+ * \param msg the message to reference
  * \param action the action to fill
  */
 void
-message_action(const struct message *msg, struct action *action);
+message_action(struct message *msg, struct action *action);
 
 #endif /* !MOLKO_MESSAGE_H */
--- a/libcore/core/script.c	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/script.c	Tue Oct 13 09:38:44 2020 +0200
@@ -20,41 +20,41 @@
 #include <stdlib.h>
 #include <string.h>
 
+#include "action.h"
+#include "error.h"
 #include "script.h"
-#include "util.h"
 
-static void
-script_action_finish(struct action *a)
+static struct action *
+current(struct script *s)
 {
-	assert(a);
+	if (s->cur >= s->actionsz)
+		return NULL;
 
-	script_finish(a->data);
-	free(a->data);
+	return s->actions[s->cur];
 }
 
 static void
-script_action_handle(struct action *a, const union event *ev)
+handle(struct action *a, const union event *ev)
 {
-	assert(a);
-	assert(ev);
-
 	script_handle(a->data, ev);
 }
 
 static bool
-script_action_update(struct action *a, unsigned int ticks)
+update(struct action *a, unsigned int ticks)
 {
-	assert(a);
-
 	return script_update(a->data, ticks);
 }
 
 static void
-script_action_draw(struct action *a)
+draw(struct action *a)
 {
-	assert(a);
+	script_draw(a->data);
+}
 
-	script_draw(a->data);
+static void
+finish(struct action *a)
+{
+	script_finish(a->data);
 }
 
 void
@@ -62,30 +62,21 @@
 {
 	assert(s);
 
-	memset(s, 0, sizeof (struct script));
-	s->tail = &s->head;
+	memset(s, 0, sizeof (*s));
 }
 
-void
-script_start(struct script *s)
-{
-	assert(s);
-
-	s->iter = s->head;
-}
-
-void
-script_append(struct script *s, const struct action *a)
+bool
+script_append(struct script *s, struct action *a)
 {
 	assert(s);
 	assert(a);
-	assert(a->update);
 
-	struct script_action *iter = ecalloc(1, sizeof (struct script_action));
+	if (s->actionsz >= SCRIPT_ACTION_MAX)
+		return error_printf("script is full");
 
-	memcpy(&iter->action, a, sizeof (struct action));
-	*s->tail = iter;
-	s->tail = &iter->next;
+	s->actions[s->actionsz++] = a;
+
+	return true;
 }
 
 void
@@ -94,8 +85,10 @@
 	assert(s);
 	assert(ev);
 
-	if (s->iter && s->iter->action.handle)
-		s->iter->action.handle(&s->iter->action, ev);
+	struct action *a = current(s);
+	
+	if (a)
+		action_handle(a, ev);
 }
 
 bool
@@ -103,21 +96,17 @@
 {
 	assert(s);
 
-	if (!s->iter)
+	struct action *a = current(s);
+
+	if (!a)
 		return true;
 
-	struct action *a = &s->iter->action;
-
-	if (a->update(a, ticks)) {
-		if (a->end)
-			a->end(a);
-		if (a->finish)
-			a->finish(a);
-
-		s->iter = s->iter->next;
+	if (action_update(a, ticks)) {
+		action_end(a);
+		s->cur++;
 	}
 
-	return s->iter == NULL;
+	return s->cur >= s->actionsz;
 }
 
 void
@@ -125,22 +114,18 @@
 {
 	assert(s);
 
-	if (s->iter && s->iter->action.draw)
-		s->iter->action.draw(&s->iter->action);
+	struct action *a = current(s);
+
+	if (a)
+		action_draw(a);
 }
 
-void
-script_action(const struct script *s, struct action *action)
+bool
+script_completed(const struct script *s)
 {
 	assert(s);
-	assert(action);
 
-	memset(action, 0, sizeof (struct action));
-	action->data = ememdup(s, sizeof (struct script));
-	action->handle = script_action_handle;
-	action->update = script_action_update;
-	action->draw = script_action_draw;
-	action->finish = script_action_finish;
+	return s->cur >= s->actionsz;
 }
 
 void
@@ -148,12 +133,22 @@
 {
 	assert(s);
 
-	struct script_action *iter, *next;
+	for (size_t i = 0; i < s->actionsz; ++i)
+		action_finish(s->actions[i]);
+
+	memset(s, 0, sizeof (*s));
+}
 
-	for (iter = s->head; iter; iter = next) {
-		next = iter->next;
-		free(iter);
-	}
+void
+script_action(struct script *s, struct action *action)
+{
+	assert(s);
+	assert(action);
 
-	memset(s, 0, sizeof (struct script));
+	memset(action, 0, sizeof (*action));
+	action->data = s;
+	action->handle = handle;
+	action->update = update;
+	action->draw = draw;
+	action->finish = finish;
 }
--- a/libcore/core/script.h	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/script.h	Tue Oct 13 09:38:44 2020 +0200
@@ -34,40 +34,40 @@
  *
  * 1. Create a script with see \ref script_init,
  * 2. Create one or more actions and append with \ref script_append,
- * 3. Start the action using \ref script_start,
- * 4. Put the script into the game using \ref game_add_action.
+ * 3. Put the script into the game using \ref game_add_action.
  *
  * \warning You must always call \ref script_init before using this object.
  */
 
 #include <stdbool.h>
+#include <stddef.h>
 
-#include "action.h"
+struct action;
 
 union event;
 
 /**
- * \brief Single-linked list of actions.
+ * \brief Maximum number of actions in a script.
  */
-struct script_action {
-	struct action action;           /*!< (RW) Action to use */
-	struct script_action *next;     /*!< (RO) Pointer to next action */
-};
+#define SCRIPT_ACTION_MAX       (128)
 
 /**
  * \brief Sequence of actions and state holder.
+ *
+ * Setup the array actions within the structure for each action you want to run
+ * in order. You can use the convenient \ref script_append instead. If you do
+ * manually don't forget to adjust actionsz field accordingly.
  */
 struct script {
-	struct script_action *iter;     /*!< (RO) Current action */
-	struct script_action *head;     /*!< (RO) Beginning */
-	struct script_action **tail;    /*!< (RO) Pointer to add to tail */
+	struct action *actions[SCRIPT_ACTION_MAX];	/*!< (RW, ref) Array of actions. */
+	size_t actionsz;                                /*!< (RO) Number of actions in array. */
+	size_t cur;                                     /*!< (RO) Current action index.*/
 };
 
 /**
  * Initialize a script.
  *
- * This is mandatory before using any functions, do not zero-initialize the
- * structure yourself.
+ * This is not necessary if you zero'ed the structure.
  *
  * \pre s != NULL
  * \param s the script
@@ -76,29 +76,18 @@
 script_init(struct script *s);
 
 /**
- * Call this function before putting the script in the game.
- *
- * \pre s != NULL
- * \param s the script
- */
-void
-script_start(struct script *s);
-
-/**
  * Append a new action to the script.
  *
- * The action is copied into the script and does not need to be allocated on
- * the heap.
- *
- * The action can be empty but must have at least update member set.
+ * The action must be kept alive until the script is no longer used.
  *
  * \pre s != NULL
- * \pre a != NULL && a->update
+ * \pre a != NULL
  * \param s the script
- * \param a the action to copy
+ * \param a the action to reference
+ * \return false if unable to append
  */
-void
-script_append(struct script *s, const struct action *a);
+bool
+script_append(struct script *s, struct action *a);
 
 /**
  * Handle the event into the current action.
@@ -107,7 +96,6 @@
  * \pre ev != NULL
  * \param s the script
  * \param ev the event
- * \note You usually don't need to call this yourself.
  */
 void
 script_handle(struct script *s, const union event *ev);
@@ -118,7 +106,7 @@
  * \pre s != NULL
  * \param s the script
  * \param ticks the number of milliseconds since last frame
- * \note You usually don't need to call this yourself.
+ * \return true if the script completed
  */
 bool
 script_update(struct script *s, unsigned int ticks);
@@ -128,16 +116,33 @@
  *
  * \pre s != NULL
  * \param s the script
- * \note You usually don't need to call this yourself.
  */
 void
 script_draw(struct script *s);
 
 /**
- * Create an action from the script for use into the game.
+ * Tells if the script is terminated.
  *
- * This function is meant to transform the script into an action itself and be
- * added to the game using \ref game_add_action.
+ * \pre s != NULL
+ * \param s the script
+ * \return true if all action were completed
+ */
+bool
+script_completed(const struct script *s);
+
+/**
+ * Destroy all the actions into the script.
+ *
+ * \pre s != NULL
+ * \param s the script
+ */
+void
+script_finish(struct script *s);
+
+/**
+ * Create an action from the script itself for use into the game.
+ *
+ * The script must be kept alive until the action is no longer needed.
  *
  * \pre s != NULL
  * \pre dst != NULL
@@ -145,16 +150,6 @@
  * \param dst the action to build with the script
  */
 void
-script_action(const struct script *s, struct action *dst);
-
-/**
- * Destroy all the actions into the script.
- *
- * \pre s != NULL
- * \param s the script
- * \note You usually don't need to call this yourself.
- */
-void
-script_finish(struct script *s);
+script_action(struct script *s, struct action *dst);
 
 #endif /* !MOLKO_SCRIPT_H */
--- a/libcore/core/wait.c	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/wait.c	Tue Oct 13 09:38:44 2020 +0200
@@ -25,25 +25,18 @@
 #include "util.h"
 
 static bool
-action_update(struct action *a, unsigned int ticks)
+update(struct action *a, unsigned int ticks)
 {
 	assert(a);
 
 	return wait_update(a->data, ticks);
 }
 
-static void
-action_finish(struct action *a)
-{
-	assert(a);
-
-	free(a->data);
-}
-
 void
 wait_start(struct wait *w)
 {
 	assert(w);
+
 	w->elapsed = 0u;
 }
 
@@ -61,13 +54,13 @@
 }
 
 void
-wait_action(const struct wait *w, struct action *a)
+wait_action(struct wait *w, struct action *a)
 {
 	assert(w);
 	assert(a);
 
 	memset(a, 0, sizeof (struct action));
-	a->data = ememdup(w, sizeof (struct wait));
-	a->update = action_update;
-	a->finish = action_finish;
+
+	a->data = w;
+	a->update = update;
 }
--- a/libcore/core/wait.h	Sun Oct 11 15:01:26 2020 +0200
+++ b/libcore/core/wait.h	Tue Oct 13 09:38:44 2020 +0200
@@ -85,12 +85,14 @@
 /**
  * Create an action from the wait object.
  *
+ * The wait action must be kept alive until the action completes.
+ *
  * \pre w != NULL
- * \pre a != NULL
+ * \pre act != NULL
  * \param w the wait object to copy from
- * \param a the action to fill
+ * \param act the action to fill
  */
 void
-wait_action(const struct wait *w, struct action *a);
+wait_action(struct wait *w, struct action *act);
 
 #endif /* !MOLKO_WAIT_H */
--- a/tests/CMakeLists.txt	Sun Oct 11 15:01:26 2020 +0200
+++ b/tests/CMakeLists.txt	Tue Oct 13 09:38:44 2020 +0200
@@ -18,6 +18,8 @@
 
 project(tests)
 
+molko_define_test(TARGET action SOURCES test-action.c)
+molko_define_test(TARGET action-script SOURCES test-action-script.c)
 molko_define_test(TARGET color SOURCES test-color.c)
 molko_define_test(TARGET error SOURCES test-error.c)
 molko_define_test(TARGET inventory SOURCES test-inventory.c)
@@ -34,4 +36,3 @@
 )
 molko_define_test(TARGET rbuf SOURCES test-rbuf.c)
 molko_define_test(TARGET save SOURCES test-save.c)
-molko_define_test(TARGET script SOURCES test-script.c)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-action-script.c	Tue Oct 13 09:38:44 2020 +0200
@@ -0,0 +1,578 @@
+/*
+ * test-script.c -- test script action
+ *
+ * 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 GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+#include <core/action.h>
+#include <core/event.h>
+#include <core/script.h>
+
+struct invokes {
+	int handle;
+	int update;
+	int draw;
+	int end;
+	int finish;
+};
+
+#define INIT(dat, up) {         \
+	.data = (dat),          \
+	.handle = my_handle,    \
+	.update = (up),         \
+	.draw = my_draw,        \
+	.end = my_end,          \
+	.finish = my_finish     \
+}
+
+static void
+my_handle(struct action *act, const union event *ev)
+{
+	(void)ev;
+
+	((struct invokes *)act->data)->handle++;
+}
+
+static bool
+my_update_false(struct action *act, unsigned int ticks)
+{
+	(void)ticks;
+
+	((struct invokes *)act->data)->update++;
+
+	return false;
+}
+
+static bool
+my_update_true(struct action *act, unsigned int ticks)
+{
+	(void)ticks;
+
+	((struct invokes *)act->data)->update++;
+
+	return true;
+}
+
+static void
+my_draw(struct action *act)
+{
+	((struct invokes *)act->data)->draw++;
+}
+
+static void
+my_end(struct action *act)
+{
+	((struct invokes *)act->data)->end++;
+}
+
+static void
+my_finish(struct action *act)
+{
+	((struct invokes *)act->data)->finish++;
+}
+
+GREATEST_TEST
+basics_init(void)
+{
+	struct script sc;
+
+	script_init(&sc);
+
+	GREATEST_ASSERT_EQ(sc.actionsz, 0U);
+	GREATEST_ASSERT_EQ(sc.cur, 0U);
+	
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_append(void)
+{
+	struct action act[3] = {0};
+	struct script sc = {0};
+
+	GREATEST_ASSERT(script_append(&sc, &act[0]));
+	GREATEST_ASSERT_EQ(sc.cur, 0U);
+	GREATEST_ASSERT_EQ(sc.actionsz, 1U);
+	GREATEST_ASSERT_EQ(sc.actions[0], &act[0]);
+
+	GREATEST_ASSERT(script_append(&sc, &act[1]));
+	GREATEST_ASSERT_EQ(sc.cur, 0U);
+	GREATEST_ASSERT_EQ(sc.actionsz, 2U);
+	GREATEST_ASSERT_EQ(sc.actions[0], &act[0]);
+	GREATEST_ASSERT_EQ(sc.actions[1], &act[1]);
+
+	GREATEST_ASSERT(script_append(&sc, &act[2]));
+	GREATEST_ASSERT_EQ(sc.cur, 0U);
+	GREATEST_ASSERT_EQ(sc.actionsz, 3U);
+	GREATEST_ASSERT_EQ(sc.actions[0], &act[0]);
+	GREATEST_ASSERT_EQ(sc.actions[1], &act[1]);
+	GREATEST_ASSERT_EQ(sc.actions[2], &act[2]);
+
+	script_finish(&sc);
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_handle(void)
+{
+	struct {
+		struct invokes inv;
+		struct action act;
+	} table[] = {
+		{ .act = INIT(&table[0].inv, my_update_true)    },
+		{ .act = INIT(&table[1].inv, my_update_true)    },
+		{ .act = INIT(&table[2].inv, my_update_false)   }
+	};
+	
+	struct script sc = {0};
+
+	GREATEST_ASSERT(script_append(&sc, &table[0].act));
+	GREATEST_ASSERT(script_append(&sc, &table[1].act));
+	GREATEST_ASSERT(script_append(&sc, &table[2].act));
+
+	/* [0] */
+	script_handle(&sc, &(union event){0});
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	/* [0] -> [1] */
+	GREATEST_ASSERT(!script_update(&sc, 0));
+	script_handle(&sc, &(union event){0});
+
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	/* [2] */
+	GREATEST_ASSERT(!script_update(&sc, 0));
+	script_handle(&sc, &(union event){0});
+
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_update(void)
+{
+	struct {
+		struct invokes inv;
+		struct action act;
+	} table[] = {
+		{ .act = INIT(&table[0].inv, my_update_true)    },
+		{ .act = INIT(&table[1].inv, my_update_true)    },
+		{ .act = INIT(&table[2].inv, my_update_false)   }
+	};
+	
+	struct script sc = {0};
+
+	GREATEST_ASSERT(script_append(&sc, &table[0].act));
+	GREATEST_ASSERT(script_append(&sc, &table[1].act));
+	GREATEST_ASSERT(script_append(&sc, &table[2].act));
+
+	/* 0 -> 1 */
+	GREATEST_ASSERT(!script_update(&sc, 0));
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	/* 1 -> 2 */
+	GREATEST_ASSERT(!script_update(&sc, 0));
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	/* 2 stays, it never ends. */
+	GREATEST_ASSERT(!script_update(&sc, 0));
+	GREATEST_ASSERT(!script_update(&sc, 0));
+	GREATEST_ASSERT(!script_update(&sc, 0));
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 3);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	/* Now, change its update function to complete the script. */
+	table[2].act.update = my_update_true;
+	GREATEST_ASSERT(script_update(&sc, 0));
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 4);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_draw(void)
+{
+	struct {
+		struct invokes inv;
+		struct action act;
+	} table[] = {
+		{ .act = INIT(&table[0].inv, my_update_true)    },
+		{ .act = INIT(&table[1].inv, my_update_true)    },
+		{ .act = INIT(&table[2].inv, my_update_false)   }
+	};
+	
+	struct script sc = {0};
+
+	GREATEST_ASSERT(script_append(&sc, &table[0].act));
+	GREATEST_ASSERT(script_append(&sc, &table[1].act));
+	GREATEST_ASSERT(script_append(&sc, &table[2].act));
+
+	/* [0] */
+	script_draw(&sc);
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	/* [0] -> [1] */
+	GREATEST_ASSERT(!script_update(&sc, 0));
+	script_draw(&sc);
+
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	/* [2] */
+	GREATEST_ASSERT(!script_update(&sc, 0));
+	script_draw(&sc);
+
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_finish(void)
+{
+	struct {
+		struct invokes inv;
+		struct action act;
+	} table[] = {
+		{ .act = INIT(&table[0].inv, my_update_true)    },
+		{ .act = INIT(&table[1].inv, my_update_true)    },
+		{ .act = INIT(&table[2].inv, my_update_false)   }
+	};
+	
+	struct script sc = {0};
+
+	GREATEST_ASSERT(script_append(&sc, &table[0].act));
+	GREATEST_ASSERT(script_append(&sc, &table[1].act));
+	GREATEST_ASSERT(script_append(&sc, &table[2].act));
+
+	/* Update once so that the current action goes to 1. */
+	GREATEST_ASSERT(!script_update(&sc, 0));
+
+	script_finish(&sc);
+
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 1);
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_basics)
+{
+	GREATEST_RUN_TEST(basics_init);
+	GREATEST_RUN_TEST(basics_append);
+	GREATEST_RUN_TEST(basics_handle);
+	GREATEST_RUN_TEST(basics_update);
+	GREATEST_RUN_TEST(basics_draw);
+	GREATEST_RUN_TEST(basics_finish);
+}
+
+GREATEST_TEST
+action_simple(void)
+{
+	struct {
+		struct invokes inv;
+		struct action act;
+	} table[] = {
+		{ .act = INIT(&table[0].inv, my_update_true)    },
+		{ .act = INIT(&table[1].inv, my_update_true)    },
+		{ .act = INIT(&table[2].inv, my_update_false)   }
+	};
+	
+	struct script sc = {0};
+	struct action act;
+
+	GREATEST_ASSERT(script_append(&sc, &table[0].act));
+	GREATEST_ASSERT(script_append(&sc, &table[1].act));
+	GREATEST_ASSERT(script_append(&sc, &table[2].act));
+
+	/* Now convert this script into an action itself. */
+	script_action(&sc, &act);
+
+	/* Draw and input before updating. */
+	action_handle(&act, &(union event){0});
+	action_draw(&act);
+
+	/* [0] -> [1] */
+	GREATEST_ASSERT(!action_update(&act, 0));
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	action_handle(&act, &(union event){0});
+	action_draw(&act);
+
+	/* [1] -> [2] */
+	GREATEST_ASSERT(!action_update(&act, 0));
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	action_handle(&act, &(union event){0});
+	action_draw(&act);
+
+	/* 2 stays, it never ends. */
+	GREATEST_ASSERT(!action_update(&act, 0));
+	GREATEST_ASSERT(!action_update(&act, 0));
+	GREATEST_ASSERT(!action_update(&act, 0));
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 3);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	table[2].act.update = my_update_true;
+	GREATEST_ASSERT(action_update(&act, 0));
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 0);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 4);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 0);
+
+	/* Also dispose resources. */
+	action_finish(&act);
+	GREATEST_ASSERT_EQ(table[0].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[0].inv.finish, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.update, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[1].inv.finish, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.handle, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.update, 4);
+	GREATEST_ASSERT_EQ(table[2].inv.draw, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.end, 1);
+	GREATEST_ASSERT_EQ(table[2].inv.finish, 1);
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_action)
+{
+	GREATEST_RUN_TEST(action_simple);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_basics);
+	GREATEST_RUN_SUITE(suite_action);
+	GREATEST_MAIN_END();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-action.c	Tue Oct 13 09:38:44 2020 +0200
@@ -0,0 +1,389 @@
+/*
+ * test-action.c -- test actions
+ *
+ * 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 GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+#include <core/action.h>
+#include <core/event.h>
+
+struct invokes {
+	bool handle;
+	bool update;
+	bool draw;
+	bool end;
+	bool finish;
+};
+
+static union event dummy;
+
+#define INIT(dat, up) {         \
+        .data = (dat),          \
+        .handle = my_handle,    \
+        .update = (up),         \
+        .draw = my_draw,        \
+        .end = my_end,          \
+        .finish = my_finish     \
+}
+
+static void
+my_handle(struct action *act, const union event *ev)
+{
+	(void)ev;
+
+	((struct invokes *)act->data)->handle = true;
+}
+
+static bool
+my_update_false(struct action *act, unsigned int ticks)
+{
+	(void)ticks;
+
+	((struct invokes *)act->data)->update = true;
+
+	return false;
+}
+
+static bool
+my_update_true(struct action *act, unsigned int ticks)
+{
+	(void)ticks;
+
+	((struct invokes *)act->data)->update = true;
+
+	return true;
+}
+
+static void
+my_draw(struct action *act)
+{
+	((struct invokes *)act->data)->draw = true;
+}
+
+static void
+my_end(struct action *act)
+{
+	((struct invokes *)act->data)->end = true;
+}
+
+static void
+my_finish(struct action *act)
+{
+	((struct invokes *)act->data)->finish = true;
+}
+
+GREATEST_TEST
+basics_handle(void)
+{
+	struct invokes inv = {0};
+	struct action act = INIT(&inv, my_update_true);
+
+	action_handle(&act, &dummy);
+
+	GREATEST_ASSERT(inv.handle);
+	GREATEST_ASSERT(!inv.update);
+	GREATEST_ASSERT(!inv.draw);
+	GREATEST_ASSERT(!inv.end);
+	GREATEST_ASSERT(!inv.finish);
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_update(void)
+{
+	struct {
+		struct invokes inv;
+		struct action act;
+	} table[] = {
+		{ .act = INIT(&table[0], my_update_true)        },
+		{ .act = INIT(&table[1], my_update_false)       }
+	};
+
+	/* True version. */
+	GREATEST_ASSERT(action_update(&table[0].act, 0));
+	GREATEST_ASSERT(!table[0].inv.handle);
+	GREATEST_ASSERT(table[0].inv.update);
+	GREATEST_ASSERT(!table[0].inv.draw);
+	GREATEST_ASSERT(!table[0].inv.end);
+	GREATEST_ASSERT(!table[0].inv.finish);
+
+	/* False version. */
+	GREATEST_ASSERT(!action_update(&table[1].act, 0));
+	GREATEST_ASSERT(!table[1].inv.handle);
+	GREATEST_ASSERT(table[1].inv.update);
+	GREATEST_ASSERT(!table[1].inv.draw);
+	GREATEST_ASSERT(!table[1].inv.end);
+	GREATEST_ASSERT(!table[1].inv.finish);
+	
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_draw(void)
+{
+	struct invokes inv;
+	struct action act = INIT(&inv, my_update_true);
+
+	action_draw(&act);
+
+	GREATEST_ASSERT(!inv.handle);
+	GREATEST_ASSERT(!inv.update);
+	GREATEST_ASSERT(inv.draw);
+	GREATEST_ASSERT(!inv.end);
+	GREATEST_ASSERT(!inv.finish);
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_end(void)
+{
+	struct invokes inv;
+	struct action act = INIT(&inv, my_update_true);
+
+	action_end(&act);
+
+	GREATEST_ASSERT(!inv.handle);
+	GREATEST_ASSERT(!inv.update);
+	GREATEST_ASSERT(!inv.draw);
+	GREATEST_ASSERT(inv.end);
+	GREATEST_ASSERT(!inv.finish);
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_finish(void)
+{
+	struct invokes inv;
+	struct action act = INIT(&inv, my_update_true);
+
+	action_finish(&act);
+
+	GREATEST_ASSERT(!inv.handle);
+	GREATEST_ASSERT(!inv.update);
+	GREATEST_ASSERT(!inv.draw);
+	GREATEST_ASSERT(!inv.end);
+	GREATEST_ASSERT(inv.finish);
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_basics)
+{
+	GREATEST_RUN_TEST(basics_handle);
+	GREATEST_RUN_TEST(basics_update);
+	GREATEST_RUN_TEST(basics_draw);
+	GREATEST_RUN_TEST(basics_end);
+	GREATEST_RUN_TEST(basics_finish);
+}
+
+GREATEST_TEST
+stack_add(void)
+{
+	struct action_stack st = {0};
+	struct action act = {0};
+
+	GREATEST_ASSERT(action_stack_add(&st, &act));
+
+	/* Now fill up. */
+	for (int i = 0; i < ACTION_STACK_MAX - 1; ++i)
+		GREATEST_ASSERT(action_stack_add(&st, &act));
+
+	/* This one should not fit in. */
+	GREATEST_ASSERT(!action_stack_add(&st, &act));
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+stack_handle(void)
+{
+	struct {
+		bool called;
+		struct action act;
+	} table[] = {
+		{ false, { .data = &table[0].called, .handle = my_handle } },
+		{ false, { .data = &table[1].called, .handle = my_handle } },
+		{ false, { .data = &table[2].called, .handle = my_handle } },
+	};
+
+	struct action_stack st = {0};
+
+	action_stack_add(&st, &table[0].act);
+	action_stack_add(&st, &table[1].act);
+	action_stack_add(&st, &table[2].act);
+	action_stack_handle(&st, &dummy);
+
+	GREATEST_ASSERT(table[0].called);
+	GREATEST_ASSERT(table[1].called);
+	GREATEST_ASSERT(table[2].called);
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+stack_update(void)
+{
+	struct {
+		struct invokes inv;
+		struct action act;
+	} table[] = {
+		{ .act = INIT(&table[0], my_update_false)       },
+		{ .act = INIT(&table[1], my_update_true)        },
+		{ .act = INIT(&table[2], my_update_false)       },
+		{ .act = INIT(&table[3], my_update_false)       },
+		{ .act = INIT(&table[4], my_update_true)        },
+		{ .act = INIT(&table[5], my_update_true)        },
+		{ .act = INIT(&table[6], my_update_false)	},
+	};
+
+	struct action_stack st = {0};
+
+	action_stack_add(&st, &table[0].act);
+	action_stack_add(&st, &table[1].act);
+	action_stack_add(&st, &table[2].act);
+	action_stack_add(&st, &table[3].act);
+	action_stack_add(&st, &table[4].act);
+	action_stack_add(&st, &table[5].act);
+	action_stack_add(&st, &table[6].act);
+
+	GREATEST_ASSERT(!action_stack_update(&st, 0));
+
+	GREATEST_ASSERT(!table[0].inv.handle);
+	GREATEST_ASSERT(!table[1].inv.handle);
+	GREATEST_ASSERT(!table[2].inv.handle);
+	GREATEST_ASSERT(!table[3].inv.handle);
+	GREATEST_ASSERT(!table[4].inv.handle);
+	GREATEST_ASSERT(!table[5].inv.handle);
+	GREATEST_ASSERT(!table[6].inv.handle);
+
+	GREATEST_ASSERT(table[0].inv.update);
+	GREATEST_ASSERT(table[1].inv.update);
+	GREATEST_ASSERT(table[2].inv.update);
+	GREATEST_ASSERT(table[3].inv.update);
+	GREATEST_ASSERT(table[4].inv.update);
+	GREATEST_ASSERT(table[5].inv.update);
+	GREATEST_ASSERT(table[6].inv.update);
+
+	GREATEST_ASSERT(!table[0].inv.draw);
+	GREATEST_ASSERT(!table[1].inv.draw);
+	GREATEST_ASSERT(!table[2].inv.draw);
+	GREATEST_ASSERT(!table[3].inv.draw);
+	GREATEST_ASSERT(!table[4].inv.draw);
+	GREATEST_ASSERT(!table[5].inv.draw);
+	GREATEST_ASSERT(!table[6].inv.draw);
+
+	GREATEST_ASSERT(!table[0].inv.end);
+	GREATEST_ASSERT(table[1].inv.end);
+	GREATEST_ASSERT(!table[2].inv.end);
+	GREATEST_ASSERT(!table[3].inv.end);
+	GREATEST_ASSERT(table[4].inv.end);
+	GREATEST_ASSERT(table[5].inv.end);
+	GREATEST_ASSERT(!table[6].inv.end);
+
+	GREATEST_ASSERT(!table[0].inv.finish);
+	GREATEST_ASSERT(table[1].inv.finish);
+	GREATEST_ASSERT(!table[2].inv.finish);
+	GREATEST_ASSERT(!table[3].inv.finish);
+	GREATEST_ASSERT(table[4].inv.finish);
+	GREATEST_ASSERT(table[5].inv.finish);
+	GREATEST_ASSERT(!table[6].inv.finish);
+
+	/* The following must still be there. */
+	GREATEST_ASSERT_EQ(st.actions[0], &table[0].act);
+	GREATEST_ASSERT_EQ(st.actions[2], &table[2].act);
+	GREATEST_ASSERT_EQ(st.actions[3], &table[3].act);
+	GREATEST_ASSERT_EQ(st.actions[6], &table[6].act);
+
+	/* The following must have been NULL-ed. */
+	GREATEST_ASSERT_EQ(st.actions[1], NULL);
+	GREATEST_ASSERT_EQ(st.actions[4], NULL);
+	GREATEST_ASSERT_EQ(st.actions[5], NULL);
+	
+	/*
+	 * Now make all actions to return true and check if it cleans the stack.
+	 */
+	table[0].act.update =
+	    table[2].act.update =
+	    table[3].act.update =
+	    table[6].act.update = my_update_true;
+
+	GREATEST_ASSERT(action_stack_update(&st, 0));
+	GREATEST_ASSERT_EQ(st.actions[0], NULL);
+	GREATEST_ASSERT_EQ(st.actions[1], NULL);
+	GREATEST_ASSERT_EQ(st.actions[2], NULL);
+	GREATEST_ASSERT_EQ(st.actions[3], NULL);
+	GREATEST_ASSERT_EQ(st.actions[4], NULL);
+	GREATEST_ASSERT_EQ(st.actions[5], NULL);
+	GREATEST_ASSERT_EQ(st.actions[6], NULL);
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+stack_finish(void)
+{
+	struct {
+		struct invokes inv;
+		struct action act;
+	} table[] = {
+		{ .act = INIT(&table[0], my_update_true)        },
+		{ .act = INIT(&table[0], my_update_false)       },
+	};
+	struct action_stack st = {0};
+
+	action_stack_add(&st, &table[0].act);
+	action_stack_add(&st, &table[1].act);
+	action_stack_finish(&st);
+
+	GREATEST_ASSERT(!table[0].inv.handle);
+	GREATEST_ASSERT(!table[0].inv.update);
+	GREATEST_ASSERT(!table[0].inv.draw);
+	GREATEST_ASSERT(table[0].inv.end);
+	GREATEST_ASSERT(table[0].inv.finish);
+
+	GREATEST_ASSERT(!table[0].inv.handle);
+	GREATEST_ASSERT(!table[0].inv.update);
+	GREATEST_ASSERT(!table[0].inv.draw);
+	GREATEST_ASSERT(table[0].inv.end);
+	GREATEST_ASSERT(table[0].inv.finish);
+
+	/* They should also be NULL'ed. */
+	GREATEST_ASSERT_EQ(st.actions[0], NULL);
+	GREATEST_ASSERT_EQ(st.actions[1], NULL);
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_stack)
+{
+	GREATEST_RUN_TEST(stack_add);
+	GREATEST_RUN_TEST(stack_handle);
+	GREATEST_RUN_TEST(stack_update);
+	GREATEST_RUN_TEST(stack_finish);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_basics);
+	GREATEST_RUN_SUITE(suite_stack);
+	GREATEST_MAIN_END();
+
+	return 0;
+}
--- a/tests/test-script.c	Sun Oct 11 15:01:26 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,185 +0,0 @@
-/*
- * test-script.c -- test script action
- *
- * 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 <greatest.h>
-
-#include <core/script.h>
-
-static bool
-truth(struct action *a, unsigned int ticks)
-{
-	(void)a;
-	(void)ticks;
-
-	return true;
-}
-
-static bool
-nope(struct action *a, unsigned int ticks)
-{
-	(void)a;
-	(void)ticks;
-
-	return false;
-}
-
-TEST
-append(void)
-{
-	int ref1 = 0, ref2 = 0;
-	struct script sc;
-
-	script_init(&sc);
-
-	/* ref1 */
-	script_append(&sc, &(struct action) {
-		.update = truth,
-		.data = &ref1
-	});
-
-	ASSERT(sc.head);
-	ASSERT_EQ(sc.head->action.data, &ref1);
-
-	/* ref2 */
-	script_append(&sc, &(struct action) {
-		.update = truth,
-		.data = &ref2
-	});
-
-	ASSERT(sc.head);
-	ASSERT_EQ(sc.head->action.data, &ref1);
-	ASSERT_EQ(sc.head->next->action.data, &ref2);
-
-	script_finish(&sc);
-
-	PASS();
-}
-
-TEST
-finish(void)
-{
-	struct script sc;
-
-	script_init(&sc);
-	script_append(&sc, &(struct action) {
-		.update = truth,
-	});
-	script_append(&sc, &(struct action) {
-		.update = truth,
-	});
-	script_finish(&sc);
-
-	ASSERT(!sc.head);
-	PASS();
-}
-
-SUITE(manipulation)
-{
-	RUN_TEST(append);
-	RUN_TEST(finish);
-}
-
-TEST
-advance(void)
-{
-	int ref1 = 0, ref2 = 0;
-	struct script sc;
-
-	script_init(&sc);
-
-	script_append(&sc, &(struct action) {
-		.update = truth,
-		.data = &ref1
-	});
-	script_append(&sc, &(struct action) {
-		.update = truth,
-		.data = &ref2
-	});
-
-	/* Start -> 1 */
-	script_start(&sc);
-
-	ASSERT(sc.iter);
-	ASSERT_EQ(sc.iter->action.data, &ref1);
-
-	/* Update -> 2 */
-	script_update(&sc, 0);
-
-	ASSERT(sc.iter);
-	ASSERT_EQ(sc.iter->action.data, &ref2);
-
-	/* Update -> end */
-	script_update(&sc, 0);
-
-	ASSERT(!sc.iter);
-
-	script_finish(&sc);
-
-	PASS();
-}
-
-TEST
-dont_advance(void)
-{
-	int ref1 = 0, ref2 = 0;
-	struct script sc;
-
-	script_init(&sc);
-
-	script_append(&sc, &(struct action) {
-		.update = nope,
-		.data = &ref1
-	});
-	script_append(&sc, &(struct action) {
-		.update = truth,
-		.data = &ref2
-	});
-
-	/* Start -> 1 */
-	script_start(&sc);
-
-	ASSERT(sc.iter);
-	ASSERT_EQ(sc.iter->action.data, &ref1);
-
-	/* Update -> 1 */
-	script_update(&sc, 0);
-
-	ASSERT(sc.iter);
-	ASSERT_EQ(sc.iter->action.data, &ref1);
-
-	script_finish(&sc);
-
-	PASS();
-}
-
-SUITE(usage)
-{
-	RUN_TEST(advance);
-	RUN_TEST(dont_advance);
-}
-
-GREATEST_MAIN_DEFS();
-
-int
-main(int argc, char **argv)
-{
-	GREATEST_MAIN_BEGIN();
-	RUN_SUITE(manipulation);
-	RUN_SUITE(usage);
-	GREATEST_MAIN_END();
-}