aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi.dcr@tutanota.com>2021-07-03 00:49:03 +0200
committerXavier Del Campo Romero <xavi.dcr@tutanota.com>2022-03-30 08:20:20 +0200
commit6b9f686913efc3725b2690033cd4f398e07076ba (patch)
treee9aa91a6b9f617d78123ebe7ad272fc42a60d306 /src
parentc9e6ae44a9aeb89b3f48f3443d6baa80103f7445 (diff)
downloadjancity-6b9f686913efc3725b2690033cd4f398e07076ba.tar.gz
Add project source code
Diffstat (limited to 'src')
-rw-r--r--src/building/CMakeLists.txt3
-rw-r--r--src/building/inc/building.h41
-rw-r--r--src/building/inc/building_type.h11
-rw-r--r--src/building/src/building.c80
-rw-r--r--src/camera/CMakeLists.txt3
-rw-r--r--src/camera/inc/camera.h42
-rw-r--r--src/camera/src/camera.c203
-rw-r--r--src/container/CMakeLists.txt11
-rw-r--r--src/container/inc/container.h43
-rw-r--r--src/container/ps1/inc/container/port.h6
-rw-r--r--src/container/sdl-1.2/inc/container/port.h6
-rw-r--r--src/container/src/container.c225
-rw-r--r--src/font/CMakeLists.txt3
-rw-r--r--src/font/inc/font.h22
-rw-r--r--src/font/src/font.c110
-rw-r--r--src/game/CMakeLists.txt15
-rw-r--r--src/game/inc/game.h15
-rw-r--r--src/game/privinc/game_private.h16
-rw-r--r--src/game/src/game.c101
-rw-r--r--src/game/src/res.c247
-rw-r--r--src/gfx/CMakeLists.txt54
-rw-r--r--src/gfx/inc/gfx.h37
-rw-r--r--src/gfx/privinc/gfx_private.h22
-rw-r--r--src/gfx/ps1/inc/gfx/port.h142
-rw-r--r--src/gfx/ps1/privinc/ps1/gfx_private.h25
-rw-r--r--src/gfx/ps1/src/4line.c14
-rw-r--r--src/gfx/ps1/src/env.c42
-rw-r--r--src/gfx/ps1/src/heap.c19
-rw-r--r--src/gfx/ps1/src/init.c16
-rw-r--r--src/gfx/ps1/src/quad.c23
-rw-r--r--src/gfx/ps1/src/rect.c23
-rw-r--r--src/gfx/ps1/src/sort.c75
-rw-r--r--src/gfx/ps1/src/sprite.c244
-rw-r--r--src/gfx/sdl-1.2/inc/gfx/port.h52
-rw-r--r--src/gfx/sdl-1.2/privinc/sdl-1.2/gfx_private.h19
-rw-r--r--src/gfx/sdl-1.2/src/env.c82
-rw-r--r--src/gfx/sdl-1.2/src/heap.c11
-rw-r--r--src/gfx/sdl-1.2/src/line.c15
-rw-r--r--src/gfx/sdl-1.2/src/quad.c18
-rw-r--r--src/gfx/sdl-1.2/src/rect.c33
-rw-r--r--src/gfx/sdl-1.2/src/sort.c22
-rw-r--r--src/gfx/sdl-1.2/src/sprite.c75
-rw-r--r--src/gfx/src/heap.c56
-rw-r--r--src/gui/CMakeLists.txt3
-rw-r--r--src/gui/inc/gui.h36
-rw-r--r--src/gui/src/gui.c473
-rw-r--r--src/instance/CMakeLists.txt3
-rw-r--r--src/instance/inc/instance.h68
-rw-r--r--src/instance/src/instance.c150
-rw-r--r--src/main.c14
-rw-r--r--src/pad/CMakeLists.txt10
-rw-r--r--src/pad/inc/pad.h49
-rw-r--r--src/pad/privinc/pad_private.h17
-rw-r--r--src/pad/ps1/inc/pad/port.h20
-rw-r--r--src/pad/ps1/src/pad.c41
-rw-r--r--src/pad/sdl-1.2/inc/pad/port.h20
-rw-r--r--src/pad/sdl-1.2/src/pad.c80
-rw-r--r--src/pad/src/pad.c37
-rw-r--r--src/player/CMakeLists.txt16
-rw-r--r--src/player/inc/human_player.h69
-rw-r--r--src/player/inc/player.h71
-rw-r--r--src/player/src/human_player.c570
-rw-r--r--src/player/src/player.c112
-rw-r--r--src/resource/CMakeLists.txt3
-rw-r--r--src/resource/inc/resource.h57
-rw-r--r--src/resource/inc/resource_type.h21
-rw-r--r--src/resource/src/resource.c139
-rw-r--r--src/sfx/CMakeLists.txt37
-rw-r--r--src/sfx/inc/sfx.h22
-rw-r--r--src/sfx/ps1/inc/sfx/port.h17
-rw-r--r--src/sfx/ps1/src/sound.c188
-rw-r--r--src/sfx/sdl-1.2/inc/sfx/port.h11
-rw-r--r--src/sfx/sdl-1.2/src/sound.c71
-rw-r--r--src/system.cnf4
-rw-r--r--src/system/CMakeLists.txt13
-rw-r--r--src/system/inc/system.h17
-rw-r--r--src/system/ps1/inc/system/port.h17
-rw-r--r--src/system/ps1/src/init.c32
-rw-r--r--src/system/sdl-1.2/inc/system/port.h13
-rw-r--r--src/system/sdl-1.2/src/system.c36
-rw-r--r--src/tech/CMakeLists.txt2
-rw-r--r--src/tech/inc/tech.h20
-rw-r--r--src/terrain/CMakeLists.txt3
-rw-r--r--src/terrain/inc/terrain.h41
-rw-r--r--src/terrain/src/terrain.c67
-rw-r--r--src/unit/CMakeLists.txt3
-rw-r--r--src/unit/inc/unit.h134
-rw-r--r--src/unit/inc/unit_type.h11
-rw-r--r--src/unit/src/unit.c660
-rw-r--r--src/util/CMakeLists.txt2
-rw-r--r--src/util/inc/util.h31
-rw-r--r--src/util/src/util.c10
92 files changed, 5863 insertions, 0 deletions
diff --git a/src/building/CMakeLists.txt b/src/building/CMakeLists.txt
new file mode 100644
index 0000000..3eb352e
--- /dev/null
+++ b/src/building/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_library(building "src/building.c")
+target_include_directories(building PUBLIC "inc")
+target_link_libraries(building PUBLIC gfx instance camera)
diff --git a/src/building/inc/building.h b/src/building/inc/building.h
new file mode 100644
index 0000000..7763b7d
--- /dev/null
+++ b/src/building/inc/building.h
@@ -0,0 +1,41 @@
+#ifndef BUILDING_H
+#define BUILDING_H
+
+#include <building_type.h>
+#include <camera.h>
+#include <gfx.h>
+#include <instance.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+struct building
+{
+ struct instance instance;
+ enum building_type type;
+};
+
+UTIL_STATIC_ASSERT(!offsetof(struct building, instance), "must be at offset zero");
+
+struct building_cfg
+{
+ enum building_type type;
+ unsigned long x, y;
+};
+
+void building_create(const struct building_cfg *cfg, struct building *b);
+int building_render(const struct building *b, const struct camera *cam, bool sel);
+instance_hp building_maxhp(const struct building *b);
+const char *building_str(const struct building *b);
+
+extern struct sprite building_sprites[MAX_BUILDING_TYPES];
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* BUILDING_H */
diff --git a/src/building/inc/building_type.h b/src/building/inc/building_type.h
new file mode 100644
index 0000000..eca76b4
--- /dev/null
+++ b/src/building/inc/building_type.h
@@ -0,0 +1,11 @@
+#ifndef BUILDING_TYPE_H
+#define BUILDING_TYPE_H
+
+enum building_type
+{
+ BUILDING_TYPE_BARRACKS,
+
+ MAX_BUILDING_TYPES
+};
+
+#endif /* BUILDING_TYPE_H */
diff --git a/src/building/src/building.c b/src/building/src/building.c
new file mode 100644
index 0000000..0b90370
--- /dev/null
+++ b/src/building/src/building.c
@@ -0,0 +1,80 @@
+#include <building.h>
+#include <camera.h>
+#include <gfx.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+struct sprite building_sprites[MAX_BUILDING_TYPES];
+
+instance_hp building_maxhp(const struct building *const b)
+{
+ static const instance_hp hp[] =
+ {
+ [BUILDING_TYPE_BARRACKS] = 100
+ };
+
+ return hp[b->type];
+}
+
+int building_render(const struct building *const b,
+ const struct camera *const cam, const bool sel)
+{
+ if (!b->instance.alive)
+ return 0;
+
+ struct sprite *const s = sprite_get();
+
+ if (!s || sprite_clone(&building_sprites[b->type], s))
+ return -1;
+
+ const struct instance_render_cfg cfg =
+ {
+ .i = &b->instance,
+ .prim_type = INSTANCE_RENDER_CFG_SPRITE,
+ .prim = {.s = s},
+ .cam = cam,
+ .sel = sel,
+ .max_hp = building_maxhp(b)
+ };
+
+ return instance_render(&cfg);
+}
+
+static void get_dimensions(const enum building_type type, short *const w,
+ short *const h)
+{
+ static const struct dim
+ {
+ short w, h;
+ } dim[] =
+ {
+ [BUILDING_TYPE_BARRACKS] = {.w = 68, .h = 66}
+ };
+
+ const struct dim *const d = &dim[type];
+ *w = d->w;
+ *h = d->h;
+}
+
+void building_create(const struct building_cfg *const cfg,
+ struct building *const b)
+{
+ struct instance *const i = &b->instance;
+
+ get_dimensions(cfg->type, &i->r.w, &i->r.h);
+ b->type = cfg->type;
+ i->r.x = cfg->x;
+ i->r.y = cfg->y;
+ i->alive = true;
+ i->hp = building_maxhp(b);
+}
+
+const char *building_str(const struct building *const b)
+{
+ static const char *const str[] =
+ {
+ [BUILDING_TYPE_BARRACKS] = "Barracks"
+ };
+
+ return str[b->type];
+}
diff --git a/src/camera/CMakeLists.txt b/src/camera/CMakeLists.txt
new file mode 100644
index 0000000..fd029b0
--- /dev/null
+++ b/src/camera/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_library(camera "src/camera.c")
+target_include_directories(camera PUBLIC "inc")
+target_link_libraries(camera PUBLIC container pad util PRIVATE gfx terrain)
diff --git a/src/camera/inc/camera.h b/src/camera/inc/camera.h
new file mode 100644
index 0000000..1daebd2
--- /dev/null
+++ b/src/camera/inc/camera.h
@@ -0,0 +1,42 @@
+#ifndef CAMERA_H
+#define CAMERA_H
+
+#include <pad.h>
+#include <util.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+struct camera
+{
+ int x, y, x_speed, y_speed;
+ unsigned int xt, yt;
+
+ struct cursor
+ {
+ unsigned int x, y, x_init, y_init;
+ enum
+ {
+ CURSOR_STATE_IDLE,
+ CURSOR_STATE_PRESSED
+ } state;
+ } cursor;
+};
+
+extern struct sprite cursor_sprite;
+
+void camera_update(struct camera *cam, const struct pad *p);
+bool camera_translate(const struct camera *cam, const struct util_rect *dim, short *x, short *y);
+void cursor_init(struct cursor *c);
+bool cursor_collision(const struct camera *cam, const struct util_rect *d);
+void cursor_pos(const struct camera *cam, unsigned long *x, unsigned long *y);
+int cursor_render(const struct cursor *c);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* CAMERA_H */
diff --git a/src/camera/src/camera.c b/src/camera/src/camera.c
new file mode 100644
index 0000000..4b046e1
--- /dev/null
+++ b/src/camera/src/camera.c
@@ -0,0 +1,203 @@
+#include <camera.h>
+#include <gfx.h>
+#include <pad.h>
+#include <terrain.h>
+#include <util.h>
+#include <limits.h>
+#include <stdbool.h>
+
+struct sprite cursor_sprite;
+
+enum
+{
+ CURSOR_WIDTH = 20,
+ CURSOR_HEIGHT = 20
+};
+
+bool cursor_collision(const struct camera *const cam, const struct util_rect *const d)
+{
+ unsigned long x, y;
+
+ cursor_pos(cam, &x, &y);
+
+ const struct util_rect cd =
+ {
+ .x = x,
+ .y = y,
+ .w = CURSOR_WIDTH / 4,
+ .h = CURSOR_HEIGHT / 4
+ };
+
+ return util_collision(d, &cd);
+}
+
+void cursor_pos(const struct camera *const cam, unsigned long *const x,
+ unsigned long *const y)
+{
+ const struct cursor *const c = &cam->cursor;
+
+ *x = c->x - cam->x;
+ *y = c->y - cam->y;
+}
+
+static void cursor_update(struct camera *const cam, const struct pad *const p)
+{
+ struct cursor *const c = &cam->cursor;
+ enum {STEP = 4};
+
+ if (pad_pressed(p, PAD_KEY_LEFT)
+ && (c->x - STEP)
+ && (!cam->x || c->x != c->x_init))
+ c->x -= STEP;
+ else if (pad_pressed(p, PAD_KEY_RIGHT)
+ && (c->x + STEP < screen_w)
+ && (c->x != c->x_init || cam->x <= -MAP_X))
+ c->x += STEP;
+
+ if (pad_pressed(p, PAD_KEY_UP)
+ && (c->y - STEP)
+ && (c->y != c->y_init || !cam->y))
+ c->y -= STEP;
+ else if (pad_pressed(p, PAD_KEY_DOWN)
+ && (c->y + STEP < screen_h)
+ && (c->y != c->y_init || cam->y <= -MAP_Y))
+ c->y += STEP;
+
+ c->state = pad_pressed(p, PAD_KEY_A) || pad_pressed(p, PAD_KEY_B) ?
+ CURSOR_STATE_PRESSED: CURSOR_STATE_IDLE;
+}
+
+int cursor_render(const struct cursor *const c)
+{
+ struct sprite *const s = sprite_get();
+
+ if (!s || sprite_clone(&cursor_sprite, s))
+ return -1;
+
+ s->x = c->x;
+ s->y = c->y;
+ s->w = CURSOR_WIDTH;
+ s->h = CURSOR_HEIGHT;
+
+ switch (c->state)
+ {
+ case CURSOR_STATE_IDLE:
+ s->u += CURSOR_WIDTH;
+ break;
+
+ case CURSOR_STATE_PRESSED:
+ break;
+ }
+
+ sprite_sort(s);
+ return 0;
+}
+
+void cursor_init(struct cursor *const c)
+{
+ c->x = c->x_init = (screen_w / 2) - CURSOR_WIDTH;
+ c->y = c->y_init = (screen_h / 2) - CURSOR_HEIGHT;
+}
+
+static void update_speed(struct camera *const cam, const struct pad *const p)
+{
+ enum
+ {
+ MAX_SPEED = 10,
+ STEP = 1,
+ T_STEP = 3
+ };
+
+ const struct cursor *const c = &cam->cursor;
+
+ if (c->x == c->x_init
+ && (!cam->x_speed || ++cam->xt >= T_STEP))
+ {
+ if (pad_pressed(p, PAD_KEY_RIGHT))
+ {
+ if (cam->x_speed > 0)
+ cam->x_speed = -STEP;
+ else if (cam->x_speed - STEP >= -MAX_SPEED)
+ cam->x_speed -= STEP;
+ }
+ else if (pad_pressed(p, PAD_KEY_LEFT))
+ {
+ if (cam->x_speed < 0)
+ cam->x_speed = STEP;
+ else if (cam->x_speed + STEP <= MAX_SPEED)
+ cam->x_speed += STEP;
+ }
+ else
+ cam->x_speed = 0;
+
+ cam->xt = 0;
+ }
+ else if (c->x != c->x_init)
+ cam->x_speed = 0;
+
+ if (c->y == c->y_init
+ && (!cam->y_speed || ++cam->yt >= T_STEP))
+ {
+ if (pad_pressed(p, PAD_KEY_DOWN))
+ {
+ if (cam->y_speed > 0)
+ cam->y_speed = STEP;
+ else if (cam->y_speed - STEP >= -MAX_SPEED)
+ cam->y_speed -= STEP;
+ }
+ else if (pad_pressed(p, PAD_KEY_UP))
+ {
+ if (cam->y_speed < 0)
+ cam->y_speed = -STEP;
+ else if (cam->y_speed + STEP <= MAX_SPEED)
+ cam->y_speed += STEP;
+ }
+ else
+ cam->y_speed = 0;
+
+ cam->yt = 0;
+ }
+ else if (c->y != c->y_init)
+ cam->y_speed = 0;
+}
+
+static void update_pos(struct camera *const cam)
+{
+ const int x = cam->x + cam->x_speed;
+
+ cam->x = x;
+
+ if (cam->x > 0)
+ cam->x = 0;
+ else if (cam->x < -MAP_X)
+ cam->x = -MAP_X;
+
+ const int y = cam->y + cam->y_speed;
+
+ cam->y = y;
+
+ if (cam->y > 0)
+ cam->y = 0;
+ else if (cam->y < -MAP_Y)
+ cam->y = -MAP_Y;
+}
+
+void camera_update(struct camera *const cam, const struct pad *const p)
+{
+ cursor_update(cam, p);
+ update_speed(cam, p);
+ update_pos(cam);
+}
+
+bool camera_translate(const struct camera *const cam, const struct util_rect *const dim,
+ short *const x, short *const y)
+{
+ const long tx = dim->x + cam->x, ty = dim->y + cam->y;
+
+ if (!gfx_inside_drawenv(tx, ty, dim->w, dim->h))
+ return false;
+
+ *x = tx;
+ *y = ty;
+ return true;
+}
diff --git a/src/container/CMakeLists.txt b/src/container/CMakeLists.txt
new file mode 100644
index 0000000..740fe37
--- /dev/null
+++ b/src/container/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(inc "inc")
+
+if(PS1_BUILD)
+ set(inc ${inc} "ps1/inc")
+elseif(SDL1_2_BUILD)
+ set(inc ${inc} "sdl-1.2/inc")
+endif()
+
+add_library(container "src/container.c")
+target_include_directories(container PUBLIC ${inc})
+target_link_libraries(container PUBLIC gfx sfx)
diff --git a/src/container/inc/container.h b/src/container/inc/container.h
new file mode 100644
index 0000000..59ab5fd
--- /dev/null
+++ b/src/container/inc/container.h
@@ -0,0 +1,43 @@
+#ifndef CONTAINER_H
+#define CONTAINER_H
+
+#include <container/port.h>
+#include <gfx.h>
+#include <sfx.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+struct container
+{
+ const char *path;
+
+ enum
+ {
+ CONTAINER_TYPE_SPRITE,
+ CONTAINER_TYPE_SOUND
+ } type;
+
+ union
+ {
+ struct sprite *sprite;
+ struct sound *sound;
+ } data;
+
+ struct container_rt
+ {
+ struct container *c;
+ } *rt;
+};
+
+int container_load_ex(const char *path, const struct container *list, size_t n);
+void container_free(const struct container *list, size_t n);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* CONTAINER_H */
diff --git a/src/container/ps1/inc/container/port.h b/src/container/ps1/inc/container/port.h
new file mode 100644
index 0000000..aaf2f96
--- /dev/null
+++ b/src/container/ps1/inc/container/port.h
@@ -0,0 +1,6 @@
+#ifndef CONTAINER_PS1_H
+#define CONTAINER_PS1_H
+
+#define container_load(path, list, n) container_load_ex("cdrom:\\"path";1", list, n)
+
+#endif /* CONTAINER_PS1_H */
diff --git a/src/container/sdl-1.2/inc/container/port.h b/src/container/sdl-1.2/inc/container/port.h
new file mode 100644
index 0000000..b58697d
--- /dev/null
+++ b/src/container/sdl-1.2/inc/container/port.h
@@ -0,0 +1,6 @@
+#ifndef CONTAINER_SDL_12_H
+#define CONTAINER_SDL_12_H
+
+#define container_load(path, list, n) container_load_ex(path, list, n)
+
+#endif /* CONTAINER_SDL_12_H */
diff --git a/src/container/src/container.c b/src/container/src/container.c
new file mode 100644
index 0000000..008ca58
--- /dev/null
+++ b/src/container/src/container.c
@@ -0,0 +1,225 @@
+#include <container.h>
+#include <gfx.h>
+#include <sfx.h>
+#include <errno.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static int readstr(char *const str, const size_t n, FILE *const f)
+{
+ int ret = -1;
+ size_t i = 0;
+
+ do
+ {
+ if (i >= n)
+ {
+ fprintf(stderr, "%s: string too long (> %zu bytes): %*.s\n",
+ __func__, n, (int)n, str);
+ goto end;
+ }
+ else if (!fread(&str[i], sizeof *str, 1, f))
+ {
+ fprintf(stderr, "%s: expected null-terminated string\n", __func__);
+ goto end;
+ }
+ } while (str[i++]);
+
+ ret = 0;
+
+end:
+ return ret;
+}
+
+static int get_file_size(size_t *const sz, FILE *const f)
+{
+ char szstr[sizeof "2147483647"];
+
+ if (readstr(szstr, sizeof szstr, f))
+ return -1;
+
+ errno = 0;
+ const unsigned long val = strtoul(szstr, NULL, 10);
+
+ if (errno)
+ {
+ fprintf(stderr, "%s: strtoul(3): %s\n", __func__, strerror(errno));
+ return -1;
+ }
+
+ *sz = val;
+ return 0;
+}
+
+static int read_file_contents(const struct container *const el, FILE *const f,
+ const long init_off, const size_t sz)
+{
+ int ret = -1;
+
+ switch (el->type)
+ {
+ case CONTAINER_TYPE_SPRITE:
+ if (sprite_from_fp(el->data.sprite, f))
+ goto end;
+
+ break;
+
+ case CONTAINER_TYPE_SOUND:
+ if (sfx_sound_from_fp(el->data.sound, f))
+ goto end;
+
+ break;
+ }
+
+ const long off = ftell(f);
+
+ if (off < 0)
+ {
+ fprintf(stderr, "%s:%d: fseek failed: %s\n",
+ __func__, __LINE__, strerror(errno));
+ goto end;
+ }
+ else if (off > init_off + sz)
+ {
+ fprintf(stderr, "%s: %s: detected read past file contents\n",
+ __func__, el->path);
+ goto end;
+ }
+ else if (off != init_off + sz)
+ {
+ fprintf(stderr, "only %ld bytes read, %ld expected\n",
+ off, init_off + sz);
+ goto end;
+ }
+
+ ret = 0;
+
+end:
+ return ret;
+}
+
+static const struct container *find_element(const struct container *const list,
+ const size_t n, FILE *const f)
+{
+ char name[128];
+
+ if (readstr(name, sizeof name, f))
+ return NULL;
+
+ for (size_t i = 0; i < n; i++)
+ {
+ const struct container *const el = &list[i];
+
+ if (!strcmp(el->path, name))
+ return el;
+ }
+
+ fprintf(stderr, "file %s not found on the list\n", name);
+ return NULL;
+}
+
+static int read_element(const struct container *const list, const size_t n,
+ FILE *const f, bool *const done)
+{
+ int ret = -1;
+ long init_off;
+ const struct container *el = NULL;
+ size_t sz;
+
+ if (!(el = find_element(list, n, f)))
+ goto end;
+ else if (get_file_size(&sz, f))
+ goto end;
+ else if ((init_off = ftell(f)) < 0)
+ {
+ fprintf(stderr, "%s:%d: fseek failed: %s\n",
+ __func__, __LINE__, strerror(errno));
+ goto end;
+ }
+ else if (read_file_contents(el, f, init_off, sz))
+ goto end;
+
+ done[el - list] = true;
+ ret = 0;
+
+end:
+ return ret;
+}
+
+static int read_all_elements(const struct container *const list, const size_t n,
+ FILE *const f)
+{
+ int ret = -1;
+ /* VLAs are generally frowned upon, but we can safely assume
+ * 'n' is reasonably low. */
+ bool done[n];
+
+ memset(&done, 0, sizeof done);
+
+ for (size_t i = 0; i < sizeof done / sizeof *done; i++)
+ if (feof(f) || ferror(f)
+ || read_element(list, n, f, done))
+ goto end;
+
+ for (size_t i = 0; i < sizeof done / sizeof *done; i++)
+ {
+ if (!done[i])
+ {
+ fprintf(stderr, "%s: %s not found inside container file\n",
+ __func__, list[i].path);
+ goto end;
+ }
+ }
+
+ ret = 0;
+
+end:
+ return ret;
+}
+
+void container_free(const struct container *const list, const size_t n)
+{
+ if (!list)
+ return;
+
+ for (size_t i = 0; i < n; i++)
+ {
+ const struct container *const el = &list[i];
+
+ switch (el->type)
+ {
+ case CONTAINER_TYPE_SPRITE:
+ sprite_free(el->data.sprite);
+ break;
+
+ case CONTAINER_TYPE_SOUND:
+ sfx_free(el->data.sound);
+ break;
+ }
+ }
+}
+
+int container_load_ex(const char *const path, const struct container *const list,
+ const size_t n)
+{
+ int ret = -1;
+ FILE *f = NULL;
+
+ if (!(f = fopen(path, "rb")))
+ {
+ fprintf(stderr, "could not open %s: %s\n", path, strerror(errno));
+ goto end;
+ }
+ else if (read_all_elements(list, n, f))
+ goto end;
+
+ ret = 0;
+
+end:
+ if (f)
+ fclose(f);
+
+ return ret;
+}
diff --git a/src/font/CMakeLists.txt b/src/font/CMakeLists.txt
new file mode 100644
index 0000000..46336ef
--- /dev/null
+++ b/src/font/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_library(font "src/font.c")
+target_include_directories(font PUBLIC "inc")
+target_link_libraries(font PUBLIC container PRIVATE gfx)
diff --git a/src/font/inc/font.h b/src/font/inc/font.h
new file mode 100644
index 0000000..2d8bd2f
--- /dev/null
+++ b/src/font/inc/font.h
@@ -0,0 +1,22 @@
+#ifndef FONT_H
+#define FONT_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+enum font
+{
+ FONT
+};
+
+int font_printf(enum font f, short x, short y, const char *fmt, ...);
+
+extern struct sprite font_sprite;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* FONT_H */
diff --git a/src/font/src/font.c b/src/font/src/font.c
new file mode 100644
index 0000000..a6a28d3
--- /dev/null
+++ b/src/font/src/font.c
@@ -0,0 +1,110 @@
+#include <font.h>
+#include <gfx.h>
+#include <stdarg.h>
+#include <stdio.h>
+
+struct sprite font_sprite;
+
+static int renderstr(const enum font f, const short x, short y, const char *str)
+{
+ static const struct cfg
+ {
+ const struct sprite *s;
+ short fw, fh, fs;
+ } cfgs[] =
+ {
+ [FONT] =
+ {
+ .s = &font_sprite,
+ .fw = 12,
+ .fh = 14,
+ .fs = 8
+ }
+ };
+
+ const struct cfg *const cfg = &cfgs[f];
+ char c;
+ short rx = x;
+
+ while ((c = *str++))
+ {
+ if (c == ' ')
+ {
+ rx += cfg->fs;
+ continue;
+ }
+ else if (c == '\n' || c == '\r')
+ {
+ rx = x;
+ y += cfg->fh;
+ continue;
+ }
+
+ struct sprite *const s = sprite_get();
+
+ if (!s || sprite_clone(cfg->s, s))
+ return -1;
+
+ s->w = cfg->fw;
+ s->h = cfg->fh;
+
+ /* Substract non-printable characters (NUL to SP). */
+ const char ch = c - '!';
+ const short u = (cfg->fw * ch) % cfg->s->w;
+ const short v = cfg->fh * ((cfg->fw * ch) / cfg->s->w);
+
+ s->u += u;
+ s->v += v;
+ s->x = rx;
+ s->y = y;
+ sprite_sort(s);
+ rx += cfg->fs;
+ }
+
+ return 0;
+}
+
+int font_printf(const enum font f, const short x, const short y,
+ const char *const fmt, ...)
+{
+ va_list ap, aq, at;
+
+ va_start(ap, fmt);
+ va_copy(aq, ap);
+ va_copy(at, ap);
+
+ /* Short string optimization. */
+ char defstr[64];
+ int sz = vsnprintf(defstr, sizeof defstr, fmt, ap);
+
+ if (sz < 0)
+ goto end;
+ else if (sz >= sizeof defstr)
+ {
+ va_list at;
+
+ sz = vsnprintf(NULL, 0, fmt, ap);
+
+ if (sz < 0)
+ goto end;
+
+ /* VLAs are generally frowned upon, but using the heap
+ * might not be a good idea for a video game like this. */
+ char str[sz + 1];
+
+ sz = vsnprintf(str, sizeof str, fmt, ap);
+ va_end(at);
+
+ if (sz < 0)
+ goto end;
+ }
+
+ if (renderstr(f, x, y, defstr))
+ return -1;
+
+end:
+ va_end(at);
+ va_end(aq);
+ va_end(ap);
+ return sz;
+}
diff --git a/src/game/CMakeLists.txt b/src/game/CMakeLists.txt
new file mode 100644
index 0000000..0c21b0c
--- /dev/null
+++ b/src/game/CMakeLists.txt
@@ -0,0 +1,15 @@
+add_library(game "src/game.c" "src/res.c")
+target_include_directories(game PUBLIC "inc" PRIVATE "privinc")
+target_link_libraries(game PRIVATE
+ building
+ container
+ font
+ gfx
+ gui
+ instance
+ pad
+ player
+ resource
+ system
+ terrain
+ unit)
diff --git a/src/game/inc/game.h b/src/game/inc/game.h
new file mode 100644
index 0000000..8275d54
--- /dev/null
+++ b/src/game/inc/game.h
@@ -0,0 +1,15 @@
+#ifndef GAME_H
+#define GAME_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+int game(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GAME_H */
diff --git a/src/game/privinc/game_private.h b/src/game/privinc/game_private.h
new file mode 100644
index 0000000..1b9449f
--- /dev/null
+++ b/src/game/privinc/game_private.h
@@ -0,0 +1,16 @@
+#ifndef GAME_PRIVATE_H
+#define GAME_PRIVATE_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+int game_resinit(void);
+void game_free(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GAME_PRIVATE_H */
diff --git a/src/game/src/game.c b/src/game/src/game.c
new file mode 100644
index 0000000..2f13a64
--- /dev/null
+++ b/src/game/src/game.c
@@ -0,0 +1,101 @@
+#include <game.h>
+#include <game_private.h>
+#include <gfx.h>
+#include <human_player.h>
+#include <player.h>
+#include <resource.h>
+#include <system.h>
+#include <terrain.h>
+#include <stddef.h>
+
+int game(void)
+{
+ int ret = -1;
+
+ if (game_resinit())
+ goto end;
+
+ enum {HUMAN_PLAYERS = 1, MAP_RESOURCES = 3};
+ struct human_player humans[HUMAN_PLAYERS];
+
+ for (size_t i = 0; i < sizeof humans / sizeof *humans; i++)
+ {
+ const struct human_player_cfg cfg =
+ {
+ .padn = i,
+ .pl =
+ {
+ .team = PLAYER_COLOR_BLUE,
+ .x = 80,
+ .y = 40
+ }
+ };
+
+ if (human_player_init(&cfg, &humans[i]))
+ goto end;
+ }
+
+ struct terrain_map map;
+ struct resource res[MAP_RESOURCES] = {0};
+
+ terrain_init(&map);
+#if 1
+ if (resource_create(&(const struct resource_cfg)
+ {
+ .type = RESOURCE_TYPE_GOLD,
+ .x = 50,
+ .y = 200
+ }, res, sizeof res / sizeof *res)
+ || resource_create(&(const struct resource_cfg)
+ {
+ .type = RESOURCE_TYPE_WOOD,
+ .x = 180,
+ .y = 200
+ }, res, sizeof res / sizeof *res)
+ || resource_create(&(const struct resource_cfg)
+ {
+ .type = RESOURCE_TYPE_WOOD,
+ .x = 240,
+ .y = 200
+ }, res, sizeof res / sizeof *res))
+ goto end;
+#endif
+ bool exit = false;
+
+ while (!exit)
+ {
+ system_loop();
+
+ for (size_t i = 0; i < sizeof humans / sizeof *humans; i++)
+ {
+ const struct human_player *const h = &humans[i];
+
+ if (h->pl.alive)
+ {
+ struct player_others o =
+ {
+ .res = res,
+ .n_res = sizeof res / sizeof *res
+ };
+
+ exit |= human_player_update(&humans[i], &o);
+
+ if (terrain_render(&map, &h->cam)
+ || human_player_render(h, &o)
+ || cursor_render(&h->cam.cursor))
+ goto end;
+
+ /* TODO: render AI players. */
+ }
+ }
+
+ instance_cyclic();
+ gfx_draw();
+ }
+
+ ret = 0;
+
+end:
+ game_free();
+ return ret;
+}
diff --git a/src/game/src/res.c b/src/game/src/res.c
new file mode 100644
index 0000000..39b3b5e
--- /dev/null
+++ b/src/game/src/res.c
@@ -0,0 +1,247 @@
+#include <game_private.h>
+#include <building.h>
+#include <container.h>
+#include <font.h>
+#include <gfx.h>
+#include <gui.h>
+#include <resource.h>
+#include <terrain.h>
+#include <unit.h>
+#include <stdbool.h>
+
+static const struct container c[] =
+{
+ {
+ .path = "barracks",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &building_sprites[BUILDING_TYPE_BARRACKS]
+ }
+ },
+
+ {
+ .path = "worker_n",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &unit_sprites[UNIT_SPRITE_N]
+ }
+ },
+
+ {
+ .path = "worker_ne",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &unit_sprites[UNIT_SPRITE_NE]
+ }
+ },
+
+ {
+ .path = "worker_e",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &unit_sprites[UNIT_SPRITE_E]
+ }
+ },
+
+ {
+ .path = "worker_se",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &unit_sprites[UNIT_SPRITE_SE]
+ }
+ },
+
+ {
+ .path = "worker_s",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &unit_sprites[UNIT_SPRITE_S]
+ }
+ },
+
+ {
+ .path = "grass",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &grass_sprite
+ }
+ },
+
+ {
+ .path = "cursor",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &cursor_sprite
+ }
+ },
+
+ {
+ .path = "gui_bar_left",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &gui_sprites[GUI_BAR_LEFT]
+ }
+ },
+
+ {
+ .path = "gui_bar_mid",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &gui_sprites[GUI_BAR_MID]
+ }
+ },
+
+ {
+ .path = "gui_bar_right",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &gui_sprites[GUI_BAR_RIGHT]
+ }
+ },
+
+ {
+ .path = "sel_up_left",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &gui_sprites[GUI_SELECTION_UP_LEFT]
+ }
+ },
+
+ {
+ .path = "sel_up_right",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &gui_sprites[GUI_SELECTION_UP_RIGHT]
+ }
+ },
+
+ {
+ .path = "sel_down_left",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &gui_sprites[GUI_SELECTION_DOWN_LEFT]
+ }
+ },
+
+ {
+ .path = "sel_down_right",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &gui_sprites[GUI_SELECTION_DOWN_RIGHT]
+ }
+ },
+
+ {
+ .path = "sel_mid",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &gui_sprites[GUI_SELECTION_MID]
+ }
+ },
+
+ {
+ .path = "sel_mid_v",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &gui_sprites[GUI_SELECTION_MID_VERT]
+ }
+ },
+
+ {
+ .path = "font",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &font_sprite
+ }
+ },
+
+ {
+ .path = "gold_mine",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &resource_sprites[RESOURCE_TYPE_GOLD]
+ }
+ },
+
+ {
+ .path = "tree",
+ .type = CONTAINER_TYPE_SPRITE,
+ .data =
+ {
+ .sprite = &resource_sprites[RESOURCE_TYPE_WOOD]
+ }
+ },
+
+ {
+ .path = "acknowledge_01",
+ .type = CONTAINER_TYPE_SOUND,
+ .data =
+ {
+ .sound = &unit_sounds[UNIT_SOUND_MOVE]
+ }
+ },
+
+ {
+ .path = "acknowledge_02",
+ .type = CONTAINER_TYPE_SOUND,
+ .data =
+ {
+ .sound = &unit_sounds[UNIT_SOUND_MOVE_2]
+ }
+ },
+
+ {
+ .path = "selected_01",
+ .type = CONTAINER_TYPE_SOUND,
+ .data =
+ {
+ .sound = &unit_sounds[UNIT_SOUND_SELECTED]
+ }
+ }
+};
+
+static bool init;
+
+void game_free(void)
+{
+ if (init)
+ {
+ container_free(c, sizeof c / sizeof *c);
+ init = false;
+ }
+}
+
+int game_resinit(void)
+{
+ if (!init)
+ {
+ if (container_load("rts.cnt", c, sizeof c / sizeof *c))
+ {
+ perror("container_load");
+ return -1;
+ }
+
+ init = true;
+ }
+
+ return 0;
+}
diff --git a/src/gfx/CMakeLists.txt b/src/gfx/CMakeLists.txt
new file mode 100644
index 0000000..c449be5
--- /dev/null
+++ b/src/gfx/CMakeLists.txt
@@ -0,0 +1,54 @@
+set(src "src/heap.c")
+set(inc "inc")
+set(privinc "privinc")
+set(deps util)
+
+if(PS1_BUILD)
+ set(src ${src}
+ "ps1/src/4line.c"
+ "ps1/src/env.c"
+ "ps1/src/heap.c"
+ "ps1/src/init.c"
+ "ps1/src/rect.c"
+ "ps1/src/sort.c"
+ "ps1/src/sprite.c"
+ "ps1/src/quad.c")
+ set(inc ${inc} "ps1/inc")
+ set(privinc ${privinc} "ps1/privinc")
+ set(privdeps ${privdeps} system)
+elseif(SDL1_2_BUILD)
+ set(inc ${inc} "sdl-1.2/inc")
+ set(privinc ${privinc} "sdl-1.2/privinc")
+ set(src ${src}
+ "sdl-1.2/src/env.c"
+ "sdl-1.2/src/heap.c"
+ "sdl-1.2/src/line.c"
+ "sdl-1.2/src/rect.c"
+ "sdl-1.2/src/sort.c"
+ "sdl-1.2/src/sprite.c"
+ "sdl-1.2/src/quad.c")
+ set(deps ${deps} SDL)
+endif()
+
+add_library(gfx ${src})
+target_include_directories(gfx PUBLIC ${inc})
+target_include_directories(gfx PRIVATE ${privinc})
+target_link_libraries(gfx PUBLIC ${deps} PRIVATE ${privdeps})
+
+if(PS1_BUILD)
+ set(modes VMODE_PAL VMODE_NTSC)
+
+ if(VIDEO_MODE)
+ if(NOT "${VIDEO_MODE}" IN_LIST modes)
+ message(FATAL_ERROR "Invalid video mode ${VIDEO_MODE}. Available options:\n"
+ "${modes}\n"
+ "Run CMake again using one of the available video modes e.g.: cmake .. -DVIDEO_MODE=VMODE_PAL")
+ endif()
+
+ target_compile_definitions(gfx PRIVATE VIDEO_MODE=${VIDEO_MODE})
+ else()
+ message(FATAL_ERROR "Please define video mode. Available options:\n"
+ "${modes}\n"
+ "Run CMake again using one of the available video modes e.g.: cmake .. -DVIDEO_MODE=VMODE_PAL")
+ endif()
+endif()
diff --git a/src/gfx/inc/gfx.h b/src/gfx/inc/gfx.h
new file mode 100644
index 0000000..0c30911
--- /dev/null
+++ b/src/gfx/inc/gfx.h
@@ -0,0 +1,37 @@
+#ifndef GFX_H
+#define GFX_H
+
+#include <gfx/port.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+int gfx_init(void);
+void gfx_draw(void);
+void gfx_sync(void);
+void sprite_sort(struct sprite *s);
+int sprite_clone(const struct sprite *src, struct sprite *dst);
+void quad_sort(struct quad *q);
+void rect_sort(struct rect *r);
+void stp_4line_sort(struct stp_4line *l);
+int sprite_from_fp(struct sprite *s, FILE *f);
+struct sprite *sprite_get(void);
+struct quad *quad_get(void);
+struct rect *rect_get(bool semitrans);
+struct stp_4line *stp_4line_get(void);
+int quad_from_sprite(const struct sprite *s, struct quad *q);
+bool gfx_inside_drawenv(short x, short y, short w, short h);
+void gfx_deinit(void);
+void sprite_free(struct sprite *src);
+
+extern int screen_w, screen_h;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GFX_H */
diff --git a/src/gfx/privinc/gfx_private.h b/src/gfx/privinc/gfx_private.h
new file mode 100644
index 0000000..0553362
--- /dev/null
+++ b/src/gfx/privinc/gfx_private.h
@@ -0,0 +1,22 @@
+#ifndef GFX_PRIVATE_H
+#define GFX_PRIVATE_H
+
+#include <gfx.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+void rect_init(struct rect *r);
+void semitrans_rect_init(struct rect *r);
+void stp_4line_init(struct stp_4line *l);
+void *gfx_port_heap(size_t *n);
+void gfx_reset_heap(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GFX_PRIVATE_H */
diff --git a/src/gfx/ps1/inc/gfx/port.h b/src/gfx/ps1/inc/gfx/port.h
new file mode 100644
index 0000000..dec00c3
--- /dev/null
+++ b/src/gfx/ps1/inc/gfx/port.h
@@ -0,0 +1,142 @@
+#ifndef GFX_PS1_H
+#define GFX_PS1_H
+
+#include <util.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+/* 0-3 Texture page X Base (N*64) (ie. in 64-halfword steps) ;GPUSTAT.0-3
+ 4 Texture page Y Base (N*256) (ie. 0 or 256) ;GPUSTAT.4
+ 5-6 Semi Transparency (0=B/2+F/2, 1=B+F, 2=B-F, 3=B+F/4) ;GPUSTAT.5-6
+ 7-8 Texture page colors (0=4bit, 1=8bit, 2=15bit, 3=Reserved);GPUSTAT.7-8
+ 9 Dither 24bit to 15bit (0=Off/strip LSBs, 1=Dither Enabled) ;GPUSTAT.9
+ 10 Drawing to display area (0=Prohibited, 1=Allowed) ;GPUSTAT.10
+ 11 Texture Disable (0=Normal, 1=Disable if GP1(09h).Bit0=1) ;GPUSTAT.15
+ (Above might be chipselect for (absent) second VRAM chip?)
+ 12 Textured Rectangle X-Flip (BIOS does set this bit on power-up...?)
+ 13 Textured Rectangle Y-Flip (BIOS does set it equal to GPUSTAT.13...?)
+ 14-23 Not used (should be 0)
+ 24-31 Command (E1h)*/
+union gfx_common
+{
+ struct
+ {
+ uint32_t tpagex :4;
+ uint32_t tpagey :1;
+ uint32_t stp :2;
+ uint32_t bpp :2;
+ uint32_t dither :1;
+ uint32_t draw_to_disp :1;
+ uint32_t disable :1;
+ uint32_t xflip :1;
+ uint32_t yflip :1;
+ uint32_t :10;
+ uint8_t cmd;
+ } f;
+
+ uint32_t mask;
+};
+
+union gfx_sznext
+{
+ struct
+ {
+ uint32_t next :24;
+ uint32_t sz :8;
+ } f;
+
+ uint32_t cmd_next;
+};
+
+struct sprite
+{
+ union gfx_sznext sznext;
+ union gfx_common common;
+ uint8_t r, g, b;
+ uint8_t cmd;
+ int16_t x, y;
+ uint8_t u, v;
+ uint16_t clutid;
+ uint16_t w, h;
+};
+
+struct quad
+{
+ union gfx_sznext sznext;
+ uint8_t r, g, b;
+ uint8_t cmd;
+ int16_t x0, y0;
+ uint8_t u0, v0;
+ uint16_t clutid;
+ int16_t x1, y1;
+ uint8_t u1, v1;
+
+ union
+ {
+ struct
+ {
+ /* 0-8 Same as GP0(E1h).Bit0-8. */
+ uint16_t lb :9;
+ uint16_t :2;
+ /* 11 Same as GP0(E1h).Bit11. */
+ uint16_t hb :1;
+ uint16_t :4;
+ } bit;
+
+ uint16_t mask;
+ } tpage;
+
+ int16_t x2, y2;
+ uint8_t u2, v2;
+ uint16_t :16;
+ int16_t x3, y3;
+ uint8_t u3, v3;
+ uint16_t :16;
+};
+
+struct rect
+{
+ union gfx_sznext sznext;
+ union gfx_common common;
+ uint8_t r, g, b;
+ uint8_t cmd;
+ int16_t x, y;
+ uint16_t w, h;
+};
+
+struct stp_4line
+{
+ union gfx_sznext sznext;
+ union gfx_common common;
+ uint8_t r, g, b;
+ uint8_t cmd;
+ int16_t x, y;
+
+ struct stp_4line_vtx
+ {
+ uint32_t r :8;
+ uint32_t g :8;
+ uint32_t b :8;
+ uint32_t :8;
+
+ int16_t x, y;
+ } vertices[4];
+
+ uint32_t end;
+};
+
+enum {DRAW_MODE = 0xE1};
+
+int sprite_from_file_ex(const char *path, struct sprite *s);
+
+#define sprite_from_file(path, s) sprite_from_file_ex("cdrom:\\"path";1", s)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GFX_PS1_H */
diff --git a/src/gfx/ps1/privinc/ps1/gfx_private.h b/src/gfx/ps1/privinc/ps1/gfx_private.h
new file mode 100644
index 0000000..fd5a790
--- /dev/null
+++ b/src/gfx/ps1/privinc/ps1/gfx_private.h
@@ -0,0 +1,25 @@
+#ifndef GFX_PS1_PRIVATE_H
+#define GFX_PS1_PRIVATE_H
+
+#include <gfx.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+enum
+{
+ SCREEN_W = 368,
+ SCREEN_H = 240
+};
+
+void gfx_swapheap(void);
+void gfx_initenvs(void);
+void gfx_swapbuffers(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GFX_PS1_PRIVATE_H */
diff --git a/src/gfx/ps1/src/4line.c b/src/gfx/ps1/src/4line.c
new file mode 100644
index 0000000..955277e
--- /dev/null
+++ b/src/gfx/ps1/src/4line.c
@@ -0,0 +1,14 @@
+#include <gfx.h>
+#include <psxgpu.h>
+#include <stdint.h>
+#include <string.h>
+
+void stp_4line_init(struct stp_4line *const l)
+{
+ memset(l, 0, sizeof *l);
+ l->sznext.f.sz = (sizeof *l - sizeof l->sznext) / sizeof (uint32_t);
+ l->common.f.cmd = DRAW_MODE;
+ l->cmd = 0x5A;
+ enum {TERMINATION_CODE = 0x55555555};
+ l->end = TERMINATION_CODE;
+}
diff --git a/src/gfx/ps1/src/env.c b/src/gfx/ps1/src/env.c
new file mode 100644
index 0000000..c12002b
--- /dev/null
+++ b/src/gfx/ps1/src/env.c
@@ -0,0 +1,42 @@
+#include <gfx.h>
+#include <gfx_private.h>
+#include <ps1/gfx_private.h>
+#include <psxgpu.h>
+#include <stdbool.h>
+
+static GsDispEnv dispenv;
+enum {ENV_Y = SCREEN_H + 16};
+
+static GsDrawEnv drawenv =
+{
+ .y = ENV_Y,
+ .w = SCREEN_W,
+ .h = SCREEN_H
+};
+
+int screen_w = SCREEN_W, screen_h = SCREEN_H;
+
+void gfx_swapbuffers(void)
+{
+ const short y = drawenv.y;
+
+ drawenv.y = dispenv.y;
+ dispenv.y = y;
+ GsSetDrawEnv(&drawenv);
+ GsSetDispEnv(&dispenv);
+}
+
+void gfx_initenvs(void)
+{
+ GsSetDrawEnv(&drawenv);
+ GsSetDispEnv(&dispenv);
+}
+
+bool gfx_inside_drawenv(const short x, const short y, const short w,
+ const short h)
+{
+ return (x + w >= 0)
+ && x < drawenv.w
+ && (y + h >= 0)
+ && y < drawenv.h;
+}
diff --git a/src/gfx/ps1/src/heap.c b/src/gfx/ps1/src/heap.c
new file mode 100644
index 0000000..b67ee3e
--- /dev/null
+++ b/src/gfx/ps1/src/heap.c
@@ -0,0 +1,19 @@
+#include <gfx.h>
+#include <gfx_private.h>
+#include <stddef.h>
+
+static unsigned int sel;
+
+void gfx_swapheap(void)
+{
+ sel ^= 1;
+}
+
+void *gfx_port_heap(size_t *const n)
+{
+ enum {HEAP_SZ = 5120, N_HEAPS = 2};
+ static char heaps[N_HEAPS][HEAP_SZ];
+
+ *n = sizeof *heaps / sizeof **heaps;
+ return heaps[sel];
+}
diff --git a/src/gfx/ps1/src/init.c b/src/gfx/ps1/src/init.c
new file mode 100644
index 0000000..15ecd3e
--- /dev/null
+++ b/src/gfx/ps1/src/init.c
@@ -0,0 +1,16 @@
+#include <gfx_private.h>
+#include <ps1/gfx_private.h>
+#include <psxgpu.h>
+
+void gfx_deinit(void)
+{
+}
+
+int gfx_init(void)
+{
+ GsInit();
+ GsClearMem();
+ GsSetVideoMode(SCREEN_W, SCREEN_H, VIDEO_MODE);
+ gfx_initenvs();
+ return 0;
+}
diff --git a/src/gfx/ps1/src/quad.c b/src/gfx/ps1/src/quad.c
new file mode 100644
index 0000000..649426f
--- /dev/null
+++ b/src/gfx/ps1/src/quad.c
@@ -0,0 +1,23 @@
+#include <gfx.h>
+#include <gfx/port.h>
+#include <stdint.h>
+#include <string.h>
+
+static void quad_init(struct quad *const q)
+{
+ memset(q, 0, sizeof *q);
+ q->sznext.f.sz = (sizeof *q - sizeof q->sznext) / sizeof (uint32_t);
+ q->cmd = 0x2d;
+}
+
+int quad_from_sprite(const struct sprite *const s, struct quad *const q)
+{
+ quad_init(q);
+ q->tpage.mask = s->common.mask;
+ q->clutid = s->clutid;
+ q->u0 = q->u2 = s->u;
+ q->v0 = q->v1 = s->v;
+ q->u1 = q->u3 = s->u + s->w - 1;
+ q->v2 = q->v3 = s->v + s->h - 1;
+ return 0;
+}
diff --git a/src/gfx/ps1/src/rect.c b/src/gfx/ps1/src/rect.c
new file mode 100644
index 0000000..7e593b7
--- /dev/null
+++ b/src/gfx/ps1/src/rect.c
@@ -0,0 +1,23 @@
+#include <gfx.h>
+#include <psxgpu.h>
+#include <stdint.h>
+#include <string.h>
+
+static void common_init(struct rect *const r)
+{
+ memset(r, 0, sizeof *r);
+ r->sznext.f.sz = (sizeof *r - sizeof r->sznext) / sizeof (uint32_t);
+ r->common.f.cmd = DRAW_MODE;
+}
+
+void rect_init(struct rect *const r)
+{
+ common_init(r);
+ r->cmd = 0x60;
+}
+
+void semitrans_rect_init(struct rect *const r)
+{
+ common_init(r);
+ r->cmd = 0x62;
+}
diff --git a/src/gfx/ps1/src/sort.c b/src/gfx/ps1/src/sort.c
new file mode 100644
index 0000000..5cc14aa
--- /dev/null
+++ b/src/gfx/ps1/src/sort.c
@@ -0,0 +1,75 @@
+#include <gfx.h>
+#include <gfx_private.h>
+#include <ps1/gfx_private.h>
+#include <system/port.h>
+#include <psxgpu.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+static union gfx_sznext *first, *last;
+
+static void add_to_list(union gfx_sznext *const p)
+{
+ if (!first)
+ first = p;
+ else if (last)
+ last->f.next = (uint32_t)p;
+
+ last = p;
+}
+
+void sprite_sort(struct sprite *const s)
+{
+ add_to_list(&s->sznext);
+}
+
+void quad_sort(struct quad *const q)
+{
+ add_to_list(&q->sznext);
+}
+
+void rect_sort(struct rect *const r)
+{
+ add_to_list(&r->sznext);
+}
+
+void stp_4line_sort(struct stp_4line *const l)
+{
+ add_to_list(&l->sznext);
+}
+
+void gfx_draw(void)
+{
+ static union gfx_sznext term = {.cmd_next = 0xffffff};
+
+ add_to_list(&term);
+
+ void gpu_ctrl(unsigned int command, unsigned int param);
+
+ gfx_sync();
+ gfx_swapbuffers();
+ gfx_swapheap();
+ gpu_ctrl(4, 2);
+ D2_MADR = (uint32_t)first;
+ D2_BCR = 0;
+ D2_CHCR = (1 << 0xa) | 1 | (1 << 0x18);
+ first = NULL;
+ gfx_reset_heap();
+}
+
+void gfx_sync(void)
+{
+ /* Wait for the GPU to finish drawing primitives. */
+ while (!(GPU_CONTROL_PORT & (1 << 0x1a)))
+ ;
+
+ /* Wait for the GPU to be free. */
+ while (!(GPU_CONTROL_PORT & (1 << 0x1c)))
+ ;
+
+ while (!vblank_set)
+ ;
+
+ vblank_set = false;
+}
diff --git a/src/gfx/ps1/src/sprite.c b/src/gfx/ps1/src/sprite.c
new file mode 100644
index 0000000..3e35a70
--- /dev/null
+++ b/src/gfx/ps1/src/sprite.c
@@ -0,0 +1,244 @@
+#include <gfx.h>
+#include <psxgpu.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+
+void sprite_free(struct sprite *const src)
+{
+}
+
+int sprite_clone(const struct sprite *const src, struct sprite *const dst)
+{
+ *dst = *src;
+ return 0;
+}
+
+static void sprite_init(struct sprite *const s)
+{
+ memset(s, 0, sizeof *s);
+ s->sznext.f.sz = (sizeof *s - sizeof s->sznext) / sizeof (uint32_t);
+ s->common.f.cmd = DRAW_MODE;
+ s->cmd = 0x65;
+}
+
+struct tim_pos
+{
+ uint16_t x, y, w, h;
+};
+
+static void transfer_init(const struct tim_pos *const p)
+{
+ while (!(GPU_CONTROL_PORT & (1 << 0x1c)))
+ ;
+
+ GPU_CONTROL_PORT = 0x04000000;
+ GPU_DATA_PORT = 0x01000000;
+ GPU_DATA_PORT = 0xE6000000;
+ GPU_DATA_PORT = 0xA0000000;
+ GPU_DATA_PORT = (p->y << 16) | p->x;
+ GPU_DATA_PORT = (p->h << 16) | p->w;
+}
+
+static int transfer(const size_t sz, FILE *const f)
+{
+ const size_t rem = sz % sizeof (uint32_t);
+
+ if (sz >= sizeof (uint32_t))
+ {
+ for (size_t i = 0; i < sz / sizeof (uint32_t); i++)
+ {
+ uint32_t pix;
+
+ if (!fread(&pix, sizeof pix, 1, f))
+ {
+ fprintf(stderr, "could not read CLUT word %zu/%zu\n",
+ i, sz / sizeof pix);
+ return -1;
+ }
+
+ GPU_DATA_PORT = pix;
+ }
+ }
+
+ if (rem)
+ {
+ uint32_t pix = 0;
+
+ if (!fread(&pix, rem, 1, f))
+ {
+ fprintf(stderr, "failed reading remaining %zu bytes\n", rem);
+ return -1;
+ }
+
+ GPU_DATA_PORT = pix;
+ }
+
+ return 0;
+}
+
+struct header
+{
+ uint32_t sz;
+ struct tim_pos pos;
+};
+
+static int upload_clut(struct sprite *const s, FILE *const f)
+{
+ struct header clut;
+
+ if (!fread(&clut, sizeof clut, 1, f))
+ {
+ fprintf(stderr, "no CLUT pos data found\n");
+ return -1;
+ }
+
+ transfer_init(&clut.pos);
+
+ const size_t sz = clut.sz - sizeof clut;
+
+ if (transfer(sz, f))
+ return -1;
+
+ s->clutid = get_clutid(clut.pos.x, clut.pos.y);
+ return 0;
+}
+
+enum bpp
+{
+ BPP_4 = 0,
+ BPP_8 = 1,
+ BPP_16 = 2,
+ BPP_24 = 4
+};
+
+static int upload_img(struct sprite *const s, const enum bpp bpp, FILE *const f)
+{
+ struct header img;
+
+ if (!fread(&img, sizeof img, 1, f))
+ {
+ fprintf(stderr, "could not extract image header\n");
+ return -1;
+ }
+
+ transfer_init(&img.pos);
+
+ const size_t sz = img.sz - sizeof img;
+
+ if (transfer(sz, f))
+ return -1;
+
+ enum
+ {
+ VRAM_X = 1024,
+ VRAM_Y = 512,
+ TPAGE_WIDTH = 64
+ };
+
+ s->common.f.tpagex = img.pos.x / TPAGE_WIDTH;
+ s->common.f.tpagey = img.pos.y / (VRAM_Y / 2);
+ s->u = img.pos.x % TPAGE_WIDTH;
+ s->v = img.pos.y % (VRAM_Y / 2);
+
+ switch (bpp)
+ {
+ case BPP_4:
+ s->w = img.pos.w * 4;
+ s->u <<= 2;
+ break;
+
+ case BPP_8:
+ s->w = img.pos.w * 2;
+ s->u <<= 1;
+ break;
+
+ case BPP_16:
+ s->w = img.pos.w;
+ break;
+
+ case BPP_24:
+ s->w = img.pos.w + (img.pos.w / 2);
+ break;
+ }
+
+ s->h = img.pos.h;
+ return 0;
+}
+
+int sprite_from_fp(struct sprite *const s, FILE *const f)
+{
+ int ret = -1;
+
+ sprite_init(s);
+
+ struct
+ {
+ uint32_t version;
+ enum bpp bpp :3;
+ bool has_clut :1;
+ uint32_t :28;
+ } h;
+
+ enum {VERSION_ID = 0x10};
+
+ if (!fread(&h, sizeof h, 1, f))
+ {
+ fprintf(stderr, "TIM header not found\n");
+ goto end;
+ }
+ else if (h.version != VERSION_ID)
+ {
+ fprintf(stderr, "%s: invalid TIM header %#" PRIx32 "\n", h.version);
+ goto end;
+ }
+ else if (h.bpp == BPP_24)
+ {
+ fprintf(stderr, "24-bit mode unsupported\n");
+ goto end;
+ }
+ else if ((h.has_clut && upload_clut(s, f))
+ || upload_img(s, h.bpp, f))
+ goto end;
+
+ s->common.f.bpp = h.bpp ? __builtin_ctz(h.bpp) + 1 : 0;
+ ret = 0;
+
+end:
+ return ret;
+}
+
+int sprite_from_file_ex(const char *const path, struct sprite *const s)
+{
+ int ret = -1;
+ FILE *f = NULL;
+
+ if (!path)
+ {
+ errno = EINVAL;
+ goto end;
+ }
+
+ f = fopen(path, "rb");
+
+ if (!f)
+ {
+ fprintf(stderr, "could not open %s: %s\n", path, strerror(errno));
+ goto end;
+ }
+ else if (sprite_from_fp(s, f))
+ {
+ fprintf(stderr, "%s: sprite_from_fp failed\n", path);
+ goto end;
+ }
+
+end:
+
+ if (f)
+ fclose(f);
+
+ return ret;
+}
diff --git a/src/gfx/sdl-1.2/inc/gfx/port.h b/src/gfx/sdl-1.2/inc/gfx/port.h
new file mode 100644
index 0000000..ec1093e
--- /dev/null
+++ b/src/gfx/sdl-1.2/inc/gfx/port.h
@@ -0,0 +1,52 @@
+#ifndef GFX_SDL_H
+#define GFX_SDL_H
+
+#include <SDL/SDL.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+struct sprite
+{
+ SDL_Surface *s;
+ short x, y, w, h;
+ unsigned char u, v;
+};
+
+struct quad
+{
+ unsigned char r, g, b;
+ short x0, x1, x2, x3;
+ short y0, y1, y2, y3;
+ unsigned char u0, u1, u2, u3;
+ unsigned char v0, v1, v2, v3;
+ SDL_Surface *s;
+};
+
+struct rect
+{
+ unsigned char r, g, b;
+ short x, y, w, h;
+};
+
+struct stp_4line
+{
+ short x, y;
+ unsigned char r, g, b;
+
+ struct stp_4line_vtx
+ {
+ unsigned char r, g, b;
+ short x, y;
+ } vertices[4];
+};
+
+SDL_Surface *gfx_port_screen(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GFX_SDL_H */
diff --git a/src/gfx/sdl-1.2/privinc/sdl-1.2/gfx_private.h b/src/gfx/sdl-1.2/privinc/sdl-1.2/gfx_private.h
new file mode 100644
index 0000000..c067af7
--- /dev/null
+++ b/src/gfx/sdl-1.2/privinc/sdl-1.2/gfx_private.h
@@ -0,0 +1,19 @@
+#ifndef GFX_SDL_12_PRIVATE_H
+#define GFX_SDL_12_PRIVATE_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+enum
+{
+ SCREEN_W = 320,
+ SCREEN_H = 240
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GFX_SDL_12_PRIVATE_H */
diff --git a/src/gfx/sdl-1.2/src/env.c b/src/gfx/sdl-1.2/src/env.c
new file mode 100644
index 0000000..6589d84
--- /dev/null
+++ b/src/gfx/sdl-1.2/src/env.c
@@ -0,0 +1,82 @@
+#include <gfx.h>
+#include <sdl-1.2/gfx_private.h>
+#include <SDL/SDL.h>
+#include <stdbool.h>
+
+int screen_w = SCREEN_W, screen_h = SCREEN_H;
+static bool fullscreen;
+static SDL_Surface *screen;
+
+void gfx_deinit(void)
+{
+ if (screen)
+ SDL_FreeSurface(screen);
+
+ SDL_QuitSubSystem(SDL_INIT_VIDEO);
+}
+
+static int resize_screen(const int w, const int h, const bool full_screen)
+{
+ Uint32 flags = SDL_HWSURFACE | SDL_RESIZABLE | SDL_DOUBLEBUF;
+
+ const SDL_VideoInfo *const info = SDL_GetVideoInfo();
+
+ if (!info)
+ {
+ fprintf(stderr, "SDL_GetVideoInfo: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ if (fullscreen)
+ flags |= SDL_FULLSCREEN;
+
+#if 0
+ SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
+ SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 5);
+ SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
+ SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
+ SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
+#endif
+
+ const int bpp = info->vfmt->BitsPerPixel;
+
+ if (screen)
+ SDL_FreeSurface(screen);
+
+ if (!(screen = SDL_SetVideoMode(screen_w, screen_h, bpp, flags)))
+ {
+ fprintf(stderr, "SDL_SetVideoMode: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ screen_w = w;
+ screen_h = h;
+ fullscreen = full_screen;
+ return 0;
+}
+
+int gfx_init(void)
+{
+ if (SDL_InitSubSystem(SDL_INIT_VIDEO))
+ {
+ fprintf(stderr, "%s: SDL_InitSubSystem: %s\n",
+ __func__, SDL_GetError());
+ return -1;
+ }
+
+ return resize_screen(screen_w, screen_h, fullscreen);
+}
+
+bool gfx_inside_drawenv(const short x, const short y, const short w,
+ const short h)
+{
+ return (x + w >= 0)
+ && x < screen_w
+ && (y + h >= 0)
+ && y < screen_h;
+}
+
+SDL_Surface *gfx_port_screen(void)
+{
+ return screen;
+}
diff --git a/src/gfx/sdl-1.2/src/heap.c b/src/gfx/sdl-1.2/src/heap.c
new file mode 100644
index 0000000..b84760a
--- /dev/null
+++ b/src/gfx/sdl-1.2/src/heap.c
@@ -0,0 +1,11 @@
+#include <gfx.h>
+#include <gfx_private.h>
+#include <stddef.h>
+
+void *gfx_port_heap(size_t *const n)
+{
+ static char heap[5120];
+
+ *n = sizeof heap / sizeof *heap;
+ return heap;
+}
diff --git a/src/gfx/sdl-1.2/src/line.c b/src/gfx/sdl-1.2/src/line.c
new file mode 100644
index 0000000..aeeda7b
--- /dev/null
+++ b/src/gfx/sdl-1.2/src/line.c
@@ -0,0 +1,15 @@
+#include <gfx.h>
+#include <gfx/port.h>
+#include <stddef.h>
+
+void stp_4line_init(struct stp_4line *const l)
+{
+}
+
+void semitrans_stp_4line_init(struct stp_4line *r)
+{
+}
+
+void stp_4line_sort(struct stp_4line *const r)
+{
+}
diff --git a/src/gfx/sdl-1.2/src/quad.c b/src/gfx/sdl-1.2/src/quad.c
new file mode 100644
index 0000000..fe1b39f
--- /dev/null
+++ b/src/gfx/sdl-1.2/src/quad.c
@@ -0,0 +1,18 @@
+#include <gfx.h>
+#include <gfx/port.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+int quad_from_sprite(const struct sprite *const s, struct quad *const q)
+{
+ q->s = s->s;
+ q->u0 = q->u2 = s->u;
+ q->v0 = q->v1 = s->v;
+ q->u1 = q->u3 = s->u + s->w - 1;
+ q->v2 = q->v3 = s->v + s->h - 1;
+ return 0;
+}
+
+void quad_sort(struct quad *const q)
+{
+}
diff --git a/src/gfx/sdl-1.2/src/rect.c b/src/gfx/sdl-1.2/src/rect.c
new file mode 100644
index 0000000..ee50378
--- /dev/null
+++ b/src/gfx/sdl-1.2/src/rect.c
@@ -0,0 +1,33 @@
+#include <gfx.h>
+#include <gfx/port.h>
+#include <SDL/SDL.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+void rect_sort(struct rect *const r)
+{
+ SDL_Rect rct =
+ {
+ .x = r->x,
+ .y = r->y,
+ .w = r->w,
+ .h = r->h
+ };
+
+ SDL_Surface *const screen = gfx_port_screen();
+ const Uint32 map = SDL_MapRGB(screen->format, r->r, r->g, r->b);
+
+ if (SDL_FillRect(screen, &rct, map))
+ {
+ fprintf(stderr, "SDL_FillRect: %s\n", SDL_GetError());
+ return;
+ }
+}
+
+void semitrans_rect_init(struct rect *const r)
+{
+}
+
+void rect_init(struct rect *const r)
+{
+}
diff --git a/src/gfx/sdl-1.2/src/sort.c b/src/gfx/sdl-1.2/src/sort.c
new file mode 100644
index 0000000..2cc4e75
--- /dev/null
+++ b/src/gfx/sdl-1.2/src/sort.c
@@ -0,0 +1,22 @@
+#include <gfx.h>
+#include <gfx_private.h>
+#include <gfx/port.h>
+#include <SDL/SDL.h>
+#include <stdio.h>
+
+void gfx_draw(void)
+{
+ enum {FPS = 50, REFRESH_MS = 1000 / FPS};
+ static Uint32 prev;
+ const Uint32 cur = SDL_GetTicks();
+
+ if (cur - prev < REFRESH_MS)
+ SDL_Delay(REFRESH_MS - (cur - prev));
+
+ prev = SDL_GetTicks();
+
+ if (SDL_Flip(gfx_port_screen()))
+ fprintf(stderr, "SDL_Flip: %s\n", SDL_GetError());
+
+ gfx_reset_heap();
+}
diff --git a/src/gfx/sdl-1.2/src/sprite.c b/src/gfx/sdl-1.2/src/sprite.c
new file mode 100644
index 0000000..9d65c0d
--- /dev/null
+++ b/src/gfx/sdl-1.2/src/sprite.c
@@ -0,0 +1,75 @@
+#include <gfx.h>
+#include <gfx/port.h>
+#include <SDL/SDL.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+void sprite_free(struct sprite *const s)
+{
+ if (s && s->s)
+ SDL_FreeSurface(s->s);
+}
+
+int sprite_clone(const struct sprite *const src, struct sprite *const dst)
+{
+ *dst = *src;
+ return 0;
+}
+
+int sprite_from_fp(struct sprite *const s, FILE *const f)
+{
+ int ret = -1;
+ SDL_RWops *ops = NULL;
+ SDL_Surface *ts = NULL;
+
+ if (!(ops = SDL_RWFromFP(f, 0)))
+ {
+ fprintf(stderr, "SDL_RWFromFP: %s\n", SDL_GetError());
+ goto end;
+ }
+ if (!(ts = SDL_LoadBMP_RW(ops, 0)))
+ {
+ fprintf(stderr, "SDL_LoadBMP_RW: %s\n", SDL_GetError());
+ goto end;
+ }
+ /* Magenta as transparent. */
+ else if (SDL_SetColorKey(ts, SDL_SRCCOLORKEY, SDL_MapRGB(ts->format, 255, 0, 255)))
+ {
+ fprintf(stderr, "SDL_SetColorKey: %s\n", SDL_GetError());
+ goto end;
+ }
+ else if (!(s->s = SDL_DisplayFormat(ts)))
+ {
+ fprintf(stderr, "SDL_DisplayFormat: %s\n", SDL_GetError());
+ goto end;
+ }
+
+ s->w = ts->w;
+ s->h = ts->h;
+ ret = 0;
+
+end:
+ SDL_FreeRW(ops);
+ SDL_FreeSurface(ts);
+ return ret;
+}
+
+void sprite_sort(struct sprite *const s)
+{
+ SDL_Rect r =
+ {
+ .x = s->x,
+ .y = s->y
+ };
+
+ SDL_Rect clip =
+ {
+ .x = s->u,
+ .y = s->v,
+ .w = s->w,
+ .h = s->h
+ };
+
+ if (SDL_BlitSurface(s->s, &clip, gfx_port_screen(), &r))
+ fprintf(stderr, "SDL_BlitSurface: %s\n", SDL_GetError());
+}
diff --git a/src/gfx/src/heap.c b/src/gfx/src/heap.c
new file mode 100644
index 0000000..40a856c
--- /dev/null
+++ b/src/gfx/src/heap.c
@@ -0,0 +1,56 @@
+#include <gfx.h>
+#include <gfx_private.h>
+#include <stddef.h>
+
+static size_t heap_i;
+
+void gfx_reset_heap(void)
+{
+ heap_i = 0;
+}
+
+static void *get_element(const size_t sz)
+{
+ size_t n;
+ char *ret = gfx_port_heap(&n);
+
+ if (ret && heap_i + sz < n)
+ {
+ ret += heap_i;
+ heap_i += sz;
+ }
+ else
+ return NULL;
+
+ return ret;
+}
+
+struct sprite *sprite_get(void)
+{
+ return get_element(sizeof (struct sprite));
+}
+
+struct quad *quad_get(void)
+{
+ return get_element(sizeof (struct quad));
+}
+
+struct rect *rect_get(const bool semitrans)
+{
+ struct rect *const r = get_element(sizeof (struct rect));
+
+ if (r)
+ semitrans ? semitrans_rect_init(r) : rect_init(r);
+
+ return r;
+}
+
+struct stp_4line *stp_4line_get(void)
+{
+ struct stp_4line *const l = get_element(sizeof (struct stp_4line));
+
+ if (l)
+ stp_4line_init(l);
+
+ return l;
+}
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
new file mode 100644
index 0000000..59ee94d
--- /dev/null
+++ b/src/gui/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_library(gui "src/gui.c")
+target_include_directories(gui PUBLIC "inc")
+target_link_libraries(gui PUBLIC gfx player PRIVATE font unit building)
diff --git a/src/gui/inc/gui.h b/src/gui/inc/gui.h
new file mode 100644
index 0000000..254fd47
--- /dev/null
+++ b/src/gui/inc/gui.h
@@ -0,0 +1,36 @@
+#ifndef GUI_H
+#define GUI_H
+
+#include <gfx.h>
+#include <human_player.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+enum
+{
+ GUI_BAR_LEFT,
+ GUI_BAR_MID,
+ GUI_BAR_RIGHT,
+ GUI_SELECTION_UP_LEFT,
+ GUI_SELECTION_UP_RIGHT,
+ GUI_SELECTION_MID_VERT,
+ GUI_SELECTION_DOWN_LEFT,
+ GUI_SELECTION_DOWN_RIGHT,
+ GUI_SELECTION_MID,
+
+ MAX_GUI_SPRITES
+};
+
+void gui_update(struct human_player *h);
+int gui_render(const struct human_player *h);
+
+extern struct sprite gui_sprites[MAX_GUI_SPRITES];
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GUI_H */
diff --git a/src/gui/src/gui.c b/src/gui/src/gui.c
new file mode 100644
index 0000000..2a5e53b
--- /dev/null
+++ b/src/gui/src/gui.c
@@ -0,0 +1,473 @@
+#include <gui.h>
+#include <building.h>
+#include <font.h>
+#include <gfx.h>
+#include <human_player.h>
+#include <player.h>
+#include <unit.h>
+#include <limits.h>
+#include <stddef.h>
+
+struct sprite gui_sprites[MAX_GUI_SPRITES];
+
+static int render_topleft(void)
+{
+ struct sprite *const s = sprite_get();
+
+ if (!s
+ || sprite_clone(&gui_sprites[GUI_BAR_LEFT], s))
+ return -1;
+
+ sprite_sort(s);
+ return 0;
+}
+
+static int render_topright(void)
+{
+ struct sprite *const s = sprite_get();
+
+ if (!s || sprite_clone(&gui_sprites[GUI_BAR_RIGHT], s))
+ return -1;
+
+ s->x = screen_w - s->w;
+ sprite_sort(s);
+ return 0;
+}
+
+static int render_topmid(void)
+{
+ const uint16_t mid_w = gui_sprites[GUI_BAR_MID].w;
+ const uint16_t lw = gui_sprites[GUI_BAR_LEFT].w;
+ const size_t w = screen_w - lw - lw;
+ const size_t rem_mid = w % mid_w;
+ const size_t whole_mid = w / mid_w;
+ const size_t n_mid = rem_mid ? whole_mid + 1 : whole_mid;
+
+ for (struct
+ {
+ size_t i;
+ short x;
+ } a = {.i = 0, .x = lw};
+ a.i < n_mid;
+ a.i++, a.x += mid_w)
+ {
+ struct sprite *const m = sprite_get();
+
+ if (!m
+ || sprite_clone(&gui_sprites[GUI_BAR_MID], m))
+ return -1;
+
+ m->x = a.x;
+
+ if (rem_mid && a.i + 1 == n_mid)
+ m->w = rem_mid;
+ else
+ m->w = mid_w;
+
+ sprite_sort(m);
+ }
+
+ return 0;
+}
+
+static int render_top(void)
+{
+ if (render_topleft()
+ || render_topright()
+ || render_topmid())
+ return -1;
+
+ return 0;
+}
+
+static int render_seltop(struct sprite *const up)
+{
+ enum {OFFSET = 48};
+
+ if (sprite_clone(&gui_sprites[GUI_SELECTION_UP_LEFT], up))
+ return -1;
+
+ up->y = screen_h - up->h - OFFSET;
+ up->x = 16;
+ sprite_sort(up);
+
+ struct sprite *const right = sprite_get();
+
+ if (!right
+ || sprite_clone(&gui_sprites[GUI_SELECTION_UP_RIGHT], right))
+ return -1;
+
+ right->x = screen_w - right->w - up->x;
+ right->y = up->y;
+ sprite_sort(right);
+ return 0;
+}
+
+static int render_selvert(const struct sprite *const up, const short x)
+{
+ const uint16_t vert_h = gui_sprites[GUI_SELECTION_MID_VERT].h;
+ const size_t h = screen_h - up->y - up->h - up->h;
+ const size_t rem_vert = h % vert_h;
+ const size_t whole_vert = h / vert_h;
+ const size_t n_vert = rem_vert ? whole_vert + 1 : whole_vert;
+
+ for (struct
+ {
+ size_t i;
+ short y;
+ } a = {.i = 0, .y = up->y + up->h};
+ a.i < n_vert;
+ a.i++, a.y += vert_h)
+ {
+ struct sprite *const v = sprite_get();
+
+ if (!v)
+ return -1;
+
+ *v = gui_sprites[GUI_SELECTION_MID_VERT];
+ v->y = a.y;
+ v->x = x;
+
+ if (rem_vert && a.i + 1 == n_vert)
+ v->h = rem_vert;
+ else
+ v->h = vert_h;
+
+ sprite_sort(v);
+ }
+
+ return 0;
+}
+
+static int render_selvertleft(const struct sprite *const up)
+{
+ return render_selvert(up, up->x);
+}
+
+static int render_selvertright(const struct sprite *const up)
+{
+ return render_selvert(up, screen_w - gui_sprites[GUI_SELECTION_MID_VERT].w - up->x);
+}
+
+static int render_seldown(const struct sprite *const up)
+{
+ {
+ struct sprite *const left = sprite_get();
+
+ if (!left)
+ return -1;
+
+ *left = gui_sprites[GUI_SELECTION_DOWN_LEFT];
+ left->x = up->x;
+ left->y = screen_h - left->h;
+ sprite_sort(left);
+ }
+
+ {
+ struct sprite *const right = sprite_get();
+
+ if (!right)
+ return -1;
+
+ *right = gui_sprites[GUI_SELECTION_DOWN_RIGHT];
+ right->x = screen_w - right->w - up->x;
+ right->y = screen_h - right->h;
+ sprite_sort(right);
+ }
+
+ return 0;
+}
+
+static int render_selrect(const struct sprite *const up)
+{
+ struct rect *const sel = rect_get(true);
+
+ if (!sel)
+ return -1;
+
+ const struct sprite *const mid = &gui_sprites[GUI_SELECTION_MID],
+ *const vert = &gui_sprites[GUI_SELECTION_MID_VERT];
+ sel->x = up->x + vert->w;
+ sel->y = up->y + mid->h;
+ sel->w = screen_w - sel->x - vert->w - up->x;
+ sel->h = screen_h - sel->y - mid->h;
+ sel->r = 72;
+ sel->g = 66;
+ sel->b = 56;
+ rect_sort(sel);
+ return 0;
+}
+
+static int render_selmid(const struct sprite *const up, const short y)
+{
+ const uint16_t mid_w = gui_sprites[GUI_SELECTION_MID].w;
+ const size_t w = screen_w - up->x - up->x - up->w - up->w;
+ const size_t rem_mid = w % mid_w;
+ const size_t whole_mid = w / mid_w;
+ const size_t n_mid = rem_mid ? whole_mid + 1 : whole_mid;
+
+ for (struct
+ {
+ size_t i;
+ short x;
+ } a = {.i = 0, .x = up->x + up->w};
+ a.i < n_mid;
+ a.i++, a.x += mid_w)
+ {
+ struct sprite *const m = sprite_get();
+
+ if (!m)
+ return -1;
+
+ *m = gui_sprites[GUI_SELECTION_MID];
+ m->x = a.x;
+ m->y = y;
+
+ if (rem_mid && a.i + 1 == n_mid)
+ m->w = rem_mid;
+ else
+ m->w = mid_w;
+
+ sprite_sort(m);
+ }
+
+ return 0;
+}
+
+static int render_selmidtop(const struct sprite *const up)
+{
+ return render_selmid(up, up->y);
+}
+
+static int render_selmiddown(const struct sprite *const up)
+{
+ return render_selmid(up, screen_h - gui_sprites[GUI_SELECTION_MID].h);
+}
+
+static int render_topmenu(const struct human_player *const h)
+{
+ const struct player *const pl = &h->pl;
+
+ if (render_top()
+ || (font_printf(FONT, 16, 6, "Wood=%u",
+ h->gui_res[RESOURCE_TYPE_WOOD]) < 0)
+ || (font_printf(FONT, 108, 6, "Gold=%u",
+ h->gui_res[RESOURCE_TYPE_GOLD]) < 0)
+ || (font_printf(FONT, 212, 6, "Pop.=%u/%zu",
+ pl->pop, sizeof pl->units / sizeof *pl->units) < 0))
+ return -1;
+
+ return 0;
+}
+
+static int draw_hp(const struct instance *const i, const short x, const short y,
+ const instance_hp max_hp)
+{
+ enum {OFFSET = 4, WIDTH = 64, HEIGHT = 4};
+ const short ry = y - OFFSET - HEIGHT;
+
+ if (ry < 0)
+ return 0;
+
+ const short gw = (WIDTH * i->hp) / max_hp;
+
+ {
+ struct rect *const gr = rect_get(true);
+
+ if (!gr)
+ return -1;
+
+ gr->x = x;
+ gr->y = ry;
+ gr->w = gw;
+ gr->h = HEIGHT;
+ gr->g = UCHAR_MAX >> 1;
+ rect_sort(gr);
+ }
+
+ {
+ struct rect *const rr = rect_get(true);
+
+ if (!rr)
+ return -1;
+
+ rr->x = x + gw;
+ rr->y = ry;
+ rr->w = WIDTH - gw;
+ rr->h = HEIGHT;
+ rr->r = UCHAR_MAX >> 1;
+ rect_sort(rr);
+ }
+
+ return 0;
+}
+
+static int draw_miners(const struct resource *const r)
+{
+ const struct resource_gold *const g = &r->res.gold;
+
+ if (!g->n_miners)
+ return 0;
+
+ for (size_t i = 0, n = 0; i < sizeof g->miners / sizeof *g->miners; i++)
+ {
+ if (g->miners[i])
+ {
+ enum {OFFSET = 112, SZ = 16, GAP = 4};
+ struct rect *const r = rect_get(true);
+
+ if (!r)
+ return -1;
+
+ r->x = OFFSET + n * (SZ + GAP);
+ r->y = screen_h - 40;
+ r->r = r->g = r->b = 127;
+ r->w = r->h = SZ;
+ rect_sort(r);
+
+ if (++n >= g->n_miners)
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static int draw_carry(const enum font f, const short x, const short y,
+ const struct unit *const u)
+{
+ if (unit_can_harvest(u))
+ {
+ const struct unit_harvester *const uh = &u->us.harvester;
+
+ if (uh->carry)
+ return font_printf(f, x, y, "%d", uh->carry) < 0;
+ }
+
+ return 0;
+}
+
+static int render_selsingle(const struct sprite *const up,
+ const struct human_player *const h)
+{
+ enum {X_OFF = 8, Y_OFF = 8, HP_Y = 32};
+ const short x = up->x + X_OFF, y = up->y + Y_OFF;
+
+ for (size_t i = 0; i < sizeof h->sel / sizeof *h->sel; i++)
+ {
+ const struct sel_instance *const sel = &h->sel[i];
+
+ if (sel)
+ {
+ switch (sel->type)
+ {
+ case INSTANCE_TYPE_BUILDING:
+ {
+ const struct building *const b = sel->d.b;
+ const struct instance *const in = &b->instance;
+ const instance_hp hp = in->hp, max_hp = building_maxhp(b);
+
+ if ((font_printf(FONT, x, y, "%s", building_str(b)) < 0)
+ || (font_printf(FONT, x, y + HP_Y, "%u/%u", hp, max_hp) < 0)
+ || draw_hp(in, x, y + HP_Y - 4, max_hp))
+ return -1;
+
+ return 0;
+ }
+ break;
+
+ case INSTANCE_TYPE_UNIT:
+ {
+ const struct unit *const u = sel->d.u;
+ const struct instance *const in = &u->instance;
+ const instance_hp hp = in->hp, max_hp = unit_maxhp(u);
+ enum {CARRY_X = 96, CARRY_Y = 8};
+
+ if ((font_printf(FONT, x, y, "%s", unit_str(u)) < 0)
+ || (font_printf(FONT, x, y + HP_Y, "%u/%u", hp, max_hp) < 0)
+ || draw_carry(FONT, x + CARRY_X, y, u)
+ || draw_hp(in, x, y + HP_Y - 4, max_hp))
+ return -1;
+
+ return 0;
+ }
+ break;
+
+ case INSTANCE_TYPE_RESOURCE:
+ {
+ const struct resource *const r = sel->d.r;
+ const struct instance *const in = &r->instance;
+ const instance_hp hp = in->hp, max_hp = resource_maxhp(r);
+
+ if ((font_printf(FONT, x, y, "%s", resource_str(r)) < 0)
+ || (font_printf(FONT, x, y + HP_Y, "%u/%u", hp, max_hp) < 0)
+ || draw_hp(in, x, y + HP_Y - 4, max_hp)
+ || (r->type == RESOURCE_TYPE_GOLD && draw_miners(r)))
+ return -1;
+
+ return 0;
+ }
+ break;
+ }
+
+ return 0;
+ }
+ }
+
+ /* Unreachable. */
+ return -1;
+}
+
+static int render_selinfo(const struct sprite *const up,
+ const struct human_player *const h)
+{
+ enum {X_OFF = 8, Y_OFF = 8};
+
+ if (h->n_sel == 1)
+ return render_selsingle(up, h);
+ else if (font_printf(FONT, up->x + X_OFF, up->y + Y_OFF,
+ "%u units selected", h->n_sel) < 0)
+ return -1;
+
+ return 0;
+}
+
+static int render_sel(const struct human_player *const h)
+{
+ struct sprite *const up = sprite_get();
+
+ if (!up
+ || render_seltop(up)
+ || render_selrect(up)
+ || render_selmidtop(up)
+ || render_selmiddown(up)
+ || render_selvertleft(up)
+ || render_selvertright(up)
+ || render_seldown(up)
+ || render_selinfo(up, h))
+ return -1;
+
+ return 0;
+}
+
+void gui_update(struct human_player *const h)
+{
+ struct player *const pl = &h->pl;
+
+ for (size_t i = 0; i < sizeof pl->resources / sizeof *pl->resources; i++)
+ {
+ if (h->gui_res[i] > pl->resources[i])
+ h->gui_res[i]--;
+ else if (h->gui_res[i] < pl->resources[i])
+ h->gui_res[i]++;
+ }
+}
+
+int gui_render(const struct human_player *const h)
+{
+ if ((h->top_gui && render_topmenu(h))
+ || (h->n_sel && render_sel(h)))
+ return -1;
+
+ return 0;
+}
diff --git a/src/instance/CMakeLists.txt b/src/instance/CMakeLists.txt
new file mode 100644
index 0000000..2e777a9
--- /dev/null
+++ b/src/instance/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_library(instance "src/instance.c")
+target_include_directories(instance PUBLIC "inc")
+target_link_libraries(instance PUBLIC camera gfx util)
diff --git a/src/instance/inc/instance.h b/src/instance/inc/instance.h
new file mode 100644
index 0000000..ca8a606
--- /dev/null
+++ b/src/instance/inc/instance.h
@@ -0,0 +1,68 @@
+#ifndef INSTANCE_H
+#define INSTANCE_H
+
+#include <camera.h>
+#include <gfx.h>
+#include <util.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+typedef unsigned int instance_hp;
+
+struct instance
+{
+ bool alive, dying;
+ unsigned int hp;
+ struct util_rect r;
+};
+
+typedef bool (*instance_attacked_cb)(struct instance *i, unsigned int ap);
+typedef bool (*instance_sheltered_cb)(struct instance *self, struct instance *other);
+typedef void (*instance_done_cb)(struct instance *i, void *op);
+
+struct instance_render_cfg
+{
+ const struct instance *i;
+ const struct camera *cam;
+ enum
+ {
+ INSTANCE_RENDER_CFG_SPRITE,
+ INSTANCE_RENDER_CFG_QUAD
+ } prim_type;
+
+ union
+ {
+ struct sprite *s;
+ const struct instance_render_quad
+ {
+ struct quad *q;
+ short w, h;
+ uint8_t u;
+ bool xflip;
+ } *quad;
+ } prim;
+
+ bool sel;
+ instance_hp max_hp;
+ const struct instance_render_off
+ {
+ short x, y;
+ } *off;
+};
+
+bool instance_attacked(struct instance *self, unsigned int ap);
+void instance_clear_pools(void);
+int instance_render(const struct instance_render_cfg *cfg);
+int instance_render_target(const struct instance *const i, const struct camera *cam);
+void instance_cyclic(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* INSTANCE_H */
diff --git a/src/instance/src/instance.c b/src/instance/src/instance.c
new file mode 100644
index 0000000..03f4872
--- /dev/null
+++ b/src/instance/src/instance.c
@@ -0,0 +1,150 @@
+#include <instance.h>
+#include <gfx.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+static unsigned char line_g;
+static bool line_g_flip;
+
+bool instance_attacked(struct instance *const self, const instance_hp ap)
+{
+ if (self->hp > ap)
+ self->hp -= ap;
+ else
+ {
+ self->hp = 0;
+ self->alive = false;
+ }
+
+ return !self->alive;
+}
+
+void instance_cyclic(void)
+{
+ if (!line_g_flip)
+ {
+ if ((line_g += 5) >= UCHAR_MAX)
+ line_g_flip ^= true;
+ }
+ else if (!(line_g -= 5))
+ line_g_flip ^= true;
+}
+
+static int draw_sel(const struct instance *const i, const short x, const short y)
+{
+ struct stp_4line *const l = stp_4line_get();
+
+ if (!l)
+ return -1;
+
+ enum {R = 0, G = 255, B = 0};
+
+ l->x = l->vertices[2].x = l->vertices[3].x = x;
+ l->y = l->vertices[0].y = l->vertices[3].y = y;
+ l->vertices[0].x = l->vertices[1].x = x + i->r.w;
+ l->vertices[1].y = l->vertices[2].y = y + i->r.h;
+ l->r = R;
+ l->g = line_g;
+ l->b = B >> 2;
+
+ for (size_t i = 0; i < sizeof l->vertices / sizeof *l->vertices; i++)
+ {
+ struct stp_4line_vtx *const v = &l->vertices[i];
+
+ v->r = R;
+ v->b = B;
+ }
+
+ l->vertices[0].g = l->vertices[2].g = UCHAR_MAX - line_g;
+ l->vertices[1].g = l->vertices[3].g = line_g;
+ stp_4line_sort(l);
+
+ struct stp_4line *const ll = stp_4line_get();
+
+ if (!ll)
+ return -1;
+
+ *ll = *l;
+ ll->x = ll->vertices[2].x = ll->vertices[3].x = l->x ? l->x - 1 : 0;
+ ll->y = ll->vertices[0].y = ll->vertices[3].y = l->y ? l->y - 1 : 0;
+ ll->vertices[0].x = ll->vertices[1].x = l->vertices[0].x + 1;
+ ll->vertices[1].y = ll->vertices[2].y = l->vertices[1].y + 1;
+ stp_4line_sort(ll);
+ return 0;
+}
+
+static void render_sprite(struct sprite *const s,
+ const struct instance_render_off *const off, const short x, const short y)
+{
+ s->x = off ? x + off->x : x;
+ s->y = off ? y + off->y : y;
+ sprite_sort(s);
+}
+
+static void render_quad(const struct instance_render_quad *const rq,
+ const struct instance_render_off *const off, const short x, const short y)
+{
+ struct quad *const q = rq->q;
+ const short x0 = x + off->x, x1 = x0 + rq->w - 1;
+
+ if (rq->xflip)
+ {
+ q->x0 = q->x2 = x1;
+ q->x1 = q->x3 = x0;
+ q->u0 = q->u2 = rq->u;
+ q->u1 = q->u3 = rq->u + rq->w - 1;
+ }
+ else
+ {
+ q->x0 = q->x2 = x0;
+ q->x1 = q->x3 = x1;
+ q->u0 = q->u2 = rq->u;
+ q->u1 = q->u3 = rq->u + rq->w - 1;
+ }
+
+ const short y0 = y + off->y, y1 = y0 + rq->h - 1;
+
+ q->y0 = q->y1 = y0;
+ q->y2 = q->y3 = y1;
+ quad_sort(q);
+}
+
+int instance_render(const struct instance_render_cfg *const cfg)
+{
+ const struct instance *const i = cfg->i;
+ short x, y;
+
+ if (camera_translate(cfg->cam, &i->r, &x, &y))
+ {
+ const struct instance_render_off *const off = cfg->off;
+
+ if (cfg->sel && draw_sel(i, x, y))
+ return -1;
+
+ switch (cfg->prim_type)
+ {
+ case INSTANCE_RENDER_CFG_SPRITE:
+ render_sprite(cfg->prim.s, off, x, y);
+ break;
+
+ case INSTANCE_RENDER_CFG_QUAD:
+ render_quad(cfg->prim.quad, off, x, y);
+ break;
+ }
+ }
+
+ return 0;
+}
+
+int instance_render_target(const struct instance *const i,
+ const struct camera *const cam)
+{
+ short x, y;
+
+ if (camera_translate(cam, &i->r, &x, &y))
+ return draw_sel(i, x, y);
+
+ return -1;
+}
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..fd0759f
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,14 @@
+#include <game.h>
+#include <system.h>
+#include <stdlib.h>
+
+int main(void)
+{
+ int ret = EXIT_SUCCESS;
+
+ if (system_init() || game())
+ ret = EXIT_FAILURE;
+
+ system_deinit();
+ return ret;
+}
diff --git a/src/pad/CMakeLists.txt b/src/pad/CMakeLists.txt
new file mode 100644
index 0000000..4862c2b
--- /dev/null
+++ b/src/pad/CMakeLists.txt
@@ -0,0 +1,10 @@
+add_library(pad "src/pad.c")
+target_include_directories(pad PUBLIC "inc" PRIVATE "privinc")
+
+if(PS1_BUILD)
+ target_include_directories(pad PUBLIC "ps1/inc")
+ target_sources(pad PRIVATE "ps1/src/pad.c")
+elseif(SDL1_2_BUILD)
+ target_include_directories(pad PUBLIC "sdl-1.2/inc")
+ target_sources(pad PRIVATE "sdl-1.2/src/pad.c")
+endif()
diff --git a/src/pad/inc/pad.h b/src/pad/inc/pad.h
new file mode 100644
index 0000000..2579c84
--- /dev/null
+++ b/src/pad/inc/pad.h
@@ -0,0 +1,49 @@
+#ifndef PAD_H
+#define PAD_H
+
+#include <pad/port.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#define PAD_KEYS \
+ X(PAD_KEY_LEFT) \
+ X(PAD_KEY_RIGHT) \
+ X(PAD_KEY_UP) \
+ X(PAD_KEY_DOWN) \
+ X(PAD_KEY_A) \
+ X(PAD_KEY_B) \
+ X(PAD_KEY_C) \
+ X(PAD_KEY_D) \
+ X(PAD_KEY_E) \
+ X(PAD_KEY_OPTIONS)
+
+enum pad_key
+{
+#define X(x) x,
+ PAD_KEYS
+#undef X
+};
+
+struct pad
+{
+ int player;
+ int mask, oldmask;
+ struct pad_port port;
+};
+
+void pad_init(int player, struct pad *p);
+void pad_update(struct pad *p);
+bool pad_pressed(const struct pad *p, enum pad_key k);
+bool pad_justpressed(const struct pad *p, enum pad_key k);
+bool pad_released(const struct pad *p, enum pad_key k);
+const char *pad_str(enum pad_key k);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PAD_H */
diff --git a/src/pad/privinc/pad_private.h b/src/pad/privinc/pad_private.h
new file mode 100644
index 0000000..095c37e
--- /dev/null
+++ b/src/pad/privinc/pad_private.h
@@ -0,0 +1,17 @@
+#ifndef PAD_PRIVATE_H
+#define PAD_PRIVATE_H
+
+#include <pad.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+void pad_port_update(struct pad *p);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PAD_PRIVATE_H */
diff --git a/src/pad/ps1/inc/pad/port.h b/src/pad/ps1/inc/pad/port.h
new file mode 100644
index 0000000..14dc784
--- /dev/null
+++ b/src/pad/ps1/inc/pad/port.h
@@ -0,0 +1,20 @@
+#ifndef PAD_PS1_H
+#define PAD_PS1_H
+
+#include <psx.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+struct pad_port
+{
+ psx_pad_state pps;
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PAD_PS1_H */
diff --git a/src/pad/ps1/src/pad.c b/src/pad/ps1/src/pad.c
new file mode 100644
index 0000000..eea56ed
--- /dev/null
+++ b/src/pad/ps1/src/pad.c
@@ -0,0 +1,41 @@
+#include <pad.h>
+#include <pad_private.h>
+#include <pad/port.h>
+#include <stddef.h>
+#include <string.h>
+
+void pad_init(const int player, struct pad *const p)
+{
+ memset(p, 0, sizeof *p);
+ p->player = player;
+}
+
+void pad_port_update(struct pad *const p)
+{
+ static const int psx_keys[] =
+ {
+ [PAD_KEY_LEFT] = PAD_LEFT,
+ [PAD_KEY_RIGHT] = PAD_RIGHT,
+ [PAD_KEY_UP] = PAD_UP,
+ [PAD_KEY_DOWN] = PAD_DOWN,
+ [PAD_KEY_A] = PAD_CROSS,
+ [PAD_KEY_B] = PAD_CIRCLE,
+ [PAD_KEY_C] = PAD_TRIANGLE,
+ [PAD_KEY_D] = PAD_SQUARE,
+ [PAD_KEY_E] = PAD_L1,
+ [PAD_KEY_OPTIONS] = PAD_START
+ };
+
+ p->mask = 0;
+ PSX_PollPad(p->player, &p->port.pps);
+
+ for (size_t i = 0; i < sizeof psx_keys / sizeof *psx_keys; i++)
+ {
+ const int mask = 1 << i;
+
+ if (p->port.pps.buttons & psx_keys[i])
+ p->mask |= mask;
+ else
+ p->mask &= ~mask;
+ }
+}
diff --git a/src/pad/sdl-1.2/inc/pad/port.h b/src/pad/sdl-1.2/inc/pad/port.h
new file mode 100644
index 0000000..b6ae85a
--- /dev/null
+++ b/src/pad/sdl-1.2/inc/pad/port.h
@@ -0,0 +1,20 @@
+#ifndef PAD_SDL_1_2_H
+#define PAD_SDL_1_2_H
+
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+struct pad_port
+{
+ bool exit;
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PAD_SDL_1_2_H */
diff --git a/src/pad/sdl-1.2/src/pad.c b/src/pad/sdl-1.2/src/pad.c
new file mode 100644
index 0000000..a062758
--- /dev/null
+++ b/src/pad/sdl-1.2/src/pad.c
@@ -0,0 +1,80 @@
+#include <pad.h>
+#include <SDL/SDL.h>
+#include <stdio.h>
+
+static void key_event(const SDL_KeyboardEvent *const key, struct pad *const p)
+{
+ static const int keys[] =
+ {
+ [PAD_KEY_LEFT] = SDLK_LEFT,
+ [PAD_KEY_RIGHT] = SDLK_RIGHT,
+ [PAD_KEY_UP] = SDLK_UP,
+ [PAD_KEY_DOWN] = SDLK_DOWN,
+ [PAD_KEY_A] = SDLK_x,
+ [PAD_KEY_B] = SDLK_d,
+ [PAD_KEY_C] = SDLK_w,
+ [PAD_KEY_D] = SDLK_a,
+ [PAD_KEY_E] = SDLK_q,
+ [PAD_KEY_OPTIONS] = SDLK_ESCAPE
+ };
+
+ for (size_t i = 0; i < sizeof keys / sizeof *keys; i++)
+ {
+ const SDLKey k = key->keysym.sym;
+
+ if (k == keys[i])
+ {
+ const int mask = 1 << i;
+
+ if (key->type == SDL_KEYDOWN)
+ {
+ printf("%s pressed\n", pad_str(i));
+ p->mask |= mask;
+ }
+ else
+ {
+ printf("%s released\n", pad_str(i));
+ p->mask &= ~mask;
+ }
+ }
+ }
+}
+
+void pad_port_update(struct pad *const p)
+{
+ SDL_Event ev;
+ static const int masks[] = {SDL_KEYEVENTMASK};
+ int n;
+
+ while ((n = SDL_PeepEvents(&ev, 1, SDL_GETEVENT,
+ masks[p->player] | SDL_QUITMASK)) > 0)
+ {
+ switch (ev.type)
+ {
+ case SDL_KEYDOWN:
+ /* Fall through. */
+ case SDL_KEYUP:
+ key_event(&ev.key, p);
+ break;
+
+ case SDL_QUIT:
+ p->port.exit = true;
+ break;
+
+ default:
+ fprintf(stderr, "%s: unexpected SDL_Event %d\n",
+ __func__, ev.type);
+ break;
+ }
+ }
+
+ if (n < 0)
+ {
+ fprintf(stderr, "%s: SDL_PeepEvents: %s\n", __func__, SDL_GetError());
+ return;
+ }
+}
+
+void pad_init(const int player, struct pad *const p)
+{
+}
diff --git a/src/pad/src/pad.c b/src/pad/src/pad.c
new file mode 100644
index 0000000..b95a43e
--- /dev/null
+++ b/src/pad/src/pad.c
@@ -0,0 +1,37 @@
+#include <pad.h>
+#include <pad_private.h>
+#include <pad/port.h>
+#include <stdbool.h>
+
+bool pad_pressed(const struct pad *const p, const enum pad_key k)
+{
+ return p->mask & (1 << k);
+}
+
+bool pad_justpressed(const struct pad *const p, const enum pad_key k)
+{
+ return p->mask & (1 << k) && !(p->oldmask & (1 << k));
+}
+
+bool pad_released(const struct pad *const p, const enum pad_key k)
+{
+ return !(p->mask & (1 << k)) && p->oldmask & (1 << k);
+}
+
+void pad_update(struct pad *const p)
+{
+ p->oldmask = p->mask;
+ pad_port_update(p);
+}
+
+const char *pad_str(const enum pad_key k)
+{
+ static const char *const s[] =
+ {
+#define X(x) [x] = #x,
+ PAD_KEYS
+#undef X
+ };
+
+ return s[k];
+}
diff --git a/src/player/CMakeLists.txt b/src/player/CMakeLists.txt
new file mode 100644
index 0000000..d7a275d
--- /dev/null
+++ b/src/player/CMakeLists.txt
@@ -0,0 +1,16 @@
+add_library(player "src/player.c" "src/human_player.c")
+target_include_directories(player PUBLIC "inc")
+target_link_libraries(player
+ PUBLIC
+ building
+ camera
+ gfx
+ instance
+ pad
+ resource
+ tech
+ unit
+ util
+ PRIVATE
+ pad
+ gui)
diff --git a/src/player/inc/human_player.h b/src/player/inc/human_player.h
new file mode 100644
index 0000000..6d97cf7
--- /dev/null
+++ b/src/player/inc/human_player.h
@@ -0,0 +1,69 @@
+#ifndef HUMAN_PLAYER_H
+#define HUMAN_PLAYER_H
+
+#include <camera.h>
+#include <instance.h>
+#include <pad.h>
+#include <player.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+enum {MAX_SELECTED_INSTANCES = 4};
+
+struct human_player
+{
+ struct player pl;
+ struct camera cam;
+ struct pad pad;
+
+ struct sel_instance
+ {
+ enum sel_type
+ {
+ INSTANCE_TYPE_UNIT,
+ INSTANCE_TYPE_BUILDING,
+ INSTANCE_TYPE_RESOURCE
+ } type;
+
+ union sel_data
+ {
+ const struct unit *u;
+ const struct building *b;
+ const struct resource *r;
+ const struct instance *i;
+ struct unit *rw_u;
+ } d;
+ } sel[MAX_SELECTED_INSTANCES];
+
+ struct
+ {
+ const struct instance *ins;
+ unsigned char t, n;
+ bool render;
+ } target;
+
+ size_t n_sel;
+ bool top_gui;
+ unsigned long gui_res[MAX_RESOURCE_TYPES];
+};
+
+struct human_player_cfg
+{
+ struct player_cfg pl;
+ int padn;
+};
+
+int human_player_init(const struct human_player_cfg *cfg, struct human_player *h);
+bool human_player_update(struct human_player *h, struct player_others *o);
+int human_player_render(const struct human_player *h, const struct player_others *o);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HUMAN_PLAYER_H */
diff --git a/src/player/inc/player.h b/src/player/inc/player.h
new file mode 100644
index 0000000..25f4d81
--- /dev/null
+++ b/src/player/inc/player.h
@@ -0,0 +1,71 @@
+#ifndef PLAYER_H
+#define PLAYER_H
+
+#include <building.h>
+#include <camera.h>
+#include <instance.h>
+#include <pad.h>
+#include <resource.h>
+#include <tech.h>
+#include <unit.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+typedef unsigned int player_team;
+
+enum
+{
+ PLAYER_MAX_UNITS = 5,
+ PLAYER_MAX_BUILDINGS = 5
+};
+
+struct player
+{
+ enum player_color
+ {
+ PLAYER_COLOR_BLUE,
+ PLAYER_COLOR_RED
+ } color;
+
+ player_team team;
+ bool alive;
+ struct unit units[PLAYER_MAX_UNITS];
+ struct building buildings[PLAYER_MAX_BUILDINGS];
+ unsigned long resources[MAX_RESOURCE_TYPES];
+ size_t pop, bpop;
+
+ struct
+ {
+ struct unit_tech u;
+ } tree;
+};
+
+struct player_cfg
+{
+ enum player_color color;
+ player_team team;
+ unsigned long x, y;
+};
+
+struct player_others
+{
+ struct player *pl;
+ struct resource *res;
+ size_t n_pl, n_res;
+};
+
+int player_init(const struct player_cfg *cfg, struct player *pl);
+int player_create_unit(const struct unit_cfg *cfg, struct player *pl);
+int player_create_building(const struct building_cfg *cfg, struct player *pl);
+void player_update(struct player *p);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PLAYER_H */
diff --git a/src/player/src/human_player.c b/src/player/src/human_player.c
new file mode 100644
index 0000000..7fd558a
--- /dev/null
+++ b/src/player/src/human_player.c
@@ -0,0 +1,570 @@
+#include <human_player.h>
+#include <player.h>
+#include <building.h>
+#include <camera.h>
+#include <gui.h>
+#include <instance.h>
+#include <pad.h>
+#include <resource.h>
+#include <unit.h>
+#include <util.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+static bool instance_selected(const struct human_player *const h,
+ const union sel_data *const sel)
+{
+ if (h->n_sel)
+ for (size_t i = 0; i < sizeof h->sel / sizeof *h->sel; i++)
+ {
+ const struct sel_instance *const si = &h->sel[i];
+
+ if (si && sel->u == si->d.u)
+ return true;
+ }
+
+ return false;
+}
+
+static bool select_units(struct human_player *const h, const short x,
+ const short y)
+{
+ struct player *const pl = &h->pl;
+
+ for (size_t i = 0; i < sizeof pl->units / sizeof *pl->units; i++)
+ {
+ const struct unit *const u = &pl->units[i];
+ const struct instance *const in = &u->instance;
+
+ if (in->alive)
+ {
+ switch (u->state)
+ {
+ default:
+ {
+ const union sel_data d = {.u = u};
+
+ if (!instance_selected(h, &d)
+ && cursor_collision(&h->cam, &in->r)
+ && h->n_sel < sizeof h->sel / sizeof *h->sel)
+ {
+ for (size_t i = 0; i < sizeof h->sel / sizeof *h->sel; i++)
+ {
+ struct sel_instance *const sel = &h->sel[i];
+
+ if (!sel->d.u)
+ {
+ sel->type = INSTANCE_TYPE_UNIT;
+ sel->d.u = u;
+ h->n_sel++;
+ sfx_play(&unit_sounds[UNIT_SOUND_SELECTED]);
+ return true;
+ }
+ }
+ }
+ }
+ break;
+
+ case UNIT_STATE_SHELTERED:
+ /* Fall through. */
+ case UNIT_STATE_HARVESTING_GOLD:
+ break;
+ }
+ }
+ }
+
+ return false;
+}
+
+static bool select_buildings(struct human_player *const h, const short x,
+ const short y)
+{
+ struct player *const pl = &h->pl;
+
+ for (size_t i = 0; i < sizeof pl->buildings / sizeof *pl->buildings; i++)
+ {
+ const struct building *const b = &pl->buildings[i];
+ const struct instance *const in = &b->instance;
+
+ if (in->alive)
+ {
+ const union sel_data d = {.b = b};
+
+ if (!instance_selected(h, &d)
+ && cursor_collision(&h->cam, &in->r)
+ && h->n_sel < sizeof h->sel / sizeof *h->sel)
+ {
+ for (size_t i = 0; i < sizeof h->sel / sizeof *h->sel; i++)
+ {
+ struct sel_instance *const sel = &h->sel[i];
+
+ if (!sel->d.b)
+ {
+ sel->type = INSTANCE_TYPE_BUILDING;
+ sel->d.b = b;
+ h->n_sel++;
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+static bool select_resources(struct human_player *const h, const short x,
+ const short y, const struct player_others *const o)
+{
+ for (size_t i = 0; i < o->n_res; i++)
+ {
+ const struct resource *const r = &o->res[i];
+ const struct instance *const in = &r->instance;
+
+ if (in->alive)
+ {
+ const union sel_data d = {.r = r};
+
+ if (!instance_selected(h, &d)
+ && cursor_collision(&h->cam, &in->r)
+ && h->n_sel < sizeof h->sel / sizeof *h->sel)
+ {
+ for (size_t i = 0; i < sizeof h->sel / sizeof *h->sel; i++)
+ {
+ struct sel_instance *const sel = &h->sel[i];
+
+ if (!sel->d.r)
+ {
+ sel->type = INSTANCE_TYPE_RESOURCE;
+ sel->d.r = r;
+ h->n_sel++;
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+static void select_instances(struct human_player *const h,
+ const struct player_others *const o)
+{
+ unsigned long x, y;
+
+ cursor_pos(&h->cam, &x, &y);
+
+ if (!select_buildings(h, x, y)
+ && !select_units(h, x, y))
+ select_resources(h, x, y, o);
+}
+
+static bool instance_collision(const struct camera *const cam,
+ const struct instance *const in)
+{
+ return in->alive && cursor_collision(cam, &in->r);
+}
+
+static void resources_stored(struct instance *const i, void *const op)
+{
+ struct player *const p = op;
+ struct unit *const u = (struct unit *)i;
+ struct unit_harvester *const uh = &u->us.harvester;
+
+ p->resources[uh->type] += uh->carry;
+ uh->carry = 0;
+ unit_set_target(u, &uh->prev_target);
+}
+
+static void harvest_done(struct instance *const ins, void *const op)
+{
+ struct player *const p = op;
+ struct unit *const u = (struct unit *)ins;
+ struct resource *const res = (struct resource *)u->target.ins;
+
+ if (res->type == RESOURCE_TYPE_GOLD)
+ {
+ struct resource_gold *const g = &res->res.gold;
+
+ for (size_t i = 0; i < sizeof g->miners / sizeof *g->miners; i++)
+ {
+ struct instance **const pi = &g->miners[i];
+
+ if (*pi == ins)
+ {
+ *pi = NULL;
+ g->n_miners--;
+ break;
+ }
+ }
+ }
+
+ for (size_t i = 0; i < sizeof p->buildings / sizeof *p->buildings; i++)
+ {
+ struct building *const b = &p->buildings[i];
+
+ if (b->instance.alive && b->type == BUILDING_TYPE_BARRACKS)
+ {
+ const struct unit_target t =
+ {
+ .ins = &b->instance,
+ .state = UNIT_STATE_CARRYING,
+ .done = resources_stored,
+ .op = p
+ /* TODO .state = UNIT_STATE_SHELTERED */
+ };
+
+ u->us.harvester.prev_target = u->target;
+ unit_set_target(u, &t);
+ break;
+ }
+ }
+}
+
+static bool target_from_own(struct player *const p,
+ const struct camera *const cam, struct unit_target *const t)
+{
+ for (size_t i = 0; i < sizeof p->buildings / sizeof *p->buildings; i++)
+ {
+ struct building *const b = &p->buildings[i];
+ struct instance *const in = &b->instance;
+
+ if (instance_collision(cam, in))
+ {
+ *t = (const struct unit_target)
+ {
+ .ins = in,
+ /* TODO .state = UNIT_STATE_SHELTERED */
+ };
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool target_from_others(const struct player *const p,
+ const struct player_others *const o,
+ const struct camera *const cam, struct unit_target *const t)
+{
+ for (size_t i = 0; i < o->n_pl; i++)
+ {
+ struct player *const pl = &o->pl[i];
+
+ if (p->team != pl->team)
+ {
+ for (size_t i = 0; i < sizeof pl->units / sizeof *pl->units; i++)
+ {
+ struct unit *const u = &pl->units[i];
+ struct instance *const in = &u->instance;
+
+ if (instance_collision(cam, in))
+ {
+ *t = (const struct unit_target)
+ {
+ .ins = in,
+ .attack = unit_attacked,
+ .state = UNIT_STATE_ATTACKING
+ };
+
+ return true;
+ }
+ }
+
+ for (size_t i = 0; i < sizeof pl->buildings / sizeof *pl->buildings; i++)
+ {
+ struct building *const b = &pl->buildings[i];
+ struct instance *const in = &b->instance;
+
+ if (instance_collision(cam, in))
+ {
+ *t = (const struct unit_target)
+ {
+ .ins = in,
+ .state = UNIT_STATE_ATTACKING
+ };
+
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+static enum unit_state target_state_from_res(const struct resource *const res)
+{
+ switch (res->type)
+ {
+ case RESOURCE_TYPE_GOLD:
+ return UNIT_STATE_HARVESTING_GOLD;
+
+ case RESOURCE_TYPE_WOOD:
+ return UNIT_STATE_HARVESTING_WOOD;
+
+ default:
+ break;
+ }
+
+ return UNIT_STATE_IDLE_MOVING;
+}
+
+static bool target_from_res(struct player *const p,
+ const struct player_others *const o,
+ const struct camera *const cam, struct unit_target *const t)
+{
+ for (size_t i = 0; i < o->n_res; i++)
+ {
+ struct resource *const res = &o->res[i];
+ struct instance *const in = &res->instance;
+
+ if (instance_collision(cam, in))
+ {
+ *t = (const struct unit_target)
+ {
+ .ins = in,
+ .attack = resource_harvested,
+ .shelter = resource_shelter(res),
+ .state = target_state_from_res(res),
+ .done = harvest_done,
+ .op = p
+ };
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static void set_target(struct human_player *const h, struct unit *const u,
+ const struct unit_target *const t)
+{
+ unit_set_target(u, t);
+ memset(&h->target, 0, sizeof h->target);
+ h->target.ins = t->ins;
+ h->target.render = true;
+}
+
+static void target_from_pos(struct player *const p,
+ const struct player_others *const o,
+ const struct camera *const cam, struct unit_target *const t)
+{
+ if (!target_from_own(p, cam, t)
+ && !target_from_others(p, o, cam, t)
+ && !target_from_res(p, o, cam, t))
+ t->ins = NULL;
+}
+
+static void move_units(struct human_player *const h,
+ struct player_others *const o)
+{
+ if (!h->n_sel)
+ return;
+
+ unsigned long x, y;
+ struct player *const p = &h->pl;
+ struct unit_target t;
+
+ target_from_pos(p, o, &h->cam, &t);
+ cursor_pos(&h->cam, &x, &y);
+
+ bool unit_move = false;
+
+ for (size_t i = 0; i < sizeof h->sel / sizeof *h->sel; i++)
+ {
+ struct sel_instance *const si = &h->sel[i];
+
+ if (si->d.u && si->type == INSTANCE_TYPE_UNIT)
+ {
+ struct unit *const u = si->d.rw_u;
+
+ if (!t.ins || !unit_target_valid(u, &t))
+ unit_move_to(u, x, y);
+ else
+ set_target(h, u, &t);
+
+ unit_move = true;
+ }
+ }
+
+ if (unit_move)
+ {
+ const enum unit_sound s = rand() & 1 ?
+ UNIT_SOUND_MOVE : UNIT_SOUND_MOVE_2;
+ sfx_play(&unit_sounds[s]);
+ }
+}
+
+static void deselect_instances(struct human_player *const h)
+{
+ memset(h->sel, 0, sizeof h->sel);
+ h->n_sel = 0;
+}
+
+static void update_selected(struct human_player *const h)
+{
+ if (h->n_sel)
+ {
+ for (size_t i = 0; i < sizeof h->sel / sizeof *h->sel; i++)
+ {
+ struct sel_instance *const si = &h->sel[i];
+
+ if (si->d.i && (!si->d.i->alive || si->d.i->dying
+ || (si->type == INSTANCE_TYPE_UNIT
+ && (si->d.u->state == UNIT_STATE_HARVESTING_GOLD
+ || si->d.u->state == UNIT_STATE_SHELTERED))))
+ {
+ si->d.i = NULL;
+ h->n_sel--;
+ }
+ }
+ }
+}
+
+static void update_target(struct human_player *const h)
+{
+ const struct instance *const i = h->target.ins;
+
+ if (i)
+ {
+ enum {TOGGLE = 10};
+
+ if (!i->alive)
+ memset(&h->target, 0, sizeof h->target);
+ else if (++h->target.t >= TOGGLE)
+ {
+ enum {TIMES = 5};
+
+ h->target.render ^= true;
+ h->target.t = 0;
+
+ if (++h->target.n >= TIMES)
+ memset(&h->target, 0, sizeof h->target);
+ }
+ }
+}
+
+bool human_player_update(struct human_player *const h,
+ struct player_others *const o)
+{
+ bool ret = false;
+ struct player *const p = &h->pl;
+
+ if (p->alive)
+ {
+ gui_update(h);
+ pad_update(&h->pad);
+ update_selected(h);
+ update_target(h);
+
+ if (pad_justpressed(&h->pad, PAD_KEY_OPTIONS))
+ ret = true;
+ else if (pad_justpressed(&h->pad, PAD_KEY_A))
+ select_instances(h, o);
+ else if (pad_justpressed(&h->pad, PAD_KEY_B))
+ move_units(h, o);
+ else if (pad_justpressed(&h->pad, PAD_KEY_C))
+ deselect_instances(h);
+ else if (pad_justpressed(&h->pad, PAD_KEY_E))
+ h->top_gui ^= true;
+
+ camera_update(&h->cam, &h->pad);
+ player_update(p);
+ }
+
+ return ret;
+}
+
+static int render_target(const struct human_player *const h)
+{
+ const struct instance *const i = h->target.ins;
+
+ if (!i || !i->alive || !h->target.render)
+ return 0;
+
+ return instance_render_target(i, &h->cam);
+}
+
+static int render_own_units(const struct human_player *const h)
+{
+ const struct player *const p = &h->pl;
+
+ for (size_t i = 0; i < sizeof p->units / sizeof *p->units; i++)
+ {
+ const struct unit *const u = &p->units[i];
+ const union sel_data d = {.u = u};
+ const bool sel = instance_selected(h, &d);
+
+ if (unit_render(u, &h->cam, sel))
+ return -1;
+ }
+
+ return 0;
+}
+
+static int render_own_buildings(const struct human_player *const h)
+{
+ const struct player *const p = &h->pl;
+
+ for (size_t i = 0; i < sizeof p->buildings / sizeof *p->buildings; i++)
+ {
+ const struct building *const b = &p->buildings[i];
+ const union sel_data d = {.b = b};
+ const bool sel = instance_selected(h, &d);
+
+ if (building_render(b, &h->cam, sel))
+ return -1;
+ }
+
+ return 0;
+}
+
+static int render_resources(const struct human_player *const h,
+ const struct resource *const res, const size_t n)
+{
+ for (size_t i = 0; i < n; i++)
+ {
+ const struct resource *const r = &res[i];
+ const union sel_data d = {.r = r};
+ const bool sel = instance_selected(h, &d);
+
+ if (resource_render(r, &h->cam, sel))
+ return -1;
+ }
+
+ return 0;
+}
+
+int human_player_render(const struct human_player *const h,
+ const struct player_others *const o)
+{
+ if (render_target(h)
+ || render_own_units(h)
+ || render_own_buildings(h)
+ || render_resources(h, o->res, o->n_res)
+ || gui_render(h))
+ return -1;
+
+ return 0;
+}
+
+int human_player_init(const struct human_player_cfg *const cfg,
+ struct human_player *const h)
+{
+ memset(h, 0, sizeof *h);
+
+ if (player_init(&cfg->pl, &h->pl))
+ return -1;
+
+ pad_init(cfg->padn, &h->pad);
+ cursor_init(&h->cam.cursor);
+ h->top_gui = true;
+ memmove(h->gui_res, h->pl.resources, sizeof h->gui_res);
+ return 0;
+}
diff --git a/src/player/src/player.c b/src/player/src/player.c
new file mode 100644
index 0000000..a638cb8
--- /dev/null
+++ b/src/player/src/player.c
@@ -0,0 +1,112 @@
+#include <player.h>
+#include <building.h>
+#include <unit.h>
+#include <stddef.h>
+
+void player_update(struct player *const p)
+{
+ for (size_t i = 0; i < sizeof p->units / sizeof *p->units; i++)
+ {
+ struct unit *const u = &p->units[i];
+
+ unit_update(&p->tree.u, u);
+ }
+}
+
+int player_create_unit(const struct unit_cfg *const cfg, struct player *const pl)
+{
+ enum {N = sizeof pl->units / sizeof *pl->units};
+
+ if (pl->pop < N)
+ {
+ for (size_t i = 0; i < N; i++)
+ {
+ struct unit *const u = &pl->units[i];
+ struct instance *const i = &u->instance;
+
+ if (!i->alive)
+ {
+ unit_create(cfg, u);
+ pl->pop++;
+ return 0;
+ }
+ }
+ }
+
+ return -1;
+}
+
+int player_create_building(const struct building_cfg *const cfg, struct player *const pl)
+{
+ enum {N = sizeof pl-> buildings / sizeof *pl->buildings};
+
+ if (pl->bpop < N)
+ {
+ for (size_t i = 0; i < N; i++)
+ {
+ struct building *const b = &pl->buildings[i];
+ struct instance *const i = &b->instance;
+
+ if (!i->alive)
+ {
+ building_create(cfg, b);
+ pl->bpop++;
+ return 0;
+ }
+ }
+ }
+
+ return -1;
+}
+
+int player_init(const struct player_cfg *const cfg, struct player *const pl)
+{
+ const unsigned long x = cfg->x, y = cfg->y;
+
+ const struct building_cfg bcfg =
+ {
+ .type = BUILDING_TYPE_BARRACKS,
+ .x = x,
+ .y = y
+ };
+
+ const struct unit_cfg cfgs[] =
+ {
+ {
+ .type = UNIT_TYPE_PEASANT,
+ .x = x + 80,
+ .y = y
+ },
+
+ {
+ .type = UNIT_TYPE_PEASANT,
+ .x = x + 80,
+ .y = y + 40
+ },
+
+ {
+ .type = UNIT_TYPE_PEASANT,
+ .x = x + 100,
+ .y = y + 20
+ }
+ };
+
+ if (player_create_building(&bcfg, pl))
+ return -1;
+
+ for (size_t i = 0; i < sizeof cfgs / sizeof *cfgs; i++)
+ if (player_create_unit(&cfgs[i], pl))
+ return -1;
+
+ for (size_t i = 0; i < sizeof pl->resources / sizeof *pl->resources; i++)
+ {
+ enum {DEFAULT_RES = 120};
+
+ pl->resources[i] = DEFAULT_RES;
+ }
+
+ pl->alive = true;
+ pl->color = cfg->color;
+ pl->team = cfg->team;
+ return 0;
+}
diff --git a/src/resource/CMakeLists.txt b/src/resource/CMakeLists.txt
new file mode 100644
index 0000000..1591e58
--- /dev/null
+++ b/src/resource/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_library(resource "src/resource.c")
+target_include_directories(resource PUBLIC "inc")
+target_link_libraries(resource PUBLIC camera gfx instance util)
diff --git a/src/resource/inc/resource.h b/src/resource/inc/resource.h
new file mode 100644
index 0000000..56da45e
--- /dev/null
+++ b/src/resource/inc/resource.h
@@ -0,0 +1,57 @@
+#ifndef RESOURCE_H
+#define RESOURCE_H
+
+#include <camera.h>
+#include <container.h>
+#include <gfx.h>
+#include <instance.h>
+#include <resource_type.h>
+#include <util.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+enum {RESOURCE_GOLD_CAPACITY = 2};
+
+struct resource
+{
+ struct instance instance;
+ enum resource_type type;
+
+ union
+ {
+ struct resource_gold
+ {
+ struct instance *miners[RESOURCE_GOLD_CAPACITY];
+ size_t n_miners;
+ } gold;
+ } res;
+};
+
+UTIL_STATIC_ASSERT(!offsetof(struct resource, instance), "must be at offset zero");
+
+struct resource_cfg
+{
+ enum resource_type type;
+ unsigned long x, y;
+};
+
+const struct container_list *resource_res(void);
+int resource_create(const struct resource_cfg *cfg, struct resource *list, size_t n);
+int resource_render(const struct resource *res, const struct camera *cam, bool sel);
+instance_sheltered_cb resource_shelter(const struct resource *res);
+bool resource_harvested(struct instance *i, instance_hp ap);
+instance_hp resource_maxhp(const struct resource *res);
+const char *resource_str(const struct resource *res);
+
+extern struct sprite resource_sprites[MAX_RESOURCE_TYPES];
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* RESOURCE_H */
diff --git a/src/resource/inc/resource_type.h b/src/resource/inc/resource_type.h
new file mode 100644
index 0000000..8a41148
--- /dev/null
+++ b/src/resource/inc/resource_type.h
@@ -0,0 +1,21 @@
+#ifndef RESOURCE_TYPE_H
+#define RESOURCE_TYPE_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+enum resource_type
+{
+ RESOURCE_TYPE_WOOD,
+ RESOURCE_TYPE_GOLD,
+
+ MAX_RESOURCE_TYPES
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* RESOURCE_TYPE_H */
diff --git a/src/resource/src/resource.c b/src/resource/src/resource.c
new file mode 100644
index 0000000..dd59bac
--- /dev/null
+++ b/src/resource/src/resource.c
@@ -0,0 +1,139 @@
+#include <resource.h>
+#include <gfx.h>
+#include <instance.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+struct sprite resource_sprites[MAX_RESOURCE_TYPES];
+
+static bool gold_shelter(struct instance *const self,
+ struct instance *const other)
+{
+ struct resource *const res = (struct resource *)self;
+ struct resource_gold *const g = &res->res.gold;
+
+ for (size_t i = 0; i < sizeof g->miners / sizeof *g->miners; i++)
+ {
+ struct instance **const ins = &g->miners[i];
+
+ if (!*ins)
+ {
+ *ins = other;
+ g->n_miners++;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+instance_sheltered_cb resource_shelter(const struct resource *res)
+{
+ static const instance_sheltered_cb s[] =
+ {
+ [RESOURCE_TYPE_GOLD] = gold_shelter
+ };
+
+ return s[res->type];
+}
+
+bool resource_harvested(struct instance *const self, const instance_hp ap)
+{
+ return instance_attacked(self, ap);
+}
+
+instance_hp resource_maxhp(const struct resource *const res)
+{
+ static const instance_hp hp[] =
+ {
+ [RESOURCE_TYPE_GOLD] = 1000,
+ [RESOURCE_TYPE_WOOD] = 45,
+ };
+
+ return hp[res->type];
+}
+
+int resource_render(const struct resource *const res,
+ const struct camera *const cam, const bool sel)
+{
+ const struct instance *const in = &res->instance;
+
+ if (!in->alive)
+ return 0;
+
+ struct sprite *const s = sprite_get();
+
+ if (!s || sprite_clone(&resource_sprites[res->type], s))
+ return -1;
+
+ if (res->type == RESOURCE_TYPE_GOLD)
+ {
+ s->h = in->r.h;
+
+ if (res->res.gold.n_miners)
+ s->v += s->h;
+ }
+
+ const struct instance_render_cfg cfg =
+ {
+ .i = &res->instance,
+ .prim_type = INSTANCE_RENDER_CFG_SPRITE,
+ .prim = {.s = s},
+ .cam = cam,
+ .sel = sel,
+ .max_hp = resource_maxhp(res)
+ };
+
+ return instance_render(&cfg);
+}
+
+static void get_dimensions(const enum resource_type type, short *const w,
+ short *const h)
+{
+ static const struct dim
+ {
+ short w, h;
+ } dim[] =
+ {
+ [RESOURCE_TYPE_GOLD] = {.w = 96, .h = 96},
+ [RESOURCE_TYPE_WOOD] = {.w = 32, .h = 46}
+ };
+
+ const struct dim *const d = &dim[type];
+ *w = d->w;
+ *h = d->h;
+}
+
+int resource_create(const struct resource_cfg *const cfg, struct resource *const list,
+ const size_t n)
+{
+ for (size_t i = 0; i < n; i++)
+ {
+ struct resource *const r = &list[i];
+ struct instance *const in = &r->instance;
+
+ if (!in->alive)
+ {
+ get_dimensions(cfg->type, &in->r.w, &in->r.h);
+ r->type = cfg->type;
+ in->r.x = cfg->x;
+ in->r.y = cfg->y;
+ in->alive = true;
+ in->hp = resource_maxhp(r);
+ return 0;
+ }
+ }
+
+ return -1;
+}
+
+const char *resource_str(const struct resource *const res)
+{
+ static const char *const str[] =
+ {
+ [RESOURCE_TYPE_GOLD] = "Gold mine",
+ [RESOURCE_TYPE_WOOD] = "Pine tree"
+ };
+
+ return str[res->type];
+}
diff --git a/src/sfx/CMakeLists.txt b/src/sfx/CMakeLists.txt
new file mode 100644
index 0000000..3453d2e
--- /dev/null
+++ b/src/sfx/CMakeLists.txt
@@ -0,0 +1,37 @@
+set(inc "inc")
+
+if(PS1_BUILD)
+ set(src
+ "ps1/src/sound.c")
+ set(inc ${inc} "ps1/inc")
+ set(privdeps system)
+elseif(SDL1_2_BUILD)
+ set(src
+ "sdl-1.2/src/sound.c")
+ set(inc ${inc} "sdl-1.2/inc")
+ set(deps ${deps} SDL_mixer)
+ set(privdeps ${privdeps} SDL)
+endif()
+
+add_library(sfx ${src})
+target_include_directories(sfx PUBLIC ${inc})
+target_include_directories(sfx PRIVATE ${privinc})
+target_link_libraries(sfx PUBLIC ${deps} PRIVATE ${privdeps})
+
+if(PS1_BUILD)
+ set(modes VMODE_PAL VMODE_NTSC)
+
+ if(VIDEO_MODE)
+ if(NOT "${VIDEO_MODE}" IN_LIST modes)
+ message(FATAL_ERROR "Invalid video mode ${VIDEO_MODE}. Available options:\n"
+ "${modes}\n"
+ "Run CMake again using one of the available video modes e.g.: cmake .. -DVIDEO_MODE=VMODE_PAL")
+ endif()
+
+ target_compile_definitions(sfx PRIVATE VIDEO_MODE=${VIDEO_MODE})
+ else()
+ message(FATAL_ERROR "Please define video mode. Available options:\n"
+ "${modes}\n"
+ "Run CMake again using one of the available video modes e.g.: cmake .. -DVIDEO_MODE=VMODE_PAL")
+ endif()
+endif()
diff --git a/src/sfx/inc/sfx.h b/src/sfx/inc/sfx.h
new file mode 100644
index 0000000..b8fe44f
--- /dev/null
+++ b/src/sfx/inc/sfx.h
@@ -0,0 +1,22 @@
+#ifndef SFX_H
+#define SFX_H
+
+#include <sfx/port.h>
+#include <stdio.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+int sfx_init(void);
+int sfx_sound_from_fp(struct sound *s, FILE *f);
+int sfx_play(const struct sound *s);
+void sfx_free(struct sound *s);
+void sfx_deinit(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SFX_H */
diff --git a/src/sfx/ps1/inc/sfx/port.h b/src/sfx/ps1/inc/sfx/port.h
new file mode 100644
index 0000000..80c97e8
--- /dev/null
+++ b/src/sfx/ps1/inc/sfx/port.h
@@ -0,0 +1,17 @@
+#ifndef SFX_PS1_H
+#define SFX_PS1_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+typedef uint32_t sfx_spu_addr;
+typedef uint8_t sfx_spu_voice;
+
+struct sound
+{
+ sfx_spu_addr addr;
+ uint16_t sample_rate;
+ sfx_spu_voice voice;
+};
+
+#endif /* SFX_PS1_H */
diff --git a/src/sfx/ps1/src/sound.c b/src/sfx/ps1/src/sound.c
new file mode 100644
index 0000000..e0377c7
--- /dev/null
+++ b/src/sfx/ps1/src/sound.c
@@ -0,0 +1,188 @@
+#include <sfx.h>
+#include <sfx/port.h>
+#include <psxspu.h>
+#include <ctype.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+
+void sfx_free(struct sound *const s)
+{
+}
+
+void sfx_deinit(void)
+{
+}
+
+static int vag_ntohs(int n)
+{
+ return (n & 0xFF) << 24
+ | (n & 0xFF00) << 16
+ | (n & 0xFF0000) >> 8
+ | (n & 0xFF000000) >> 24;
+}
+
+static const char magic[] = "VAGp";
+
+struct vag_header
+{
+ char magic_bytes[sizeof magic - 1];
+ uint32_t version;
+ uint32_t :32;
+ uint32_t size;
+ uint32_t sample_rate;
+ uint8_t reserved[12];
+ char name[16];
+};
+
+static int get_header(struct vag_header *const h, FILE *const f)
+{
+ int ret = -1;
+
+ if (!fread(h, sizeof *h, 1, f))
+ {
+ fprintf(stderr, "VAG header not found\n");
+ goto end;
+ }
+ else if (strncmp(h->magic_bytes, magic, sizeof h->magic_bytes))
+ {
+ fprintf(stderr, "Expected magic bytes \"%s\", found: ", magic);
+ enum {N = sizeof h->magic_bytes / sizeof *h->magic_bytes};
+
+ for (size_t i = 0; i < N; i++)
+ {
+ const char c = h->magic_bytes[i];
+
+ fprintf(stderr, "[%zu] = %#x", i, c);
+
+ if ((i + 1) < N)
+ fprintf(stderr, ", ");
+
+ if (isalnum(c))
+ fprintf(stderr, "(%c)", c);
+ }
+
+ putchar('\n');
+ goto end;
+ }
+
+ /* File data is stored as big endian for some reason. */
+ h->version = vag_ntohs(h->version);
+ h->sample_rate = vag_ntohs(h->sample_rate);
+ h->size = vag_ntohs(h->size);
+ ret = 0;
+
+end:
+ return ret;
+}
+
+static sfx_spu_addr cur_addr = SPU_DATA_BASE_ADDR;
+
+static int upload_vag(const struct vag_header *const h,
+ struct sound *const s, FILE *const f)
+{
+ enum {SPU_RAM = 512 << 10};
+ const sfx_spu_addr end_addr = cur_addr + h->size;
+
+ if (end_addr >= SPU_RAM)
+ {
+ fprintf(stderr, "maximum SPU address exceeded (%#x, max %#x)\n",
+ end_addr, SPU_RAM);
+ return -1;
+ }
+
+ s->addr = cur_addr;
+ s->sample_rate = h->sample_rate;
+
+ for (size_t i = 0; i < h->size; /* See loop body. */)
+ {
+ /* From https://problemkaputt.de/psx-spx.htm#soundprocessingunitspu :
+ * "Sound RAM Data Transfer Fifo" supports up to 32 samples. */
+ uint16_t hwords[32];
+ const size_t rem = h->size - i;
+ const size_t n = rem > sizeof hwords ? sizeof hwords : rem;
+
+ if (!fread(&hwords, n, 1, f))
+ {
+ fprintf(stderr, "%s: failed to read %zu bytes\n", __func__, n);
+ return -1;
+ }
+
+ SsUpload(&hwords, n, cur_addr);
+ cur_addr += n;
+ i += n;
+ }
+
+#if 0
+ const unsigned used = cur_addr - SPU_DATA_BASE_ADDR;
+ printf("SPU RAM: %u bytes (%u%%)\n", used, (used * 100) / (SPU_RAM - SPU_DATA_BASE_ADDR));
+#endif
+ return 0;
+}
+
+typedef volatile struct
+{
+ uint16_t l_vol, r_vol;
+ uint16_t adpcm_sample_rate;
+ uint16_t adpcm_start_addr;
+
+ union
+ {
+ struct
+ {
+ uint32_t sustain_lvl :4;
+ uint32_t decay_shift :4;
+ uint32_t attack_step :2;
+ uint32_t attack_shift :5;
+ uint32_t attack_mode :1;
+ uint32_t release_shift :5;
+ uint32_t release_mode :1;
+ uint32_t sustain_step :2;
+ uint32_t sustain_shift :5;
+ uint32_t :1;
+ uint32_t sustain_dir :1;
+ uint32_t sustain_mode :1;
+ } bit;
+
+ uint32_t mask;
+ } adsr;
+
+ uint16_t adsr_vol;
+ uint16_t adsr_repeat_addr;
+} *spu_voice;
+
+#define SPU_VOICE ((spu_voice)0x1f801c00)
+
+int sfx_play(const struct sound *const s)
+{
+ const sfx_spu_voice voice = s->voice;
+
+ SPU_VOICE[voice].adpcm_sample_rate = SsFreqToPitch(s->sample_rate);
+ SPU_VOICE[voice].adpcm_start_addr = s->addr >> 3;
+ SPU_VOICE[voice].l_vol = 0x3FFF;
+ SPU_VOICE[voice].r_vol = 0x3FFF;
+
+ if (voice > sizeof SPU_KEY_ON1 * 8)
+ SPU_KEY_ON2 = 1 << (voice - sizeof SPU_KEY_ON1 * 8);
+ else
+ SPU_KEY_ON1 = 1 << voice;
+
+ return 0;
+}
+
+int sfx_sound_from_fp(struct sound *const s, FILE *const f)
+{
+ struct vag_header h;
+
+ if (get_header(&h, f) || upload_vag(&h, s, f))
+ return -1;
+
+ return 0;
+}
+
+int sfx_init(void)
+{
+ SsInit();
+ return 0;
+}
diff --git a/src/sfx/sdl-1.2/inc/sfx/port.h b/src/sfx/sdl-1.2/inc/sfx/port.h
new file mode 100644
index 0000000..d8418f7
--- /dev/null
+++ b/src/sfx/sdl-1.2/inc/sfx/port.h
@@ -0,0 +1,11 @@
+#ifndef SFX_SDL1_2_H
+#define SFX_SDL1_2_H
+
+#include <SDL/SDL_mixer.h>
+
+struct sound
+{
+ Mix_Chunk *chunk;
+};
+
+#endif /* SFX_SDL1_2_H */
diff --git a/src/sfx/sdl-1.2/src/sound.c b/src/sfx/sdl-1.2/src/sound.c
new file mode 100644
index 0000000..e843923
--- /dev/null
+++ b/src/sfx/sdl-1.2/src/sound.c
@@ -0,0 +1,71 @@
+#include <sfx.h>
+#include <sfx/port.h>
+#include <SDL/SDL.h>
+#include <SDL/SDL_mixer.h>
+#include <stdio.h>
+
+void sfx_free(struct sound *const s)
+{
+ if (s && s->chunk)
+ Mix_FreeChunk(s->chunk);
+}
+
+int sfx_play(const struct sound *const s)
+{
+ if (Mix_PlayChannel(-1, s->chunk, 0) < 0)
+ return -1;
+
+ return 0;
+}
+
+int sfx_sound_from_fp(struct sound *const s, FILE *const f)
+{
+ int ret = -1;
+ SDL_RWops *const ops = SDL_RWFromFP(f, 0);
+
+ if (!ops)
+ {
+ fprintf(stderr, "SDL_RWFromFP: %s\n", SDL_GetError());
+ goto end;
+ }
+ else if (!(s->chunk = Mix_LoadWAV_RW(ops, 0)))
+ {
+ fprintf(stderr, "Mix_LoadWAV_RW: %s\n", SDL_GetError());
+ goto end;
+ }
+
+ ret = 0;
+
+end:
+ SDL_FreeRW(ops);
+ return ret;
+}
+
+void sfx_deinit(void)
+{
+ Mix_CloseAudio();
+ SDL_QuitSubSystem(SDL_INIT_AUDIO);
+}
+
+int sfx_init(void)
+{
+ enum {CHUNK_SZ = 4096};
+
+ if (SDL_InitSubSystem(SDL_INIT_AUDIO))
+ {
+ fprintf(stderr, "SDL_InitSubSystem: %s\n", SDL_GetError());
+ goto failure;
+ }
+ else if (Mix_OpenAudio(MIX_DEFAULT_FREQUENCY,
+ MIX_DEFAULT_FORMAT, MIX_DEFAULT_CHANNELS, CHUNK_SZ))
+ {
+ fprintf(stderr, "Mix_OpenAudio: %s\n", SDL_GetError());
+ goto failure;
+ }
+
+ return 0;
+
+failure:
+ sfx_deinit();
+ return -1;
+}
diff --git a/src/system.cnf b/src/system.cnf
new file mode 100644
index 0000000..c1c0208
--- /dev/null
+++ b/src/system.cnf
@@ -0,0 +1,4 @@
+BOOT = cdrom:\rts.exe;1
+TCB = 4
+EVENT = 8
+STACK = 801FF800
diff --git a/src/system/CMakeLists.txt b/src/system/CMakeLists.txt
new file mode 100644
index 0000000..679d5b4
--- /dev/null
+++ b/src/system/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(inc "inc")
+
+if(PS1_BUILD)
+ set(src "ps1/src/init.c")
+ set(inc ${inc} "ps1/inc")
+elseif(SDL1_2_BUILD)
+ set(src "sdl-1.2/src/system.c")
+ set(inc ${inc} "sdl-1.2/inc")
+endif()
+
+add_library(system ${src})
+target_include_directories(system PUBLIC ${inc})
+target_link_libraries(system PRIVATE gfx sfx)
diff --git a/src/system/inc/system.h b/src/system/inc/system.h
new file mode 100644
index 0000000..5338a0b
--- /dev/null
+++ b/src/system/inc/system.h
@@ -0,0 +1,17 @@
+#ifndef SYSTEM_H
+#define SYSTEM_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+int system_init(void);
+void system_deinit(void);
+void system_loop(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SYSTEM_H */
diff --git a/src/system/ps1/inc/system/port.h b/src/system/ps1/inc/system/port.h
new file mode 100644
index 0000000..395097e
--- /dev/null
+++ b/src/system/ps1/inc/system/port.h
@@ -0,0 +1,17 @@
+#ifndef INIT_PS1_H
+#define INIT_PS1_H
+
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+extern volatile bool vblank_set;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* INIT_PS1_H */
diff --git a/src/system/ps1/src/init.c b/src/system/ps1/src/init.c
new file mode 100644
index 0000000..a644e47
--- /dev/null
+++ b/src/system/ps1/src/init.c
@@ -0,0 +1,32 @@
+#include <gfx.h>
+#include <sfx.h>
+#include <system.h>
+#include <psx.h>
+#include <stdbool.h>
+
+volatile bool vblank_set;
+
+static void vblank(void *const arg)
+{
+ vblank_set = true;
+}
+
+void system_deinit(void)
+{
+ gfx_deinit();
+ sfx_deinit();
+}
+
+int system_init(void)
+{
+ SetVBlankHandler(vblank);
+
+ if (gfx_init() || sfx_init())
+ return -1;
+
+ return 0;
+}
+
+void system_loop(void)
+{
+}
diff --git a/src/system/sdl-1.2/inc/system/port.h b/src/system/sdl-1.2/inc/system/port.h
new file mode 100644
index 0000000..3838026
--- /dev/null
+++ b/src/system/sdl-1.2/inc/system/port.h
@@ -0,0 +1,13 @@
+#ifndef INIT_SDL_1_2_H
+#define INIT_SDL_1_2_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* INIT_SDL_H */
diff --git a/src/system/sdl-1.2/src/system.c b/src/system/sdl-1.2/src/system.c
new file mode 100644
index 0000000..a738a75
--- /dev/null
+++ b/src/system/sdl-1.2/src/system.c
@@ -0,0 +1,36 @@
+#include <gfx.h>
+#include <sfx.h>
+#include <system.h>
+#include <SDL/SDL.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+void system_loop(void)
+{
+ SDL_PumpEvents();
+}
+
+void system_deinit(void)
+{
+ gfx_deinit();
+ sfx_deinit();
+ SDL_Quit();
+}
+
+int system_init(void)
+{
+ if (SDL_Init(0))
+ {
+ fprintf(stderr, "SDL_Init: %s\n", SDL_GetError());
+ goto failure;
+ }
+ else if (gfx_init() || sfx_init())
+ goto failure;
+
+ SDL_WM_SetCaption("rts", NULL);
+ return 0;
+
+failure:
+ system_deinit();
+ return -1;
+}
diff --git a/src/tech/CMakeLists.txt b/src/tech/CMakeLists.txt
new file mode 100644
index 0000000..1fd6963
--- /dev/null
+++ b/src/tech/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_library(tech INTERFACE)
+target_include_directories(tech INTERFACE "inc")
diff --git a/src/tech/inc/tech.h b/src/tech/inc/tech.h
new file mode 100644
index 0000000..071515f
--- /dev/null
+++ b/src/tech/inc/tech.h
@@ -0,0 +1,20 @@
+#ifndef TECH_H
+#define TECH_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+enum tech_level
+{
+ TECH_LEVEL_1,
+ TECH_LEVEL_2,
+ TECH_LEVEL_3
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TECH_H */
diff --git a/src/terrain/CMakeLists.txt b/src/terrain/CMakeLists.txt
new file mode 100644
index 0000000..c74de94
--- /dev/null
+++ b/src/terrain/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_library(terrain "src/terrain.c")
+target_include_directories(terrain PUBLIC "inc")
+target_link_libraries(terrain PUBLIC container camera gfx)
diff --git a/src/terrain/inc/terrain.h b/src/terrain/inc/terrain.h
new file mode 100644
index 0000000..9552555
--- /dev/null
+++ b/src/terrain/inc/terrain.h
@@ -0,0 +1,41 @@
+#ifndef TERRAIN_H
+#define TERRAIN_H
+
+#include <camera.h>
+#include <gfx.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+enum
+{
+ MAP_TILES = 64,
+ TERRAIN_SZ = 32,
+ MAP_X = MAP_TILES * TERRAIN_SZ,
+ MAP_Y = MAP_TILES * TERRAIN_SZ
+};
+
+enum terrain_type
+{
+ TERRAIN_TYPE_GRASS
+};
+
+struct terrain_map
+{
+ enum terrain_type m[MAP_TILES][MAP_TILES];
+ int nx, ny;
+};
+
+void terrain_init(struct terrain_map *map);
+int terrain_render(const struct terrain_map *map, const struct camera *cam);
+
+extern struct sprite grass_sprite;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TERRAIN_H */
diff --git a/src/terrain/src/terrain.c b/src/terrain/src/terrain.c
new file mode 100644
index 0000000..04df6e6
--- /dev/null
+++ b/src/terrain/src/terrain.c
@@ -0,0 +1,67 @@
+#include <terrain.h>
+#include <camera.h>
+#include <gfx.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct sprite grass_sprite;
+
+int terrain_render(const struct terrain_map *const map,
+ const struct camera *const cam)
+{
+ const int start_x = abs(cam->x / TERRAIN_SZ),
+ start_y = abs(cam->y / TERRAIN_SZ);
+
+ const int remx = cam->x % TERRAIN_SZ,
+ remy = cam->y % TERRAIN_SZ;
+
+ int nx = map->nx, ny = map->ny;
+
+ if (abs(remx) >= TERRAIN_SZ / 2)
+ nx++;
+
+ if (abs(remy) >= TERRAIN_SZ / 2)
+ ny++;
+
+ struct m
+ {
+ size_t i;
+ int p;
+ };
+
+ for (struct m x = {.i = start_x, .p = remx};
+ x.i < nx + start_x; x.i++, x.p += TERRAIN_SZ)
+ for (struct m y = {.i = start_y, .p = remy};
+ y.i < ny + start_y; y.i++, y.p += TERRAIN_SZ)
+ {
+ struct sprite *const s = sprite_get();
+
+ if (!s)
+ return -1;
+
+ switch (map->m[y.i][x.i])
+ {
+ case TERRAIN_TYPE_GRASS:
+ if (sprite_clone(&grass_sprite, s))
+ return -1;
+
+ break;
+ }
+
+ s->x = x.p;
+ s->y = y.p;
+ sprite_sort(s);
+ }
+
+ return 0;
+}
+
+void terrain_init(struct terrain_map *const map)
+{
+ const int extra = !!(screen_w % TERRAIN_SZ);
+
+ memset(map, 0, sizeof *map);
+ map->nx = screen_w / TERRAIN_SZ + extra;
+ map->ny = screen_h / TERRAIN_SZ + extra;
+}
diff --git a/src/unit/CMakeLists.txt b/src/unit/CMakeLists.txt
new file mode 100644
index 0000000..460cf43
--- /dev/null
+++ b/src/unit/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_library(unit "src/unit.c")
+target_include_directories(unit PUBLIC "inc")
+target_link_libraries(unit PUBLIC fixmath container camera gfx instance resource sfx tech util)
diff --git a/src/unit/inc/unit.h b/src/unit/inc/unit.h
new file mode 100644
index 0000000..4a5ace7
--- /dev/null
+++ b/src/unit/inc/unit.h
@@ -0,0 +1,134 @@
+#ifndef UNIT_H
+#define UNIT_H
+
+#include <camera.h>
+#include <gfx.h>
+#include <sfx.h>
+#include <instance.h>
+#include <resource_type.h>
+#include <tech.h>
+#include <unit_type.h>
+#include <util.h>
+#include <fixmath.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+struct unit_tech
+{
+ enum tech_level carry;
+};
+
+enum unit_state
+{
+ UNIT_STATE_IDLE_MOVING,
+ UNIT_STATE_SHELTERED,
+ UNIT_STATE_HARVESTING_WOOD,
+ UNIT_STATE_HARVESTING_GOLD,
+ UNIT_STATE_CARRYING,
+ UNIT_STATE_ATTACKING
+};
+
+struct unit_target
+{
+ struct instance *ins;
+ enum unit_state state;
+ instance_sheltered_cb shelter;
+ instance_attacked_cb attack;
+ instance_done_cb done;
+ void *op;
+};
+
+struct unit
+{
+ struct instance instance;
+ enum unit_type type;
+
+ enum unit_dir
+ {
+ UNIT_DIR_N,
+ UNIT_DIR_NE,
+ UNIT_DIR_E,
+ UNIT_DIR_SE,
+ UNIT_DIR_S,
+ UNIT_DIR_SW,
+ UNIT_DIR_W,
+ UNIT_DIR_NW,
+
+ MAX_UNIT_DIRECTIONS
+ } dir;
+
+ enum unit_state state;
+
+ struct
+ {
+ unsigned char t, i;
+ } frame;
+
+ union
+ {
+ struct unit_harvester
+ {
+ enum resource_type type;
+ unsigned char carry, t;
+ struct unit_target prev_target;
+ } harvester;
+ } us;
+
+ fix16_t rx, ry, tx, ty;
+
+ struct unit_target target;
+};
+
+UTIL_STATIC_ASSERT(!offsetof(struct unit, instance), "must be at offset zero");
+
+struct unit_cfg
+{
+ enum unit_type type;
+ unsigned long x, y;
+};
+
+void unit_create(const struct unit_cfg *cfg, struct unit *u);
+int unit_render(const struct unit *u, const struct camera *cam, bool sel);
+bool unit_can_harvest(const struct unit *u);
+bool unit_target_valid(const struct unit *u, const struct unit_target *t);
+void unit_set_target(struct unit *u, const struct unit_target *t);
+void unit_move_to(struct unit *u, unsigned long x, unsigned long y);
+bool unit_attacked(struct instance *, instance_hp ap);
+void unit_update(const struct unit_tech *t, struct unit *u);
+instance_hp unit_maxhp(const struct unit *u);
+const char *unit_str(const struct unit *u);
+
+enum
+{
+ UNIT_SPRITE_N,
+ UNIT_SPRITE_NE,
+ UNIT_SPRITE_E,
+ UNIT_SPRITE_SE,
+ UNIT_SPRITE_S,
+
+ MAX_UNIT_SPRIES
+};
+
+extern struct sprite unit_sprites[MAX_UNIT_SPRIES];
+
+enum unit_sound
+{
+ UNIT_SOUND_SELECTED,
+ UNIT_SOUND_MOVE,
+ UNIT_SOUND_MOVE_2,
+
+ MAX_UNIT_SOUNDS
+};
+
+extern struct sound unit_sounds[MAX_UNIT_SOUNDS];
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UNIT_H */
diff --git a/src/unit/inc/unit_type.h b/src/unit/inc/unit_type.h
new file mode 100644
index 0000000..fddd5c9
--- /dev/null
+++ b/src/unit/inc/unit_type.h
@@ -0,0 +1,11 @@
+#ifndef UNIT_TYPE_H
+#define UNIT_TYPE_H
+
+enum unit_type
+{
+ UNIT_TYPE_PEASANT,
+
+ MAX_UNIT_TYPES
+};
+
+#endif /* UNIT_TYPE_H */
diff --git a/src/unit/src/unit.c b/src/unit/src/unit.c
new file mode 100644
index 0000000..bb8e243
--- /dev/null
+++ b/src/unit/src/unit.c
@@ -0,0 +1,660 @@
+#include <unit.h>
+#include <camera.h>
+#include <gfx.h>
+#include <sfx.h>
+#include <fixmath.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct sprite unit_sprites[MAX_UNIT_SPRIES];
+struct sound unit_sounds[MAX_UNIT_SOUNDS];
+
+enum {N_FRAMES = 5};
+
+static void move_unit(struct unit *const u, const fix16_t sx, const fix16_t sy)
+{
+ switch (u->dir)
+ {
+ case UNIT_DIR_N:
+ u->ry -= sy;
+ break;
+
+ case UNIT_DIR_NE:
+ u->rx += sx;
+ u->ry -= sy;
+ break;
+
+ case UNIT_DIR_E:
+ u->rx += sx;
+ break;
+
+ case UNIT_DIR_SE:
+ u->rx += sx;
+ u->ry += sy;
+ break;
+
+ case UNIT_DIR_S:
+ u->ry += sy;
+ break;
+
+ case UNIT_DIR_SW:
+ u->rx -= sx;
+ u->ry += sy;
+ break;
+
+ case UNIT_DIR_W:
+ u->rx -= sx;
+ break;
+
+ case UNIT_DIR_NW:
+ u->rx -= sx;
+ u->ry -= sy;
+ break;
+
+ default:
+ break;
+ }
+}
+
+static void get_speed(const struct unit *const u, fix16_t *const x,
+ fix16_t *const y)
+{
+ static const struct speed
+ {
+ fix16_t x, y;
+ } speed[] =
+ {
+ [UNIT_TYPE_PEASANT] =
+ {
+ .x = FIX16_C_FROM_INT(1),
+ .y = FIX16_C_FROM_INT(1)
+ }
+ };
+
+ const struct speed *const s = &speed[u->type];
+ const int dx = abs(u->rx - u->tx);
+ const int dy = abs(u->ry - u->ty);
+
+ *x = dx < s->x ? dx : s->x;
+ *y = dy < s->y ? dy : s->y;
+}
+
+static enum unit_dir get_direction(const struct unit *const u)
+{
+ const fix16_t x = u->rx, y = u->ry, tx = u->tx, ty = u->ty;
+ enum unit_dir dir = 0;
+
+ if (x != tx && y != ty)
+ {
+ if (x > tx && y > ty)
+ dir = UNIT_DIR_NW;
+ else if (x < tx && y > ty)
+ dir = UNIT_DIR_NE;
+ else if (x < tx && y < ty)
+ dir = UNIT_DIR_SE;
+ else
+ dir = UNIT_DIR_SW;
+ }
+ else if (x != tx)
+ {
+ if (x > tx)
+ dir = UNIT_DIR_W;
+ else
+ dir = UNIT_DIR_E;
+ }
+ else if (y != ty)
+ {
+ if (y > ty)
+ dir = UNIT_DIR_N;
+ else
+ dir = UNIT_DIR_S;
+ }
+
+ return dir;
+}
+
+static instance_hp attack_points(const struct unit *const u)
+{
+ static instance_hp ap[] =
+ {
+ [UNIT_TYPE_PEASANT] = 1
+ };
+
+ return ap[u->type];
+}
+
+static void unit_stop(struct unit *const u)
+{
+ u->tx = u->rx;
+ u->ty = u->ry;
+}
+
+static void target_reset(struct unit *const u)
+{
+ memset(&u->target, 0, sizeof u->target);
+ u->state = UNIT_STATE_IDLE_MOVING;
+ unit_stop(u);
+}
+
+static void unit_chase(const struct instance *const i, struct unit *const u)
+{
+ u->tx = fix16_from_int(i->r.x + (i->r.w >> 1));
+ u->ty = fix16_from_int(i->r.y + (i->r.h >> 1));
+ u->state = UNIT_STATE_IDLE_MOVING;
+}
+
+static void target_interact(struct unit *const u)
+{
+ struct unit_target *const t = &u->target;
+ const struct instance *const ins = t->ins;
+
+ /* TODO: lose u->ti if not visible. */
+ if (ins->alive)
+ {
+ if (t->state != u->state)
+ {
+ struct instance *const ui = &u->instance;
+
+ if (util_collision(&ins->r, &ui->r))
+ {
+ switch (t->state)
+ {
+ case UNIT_STATE_CARRYING:
+ u->target.done(&u->instance, u->target.op);
+ break;
+
+ case UNIT_STATE_ATTACKING:
+ t->attack(t->ins, attack_points(u));
+ u->state = t->state;
+ break;
+
+ case UNIT_STATE_HARVESTING_WOOD:
+ {
+ struct unit_harvester *const uh = &u->us.harvester;
+
+ if (uh->type != RESOURCE_TYPE_WOOD)
+ uh->carry = 0;
+
+ uh->type = RESOURCE_TYPE_WOOD;
+ u->state = t->state;
+ unit_stop(u);
+ }
+ break;
+
+ case UNIT_STATE_HARVESTING_GOLD:
+ {
+ struct unit_harvester *const uh = &u->us.harvester;
+
+ if (uh->type != RESOURCE_TYPE_GOLD)
+ uh->carry = 0;
+
+ uh->type = RESOURCE_TYPE_GOLD;
+
+ if (t->shelter(t->ins, ui))
+ u->state = t->state;
+ else
+ unit_stop(u);
+ }
+ break;
+
+ case UNIT_STATE_SHELTERED:
+ u->state = t->state;
+ break;
+
+ default:
+ break;
+ }
+ }
+ else
+ unit_chase(ins, u);
+ }
+ }
+ else
+ target_reset(u);
+}
+
+static bool must_move(const struct unit *const u)
+{
+ return u->rx != u->tx || u->ry != u->ty;
+}
+
+static void update_harvest(const struct unit_tech *const t, struct unit *const u)
+{
+ static const unsigned char carry[] =
+ {
+ [TECH_LEVEL_1] = 10,
+ [TECH_LEVEL_2] = 15,
+ [TECH_LEVEL_3] = 25
+ };
+
+ static const char inc[] =
+ {
+ [TECH_LEVEL_1] = 10,
+ [TECH_LEVEL_2] = 50,
+ [TECH_LEVEL_3] = 25
+ };
+
+ struct unit_harvester *const uh = &u->us.harvester;
+ bool ret = false;
+
+ if (++uh->t >= inc[t->carry])
+ {
+ if (uh->carry < carry[t->carry])
+ {
+ ++uh->carry;
+ ret = u->target.attack(u->target.ins, 1);
+ }
+
+ uh->t = 0;
+ }
+
+ if (ret || uh->carry >= carry[t->carry])
+ {
+ u->state = UNIT_STATE_CARRYING;
+ u->target.done(&u->instance, u->target.op);
+ }
+}
+
+void unit_update(const struct unit_tech *const t, struct unit *const u)
+{
+ const struct instance *const i = &u->instance;
+
+ if (i->alive)
+ {
+ if (u->target.ins)
+ target_interact(u);
+
+ switch (u->state)
+ {
+ case UNIT_STATE_SHELTERED:
+ break;
+
+ case UNIT_STATE_HARVESTING_GOLD:
+ /* Fall through. */
+ case UNIT_STATE_HARVESTING_WOOD:
+ update_harvest(t, u);
+ break;
+
+ case UNIT_STATE_ATTACKING:
+ if (must_move(u))
+ u->state = UNIT_STATE_IDLE_MOVING;
+
+ break;
+
+ case UNIT_STATE_CARRYING:
+ /* Fall through. */
+ case UNIT_STATE_IDLE_MOVING:
+
+ if (must_move(u))
+ {
+ fix16_t x_step, y_step;
+
+ u->dir = get_direction(u);
+ get_speed(u, &x_step, &y_step);
+ move_unit(u, x_step, y_step);
+
+ enum {FRAME_RATE = 6};
+
+ if (++u->frame.t >= FRAME_RATE)
+ {
+ u->frame.t = 0;
+
+ if (++u->frame.i >= N_FRAMES)
+ u->frame.i = 0;
+ }
+
+ u->state = UNIT_STATE_IDLE_MOVING;
+ }
+ else
+ u->frame.i = 0;
+
+ u->instance.r.x = fix16_to_int(u->rx);
+ u->instance.r.y = fix16_to_int(u->ry);
+ break;
+ }
+ }
+}
+
+bool can_attack(const struct unit *const u)
+{
+ static const bool a[] =
+ {
+ [UNIT_TYPE_PEASANT] = true
+ };
+
+ return a[u->type];
+}
+
+static bool can_shelter(const struct unit *const u)
+{
+ static const bool s[] =
+ {
+ [UNIT_TYPE_PEASANT] = true
+ };
+
+ return s[u->type];
+}
+
+bool unit_can_harvest(const struct unit *const u)
+{
+ static const bool h[] =
+ {
+ [UNIT_TYPE_PEASANT] = true
+ };
+
+ return h[u->type];
+}
+
+bool unit_attacked(struct instance *const i, const instance_hp ap)
+{
+ return instance_attacked(i, ap);
+}
+
+bool unit_target_valid(const struct unit *const u,
+ const struct unit_target *const t)
+{
+ switch (t->state)
+ {
+ case UNIT_STATE_HARVESTING_GOLD:
+ /* Fall through. */
+ case UNIT_STATE_HARVESTING_WOOD:
+ return unit_can_harvest(u);
+
+ case UNIT_STATE_ATTACKING:
+ return can_attack(u);
+
+ case UNIT_STATE_SHELTERED:
+ if (t->shelter)
+ return can_shelter(u);
+ else
+ break;
+
+ case UNIT_STATE_IDLE_MOVING:
+ /* Fall through. */
+ case UNIT_STATE_CARRYING:
+ break;
+ }
+
+ return false;
+}
+
+void unit_set_target(struct unit *const u, const struct unit_target *const t)
+{
+ const struct instance *const ti = t->ins;
+
+ u->target = *t;
+ unit_chase(ti, u);
+}
+
+void unit_move_to(struct unit *const u, const unsigned long x, const unsigned long y)
+{
+ const struct instance *const i = &u->instance;
+ const unsigned long x_off = i->r.w >> 1;
+ const unsigned long y_off = i->r.h >> 1;
+
+ u->target.ins = NULL;
+ u->state = UNIT_STATE_IDLE_MOVING;
+ u->tx = x > x_off ? fix16_from_int(x - x_off) : 0;
+ u->ty = y > y_off ? fix16_from_int(y - y_off) : 0;
+}
+
+instance_hp unit_maxhp(const struct unit *const u)
+{
+ static const instance_hp hp[] =
+ {
+ [UNIT_TYPE_PEASANT] = 25
+ };
+
+ return hp[u->type];
+}
+
+static int get_ux(const struct unit *const u)
+{
+ switch (u->dir)
+ {
+ case UNIT_DIR_N:
+ return 0;
+
+ case UNIT_DIR_NE:
+ /* Fall through. */
+ case UNIT_DIR_NW:
+ return 1;
+
+ case UNIT_DIR_E:
+ /* Fall through. */
+ case UNIT_DIR_W:
+ return 2;
+
+ case UNIT_DIR_SE:
+ /* Fall through. */
+ case UNIT_DIR_SW:
+ return 3;
+
+ case UNIT_DIR_S:
+ return 4;
+
+ default:
+ break;
+ }
+
+ return -1;
+}
+
+typedef const struct
+{
+ const struct sprite *s;
+ char xo, x[N_FRAMES], w[N_FRAMES];
+ short y;
+ short h;
+} anim_dim;
+
+static anim_dim *peasant_anim(const struct unit *const u)
+{
+ static anim_dim t[] =
+ {
+ {
+ .s = &unit_sprites[UNIT_SPRITE_N],
+ .xo = 5,
+ .x = {0, 1, 1, 2, 1},
+ .w = {25, 22, 24, 22, 24},
+ .y = 2,
+ .h = 31
+ },
+
+ {
+ .s = &unit_sprites[UNIT_SPRITE_NE],
+ .xo = 11,
+ .x = {0, -4, -1, -4, -2},
+ .w = {18, 26, 22, 23, 20},
+ .y = 2,
+ .h = 31
+ },
+
+ {
+ .s = &unit_sprites[UNIT_SPRITE_E],
+ .xo = 10,
+ .x = {0, -6, -1, -6, -3},
+ .w = {14, 26, 17, 24, 19},
+ .y = 2,
+ .h = 33
+ },
+
+ {
+ .s = &unit_sprites[UNIT_SPRITE_SE],
+ .xo = 6,
+ .x = {0, 1, 2, 0, 0},
+ .w = {20, 22, 18, 21, 21},
+ .y = 2,
+ .h = 31
+ },
+
+ {
+ .s = &unit_sprites[UNIT_SPRITE_S],
+ .xo = 7,
+ .x = {0, 1, 0, 0, 0},
+ .w = {24, 23, 24, 23, 24},
+ .y = 2,
+ .h = 33
+ }
+ };
+
+ const int ux = get_ux(u);
+
+ if (ux < 0)
+ return NULL;
+
+ return &t[ux];
+}
+
+struct render_cfg
+{
+ struct instance_render_off off;
+ struct instance_render_quad qcfg;
+};
+
+static anim_dim *peasant_quad(const struct unit *const u)
+{
+ return peasant_anim(u);
+}
+
+static void adjust_quad(const struct unit *const u, anim_dim *const dim,
+ struct render_cfg *const rcfg)
+{
+ const unsigned char n = u->frame.i;
+ short u_off = 0;
+
+ for (unsigned char i = 0; i < n; i++)
+ u_off += dim->w[i];
+
+ struct instance_render_quad *const qcfg = &rcfg->qcfg;
+
+ qcfg->u = u_off;
+ qcfg->w = dim->w[n];
+ qcfg->h = dim->h;
+
+ struct instance_render_off *const off = &rcfg->off;
+
+ off->x = dim->xo;
+ off->y = dim->y;
+
+ switch (u->dir)
+ {
+ case UNIT_DIR_SW:
+ /* Fall through. */
+ case UNIT_DIR_W:
+ /* Fall through. */
+ case UNIT_DIR_NW:
+ qcfg->xflip = true;
+ off->x += dim->x[n];
+ break;
+
+ default:
+ qcfg->xflip = false;
+ off->x += dim->x[n];
+ break;
+ }
+}
+
+static int unit_quad(const struct unit *const u, struct render_cfg *const rcfg)
+{
+ struct instance_render_quad *const qcfg = &rcfg->qcfg;
+
+ qcfg->q = quad_get();
+
+ if (!qcfg->q)
+ return -1;
+
+ static anim_dim *(*const f[])(const struct unit *) =
+ {
+ [UNIT_TYPE_PEASANT] = peasant_quad
+ };
+
+ anim_dim *const dim = f[u->type](u);
+
+ if (!dim)
+ return -1;
+
+ adjust_quad(u, dim, rcfg);
+
+ if (quad_from_sprite(dim->s, qcfg->q))
+ return -1;
+
+ return 0;
+}
+
+int unit_render(const struct unit *const u, const struct camera *const cam,
+ const bool sel)
+{
+ if (!u->instance.alive
+ || u->state == UNIT_STATE_SHELTERED
+ || u->state == UNIT_STATE_HARVESTING_GOLD)
+ return 0;
+
+ struct render_cfg rcfg;
+
+ if (unit_quad(u, &rcfg))
+ return -1;
+
+ const struct instance_render_cfg cfg =
+ {
+ .i = &u->instance,
+ .prim_type = INSTANCE_RENDER_CFG_QUAD,
+ .prim = {.quad = &rcfg.qcfg},
+ .cam = cam,
+ .sel = sel,
+ .max_hp = unit_maxhp(u),
+ .off = &rcfg.off
+ };
+
+ return instance_render(&cfg);
+}
+
+static void get_dimensions(const enum unit_type type, short *const w,
+ short *const h)
+{
+ static const struct dim
+ {
+ short w, h;
+ } dim[] =
+ {
+ [UNIT_TYPE_PEASANT] = {.w = 36, .h = 36}
+ };
+
+ const struct dim *const d = &dim[type];
+ *w = d->w;
+ *h = d->h;
+}
+
+void unit_create(const struct unit_cfg *const cfg, struct unit *const u)
+{
+ struct instance *const i = &u->instance;
+
+ *u = (const struct unit)
+ {
+ .instance =
+ {
+ .alive = true,
+ .hp = unit_maxhp(u)
+ },
+
+ .type = cfg->type,
+ .dir = UNIT_DIR_S,
+ .rx = fix16_from_int(cfg->x),
+ .ry = fix16_from_int(cfg->y)
+ };
+
+ get_dimensions(cfg->type, &i->r.w, &i->r.h);
+ unit_stop(u);
+}
+
+const char *unit_str(const struct unit *const u)
+{
+ static const char *const str[] =
+ {
+ [UNIT_TYPE_PEASANT] = "Peasant"
+ };
+
+ return str[u->type];
+}
diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt
new file mode 100644
index 0000000..9c808f5
--- /dev/null
+++ b/src/util/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_library(util "src/util.c")
+target_include_directories(util PUBLIC "inc")
diff --git a/src/util/inc/util.h b/src/util/inc/util.h
new file mode 100644
index 0000000..e42c5b2
--- /dev/null
+++ b/src/util/inc/util.h
@@ -0,0 +1,31 @@
+#ifndef UTIL_H
+#define UTIL_H
+
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#if __STDC_VERSION__ >= 201112L
+#define UTIL_STATIC_ASSERT(exp, msg) _Static_assert(exp, msg)
+#else
+#define UTIL_STATIC_ASSERT__(exp, l, msg) enum {static_assert##l##__ = 1 / !!(exp)}
+#define UTIL_STATIC_ASSERT_(exp, l, msg) UTIL_STATIC_ASSERT__(exp, l, msg)
+#define UTIL_STATIC_ASSERT(exp, msg) UTIL_STATIC_ASSERT_(exp, __LINE__, msg)
+#endif
+
+struct util_rect
+{
+ unsigned long x, y;
+ short w, h;
+};
+
+bool util_collision(const struct util_rect *a, const struct util_rect *b);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UTIL_H */
diff --git a/src/util/src/util.c b/src/util/src/util.c
new file mode 100644
index 0000000..f87c14a
--- /dev/null
+++ b/src/util/src/util.c
@@ -0,0 +1,10 @@
+#include <util.h>
+#include <stdbool.h>
+
+bool util_collision(const struct util_rect *const a, const struct util_rect *const b)
+{
+ return !((a->x >= b->x + b->w)
+ || (b->x >= a->x + a->w)
+ || (a->y >= b->y + b->h)
+ || (b->y >= a->y + a->h));
+}