summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaweł Redman <pawel.redman@gmail.com>2016-12-20 09:35:09 +0100
committerPaweł Redman <pawel.redman@gmail.com>2016-12-20 09:35:09 +0100
commit337247d56e3300d17301daa7fa3fbfb6084cd540 (patch)
tree3299b79b08ff2112c21f6a3d1b33ff18ce88f2f5
Initial commit.
-rw-r--r--.gitignore3
-rw-r--r--Makefile39
-rw-r--r--src/common.c100
-rw-r--r--src/common.h86
-rw-r--r--src/elist.h113
-rw-r--r--src/lexer.c253
-rw-r--r--src/main.c112
-rw-r--r--src/mapcat.c483
8 files changed, 1189 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c4c2254
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+obj/
+mapcat
+test/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..441c008
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,39 @@
+CC = gcc
+CFLAGS += -g -Wall
+CPPFLAGS += -MMD
+LDFLAGS +=
+
+PP_BOLD := $(shell tput bold)
+PP_RESET := $(shell tput sgr0)
+PP_CC := $(PP_BOLD)$(shell tput setf 6)CC$(PP_RESET)
+PP_LD := $(PP_BOLD)$(shell tput setf 2)LD$(PP_RESET)
+PP_RM := $(PP_BOLD)$(shell tput setf 4)RM$(PP_RESET)
+
+SRC := src/common.c \
+ src/lexer.c \
+ src/main.c \
+ src/mapcat.c
+OBJ := $(SRC:src/%.c=obj/%.o)
+OUT := mapcat
+
+all: $(OUT)
+
+-include $(OBJ:.o=.d)
+
+obj/%.o : src/%.c
+ @echo "$(PP_CC) src/$*.c"
+ @mkdir -p $(@D)
+ @$(CC) $(CFLAGS) $(CPPFLAGS) -c src/$*.c -o obj/$*.o
+
+$(OUT): $(OBJ)
+ @echo "$(PP_LD) $(OUT)"
+ @$(CC) $(OBJ) -o $(OUT) $(LDFLAGS)
+
+clean:
+ @echo "${PP_RM} obj"
+ @rm -rf obj
+ @echo "${PP_RM} ${OUT}"
+ @rm -rf ${OUT}
+
+.PHONY: clean
+
diff --git a/src/common.c b/src/common.c
new file mode 100644
index 0000000..1c057a2
--- /dev/null
+++ b/src/common.c
@@ -0,0 +1,100 @@
+/*
+Copyright (C) 2016 Paweł Redman
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 3
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software Foundation,
+Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include "common.h"
+
+void vstr_init(vstr_t *vstr)
+{
+ memset(vstr, 0, sizeof(*vstr));
+}
+
+void vstr_free(vstr_t *vstr)
+{
+ free(vstr->data);
+}
+
+void vstr_clear(vstr_t *vstr)
+{
+ vstr->size = 0;
+}
+
+static int vstr_enlarge(vstr_t *vstr)
+{
+ size_t new_alloc;
+
+ new_alloc = (vstr->alloc + 4) * 3 / 2;
+ debug("%zu -> %zu\n", vstr->alloc, new_alloc);
+
+ vstr->data = realloc(vstr->data, new_alloc);
+ if (!vstr->data)
+ return 1;
+
+ vstr->alloc = new_alloc;
+ return 0;
+}
+
+int vstr_putc(vstr_t *vstr, char ch)
+{
+ // note: keep at least one character free at all times for vstr_termz
+ if (vstr->size + 2 > vstr->alloc)
+ if (vstr_enlarge(vstr))
+ return -ENOMEM;
+
+ vstr->data[vstr->size] = ch;
+ vstr->size++;
+ return 0;
+}
+
+int vstr_cmp(vstr_t *vstr, const char *str)
+{
+ size_t len;
+
+ len = strlen(str);
+ if (vstr->size < len)
+ len = vstr->size;
+
+ return memcmp(vstr->data, str, len);
+}
+
+char *vstr_strdup(vstr_t *vstr)
+{
+ char *str;
+
+ str = malloc(vstr->size + 1);
+ if (!str)
+ return NULL;
+
+ memcpy(str, vstr->data, vstr->size);
+ str[vstr->size] = 0;
+
+ return str;
+}
+
+void vstr_termz(vstr_t *vstr)
+{
+ vstr->data[vstr->size] = 0;
+}
+
+float vstr_atof(vstr_t *vstr)
+{
+ if (!vstr->size)
+ return 0;
+
+ vstr_termz(vstr);
+ return atof(vstr->data);
+}
diff --git a/src/common.h b/src/common.h
new file mode 100644
index 0000000..8c6f43f
--- /dev/null
+++ b/src/common.h
@@ -0,0 +1,86 @@
+/*
+Copyright (C) 2016 Paweł Redman
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 3
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software Foundation,
+Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdarg.h>
+#include "elist.h"
+
+#ifdef DEBUG
+#define debug(fmt, ...) fprintf(stderr, "%s: " fmt, __func__, ##__VA_ARGS__)
+#else
+#define debug(fmt, ...)
+#endif
+
+#define PROGRAM_NAME "mapcat"
+#define PROGRAM_VERSION "0.1.0"
+
+// common.c
+
+typedef struct {
+ char *data;
+ size_t size, alloc;
+} vstr_t;
+
+void vstr_init(vstr_t *vstr);
+void vstr_free(vstr_t *vstr);
+void vstr_clear(vstr_t *vstr);
+int vstr_putc(vstr_t *vstr, char ch);
+int vstr_cmp(vstr_t *vstr, const char *str);
+char *vstr_strdup(vstr_t *vstr);
+void vstr_termz(vstr_t *vstr);
+float vstr_atof(vstr_t *vstr);
+
+// lexer.c
+
+#define LEXER_BUFFER 1024
+
+typedef struct {
+ int error;
+ const char *path;
+ FILE *fp;
+ bool eof;
+
+ vstr_t *token;
+ char buf[LEXER_BUFFER];
+ char *buf_c, *buf_e;
+
+ size_t cc, lc, Cc; // character, line, and column counters
+ char last;
+
+ bool in_token;
+ bool in_quote;
+ bool in_comment;
+} lexer_state_t;
+
+int lexer_open(lexer_state_t *ls, const char *path, vstr_t *token);
+int lexer_get_token(lexer_state_t *ls);
+int lexer_assert(lexer_state_t *ls, const char *match, const char *desc);
+int lexer_assert_or_eof(lexer_state_t *ls, const char *match, const char *desc);
+void lexer_perror(lexer_state_t *ls, const char *fmt, ...);
+void lexer_perror_eg(lexer_state_t *ls, const char *expected);
+int lexer_get_floats(lexer_state_t *ls, float *out, size_t count);
+
+// mapcat.c
+
+int mapcat_load(const char *path);
+int mapcat_save(const char *path);
+void mapcat_free(void);
diff --git a/src/elist.h b/src/elist.h
new file mode 100644
index 0000000..da6d172
--- /dev/null
+++ b/src/elist.h
@@ -0,0 +1,113 @@
+/*
+Copyright (C) 2016 Paweł Redman
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 3
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software Foundation,
+Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#define offsetin(V, M) ((size_t)&(V)->M - (size_t)(V))
+
+typedef struct {
+ void *prev, *next;
+} elist_header_t;
+
+static inline elist_header_t *elist_header(void *entry, size_t offs)
+{
+ return (elist_header_t*)((char*)entry + offs);
+}
+
+static inline void elist_append_real(void **head, void *entry, size_t offs)
+{
+ elist_header_t *head_header, *entry_header;
+
+ entry_header = elist_header(entry, offs);
+
+ if (!*head) {
+ *head = entry;
+ entry_header->prev = entry;
+ entry_header->next = NULL;
+ return;
+ }
+
+ head_header = elist_header(*head, offs);
+
+ entry_header->prev = head_header->prev;
+ if (head_header->prev)
+ elist_header(head_header->prev, offs)->next = entry;
+ head_header->prev = entry;
+
+ entry_header->next = NULL;
+}
+
+#define elist_append(head, entry, member) \
+elist_append_real((void**)(head), (entry), offsetin((entry), member))
+
+#define elist_next(entry, member) \
+(elist_header((entry), offsetin((entry), member))->next)
+
+#define elist_for(iter, head, member) \
+for ((iter) = (head); (iter); (iter) = elist_next((iter), member))
+
+static inline void elist_unlink_real(void **head, void *entry, size_t offs)
+{
+ elist_header_t *entry_header;
+
+ entry_header = elist_header(entry, offs);
+
+ if (entry_header->prev && *head != entry) {
+ elist_header_t *prev_header;
+ prev_header = elist_header(entry_header->prev, offs);
+ prev_header->next = entry_header->next;
+ }
+
+ if (entry_header->next) {
+ elist_header_t *next_header;
+ next_header = elist_header(entry_header->next, offs);
+ next_header->prev = entry_header->prev;
+ } else {
+ elist_header_t *head_header;
+ head_header = elist_header(*head, offs);
+ head_header->prev = entry_header->prev;
+ }
+
+ if (*head == entry)
+ *head = entry_header->next;
+
+}
+
+#define elist_unlink(head, entry, member) \
+elist_unlink_real((void**)(head), (entry), offsetin((entry), member))
+
+static inline void elist_append_list_real(void **head1, void *head2, size_t offs)
+{
+ elist_header_t *head1_header, *head2_header, *last1_header;
+
+ if (!*head1) {
+ *head1 = head2;
+ return;
+ }
+
+ if (!head2)
+ return;
+
+ head1_header = elist_header(head1, offs);
+ head2_header = elist_header(head2, offs);
+ last1_header = elist_header(head1_header->prev, offs);
+ head1_header->prev = head2_header->prev;
+ last1_header->next = head2;
+ head2_header->prev = head1_header->prev;
+}
+
+#define elist_append_list(head1, head2, member) \
+elist_append_list_real((void**)(head1), (head2), offsetin((head2), member))
diff --git a/src/lexer.c b/src/lexer.c
new file mode 100644
index 0000000..41d6784
--- /dev/null
+++ b/src/lexer.c
@@ -0,0 +1,253 @@
+/*
+Copyright (C) 2016 Paweł Redman
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 3
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software Foundation,
+Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include "common.h"
+#include <ctype.h>
+
+int lexer_open(lexer_state_t *ls, const char *path, vstr_t *token)
+{
+ ls->error = 0;
+ ls->path = path;
+
+ ls->fp = fopen(path, "r");
+ if (!ls->fp)
+ return -errno;
+
+ ls->eof = false;
+
+ ls->token = token;
+ ls->buf_e = ls->buf_c = ls->buf;
+ ls->cc = ls->lc = ls->Cc = 0;
+
+ ls->in_token = false;
+ ls->in_quote = false;
+ ls->in_comment = false;
+
+ return 0;
+}
+
+//RETURN VALUES
+// <0 on error
+// 0 on success
+// note: sets ls->eof to true if there's no more data left
+static int fill_buffer(lexer_state_t *ls)
+{
+ size_t read;
+
+ read = fread(ls->buf, 1, sizeof(ls->buf), ls->fp);
+ debug("read = %zu\n", read);
+ if (read < sizeof(ls->buf)) {
+ if (ferror(ls->fp)) {
+ fclose(ls->fp);
+ return -errno;
+ }
+
+ ls->eof = true;
+ fclose(ls->fp);
+ debug("no data left, ls->fp closed\n");
+ }
+
+ ls->buf_c = ls->buf;
+ ls->buf_e = ls->buf + read;
+ return 0;
+}
+
+//RETURN VALUES
+// -ENOMEM
+// -EAGAIN when the buffer runs out
+// 0 on success
+// 1 when no data is left
+static int read_buffer(lexer_state_t *ls)
+{
+ while (ls->buf_c < ls->buf_e) {
+ bool ret_token = false;
+
+ debug("*ls->buf_c = %c, ls->last = %c\n", *ls->buf_c, ls->last);
+
+ if (*ls->buf_c == '\n') {
+ ls->lc++;
+ ls->Cc = 0;
+ }
+
+ if (ls->in_comment) {
+ if (*ls->buf_c == '\n')
+ ls->in_comment = false;
+ } else if (isspace(*ls->buf_c) && !ls->in_quote) {
+ if (ls->in_token) {
+ ls->in_token = false;
+ ret_token = true;
+ }
+ } else if (*ls->buf_c == '/' && ls->last == '/') {
+ ls->in_comment = true;
+ ls->in_token = false;
+
+ ls->token->size--; // remove the first slash
+ if (ls->token->size)
+ ret_token = true;
+ } else if (*ls->buf_c == '\"' &&
+ (ls->cc && ls->last != '\\')) {
+ ls->in_quote = !ls->in_quote;
+
+ if (!ls->in_quote) {
+ ls->in_token = false;
+ ret_token = true;
+ }
+ } else {
+ if (!ls->in_token) {
+ ls->in_token = true;
+ }
+ }
+
+ if (ls->in_token)
+ if (vstr_putc(ls->token, *ls->buf_c)) {
+ ls->error = ENOMEM;
+ return -ENOMEM;
+ }
+
+ ls->last = *ls->buf_c;
+ ls->buf_c++;
+ ls->cc++;
+ ls->Cc++;
+
+ if (ret_token)
+ return 0;
+ }
+
+ if (ls->eof) {
+ if (ls->token->size > 0)
+ return 0;
+ return 1;
+ }
+
+ return -EAGAIN;
+}
+
+//RETURN VALUES
+// <0 on error
+// 0 on success
+// 1 when no data is left
+int lexer_get_token(lexer_state_t *ls)
+{
+ int ret;
+
+ vstr_clear(ls->token);
+
+ while (1) {
+ ret = read_buffer(ls);
+ debug("read_buffer = %i\n", ret);
+ if (ret != -EAGAIN)
+ return ret;
+
+ ret = fill_buffer(ls);
+ debug("fill_buffer = %i\n", ret);
+ if (ret < 0)
+ return ret;
+ }
+}
+
+void lexer_perror(lexer_state_t *ls, const char *fmt, ...)
+{
+ va_list vl;
+
+ fprintf(stderr, "%s:%zu:%zu: ", ls->path, ls->lc + 1, ls->Cc + 1);
+
+ if (ls->error) {
+ perror(NULL);
+ } else {
+ va_start(vl, fmt);
+ vfprintf(stderr, fmt, vl);
+ va_end(vl);
+ }
+}
+
+void lexer_perror_eg(lexer_state_t *ls, const char *expected)
+{
+ if (ls->eof && ls->buf_c == ls->buf_e)
+ lexer_perror(ls, "expected %s, got EOF\n", expected);
+ else {
+ vstr_termz(ls->token);
+ lexer_perror(ls, "expected %s, got \"%s\"\n", expected,
+ ls->token->data);
+ }
+}
+
+int lexer_assert(lexer_state_t *ls, const char *match, const char *desc)
+{
+ int ret;
+
+ ret = lexer_get_token(ls);
+ if (ret) {
+ lexer_perror(ls, "expected %s%s\"%s\", got EOF\n",
+ (desc ? desc : ""), (desc ? " " : ""), match);
+ return 1;
+ }
+
+ if (vstr_cmp(ls->token, match)) {
+ vstr_termz(ls->token);
+ lexer_perror(ls, "expected %s%s\"%s\", got \"%s\"\n",
+ (desc ? desc : ""), (desc ? " " : ""), match,
+ ls->token->data);
+ return 1;
+ }
+
+ return 0;
+}
+
+//RETURN VALUE
+// -1 on eof (also success)
+// 0 on success
+// 1 on error
+int lexer_assert_or_eof(lexer_state_t *ls, const char *match, const char *desc)
+{
+ int ret;
+
+ ret = lexer_get_token(ls);
+ if (ret < 0) {
+ perror("lexer");
+ return 1;
+ }
+
+ if (ret == 1) {
+ return -1;
+ }
+
+ if (vstr_cmp(ls->token, match)) {
+ lexer_perror(ls, "expected %s%s\"%s\" or EOF, got \"%.*s\"\n",
+ (desc ? desc : ""), (desc ? " " : ""), match,
+ (int)ls->token->size, ls->token->data);
+ return 1;
+ }
+
+ return 0;
+}
+
+int lexer_get_floats(lexer_state_t *ls, float *out, size_t count)
+{
+ size_t i;
+
+ for (i = 0; i < count; i++) {
+ if (lexer_get_token(ls)) {
+ lexer_perror_eg(ls, "a number");
+ return 1;
+ }
+
+ out[i] = vstr_atof(ls->token);
+ }
+
+ return 0;
+}
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..d34f00e
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,112 @@
+/*
+Copyright (C) 2016 Paweł Redman
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 3
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software Foundation,
+Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include "common.h"
+#include <unistd.h>
+
+#define error(fmt, ...) fprintf(stderr, PROGRAM_NAME ": " fmt, ##__VA_ARGS__)
+
+void print_version(void)
+{
+ puts(PROGRAM_NAME " " PROGRAM_VERSION ", Copyright (C) 2016 Paweł Redman\n"
+ PROGRAM_NAME " comes with ABSOLUTELY NO WARRANTY.\n"
+ "This is free software, and you are welcome to redistribute it\n"
+ "under certain conditions. See the file COPYING.");
+}
+
+void print_usage(void)
+{
+ puts(PROGRAM_NAME " " PROGRAM_VERSION "\n"
+ "usage: " PROGRAM_NAME " -o outfile infile...\n"
+ " or " PROGRAM_NAME " -v\n"
+ " or " PROGRAM_NAME " -h");
+}
+
+typedef struct {
+ char *path;
+ elist_header_t list;
+} input_file_t;
+
+int main(int argc, char **argv)
+{
+ int rv = 1, i;
+ input_file_t *inputs = NULL, *input, *next;
+ char *output = NULL;
+
+ for (i = 1; i < argc; i++) {
+ if (!strcmp(argv[i], "-v")) {
+ print_version();
+ rv = 0;
+ goto out;
+ } else if (!strcmp(argv[i], "-h")) {
+ print_usage();
+ rv = 0;
+ goto out;
+ } else if (!strcmp(argv[i], "-o")) {
+ if (i + 1 >= argc) {
+ error("-o needs an argument\n");
+ goto out;
+ }
+
+ if (output) {
+ error("-o can be specified only once\n");
+ goto out;
+ }
+
+ output = argv[i + 1];
+ i++;
+ } else {
+ input = malloc(sizeof(input_file_t));
+ if (!input) {
+ error("out of memory\n");
+ goto out;
+ }
+
+ input->path = argv[i];
+ elist_append(&inputs, input, list);
+ }
+ }
+
+ if (!inputs) {
+ error("no input files specified, try '" PROGRAM_NAME " -h'\n");
+ goto out;
+ }
+
+ if (!output) {
+ error("no output file specified, try '" PROGRAM_NAME " -h'\n");
+ goto out;
+ }
+
+ elist_for (input, inputs, list)
+ if (mapcat_load(input->path))
+ goto out;
+
+ if (mapcat_save(output))
+ goto out;
+
+ rv = 0;
+out:
+ mapcat_free();
+
+ for (input = inputs; input; input = next) {
+ next = elist_next(input, list);
+ free(input);
+ }
+
+ return rv;
+}
diff --git a/src/mapcat.c b/src/mapcat.c
new file mode 100644
index 0000000..6420cf5
--- /dev/null
+++ b/src/mapcat.c
@@ -0,0 +1,483 @@
+/*
+Copyright (C) 2016 Paweł Redman
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 3
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software Foundation,
+Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#define DEBUG
+#include "common.h"
+
+typedef struct {
+ float def[9];
+ char *shader;
+ float texmap[8];
+ elist_header_t list;
+} brush_face_t;
+
+typedef struct {
+ brush_face_t *faces;
+ elist_header_t list;
+} brush_t;
+
+typedef struct {
+ char *key;
+ char *value;
+ elist_header_t list;
+} entity_key_t;
+
+typedef struct {
+ char *classname;
+ brush_t *brushes;
+ entity_key_t *keys;
+
+ elist_header_t list;
+} entity_t;
+
+entity_t *worldspawn;
+entity_t *entities;
+
+static int read_entity_key(lexer_state_t *ls, entity_t *entity)
+{
+ entity_key_t *key;
+
+ // classnames are stored separately for easier access later
+ if (!vstr_cmp(ls->token, "classname")) {
+ if (entity->classname) {
+ lexer_perror(ls, "warning: duplicate classname\n");
+ free(entity->classname);
+ entity->classname = NULL;
+ }
+
+ if (lexer_get_token(ls)) {
+ lexer_perror_eg(ls, "the classname");
+ return 1;
+ }
+
+ entity->classname = vstr_strdup(ls->token);
+ if (!entity->classname) {
+ lexer_perror(ls, "out of memory\n");
+ return 1;
+ }
+
+ return 0;
+ }
+
+ key = malloc(sizeof(entity_key_t));
+ if (!key)
+ goto error_oom;
+
+ memset(key, 0, sizeof(*key));
+ elist_append(&entity->keys, key, list);
+
+ key->key = vstr_strdup(ls->token);
+ if (!key->key)
+ goto error_oom;
+
+ if (lexer_get_token(ls)) {
+ lexer_perror_eg(ls, "the key value");
+ return 1;
+ }
+
+ key->value = vstr_strdup(ls->token);
+ if (!key->value)
+ goto error_oom;
+
+ return 0;
+error_oom:
+ lexer_perror(ls, "out of memory\n");
+ return 1;
+}
+
+static int read_brush_face(lexer_state_t *ls, brush_t *brush)
+{
+ brush_face_t *face;
+
+ face = malloc(sizeof(brush_face_t));
+ if (!face) {
+ lexer_perror(ls, "out of memory\n");
+ return 1;
+ }
+
+ memset(face, 0, sizeof(*face));
+ elist_append(&brush->faces, face, list);
+
+ if (lexer_get_floats(ls, face->def, 3))
+ return 1;
+
+ if (lexer_assert(ls, ")", "the end of this face's 1st vector"))
+ return 1;
+
+ if (lexer_assert(ls, "(", "the beginning of this face's 2nd vector"))
+ return 1;
+
+ if (lexer_get_floats(ls, face->def + 3, 3))
+ return 1;
+
+ if (lexer_assert(ls, ")", "the end of this face's 2nd vector"))
+ return 1;
+
+ if (lexer_assert(ls, "(", "the beginning of this face's 3rd vector"))
+ return 1;
+
+ if (lexer_get_floats(ls, face->def + 6, 3))
+ return 1;
+
+ if (lexer_assert(ls, ")", "the end of this face's 3rd vector"))
+ return 1;
+
+ if (lexer_get_token(ls)) {
+ lexer_perror_eg(ls, "a shader name");
+ return 1;
+ }
+
+ face->shader = vstr_strdup(ls->token);
+ if (!face->shader) {
+ lexer_perror(ls, "out of memory\n");
+ return 1;
+ }
+
+ if (lexer_get_floats(ls, face->texmap, 8))
+ return 1;
+
+ return 0;
+}
+
+static int read_brush_faces(lexer_state_t *ls, brush_t *brush)
+{
+ while (1) {
+ if (lexer_get_token(ls)) {
+ lexer_perror_eg(ls, "the beginning of a brush face "
+ " ('(') or the end of this brush"
+ " ('}')");
+ return 1;
+ }
+
+ if (!vstr_cmp(ls->token, "(")) {
+ if (read_brush_face(ls, brush))
+ return 1;
+ } else if (!vstr_cmp(ls->token, "}"))
+ break;
+ }
+
+ return 0;
+}
+
+static void free_entity_keys(entity_t *entity)
+{
+ entity_key_t *key, *next;
+
+ for (key = entity->keys; key; key = next) {
+ next = elist_next(key, list);
+
+ free(key->key);
+ free(key->value);
+ free(key);
+ }
+}
+
+static void free_brush(brush_t *brush)
+{
+ brush_face_t *face, *face_next;
+
+ for (face = brush->faces; face; face = face_next) {
+ face_next = elist_next(face, list);
+
+ free(face->shader);
+ free(face);
+ }
+
+ free(brush);
+}
+
+// this frees an entity_t and its children
+// note: this function does NOT unlink the entity from the global list
+static void free_entity(entity_t *entity)
+{
+ brush_t *brush, *next;
+
+ free_entity_keys(entity);
+ free(entity->classname);
+
+ for (brush = entity->brushes; brush; brush = next) {
+ next = elist_next(brush, list);
+ free_brush(brush);
+ }
+
+ free(entity);
+}
+
+static void merge_into_worldspawn(entity_t *entity)
+{
+ // the first worldspawn becomes the output's worldspawn.
+ // other worldspawn's brushes are appended to it
+ if (!worldspawn) {
+ worldspawn = entity;
+ return;
+ }
+
+ // keep only the original key-value pairs
+ free_entity_keys(entity);
+
+ elist_append_list(worldspawn->brushes, entity->brushes, list);
+
+ free(entity);
+}
+
+static bool brush_discard(brush_t *brush)
+{
+ brush_face_t *face;
+
+ elist_for(face, brush->faces, list)
+ if (!strcmp(face->shader, "mapcat_discard"))
+ return true;
+
+ return false;
+}
+
+static int read_entity(lexer_state_t *ls)
+{
+ entity_t *entity;
+
+ entity = malloc(sizeof(entity_t));
+ if (!entity) {
+ lexer_perror(ls, "out of memory\n");
+ return 1;
+ }
+
+ memset(entity, 0, sizeof(*entity));
+
+ // read keys and values
+ while (1) {
+ if (lexer_get_token(ls)) {
+ lexer_perror_eg(ls, "a key or the beginning of a brush"
+ " \"{\" or the end of this entity"
+ " \"}\"");
+ goto error;
+ }
+
+ if (!vstr_cmp(ls->token, "{"))
+ break;
+
+ if (!vstr_cmp(ls->token, "}"))
+ goto no_brushes;
+
+ if (read_entity_key(ls, entity))
+ goto error;
+ }
+
+ // the opening brace of the first brush in this entity was already
+ // read in the loop above, so it has to be skipped in the loop below.
+ // the problem is solved by jumping in the middle of the loop
+ goto skip_first_brace;
+
+ while (1) {
+ brush_t *brush;
+
+ if (lexer_get_token(ls)) {
+ L1:
+ lexer_perror_eg(ls, "the beginning of a brush \"{\""
+ " or the end of this entity \"}\"");
+ goto error;
+ }
+
+ if (!vstr_cmp(ls->token, "}"))
+ break;
+ else if (vstr_cmp(ls->token, "{"))
+ goto L1;
+
+ skip_first_brace:
+ brush = malloc(sizeof(brush_t));
+ if (!brush) {
+ lexer_perror(ls, "out of memory\n");
+ goto error;
+ }
+
+ memset(brush, 0, sizeof(*brush));
+
+ if (read_brush_faces(ls, brush)) {
+ free_brush(brush);
+ goto error;
+ }
+
+ if (brush_discard(brush))
+ free_brush(brush);
+ else
+ elist_append(&entity->brushes, brush, list);
+ }
+
+no_brushes:
+
+ if (entity->classname && !strcmp(entity->classname, "worldspawn"))
+ merge_into_worldspawn(entity);
+ else
+ elist_append(&entities, entity, list);
+
+ return 0;
+error:
+ free_entity(entity);
+ return 1;
+}
+
+
+int mapcat_load(const char *path)
+{
+ int rv = 1;
+ lexer_state_t lexer;
+ vstr_t token;
+
+ vstr_init(&token);
+
+ if (lexer_open(&lexer, path, &token)) {
+ perror(path);
+ return 1;
+ }
+
+ while (1) {
+ int ret;
+
+ ret = lexer_assert_or_eof(&lexer, "{",
+ "the beginning of an entity");
+ if (ret == -1)
+ break;
+ if (ret > 0)
+ goto out;
+
+ if (read_entity(&lexer))
+ goto out;
+ }
+
+ rv = 0;
+out:
+ vstr_free(&token);
+ return rv;
+}
+
+static int write_brush(FILE *fp, brush_t *brush)
+{
+ brush_face_t *face;
+
+ elist_for(face, brush->faces, list) {
+ size_t i;
+
+ for (i = 0; i < 9; i += 3) {
+ if (i)
+ fprintf(fp, " ");
+
+ fprintf(fp, "( %f %f %f )", face->def[i],
+ face->def[i + 1], face->def[i + 2]);
+ }
+
+ fprintf(fp, " %s", face->shader);
+
+ for (i = 0; i < 5; i++)
+ fprintf(fp, " %f", face->texmap[i]);
+
+ // the last three values are integers
+ for (i = 5; i < 8; i++)
+ fprintf(fp, " %.0f", face->texmap[i]);
+
+ fprintf(fp, "\n");
+ }
+
+ if (ferror(fp))
+ return -errno;
+
+ return 0;
+}
+
+static int write_entity(FILE *fp, entity_t *entity)
+{
+ entity_key_t *key;
+ brush_t *brush;
+ size_t brush_counter = 0;
+
+ fprintf(fp, "\"classname\" \"%s\"\n", entity->classname);
+
+ elist_for(key, entity->keys, list)
+ fprintf(fp, "\"%s\" \"%s\"\n", key->key, key->value);
+
+ elist_for(brush, entity->brushes, list) {
+ fprintf(fp, "// brush %zu\n{\n", brush_counter);
+ write_brush(fp, brush);
+ fprintf(fp, "}\n");
+ brush_counter++;
+ }
+
+ if (ferror(fp))
+ return -errno;
+
+ return 0;
+}
+
+int mapcat_save(const char *path)
+{
+ int rv = 1;
+ FILE *fp;
+ entity_t *entity;
+ size_t entity_counter = 1; // worldspawn is #0
+
+ fp = fopen(path, "w");
+ if (!fp) {
+ perror(path);
+ goto out;
+ }
+
+ if (!worldspawn) {
+ fprintf(stderr, "error: worldspawn is missing\n");
+ goto out;
+ }
+
+ fprintf(fp, "// entity 0\n{\n");
+ write_entity(fp, worldspawn);
+ fprintf(fp, "}\n");
+
+ elist_for(entity, entities, list) {
+ fprintf(fp, "// entity %zu\n{\n", entity_counter);
+
+ if (write_entity(fp, entity)) {
+ perror(path);
+ goto out;
+ }
+
+ fprintf(fp, "}\n");
+ entity_counter++;
+ }
+
+ if (ferror(fp) || fclose(fp)) {
+ perror(path);
+ goto out;
+ }
+
+ fp = NULL;
+ rv = 0;
+
+out:
+ if (fp)
+ fclose(fp);
+ return rv;
+}
+
+void mapcat_free(void)
+{
+ entity_t *entity, *next;
+
+ if (worldspawn)
+ free_entity(worldspawn);
+
+ for (entity = entities; entity; entity = next) {
+ next = elist_next(entity, list);
+ free_entity(entity);
+ }
+}