From 64a416a3fa0b5097afe723557ca71fc99606d3d9 Mon Sep 17 00:00:00 2001 From: KonAki Date: Sun, 11 Oct 2026 04:51:06 +0800 Subject: add sector module: room maps in a text format with entities --- README.md | 1 + build.sh | 4 +- examples/maps/house.kmap | 28 +++ kon/konsector.c | 636 +++++++++++++++++++++++++++++++++++++++++++++++ kon/konsector.h | 133 ++++++++++ 5 files changed, 800 insertions(+), 2 deletions(-) create mode 100644 examples/maps/house.kmap create mode 100644 kon/konsector.c create mode 100644 kon/konsector.h diff --git a/README.md b/README.md index 2c4082d..a6e20a8 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ The engine modules are a `.h` and a `.c` pair, they depend on each other a bit, | toml | kontoml | | tile | tilemaps and sprite sheets | | 3d | first person kit: mouse look body, walls from a tilemap, editable hill terrain (adds tile) | +| sector | maps made of rooms (vertices, floors, ceilings, walls) with entities like doors and buttons, in a text format | | pack | encrypted asset packs | | assets | named asset cache with reference counts, loose files or a pack | | image | stb_image so assets can load pngs | diff --git a/build.sh b/build.sh index 08749a9..b792931 100755 --- a/build.sh +++ b/build.sh @@ -12,7 +12,7 @@ developer=0 usage() { echo "usage: build.sh [-d] [-D] [-p platform] [-m module,...] [-o objdir] [target ...]" - echo "modules: font toml tile pack ui debug assets image audio mic model 3d" + echo "modules: font toml tile pack ui debug assets image audio mic model 3d sector" echo "platforms: native linux bsd windows" echo "targets: all run clean tools pack examples" exit 1 @@ -77,7 +77,7 @@ addobj() for m in $modules; do DEFS="$DEFS -DKON_MODULE_$(echo "$m" | tr 'a-z' 'A-Z')" case $m in - font|toml|tile|pack|ui|debug|3d) addobj "kon$m" ;; + font|toml|tile|pack|ui|debug|3d|sector) addobj "kon$m" ;; assets) addobj konassets ;; model) addobj konmodel; addobj konassets ;; image) addobj stb_image ;; diff --git a/examples/maps/house.kmap b/examples/maps/house.kmap new file mode 100644 index 0000000..10fc39a --- /dev/null +++ b/examples/maps/house.kmap @@ -0,0 +1,28 @@ +kmap 1 + +# the plan seen from above: x goes east and z goes south. corners go counter clockwise, +# so with north up you walk down the west wall first + +v 0 0 +v 0 4 +v 8 4 +v 8 0 +v 0 -4 +v 8 -4 +v 14 4 +v 14 2 +v 11 2 +v 11 0 + +# floor ceiling, then settings, then a colon and the corners +sector 0 3 light 0.8 floor tile ceiling plaster wall brick : 0 1 2 3 +sector 0.25 3 light 0.5 floor tile ceiling plaster wall brick : 4 0 3 5 +sector 0 2.6 light 0.3 floor tile ceiling plaster wall brick : 3 2 6 7 8 9 + +edge 1 2 wallpaper + +# kind x y z angle, then name= and target= and anything else you like +entity spawn 4 1.6 2 0 +entity light 4 2.6 2 0 color=ffe0a0 radius=8 +entity button 7.8 1.2 1 270 target=door1 +entity door 8 0 2 90 name=door1 speed=2 diff --git a/kon/konsector.c b/kon/konsector.c new file mode 100644 index 0000000..5ae2968 --- /dev/null +++ b/kon/konsector.c @@ -0,0 +1,636 @@ +/* SPDX-License-Identifier: MIT */ +/* + * konsector.c + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "konsector.h" + +#include +#include +#include +#include +#include + +#define WORD_LENGTH 128 +#define MAX_FILE (16 * 1024 * 1024) + +/*** private ***/ + +typedef struct kon_builder { + kon_sectorMap_t *map; + int vertexCap, sectorCap, lineCap, indexCap, edgeCap, entityCap, textureCap; + int *sectorLine; /* the line each sector was written on, for the errors found afterwards */ + int line; + char *error; + size_t errorSize; +} kon_builder_t; + +static int kon_fail_(kon_builder_t *b, int line, const char *format, ...) +{ + va_list args; + int used = 0; + + if (b->error && b->errorSize > 0) { + if (line > 0) used = snprintf(b->error, b->errorSize, "line %d: ", line); + if (used < 0 || (size_t)used >= b->errorSize) used = 0; + va_start(args, format); + vsnprintf(b->error + used, b->errorSize - (size_t)used, format, args); + va_end(args); + } + return 0; +} + +static int kon_grow_(void **array, int *capacity, size_t size, int needed) +{ + void *bigger; + int cap = *capacity; + + if (needed <= cap) return 1; + if (cap < 8) cap = 8; + while (cap < needed) cap *= 2; + bigger = realloc(*array, (size_t)cap * size); + if (!bigger) return 0; + *array = bigger; + *capacity = cap; + return 1; +} + +/* the next word of a line, NULL at the end of it. the line ends at a newline or a #. the word is copied into out */ +static const char *kon_word_(const char *p, char *out, size_t size) +{ + size_t n = 0; + + while (*p == ' ' || *p == '\t' || *p == '\r') p++; + if (*p == '\0' || *p == '\n' || *p == '#') return NULL; + while (*p != '\0' && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && *p != '#') { + if (n + 1 < size) out[n++] = *p; + p++; + } + out[n] = '\0'; + return p; +} + +static int kon_number_(const char *word, double *value) +{ + char *end; + + if (*word == '\0') return 0; + *value = strtod(word, &end); + return *end == '\0' && isfinite(*value); +} + +static int kon_texture_(kon_builder_t *b, const char *name, int *index) +{ + kon_sectorMap_t *map = b->map; + int i; + + if (strcmp(name, "-") == 0) { + *index = -1; + return 1; + } + for (i = 0; i < map->textureCount; i++) { + if (strcmp(map->textures[i], name) == 0) { + *index = i; + return 1; + } + } + if (strlen(name) >= KON_NAME_LENGTH) return 0; + if (!kon_grow_((void **)&map->textures, &b->textureCap, sizeof(map->textures[0]), map->textureCount + 1)) return 0; + strcpy(map->textures[map->textureCount], name); + *index = map->textureCount++; + return 1; +} + +static double kon_area_(const kon_sectorMap_t *map, const kon_sector_t *sector) +{ + double sum = 0.0; + int i; + + for (i = 0; i < sector->count; i++) { + kon_vec2_t a = map->vertices[map->indices[sector->first + i]]; + kon_vec2_t b = map->vertices[map->indices[sector->first + (i + 1) % sector->count]]; + + sum += (double)a.x * (double)b.y - (double)b.x * (double)a.y; + } + return sum * 0.5; +} + +static double kon_side_(kon_vec2_t a, kon_vec2_t b, kon_vec2_t p) +{ + return ((double)b.x - (double)a.x) * ((double)p.y - (double)a.y) - ((double)b.y - (double)a.y) * ((double)p.x - (double)a.x); +} + +static int kon_onSegment_(kon_vec2_t a, kon_vec2_t b, kon_vec2_t p) +{ + return p.x >= (a.x < b.x ? a.x : b.x) && p.x <= (a.x > b.x ? a.x : b.x) && p.y >= (a.y < b.y ? a.y : b.y) && p.y <= (a.y > b.y ? a.y : b.y); +} + +/* touching counts as crossing */ +static int kon_crossing_(kon_vec2_t a, kon_vec2_t b, kon_vec2_t c, kon_vec2_t d) +{ + double d1 = kon_side_(a, b, c), d2 = kon_side_(a, b, d), d3 = kon_side_(c, d, a), d4 = kon_side_(c, d, b); + + if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) return 1; + if (d1 == 0 && kon_onSegment_(a, b, c)) return 1; + if (d2 == 0 && kon_onSegment_(a, b, d)) return 1; + if (d3 == 0 && kon_onSegment_(c, d, a)) return 1; + if (d4 == 0 && kon_onSegment_(c, d, b)) return 1; + return 0; +} + +static int kon_checkSector_(kon_builder_t *b, int s) +{ + kon_sectorMap_t *map = b->map; + kon_sector_t *sector = &map->sectors[s]; + int line = b->sectorLine[s], i, j; + double area; + + if (sector->count < 3) return kon_fail_(b, line, "sector %d needs at least 3 corners", s); + if (!(sector->ceiling > sector->floor)) return kon_fail_(b, line, "sector %d has a ceiling that is not above its floor", s); + + for (i = 0; i < sector->count; i++) { + kon_vec2_t p = kon_sectorCorner(map, s, i), q = kon_sectorCorner(map, s, i + 1); + + if (p.x == q.x && p.y == q.y) return kon_fail_(b, line, "sector %d has an edge with no length at corner %d", s, i); + } + + for (i = 0; i < sector->count; i++) { + for (j = i + 2; j < sector->count; j++) { + if (i == 0 && j == sector->count - 1) continue; + if (kon_crossing_(kon_sectorCorner(map, s, i), kon_sectorCorner(map, s, i + 1), kon_sectorCorner(map, s, j), kon_sectorCorner(map, s, j + 1))) { + return kon_fail_(b, line, "sector %d crosses itself, edges %d and %d touch", s, i, j); + } + } + } + + area = kon_area_(map, sector); + if (area == 0.0) return kon_fail_(b, line, "sector %d has no area", s); + + /* the shoelace sum is positive going clockwise on a plan with z pointing down, the format wants the other way round */ + if (area > 0.0) { + int *c = &map->indices[sector->first]; + kon_sectorEdge_t *e = &map->edges[sector->first]; + + for (i = 0, j = sector->count - 1; i < j; i++, j--) { + int t = c[i]; + + c[i] = c[j]; + c[j] = t; + } + /* the corners are reversed, so edge n now runs along old edge count - 2 - n backwards. the closing edge stays the closing edge */ + for (i = 0, j = sector->count - 2; i < j; i++, j--) { + kon_sectorEdge_t t = e[i]; + + e[i] = e[j]; + e[j] = t; + } + } + return 1; +} + +static int kon_samePoint_(kon_vec2_t a, kon_vec2_t b) +{ + return a.x == b.x && a.y == b.y; +} + +static int kon_linkPortals_(kon_builder_t *b) +{ + kon_sectorMap_t *map = b->map; + int s, t, i, j; + + for (s = 0; s < map->sectorCount; s++) { + for (i = 0; i < map->sectors[s].count; i++) { + kon_vec2_t p = kon_sectorCorner(map, s, i), q = kon_sectorCorner(map, s, i + 1); + + map->edges[map->sectors[s].first + i].portal = -1; + for (t = 0; t < map->sectorCount; t++) { + if (t == s) continue; + for (j = 0; j < map->sectors[t].count; j++) { + kon_vec2_t u = kon_sectorCorner(map, t, j), v = kon_sectorCorner(map, t, j + 1); + + if (kon_samePoint_(p, u) && kon_samePoint_(q, v)) { + return kon_fail_(b, b->sectorLine[s], "sectors %d and %d overlap along an edge, they run the same way", s, t); + } + if (kon_samePoint_(p, v) && kon_samePoint_(q, u)) map->edges[map->sectors[s].first + i].portal = t; + } + } + } + } + return 1; +} + +/*** loading ***/ + +static int kon_parseSector_(kon_builder_t *b, const char *p) +{ + kon_sectorMap_t *map = b->map; + kon_sector_t *sector; + char word[WORD_LENGTH], value[WORD_LENGTH]; + double floorHeight, ceilingHeight, number; + int line = b->line; + + if (!(p = kon_word_(p, word, sizeof(word))) || !kon_number_(word, &floorHeight)) return kon_fail_(b, line, "sector needs a floor height"); + if (!(p = kon_word_(p, word, sizeof(word))) || !kon_number_(word, &ceilingHeight)) return kon_fail_(b, line, "sector needs a ceiling height"); + + if (!kon_grow_((void **)&map->sectors, &b->sectorCap, sizeof(*map->sectors), map->sectorCount + 1)) return kon_fail_(b, 0, "out of memory"); + if (!kon_grow_((void **)&b->sectorLine, &b->lineCap, sizeof(int), map->sectorCount + 1)) return kon_fail_(b, 0, "out of memory"); + sector = &map->sectors[map->sectorCount]; + memset(sector, 0, sizeof(*sector)); + sector->floor = (float)floorHeight; + sector->ceiling = (float)ceilingHeight; + sector->light = 1.0f; + sector->floorTexture = sector->ceilingTexture = sector->wallTexture = -1; + sector->first = map->indexCount; + b->sectorLine[map->sectorCount] = line; + + /* key value pairs until the colon */ + for (;;) { + if (!(p = kon_word_(p, word, sizeof(word)))) return kon_fail_(b, line, "sector needs a colon before its corners"); + if (strcmp(word, ":") == 0) break; + if (kon_number_(word, &number)) return kon_fail_(b, line, "the corners of a sector come after a colon"); + if (!(p = kon_word_(p, value, sizeof(value)))) return kon_fail_(b, line, "%s needs a value", word); + + if (strcmp(word, "light") == 0) { + if (!kon_number_(value, &number) || number < 0.0 || number > 1.0) return kon_fail_(b, line, "light is a number from 0 to 1"); + sector->light = (float)number; + } else if (strcmp(word, "floor") == 0) { + if (!kon_texture_(b, value, §or->floorTexture)) return kon_fail_(b, line, "bad texture name %s", value); + } else if (strcmp(word, "ceiling") == 0) { + if (!kon_texture_(b, value, §or->ceilingTexture)) return kon_fail_(b, line, "bad texture name %s", value); + } else if (strcmp(word, "wall") == 0) { + if (!kon_texture_(b, value, §or->wallTexture)) return kon_fail_(b, line, "bad texture name %s", value); + } else { + return kon_fail_(b, line, "unknown sector setting %s", word); + } + } + + while ((p = kon_word_(p, word, sizeof(word)))) { + if (!kon_number_(word, &number) || number != floor(number) || number < 0.0 || number >= (double)map->vertexCount) { + return kon_fail_(b, line, "%s is not a vertex that was defined above", word); + } + if (!kon_grow_((void **)&map->indices, &b->indexCap, sizeof(int), map->indexCount + 1)) return kon_fail_(b, 0, "out of memory"); + if (!kon_grow_((void **)&map->edges, &b->edgeCap, sizeof(*map->edges), map->indexCount + 1)) return kon_fail_(b, 0, "out of memory"); + map->indices[map->indexCount] = (int)number; + map->edges[map->indexCount].portal = -1; + map->edges[map->indexCount].texture = -1; + map->indexCount++; + } + sector->count = map->indexCount - sector->first; + map->sectorCount++; + return 1; +} + +static int kon_parseEntity_(kon_builder_t *b, const char *p) +{ + kon_sectorMap_t *map = b->map; + kon_entity_t *entity; + char word[WORD_LENGTH]; + double v[4]; + int i, line = b->line; + + if (!kon_grow_((void **)&map->entities, &b->entityCap, sizeof(*map->entities), map->entityCount + 1)) return kon_fail_(b, 0, "out of memory"); + entity = &map->entities[map->entityCount]; + memset(entity, 0, sizeof(*entity)); + entity->sector = -1; + + if (!(p = kon_word_(p, word, sizeof(word)))) return kon_fail_(b, line, "entity needs a kind"); + if (strlen(word) >= KON_NAME_LENGTH) return kon_fail_(b, line, "the kind %s is too long", word); + strcpy(entity->kind, word); + + for (i = 0; i < 4; i++) { + if (!(p = kon_word_(p, word, sizeof(word))) || !kon_number_(word, &v[i])) return kon_fail_(b, line, "entity needs x y z and an angle"); + } + entity->position = KON_VEC3((float)v[0], (float)v[1], (float)v[2]); + entity->angle = (float)(v[3] * 3.14159265358979323846 / 180.0); + + while ((p = kon_word_(p, word, sizeof(word)))) { + char *equals = strchr(word, '='); + + if (!equals || equals == word) return kon_fail_(b, line, "%s should be written key=value", word); + *equals++ = '\0'; + + if (strcmp(word, "name") == 0 || strcmp(word, "target") == 0) { + char *dest = word[0] == 'n' ? entity->name : entity->target; + + if (strlen(equals) >= KON_NAME_LENGTH) return kon_fail_(b, line, "the %s %s is too long", word, equals); + strcpy(dest, equals); + } else { + if (entity->propCount >= KON_ENTITY_PROPS) return kon_fail_(b, line, "an entity can have %d settings at most", KON_ENTITY_PROPS); + if (strlen(word) >= sizeof(entity->props[0].key) || strlen(equals) >= sizeof(entity->props[0].value)) return kon_fail_(b, line, "the setting %s is too long", word); + strcpy(entity->props[entity->propCount].key, word); + strcpy(entity->props[entity->propCount].value, equals); + entity->propCount++; + } + } + map->entityCount++; + return 1; +} + +static int kon_parseLine_(kon_builder_t *b, const char *p, int *started) +{ + kon_sectorMap_t *map = b->map; + char word[WORD_LENGTH]; + double number, x, z; + int line = b->line; + + if (!(p = kon_word_(p, word, sizeof(word)))) return 1; + + if (!*started) { + if (strcmp(word, "kmap") != 0) return kon_fail_(b, line, "a map starts with \"kmap 1\""); + if (!(p = kon_word_(p, word, sizeof(word))) || !kon_number_(word, &number) || number != 1.0) return kon_fail_(b, line, "this reads kmap version 1"); + *started = 1; + return 1; + } + + if (strcmp(word, "v") == 0) { + char a[WORD_LENGTH], c[WORD_LENGTH]; + + if (!(p = kon_word_(p, a, sizeof(a))) || !(p = kon_word_(p, c, sizeof(c))) || !kon_number_(a, &x) || !kon_number_(c, &z)) return kon_fail_(b, line, "v needs x and z"); + if (!kon_grow_((void **)&map->vertices, &b->vertexCap, sizeof(*map->vertices), map->vertexCount + 1)) return kon_fail_(b, 0, "out of memory"); + map->vertices[map->vertexCount++] = KON_VEC2((float)x, (float)z); + return 1; + } + if (strcmp(word, "sector") == 0) return kon_parseSector_(b, p); + if (strcmp(word, "entity") == 0) return kon_parseEntity_(b, p); + if (strcmp(word, "edge") == 0) { + char a[WORD_LENGTH], c[WORD_LENGTH], name[WORD_LENGTH]; + double s, n; + + if (!(p = kon_word_(p, a, sizeof(a))) || !(p = kon_word_(p, c, sizeof(c))) || !(p = kon_word_(p, name, sizeof(name))) || !kon_number_(a, &s) || !kon_number_(c, &n)) { + return kon_fail_(b, line, "edge needs a sector, an edge number and a texture"); + } + if (s < 0 || s >= map->sectorCount || s != floor(s)) return kon_fail_(b, line, "there is no sector %s above", a); + if (n < 0 || n >= map->sectors[(int)s].count || n != floor(n)) return kon_fail_(b, line, "sector %s has no edge %s", a, c); + if (!kon_texture_(b, name, &map->edges[map->sectors[(int)s].first + (int)n].texture)) return kon_fail_(b, line, "bad texture name %s", name); + return 1; + } + return kon_fail_(b, line, "unknown word %s", word); +} + +kon_sectorMap_t *kon_sectorParse(const char *text, char *error, size_t errorSize) +{ + kon_builder_t b; + kon_sectorMap_t *map; + const char *p = text; + int started = 0, i; + + if (error && errorSize > 0) error[0] = '\0'; + if (!text) return NULL; + map = calloc(1, sizeof(*map)); + if (!map) return NULL; + memset(&b, 0, sizeof(b)); + b.map = map; + b.error = error; + b.errorSize = errorSize; + + while (*p != '\0') { + b.line++; + if (!kon_parseLine_(&b, p, &started)) goto bad; + while (*p != '\0' && *p != '\n') p++; + if (*p == '\n') p++; + } + + if (!started) { + kon_fail_(&b, 0, "the map is empty, it should start with \"kmap 1\""); + goto bad; + } + for (i = 0; i < map->sectorCount; i++) { + if (!kon_checkSector_(&b, i)) goto bad; + } + if (!kon_linkPortals_(&b)) goto bad; + for (i = 0; i < map->entityCount; i++) map->entities[i].sector = kon_sectorAt(map, map->entities[i].position.x, map->entities[i].position.z, -1); + + free(b.sectorLine); + return map; + +bad: + free(b.sectorLine); + kon_sectorFree(map); + return NULL; +} + +kon_sectorMap_t *kon_sectorLoad(const char *path, char *error, size_t errorSize) +{ + kon_sectorMap_t *map; + FILE *file; + char *text; + long size; + + if (error && errorSize > 0) error[0] = '\0'; + file = path ? fopen(path, "rb") : NULL; + if (!file) { + if (error && errorSize > 0) snprintf(error, errorSize, "could not open %s", path ? path : "(null)"); + return NULL; + } + fseek(file, 0, SEEK_END); + size = ftell(file); + fseek(file, 0, SEEK_SET); + if (size < 0 || size > MAX_FILE) { + fclose(file); + if (error && errorSize > 0) snprintf(error, errorSize, "%s is too big for a map", path); + return NULL; + } + text = malloc((size_t)size + 1); + if (!text) { + fclose(file); + return NULL; + } + if (fread(text, 1, (size_t)size, file) != (size_t)size) { + free(text); + fclose(file); + if (error && errorSize > 0) snprintf(error, errorSize, "could not read %s", path); + return NULL; + } + text[size] = '\0'; + fclose(file); + + map = kon_sectorParse(text, error, errorSize); + free(text); + return map; +} + +void kon_sectorFree(kon_sectorMap_t *map) +{ + if (!map) return; + free(map->vertices); + free(map->sectors); + free(map->indices); + free(map->edges); + free(map->entities); + free(map->textures); + free(map); +} + +int kon_sectorSave(const kon_sectorMap_t *map, const char *path) +{ + FILE *file; + int i, j; + + if (!map || !path) return 1; + file = fopen(path, "w"); + if (!file) return 1; + + fprintf(file, "kmap 1\n\n"); + for (i = 0; i < map->vertexCount; i++) fprintf(file, "v %.9g %.9g\n", (double)map->vertices[i].x, (double)map->vertices[i].y); + fputc('\n', file); + + for (i = 0; i < map->sectorCount; i++) { + const kon_sector_t *s = &map->sectors[i]; + + fprintf(file, "sector %.9g %.9g light %.9g floor %s ceiling %s wall %s :", (double)s->floor, (double)s->ceiling, (double)s->light, + s->floorTexture >= 0 ? map->textures[s->floorTexture] : "-", s->ceilingTexture >= 0 ? map->textures[s->ceilingTexture] : "-", + s->wallTexture >= 0 ? map->textures[s->wallTexture] : "-"); + for (j = 0; j < s->count; j++) fprintf(file, " %d", map->indices[s->first + j]); + fputc('\n', file); + } + for (i = 0; i < map->sectorCount; i++) { + for (j = 0; j < map->sectors[i].count; j++) { + int texture = map->edges[map->sectors[i].first + j].texture; + + if (texture >= 0) fprintf(file, "edge %d %d %s\n", i, j, map->textures[texture]); + } + } + fputc('\n', file); + + for (i = 0; i < map->entityCount; i++) { + const kon_entity_t *e = &map->entities[i]; + + fprintf(file, "entity %s %.9g %.9g %.9g %.9g", e->kind, (double)e->position.x, (double)e->position.y, (double)e->position.z, (double)e->angle * 180.0 / 3.14159265358979323846); + if (e->name[0]) fprintf(file, " name=%s", e->name); + if (e->target[0]) fprintf(file, " target=%s", e->target); + for (j = 0; j < e->propCount; j++) fprintf(file, " %s=%s", e->props[j].key, e->props[j].value); + fputc('\n', file); + } + return fclose(file) != 0; +} + +/*** looking things up ***/ + +kon_vec2_t kon_sectorCorner(const kon_sectorMap_t *map, int sector, int n) +{ + const kon_sector_t *s = &map->sectors[sector]; + + return map->vertices[map->indices[s->first + n % s->count]]; +} + +int kon_sectorWallTexture(const kon_sectorMap_t *map, int sector, int edge) +{ + int texture = map->edges[map->sectors[sector].first + edge].texture; + + return texture >= 0 ? texture : map->sectors[sector].wallTexture; +} + +int kon_sectorTextureIndex(const kon_sectorMap_t *map, const char *name) +{ + int i; + + for (i = 0; i < map->textureCount; i++) { + if (strcmp(map->textures[i], name) == 0) return i; + } + return -1; +} + +static int kon_inside_(const kon_sectorMap_t *map, int sector, float x, float z) +{ + int i, count = map->sectors[sector].count, inside = 0; + + for (i = 0; i < count; i++) { + kon_vec2_t a = kon_sectorCorner(map, sector, i), b = kon_sectorCorner(map, sector, i + 1); + + if ((a.y > z) != (b.y > z) && x < (b.x - a.x) * (z - a.y) / (b.y - a.y) + a.x) inside = !inside; + } + return inside; +} + +int kon_sectorAt(const kon_sectorMap_t *map, float x, float z, int hint) +{ + int i; + + if (!map) return -1; + if (hint >= 0 && hint < map->sectorCount) { + const kon_sector_t *s = &map->sectors[hint]; + + if (kon_inside_(map, hint, x, z)) return hint; + for (i = 0; i < s->count; i++) { + int next = map->edges[s->first + i].portal; + + if (next >= 0 && kon_inside_(map, next, x, z)) return next; + } + } + for (i = 0; i < map->sectorCount; i++) { + if (kon_inside_(map, i, x, z)) return i; + } + return -1; +} + +/*** entities ***/ + +int kon_entityFind(const kon_sectorMap_t *map, const char *name) +{ + int i; + + if (!map || !name || !name[0]) return -1; + for (i = 0; i < map->entityCount; i++) { + if (strcmp(map->entities[i].name, name) == 0) return i; + } + return -1; +} + +int kon_entityTargets(const kon_sectorMap_t *map, const char *name, int *out, int max) +{ + int i, total = 0; + + if (!map || !name || !name[0]) return 0; + for (i = 0; i < map->entityCount; i++) { + if (strcmp(map->entities[i].name, name) != 0) continue; + if (out && total < max) out[total] = i; + total++; + } + return total; +} + +const char *kon_entityString(const kon_entity_t *entity, const char *key, const char *fallback) +{ + int i; + + for (i = 0; entity && i < entity->propCount; i++) { + if (strcmp(entity->props[i].key, key) == 0) return entity->props[i].value; + } + return fallback; +} + +float kon_entityFloat(const kon_entity_t *entity, const char *key, float fallback) +{ + const char *text = kon_entityString(entity, key, NULL); + double value; + + return text && kon_number_(text, &value) ? (float)value : fallback; +} + +int kon_entityInt(const kon_entity_t *entity, const char *key, int fallback) +{ + const char *text = kon_entityString(entity, key, NULL); + double value; + + return text && kon_number_(text, &value) ? (int)value : fallback; +} diff --git a/kon/konsector.h b/kon/konsector.h new file mode 100644 index 0000000..58a4ecb --- /dev/null +++ b/kon/konsector.h @@ -0,0 +1,133 @@ +/* SPDX-License-Identifier: MIT */ +/* + * konsector.h + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef KONSECTOR_H +#define KONSECTOR_H + +#include + +#include "konmath.h" + +/*** definition ***/ + +/* a map is rooms seen from above. every room (sector) is a polygon of vertices with a floor and a ceiling height, and every + edge of the polygon is a wall. where two sectors share an edge it is an opening instead of a wall, and if their heights + differ there is a step or a lintel there. the plan is x going east and z going south, y is up like everywhere else. + a room cannot sit on top of another one */ + +#define KON_NAME_LENGTH 24 +#define KON_ENTITY_PROPS 8 + +typedef struct kon_sectorEdge { + int portal; /* the sector on the other side, -1 for a wall with nothing behind it */ + int texture; /* index into the map's textures, -1 uses the sector's wall texture */ +} kon_sectorEdge_t; + +typedef struct kon_sector { + int first, count; /* where its corners start in indices and edges, and how many there are */ + float floor, ceiling; + float light; /* 0 to 1, how bright the room is, 1 */ + int floorTexture, ceilingTexture, wallTexture; /* -1 for none */ +} kon_sector_t; + +typedef struct kon_entityProp { + char key[16]; + char value[32]; +} kon_entityProp_t; + +/* things placed in the map that are not walls: where you start, doors, buttons, lights. the engine only reads them, + what a kind does is up to the game. name and target are how things point at each other, a button with the target "door1" + is meant for the entity named "door1" */ +typedef struct kon_entity { + char kind[KON_NAME_LENGTH]; + char name[KON_NAME_LENGTH]; + char target[KON_NAME_LENGTH]; + kon_vec3_t position; + float angle; /* radians, 0 faces north (towards -z) and it turns clockwise from above like the camera yaw */ + int sector; /* the sector it is in, -1 if the position is outside every sector */ + int propCount; + kon_entityProp_t props[KON_ENTITY_PROPS]; +} kon_entity_t; + +typedef struct kon_sectorMap { + int vertexCount; + kon_vec2_t *vertices; /* x, z */ + + /* the corners of a sector are indices[first] to indices[first + count - 1], counter clockwise seen from above. + edge n runs from corner n to corner n + 1 and the last one closes the loop */ + int sectorCount; + kon_sector_t *sectors; + int indexCount; + int *indices; + kon_sectorEdge_t *edges; + + int entityCount; + kon_entity_t *entities; + + int textureCount; + char (*textures)[KON_NAME_LENGTH]; /* names only, the game decides what picture goes with each */ +} kon_sectorMap_t; + +/*** loading ***/ + +/* reads a map from text. returns NULL and fills error (if it is not NULL) with something like "line 12: sector 2 has a ceiling below its floor". + a clockwise sector is turned around, anything else wrong is an error */ +kon_sectorMap_t *kon_sectorParse(const char *text, char *error, size_t errorSize); +kon_sectorMap_t *kon_sectorLoad(const char *path, char *error, size_t errorSize); +void kon_sectorFree(kon_sectorMap_t *map); + +/* the same text format, so a map can be edited in the game and saved. returns 0 on success */ +int kon_sectorSave(const kon_sectorMap_t *map, const char *path); + +/*** looking things up ***/ + +/* the sector that holds the point, or -1. hint is a sector to try first (and its neighbours), pass -1 if you have none. + moving things should pass the sector they were in last frame, that makes this a few comparisons instead of a search */ +int kon_sectorAt(const kon_sectorMap_t *map, float x, float z, int hint); + +/* where corner n of a sector is */ +kon_vec2_t kon_sectorCorner(const kon_sectorMap_t *map, int sector, int n); + +/* the texture of an edge, the sector's own wall texture when the edge has none */ +int kon_sectorWallTexture(const kon_sectorMap_t *map, int sector, int edge); + +/* -1 if the name is not used in the map */ +int kon_sectorTextureIndex(const kon_sectorMap_t *map, const char *name); + +/*** entities ***/ + +/* the first entity with this name, or -1 */ +int kon_entityFind(const kon_sectorMap_t *map, const char *name); + +/* fills out with the indices of every entity named like this, up to max, and returns how many there are in total. + this is how a button finds what it is for: ask for the entities named like its target */ +int kon_entityTargets(const kon_sectorMap_t *map, const char *name, int *out, int max); + +/* props are the key=value pairs written after the entity in the file. they are text, these read them as numbers */ +const char *kon_entityString(const kon_entity_t *entity, const char *key, const char *fallback); +float kon_entityFloat(const kon_entity_t *entity, const char *key, float fallback); +int kon_entityInt(const kon_entity_t *entity, const char *key, int fallback); + +#endif -- cgit