summaryrefslogtreecommitdiff
path: root/src/eetg.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/eetg.c')
-rw-r--r--src/eetg.c719
1 files changed, 719 insertions, 0 deletions
diff --git a/src/eetg.c b/src/eetg.c
new file mode 100644
index 0000000..ebfd394
--- /dev/null
+++ b/src/eetg.c
@@ -0,0 +1,719 @@
+/*
+ * Copyright (c) 2024 Richard Braun.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for
+ * any purpose with or without fee is hereby granted.
+ *
+ * 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.
+ *
+ *
+ * Engine for embedded text-based games.
+ */
+
+#include <stdio.h>
+
+#include <assert.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+#include "eetg.h"
+#include "macros.h"
+
+#define EETG_RENDERING_DISABLED 0
+
+#define EETG_BG_COLOR EETG_COLOR_BLACK
+#define EETG_FG_COLOR EETG_COLOR_WHITE
+
+#define EETG_CSI "\e["
+
+static unsigned int eetg_rand_next = 1;
+
+static void
+eetg_object_set(struct eetg_object *object, struct eetg_world *world,
+ int x, int y)
+{
+ assert(object);
+ assert(!object->world);
+ assert(world);
+
+ object->world = world;
+ object->x = x;
+ object->y = y;
+}
+
+static void
+eetg_object_unset(struct eetg_object *object)
+{
+ assert(object);
+ assert(object->world);
+
+ object->world = NULL;
+ object->next = NULL;
+}
+
+static char
+eetg_object_get_char(const struct eetg_object *object, int x, int y)
+{
+ int index;
+
+ index = eetg_object_get_cell(object, x, y);
+
+ if (index == -1)
+ {
+ return index;
+ }
+
+ return object->sprite[index];
+}
+
+static void
+eetg_object_check_collision(struct eetg_object *object1,
+ struct eetg_object *object2,
+ eetg_handle_collision_fn handle_collision_fn,
+ void *handle_collision_fn_arg)
+{
+ int o1xbr, o1ybr, o2xbr, o2ybr;
+ int xtl, ytl, xbr, ybr;
+
+ assert(object1);
+ assert(object2);
+ assert(handle_collision_fn);
+
+ o1xbr = object1->x + object1->width - 1;
+ o1ybr = object1->y + object1->height - 1;
+
+ o2xbr = object2->x + object2->width - 1;
+ o2ybr = object2->y + object2->height - 1;
+
+ if ((object2->x > o1xbr) || (object1->x > o2xbr) ||
+ (object2->y > o1ybr) || (object1->y > o2ybr)) {
+ return;
+ }
+
+ xtl = (object1->x > object2->x) ? object1->x : object2->x;
+ ytl = (object1->y > object2->y) ? object1->y : object2->y;
+ xbr = (o1xbr < o2xbr) ? o1xbr : o2xbr;
+ ybr = (o1ybr < o2ybr) ? o1ybr : o2ybr;
+
+ assert(xtl <= xbr);
+ assert(ytl <= ybr);
+
+ for (int i = xtl; i <= xbr; i++) {
+ for (int j = ytl; j <= ybr; j++) {
+ char c1, c2;
+
+ c1 = eetg_object_get_char(object1, i, j);
+ c2 = eetg_object_get_char(object2, i, j);
+
+ if ((c1 <= 0) || (c2 <= 0)) {
+ return;
+ } else if ((c1 == ' ') || (c2 == ' ')) {
+ continue;
+ }
+
+ handle_collision_fn(object1, object2,
+ i, j, handle_collision_fn_arg);
+ return;
+ }
+ }
+}
+
+static void
+eetg_view_cell_set(struct eetg_view_cell *view_cell,
+ char c, int color)
+{
+ assert(view_cell);
+
+ view_cell->c = c;
+ view_cell->color = color;
+}
+
+static char
+eetg_view_cell_get_c(const struct eetg_view_cell *view_cell)
+{
+ assert(view_cell);
+
+ return view_cell->c;
+}
+
+static int
+eetg_view_cell_get_color(const struct eetg_view_cell *view_cell)
+{
+ assert(view_cell);
+
+ return view_cell->color;
+}
+
+static struct eetg_view_cell *
+eetg_view_row_get_cell(struct eetg_view_row *view_row, int index)
+{
+ assert(view_row);
+ assert(index >= 0);
+ assert(index < (int)ARRAY_SIZE(view_row->columns));
+
+ return &view_row->columns[index];
+}
+
+static void
+eetg_view_row_init(struct eetg_view_row *view_row)
+{
+ assert(view_row);
+
+ for (size_t i = 0; i < ARRAY_SIZE(view_row->columns); i++) {
+ struct eetg_view_cell *view_cell;
+
+ view_cell = eetg_view_row_get_cell(view_row, (int)i);
+ eetg_view_cell_set(view_cell, ' ', EETG_BG_COLOR);
+ }
+}
+
+static struct eetg_view_row *
+eetg_view_get_row(struct eetg_view *view, int index)
+{
+ assert(view);
+ assert(index >= 0);
+ assert(index < (int)ARRAY_SIZE(view->rows));
+
+ return &view->rows[index];
+}
+
+static void
+eetg_view_init(struct eetg_view *view)
+{
+ assert(view);
+
+ for (size_t i = 0; i < ARRAY_SIZE(view->rows); i++) {
+ eetg_view_row_init(eetg_view_get_row(view, (int)i));
+ }
+}
+
+void
+eetg_world_init(struct eetg_world *world, eetg_write_fn write_fn, void *arg)
+{
+ assert(world);
+ assert(write_fn);
+
+ world->write_fn = write_fn;
+ world->write_fn_arg = arg;
+
+ world->handle_collision_fn = NULL;
+ world->objects = NULL;
+
+ eetg_view_init(&world->views[0]);
+ eetg_view_init(&world->views[1]);
+
+ world->view = &world->views[0];
+ world->prev_view = &world->views[1];
+
+ world->cursor_row = -1;
+ world->cursor_column = -1;
+ world->current_color = EETG_FG_COLOR;
+}
+
+void
+eetg_world_clear(struct eetg_world *world)
+{
+ struct eetg_object *object;
+
+ assert(world);
+
+ object = world->objects;
+
+ while (object) {
+ struct eetg_object *next = object->next;
+
+ eetg_object_unset(object);
+ object = next;
+ }
+
+ world->objects = NULL;
+}
+
+void
+eetg_world_register_collision_fn(struct eetg_world *world,
+ eetg_handle_collision_fn handle_collision_fn,
+ void *arg)
+{
+ assert(world);
+ assert(handle_collision_fn);
+
+ world->handle_collision_fn = handle_collision_fn;
+ world->handle_collision_fn_arg = arg;
+}
+
+static void
+eetg_world_scan_collisions(struct eetg_world *world, struct eetg_object *object)
+{
+ assert(world);
+
+ if (!world->handle_collision_fn) {
+ return;
+ }
+
+ for (struct eetg_object *tmp = world->objects; tmp; tmp = tmp->next) {
+ if (tmp == object) {
+ continue;
+ }
+
+ eetg_object_check_collision(object, tmp,
+ world->handle_collision_fn,
+ world->handle_collision_fn_arg);
+ }
+}
+
+void
+eetg_world_add(struct eetg_world *world, struct eetg_object *object,
+ int x, int y)
+{
+ assert(world);
+ assert(eetg_object_get_world(object) == NULL);
+
+ object->next = world->objects;
+ world->objects = object;
+
+ eetg_object_set(object, world, x, y);
+ eetg_world_scan_collisions(world, object);
+}
+
+void
+eetg_world_remove(struct eetg_world *world, struct eetg_object *object)
+{
+ assert(world);
+ assert(eetg_object_get_world(object) == world);
+
+ if (world->objects == object) {
+ world->objects = object->next;
+ goto out;
+ }
+
+ for (struct eetg_object *tmp = world->objects; tmp->next; tmp = tmp->next) {
+ if (tmp->next == object) {
+ tmp->next = tmp->next->next;
+ break;
+ }
+ }
+
+out:
+ eetg_object_unset(object);
+}
+
+static void
+eetg_object_render_row(struct eetg_object *object, int row,
+ struct eetg_view_row *view_row)
+{
+ const char *line;
+
+ assert(object);
+ assert(row < object->height);
+
+ line = &object->sprite[(object->width + 1) * row];
+
+ for (int obj_column = 0; obj_column < object->width; obj_column++) {
+ int column = object->x + obj_column;
+ char c;
+
+ if ((column < 0) || (column >= EETG_COLUMNS)) {
+ continue;
+ }
+
+ c = line[obj_column];
+
+ if (c != ' ') {
+ struct eetg_view_cell *view_cell;
+
+ view_cell = eetg_view_row_get_cell(view_row, column);
+ eetg_view_cell_set(view_cell, c, object->color);
+ }
+ }
+}
+
+static void
+eetg_object_render(struct eetg_object *object, struct eetg_view *view)
+{
+ assert(object);
+
+ for (int obj_row = 0; obj_row < object->height; obj_row++) {
+ int row = object->y + obj_row;
+
+ if ((row < 0) || (row >= EETG_ROWS)) {
+ continue;
+ }
+
+ eetg_object_render_row(object, obj_row, eetg_view_get_row(view, row));
+ }
+}
+
+static void
+eetg_world_write(struct eetg_world *world, const void *buffer, size_t size)
+{
+ assert(world);
+ assert(world->write_fn);
+ assert(buffer);
+
+#if EETG_RENDERING_DISABLED
+ (void)buffer;
+ (void)size;
+#else
+ world->write_fn(buffer, size, world->write_fn_arg);
+#endif
+}
+
+static void
+eetg_world_write_str(struct eetg_world *world, const char *str)
+{
+ eetg_world_write(world, str, strlen(str));
+}
+
+static void
+eetg_world_set_cursor(struct eetg_world *world, int row, int column)
+{
+ char str[32];
+
+ assert(world);
+ assert(row >= 0);
+ assert(row < EETG_ROWS);
+ assert(column >= 0);
+ assert(column < EETG_COLUMNS);
+
+ if ((world->cursor_row == row) && (world->cursor_column == column)) {
+ return;
+ }
+
+ snprintf(str, sizeof(str), EETG_CSI "%d;%dH", row + 1, column + 1);
+ eetg_world_write_str(world, str);
+
+ world->cursor_row = row;
+ world->cursor_column = column;
+}
+
+static int
+eetg_convert_fg_color(int color)
+{
+ return color + 30;
+}
+
+static int
+eetg_convert_bg_color(int color)
+{
+ return eetg_convert_fg_color(color) + 10;
+}
+
+static void
+eetg_world_set_color(struct eetg_world *world, int color, bool force)
+{
+ char str[16];
+
+ if ((color == world->current_color) && !force) {
+ return;
+ }
+
+ snprintf(str, sizeof(str), EETG_CSI "%d;%dm",
+ eetg_convert_fg_color(color),
+ eetg_convert_bg_color(EETG_BG_COLOR));
+
+ eetg_world_write_str(world, str);
+
+ world->current_color = color;
+}
+
+static void
+eetg_world_write_char(struct eetg_world *world, char c)
+{
+ eetg_world_write(world, &c, sizeof(c));
+
+ world->cursor_column++;
+
+ if (world->cursor_column == EETG_COLUMNS) {
+ if (world->cursor_row == EETG_ROWS) {
+ world->cursor_column = EETG_COLUMNS - 1;
+ } else {
+ world->cursor_column = 0;
+ world->cursor_row++;
+ }
+ }
+}
+
+static void
+eetg_world_swap_views(struct eetg_world *world)
+{
+ struct eetg_view *tmp;
+
+ assert(world);
+
+ tmp = world->view;
+ world->view = world->prev_view;
+ world->prev_view = tmp;
+}
+
+static void
+eetg_world_render_sync(struct eetg_world *world)
+{
+ assert(world);
+
+ eetg_world_write_str(world, EETG_CSI "?25l"); /* cursor invisible */
+ eetg_world_set_color(world, EETG_FG_COLOR, true);
+ eetg_world_write_str(world, EETG_CSI "2J"); /* clear screen */
+ eetg_world_set_cursor(world, 0, 0);
+
+ for (int row = 0; row < EETG_ROWS; row++) {
+ struct eetg_view_row *view_row;
+
+ view_row = eetg_view_get_row(world->view, row);
+
+ for (int column = 0; column < EETG_COLUMNS; column++) {
+ struct eetg_view_cell *view_cell;
+ int color;
+ char c;
+
+ view_cell = eetg_view_row_get_cell(view_row, column);
+ color = eetg_view_cell_get_color(view_cell);
+ c = eetg_view_cell_get_c(view_cell);
+
+ if (c == ' ') {
+ continue;
+ }
+
+ eetg_world_set_cursor(world, row, column);
+ eetg_world_set_color(world, color, false);
+ eetg_world_write_char(world, c);
+ }
+ }
+}
+
+static void
+eetg_world_render_delta(struct eetg_world *world)
+{
+ assert(world);
+
+ for (int row = 0; row < EETG_ROWS; row++) {
+ struct eetg_view_row *view_row, *prev_view_row;
+
+ view_row = eetg_view_get_row(world->view, row);
+ prev_view_row = eetg_view_get_row(world->prev_view, row);
+
+ for (int column = 0; column < EETG_COLUMNS; column++) {
+ struct eetg_view_cell *view_cell, *prev_view_cell;
+ int color, prev_color;
+ char c, prev_c;
+
+ view_cell = eetg_view_row_get_cell(view_row, column);
+ color = eetg_view_cell_get_color(view_cell);
+ c = eetg_view_cell_get_c(view_cell);
+
+ prev_view_cell = eetg_view_row_get_cell(prev_view_row, column);
+ prev_color = eetg_view_cell_get_color(prev_view_cell);
+ prev_c = eetg_view_cell_get_c(prev_view_cell);
+
+ if ((color != prev_color) || (c != prev_c)) {
+ eetg_world_set_cursor(world, row, column);
+ eetg_world_set_color(world, color, false);
+ eetg_world_write_char(world, c);
+ }
+ }
+ }
+}
+
+void
+eetg_world_render(struct eetg_world *world, bool sync)
+{
+ for (int row = 0; row < EETG_ROWS; row++) {
+ struct eetg_view_row *view_row;
+
+ view_row = eetg_view_get_row(world->view, row);
+
+ for (int column = 0; column < EETG_COLUMNS; column++) {
+ struct eetg_view_cell *view_cell;
+
+ view_cell = eetg_view_row_get_cell(view_row, column);
+ eetg_view_cell_set(view_cell, ' ', EETG_FG_COLOR);
+ }
+ }
+
+ for (struct eetg_object *obj = world->objects; obj; obj = obj->next) {
+ eetg_object_render(obj, world->view);
+ }
+
+ if (sync) {
+ eetg_world_render_sync(world);
+ } else {
+ eetg_world_render_delta(world);
+ }
+
+ eetg_world_set_cursor(world, 0, 0);
+ eetg_world_swap_views(world);
+}
+
+void
+eetg_object_init(struct eetg_object *object, int type, const char *sprite)
+{
+ size_t width;
+ char *ptr;
+
+ assert(object);
+
+ object->world = NULL;
+ object->next = NULL;
+ object->sprite = sprite;
+ object->type = type;
+ object->x = 0;
+ object->y = 0;
+ object->color = EETG_FG_COLOR;
+
+ ptr = strchr(sprite, '\n');
+ assert(ptr);
+
+ width = ptr - sprite;
+ assert(width <= INT_MAX);
+
+ object->width = (int)width;
+ object->height = 1;
+
+ for (;;) {
+ char *next;
+
+ ptr = &ptr[1];
+ next = strchr(ptr, '\n');
+
+ if (!next) {
+ break;
+ }
+
+ width = next - ptr;
+ assert(width <= INT_MAX);
+ assert((int)width == object->width);
+
+ assert(object->height < INT_MAX);
+ object->height++;
+
+ ptr = next;
+ }
+}
+
+void
+eetg_object_set_color(struct eetg_object *object, int color)
+{
+ assert(object);
+
+ object->color = color;
+}
+
+int
+eetg_object_get_type(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->type;
+}
+
+const char *
+eetg_object_get_sprite(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->sprite;
+}
+
+int
+eetg_object_get_x(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->x;
+}
+
+int
+eetg_object_get_y(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->y;
+}
+
+int
+eetg_object_get_width(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->width;
+}
+
+int
+eetg_object_get_height(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->height;
+}
+
+bool
+eetg_object_is_empty(const struct eetg_object *object)
+{
+ assert(object);
+
+ for (const char *ptr = object->sprite; ptr; ptr++) {
+ char c = *ptr;
+
+ if (c == '\0') {
+ break;
+ } else if ((c != ' ') && (c != '\n')) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void
+eetg_object_move(struct eetg_object *object, int x, int y)
+{
+ assert(object);
+
+ object->x = x;
+ object->y = y;
+
+ if (object->world) {
+ eetg_world_scan_collisions(object->world, object);
+ }
+}
+
+int
+eetg_object_get_cell(const struct eetg_object *object, int x, int y)
+{
+ assert(object);
+
+ x -= object->x;
+ y -= object->y;
+
+ if ((x < 0) || (x >= object->width) || (y < 0) || (y >= object->height)) {
+ return -1;
+ }
+
+ return ((object->width + 1) * y) + (x % object->width);
+}
+
+struct eetg_world *
+eetg_object_get_world(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->world;
+}
+
+void eetg_init_rand(unsigned int seed)
+{
+ eetg_rand_next = seed;
+}
+
+int eetg_rand(void)
+{
+ eetg_rand_next = (eetg_rand_next * 1103515245) + 12345;
+ return (eetg_rand_next / 65536) % (EETG_RAND_MAX + 1);
+}