commit 76344983a86970e742dac6dbc7ac6645f5fc26a9
parent 804d1b2c51e6396f7345e0d7991e8886ad94958d
Author: beep <beep@wimdupont.com>
Date: Sat, 4 Apr 2026 16:38:35 +0000
Load maps from compiled text assets
Diffstat:
7 files changed, 324 insertions(+), 58 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,10 +1,16 @@
BIN = bin/phantasia
+MAPC = bin/ph-mapc
OBJDIR = obj
+MAPDIR = $(OBJDIR)/maps
+MAPSRC = data/maps/ashen-meadow.txt
+MAPBIN = $(MAPSRC:data/maps/%.txt=$(MAPDIR)/%.phmap)
SRC = \
src/engine/world.c \
src/game/main.c
+MAPC_SRC = src/tools/mapc.c
+
OBJ = $(SRC:src/%.c=$(OBJDIR)/%.o)
DEP = $(OBJ:.o=.d)
@@ -25,16 +31,24 @@ endif
.PHONY: all clean render-smoke smoke
-all: $(BIN)
+all: $(BIN) $(MAPBIN)
-$(BIN): $(OBJ)
+$(BIN): $(OBJ) $(MAPBIN)
@mkdir -p $(dir $@)
$(CC) $(OBJ) -o $@ $(LDLIBS)
+$(MAPC): $(MAPC_SRC)
+ @mkdir -p $(dir $@)
+ $(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@
+
$(OBJDIR)/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
+$(MAPDIR)/%.phmap: data/maps/%.txt $(MAPC)
+ @mkdir -p $(dir $@)
+ $(MAPC) $< $@
+
smoke: $(BIN)
$(BIN) --smoke-test
@@ -42,6 +56,6 @@ render-smoke: $(BIN)
SDL_VIDEODRIVER=dummy $(BIN) --render-smoke-test
clean:
- rm -rf $(BIN) $(OBJDIR)
+ rm -rf bin $(OBJDIR)
-include $(DEP)
diff --git a/README.adoc b/README.adoc
@@ -37,3 +37,15 @@ With SDL3 available, run the game normally:
----
./bin/phantasia
----
+
+== Maps
+
+Map source files live in `data/maps/*.txt`.
+
+`make` compiles them into binary `obj/maps/*.phmap` files with `bin/ph-mapc`,
+and the game loads those binary map files at runtime.
+
+Current tile legend:
+
+* `.` = walkable ground
+* `#` = blocked wall
diff --git a/data/maps/ashen-meadow.txt b/data/maps/ashen-meadow.txt
@@ -0,0 +1,26 @@
+name: Ashen Meadow
+
+################################
+#..............................#
+#..............................#
+#......####....................#
+#......#..#....................#
+#......####............###.....#
+#......................#.......#
+#......................#.......#
+#......................###.....#
+#..............................#
+#............#####.............#
+#............#...#.............#
+#............#...#.............#
+#............#####.............#
+#..............................#
+#..............................#
+#....###.......................#
+#......#.......................#
+#....###.......................#
+#..............................#
+#..............................#
+#..............................#
+#..............................#
+################################
diff --git a/src/engine/world.c b/src/engine/world.c
@@ -1,8 +1,25 @@
#include "engine/world.h"
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
#include <math.h>
#include <string.h>
+enum {
+ PH_MAP_MAGIC = 0x50484d50,
+ PH_MAP_VERSION = 1,
+ PH_AREA_NAME_MAX = 64,
+};
+
+typedef struct {
+ uint32_t magic;
+ uint32_t version;
+ uint32_t width;
+ uint32_t height;
+ char name[PH_AREA_NAME_MAX];
+} PhMapHeader;
+
static PhVec2
ph_vec2(float x, float y)
{
@@ -129,6 +146,73 @@ ph_world_init(PhWorld *world, PhArea area, float viewport_w, float viewport_h)
}
int
+ph_area_load(PhArea *area, const char *path)
+{
+ FILE *fp;
+ PhMapHeader header;
+ size_t tile_count;
+ size_t name_len;
+ unsigned char *tiles;
+ char *name;
+
+ memset(area, 0, sizeof(*area));
+
+ fp = fopen(path, "rb");
+ if (!fp) {
+ return -1;
+ }
+
+ if (fread(&header, sizeof(header), 1, fp) != 1 ||
+ header.magic != PH_MAP_MAGIC ||
+ header.version != PH_MAP_VERSION ||
+ header.width == 0 ||
+ header.height == 0) {
+ fclose(fp);
+ return -1;
+ }
+
+ tile_count = (size_t)header.width * (size_t)header.height;
+ tiles = malloc(tile_count);
+ if (!tiles) {
+ fclose(fp);
+ return -1;
+ }
+
+ if (fread(tiles, 1, tile_count, fp) != tile_count) {
+ free(tiles);
+ fclose(fp);
+ return -1;
+ }
+ fclose(fp);
+
+ header.name[PH_AREA_NAME_MAX - 1] = '\0';
+ name_len = strlen(header.name) + 1;
+ name = malloc(name_len);
+ if (!name) {
+ free(tiles);
+ return -1;
+ }
+ memcpy(name, header.name, name_len);
+
+ area->name = name;
+ area->width = (int)header.width;
+ area->height = (int)header.height;
+ area->tiles = tiles;
+ area->owns_tiles = 1;
+ return 0;
+}
+
+void
+ph_area_free(PhArea *area)
+{
+ if (area->owns_tiles) {
+ free(area->tiles);
+ free((char *)area->name);
+ }
+ memset(area, 0, sizeof(*area));
+}
+
+int
ph_world_add_entity_def(PhWorld *world, PhEntityDef def)
{
if (world->entity_def_count >= PH_MAX_ENTITY_TYPES) {
diff --git a/src/engine/world.h b/src/engine/world.h
@@ -4,8 +4,6 @@
#include <stddef.h>
enum {
- PH_AREA_TILES_W = 32,
- PH_AREA_TILES_H = 24,
PH_TILE_SIZE = 16,
PH_MAX_ENTITY_TYPES = 32,
PH_MAX_ITEM_TYPES = 64,
@@ -62,7 +60,8 @@ typedef struct {
const char *name;
int width;
int height;
- const unsigned char *tiles;
+ unsigned char *tiles;
+ int owns_tiles;
} PhArea;
typedef struct {
@@ -92,6 +91,9 @@ typedef struct {
int interact;
} PhInput;
+int ph_area_load(PhArea *area, const char *path);
+void ph_area_free(PhArea *area);
+
void ph_world_init(PhWorld *world, PhArea area, float viewport_w, float viewport_h);
int ph_world_add_entity_def(PhWorld *world, PhEntityDef def);
int ph_world_add_item_def(PhWorld *world, PhItemDef def);
diff --git a/src/game/main.c b/src/game/main.c
@@ -23,53 +23,21 @@ enum {
PH_ITEM_TALENT_SHARD = 1,
};
-static const unsigned char PH_MEADOW_TILES[] =
- "################################"
- "#..............................#"
- "#..............................#"
- "#......####....................#"
- "#......#..#....................#"
- "#......####............###.....#"
- "#......................#.......#"
- "#......................#.......#"
- "#......................###.....#"
- "#..............................#"
- "#............#####.............#"
- "#............#...#.............#"
- "#............#...#.............#"
- "#............#####.............#"
- "#..............................#"
- "#..............................#"
- "#....###.......................#"
- "#......#.......................#"
- "#....###.......................#"
- "#..............................#"
- "#..............................#"
- "#..............................#"
- "#..............................#"
- "################################";
-
-static PhArea
-ph_make_start_area(void)
-{
- PhArea area;
-
- area.name = "Ashen Meadow";
- area.width = PH_AREA_TILES_W;
- area.height = PH_AREA_TILES_H;
- area.tiles = PH_MEADOW_TILES;
-
- return area;
-}
+#define PH_START_MAP_PATH "obj/maps/ashen-meadow.phmap"
-static PhWorld
-ph_make_world(void)
+static int
+ph_make_world(PhWorld *world)
{
- PhWorld world;
+ PhArea area;
int player;
- ph_world_init(&world, ph_make_start_area(), (float)PH_VIEW_W, (float)PH_VIEW_H);
- ph_world_add_entity_def(&world, (PhEntityDef){
+ if (ph_area_load(&area, PH_START_MAP_PATH) < 0) {
+ fprintf(stderr, "failed to load %s\n", PH_START_MAP_PATH);
+ return -1;
+ }
+
+ ph_world_init(world, area, (float)PH_VIEW_W, (float)PH_VIEW_H);
+ ph_world_add_entity_def(world, (PhEntityDef){
.id = PH_DEF_PLAYER,
.name = "Wayfarer",
.max_hp = 40,
@@ -78,7 +46,7 @@ ph_make_world(void)
.sprite_tile_y = 0,
.kind = PH_ENTITY_PLAYER,
});
- ph_world_add_entity_def(&world, (PhEntityDef){
+ ph_world_add_entity_def(world, (PhEntityDef){
.id = PH_DEF_SLIME,
.name = "Mire Slime",
.max_hp = 12,
@@ -87,7 +55,7 @@ ph_make_world(void)
.sprite_tile_y = 0,
.kind = PH_ENTITY_MONSTER,
});
- ph_world_add_item_def(&world, (PhItemDef){
+ ph_world_add_item_def(world, (PhItemDef){
.id = PH_ITEM_TALENT_SHARD,
.name = "Talent Shard",
.value = 30,
@@ -95,22 +63,26 @@ ph_make_world(void)
.talent_points = 1,
});
- player = ph_world_spawn_entity(&world, PH_DEF_PLAYER, (PhVec2){ 80.0f, 80.0f });
- ph_world_spawn_entity(&world, PH_DEF_SLIME, (PhVec2){ 180.0f, 120.0f });
- ph_world_drop_item(&world, PH_ITEM_TALENT_SHARD, (PhVec2){ 104.0f, 80.0f }, 1);
- ph_world_set_player(&world, player);
+ player = ph_world_spawn_entity(world, PH_DEF_PLAYER, (PhVec2){ 80.0f, 80.0f });
+ ph_world_spawn_entity(world, PH_DEF_SLIME, (PhVec2){ 180.0f, 120.0f });
+ ph_world_drop_item(world, PH_ITEM_TALENT_SHARD, (PhVec2){ 104.0f, 80.0f }, 1);
+ ph_world_set_player(world, player);
- return world;
+ return 0;
}
static int
ph_run_smoke_test(void)
{
- PhWorld world = ph_make_world();
+ PhWorld world;
PhInput input = { .move_x = 1, .move_y = 0, .interact = 0 };
const PhEntity *player;
int i;
+ if (ph_make_world(&world) < 0) {
+ return 1;
+ }
+
for (i = 0; i < 30; ++i) {
input.interact = i == 20;
ph_world_tick(&world, input, 1.0f / 60.0f);
@@ -129,6 +101,7 @@ ph_run_smoke_test(void)
world.camera.pos.x,
world.camera.pos.y,
world.player_talent_points);
+ ph_area_free(&world.area);
return 0;
}
@@ -240,7 +213,7 @@ ph_run_game(int frame_limit)
{
SDL_Window *window;
SDL_Renderer *renderer;
- PhWorld world = ph_make_world();
+ PhWorld world;
Uint64 last_ticks;
int frame_count = 0;
int running = 1;
@@ -250,6 +223,11 @@ ph_run_game(int frame_limit)
return 1;
}
+ if (ph_make_world(&world) < 0) {
+ SDL_Quit();
+ return 1;
+ }
+
if (!SDL_CreateWindowAndRenderer("phantasia",
PH_VIEW_W * PH_SCALE,
PH_VIEW_H * PH_SCALE,
@@ -304,6 +282,7 @@ ph_run_game(int frame_limit)
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
+ ph_area_free(&world.area);
SDL_Quit();
return 0;
}
diff --git a/src/tools/mapc.c b/src/tools/mapc.c
@@ -0,0 +1,149 @@
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+enum {
+ PH_MAP_MAGIC = 0x50484d50,
+ PH_MAP_VERSION = 1,
+ PH_AREA_NAME_MAX = 64,
+ PH_MAP_LINE_MAX = 512,
+};
+
+typedef struct {
+ uint32_t magic;
+ uint32_t version;
+ uint32_t width;
+ uint32_t height;
+ char name[PH_AREA_NAME_MAX];
+} PhMapHeader;
+
+static void
+ph_strip_newline(char *line)
+{
+ size_t len = strlen(line);
+
+ while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
+ line[--len] = '\0';
+ }
+}
+
+static int
+ph_compile_map(const char *src_path, const char *dst_path)
+{
+ FILE *src;
+ FILE *dst;
+ PhMapHeader header = {
+ .magic = PH_MAP_MAGIC,
+ .version = PH_MAP_VERSION,
+ };
+ char line[PH_MAP_LINE_MAX];
+ unsigned char *tiles = NULL;
+ size_t tile_cap = 0;
+ size_t tile_count = 0;
+ int in_tiles = 0;
+
+ src = fopen(src_path, "r");
+ if (!src) {
+ perror(src_path);
+ return 1;
+ }
+
+ while (fgets(line, sizeof(line), src)) {
+ size_t len;
+
+ ph_strip_newline(line);
+ if (!in_tiles && strncmp(line, "name:", 5) == 0) {
+ const char *name = line + 5;
+ size_t name_len;
+
+ while (*name == ' ' || *name == '\t') {
+ ++name;
+ }
+ name_len = strlen(name);
+ if (name_len >= sizeof(header.name)) {
+ fprintf(stderr, "%s: map name too long\n", src_path);
+ free(tiles);
+ fclose(src);
+ return 1;
+ }
+ memcpy(header.name, name, name_len + 1);
+ continue;
+ }
+ if (!in_tiles && line[0] == '\0') {
+ in_tiles = 1;
+ continue;
+ }
+ if (line[0] == '\0') {
+ continue;
+ }
+
+ len = strlen(line);
+ if (header.width == 0) {
+ header.width = (uint32_t)len;
+ } else if (len != header.width) {
+ fprintf(stderr, "%s: inconsistent row width: got %zu, expected %u\n",
+ src_path, len, header.width);
+ free(tiles);
+ fclose(src);
+ return 1;
+ }
+
+ if (tile_count + len > tile_cap) {
+ size_t new_cap = tile_cap ? tile_cap * 2 : 256;
+ unsigned char *new_tiles;
+
+ while (new_cap < tile_count + len) {
+ new_cap *= 2;
+ }
+ new_tiles = realloc(tiles, new_cap);
+ if (!new_tiles) {
+ free(tiles);
+ fclose(src);
+ return 1;
+ }
+ tiles = new_tiles;
+ tile_cap = new_cap;
+ }
+
+ memcpy(tiles + tile_count, line, len);
+ tile_count += len;
+ ++header.height;
+ }
+ fclose(src);
+
+ if (header.width == 0 || header.height == 0 || header.name[0] == '\0') {
+ fprintf(stderr, "%s: missing name or tile data\n", src_path);
+ free(tiles);
+ return 1;
+ }
+
+ dst = fopen(dst_path, "wb");
+ if (!dst) {
+ perror(dst_path);
+ free(tiles);
+ return 1;
+ }
+
+ if (fwrite(&header, sizeof(header), 1, dst) != 1 ||
+ fwrite(tiles, 1, tile_count, dst) != tile_count) {
+ perror(dst_path);
+ free(tiles);
+ fclose(dst);
+ return 1;
+ }
+
+ free(tiles);
+ fclose(dst);
+ return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+ if (argc != 3) {
+ fprintf(stderr, "usage: %s <map.txt> <map.phmap>\n", argv[0]);
+ return 1;
+ }
+ return ph_compile_map(argv[1], argv[2]);
+}