changeset 645:83781cc87fca

core: rework game stack state mechanism The current model was fundamentally broken as the state could continue its execution when calling mlk_game_pop from itself (e.g. in update). The current model uses a sjlj mechanism with mlk_game_push/pop being disallowed in special state function like end, finish, suspend.
author David Demelier <markand@malikania.fr>
date Sun, 04 Feb 2024 15:24:00 +0100
parents 6d0f4edb79f8
children 7e1eb7f6c049
files .clang examples/CMakeLists.txt examples/example-states/CMakeLists.txt examples/example-states/assets/fonts/breamcatcher.otf examples/example-states/assets/fonts/zenda.ttf examples/example-states/assets/images/dvd.png examples/example-states/example-states.c examples/example-states/state-menu.c examples/example-states/state-menu.h examples/example-states/state-play.c examples/example-states/state-play.h examples/example-states/state-splash.c examples/example-states/state-splash.h libmlk-core/CMakeLists.txt libmlk-core/mlk/core/game.c libmlk-core/mlk/core/game.h libmlk-core/mlk/core/js/js-event.c libmlk-core/mlk/core/js/js-event.h libmlk-core/mlk/core/js/js-game.c libmlk-core/mlk/core/js/js-game.h libmlk-core/mlk/core/js/js-state.c libmlk-core/mlk/core/js/js-state.h libmlk-core/mlk/core/js/js.c libmlk-core/mlk/core/state.h
diffstat 24 files changed, 1266 insertions(+), 131 deletions(-) [+]
line wrap: on
line diff
--- a/.clang	Sat Dec 23 09:34:04 2023 +0100
+++ b/.clang	Sun Feb 04 15:24:00 2024 +0100
@@ -1,6 +1,7 @@
 -I/usr/include/SDL2
 -Iextern/libdt
 -Iextern/libsqlite
+-Iextern/libduktape
 -Ilibmlk-core
 -Ilibmlk-example
 -Ilibmlk-rpg
--- a/examples/CMakeLists.txt	Sat Dec 23 09:34:04 2023 +0100
+++ b/examples/CMakeLists.txt	Sun Feb 04 15:24:00 2024 +0100
@@ -34,6 +34,7 @@
 	example-message
 	example-notify
 	example-sprite
+	example-states
 	example-tileset
 	example-trace
 	example-ui
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/example-states/CMakeLists.txt	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,47 @@
+#
+# CMakeLists.txt -- CMake build system for Molko's Engine
+#
+# Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+project(example-states)
+
+set(
+	SOURCES
+	${example-states_SOURCE_DIR}/example-states.c
+	${example-states_SOURCE_DIR}/state-menu.c
+	${example-states_SOURCE_DIR}/state-menu.h
+	${example-states_SOURCE_DIR}/state-play.c
+	${example-states_SOURCE_DIR}/state-play.h
+	${example-states_SOURCE_DIR}/state-splash.c
+	${example-states_SOURCE_DIR}/state-splash.h
+)
+
+set(
+	ASSETS
+	${example-states_SOURCE_DIR}/assets/fonts/breamcatcher.otf
+	${example-states_SOURCE_DIR}/assets/fonts/zenda.ttf
+	${example-states_SOURCE_DIR}/assets/images/dvd.png
+)
+
+mlk_executable(
+	NAME example-states
+	FOLDER examples
+	LIBRARIES libmlk-example
+	SOURCES ${ASSETS} ${SOURCES}
+	ASSETS ${ASSETS}
+)
+
+source_group(TREE ${example-states_SOURCE_DIR} FILES ${ASSETS} ${SOURCES})
Binary file examples/example-states/assets/fonts/breamcatcher.otf has changed
Binary file examples/example-states/assets/fonts/zenda.ttf has changed
Binary file examples/example-states/assets/images/dvd.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/example-states/example-states.c	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,56 @@
+/*
+ * example-states.c -- example on how states stack works
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <mlk/core/game.h>
+#include <mlk/core/panic.h>
+
+#include <mlk/example/example.h>
+
+#include "state-splash.h"
+
+static void
+init(void)
+{
+	if (mlk_example_init("example-states") < 0)
+		mlk_panic();
+}
+
+static void
+run(void)
+{
+	mlk_game_loop(state_splash_new());
+}
+
+static void
+quit(void)
+{
+	mlk_example_finish();
+}
+
+int
+main(int argc, char **argv)
+{
+	(void)argc;
+	(void)argv;
+
+	init();
+	run();
+	quit();
+
+	return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/example-states/state-menu.c	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,159 @@
+/*
+ * state-menu.c -- basic main menu
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/event.h>
+#include <mlk/core/font.h>
+#include <mlk/core/game.h>
+#include <mlk/core/painter.h>
+#include <mlk/core/panic.h>
+#include <mlk/core/state.h>
+#include <mlk/core/util.h>
+
+#include <mlk/ui/align.h>
+#include <mlk/ui/button.h>
+#include <mlk/ui/label.h>
+
+#include <assets/fonts/breamcatcher.h>
+
+#include "state-menu.h"
+#include "state-play.h"
+
+#define BG         0xdfededff
+#define BUTTON_H   (mlk_window.h / 12)
+#define BUTTON_W   (mlk_window.w / 6)
+#define LABEL_FG   0x4d6a94ff
+#define LABEL_FONT assets_fonts_breamcatcher
+#define LABEL_SIZE 32
+#define LABEL_TEXT "Sample Game"
+
+#define MENU(self) MLK_CONTAINER_OF(self, struct menu, state)
+
+struct menu {
+	struct mlk_font font_header;
+	struct mlk_label_style label_style_header;
+	struct mlk_label label_header;
+	struct mlk_button button_play;
+	struct mlk_button button_quit;
+	struct mlk_state state;
+};
+
+static void
+state_menu__start(struct mlk_state *self)
+{
+	struct menu *menu = MENU(self);
+	unsigned int w, h;
+
+	/* Top header. */
+	if (mlk_font_openmem(&menu->font_header, LABEL_FONT, sizeof (LABEL_FONT), LABEL_SIZE) < 0)
+		mlk_panic();
+
+	menu->font_header.style = MLK_FONT_STYLE_ANTIALIASED;
+	menu->label_style_header.font = &menu->font_header;
+	menu->label_style_header.color = LABEL_FG;
+	menu->label_header.text = LABEL_TEXT;
+	menu->label_header.style = &menu->label_style_header;
+	mlk_label_query(&menu->label_header, &w, &h);
+	mlk_align(MLK_ALIGN_TOP,
+	          &menu->label_header.x,
+	          &menu->label_header.y,
+	          w,
+	          h,
+	          0,
+	          10,
+	          mlk_window.w,
+	          0);
+
+	/* Buttons. */
+	menu->button_play.text = "Play";
+	menu->button_play.w    = BUTTON_W;
+	menu->button_play.h    = BUTTON_H;
+
+	menu->button_quit.text = "Quit";
+	menu->button_quit.w    = BUTTON_W;
+	menu->button_quit.h    = BUTTON_H;
+
+	mlk_align(MLK_ALIGN_CENTER,
+	          &menu->button_play.x,
+	          &menu->button_play.y,
+	          menu->button_play.w,
+	          menu->button_play.h,
+	          0,
+	          0,
+	          mlk_window.w,
+	          mlk_window.h
+	);
+
+	menu->button_quit.x = menu->button_play.x;
+	menu->button_quit.y = menu->button_play.y + BUTTON_H + 20;
+}
+
+static void
+state_menu__handle(struct mlk_state *self, const union mlk_event *event)
+{
+	struct menu *menu = MENU(self);
+
+	switch (event->type) {
+	case MLK_EVENT_QUIT:
+		mlk_game_quit();
+		break;
+	default:
+		if (mlk_button_handle(&menu->button_play, event))
+			mlk_game_push(state_play_new());
+		else if (mlk_button_handle(&menu->button_quit, event))
+			mlk_game_quit();
+		break;
+	}
+}
+
+static void
+state_menu__update(struct mlk_state *self, unsigned int ticks)
+{
+	struct menu *menu = MENU(self);
+
+	mlk_button_update(&menu->button_play, ticks);
+	mlk_button_update(&menu->button_quit, ticks);
+}
+
+static void
+state_menu__draw(struct mlk_state *self)
+{
+	struct menu *menu = MENU(self);
+
+	mlk_painter_set_color(BG);
+	mlk_painter_clear();
+	mlk_label_draw(&menu->label_header);
+	mlk_button_draw(&menu->button_play);
+	mlk_button_draw(&menu->button_quit);
+	mlk_painter_present();
+}
+
+struct mlk_state *
+state_menu_new(void)
+{
+	struct menu *menu;
+
+	menu = mlk_alloc_new0(1, sizeof (*menu));
+	menu->state.name = "menu";
+	menu->state.start = state_menu__start;
+	menu->state.handle = state_menu__handle;
+	menu->state.update = state_menu__update;
+	menu->state.draw = state_menu__draw;
+
+	return &menu->state;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/example-states/state-menu.h	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,27 @@
+/*
+ * state-menu.h -- basic main menu
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef STATE_MENU_H
+#define STATE_MENU_H
+
+struct mlk_state;
+
+struct mlk_state *
+state_menu_new(void);
+
+#endif /* !STATE_MENU_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/example-states/state-play.c	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,179 @@
+/*
+ * state-play.c -- very funny game
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/event.h>
+#include <mlk/core/game.h>
+#include <mlk/core/image.h>
+#include <mlk/core/painter.h>
+#include <mlk/core/panic.h>
+#include <mlk/core/state.h>
+#include <mlk/core/texture.h>
+#include <mlk/core/util.h>
+
+#include <mlk/ui/align.h>
+#include <mlk/ui/button.h>
+
+#include <assets/images/dvd.h>
+
+#include "state-menu.h"
+#include "state-play.h"
+
+#define BG         0x99c2dbff
+#define BUTTON_H   (mlk_window.h / 20)
+#define BUTTON_W   (mlk_window.w / 10)
+#define LOGO       assets_images_dvd
+#define LOGO_SPEED 100
+
+#define PLAY(self) MLK_CONTAINER_OF(self, struct play, state)
+
+struct play {
+	struct mlk_button button_leave;
+	struct mlk_texture logo;
+	int x;
+	int y;
+	int dx;
+	int dy;
+	struct mlk_state state;
+};
+
+static void
+state_play__start(struct mlk_state *self)
+{
+	struct play *play = PLAY(self);
+
+	if (mlk_image_openmem(&play->logo, LOGO, sizeof (LOGO)) < 0)
+		mlk_panic();
+
+	mlk_align(MLK_ALIGN_CENTER,
+	          &play->x,
+	          &play->y,
+	          play->logo.w,
+	          play->logo.h,
+	          0,
+	          0,
+	          mlk_window.w,
+	          mlk_window.h
+	);
+
+	play->button_leave.text = "menu";
+	play->button_leave.w = BUTTON_W;
+	play->button_leave.h = BUTTON_H;
+
+	mlk_align(MLK_ALIGN_BOTTOM,
+	          &play->button_leave.x,
+	          &play->button_leave.y,
+	          play->button_leave.w,
+	          play->button_leave.h,
+	          0,
+	          0,
+	          mlk_window.w,
+	          mlk_window.h - 20
+	);
+
+	play->dx = play->dy = 1;
+}
+
+static void
+state_play__handle(struct mlk_state *self, const union mlk_event *event)
+{
+	struct play *play = PLAY(self);
+
+	if (mlk_button_handle(&play->button_leave, event))
+		mlk_game_pop();
+
+	switch (event->type) {
+	case MLK_EVENT_QUIT:
+		mlk_game_quit();
+		break;
+	case MLK_EVENT_KEYUP:
+		switch (event->key.key) {
+		case MLK_KEY_ESCAPE:
+			mlk_game_pop();
+			break;
+		default:
+			break;
+		}
+		break;
+	default:
+		break;
+	}
+}
+
+static void
+state_play__update(struct mlk_state *self, unsigned int ticks)
+{
+	struct play *play = PLAY(self);
+
+	play->x += play->dx * (LOGO_SPEED * ticks / 1000);
+	play->y += play->dy * (LOGO_SPEED * ticks / 1000);
+
+	if (play->x < 0) {
+		play->dx = 1;
+		play->x = 0;
+	} else if (play->x + play->logo.w >= mlk_window.w) {
+		play->dx = -1;
+		play->x = mlk_window.w - play->logo.w;
+	}
+
+	if (play->y < 0) {
+		play->dy = 1;
+		play->y = 0;
+	} else if (play->y + play->logo.h >= mlk_window.h) {
+		play->dy = -1;
+		play->y = mlk_window.h - play->logo.h;
+	}
+
+	mlk_button_update(&play->button_leave, ticks);
+}
+
+static void
+state_play__draw(struct mlk_state *self)
+{
+	struct play *play = PLAY(self);
+
+	mlk_painter_set_color(BG);
+	mlk_painter_clear();
+	mlk_texture_draw(&play->logo, play->x, play->y);
+	mlk_button_draw(&play->button_leave);
+	mlk_painter_present();
+}
+
+static void
+state_play__finish(struct mlk_state *self)
+{
+	struct play *play = PLAY(self);
+
+	mlk_texture_finish(&play->logo);
+}
+
+struct mlk_state *
+state_play_new(void)
+{
+	struct play *play;
+
+	play = mlk_alloc_new0(1, sizeof (*play));
+	play->state.name = "play";
+	play->state.start = state_play__start;
+	play->state.handle = state_play__handle;
+	play->state.update = state_play__update;
+	play->state.draw = state_play__draw;
+	play->state.finish = state_play__finish;
+
+	return &play->state;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/example-states/state-play.h	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,27 @@
+/*
+ * state-play.h -- very funny game
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef STATE_PLAY_H
+#define STATE_PLAY_H
+
+struct mlk_state;
+
+struct mlk_state *
+state_play_new(void);
+
+#endif /* !STATE_PLAY_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/example-states/state-splash.c	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,123 @@
+/*
+ * state-splash.c -- minimal splash screen
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/font.h>
+#include <mlk/core/game.h>
+#include <mlk/core/painter.h>
+#include <mlk/core/panic.h>
+#include <mlk/core/state.h>
+#include <mlk/core/texture.h>
+#include <mlk/core/util.h>
+#include <mlk/core/window.h>
+
+#include <mlk/ui/align.h>
+
+#include <assets/fonts/zenda.h>
+
+#include "state-menu.h"
+#include "state-splash.h"
+
+#define TITLE        "malikania"
+#define SIZE         80
+#define BG           0xf4f4f4ff
+#define FG           0x29366fff
+#define DELAY        1000
+
+#define SPLASH(self) MLK_CONTAINER_OF(self, struct splash, state)
+
+struct splash {
+	unsigned int elapsed;
+	struct mlk_texture texture;
+	int x;
+	int y;
+	struct mlk_state state;
+};
+
+static void
+state_splash__start(struct mlk_state *self)
+{
+	struct splash *splash = SPLASH(self);
+	struct mlk_font font;
+
+	if (mlk_font_openmem(&font, assets_fonts_zenda, sizeof (assets_fonts_zenda), SIZE) < 0)
+		mlk_panic();
+
+	font.style = MLK_FONT_STYLE_ANTIALIASED;
+
+	if (mlk_font_render(&font, &splash->texture, TITLE, FG) < 0)
+		mlk_panic();
+
+	mlk_align(MLK_ALIGN_CENTER, 
+	          &splash->x,
+	          &splash->y,
+	          splash->texture.w,
+	          splash->texture.h,
+	          0,
+	          0,
+	          mlk_window.w,
+	          mlk_window.h
+	);
+
+	mlk_font_finish(&font);
+}
+
+static void
+state_splash__update(struct mlk_state *self, unsigned int ticks)
+{
+	struct splash *splash = SPLASH(self);
+
+	splash->elapsed += ticks;
+
+	if (splash->elapsed >= DELAY)
+		mlk_game_push(state_menu_new());
+}
+
+static void
+state_splash__draw(struct mlk_state *self)
+{
+	struct splash *splash = SPLASH(self);
+
+	mlk_painter_set_color(BG);
+	mlk_painter_clear();
+	mlk_texture_draw(&splash->texture, splash->x, splash->y);
+	mlk_painter_present();
+}
+
+static void
+state_splash__finish(struct mlk_state *self)
+{
+	struct splash *splash = SPLASH(self);
+
+	mlk_texture_finish(&splash->texture);
+}
+
+struct mlk_state *
+state_splash_new(void)
+{
+	struct splash *splash;
+
+	splash = mlk_alloc_new0(1, sizeof (*splash));
+	splash->state.name = "splash";
+	splash->state.start = state_splash__start;
+	splash->state.update = state_splash__update;
+	splash->state.draw = state_splash__draw;
+	splash->state.finish = state_splash__finish;
+
+	return &splash->state;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/example-states/state-splash.h	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,27 @@
+/*
+ * state-splash.h -- minimal splash screen
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef STATE_SPLASH_H
+#define STATE_SPLASH_H
+
+struct mlk_state;
+
+struct mlk_state *
+state_splash_new(void);
+
+#endif /* !STATE_SPLASH_H */
--- a/libmlk-core/CMakeLists.txt	Sat Dec 23 09:34:04 2023 +0100
+++ b/libmlk-core/CMakeLists.txt	Sun Feb 04 15:24:00 2024 +0100
@@ -161,6 +161,8 @@
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-event.h
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-font.c
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-font.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-game.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-game.h
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-music.c
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-music.h
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-painter.c
@@ -169,6 +171,8 @@
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-sound.h
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-sprite.c
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-sprite.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-state.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-state.h
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-texture.c
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-texture.h
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-util.c
--- a/libmlk-core/mlk/core/game.c	Sat Dec 23 09:34:04 2023 +0100
+++ b/libmlk-core/mlk/core/game.c	Sun Feb 04 15:24:00 2024 +0100
@@ -18,6 +18,7 @@
 
 #include <assert.h>
 #include <string.h>
+#include <setjmp.h>
 
 #include "clock.h"
 #include "core_p.h"
@@ -28,7 +29,16 @@
 #include "util.h"
 #include "window.h"
 
+enum {
+	JMP_OK = 0,
+	JMP_PUSH,
+	JMP_POP
+};
+
 static struct mlk_state *states[8];
+static jmp_buf game_jmp_buf;
+static int game_jmp_allowed;
+static int game_run = 1;
 
 struct mlk_game mlk_game = {
 	.states = states,
@@ -42,68 +52,33 @@
 		mlk_game.states[i] = NULL;
 }
 
-int
+_Noreturn int
 mlk_game_push(struct mlk_state *state)
 {
 	assert(state);
-
-	if (!mlk_game.state) {
-		mlk_game.state = &mlk_game.states[0];
-		mlk_state_start(*mlk_game.state = state);
-		return 0;
-	}
+	assert(game_jmp_allowed);
 
 	if (mlk_game.state == &mlk_game.states[mlk_game.statesz - 1])
-		return mlk_errf(_("no space in game states stack"));
+		mlk_errf(_("no space in game states stack"));
+
+	mlk_game.state[1] = state;
+	longjmp(game_jmp_buf, JMP_PUSH);
+}
 
-	mlk_state_suspend(*mlk_game.state);
-	mlk_state_start(*(++mlk_game.state) = state);
+_Noreturn void
+mlk_game_pop(void)
+{
+	assert(game_jmp_allowed);
 
-	return 0;
+	longjmp(game_jmp_buf, JMP_POP);
 }
 
 void
-mlk_game_pop(void)
+mlk_game_loop(struct mlk_state *state)
 {
-	if (!mlk_game.state)
-		return;
-
-	mlk_state_end(*mlk_game.state);
-	mlk_state_finish(*mlk_game.state);
-
-	if (mlk_game.state == mlk_game.states)
-		mlk_game.state = NULL;
-	else
-		mlk_state_resume(*--mlk_game.state);
-}
-
-void
-mlk_game_handle(const union mlk_event *ev)
-{
-	assert(ev);
+	assert(state);
 
-	if (mlk_game.state && !(mlk_game.inhibit & MLK_GAME_INHIBIT_INPUT))
-		mlk_state_handle(*mlk_game.state, ev);
-}
-
-void
-mlk_game_update(unsigned int ticks)
-{
-	if (mlk_game.state && !(mlk_game.inhibit & MLK_GAME_INHIBIT_UPDATE))
-		mlk_state_update(*mlk_game.state, ticks);
-}
-
-void
-mlk_game_draw(void)
-{
-	if (mlk_game.state && !(mlk_game.inhibit & MLK_GAME_INHIBIT_DRAW))
-		mlk_state_draw(*mlk_game.state);
-}
-
-void
-mlk_game_loop(void)
-{
-	struct mlk_clock clock = {0};
+	struct mlk_clock clock = {};
 	unsigned int elapsed = 0;
 	unsigned int frametime;
 
@@ -113,36 +88,87 @@
 		/* Assuming 60.0 FPS. */
 		frametime = 1000.0 / 60.0;
 
-	while (mlk_game.state) {
-		mlk_clock_start(&clock);
+	game_jmp_allowed = 1;
+
+	while (game_run) {
+		switch (setjmp(game_jmp_buf)) {
+		case JMP_OK:
+			/* Initial entrypoint. */
+			if (!mlk_game.state) {
+				mlk_game.states[0] = state;
+				mlk_game.state = &mlk_game.states[0];
+				mlk_state_start(state);
+			}
+
+			mlk_clock_start(&clock);
 
-		for (union mlk_event ev; mlk_event_poll(&ev); )
-			mlk_game_handle(&ev);
+			for (union mlk_event ev; mlk_event_poll(&ev); ) {
+				if (mlk_game.state && !(mlk_game.inhibit & MLK_GAME_INHIBIT_INPUT))
+					mlk_state_handle(*mlk_game.state, ev);
+			}
 
-		mlk_game_update(elapsed);
-		mlk_game_draw();
+			if (mlk_game.state && !(mlk_game.inhibit & MLK_GAME_INHIBIT_UPDATE))
+				mlk_state_update(*mlk_game.state, ticks);
+			if (mlk_game.state && !(mlk_game.inhibit & MLK_GAME_INHIBIT_DRAW))
+				mlk_state_draw(*mlk_game.state);
+
+			/*
+			 * If vsync is enabled, it should have wait, otherwise
+			 * sleep a little to save CPU cycles.
+			 */
+			if ((elapsed = mlk_clock_elapsed(&clock)) < frametime)
+				mlk_util_sleep(frametime - elapsed);
+
+			elapsed = mlk_clock_elapsed(&clock);
 
-		/*
-		 * If vsync is enabled, it should have wait, otherwise sleep
-		 * a little to save CPU cycles.
-		 */
-		if ((elapsed = mlk_clock_elapsed(&clock)) < frametime)
-			mlk_util_sleep(frametime - elapsed);
+			/*
+			 * Cap to frametime if it's too slow because it would
+			 * create unexpected results otherwise.
+			 */
+			if (elapsed > frametime)
+				elapsed = frametime;
+			break;
+		case JMP_PUSH:
+			/*
+			 * We have pushed a new state, suspend should not modify
+			 * the stack because we would have a leak in the next
+			 * state being overriden without being finalized.
+			 */
+			game_jmp_allowed = 0;
+			mlk_state_suspend(*mlk_game.state);
+			game_jmp_allowed = 1;
 
-		elapsed = mlk_clock_elapsed(&clock);
+			/* Start next state. */
+			mlk_state_start(*++mlk_game.state);
+			break;
+		case JMP_POP:
+			/*
+			 * We need to finalize this state so the end/finish
+			 * functions are not allowed to modify the stack.
+			 */
+			game_jmp_allowed = 0;
+			mlk_state_end(*mlk_game.state);
+			mlk_state_finish(*mlk_game.state);
+			game_jmp_allowed = 1;
 
-		/*
-		 * Cap to frametime if it's too slow because it would create
-		 * unexpected results otherwise.
-		 */
-		if (elapsed > frametime)
-			elapsed = frametime;
+			/* Resume previous state. */
+			if (mlk_game.state == mlk_game.states)
+				mlk_game.state = NULL;
+			else
+				mlk_state_resume(*--mlk_game.state);
+			break;
+		default:
+			break;
+		}
 	}
 }
 
 void
 mlk_game_quit(void)
 {
+	game_jmp_allowed = 0;
+	game_run = 0;
+
 	for (size_t i = 0; i < mlk_game.statesz; ++i) {
 		if (mlk_game.states[i])
 			mlk_state_finish(mlk_game.states[i]);
--- a/libmlk-core/mlk/core/game.h	Sat Dec 23 09:34:04 2023 +0100
+++ b/libmlk-core/mlk/core/game.h	Sun Feb 04 15:24:00 2024 +0100
@@ -108,58 +108,34 @@
 mlk_game_init(void);
 
 /**
- * Try to append a new state into the game loop at the end unless the array is
- * full.
+ * Append the state into the game stack and switch to it, suspending current
+ * state.
  *
- * The state is inserted as-is and ownership is left to the caller.
+ * The function takes ownership of the state and will be finalized later.
  *
  * \pre state != NULL
- * \param state
- * \return 0 on success or -1 on error
+ * \param state the state to switch
  */
-int
+_Noreturn void
 mlk_game_push(struct mlk_state *state);
 
 /**
  * Pop the current state if any and resume the previous one.
  */
-void
+_Noreturn void
 mlk_game_pop(void);
 
 /**
- * Call the current state's mlk_state::handle function unless it is inhibited
- * or NULL.
- *
- * \pre event != NULL
- * \param event the event
- */
-void
-mlk_game_handle(const union mlk_event *event);
-
-/**
- * Call the current state's mlk_state::update function unless it is inhibited
- * or NULL.
- *
- * \param ticks frame ticks
- */
-void
-mlk_game_update(unsigned int ticks);
-
-/**
- * Call the current state's mlk_state::draw function unless it is inhibited
- * or NULL.
- */
-void
-mlk_game_draw(void);
-
-/**
  * Enter a game loop until there is no more states.
  *
  * The current implementation will perform a loop capped to a 60 FPS rate and
  * update the states with the appropriate number of ticks.
+ *
+ * \pre state != NULL
+ * \param state the first state to run
  */
 void
-mlk_game_loop(void);
+mlk_game_loop(struct mlk_state *state);
 
 /**
  * Request the game loop to stop by removing all states.
--- a/libmlk-core/mlk/core/js/js-event.c	Sat Dec 23 09:34:04 2023 +0100
+++ b/libmlk-core/mlk/core/js/js-event.c	Sun Feb 04 15:24:00 2024 +0100
@@ -26,8 +26,37 @@
 #include "js.h"
 
 static duk_ret_t
-push(duk_context *ctx, const union mlk_event *ev)
+mlk_js_event_poll(duk_context *ctx)
 {
+	union mlk_event ev;
+
+	if (!mlk_event_poll(&ev))
+		return 0;
+
+	return mlk_js_event_push(ctx, &ev);
+}
+
+static const duk_number_list_entry types[] = {
+	{ "CLICK_DOWN", MLK_EVENT_CLICKDOWN },
+	{ "CLICK_UP",   MLK_EVENT_CLICKUP   },
+	{ "KEY_DOWN",   MLK_EVENT_KEYDOWN   },
+	{ "KEY_UP",     MLK_EVENT_KEYUP     },
+	{ "MOUSE",      MLK_EVENT_MOUSE     },
+	{ "QUIT",       MLK_EVENT_QUIT      },
+	{ NULL,         0               }
+};
+
+static const duk_function_list_entry functions[] = {
+	{ "poll",       mlk_js_event_poll,      0 },
+	{ NULL,         NULL,                   0 }
+};
+
+duk_ret_t
+mlk_js_event_push(duk_context *ctx, const union mlk_event *ev)
+{
+	assert(ctx);
+	assert(ev);
+
 	duk_push_object(ctx);
 	duk_push_int(ctx, ev->type);
 	duk_put_prop_string(ctx, -2, "type");
@@ -62,32 +91,6 @@
 	return 1;
 }
 
-static duk_ret_t
-mlk_js_event_poll(duk_context *ctx)
-{
-	union mlk_event ev;
-
-	if (!mlk_event_poll(&ev))
-		return 0;
-
-	return push(ctx, &ev);
-}
-
-static const duk_number_list_entry types[] = {
-	{ "CLICK_DOWN", MLK_EVENT_CLICKDOWN },
-	{ "CLICK_UP",   MLK_EVENT_CLICKUP   },
-	{ "KEY_DOWN",   MLK_EVENT_KEYDOWN   },
-	{ "KEY_UP",     MLK_EVENT_KEYUP     },
-	{ "MOUSE",      MLK_EVENT_MOUSE     },
-	{ "QUIT",       MLK_EVENT_QUIT      },
-	{ NULL,         0               }
-};
-
-static const duk_function_list_entry functions[] = {
-	{ "poll",       mlk_js_event_poll,      0 },
-	{ NULL,         NULL,                   0 }
-};
-
 void
 mlk_js_event_load(duk_context *ctx)
 {
--- a/libmlk-core/mlk/core/js/js-event.h	Sat Dec 23 09:34:04 2023 +0100
+++ b/libmlk-core/mlk/core/js/js-event.h	Sun Feb 04 15:24:00 2024 +0100
@@ -21,10 +21,15 @@
 
 #include <duktape.h>
 
+union mlk_event;
+
 #if defined(__cplusplus)
 extern "C" {
 #endif
 
+duk_ret_t
+mlk_js_event_push(duk_context *ctx, const union mlk_event *ev);
+
 void
 mlk_js_event_load(duk_context *ctx);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-game.c	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,143 @@
+/*
+ * js-game.c -- main game object (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <mlk/core/err.h>
+#include <mlk/core/game.h>
+#include <mlk/core/state.h>
+
+#include "js-game.h"
+#include "js-state.h"
+
+#define REFS DUK_HIDDEN_SYMBOL("mlk::game::states")
+
+static duk_ret_t
+mlk_js_game_push(duk_context *ctx)
+{
+	struct mlk_state *st;
+
+	st = mlk_js_state_require(ctx, 0);
+
+	if (mlk_game_push(st) < 0)
+		duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	/* Keep a reference to this object, otherwise it would be collected. */
+	duk_push_global_stash(ctx);
+	duk_get_prop_string(ctx, -1, REFS);
+	duk_dup(ctx, 0);
+	duk_put_prop_index(ctx, -2, duk_get_length(ctx, -2));
+	duk_pop_n(ctx, 2);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_game_pop(duk_context *ctx)
+{
+	(void)ctx;
+
+	duk_size_t len;
+
+	mlk_game_pop();
+
+	/* Remove reference of previous state. */
+	duk_push_global_stash(ctx);
+	duk_get_prop_string(ctx, -1, REFS);
+
+	if ((len = duk_get_length(ctx, -1)))
+		duk_del_prop_index(ctx, -1, len - 1);
+
+	duk_pop_n(ctx, 2);
+
+	return 0;
+}
+
+#if 0
+static duk_ret_t
+mlk_js_game_handle(duk_context *ctx)
+{
+}
+
+static duk_ret_t
+mlk_js_game_update(duk_context *ctx)
+{
+	mlk_game_update(duk_require_uint(ctx, 0));
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_game_draw(duk_context *ctx)
+{
+	(void)ctx;
+
+	mlk_game_draw();
+
+	return 0;
+}
+#endif
+
+static duk_ret_t
+mlk_js_game_loop(duk_context *ctx)
+{
+	(void)ctx;
+
+	mlk_game_loop(NULL);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_game_quit(duk_context *ctx)
+{
+	(void)ctx;
+
+	mlk_game_quit();
+
+	return 0;
+}
+
+static const duk_function_list_entry functions[] = {
+	{ "push",   mlk_js_game_push,   1  },
+	{ "pop",    mlk_js_game_pop,    0  },
+#if 0
+	{ "update", mlk_js_game_update, 1  },
+	{ "draw",   mlk_js_game_draw,   0  },
+#endif
+	{ "loop",   mlk_js_game_loop,   0  },
+	{ "quit",   mlk_js_game_quit,   0  },
+	{ NULL,     NULL,               -1 },
+};
+
+void
+mlk_js_game_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_object(ctx);
+	duk_put_function_list(ctx, -1, functions);
+	duk_put_prop_string(ctx, -2, "Game");
+	duk_pop(ctx);
+	duk_push_global_stash(ctx);
+	duk_push_array(ctx);
+	duk_put_prop_string(ctx, -2, REFS);
+	duk_pop(ctx);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-game.h	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,30 @@
+/*
+ * js-game.h -- main game object (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <duktape.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_game_load(duk_context *ctx);
+
+#if defined(__cplusplus)
+}
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-state.c	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,255 @@
+/*
+ * js-state.c -- abstract game loop state (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/panic.h>
+#include <mlk/core/state.h>
+#include <mlk/core/util.h>
+
+#include "js-event.h"
+#include "js-state.h"
+
+#define SYMBOL DUK_HIDDEN_SYMBOL("mlk::state")
+
+/*
+ * Cache virtual functions inside the object structure for performance reasons
+ * to avoid permanent lookup.
+ */
+#define SET(obj, name)                                                  \
+do {                                                                    \
+        duk_push_this(obj->ctx);                                        \
+        duk_get_prototype(obj->ctx, -1);                                \
+        duk_get_prop_string(obj->ctx, -1, #name);                       \
+                                                                        \
+        if (duk_is_callable(obj->ctx, -1)) {                            \
+                obj->ref_##name = duk_get_heapptr(obj->ctx, -1);        \
+                obj->state.name = mlk_js_state__##name;                 \
+        }                                                               \
+                                                                        \
+        duk_pop_n(obj->ctx, 3);                                         \
+} while (0);
+
+struct object {
+	duk_context *ctx;
+	void *ref_this;
+	void *ref_start;
+	void *ref_handle;
+	void *ref_update;
+	void *ref_draw;
+	void *ref_suspend;
+	void *ref_resume;
+	void *ref_end;
+	struct mlk_state state;
+};
+
+#if 0
+static struct object *
+mlk_js_state__this(duk_context *ctx)
+{
+	struct object *obj;
+
+	duk_push_this(ctx);
+	duk_get_prop_string(ctx, -1, SYMBOL);
+	obj = duk_to_pointer(ctx, -1);
+	duk_pop_n(ctx, 2);
+
+	if (!obj)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "Not a State object");
+
+	return obj;
+}
+#endif
+
+static void
+mlk_js_state__start(struct mlk_state *self)
+{
+	struct object *obj = MLK_CONTAINER_OF(self, struct object, state);
+
+	duk_push_heapptr(obj->ctx, obj->ref_start);
+	duk_push_heapptr(obj->ctx, obj->ref_this);
+
+	if (duk_pcall_method(obj->ctx, 0) != 0)
+		mlk_panicf("%s", duk_to_string(obj->ctx, -1));
+
+	duk_pop(obj->ctx);
+}
+
+static void
+mlk_js_state__handle(struct mlk_state *self, const union mlk_event *event)
+{
+	struct object *obj = MLK_CONTAINER_OF(self, struct object, state);
+
+	duk_push_heapptr(obj->ctx, obj->ref_handle);
+	duk_push_heapptr(obj->ctx, obj->ref_this);
+	mlk_js_event_push(obj->ctx, event);
+
+	if (duk_pcall_method(obj->ctx, 1) != 0)
+		mlk_panicf("%s", duk_to_string(obj->ctx, -1));
+
+	duk_pop(obj->ctx);
+}
+
+static void
+mlk_js_state__update(struct mlk_state *self, unsigned int ticks)
+{
+	struct object *obj = MLK_CONTAINER_OF(self, struct object, state);
+
+	duk_push_heapptr(obj->ctx, obj->ref_update);
+	duk_push_heapptr(obj->ctx, obj->ref_this);
+	duk_push_uint(obj->ctx, ticks);
+
+	if (duk_pcall_method(obj->ctx, 1) != 0)
+		mlk_panicf("%s", duk_to_string(obj->ctx, -1));
+
+	duk_pop(obj->ctx);
+}
+
+static void
+mlk_js_state__draw(struct mlk_state *self)
+{
+	struct object *obj = MLK_CONTAINER_OF(self, struct object, state);
+
+	duk_push_heapptr(obj->ctx, obj->ref_draw);
+	duk_push_heapptr(obj->ctx, obj->ref_this);
+
+	if (duk_pcall_method(obj->ctx, 0) != 0)
+		mlk_panicf("%s", duk_to_string(obj->ctx, -1));
+
+	duk_pop(obj->ctx);
+}
+
+static void
+mlk_js_state__suspend(struct mlk_state *self)
+{
+	struct object *obj = MLK_CONTAINER_OF(self, struct object, state);
+
+	duk_push_heapptr(obj->ctx, obj->ref_suspend);
+	duk_push_heapptr(obj->ctx, obj->ref_this);
+
+	if (duk_pcall_method(obj->ctx, 0) != 0)
+		mlk_panicf("%s", duk_to_string(obj->ctx, -1));
+
+	duk_pop(obj->ctx);
+}
+
+static void
+mlk_js_state__resume(struct mlk_state *self)
+{
+	struct object *obj = MLK_CONTAINER_OF(self, struct object, state);
+
+	if (!obj->ref_resume)
+		return;
+
+	duk_push_heapptr(obj->ctx, obj->ref_resume);
+	duk_push_heapptr(obj->ctx, obj->ref_this);
+
+	if (duk_pcall_method(obj->ctx, 0) != 0)
+		mlk_panicf("%s", duk_to_string(obj->ctx, -1));
+
+	duk_pop(obj->ctx);
+}
+
+static void
+mlk_js_state__end(struct mlk_state *self)
+{
+	struct object *obj = MLK_CONTAINER_OF(self, struct object, state);
+
+	duk_push_heapptr(obj->ctx, obj->ref_end);
+	duk_push_heapptr(obj->ctx, obj->ref_this);
+
+	if (duk_pcall_method(obj->ctx, 0) != 0)
+		mlk_panicf("%s", duk_to_string(obj->ctx, -1));
+
+	duk_pop(obj->ctx);
+}
+
+static duk_ret_t
+mlk_js_state__finish(duk_context *ctx)
+{
+	struct object *obj;
+
+	printf("FINALIZING STATE\n");
+	duk_get_prop_string(ctx, 0, SYMBOL);
+	obj = duk_to_pointer(ctx, -1);
+	mlk_alloc_free(obj);
+	duk_pop(ctx);
+	duk_del_prop_string(ctx, 0, SYMBOL);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_state__new(duk_context *ctx)
+{
+	struct object *obj;
+
+	obj = mlk_alloc_new(1, sizeof (*obj));
+	obj->ctx = ctx;
+	obj->state.name = duk_require_string(ctx, 0);
+
+	/* Cache virtual functions for performance reasons. */
+	SET(obj, start);
+	SET(obj, handle);
+	SET(obj, update);
+	SET(obj, draw);
+	SET(obj, suspend);
+	SET(obj, resume);
+	SET(obj, end);
+
+	duk_push_this(ctx);
+	obj->ref_this = duk_get_heapptr(ctx, -1);
+	printf("%p\n", obj->ref_this);
+	duk_push_pointer(ctx, obj);
+	duk_put_prop_string(ctx, -2, SYMBOL);
+	duk_push_c_function(ctx, mlk_js_state__finish, 1);
+	duk_set_finalizer(ctx, -2);
+	duk_pop(ctx);
+
+	return 0;
+}
+
+struct mlk_state *
+mlk_js_state_require(duk_context *ctx, duk_idx_t index)
+{
+	struct object *obj;
+
+	duk_get_prop_string(ctx, index, SYMBOL);
+	obj = duk_to_pointer(ctx, -1);
+	duk_pop(ctx);
+
+	if (!obj)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "State expected on argument #%u", index);
+
+	return &obj->state;
+}
+
+void
+mlk_js_state_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_c_function(ctx, mlk_js_state__new, 1);
+	duk_push_object(ctx);
+	duk_put_prop_string(ctx, -2, "prototype");
+	duk_put_prop_string(ctx, -2, "State");
+	duk_pop(ctx);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-state.h	Sun Feb 04 15:24:00 2024 +0100
@@ -0,0 +1,35 @@
+/*
+ * js-state.h -- abstract game loop state (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <duktape.h>
+
+struct mlk_state;
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_state_load(duk_context *ctx);
+
+struct mlk_state *
+mlk_js_state_require(duk_context *ctx, duk_idx_t index);
+
+#if defined(__cplusplus)
+}
+#endif
--- a/libmlk-core/mlk/core/js/js.c	Sat Dec 23 09:34:04 2023 +0100
+++ b/libmlk-core/mlk/core/js/js.c	Sun Feb 04 15:24:00 2024 +0100
@@ -31,10 +31,12 @@
 #include "js-clock.h"
 #include "js-event.h"
 #include "js-font.h"
+#include "js-game.h"
 #include "js-music.h"
 #include "js-painter.h"
 #include "js-sound.h"
 #include "js-sprite.h"
+#include "js-state.h"
 #include "js-texture.h"
 #include "js-util.h"
 #include "js-window.h"
@@ -151,11 +153,11 @@
 	/* Create global "Mlk" property. */
 	duk_push_global_object(js->handle);
 	duk_push_object(js->handle);
-	duk_put_prop_string(js->handle, -2, "Mlk");
 	duk_push_c_function(js->handle, mlk_js_print, 1);
 	duk_put_prop_string(js->handle, -2, "print");
 	duk_push_c_function(js->handle, mlk_js_trace, 1);
 	duk_put_prop_string(js->handle, -2, "trace");
+	duk_put_prop_string(js->handle, -2, "Mlk");
 	duk_pop(js->handle);
 }
 
@@ -178,10 +180,12 @@
 	mlk_js_clock_load(js->handle);
 	mlk_js_event_load(js->handle);
 	mlk_js_font_load(js->handle);
+	mlk_js_game_load(js->handle);
 	mlk_js_music_load(js->handle);
 	mlk_js_painter_load(js->handle);
 	mlk_js_sound_load(js->handle);
 	mlk_js_sprite_load(js->handle);
+	mlk_js_state_load(js->handle);
 	mlk_js_texture_load(js->handle);
 	mlk_js_util_load(js->handle);
 	mlk_js_window_load(js->handle);
--- a/libmlk-core/mlk/core/state.h	Sat Dec 23 09:34:04 2023 +0100
+++ b/libmlk-core/mlk/core/state.h	Sun Feb 04 15:24:00 2024 +0100
@@ -44,6 +44,13 @@
 	void *data;
 
 	/**
+	 * (read-write, borrowed, optional)
+	 *
+	 * Arbitrary state name for diagnostic purposes.
+	 */
+	const char *name;
+
+	/**
 	 * (read-write, optional)
 	 *
 	 * Invoked when the state starts, which is called only one time.