phantasia

Phantasia - 2D SDL3 RPG prototype.
git clone git://git.beep.wimdupont.com/phantasia.git
Log | Files | Refs | README | LICENSE

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:
MMakefile | 20+++++++++++++++++---
MREADME.adoc | 12++++++++++++
Adata/maps/ashen-meadow.txt | 26++++++++++++++++++++++++++
Msrc/engine/world.c | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/engine/world.h | 8+++++---
Msrc/game/main.c | 83++++++++++++++++++++++++++++++-------------------------------------------------
Asrc/tools/mapc.c | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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]); +}