summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkdx <kikoodx@paranoici.org>2023-03-26 17:49:03 +0200
committerkdx <kikoodx@paranoici.org>2023-03-26 19:00:15 +0200
commitb343ac085f87db7c52552b1a86aa26aa6da7a10f (patch)
tree476a994676d75116abb3c43959e34ab4836aa9e9
downloadjamdelaloose-b343ac085f87db7c52552b1a86aa26aa6da7a10f.tar.gz
initial commit
-rw-r--r--.gitignore4
-rw-r--r--LICENSE19
-rw-r--r--Makefile37
-rw-r--r--compile_flags.txt3
-rw-r--r--map/brulez.tmj78
-rw-r--r--map/tmj2c.h32
-rw-r--r--src/cfg.h11
-rw-r--r--src/entity.c100
-rw-r--r--src/entity.h25
-rw-r--r--src/entityimpl.h30
-rw-r--r--src/entitytag.c4
-rw-r--r--src/entitytag.h10
-rw-r--r--src/game.c94
-rw-r--r--src/game.h19
-rw-r--r--src/lzr.c929
-rw-r--r--src/lzr.h129
-rw-r--r--src/main.c50
-rw-r--r--src/map.c54
-rw-r--r--src/map.h10
-rw-r--r--src/player.c17
20 files changed, 1655 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..68db9d7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/obj/
+/jambase
+/jambase.exe
+/map/maps.h
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a32c30c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2023 kdx
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..74b3a51
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,37 @@
+CC := gcc
+LD := $(CC)
+WINCC := x86_64-w64-mingw32-gcc
+CFLAGS := -g -Os -std=c2x -Wall -Wextra -DLZR_DISABLE_DEVMODE=
+LDFLAGS := -lm -lSDL2 -lSDL2_image -lSDL2_mixer -lSDL2_gfx
+NAME := jambase
+SRC := $(wildcard src/*.c)
+OBJ := $(patsubst src/%.c,obj/%.o,$(SRC))
+DEP := $(patsubst %.o,%.d,$(OBJ))
+
+all: embed
+ @make --no-print-directory $(NAME)
+
+$(NAME): $(OBJ)
+ $(LD) -o $(NAME) -MMD $(OBJ) $(LDFLAGS)
+
+obj/%.o: src/%.c
+ @mkdir -p obj
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+embed:
+ tmj2c map/*.tmj >map/maps.h
+
+run: re
+ ./$(NAME)
+
+clean:
+ rm -rf $(NAME) $(NAME).exe obj/ map/maps.h
+
+re:
+ @make --no-print-directory clean
+ @make --no-print-directory all
+
+windows: embed
+ $(WINCC) $(CFLAGS) -o $(NAME).exe $(SRC) $(LDFLAGS)
+
+.PHONY: all embed run clean re windows
diff --git a/compile_flags.txt b/compile_flags.txt
new file mode 100644
index 0000000..f5cab1b
--- /dev/null
+++ b/compile_flags.txt
@@ -0,0 +1,3 @@
+-Wall
+-Wextra
+-std=c2x
diff --git a/map/brulez.tmj b/map/brulez.tmj
new file mode 100644
index 0000000..d5997f7
--- /dev/null
+++ b/map/brulez.tmj
@@ -0,0 +1,78 @@
+{ "compressionlevel":-1,
+ "height":14,
+ "infinite":false,
+ "layers":[
+ {
+ "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ "height":14,
+ "id":1,
+ "name":"Tile Layer 1",
+ "opacity":1,
+ "type":"tilelayer",
+ "visible":true,
+ "width":25,
+ "x":0,
+ "y":0
+ },
+ {
+ "draworder":"topdown",
+ "id":2,
+ "name":"Object Layer 1",
+ "objects":[
+ {
+ "height":16,
+ "id":1,
+ "name":"player",
+ "rotation":0,
+ "type":"",
+ "visible":true,
+ "width":16,
+ "x":144,
+ "y":96
+ },
+ {
+ "height":32,
+ "id":2,
+ "name":"player",
+ "rotation":0,
+ "type":"",
+ "visible":true,
+ "width":32,
+ "x":256,
+ "y":144
+ }],
+ "opacity":1,
+ "type":"objectgroup",
+ "visible":true,
+ "x":0,
+ "y":0
+ }],
+ "nextlayerid":3,
+ "nextobjectid":3,
+ "orientation":"orthogonal",
+ "renderorder":"right-down",
+ "tiledversion":"1.10.0",
+ "tileheight":16,
+ "tilesets":[
+ {
+ "firstgid":1,
+ "source":"tset.tsj"
+ }],
+ "tilewidth":16,
+ "type":"map",
+ "version":"1.10",
+ "width":25
+} \ No newline at end of file
diff --git a/map/tmj2c.h b/map/tmj2c.h
new file mode 100644
index 0000000..ff5baba
--- /dev/null
+++ b/map/tmj2c.h
@@ -0,0 +1,32 @@
+#pragma once
+
+typedef struct {
+ const char *name;
+ double opacity;
+ unsigned int visible;
+ const unsigned int *data;
+} Tmj2cLayer;
+
+typedef struct {
+ const char *name;
+ const char *type;
+ unsigned int id;
+ double x;
+ double y;
+ double width;
+ double height;
+ double rotation;
+ unsigned int visible;
+} Tmj2cObject;
+
+typedef struct {
+ const char *path;
+ unsigned int width;
+ unsigned int height;
+ unsigned int tilewidth;
+ unsigned int tileheight;
+ unsigned int numlayers;
+ const Tmj2cLayer *layers;
+ unsigned int numobjects;
+ const Tmj2cObject *objects;
+} Tmj2cMap;
diff --git a/src/cfg.h b/src/cfg.h
new file mode 100644
index 0000000..c0e8f4e
--- /dev/null
+++ b/src/cfg.h
@@ -0,0 +1,11 @@
+#pragma once
+#include "lzr.h"
+
+#define DWIDTH 400
+#define DHEIGHT 224
+#define TARGET_FPS 30
+#define TSIZE 16
+
+static const LZR_Config cfg = {
+ DWIDTH, DHEIGHT, TARGET_FPS, TSIZE, "jambase", 0.0, true, true
+};
diff --git a/src/entity.c b/src/entity.c
new file mode 100644
index 0000000..75ad7ec
--- /dev/null
+++ b/src/entity.c
@@ -0,0 +1,100 @@
+#include "entity.h"
+#include "entitytag.h"
+#include "map.h"
+#include "game.h"
+#include <stdio.h>
+#include <string.h>
+
+static void
+_points(Entity *this, int ox, int oy, int *x0, int *x1, int *y0, int *y1)
+{
+ *x0 = this->pos[0] - this->width / 2 + ox;
+ *y0 = this->pos[1] - this->width / 2 + oy;
+ *x1 = *x0 + this->width - 1;
+ *y1 = *y0 + this->height - 1;
+}
+
+unsigned int
+entity_type(const char *typename)
+{
+ for (unsigned i = 0; i < num_entitytags; i++)
+ if (strcmp(typename, entitytags[i].name) == 0)
+ return i + 1;
+ printf("unknown type '%s'\n", typename);
+ return 0;
+}
+
+bool
+entity_collide(Entity *this, int ox, int oy)
+{
+ int x0, y0, x1, y1;
+ _points(this, ox, oy, &x0, &x1, &y0, &y1);
+ return (map_get_px(x0, y0) == 1 || map_get_px(x0, y1) == 1 ||
+ map_get_px(x1, y0) == 1 || map_get_px(x1, y1) == 1);
+}
+
+bool
+entity_meet(Entity *this, Entity *other)
+{
+ int tx0, ty0, tx1, ty1;
+ int ox0, oy0, ox1, oy1;
+ _points(this, 0, 0, &tx0, &tx1, &ty0, &ty1);
+ _points(other, 0, 0, &ox0, &ox1, &oy0, &oy1);
+ return (tx0 < ox1 && tx1 > ox0 && ty0 < oy1 && ty1 > oy0);
+}
+
+Entity *
+entity_place_meeting(Entity *this, struct Game *g, unsigned int type)
+{
+ for (__auto_type i = 0; i < MAX_ENTITIES; i++)
+ if (this != &g->entities[i] && g->entities[i].type == type &&
+ entity_meet(this, &g->entities[i]))
+ return &g->entities[i];
+ return NULL;
+}
+
+void
+entity_move(Entity *this, [[maybe_unused]] struct Game *g)
+{
+ if (this->ignore_solids) {
+ for (int a = 0; a < 2; a++) {
+ const double sum = this->vel[a] + this->rem[a];
+ int spd = (int)sum;
+ this->rem[a] = sum - spd;
+ this->pos[a] += spd;
+ }
+ return;
+ }
+ if (entity_collide(this, 0, 0)) {
+ this->vel[0] = 0.0;
+ this->vel[1] = 0.0;
+ this->rem[0] = 0.0;
+ this->rem[1] = 0.0;
+ return;
+ }
+ for (int a = 0; a < 2; a++) {
+ const double sum = this->vel[a] + this->rem[a];
+ int spd = (int)sum;
+ this->rem[a] = sum - spd;
+ const int sign = (spd > 0) - (spd < 0);
+ if (sign == 0)
+ continue;
+ while (spd != 0) {
+ this->pos[a] += sign;
+ if (entity_collide(this, 0, 0)) {
+ this->pos[a] -= sign;
+ this->rem[a] = 0.0;
+ this->vel[a] = 0.0;
+ break;
+ }
+ spd -= sign;
+ }
+ }
+}
+
+Entity *
+entity_init(Entity *this, unsigned int type, int x, int y)
+{
+ entitytags[type - 1].init(this, x, y);
+ return this;
+}
diff --git a/src/entity.h b/src/entity.h
new file mode 100644
index 0000000..01230d5
--- /dev/null
+++ b/src/entity.h
@@ -0,0 +1,25 @@
+#pragma once
+#include <stdbool.h>
+
+struct Game;
+
+typedef struct Entity Entity;
+struct Entity {
+ unsigned long uuid;
+ void (*update)(Entity *this, struct Game *g);
+ void (*draw)(Entity *this, struct Game *g);
+ unsigned int type;
+ int pos[2];
+ double vel[2];
+ double rem[2];
+ int width;
+ int height;
+ bool ignore_solids;
+};
+
+unsigned int entity_type(const char *typename);
+bool entity_collide(Entity *this, int ox, int oy);
+bool entity_meet(Entity *this, Entity *other);
+Entity *entity_place_meeting(Entity *this, struct Game *g, unsigned int type);
+void entity_move(Entity *this, struct Game *g);
+Entity *entity_init(Entity *this, unsigned int type, int x, int y);
diff --git a/src/entityimpl.h b/src/entityimpl.h
new file mode 100644
index 0000000..afc9dc2
--- /dev/null
+++ b/src/entityimpl.h
@@ -0,0 +1,30 @@
+#pragma once
+#include "game.h"
+#include "cfg.h"
+#include "lzr.h"
+#include "entitytag.h"
+#include <string.h>
+
+//[[maybe_unused]] static void *_draw;
+//[[maybe_unused]] static void *_update;
+
+#define IMPL(X) static void X(Entity *this, Game *g); \
+static void X([[maybe_unused]] Entity *this, [[maybe_unused]] Game *g)
+/*__attribute__((constructor)) static void init_##X() { _##X = X; } \ */
+
+#define IMPL_INIT(X) static void init(Entity *this, int x, int y); \
+__attribute__((constructor)) static void init_tag() { \
+ entitytags[num_entitytags].init = init; \
+ entitytags[num_entitytags++].name = #X; \
+} \
+static void _init(Entity *this); \
+static void init(Entity *this, int x, int y) { \
+ memset(this, 0, sizeof(*this)); \
+ this->update = update; \
+ this->draw = draw; \
+ this->pos[0] = x; \
+ this->pos[1] = y; \
+ this->type = entity_type(#X); \
+ _init(this); \
+} \
+static void _init([[maybe_unused]] Entity *this)
diff --git a/src/entitytag.c b/src/entitytag.c
new file mode 100644
index 0000000..de56455
--- /dev/null
+++ b/src/entitytag.c
@@ -0,0 +1,4 @@
+#include "entitytag.h"
+
+unsigned int num_entitytags = 0;
+EntityTag entitytags[256] = {};
diff --git a/src/entitytag.h b/src/entitytag.h
new file mode 100644
index 0000000..154023c
--- /dev/null
+++ b/src/entitytag.h
@@ -0,0 +1,10 @@
+#pragma once
+#include "entity.h"
+
+typedef struct {
+ const char *name;
+ void (*init)(Entity *this, int x, int y);
+} EntityTag;
+
+extern unsigned int num_entitytags;
+extern EntityTag entitytags[256];
diff --git a/src/game.c b/src/game.c
new file mode 100644
index 0000000..4cd9728
--- /dev/null
+++ b/src/game.c
@@ -0,0 +1,94 @@
+#include "game.h"
+#include "cfg.h"
+#include "entity.h"
+#include "map.h"
+#include <stdio.h>
+#include <string.h>
+
+void
+game_init(Game *this)
+{
+ memset(this, 0, sizeof(*this));
+ game_restart_scene(this);
+}
+
+void
+game_update(Game *this)
+{
+ if (this->queue_next_scene) {
+ if (--this->queue_next_scene == 0) {
+ this->queue_restart_scene = 0;
+ map_next();
+ game_restart_scene(this);
+ return;
+ }
+ }
+ if (this->queue_restart_scene) {
+ if (--this->queue_restart_scene == 0)
+ game_restart_scene(this);
+ }
+ for (__auto_type i = 0; i < MAX_ENTITIES; i++) {
+ const __auto_type e = &this->entities[i];
+ if (e->type != 0 && e->update != NULL)
+ e->update(e, this);
+ }
+}
+
+void
+game_draw(Game *this)
+{
+ map_draw();
+ for (int i = 0; i < MAX_ENTITIES; i++) {
+ const __auto_type e = &this->entities[i];
+ if (e->type != 0 && e->draw != NULL)
+ e->draw(e, this);
+ }
+}
+
+void
+game_restart_scene(Game *this)
+{
+ memset(this->entities, 0, sizeof(this->entities));
+ unsigned int size;
+ const __auto_type objects = map_objects(&size);
+ for (__auto_type i = 0u; i < size; i++) {
+ const __auto_type object = &objects[i];
+ const __auto_type type = entity_type(object->name);
+ if (type == 0)
+ continue;
+ const __auto_type x = object->x + object->width / 2;
+ const __auto_type y = object->y + object->height / 2;
+ entity_init(game_create_entity(this), type, x, y);
+ }
+}
+
+Entity *
+game_create_entity(Game *this)
+{
+ __auto_type e = &this->entities[MAX_ENTITIES - 1];
+ for (__auto_type i = 0; i < MAX_ENTITIES; i++)
+ if (this->entities[i].type == 0) {
+ e = &this->entities[i];
+ break;
+ }
+ e->uuid = this->uuid++;
+ return e;
+}
+
+int
+game_entity_count(Game *this, unsigned int type)
+{
+ int count = 0;
+ for (__auto_type i = 0; i < MAX_ENTITIES; i++)
+ count += (this->entities[i].type == type);
+ return count;
+}
+
+Entity *
+game_get_entity(Game *this, unsigned int type)
+{
+ for (__auto_type i = 0; i < MAX_ENTITIES; i++)
+ if (this->entities[i].type == type)
+ return &this->entities[i];
+ return NULL;
+}
diff --git a/src/game.h b/src/game.h
new file mode 100644
index 0000000..2d563f0
--- /dev/null
+++ b/src/game.h
@@ -0,0 +1,19 @@
+#pragma once
+#include "entity.h"
+
+enum { MAX_ENTITIES = 128 };
+
+typedef struct Game {
+ unsigned long uuid;
+ int queue_next_scene;
+ int queue_restart_scene;
+ Entity entities[MAX_ENTITIES];
+} Game;
+
+void game_init(Game *this);
+void game_update(Game *this);
+void game_draw(Game *this);
+void game_restart_scene(Game *this);
+Entity *game_create_entity(Game *this);
+int game_entity_count(Game *this, unsigned int type);
+Entity *game_get_entity(Game *this, unsigned int type);
diff --git a/src/lzr.c b/src/lzr.c
new file mode 100644
index 0000000..7f3cfe0
--- /dev/null
+++ b/src/lzr.c
@@ -0,0 +1,929 @@
+/* Licensing information can be found at the end of the file. */
+#include "lzr.h"
+#include <SDL2/SDL.h>
+#ifdef LZR_ENABLE_GFX
+# include <SDL2/SDL2_gfxPrimitives.h>
+#endif
+#ifdef LZR_ENABLE_IMAGE
+# include <SDL2/SDL_image.h>
+#endif
+#ifdef LZR_ENABLE_MIXER
+# include <SDL2/SDL_mixer.h>
+#endif
+#ifdef LZR_ENABLE_DEVMODE
+# include <sys/stat.h>
+#endif
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+
+#define UNPACKED_COLOR color[0], color[1], color[2]
+#define UNPACKED_COLOR_RGBA color[0], color[1], color[2], color[3]
+#define SCODE_BIND_MENU SDL_SCANCODE_F1
+#define SCODE_FULLSCREEN SDL_SCANCODE_F11
+
+static LZR_Config config = {0};
+static char *basepath = NULL;
+static SDL_Window *window = NULL;
+static SDL_Renderer *renderer = NULL;
+static SDL_Texture *target = NULL;
+static uint_least64_t next_time = 0;
+static uint_least64_t min_dt = 0;
+static bool should_quit = false;
+static struct {
+ SDL_Texture *tex;
+ int width, height;
+ char *path;
+ long mtime;
+} images[LZR_MAX_IMAGES] = {0};
+static unsigned int color[4] = {0};
+static unsigned int map[LZR_BUTTON_COUNT] = {
+ SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT, SDL_SCANCODE_UP,
+ SDL_SCANCODE_DOWN, SDL_SCANCODE_X, SDL_SCANCODE_C};
+static bool input[LZR_BUTTON_COUNT] = {false};
+static SDL_Point *points = NULL;
+static uint64_t tick = 0;
+static int off_x = 0;
+static int off_y = 0;
+static float scale = 1.0;
+static int mouse_x = 0;
+static int mouse_y = 0;
+
+#ifdef LZR_ENABLE_MIXER
+static struct {
+ Mix_Chunk *ptr;
+} sounds[LZR_MAX_SOUNDS] = {0};
+static Mix_Music *music = NULL;
+#endif
+
+static char *_lzrstrdup(const char *str)
+{
+ char *const cpy = malloc(strlen(str) + 1);
+ if (cpy == NULL)
+ return NULL;
+ strcpy(cpy, str);
+ return cpy;
+}
+
+static int _scode_to_button(unsigned int scode)
+{
+ for (int i = 0; i < LZR_BUTTON_MOUSE_L; i++)
+ if (map[i] == scode)
+ return i;
+ return -1;
+}
+
+static void _draw_btn(SDL_Renderer *ren, int btn, int x, int y,
+ unsigned int size)
+{
+#ifdef LZR_ENABLE_GFX
+ const unsigned int size_2thirds = size * 2 / 3;
+ switch (btn) {
+ case LZR_BUTTON_LEFT:
+ filledTrigonRGBA(ren, x - size / 2, y, x + size / 2,
+ y + size_2thirds, x + size / 2,
+ y - size_2thirds, 0, 0, 0, 255);
+ break;
+ case LZR_BUTTON_RIGHT:
+ filledTrigonRGBA(ren, x + size / 2, y, x - size / 2,
+ y + size_2thirds, x - size / 2,
+ y - size_2thirds, 0, 0, 0, 255);
+ break;
+ case LZR_BUTTON_UP:
+ filledTrigonRGBA(ren, x, y - size / 2, x - size_2thirds,
+ y + size / 2, x + size_2thirds, y + size / 2,
+ 0, 0, 0, 255);
+ break;
+ case LZR_BUTTON_DOWN:
+ filledTrigonRGBA(ren, x, y + size / 2, x - size_2thirds,
+ y - size / 2, x + size_2thirds, y - size / 2,
+ 0, 0, 0, 255);
+ break;
+ case LZR_BUTTON_O:
+ filledCircleRGBA(ren, x, y, size * 2 / 3, 0, 0, 0, 255);
+ break;
+ case LZR_BUTTON_X:
+ thickLineRGBA(ren, x - size / 2, y - size / 2, x + size / 2,
+ y + size / 2, size / 16 + 1, 0, 0, 0, 255);
+ thickLineRGBA(ren, x + size / 2, y - size / 2, x - size / 2,
+ y + size / 2, size / 16 + 1, 0, 0, 0, 255);
+ break;
+ default:
+ break;
+ }
+#else
+ (void)ren, (void)btn, (void)x, (void)y, (void)size;
+#endif
+}
+
+static void _bind_menu(void)
+{
+ SDL_Log("entering bind menu");
+ SDL_Window *win = NULL;
+ SDL_Renderer *ren = NULL;
+ if (SDL_CreateWindowAndRenderer(256, 256, 0, &win, &ren) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return;
+ }
+ int btn = 0;
+ SDL_Event e;
+ while (btn < LZR_BUTTON_MOUSE_L) {
+ while (SDL_PollEvent(&e)) {
+ if (e.type != SDL_KEYDOWN || e.key.repeat ||
+ e.key.keysym.scancode == SCODE_BIND_MENU ||
+ e.key.keysym.scancode == SCODE_FULLSCREEN)
+ continue;
+ if (e.key.keysym.scancode == SDL_SCANCODE_ESCAPE ||
+ e.type == SDL_QUIT)
+ goto exit_bind_menu;
+ LZR_ButtonBind(btn, e.key.keysym.scancode);
+ btn++;
+ }
+ SDL_SetRenderDrawColor(ren, 220, 220, 200, 255);
+ SDL_RenderClear(ren);
+ SDL_SetRenderDrawColor(ren, 0, 0, 0, 255);
+ _draw_btn(ren, btn, 128, 128, 104);
+ SDL_RenderPresent(ren);
+ sleep(0);
+ }
+exit_bind_menu:
+ SDL_DestroyRenderer(ren);
+ SDL_DestroyWindow(win);
+ SDL_Log("leaving bind menu");
+}
+
+int LZR_Init(LZR_Config cfg)
+{
+ memcpy(&config, &cfg, sizeof(config));
+ if (config.display_width == 0) {
+ SDL_Log("display_width can't be 0");
+ return -1;
+ }
+ if (config.display_height == 0) {
+ SDL_Log("display_height can't be 0");
+ return -1;
+ }
+ if (config.title == NULL) {
+ SDL_Log("title is NULL, defaulting to 'LZR'");
+ config.title = "LZR";
+ }
+ if (config.tile_size == 0)
+ config.tile_size = 1;
+ if (config.ratio <= 0.0)
+ config.ratio = 1.0;
+ else {
+ const double ratio =
+ (float)config.display_width / (float)config.display_height;
+ config.ratio /= ratio;
+ }
+ if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+#ifdef LZR_ENABLE_IMAGE
+ if (IMG_Init(IMG_INIT_PNG) != IMG_INIT_PNG) {
+ SDL_Log("%s", IMG_GetError());
+ return -1;
+ }
+#endif
+#ifdef LZR_ENABLE_MIXER
+ if (Mix_Init(MIX_INIT_FLAC) != MIX_INIT_FLAC) {
+ SDL_Log("%s", Mix_GetError());
+ return -1;
+ }
+ if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 8, 1024) < 0) {
+ SDL_Log("%s", Mix_GetError());
+ return -1;
+ }
+#endif
+ basepath = SDL_GetBasePath();
+ if (basepath == NULL) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ const int dwidth = config.display_width * config.ratio;
+ window = SDL_CreateWindow(config.title, SDL_WINDOWPOS_UNDEFINED,
+ SDL_WINDOWPOS_UNDEFINED, dwidth,
+ config.display_height, SDL_WINDOW_RESIZABLE);
+ if (window == NULL) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
+ if (renderer == NULL) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ target = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB888,
+ SDL_TEXTUREACCESS_TARGET,
+ config.display_width, config.display_height);
+ if (target == NULL) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ points =
+ calloc(cfg.display_width * cfg.display_height, sizeof(SDL_Point));
+ if (points == NULL) {
+ SDL_Log("calloc failed");
+ return -1;
+ }
+ SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, 0);
+ if (config.target_fps) {
+ min_dt = 1000 / config.target_fps;
+ next_time = SDL_GetTicks64();
+ }
+ if (config.hide_cursor && SDL_ShowCursor(SDL_DISABLE) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ if (LZR_DrawSetColor(1.0f, 1.0f, 1.0f, 1.0f))
+ return -1;
+ return 0;
+}
+
+void LZR_Quit(void)
+{
+#ifdef LZR_ENABLE_MIXER
+ for (int i = 0; i < LZR_MAX_SOUNDS; i++)
+ if (sounds[i].ptr != NULL) {
+ Mix_FreeChunk(sounds[i].ptr);
+ sounds[i].ptr = NULL;
+ }
+ Mix_CloseAudio();
+ Mix_Quit();
+#endif
+ for (int i = 0; i < LZR_MAX_IMAGES; i++) {
+ if (images[i].tex != NULL) {
+ SDL_DestroyTexture(images[i].tex);
+ images[i].tex = NULL;
+ }
+ if (images[i].path != NULL) {
+ free(images[i].path);
+ images[i].path = NULL;
+ }
+ }
+ if (points != NULL) {
+ free(points);
+ points = NULL;
+ }
+ if (target != NULL) {
+ SDL_DestroyTexture(target);
+ target = NULL;
+ }
+ if (renderer != NULL) {
+ SDL_DestroyRenderer(renderer);
+ renderer = NULL;
+ }
+ if (window != NULL) {
+ SDL_DestroyWindow(window);
+ window = NULL;
+ }
+ if (basepath != NULL) {
+ SDL_free(basepath);
+ basepath = NULL;
+ }
+#ifdef LZR_ENABLE_IMAGE
+ IMG_Quit();
+#endif
+ SDL_Quit();
+}
+
+bool LZR_ShouldQuit(void)
+{
+ return should_quit;
+}
+
+char *LZR_PathPrefix(const char *path)
+{
+ if (path == NULL) {
+ SDL_Log("path is NULL");
+ return NULL;
+ }
+ if (basepath == NULL) {
+ SDL_Log("basepath is NULL");
+ return _lzrstrdup(path);
+ }
+ char *const buf = malloc(strlen(basepath) + strlen(path) + 1);
+ if (buf == NULL) {
+ SDL_Log("malloc failed");
+ return NULL;
+ }
+ strcpy(buf, basepath);
+ strcat(buf, path);
+ return buf;
+}
+
+int LZR_ImageLoad(const char *path)
+{
+ char *apath;
+ long mtime = 0;
+#ifdef LZR_ENABLE_DEVMODE
+ apath = LZR_PathPrefix(path);
+ if (apath == NULL)
+ return -1;
+ struct stat st = {0};
+ (void)stat(apath, &st); /* stat can fail safely */
+ mtime = st.st_mtim.tv_nsec;
+#endif
+ int i;
+ for (i = 0; i < LZR_MAX_IMAGES; i++) {
+ if (images[i].path != NULL &&
+ strcmp(images[i].path, path) == 0) {
+ if (mtime != images[i].mtime) {
+ SDL_Log("reloading %d", i);
+ break;
+ }
+ return i;
+ }
+ if (images[i].tex == NULL)
+ break;
+ }
+ if (i >= LZR_MAX_IMAGES) {
+ SDL_Log("reached image limit (%d)", LZR_MAX_IMAGES);
+ return -1;
+ }
+ apath = LZR_PathPrefix(path);
+ if (apath == NULL) {
+ SDL_Log("LZR_PathPrefix failed");
+ return -1;
+ }
+#ifdef LZR_ENABLE_IMAGE
+ SDL_Surface *const surf = IMG_Load(apath);
+#else
+ SDL_Surface *const surf = SDL_LoadBMP(apath);
+#endif
+ free(apath);
+ if (surf == NULL) {
+ SDL_Log("%s: %s", path, SDL_GetError());
+ return -1;
+ }
+ SDL_Texture *const tex = SDL_CreateTextureFromSurface(renderer, surf);
+ SDL_FreeSurface(surf);
+ if (tex == NULL) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ if (images[i].tex != NULL)
+ SDL_DestroyTexture(images[i].tex);
+ images[i].tex = tex;
+ images[i].mtime = mtime;
+ if (SDL_SetTextureBlendMode(images[i].tex, SDL_BLENDMODE_BLEND) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ if (SDL_QueryTexture(tex, NULL, NULL, &images[i].width,
+ &images[i].height)) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ if (images[i].path == NULL)
+ images[i].path = _lzrstrdup(path);
+ return i;
+}
+
+int LZR_SoundLoad(const char *path, float volume)
+{
+#ifdef LZR_ENABLE_MIXER
+ int i;
+ for (i = 0; i < LZR_MAX_SOUNDS; i++)
+ if (sounds[i].ptr == NULL)
+ break;
+ if (i >= LZR_MAX_SOUNDS) {
+ SDL_Log("reached sounds limit (%d)", LZR_MAX_SOUNDS);
+ return -1;
+ }
+ char *const apath = LZR_PathPrefix(path);
+ if (apath == NULL) {
+ SDL_Log("LZR_PathPrefix failed");
+ return -1;
+ }
+ Mix_Chunk *const chunk = Mix_LoadWAV(apath);
+ free(apath);
+ if (chunk == NULL) {
+ SDL_Log("%s: %s", path, Mix_GetError());
+ return -1;
+ }
+ Mix_VolumeChunk(chunk, volume);
+ sounds[i].ptr = chunk;
+ return i;
+#else
+ (void)path, (void)volume;
+ return -1;
+#endif
+}
+
+bool LZR_PollEvent(LZR_Event *e)
+{
+ if (e == NULL) {
+ SDL_Log("e is NULL");
+ return false;
+ }
+ SDL_Event se;
+ while (SDL_PollEvent(&se)) {
+ switch (se.type) {
+ case SDL_QUIT:
+ e->type = LZR_EVENT_QUIT;
+ should_quit = true;
+ return true;
+ case SDL_KEYDOWN: {
+ if (!config.disable_bind_menu &&
+ se.key.keysym.scancode == SCODE_BIND_MENU)
+ _bind_menu();
+ if (se.key.keysym.scancode == SCODE_FULLSCREEN)
+ LZR_ToggleFullscreen();
+ const int b = _scode_to_button(se.key.keysym.scancode);
+ if (se.key.repeat || b < 0)
+ break;
+ e->type = LZR_EVENT_BUTTON_DOWN;
+ e->button = b;
+ input[b] = true;
+ return true;
+ }
+ case SDL_MOUSEBUTTONDOWN: {
+ e->type = LZR_EVENT_BUTTON_DOWN;
+ e->button = LZR_BUTTON_MOUSE_L + se.button.button - 1;
+ e->x = se.button.x, e->y = se.button.y;
+ LZR_ScreenTransform(&e->x, &e->y);
+ mouse_x = e->x, mouse_y = e->y;
+ if (e->button >= LZR_BUTTON_COUNT)
+ continue;
+ input[e->button] = true;
+ return true;
+ }
+ case SDL_KEYUP: {
+ const int b = _scode_to_button(se.key.keysym.scancode);
+ if (b < 0)
+ break;
+ e->type = LZR_EVENT_BUTTON_UP;
+ e->button = b;
+ input[b] = false;
+ return true;
+ }
+ case SDL_MOUSEBUTTONUP: {
+ e->type = LZR_EVENT_BUTTON_DOWN;
+ e->button = LZR_BUTTON_MOUSE_L + se.button.button - 1;
+ e->x = se.button.x, e->y = se.button.y;
+ LZR_ScreenTransform(&e->x, &e->y);
+ mouse_x = e->x, mouse_y = e->y;
+ if (e->button >= LZR_BUTTON_COUNT)
+ continue;
+ input[e->button] = false;
+ return true;
+ }
+ case SDL_MOUSEMOTION: {
+ e->type = LZR_EVENT_MOUSE_MOVE;
+ e->x = se.motion.x, e->y = se.motion.y;
+ LZR_ScreenTransform(&e->x, &e->y);
+ mouse_x = e->x, mouse_y = e->y;
+ return true;
+ }
+ default:
+ break;
+ }
+ }
+ return false;
+}
+
+void LZR_CycleEvents(void)
+{
+ LZR_Event e;
+ while (LZR_PollEvent(&e))
+ ;
+}
+
+bool LZR_ButtonDown(LZR_Button btn)
+{
+ if (btn >= 0 && btn < LZR_BUTTON_COUNT)
+ return input[btn];
+ else
+ SDL_Log("%d button doesn't exist", btn);
+ return false;
+}
+
+void LZR_ButtonBind(LZR_Button btn, unsigned int code)
+{
+ if (btn < LZR_BUTTON_MOUSE_L && code != SCODE_BIND_MENU) {
+ map[btn] = code;
+ SDL_Log("bound key %s to button %u",
+ SDL_GetScancodeName(map[btn]), btn);
+ } else
+ SDL_Log("button %u can't be remapped to key %s", btn,
+ SDL_GetScancodeName(code));
+}
+
+int LZR_DrawBegin(void)
+{
+ if (config.target_fps > 0)
+ next_time += min_dt;
+ if (SDL_SetRenderTarget(renderer, target) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
+ SDL_RenderClear(renderer);
+ return 0;
+}
+
+int LZR_DrawEnd(void)
+{
+ if (SDL_SetRenderTarget(renderer, NULL)) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ LZR_DrawSetColor(0.0f, 0.0f, 0.0f, 1.0f);
+ if (LZR_DrawClear()) {
+ SDL_Log("LZY_DrawClear failed");
+ return -1;
+ }
+ if (config.target_fps) {
+ const uint_least64_t cur_time = SDL_GetTicks64();
+ if (next_time <= cur_time)
+ next_time = cur_time;
+ else
+ SDL_Delay(next_time - cur_time);
+ }
+ int win_w, win_h;
+ SDL_GetWindowSize(window, &win_w, &win_h);
+ const int width = config.display_width * config.ratio;
+ const int height = config.display_height;
+ const int ratio_w = win_w / width;
+ const int ratio_h = win_h / height;
+ scale = (ratio_w <= ratio_h) ? ratio_w : ratio_h;
+ off_x = (win_w - width * scale) / 2;
+ off_y = (win_h - height * scale) / 2;
+ const SDL_Rect dest = {off_x, off_y, width * scale, height * scale};
+ if (SDL_RenderCopyEx(renderer, target, NULL, &dest, 0.0, NULL, 0) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ SDL_RenderPresent(renderer);
+ tick++;
+ return 0;
+}
+
+int LZR_DrawSetColor(float r, float g, float b, float a)
+{
+ const unsigned int ur = (unsigned int)(r * 255) & 255;
+ const unsigned int ug = (unsigned int)(g * 255) & 255;
+ const unsigned int ub = (unsigned int)(b * 255) & 255;
+ const unsigned int ua = (unsigned int)(a * 255) & 255;
+ if (SDL_SetRenderDrawColor(renderer, ur, ug, ub, ua) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ color[0] = ur, color[1] = ug, color[2] = ub, color[3] = ua;
+ return 0;
+}
+
+int LZR_DrawClear(void)
+{
+ if (SDL_RenderClear(renderer) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawPoint(int x, int y)
+{
+ if (SDL_RenderDrawPoint(renderer, x, y) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawPoints(int *x, int *y, int n)
+{
+
+ if (n > (int)(config.display_width * config.display_height)) {
+ SDL_Log("%d > %u", n,
+ config.display_width * config.display_height);
+ return -1;
+ }
+ for (int i = 0; i < n; i++) {
+ points[i].x = x[i];
+ points[i].y = y[i];
+ }
+ if (SDL_RenderDrawPoints(renderer, points, n) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawLine(int x0, int y0, int x1, int y1)
+{
+ if (SDL_RenderDrawLine(renderer, x0, y0, x1, y1) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawRectangle(bool fill, int x, int y, int w, int h)
+{
+ SDL_Rect rect = {x, y, w, h};
+ if ((fill ? SDL_RenderFillRect : SDL_RenderDrawRect)(renderer, &rect)) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawCircle(bool fill, int x, int y, int radius)
+{
+#ifdef LZR_ENABLE_GFX
+ if ((fill ? filledCircleRGBA : circleRGBA)(renderer, x, y, radius,
+ UNPACKED_COLOR_RGBA) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+#else
+ (void)fill, (void)x, (void)y, (void)radius;
+ SDL_Log("LZR GFX module is disabled");
+ return -1;
+#endif
+}
+
+int LZR_DrawPolygon(bool fill, int *vx, int *vy, int n)
+{
+#ifdef LZR_ENABLE_GFX
+ if (n > 32) {
+ SDL_Log("%d > 32", n);
+ return -1;
+ }
+ Sint16 x[32], y[32];
+ for (int i = 0; i < n; i++)
+ x[i] = vx[i], y[i] = vy[i];
+ if ((fill ? filledPolygonRGBA : polygonRGBA)(renderer, x, y, n,
+ UNPACKED_COLOR_RGBA) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+#else
+ (void)fill, (void)vx, (void)vy, (void)n;
+ SDL_Log("LZR GFX module is disabled");
+ return -1;
+#endif
+}
+
+int LZR_DrawImage(int id, int x, int y)
+{
+ if (id < 0) {
+ SDL_Log("id is negative");
+ return -1;
+ }
+ if (id >= LZR_MAX_IMAGES || images[id].tex == NULL) {
+ SDL_Log("no image with id %d", id);
+ return -1;
+ }
+ if (SDL_SetTextureColorMod(images[id].tex, UNPACKED_COLOR) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ if (SDL_SetTextureAlphaMod(images[id].tex, color[3]) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ const SDL_Rect dest = {x, y, images[id].width, images[id].height};
+ if (SDL_RenderCopy(renderer, images[id].tex, NULL, &dest) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawImageEx(int id, int x, int y, LZR_ImageDrawSettings stg)
+{
+ if (id < 0) {
+ SDL_Log("id is negative");
+ return -1;
+ }
+ if (id >= LZR_MAX_IMAGES || images[id].tex == NULL) {
+ SDL_Log("no image with id %d", id);
+ return -1;
+ }
+ const int width = (stg.width > 0) ? stg.width : images[id].width;
+ const int height = (stg.height > 0) ? stg.height : images[id].height;
+ if (stg.center) {
+ x -= stg.scale_x * width / 2;
+ y -= stg.scale_y * height / 2;
+ }
+ SDL_Rect src = {stg.ix, stg.iy, width, height};
+ SDL_Rect dst = {x, y, width * stg.scale_x, height * stg.scale_y};
+ if (stg.ix < 0) {
+ src.w += stg.ix;
+ dst.x = 0 - stg.ix;
+ dst.w += stg.ix;
+ }
+ if (stg.iy < 0) {
+ src.y = 0 - stg.iy;
+ src.h += stg.iy;
+ dst.y -= stg.iy;
+ dst.h += stg.iy;
+ }
+ if (stg.ix + width > images[id].width) {
+ src.w = images[id].width - stg.ix;
+ dst.w = images[id].width - stg.ix;
+ }
+ if (stg.iy + height > images[id].height) {
+ src.h = images[id].height - stg.iy;
+ dst.h = images[id].height - stg.iy;
+ }
+ if (SDL_SetTextureColorMod(images[id].tex, UNPACKED_COLOR) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ if (SDL_SetTextureAlphaMod(images[id].tex, color[3]) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ const int flip = (stg.flip_v ? SDL_FLIP_VERTICAL : 0) |
+ (stg.flip_h ? SDL_FLIP_HORIZONTAL : 0);
+ if (SDL_RenderCopyEx(renderer, images[id].tex, &src, &dst,
+ stg.angle * 360.0, NULL, flip)) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_DrawTile(int id, int tile, int x, int y, double rot, int flip)
+{
+ if (id < 0) {
+ SDL_Log("id is negative");
+ return -1;
+ }
+ if (id >= LZR_MAX_IMAGES || images[id].tex == NULL) {
+ SDL_Log("no image with id %d", id);
+ return -1;
+ }
+ if (tile < 0) {
+ SDL_Log("tile is negative");
+ return -1;
+ }
+ const int img_width = images[id].width / config.tile_size;
+ const int img_height = images[id].height / config.tile_size;
+ if (tile >= img_width * img_height) {
+ SDL_Log("tile exceeds boundaries");
+ return -1;
+ }
+ if (SDL_SetTextureColorMod(images[id].tex, UNPACKED_COLOR) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ if (SDL_SetTextureAlphaMod(images[id].tex, color[3]) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ SDL_Rect src;
+ src.x = (tile % img_width) * config.tile_size;
+ src.y = (tile / img_width) * config.tile_size;
+ src.w = config.tile_size, src.h = config.tile_size;
+ const SDL_Rect dst = {x, y, config.tile_size, config.tile_size};
+ if (SDL_RenderCopyEx(renderer, images[id].tex, &src, &dst, rot, NULL,
+ flip) < 0) {
+ SDL_Log("%s", SDL_GetError());
+ return -1;
+ }
+ return 0;
+}
+
+int LZR_PlaySound(int id)
+{
+#ifdef LZR_ENABLE_MIXER
+ if (id < 0) {
+ SDL_Log("id is negative");
+ return -1;
+ }
+ if (id >= LZR_MAX_SOUNDS || sounds[id].ptr == NULL) {
+ SDL_Log("no sound with id %d", id);
+ return -1;
+ }
+ if (Mix_PlayChannel(-1, sounds[id].ptr, 0) < 0) {
+ SDL_Log("%s", Mix_GetError());
+ return -1;
+ }
+ return 0;
+#else
+ (void)id;
+ SDL_Log("LZR MIXER module is disabled");
+ return -1;
+#endif
+}
+
+int LZR_SetMusicVolume(float volume)
+{
+#ifdef LZR_ENABLE_MIXER
+ if (Mix_VolumeMusic(volume * MIX_MAX_VOLUME) < 0) {
+ SDL_Log("%s", Mix_GetError());
+ return -1;
+ }
+ return 0;
+#else
+ (void)volume;
+ SDL_Log("LZR MIXER module is disabled");
+ return -1;
+#endif
+}
+
+int LZR_PlayMusic(const char *path, int loops)
+{
+#ifdef LZR_ENABLE_MIXER
+ LZR_StopMusic();
+ char *const apath = LZR_PathPrefix(path);
+ if (apath == NULL) {
+ SDL_Log("LZR_PathPrefix failed");
+ return -1;
+ }
+ music = Mix_LoadMUS(apath);
+ free(apath);
+ if (music == NULL) {
+ SDL_Log("%s: %s", path, Mix_GetError());
+ return -1;
+ }
+ if (Mix_PlayMusic(music, loops) < 0) {
+ SDL_Log("%s", Mix_GetError());
+ return -1;
+ }
+ Mix_RewindMusic();
+ return 0;
+#else
+ (void)path, (void)loops;
+ SDL_Log("LZR MIXER module is disabled");
+ return -1;
+#endif
+}
+
+void LZR_StopMusic(void)
+{
+#ifdef LZR_ENABLE_MIXER
+ if (Mix_PlayingMusic())
+ Mix_HaltMusic();
+ if (music != NULL) {
+ Mix_FreeMusic(music);
+ music = NULL;
+ }
+#endif
+}
+
+void LZR_ToggleFullscreen(void)
+{
+ static int fullscreen = 0;
+ fullscreen = !fullscreen;
+ SDL_SetWindowFullscreen(window,
+ fullscreen * SDL_WINDOW_FULLSCREEN_DESKTOP);
+}
+
+uint64_t LZR_GetTick(void)
+{
+ return tick;
+}
+
+void LZR_ScreenTransform(int *x, int *y)
+{
+ if (scale == 0.0)
+ return;
+ if (x != NULL) {
+ *x -= off_x;
+ *x /= scale;
+ }
+ if (y != NULL) {
+ *y -= off_y;
+ *y /= scale;
+ }
+}
+
+void LZR_MousePosition(int *x, int *y)
+{
+ if (x != NULL)
+ *x = mouse_x;
+ if (y != NULL)
+ *y = mouse_y;
+}
+
+/*
+** Copyright (c) 2022, 2023 kdx
+**
+** Permission is hereby granted, free of charge, to any person obtaining a copy
+** of this software and associated documentation files (the "Software"), to
+** deal in the Software without restriction, including without limitation the
+** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+** sell copies of the Software, and to permit persons to whom the Software is
+** furnished to do so, subject to the following conditions:
+**
+** The above copyright notice and this permission notice shall be included in
+** all copies or substantial portions of the Software.
+**
+** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+** IN THE SOFTWARE.
+*/
diff --git a/src/lzr.h b/src/lzr.h
new file mode 100644
index 0000000..497bd52
--- /dev/null
+++ b/src/lzr.h
@@ -0,0 +1,129 @@
+/* Licensing informations can be found at the end of the file. */
+#pragma once
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifndef LZR_DISABLE_IMAGE
+# define LZR_ENABLE_IMAGE
+#endif
+#ifndef LZR_DISABLE_MIXER
+# define LZR_ENABLE_MIXER
+#endif
+#ifndef LZR_DISABLE_GFX
+# define LZR_ENABLE_GFX
+#endif
+
+/* devmode tracks and autoreloads ressources on change */
+#ifndef LZR_DISABLE_DEVMODE
+# define LZR_ENABLE_DEVMODE
+#endif
+
+#define LZR_MAX_IMAGES 64
+#define LZR_MAX_SOUNDS 64
+#define LZR_BUTTON(btn) LZR_ButtonDown(LZR_BUTTON_##btn)
+#define LZR_IMAGE(img) LZR_ImageLoad(img)
+
+typedef struct LZR_Config {
+ unsigned int display_width;
+ unsigned int display_height;
+ unsigned int target_fps;
+ unsigned int tile_size;
+ const char *title;
+ double ratio;
+ bool disable_bind_menu;
+ bool hide_cursor;
+} LZR_Config;
+
+typedef enum LZR_EventType {
+ LZR_EVENT_QUIT,
+ LZR_EVENT_BUTTON_DOWN,
+ LZR_EVENT_BUTTON_UP,
+ LZR_EVENT_MOUSE_MOVE
+} LZR_EventType;
+
+typedef enum LZR_Button {
+ LZR_BUTTON_LEFT,
+ LZR_BUTTON_RIGHT,
+ LZR_BUTTON_UP,
+ LZR_BUTTON_DOWN,
+ LZR_BUTTON_O,
+ LZR_BUTTON_X,
+ LZR_BUTTON_MOUSE_L,
+ LZR_BUTTON_MOUSE_M,
+ LZR_BUTTON_MOUSE_R,
+ LZR_BUTTON_COUNT
+} LZR_Button;
+
+typedef struct LZR_Event {
+ LZR_EventType type;
+ LZR_Button button;
+ int x, y;
+} LZR_Event;
+
+typedef struct LZR_ImageDrawSettings {
+ int ix, iy, width, height;
+ double scale_x, scale_y, angle;
+ bool center, flip_h, flip_v;
+} LZR_ImageDrawSettings;
+
+int LZR_Init(LZR_Config cfg);
+void LZR_Quit(void);
+bool LZR_ShouldQuit(void);
+bool LZR_PollEvent(LZR_Event *e);
+void LZR_CycleEvents(void);
+bool LZR_ButtonDown(LZR_Button btn);
+void LZR_ButtonBind(LZR_Button btn, unsigned int code);
+char *LZR_PathPrefix(const char *path);
+int LZR_ImageLoad(const char *path);
+int LZR_SoundLoad(const char *path, float volume);
+int LZR_DrawBegin(void);
+int LZR_DrawEnd(void);
+int LZR_DrawSetColor(float r, float g, float b, float a);
+int LZR_DrawClear(void);
+int LZR_DrawPoint(int x, int y);
+int LZR_DrawPoints(int *x, int *y, int n);
+int LZR_DrawLine(int x0, int y0, int x1, int y1);
+int LZR_DrawRectangle(bool fill, int x, int y, int w, int h);
+int LZR_DrawCircle(bool fill, int x, int y, int radius);
+int LZR_DrawPolygon(bool fill, int *vx, int *vy, int n);
+int LZR_DrawImage(int id, int x, int y);
+int LZR_DrawImageEx(int id, int x, int y, LZR_ImageDrawSettings stg);
+int LZR_DrawTile(int id, int tile, int x, int y, double rot, int flip);
+int LZR_PlaySound(int id);
+int LZR_SetMusicVolume(float volume);
+int LZR_PlayMusic(const char *path, int loops);
+void LZR_StopMusic(void);
+void LZR_ToggleFullscreen(void);
+uint64_t LZR_GetTick(void);
+void LZR_ScreenTransform(int *x, int *y);
+void LZR_MousePosition(int *x, int *y);
+
+#ifdef __cplusplus
+}
+#endif
+
+/*
+** Copyright (c) 2022, 2023 kdx
+**
+** Permission is hereby granted, free of charge, to any person obtaining a copy
+** of this software and associated documentation files (the "Software"), to
+** deal in the Software without restriction, including without limitation the
+** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+** sell copies of the Software, and to permit persons to whom the Software is
+** furnished to do so, subject to the following conditions:
+**
+** The above copyright notice and this permission notice shall be included in
+** all copies or substantial portions of the Software.
+**
+** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+** IN THE SOFTWARE.
+*/
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..ff51bb3
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,50 @@
+#include "lzr.h"
+#include "cfg.h"
+#include "game.h"
+#include <stdio.h>
+#include <stdlib.h>
+
+static Game *game = NULL;
+
+static void deinit(void);
+
+int
+main([[maybe_unused]] int argc, [[maybe_unused]] char **argv)
+{
+ if (LZR_Init(cfg)) {
+ LZR_Quit();
+ return 1;
+ }
+
+ if (atexit(deinit)) {
+ perror("main:atexit");
+ deinit();
+ return 1;
+ }
+
+ if ((game = malloc(sizeof(*game))) == NULL) {
+ perror("main:malloc");
+ return 1;
+ }
+ game_init(game);
+
+ while (!LZR_ShouldQuit()) {
+ LZR_CycleEvents();
+ game_update(game);
+
+ LZR_DrawBegin();
+ LZR_DrawSetColor(0, 0, 0, 0);
+ LZR_DrawClear();
+ game_draw(game);
+ LZR_DrawEnd();
+ }
+ return 0;
+}
+
+static void
+deinit(void)
+{
+ if (game != NULL)
+ free(game);
+ LZR_Quit();
+}
diff --git a/src/map.c b/src/map.c
new file mode 100644
index 0000000..e3fc6d2
--- /dev/null
+++ b/src/map.c
@@ -0,0 +1,54 @@
+#include "map.h"
+#include "cfg.h"
+#include "../map/maps.h"
+#include <stddef.h>
+
+static unsigned int map_id = 0;
+
+void
+map_next(void)
+{
+ if (tmj2c_maps[map_id + 1] != NULL)
+ map_id += 1;
+}
+
+int
+map_width(void)
+{
+ return tmj2c_maps[map_id]->width;
+}
+
+int
+map_height(void)
+{
+ return tmj2c_maps[map_id]->height;
+}
+
+int
+map_get(int x, int y)
+{
+ if (x < 0 || y < 0 || x >= map_width() || y >= map_height())
+ return 1;
+ return tmj2c_maps[map_id]->layers[0].data[x + y * map_width()];
+}
+
+int
+map_get_px(int x, int y)
+{
+ if (x < 0 || y < 0)
+ return 1;
+ return map_get(x / TSIZE, y / TSIZE);
+}
+
+void
+map_draw(void)
+{
+}
+
+const Tmj2cObject *
+map_objects(unsigned int *size)
+{
+ if (size != NULL)
+ *size = tmj2c_maps[map_id]->numobjects;
+ return tmj2c_maps[map_id]->objects;
+}
diff --git a/src/map.h b/src/map.h
new file mode 100644
index 0000000..325b3bc
--- /dev/null
+++ b/src/map.h
@@ -0,0 +1,10 @@
+#pragma once
+#include "../map/tmj2c.h"
+
+void map_next(void);
+int map_width(void);
+int map_height(void);
+int map_get(int x, int y);
+int map_get_px(int x, int y);
+void map_draw(void);
+const Tmj2cObject *map_objects(unsigned int *size);
diff --git a/src/player.c b/src/player.c
new file mode 100644
index 0000000..b51572b
--- /dev/null
+++ b/src/player.c
@@ -0,0 +1,17 @@
+#include "entityimpl.h"
+
+IMPL(draw) {
+ LZR_DrawSetColor(1, 1, 1, 1);
+ LZR_DrawRectangle(true, this->pos[0] - this->width / 2,
+ this->pos[1] - this->height / 2,
+ this->width, this->height);
+}
+
+IMPL(update) {
+ this->vel[0] = LZR_BUTTON(RIGHT) - LZR_BUTTON(LEFT);
+ entity_move(this, g);
+}
+
+IMPL_INIT(player) {
+ this->height = this->width = 12;
+}