summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonAki <konaki@anokonaki.org>2026-10-11 04:51:06 +0800
committerKonAki <konaki@anokonaki.org>2026-10-11 04:51:06 +0800
commit64a416a3fa0b5097afe723557ca71fc99606d3d9 (patch)
tree6d5eea1ddec0ed709ab9a4ebdc1de9bb002c81ce
parentd3b6458db0c189d23100331c0e9e5001ff96c2f1 (diff)
add sector module: room maps in a text format with entities
-rw-r--r--README.md1
-rwxr-xr-xbuild.sh4
-rw-r--r--examples/maps/house.kmap28
-rw-r--r--kon/konsector.c636
-rw-r--r--kon/konsector.h133
5 files changed, 800 insertions, 2 deletions
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 <math.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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, &sector->floorTexture)) return kon_fail_(b, line, "bad texture name %s", value);
+ } else if (strcmp(word, "ceiling") == 0) {
+ if (!kon_texture_(b, value, &sector->ceilingTexture)) return kon_fail_(b, line, "bad texture name %s", value);
+ } else if (strcmp(word, "wall") == 0) {
+ if (!kon_texture_(b, value, &sector->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 <stddef.h>
+
+#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