commit e0a00cc206a04e6b122e1f6c9d8d2ebfda689b9d
Author: averagecoder <averagecoder@noreply.codeberg.org>
Date: Mon, 29 Jun 2026 05:31:04 +0300
init: software render and asset compiler
Diffstat:
| A | .gitignore | | | 3 | +++ |
| A | Makefile | | | 30 | ++++++++++++++++++++++++++++++ |
| A | README.md | | | 76 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/build.c | | | 2023 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/render.c | | | 2271 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
5 files changed, 4403 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+assets.tar
+bin/
+gta/
diff --git a/Makefile b/Makefile
@@ -0,0 +1,30 @@
+CC = cc
+CFLAGS = -O3 -Wall -Wextra -Wpedantic -std=c99 -pthread -MMD -MP
+LDFLAGS = -lm -pthread
+
+X11_CFLAGS = -I/usr/X11R6/include -I/usr/local/include
+X11_LDFLAGS = -L/usr/X11R6/lib -L/usr/local/lib -lX11 -lXext
+
+BIN_DIR = bin
+SRC_DIR = src
+
+TARGETS = $(BIN_DIR)/build \
+ $(BIN_DIR)/render
+
+all: $(BIN_DIR) $(TARGETS)
+
+$(BIN_DIR):
+ mkdir -p $(BIN_DIR)
+
+$(BIN_DIR)/build: $(SRC_DIR)/build.c
+ $(CC) $(CFLAGS) -o $@ $(SRC_DIR)/build.c -lm
+
+$(BIN_DIR)/render: $(SRC_DIR)/render.c
+ $(CC) $(CFLAGS) $(X11_CFLAGS) -o $@ $(SRC_DIR)/render.c $(LDFLAGS) $(X11_LDFLAGS)
+
+clean:
+ rm -rf $(BIN_DIR) assets assets.tar
+
+.PHONY: all clean
+
+-include $(SRC_DIR)/*.d
diff --git a/README.md b/README.md
@@ -0,0 +1,76 @@
+# antizona
+
+A minimalist, high-performance open-source multiplayer engine and framework
+for Grand Theft Auto: San Andreas, written in pure C99.
+
+Antizona is designed as a lightweight, secure alternative to modern bloated SA-MP
+and MTA platforms. Instead of pay-to-win mechanics, heavy server-side scripts, and
+unoptimized custom assets, the project focuses on a deep, systemic, player-driven
+Roleplay (RP) environment. Every element of the world—from weapon manufacturing and
+logistics to crime and law enforcement—is simulated as an interconnected, logical
+cycle driven solely by player actions, with zero administrative or donation bloat.
+
+## Features
+
+- **Multi-threaded Asset Compiler**: Converts models (DFF) to PLY and textures (TXD)
+ to TGA directly from GTA3.IMG and GTA_INT.IMG.
+- **Two-Pass Compilation**: Fully loads the 14,929 model IDE database before parsing
+ any IPL map placement, ensuring 100% correct object resolution.
+- **Ultra-Lightweight Renderer**: Zero-dependency software rasterizer utilizing an
+ OpenSSL-secured network and local asset pipeline.
+- **Strict POSIX Compliance**: Written in pure C99 with zero third-party dependencies,
+ ensuring maximum portability and performance.
+
+## Roadmap
+
+### Phase 1: Local Collision & Player Physics
+- Implement efficient Ray-Triangle intersection using the existing spatial grid `scene_grid` for ground height snapping.
+- Build basic player movement physics: standard gravity and simple bounding-sphere world collisions.
+
+### Phase 2: Asynchronous UDP Networking
+- Implement a minimalist asynchronous UDP server (`server.c`) utilizing Linux `epoll` or BSD `kqueue` for sub-millisecond execution.
+- Serialize compact binary player state packets (ID, XYZ coordinates, Yaw, active actions).
+- Implement local spatial partitioning on the server to broadcast state packets only to visible neighboring players.
+
+### Phase 3: Player Synchronization & Rendering
+- Load dynamic player `.dff` skins (gang members, CJ) at runtime.
+- Render other players as dynamic meshes using the existing `draw_mesh` pipeline.
+- Implement a lightweight, low-overhead skeletal animation state machine.
+
+### Phase 4: Core Systemic Interaction
+- Low-latency hitscan raycasting calculated against target bounding spheres (`sphere_cx`, `sphere_cy`, `sphere_cz`, `sphere_r`).
+- Basic player-to-world interaction (inventory item pickups, server-side state verification).
+
+## Requirements
+
+- OpenSSL (libssl, libcrypto)
+- X11 (libX11, libXext)
+- POSIX threads (pthread)
+
+## Building
+
+To compile both the asset compiler and the software rasterizer:
+
+```bash
+make
+```
+
+## Usage
+
+1. Compile and pack the game assets:
+ ```bash
+ ./bin/build /path/to/gta_sa_directory
+ ```
+
+2. Run the software rasterizer:
+ ```bash
+ ./bin/render
+ ```
+
+## Support / Donations
+
+If you appreciate financial privacy and want to support the development of a fully
+independent, decentralized, and s_u_c_k_l_e_s_s GTA SA multiplayer engine, you can donate
+via **Monero (XMR)**:
+
+`83xqB91zW31gZkw9sbeT3vjcFQ22L69nKUyCEWgTnWYYfZ4E7gbEVAX5dWh6hCpk6RGqCatZUgjvzGiDYL9HZ6dVCWoa6Jh`
diff --git a/src/build.c b/src/build.c
@@ -0,0 +1,2023 @@
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <ctype.h>
+#include <dirent.h>
+#include <err.h>
+#include <fcntl.h>
+#include <math.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define MAX_FILES 32768
+#define NAME_SZ 24
+#define PATH_CACHE_SZ 16384
+#define PATH_DB_SZ 65536
+
+struct file_list {
+ char names[MAX_FILES][NAME_SZ];
+ int count;
+};
+
+struct img_header {
+ char magic[4];
+ uint32_t entries;
+};
+
+struct img_entry {
+ uint32_t offset;
+ uint16_t size;
+ uint16_t size2;
+ char name[24];
+};
+
+struct ide_entry {
+ int id;
+ char model[NAME_SZ];
+ char txd[NAME_SZ];
+ int time_on;
+ int time_off;
+};
+
+struct mstream {
+ const uint8_t *data;
+ size_t size;
+ size_t pos;
+};
+
+struct path_entry {
+ char key[512];
+ char resolved[1024];
+};
+
+struct rw_header {
+ uint32_t type;
+ uint32_t size;
+ uint32_t version;
+} __attribute__((packed));
+
+struct mat4 { float m[4][4]; };
+struct rw_triangle { uint16_t v1, v2, m, v3; };
+struct atomic_entry { uint32_t f, g; };
+struct tex_name { char name[32]; };
+
+struct water_face {
+ float x[4], y[4], z[4];
+ float u[4], v[4], h[4];
+ int num;
+};
+
+struct timecyc_entry {
+ uint8_t amb[3];
+ uint8_t dir[3];
+ uint8_t sky_top[3];
+ uint8_t sky_bot[3];
+ uint8_t water[4];
+ float far_clp;
+ float fog_st;
+};
+
+struct path_cache_entry {
+ char rel[512];
+ char resolved[1024];
+};
+
+struct arena {
+ uint8_t *mem;
+ size_t pos;
+ size_t size;
+};
+
+struct thread_arg {
+ const uint8_t *img_data;
+ struct img_entry *entries;
+ uint32_t start;
+ uint32_t end;
+ struct file_list *fl;
+ struct arena arena;
+};
+
+struct tga_header {
+ uint8_t id_length;
+ uint8_t color_map_type;
+ uint8_t image_type;
+ uint16_t color_map_first;
+ uint16_t color_map_length;
+ uint8_t color_map_entry_size;
+ uint16_t x_origin;
+ uint16_t y_origin;
+ uint16_t width;
+ uint16_t height;
+ uint8_t pixel_depth;
+ uint8_t image_descriptor;
+} __attribute__((packed));
+
+struct tar_header {
+ char name[100];
+ char mode[8];
+ char uid[8];
+ char gid[8];
+ char size[12];
+ char mtime[12];
+ char chksum[8];
+ char typeflag;
+ char linkname[100];
+ char magic[6];
+ char version[2];
+ char uname[32];
+ char gname[32];
+ char devmajor[8];
+ char devminor[8];
+ char prefix[155];
+ char pad[12];
+} __attribute__((packed));
+
+static struct ide_entry ide_db[65536];
+static int ide_count = 0;
+static struct path_entry path_db[PATH_DB_SZ];
+static int path_db_count = 0;
+
+static char existing_models[32768][NAME_SZ];
+static int existing_models_count = 0;
+static char existing_textures[32768][NAME_SZ];
+static int existing_textures_count = 0;
+
+static struct water_face water_db[8192];
+static int water_face_count = 0;
+
+static void
+to_lower_str(char *str)
+{
+ while (*str) {
+ *str = tolower((unsigned char)*str);
+ str++;
+ }
+}
+
+static void
+scan_existing_assets(void)
+{
+ DIR *dir;
+ struct dirent *de;
+ char *ext;
+ size_t len;
+
+ dir = opendir("assets/models");
+ if (dir) {
+ while ((de = readdir(dir))) {
+ ext = strrchr(de->d_name, '.');
+ if (ext && strcasecmp(ext, ".ply") == 0 &&
+ existing_models_count < 32768) {
+ len = ext - de->d_name;
+ if (len >= NAME_SZ) {
+ len = NAME_SZ - 1;
+ }
+ memcpy(existing_models[existing_models_count],
+ de->d_name, len);
+ existing_models[existing_models_count][len] = '\0';
+ to_lower_str(existing_models[existing_models_count]);
+ existing_models_count++;
+ }
+ }
+ closedir(dir);
+ }
+
+ dir = opendir("assets/textures");
+ if (dir) {
+ while ((de = readdir(dir))) {
+ ext = strrchr(de->d_name, '.');
+ if (ext && strcasecmp(ext, ".tga") == 0 &&
+ existing_textures_count < 32768) {
+ len = ext - de->d_name;
+ if (len >= NAME_SZ) {
+ len = NAME_SZ - 1;
+ }
+ memcpy(existing_textures[existing_textures_count],
+ de->d_name, len);
+ existing_textures[existing_textures_count][len] = '\0';
+ to_lower_str(existing_textures[existing_textures_count]);
+ existing_textures_count++;
+ }
+ }
+ closedir(dir);
+ }
+}
+
+static int
+asset_cached(const char *name, int is_model)
+{
+ char base[NAME_SZ];
+ size_t len;
+ int i;
+
+ len = strlen(name);
+ if (len < 5) {
+ return (0);
+ }
+
+ strncpy(base, name, len - 4);
+ base[len - 4] = '\0';
+ to_lower_str(base);
+
+ if (is_model) {
+ for (i = 0; i < existing_models_count; i++) {
+ if (strcmp(existing_models[i], base) == 0) {
+ return (1);
+ }
+ }
+ } else {
+ for (i = 0; i < existing_textures_count; i++) {
+ if (strcmp(existing_textures[i], base) == 0) {
+ return (1);
+ }
+ }
+ }
+ return (0);
+}
+
+static int
+asset_exists(const char *name)
+{
+ if (strstr(name, ".dff") || strstr(name, ".DFF")) {
+ return (asset_cached(name, 1));
+ }
+ if (strstr(name, ".txd") || strstr(name, ".TXD")) {
+ return (asset_cached(name, 0));
+ }
+ return (0);
+}
+
+static void *
+arena_alloc(struct arena *a, size_t sz)
+{
+ size_t align;
+ void *ptr;
+
+ align = (sz + 7) & ~7;
+ if (a->pos + align > a->size) {
+ errx(1, "OOM arena");
+ }
+ ptr = a->mem + a->pos;
+ a->pos += align;
+ return (ptr);
+}
+#define A_ALLOC(a, type, count) (type *)arena_alloc((a), sizeof(type) * (count))
+
+static void
+normalize_key(const char *src, char *dst, size_t sz)
+{
+ size_t i = 0;
+ while (*src && i < sz - 1) {
+ if (*src != '/' && *src != '\\') {
+ dst[i++] = tolower((unsigned char)*src);
+ }
+ src++;
+ }
+ dst[i] = '\0';
+}
+
+static void
+index_directory_tree(const char *dir_path)
+{
+ DIR *dir;
+ struct dirent *de;
+ char sub_path[1024];
+ struct stat st;
+
+ if (!(dir = opendir(dir_path))) {
+ return;
+ }
+
+ while ((de = readdir(dir))) {
+ if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) {
+ continue;
+ }
+
+ snprintf(sub_path, sizeof(sub_path), "%s/%s", dir_path, de->d_name);
+
+ if (stat(sub_path, &st) == 0) {
+ if (S_ISDIR(st.st_mode)) {
+ index_directory_tree(sub_path);
+ } else {
+ if (path_db_count < PATH_DB_SZ) {
+ normalize_key(sub_path, path_db[path_db_count].key, 512);
+ strncpy(path_db[path_db_count].resolved, sub_path, 1024);
+ path_db_count++;
+ }
+ }
+ }
+ }
+ closedir(dir);
+}
+
+
+static size_t
+mread(void *dst, size_t sz, size_t n, struct mstream *s)
+{
+ size_t bytes;
+
+ bytes = sz * n;
+ if (s->pos + bytes <= s->size) {
+ memcpy(dst, s->data + s->pos, bytes);
+ s->pos += bytes;
+ return (n);
+ }
+ return (0);
+}
+
+static void
+mat_identity(struct mat4 *m)
+{
+ memset(m, 0, sizeof(*m));
+ m->m[0][0] = 1.0f;
+ m->m[1][1] = 1.0f;
+ m->m[2][2] = 1.0f;
+ m->m[3][3] = 1.0f;
+}
+
+static inline void
+mat_mul(struct mat4 *dst, const struct mat4 *a, const struct mat4 *b)
+{
+ struct mat4 r;
+ int i;
+
+ for (i = 0; i < 4; i++) {
+ r.m[i][0] = a->m[i][0]*b->m[0][0] + a->m[i][1]*b->m[1][0] +
+ a->m[i][2]*b->m[2][0] + a->m[i][3]*b->m[3][0];
+ r.m[i][1] = a->m[i][0]*b->m[0][1] + a->m[i][1]*b->m[1][1] +
+ a->m[i][2]*b->m[2][1] + a->m[i][3]*b->m[3][1];
+ r.m[i][2] = a->m[i][0]*b->m[0][2] + a->m[i][1]*b->m[1][2] +
+ a->m[i][2]*b->m[2][2] + a->m[i][3]*b->m[3][2];
+ r.m[i][3] = a->m[i][0]*b->m[0][3] + a->m[i][1]*b->m[1][3] +
+ a->m[i][2]*b->m[2][3] + a->m[i][3]*b->m[3][3];
+ }
+ *dst = r;
+}
+
+static int
+resolve_ci(const char *root, const char *rel, char *out, size_t out_sz)
+{
+ char target_key[1024];
+ char combined[1536];
+ int i;
+
+ snprintf(combined, sizeof(combined), "%s/%s", root, rel);
+ normalize_key(combined, target_key, sizeof(target_key));
+
+ for (i = 0; i < path_db_count; i++) {
+ if (strcmp(path_db[i].key, target_key) == 0) {
+ strncpy(out, path_db[i].resolved, out_sz - 1);
+ out[out_sz - 1] = '\0';
+ return (1);
+ }
+ }
+ return (0);
+}
+
+static FILE *
+fopen_ci(const char *root, const char *rel)
+{
+ char resolved[1024];
+
+ if (resolve_ci(root, rel, resolved, sizeof(resolved))) {
+ return (fopen(resolved, "rb"));
+ }
+ return (NULL);
+}
+
+static void
+trim_spaces(char *str)
+{
+ char *start, *end;
+
+ start = str;
+ while (*start == ' ' || *start == '\t') {
+ start++;
+ }
+ if (start != str) {
+ memmove(str, start, strlen(start) + 1);
+ }
+ end = str + strlen(str) - 1;
+ while (end > str && (*end == ' ' || *end == '\t' || *end == '\r' ||
+ *end == '\n')) {
+ *end = '\0';
+ end--;
+ }
+}
+
+static int
+list_has(struct file_list *l, const char *name)
+{
+ int i;
+
+ for (i = 0; i < l->count; i++) {
+ if (strcasecmp(l->names[i], name) == 0) {
+ return (1);
+ }
+ }
+ return (0);
+}
+
+static void
+list_add(struct file_list *l, const char *name)
+{
+ size_t len, i;
+
+ if (l->count >= MAX_FILES || list_has(l, name)) {
+ return;
+ }
+ len = strlen(name);
+ if (len >= NAME_SZ) {
+ len = NAME_SZ - 1;
+ }
+ for (i = 0; i < len; i++) {
+ l->names[l->count][i] = tolower((unsigned char)name[i]);
+ }
+ l->names[l->count][len] = '\0';
+ l->count++;
+}
+
+static void
+clean_ide_line(char *p)
+{
+ char *comment;
+ char *end;
+
+ comment = strchr(p, '#');
+ if (comment) {
+ *comment = '\0';
+ }
+
+ end = p + strlen(p) - 1;
+ while (end >= p && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
+ *end = '\0';
+ end--;
+ }
+}
+
+static void
+preparse_ide(const char *root, const char *rel)
+{
+ FILE *f;
+ char line[512];
+ char *p;
+ int in_tobj;
+
+ f = fopen_ci(root, rel);
+ if (!f) {
+ return;
+ }
+
+ in_tobj = 0;
+
+ while (fgets(line, sizeof(line), f)) {
+ p = line;
+ line[strcspn(line, "\r\n")] = '\0';
+ while (*p == ' ' || *p == '\t') {
+ p++;
+ }
+ if (*p == '\0' || *p == '#') {
+ continue;
+ }
+
+ if (strncasecmp(p, "objs", 4) == 0) {
+ in_tobj = 0;
+ continue;
+ }
+ if (strncasecmp(p, "tobj", 4) == 0) {
+ in_tobj = 1;
+ continue;
+ }
+ if (strncasecmp(p, "end", 3) == 0) {
+ in_tobj = 0;
+ continue;
+ }
+
+ if (isdigit((unsigned char)p[0]) && ide_count < 65536) {
+ char model[NAME_SZ], txd[NAME_SZ];
+ char *c1, *c2, *c3;
+ char *last_comma, *prev_comma;
+ size_t len;
+ int time_on = 0, time_off = 0;
+
+ c1 = strchr(p, ',');
+ if (!c1) {
+ continue;
+ }
+ c2 = strchr(c1 + 1, ',');
+ if (!c2) {
+ continue;
+ }
+ c3 = strchr(c2 + 1, ',');
+ if (!c3) {
+ continue;
+ }
+
+ len = c2 - (c1 + 1);
+ if (len >= NAME_SZ) {
+ len = NAME_SZ - 1;
+ }
+ memcpy(model, c1 + 1, len);
+ model[len] = '\0';
+ trim_spaces(model);
+ to_lower_str(model);
+
+ len = c3 - (c2 + 1);
+ if (len >= NAME_SZ) {
+ len = NAME_SZ - 1;
+ }
+ memcpy(txd, c2 + 1, len);
+ txd[len] = '\0';
+ trim_spaces(txd);
+ to_lower_str(txd);
+
+ if (in_tobj) {
+ char temp_line[512];
+ strncpy(temp_line, p, sizeof(temp_line) - 1);
+ temp_line[sizeof(temp_line) - 1] = '\0';
+
+ /* sanitize trailing spaces and comments first */
+ clean_ide_line(temp_line);
+
+ last_comma = strrchr(temp_line, ',');
+ if (last_comma) {
+ time_off = atoi(last_comma + 1);
+ *last_comma = '\0';
+ prev_comma = strrchr(temp_line, ',');
+ if (prev_comma) {
+ time_on = atoi(prev_comma + 1);
+ }
+ *last_comma = ',';
+ }
+ }
+
+ ide_db[ide_count].id = atoi(p);
+ strncpy(ide_db[ide_count].model, model, NAME_SZ);
+ strncpy(ide_db[ide_count].txd, txd, NAME_SZ);
+ ide_db[ide_count].time_on = time_on;
+ ide_db[ide_count].time_off = time_off;
+ ide_count++;
+ }
+ }
+ fclose(f);
+}
+
+static const struct ide_entry *
+ide_lookup(int id)
+{
+ int i;
+
+ for (i = 0; i < ide_count; i++) {
+ if (ide_db[i].id == id) {
+ return (&ide_db[i]);
+ }
+ }
+ return (NULL);
+}
+
+static int
+is_lod_name(const char *name)
+{
+ return (strstr(name, "lod") != NULL);
+}
+
+static void
+parse_text_ipl(const char *buf, size_t size, FILE *out, struct file_list *fl)
+{
+ const char *p, *end, *next;
+ char line[512];
+ char *s, *comma;
+ size_t len;
+ int in_inst;
+
+ p = buf;
+ end = buf + size;
+ in_inst = 0;
+
+ while (p < end) {
+ next = memchr(p, '\n', end - p);
+ len = next ? (size_t)(next - p) : (size_t)(end - p);
+ if (len >= sizeof(line)) {
+ len = sizeof(line) - 1;
+ }
+ memcpy(line, p, len);
+ line[len] = '\0';
+ p = next ? next + 1 : end;
+
+ s = line;
+ while (*s == ' ' || *s == '\t' || *s == '\r') {
+ s++;
+ }
+ if (*s == '\0' || *s == '#') {
+ continue;
+ }
+
+ if (strncasecmp(s, "inst", 4) == 0 || strncasecmp(s, "tobj", 4) == 0) {
+ in_inst = 1;
+ fprintf(out, "\ninst\n");
+ continue;
+ }
+ if (strncasecmp(s, "end", 3) == 0) {
+ in_inst = 0;
+ fprintf(out, "end\n");
+ continue;
+ }
+
+ if (in_inst) {
+ int id;
+ const struct ide_entry *ide;
+
+ id = atoi(s);
+ ide = ide_lookup(id);
+ if (ide && !is_lod_name(ide->model)) {
+ char d_name[32], t_name[32];
+
+ snprintf(d_name, sizeof(d_name), "%s.dff", ide->model);
+ snprintf(t_name, sizeof(t_name), "%s.txd", ide->txd);
+ list_add(fl, d_name);
+ list_add(fl, t_name);
+
+ comma = strchr(s, ',');
+ if (comma) {
+ int dummy_id;
+ char dummy_model[64];
+ int interior;
+ float px, py, pz, rx, ry, rz, rw;
+
+ if (sscanf(s, "%d, %63[^,], %d, %f, "
+ "%f, %f, %f, %f, %f, %f",
+ &dummy_id, dummy_model, &interior,
+ &px, &py, &pz, &rx, &ry, &rz, &rw) == 10) {
+ fprintf(out, "%d, %s, %d, %f, "
+ "%f, %f, %f, %f, %f, %f, "
+ "-1, %d, %d\n",
+ id, ide->model, interior,
+ px, py, pz, rx, ry, rz, rw,
+ ide->time_on, ide->time_off);
+ }
+ }
+ }
+ }
+ }
+}
+
+static void
+parse_binary_ipl(const uint8_t *buf, size_t size, FILE *out, struct file_list *fl)
+{
+ uint32_t num, off, i;
+
+ memcpy(&num, buf + 4, 4);
+ memcpy(&off, buf + 28, 4);
+ if (off + num * 40 > size) {
+ return;
+ }
+
+ fprintf(out, "\ninst\n");
+ for (i = 0; i < num; i++) {
+ struct {
+ float px, py, pz, rx, ry, rz, rw;
+ int32_t id, interior, lod;
+ } inst;
+ const struct ide_entry *ide;
+
+ memcpy(&inst, buf + off + i * 40, 40);
+
+ ide = ide_lookup(inst.id);
+ if (ide && !is_lod_name(ide->model)) {
+ char d_name[32], t_name[32];
+
+ snprintf(d_name, sizeof(d_name), "%s.dff", ide->model);
+ snprintf(t_name, sizeof(t_name), "%s.txd", ide->txd);
+ list_add(fl, d_name);
+ list_add(fl, t_name);
+
+ fprintf(out, "%d, %s, %d, %f, %f, %f, %f, %f, %f, %f, "
+ "-1, %d, %d\n",
+ inst.id, ide->model, inst.interior,
+ inst.px, inst.py, inst.pz, inst.rx, inst.ry,
+ inst.rz, inst.rw, ide->time_on, ide->time_off);
+ }
+ }
+ fprintf(out, "end\n");
+}
+
+static void
+blur_pixels(uint32_t *pixels, int w, int h)
+{
+ uint32_t *temp;
+ int x, y, kx, ky, px, py, count;
+ uint32_t r_sum, g_sum, b_sum, a_sum, c;
+
+ temp = malloc(w * h * 4);
+ if (!temp) {
+ return;
+ }
+ memcpy(temp, pixels, w * h * 4);
+
+ for (y = 0; y < h; y++) {
+ for (x = 0; x < w; x++) {
+ r_sum = 0;
+ g_sum = 0;
+ b_sum = 0;
+ a_sum = 0;
+ count = 0;
+ for (ky = -1; ky <= 1; ky++) {
+ for (kx = -1; kx <= 1; kx++) {
+ px = x + kx;
+ py = y + ky;
+ if (px >= 0 && px < w && py >= 0 &&
+ py < h) {
+ c = temp[py * w + px];
+ b_sum += (c & 0xFF);
+ g_sum += ((c >> 8) & 0xFF);
+ r_sum += ((c >> 16) & 0xFF);
+ a_sum += ((c >> 24) & 0xFF);
+ count++;
+ }
+ }
+ }
+ pixels[y * w + x] = (b_sum/count) |
+ ((g_sum/count) << 8) | ((r_sum/count) << 16) |
+ ((a_sum/count) << 24);
+ }
+ }
+ free(temp);
+}
+
+static int
+nearest_pow2(int x)
+{
+ int p = 1;
+
+ while (p * 2 <= x) {
+ p *= 2;
+ }
+ if (x - p < p * 2 - x) {
+ return (p);
+ }
+ return (p * 2);
+}
+
+static void
+convert_txd(const uint8_t *data, size_t size, const char *name, struct arena *a)
+{
+ struct mstream ms = { data, size, 0 };
+ struct rw_header h, sh;
+ struct {
+ uint32_t plt, flt; char n[32], m[32]; uint32_t fmt; char fcc[4];
+ uint16_t w, h; uint8_t d, lvl, typ, cmp;
+ } __attribute__((packed)) rh;
+ uint32_t mip_size, p_size, *pixels, code, cols[4];
+ uint8_t *cmp, r0, g0, b0, r1, g1, b1, a_val, *src;
+ uint16_t c0, c1, *src16;
+ size_t chunk_end;
+ int bx, by, is_dxt3, x, y, i, j, k;
+ char t_name[32], out_name[256];
+ FILE *out;
+ struct tga_header out_hdr;
+ uint32_t *final_pixels;
+ int new_w, new_h, sy, sx;
+
+ while (mread(&h, sizeof(h), 1, &ms) == 1) {
+ chunk_end = ms.pos + h.size;
+ if (h.type == 0x16) {
+ continue;
+ }
+
+ if (h.type == 0x15) {
+ if (mread(&sh, sizeof(sh), 1, &ms) != 1) {
+ break;
+ }
+ if (mread(&rh, sizeof(rh), 1, &ms) != 1) {
+ break;
+ }
+
+ if (rh.plt != 9 && rh.plt != 8) {
+ ms.pos = chunk_end;
+ continue;
+ }
+
+ if (mread(&mip_size, 4, 1, &ms) != 1) {
+ break;
+ }
+
+ p_size = rh.w * rh.h;
+ pixels = A_ALLOC(a, uint32_t, p_size);
+ memset(pixels, 0, p_size * 4);
+
+ if (memcmp(rh.fcc, "DXT1", 4) == 0 ||
+ memcmp(rh.fcc, "DXT3", 4) == 0) {
+ cmp = A_ALLOC(a, uint8_t, mip_size);
+ mread(cmp, 1, mip_size, &ms);
+ bx = rh.w / 4;
+ by = rh.h / 4;
+ is_dxt3 = memcmp(rh.fcc, "DXT3", 4) == 0;
+
+ for (y = 0; y < by; y++) {
+ for (x = 0; x < bx; x++) {
+ const uint8_t *blk = cmp +
+ (y * bx + x) * (is_dxt3 ? 16 : 8);
+ const uint8_t *cblk = is_dxt3 ?
+ blk + 8 : blk;
+
+ c0 = cblk[0] | (cblk[1] << 8);
+ c1 = cblk[2] | (cblk[3] << 8);
+ code = cblk[4] | (cblk[5] << 8) |
+ (cblk[6] << 16) | (cblk[7] << 24);
+
+ r0 = ((c0 >> 11) & 0x1F) << 3;
+ g0 = ((c0 >> 5) & 0x3F) << 2;
+ b0 = (c0 & 0x1F) << 3;
+ r1 = ((c1 >> 11) & 0x1F) << 3;
+ g1 = ((c1 >> 5) & 0x3F) << 2;
+ b1 = (c1 & 0x1F) << 3;
+
+ cols[0] = b0 | (g0 << 8) |
+ (r0 << 16) | (0xFFU << 24);
+ cols[1] = b1 | (g1 << 8) |
+ (r1 << 16) | (0xFFU << 24);
+ if (c0 > c1 || is_dxt3) {
+ cols[2] = ((2*b0+b1)/3) |
+ (((2*g0+g1)/3)<<8) |
+ (((2*r0+r1)/3)<<16) |
+ (0xFFU<<24);
+ cols[3] = ((b0+2*b1)/3) |
+ (((g0+2*g1)/3)<<8) |
+ (((r0+2*r1)/3)<<16) |
+ (0xFFU<<24);
+ } else {
+ cols[2] = ((b0+b1)/2) |
+ (((g0+g1)/2)<<8) |
+ (((r0+r1)/2)<<16) |
+ (0xFFU<<24);
+ cols[3] = 0;
+ }
+
+ for (i = 0; i < 4; i++) {
+ for (j = 0; j < 4; j++) {
+ int px_idx = x * 4 + j;
+ int py_idx = y * 4 + i;
+ if (px_idx < rh.w &&
+ py_idx < rh.h) {
+ uint32_t c = cols[(code >> (2 * (i * 4 + j))) & 3];
+ if (is_dxt3) {
+ a_val = (blk[i * 2 + (j / 2)] >> (4 * (j % 2))) & 0x0F;
+ a_val = (a_val << 4) | a_val;
+ c = (c & 0x00FFFFFF) | ((uint32_t)a_val << 24);
+ }
+ pixels[py_idx * rh.w + px_idx] = c;
+ }
+ }
+ }
+ }
+ }
+ } else if (rh.d == 32) {
+ mread(pixels, 1, mip_size, &ms);
+ if (rh.fmt == 22) {
+ for (i = 0; (uint32_t)i < p_size; i++) {
+ pixels[i] |= (255U << 24);
+ }
+ }
+ } else if (rh.d == 16) {
+ src16 = malloc(mip_size);
+ if (src16) {
+ mread(src16, 1, mip_size, &ms);
+ for (i = 0; (uint32_t)i < p_size; i++) {
+ uint16_t val = src16[i];
+ uint8_t r = 0, g = 0, b = 0, a_v = 255;
+ if (rh.fmt == 26) {
+ a_v = ((val >> 12) & 0x0F) * 17;
+ r = ((val >> 8) & 0x0F) * 17;
+ g = ((val >> 4) & 0x0F) * 17;
+ b = (val & 0x0F) * 17;
+ } else if (rh.fmt == 25) {
+ a_v = ((val >> 15) & 0x01) ? 255 : 0;
+ r = ((val >> 10) & 0x1F) << 3;
+ g = ((val >> 5) & 0x1F) << 3;
+ b = (val & 0x1F) << 3;
+ r |= (r >> 5); g |= (g >> 5); b |= (b >> 5);
+ } else if (rh.fmt == 23) {
+ r = ((val >> 11) & 0x1F) << 3;
+ g = ((val >> 5) & 0x3F) << 2;
+ b = (val & 0x1F) << 3;
+ r |= (r >> 5); g |= (g >> 6); b |= (b >> 5);
+ a_v = 255;
+ }
+ pixels[i] = (a_v << 24) | (r << 16) | (g << 8) | b;
+ }
+ free(src16);
+ }
+ } else if (rh.d == 24) {
+ src = malloc(mip_size);
+ if (src) {
+ mread(src, 1, mip_size, &ms);
+ for (i = 0; (uint32_t)i < p_size; i++) {
+ pixels[i] = (255U << 24) |
+ (src[i*3+2] << 16) |
+ (src[i*3+1] << 8) | src[i*3+0];
+ }
+ free(src);
+ }
+ }
+
+ for (k = 0; k < 31 && rh.n[k]; k++) {
+ t_name[k] = tolower((unsigned char)rh.n[k]);
+ }
+ t_name[k] = '\0';
+ if (t_name[0] == '\0') {
+ strncpy(t_name, name, 31);
+ t_name[31] = '\0';
+ }
+ to_lower_str(t_name);
+
+ if (strstr(t_name, "waterclear256")) {
+ blur_pixels(pixels, rh.w, rh.h);
+ blur_pixels(pixels, rh.w, rh.h);
+ }
+
+ /* smart resize to power of two on compiling */
+ new_w = nearest_pow2(rh.w);
+ new_h = nearest_pow2(rh.h);
+ final_pixels = pixels;
+
+ if (new_w != rh.w || new_h != rh.h) {
+ final_pixels = malloc(new_w * new_h * 4);
+ if (final_pixels) {
+ for (y = 0; y < new_h; y++) {
+ sy = (y * rh.h) / new_h;
+ if (sy >= rh.h) {
+ sy = rh.h - 1;
+ }
+ for (x = 0; x < new_w; x++) {
+ sx = (x * rh.w) / new_w;
+ if (sx >= rh.w) {
+ sx = rh.w - 1;
+ }
+ final_pixels[y * new_w + x] = pixels[sy * rh.w + sx];
+ }
+ }
+ } else {
+ final_pixels = pixels;
+ new_w = rh.w;
+ new_h = rh.h;
+ }
+ }
+
+ snprintf(out_name, sizeof(out_name), "assets/textures/%s.tga", t_name);
+ out = fopen(out_name, "wb");
+ if (out) {
+ memset(&out_hdr, 0, sizeof(out_hdr));
+ out_hdr.image_type = 2;
+ out_hdr.width = new_w;
+ out_hdr.height = new_h;
+ out_hdr.pixel_depth = 32;
+ out_hdr.image_descriptor = 8;
+
+ fwrite(&out_hdr, sizeof(out_hdr), 1, out);
+ fwrite(final_pixels, 4, new_w * new_h, out);
+ fclose(out);
+ }
+
+ if (final_pixels != pixels) {
+ free(final_pixels);
+ }
+ }
+ ms.pos = chunk_end;
+ }
+}
+
+static void
+process_particle_txd(const char *root, struct arena *a)
+{
+ FILE *f;
+ size_t sz;
+ uint8_t *buf;
+
+ f = fopen_ci(root, "models/particle.txd");
+ if (!f) {
+ return;
+ }
+ fseek(f, 0, SEEK_END);
+ sz = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ buf = malloc(sz);
+ if (fread(buf, 1, sz, f) == sz) {
+ a->pos = 0;
+ convert_txd(buf, sz, "particle", a);
+ }
+ free(buf);
+ fclose(f);
+}
+
+struct frame { struct mat4 abs; int32_t parent; };
+struct geom { float *v, *u; uint8_t *c; uint16_t *i, *m; uint32_t nv, ni, nm; struct tex_name *t; };
+
+static int
+is_container(uint32_t type)
+{
+ switch (type) {
+ case 0x03: case 0x06: case 0x07: case 0x08: case 0x0E: case 0x0F:
+ case 0x10: case 0x14: case 0x1A: case 0x1B:
+ return (1);
+ }
+ return (0);
+}
+
+static void
+convert_dff(const uint8_t *data, size_t size, const char *name, struct arena *a)
+{
+ struct mstream ms = { data, size, 0 };
+ struct rw_header h;
+ struct frame *frames = NULL;
+ struct geom *geoms = NULL;
+ struct atomic_entry *atomics;
+ uint32_t n_frames = 0, n_geoms = 0, n_atomics = 0;
+ uint32_t last_cont = 0;
+ int32_t cur_g = -1, cur_m = -1, wait_tex = 0;
+ size_t end;
+
+ atomics = A_ALLOC(a, struct atomic_entry, 256);
+
+ while (mread(&h, sizeof(h), 1, &ms) == 1) {
+ end = ms.pos + h.size;
+
+ if (is_container(h.type)) {
+ last_cont = h.type;
+ if (h.type == 0x0F) cur_g++;
+ if (h.type == 0x08) cur_m = -1;
+ if (h.type == 0x07) cur_m++;
+ if (h.type == 0x06) wait_tex = 1;
+ continue;
+ }
+
+ if (h.type == 0x01) {
+ if (last_cont == 0x0E) {
+ mread(&n_frames, 4, 1, &ms);
+ frames = A_ALLOC(a, struct frame, n_frames);
+ for (uint32_t i = 0; i < n_frames; i++) {
+ float rot[9], pos[3]; int32_t parent, flags;
+ mread(rot, 4, 9, &ms); mread(pos, 4, 3, &ms);
+ mread(&parent, 4, 1, &ms); mread(&flags, 4, 1, &ms);
+
+ struct mat4 l; memset(&l, 0, sizeof(l));
+ l.m[0][0] = rot[0]; l.m[0][1] = rot[3]; l.m[0][2] = rot[6];
+ l.m[1][0] = rot[1]; l.m[1][1] = rot[4]; l.m[1][2] = rot[7];
+ l.m[2][0] = rot[2]; l.m[2][1] = rot[5]; l.m[2][2] = rot[8];
+ l.m[0][3] = pos[0]; l.m[1][3] = pos[1]; l.m[2][3] = pos[2]; l.m[3][3] = 1.0f;
+
+ if (parent >= 0 && parent < (int32_t)i) {
+ mat_mul(&frames[i].abs, &frames[parent].abs, &l);
+ } else {
+ float sx = sqrtf(l.m[0][0]*l.m[0][0] + l.m[1][0]*l.m[1][0] + l.m[2][0]*l.m[2][0]);
+ float sy = sqrtf(l.m[0][1]*l.m[0][1] + l.m[1][1]*l.m[1][1] + l.m[2][1]*l.m[2][1]);
+ float sz = sqrtf(l.m[0][2]*l.m[0][2] + l.m[1][2]*l.m[1][2] + l.m[2][2]*l.m[2][2]);
+ mat_identity(&frames[i].abs);
+ frames[i].abs.m[0][0] = sx;
+ frames[i].abs.m[1][1] = sy;
+ frames[i].abs.m[2][2] = sz;
+ }
+ }
+ } else if (last_cont == 0x1A) {
+ mread(&n_geoms, 4, 1, &ms);
+ if (n_geoms) {
+ geoms = A_ALLOC(a, struct geom, n_geoms);
+ }
+ cur_g = -1;
+ } else if (last_cont == 0x0F && cur_g >= 0) {
+ struct geom *g = &geoms[cur_g];
+ uint32_t fmt, morphs;
+ mread(&fmt, 4, 1, &ms); mread(&g->ni, 4, 1, &ms);
+ mread(&g->nv, 4, 1, &ms); mread(&morphs, 4, 1, &ms);
+
+ if (g->nv > 0 && g->ni > 0) {
+ uint16_t flg = fmt & 0xFFFF, tex_c = (fmt & 0x00FF0000) >> 16;
+ if (tex_c == 0 && (flg & 0x0004)) {
+ tex_c = 1;
+ }
+
+ g->c = NULL;
+ if (flg & 0x0008) {
+ g->c = A_ALLOC(a, uint8_t, g->nv * 4);
+ mread(g->c, 1, g->nv * 4, &ms);
+ }
+
+ g->u = A_ALLOC(a, float, g->nv * 2);
+ if (tex_c > 0) {
+ mread(g->u, sizeof(float) * 2, g->nv, &ms);
+ if (tex_c > 1) {
+ ms.pos += (tex_c - 1) * sizeof(float) * 2 * g->nv;
+ }
+ }
+
+ struct rw_triangle *tris = A_ALLOC(a, struct rw_triangle, g->ni);
+ mread(tris, sizeof(*tris), g->ni, &ms);
+ ms.pos += sizeof(float) * 4;
+
+ int32_t has_v, has_n;
+ mread(&has_v, 4, 1, &ms); mread(&has_n, 4, 1, &ms);
+ if (has_v) {
+ g->v = A_ALLOC(a, float, g->nv * 3);
+ mread(g->v, sizeof(float) * 3, g->nv, &ms);
+ }
+
+ g->i = A_ALLOC(a, uint16_t, g->ni * 3);
+ g->m = A_ALLOC(a, uint16_t, g->ni);
+ for (uint32_t j = 0; j < g->ni; j++) {
+ g->i[j*3+0] = tris[j].v1;
+ g->i[j*3+1] = tris[j].v2;
+ g->i[j*3+2] = tris[j].v3;
+ g->m[j] = tris[j].m;
+ }
+ }
+ } else if (last_cont == 0x08 && cur_g >= 0) {
+ mread(&geoms[cur_g].nm, 4, 1, &ms);
+ if (geoms[cur_g].nm > 0) {
+ geoms[cur_g].t = A_ALLOC(a, struct tex_name, geoms[cur_g].nm);
+ }
+ } else if (last_cont == 0x14) {
+ uint32_t f_idx, g_idx, flg, unused;
+ mread(&f_idx, 4, 1, &ms); mread(&g_idx, 4, 1, &ms);
+ mread(&flg, 4, 1, &ms); mread(&unused, 4, 1, &ms);
+ if (n_atomics < 256) {
+ atomics[n_atomics].f = f_idx;
+ atomics[n_atomics].g = g_idx;
+ n_atomics++;
+ }
+ }
+ last_cont = 0;
+ } else if (h.type == 0x02 && wait_tex && cur_g >= 0) {
+ struct geom *g = &geoms[cur_g];
+ if (cur_m >= 0 && (uint32_t)cur_m < g->nm) {
+ size_t len = h.size < 31 ? h.size : 31;
+ mread(g->t[cur_m].name, 1, len, &ms);
+ for (size_t k = 0; k < len; k++) {
+ g->t[cur_m].name[k] = tolower((unsigned char)g->t[cur_m].name[k]);
+ }
+ }
+ wait_tex = 0;
+ }
+
+ ms.pos = end;
+ }
+
+ uint32_t t_nv = 0, t_ni = 0, t_nm = 0;
+ int has_any_colors = 0;
+ for (uint32_t i = 0; i < n_atomics; i++) {
+ if (atomics[i].g < n_geoms) {
+ t_nv += geoms[atomics[i].g].nv;
+ t_ni += geoms[atomics[i].g].ni;
+ t_nm += geoms[atomics[i].g].nm;
+ if (geoms[atomics[i].g].c) {
+ has_any_colors = 1;
+ }
+ }
+ }
+
+ if (t_nv > 0 && t_ni > 0) {
+ float *f_v = A_ALLOC(a, float, t_nv * 3);
+ float *f_u = A_ALLOC(a, float, t_nv * 2);
+ uint8_t *f_c = A_ALLOC(a, uint8_t, t_nv * 4);
+ uint16_t *f_i = A_ALLOC(a, uint16_t, t_ni * 3);
+ uint16_t *f_m = A_ALLOC(a, uint16_t, t_ni);
+ struct tex_name *f_t = t_nm > 0 ? A_ALLOC(a, struct tex_name, t_nm) : NULL;
+
+ uint32_t vo = 0, to = 0, mo = 0;
+ for (uint32_t i = 0; i < n_atomics; i++) {
+ if (atomics[i].g >= n_geoms || atomics[i].f >= n_frames) {
+ continue;
+ }
+ struct geom *g = &geoms[atomics[i].g];
+ struct frame *fr = &frames[atomics[i].f];
+
+ for (uint32_t j = 0; j < g->nv; j++) {
+ float x = g->v[j*3+0], y = g->v[j*3+1], z = g->v[j*3+2];
+ f_v[(vo+j)*3+0] = fr->abs.m[0][0]*x + fr->abs.m[0][1]*y + fr->abs.m[0][2]*z + fr->abs.m[0][3];
+ f_v[(vo+j)*3+1] = fr->abs.m[1][0]*x + fr->abs.m[1][1]*y + fr->abs.m[1][2]*z + fr->abs.m[1][3];
+ f_v[(vo+j)*3+2] = fr->abs.m[2][0]*x + fr->abs.m[2][1]*y + fr->abs.m[2][2]*z + fr->abs.m[2][3];
+ f_u[(vo+j)*2+0] = g->u[j*2+0];
+ f_u[(vo+j)*2+1] = g->u[j*2+1];
+ if (g->c) {
+ f_c[(vo+j)*4+0] = g->c[j*4+0];
+ f_c[(vo+j)*4+1] = g->c[j*4+1];
+ f_c[(vo+j)*4+2] = g->c[j*4+2];
+ f_c[(vo+j)*4+3] = g->c[j*4+3];
+ } else {
+ f_c[(vo+j)*4+0] = 255;
+ f_c[(vo+j)*4+1] = 255;
+ f_c[(vo+j)*4+2] = 255;
+ f_c[(vo+j)*4+3] = 255;
+ }
+ }
+ for (uint32_t j = 0; j < g->ni; j++) {
+ f_i[(to+j)*3+0] = g->i[j*3+0] + vo;
+ f_i[(to+j)*3+1] = g->i[j*3+1] + vo;
+ f_i[(to+j)*3+2] = g->i[j*3+2] + vo;
+ f_m[to+j] = g->m[j] + mo;
+ }
+ for (uint32_t j = 0; j < g->nm; j++) {
+ memcpy(f_t[mo+j].name, g->t[j].name, 32);
+ }
+ vo += g->nv;
+ to += g->ni;
+ mo += g->nm;
+ }
+
+ float b_cx = 0, b_cy = 0, b_cz = 0, b_r = 0;
+ float minx = f_v[0], maxx = f_v[0], miny = f_v[1], maxy = f_v[1], minz = f_v[2], maxz = f_v[2];
+ for (uint32_t i = 1; i < t_nv; i++) {
+ if (f_v[i*3+0] < minx) minx = f_v[i*3+0];
+ if (f_v[i*3+0] > maxx) maxx = f_v[i*3+0];
+ if (f_v[i*3+1] < miny) miny = f_v[i*3+1];
+ if (f_v[i*3+1] > maxy) maxy = f_v[i*3+1];
+ if (f_v[i*3+2] < minz) minz = f_v[i*3+2];
+ if (f_v[i*3+2] > maxz) maxz = f_v[i*3+2];
+ }
+ b_cx = (minx+maxx)*0.5f;
+ b_cy = (miny+maxy)*0.5f;
+ b_cz = (minz+maxz)*0.5f;
+ for (uint32_t i = 0; i < t_nv; i++) {
+ float dx = f_v[i*3+0] - b_cx, dy = f_v[i*3+1] - b_cy, dz = f_v[i*3+2] - b_cz;
+ float d2 = dx*dx + dy*dy + dz*dz;
+ if (d2 > b_r) {
+ b_r = d2;
+ }
+ }
+ b_r = sqrtf(b_r);
+
+ char out_name[256];
+ snprintf(out_name, sizeof(out_name), "assets/models/%s.ply", name);
+ FILE *out = fopen(out_name, "wb");
+ if (out) {
+ fprintf(out, "ply\n");
+ fprintf(out, "format binary_little_endian 1.0\n");
+ fprintf(out, "comment spr_cx %f\n", b_cx);
+ fprintf(out, "comment spr_cy %f\n", b_cy);
+ fprintf(out, "comment spr_cz %f\n", b_cz);
+ fprintf(out, "comment spr_r %f\n", b_r);
+ fprintf(out, "comment flags %u\n", has_any_colors ? 1 : 0);
+ for (uint32_t i = 0; i < t_nm; i++) {
+ fprintf(out, "comment texture %s\n", f_t[i].name);
+ }
+ fprintf(out, "element vertex %u\n", t_nv);
+ fprintf(out, "property float x\n");
+ fprintf(out, "property float y\n");
+ fprintf(out, "property float z\n");
+ fprintf(out, "property float s\n");
+ fprintf(out, "property float t\n");
+ fprintf(out, "property uchar red\n");
+ fprintf(out, "property uchar green\n");
+ fprintf(out, "property uchar blue\n");
+ fprintf(out, "property uchar alpha\n");
+ fprintf(out, "element face %u\n", t_ni);
+ fprintf(out, "property list uchar int vertex_indices\n");
+ fprintf(out, "property ushort material_index\n");
+ fprintf(out, "end_header\n");
+
+ for (uint32_t i = 0; i < t_nv; i++) {
+ float vx = f_v[i*3+0], vy = f_v[i*3+1], vz = f_v[i*3+2];
+ float tu = f_u[i*2+0], tv = f_u[i*2+1];
+ uint8_t cr = f_c[i*4+0], cg = f_c[i*4+1], cb = f_c[i*4+2], ca = f_c[i*4+3];
+ fwrite(&vx, 4, 1, out);
+ fwrite(&vy, 4, 1, out);
+ fwrite(&vz, 4, 1, out);
+ fwrite(&tu, 4, 1, out);
+ fwrite(&tv, 4, 1, out);
+ fwrite(&cr, 1, 1, out);
+ fwrite(&cg, 1, 1, out);
+ fwrite(&cb, 1, 1, out);
+ fwrite(&ca, 1, 1, out);
+ }
+
+ for (uint32_t i = 0; i < t_ni; i++) {
+ uint8_t count = 3;
+ int32_t idx[3] = { f_i[i*3+0], f_i[i*3+1], f_i[i*3+2] };
+ uint16_t mat = f_m[i];
+ fwrite(&count, 1, 1, out);
+ fwrite(idx, 4, 3, out);
+ fwrite(&mat, 2, 1, out);
+ }
+ fclose(out);
+ }
+ }
+}
+
+static void
+process_local_ipl(const char *root, const char *rel, FILE *out, struct file_list *fl)
+{
+ FILE *f;
+ size_t sz;
+ char *buf;
+
+ f = fopen_ci(root, rel);
+ if (!f) {
+ return;
+ }
+ fseek(f, 0, SEEK_END);
+ sz = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ buf = malloc(sz);
+ if (fread(buf, 1, sz, f) == sz) {
+ parse_text_ipl(buf, sz, out, fl);
+ }
+ free(buf);
+ fclose(f);
+}
+
+static void
+process_gta_dat_ide(const char *root, const char *dat_path)
+{
+ FILE *f;
+ char line[512];
+ char *p;
+
+ f = fopen_ci(root, dat_path);
+ if (!f) {
+ return;
+ }
+
+ while (fgets(line, sizeof(line), f)) {
+ p = line;
+ line[strcspn(line, "\r\n")] = '\0';
+ while (*p == ' ' || *p == '\t') {
+ p++;
+ }
+ if (*p == '\0' || *p == '#') {
+ continue;
+ }
+
+ if (strncasecmp(p, "IDE ", 4) == 0) {
+ char rel[512];
+ char *src = p + 4;
+ while (*src == ' ' || *src == '\t') {
+ src++;
+ }
+ for (size_t i = 0; i < sizeof(rel) - 1 && src[i]; i++) {
+ if (src[i] == '\\') {
+ rel[i] = '/';
+ } else {
+ rel[i] = src[i];
+ }
+ rel[i+1] = '\0';
+ }
+ trim_spaces(rel);
+ preparse_ide(root, rel);
+ }
+ }
+ fclose(f);
+}
+
+static void
+process_gta_dat_ipl(const char *root, const char *dat_path, FILE *map_out, struct file_list *fl)
+{
+ FILE *f;
+ char line[512];
+ char *p;
+
+ f = fopen_ci(root, dat_path);
+ if (!f) {
+ return;
+ }
+
+ while (fgets(line, sizeof(line), f)) {
+ p = line;
+ line[strcspn(line, "\r\n")] = '\0';
+ while (*p == ' ' || *p == '\t') {
+ p++;
+ }
+ if (*p == '\0' || *p == '#') {
+ continue;
+ }
+
+ if (strncasecmp(p, "IPL ", 4) == 0) {
+ char rel[512];
+ char *src = p + 4;
+ while (*src == ' ' || *src == '\t') {
+ src++;
+ }
+ for (size_t i = 0; i < sizeof(rel) - 1 && src[i]; i++) {
+ if (src[i] == '\\') {
+ rel[i] = '/';
+ } else {
+ rel[i] = src[i];
+ }
+ rel[i+1] = '\0';
+ }
+ trim_spaces(rel);
+ process_local_ipl(root, rel, map_out, fl);
+ }
+ }
+ fclose(f);
+}
+
+static void
+process_water(const char *root)
+{
+ FILE *f, *out;
+ char line[512], *p, *tok, *save;
+ char *tokens[32];
+ int t_count, i, j, k, collapsed;
+ uint32_t count;
+ float limit = 2980.0f;
+ float ax_min, ax_max, ay_min, ay_max, az;
+ float bx_min, bx_max, by_min, by_max;
+ struct water_face *a, *b, *face;
+
+ f = fopen_ci(root, "data/water.dat");
+ if (f == NULL) {
+ return;
+ }
+
+ out = fopen("assets/water.bin", "wb");
+ if (out == NULL) {
+ fclose(f);
+ return;
+ }
+
+ count = 0;
+ water_face_count = 0;
+
+ /* phase 1: read all faces to memory and clamp to boundaries */
+ while (fgets(line, sizeof(line), f)) {
+ p = line;
+ while (*p == ' ' || *p == '\t') {
+ p++;
+ }
+ if (*p == '\0' || *p == '#' || *p == '*' || *p == '\r' ||
+ *p == '\n' || strncasecmp(p, "processed", 9) == 0) {
+ continue;
+ }
+
+ t_count = 0;
+ tok = strtok_r(p, " \t\r\n", &save);
+ while (tok && t_count < 32) {
+ tokens[t_count++] = tok;
+ tok = strtok_r(NULL, " \t\r\n", &save);
+ }
+
+ if ((t_count == 29 || t_count == 22) && water_face_count < 8192) {
+ face = &water_db[water_face_count];
+ k = atoi(tokens[t_count - 1]);
+ if (k == 1 || k == 3) {
+ face->num = t_count == 29 ? 4 : 3;
+ for (j = 0; j < face->num; j++) {
+ face->x[j] = strtof(tokens[j * 7 + 0], NULL);
+ face->y[j] = strtof(tokens[j * 7 + 1], NULL);
+
+ /* snap height to 0.1m grid to eliminate micro seams */
+ face->z[j] = strtof(tokens[j * 7 + 2], NULL);
+ face->z[j] = roundf(face->z[j] * 10.0f) / 10.0f;
+
+ face->u[j] = strtof(tokens[j * 7 + 3], NULL);
+ face->v[j] = strtof(tokens[j * 7 + 4], NULL);
+ face->h[j] = strtof(tokens[j * 7 + 6], NULL);
+
+ /* clip to infinite ocean boundaries */
+ if (face->x[j] > limit) {
+ face->x[j] = limit;
+ }
+ if (face->x[j] < -limit) {
+ face->x[j] = -limit;
+ }
+ if (face->y[j] > limit) {
+ face->y[j] = limit;
+ }
+ if (face->y[j] < -limit) {
+ face->y[j] = -limit;
+ }
+ }
+ water_face_count++;
+ }
+ }
+ }
+
+ /* phase 2: smart clipping of overlapping internal tiles */
+ for (i = 0; i < water_face_count; i++) {
+ a = &water_db[i];
+ if (a->num == 0) {
+ continue;
+ }
+
+ ax_min = a->x[0];
+ ax_max = a->x[0];
+ ay_min = a->y[0];
+ ay_max = a->y[0];
+ az = a->z[0];
+ for (k = 1; k < a->num; k++) {
+ if (a->x[k] < ax_min) {
+ ax_min = a->x[k];
+ }
+ if (a->x[k] > ax_max) {
+ ax_max = a->x[k];
+ }
+ if (a->y[k] < ay_min) {
+ ay_min = a->y[k];
+ }
+ if (a->y[k] > ay_max) {
+ ay_max = a->y[k];
+ }
+ }
+
+ for (j = 0; j < water_face_count; j++) {
+ if (i == j) {
+ continue;
+ }
+ b = &water_db[j];
+ if (b->num == 0) {
+ continue;
+ }
+
+ if (fabsf(b->z[0] - az) > 0.01f) {
+ continue;
+ }
+
+ bx_min = b->x[0];
+ bx_max = b->x[0];
+ by_min = b->y[0];
+ by_max = b->y[0];
+ for (k = 1; k < b->num; k++) {
+ if (b->x[k] < bx_min) {
+ bx_min = b->x[k];
+ }
+ if (b->x[k] > bx_max) {
+ bx_max = b->x[k];
+ }
+ if (b->y[k] < by_min) {
+ by_min = b->y[k];
+ }
+ if (b->y[k] > by_max) {
+ by_max = b->y[k];
+ }
+ }
+
+ /* resolve intersecting bounding boxes on same height */
+ if (bx_max > ax_min && bx_min < ax_max && by_max > ay_min && by_min < ay_max) {
+ if (bx_min < ax_max && bx_max > ax_max && bx_min > ax_min) {
+ for (k = 0; k < b->num; k++) {
+ if (fabsf(b->x[k] - bx_min) < 0.1f) {
+ b->x[k] = ax_max;
+ }
+ }
+ } else if (bx_max > ax_min && bx_min < ax_min && bx_max < ax_max) {
+ for (k = 0; k < b->num; k++) {
+ if (fabsf(b->x[k] - bx_max) < 0.1f) {
+ b->x[k] = ax_min;
+ }
+ }
+ } else if (by_min < ay_max && by_max > ay_max && by_min > ay_min) {
+ for (k = 0; k < b->num; k++) {
+ if (fabsf(b->y[k] - by_min) < 0.1f) {
+ b->y[k] = ay_max;
+ }
+ }
+ } else if (by_max > ay_min && by_min < ay_min && by_max < ay_max) {
+ for (k = 0; k < b->num; k++) {
+ if (fabsf(b->y[k] - by_max) < 0.1f) {
+ b->y[k] = ay_min;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /* phase 3: write perfect tiles to file */
+ fwrite(&count, sizeof(count), 1, out);
+ for (i = 0; i < water_face_count; i++) {
+ face = &water_db[i];
+ collapsed = 0;
+
+ if (face->num == 4) {
+ if (fabsf(face->x[0] - face->x[2]) < 0.1f && fabsf(face->y[0] - face->y[2]) < 0.1f) {
+ collapsed = 1;
+ }
+ } else if (face->num == 3) {
+ if (fabsf(face->x[0] - face->x[1]) < 0.1f && fabsf(face->y[0] - face->y[1]) < 0.1f) {
+ collapsed = 1;
+ }
+ }
+
+ if (collapsed == 0) {
+ fwrite(face, sizeof(struct water_face), 1, out);
+ count++;
+ }
+ }
+
+ fseek(out, 0, SEEK_SET);
+ fwrite(&count, sizeof(count), 1, out);
+ fclose(out);
+ fclose(f);
+}
+
+static void
+process_timecyc(const char *root)
+{
+ FILE *f;
+ FILE *out;
+ char line[512];
+ struct timecyc_entry snaps[32 * 8];
+ int w_idx, s_idx, total_weathers;
+ char *p;
+
+ f = fopen_ci(root, "data/timecycp.dat");
+ if (!f) {
+ f = fopen_ci(root, "data/timecyc.dat");
+ }
+ if (!f) {
+ return;
+ }
+
+ out = fopen("assets/timecyc.bin", "wb");
+ if (!out) {
+ fclose(f);
+ return;
+ }
+
+ memset(snaps, 0, sizeof(snaps));
+ w_idx = 0;
+ s_idx = 0;
+ total_weathers = 0;
+
+ while (fgets(line, sizeof(line), f)) {
+ p = line;
+ while (*p == ' ' || *p == '\t') {
+ p++;
+ }
+ if (*p == '\0' || *p == '\n' || *p == '\r' ||
+ strncmp(p, "//", 2) == 0) {
+ continue;
+ }
+
+ char *tok, *save;
+ char *tokens[64];
+ int t = 0;
+
+ tok = strtok_r(p, " \t\r\n", &save);
+ while (tok && t < 64) {
+ tokens[t++] = tok;
+ tok = strtok_r(NULL, " \t\r\n", &save);
+ }
+
+ if (t >= 40) {
+ struct timecyc_entry *e = &snaps[w_idx * 8 + s_idx];
+ e->amb[0] = atoi(tokens[3]);
+ e->amb[1] = atoi(tokens[4]);
+ e->amb[2] = atoi(tokens[5]);
+ e->dir[0] = atoi(tokens[6]);
+ e->dir[1] = atoi(tokens[7]);
+ e->dir[2] = atoi(tokens[8]);
+ e->sky_top[0] = atoi(tokens[9]);
+ e->sky_top[1] = atoi(tokens[10]);
+ e->sky_top[2] = atoi(tokens[11]);
+ e->sky_bot[0] = atoi(tokens[12]);
+ e->sky_bot[1] = atoi(tokens[13]);
+ e->sky_bot[2] = atoi(tokens[14]);
+ e->far_clp = strtof(tokens[27], NULL);
+ e->fog_st = strtof(tokens[28], NULL);
+ e->water[0] = atoi(tokens[36]);
+ e->water[1] = atoi(tokens[37]);
+ e->water[2] = atoi(tokens[38]);
+ e->water[3] = (t >= 40) ? atoi(tokens[39]) : 255;
+
+ s_idx++;
+ if (s_idx == 8) {
+ s_idx = 0;
+ w_idx++;
+ if (w_idx > total_weathers) {
+ total_weathers = w_idx;
+ }
+ if (w_idx >= 32) {
+ break;
+ }
+ }
+ }
+ }
+ fwrite(&total_weathers, sizeof(int), 1, out);
+ fwrite(snaps, sizeof(struct timecyc_entry), total_weathers * 8, out);
+ fclose(out);
+ fclose(f);
+}
+
+static void *
+build_worker(void *arg)
+{
+ struct thread_arg *ta;
+ uint32_t i;
+
+ ta = arg;
+ ta->arena.size = 128 * 1024 * 1024;
+ ta->arena.mem = malloc(ta->arena.size);
+
+ for (i = ta->start; i < ta->end; i++) {
+ char n[NAME_SZ + 1];
+ size_t j;
+ for (j = 0; j < sizeof(ta->entries[i].name) && ta->entries[i].name[j]; j++) {
+ n[j] = tolower((unsigned char)ta->entries[i].name[j]);
+ }
+ n[j] = '\0';
+
+ if (list_has(ta->fl, n)) {
+ uint32_t size = ta->entries[i].size * 2048;
+ const uint8_t *buf = ta->img_data + ta->entries[i].offset * 2048;
+ char *ext = strrchr(n, '.');
+ if (ext) {
+ *ext = '\0';
+ }
+
+ ta->arena.pos = 0;
+
+ if (strstr(ta->entries[i].name, ".dff") || strstr(ta->entries[i].name, ".DFF")) {
+ convert_dff(buf, size, n, &ta->arena);
+ } else if (strstr(ta->entries[i].name, ".txd") || strstr(ta->entries[i].name, ".TXD")) {
+ convert_txd(buf, size, n, &ta->arena);
+ }
+ }
+ }
+ free(ta->arena.mem);
+ return (NULL);
+}
+
+static void
+tar_add_file(FILE *tar, const char *path, const char *tar_name)
+{
+ FILE *src;
+ size_t sz, n, padding;
+ struct tar_header th;
+ unsigned int sum;
+ uint8_t *p;
+ char buf[4096];
+
+ src = fopen(path, "rb");
+ if (!src) {
+ return;
+ }
+
+ fseek(src, 0, SEEK_END);
+ sz = ftell(src);
+ fseek(src, 0, SEEK_SET);
+
+ memset(&th, 0, sizeof(th));
+ strncpy(th.name, tar_name, sizeof(th.name) - 1);
+ snprintf(th.mode, sizeof(th.mode), "%07o", 0644);
+ snprintf(th.size, sizeof(th.size), "%011lo", (unsigned long)sz);
+ snprintf(th.magic, sizeof(th.magic), "ustar");
+
+ /* Simple checksum calculation. */
+ memset(th.chksum, ' ', 8);
+ sum = 0;
+ p = (uint8_t *)&th;
+ for (size_t i = 0; i < sizeof(th); i++) {
+ sum += p[i];
+ }
+ snprintf(th.chksum, sizeof(th.chksum), "%06o", sum);
+
+ fwrite(&th, sizeof(th), 1, tar);
+
+ while ((n = fread(buf, 1, sizeof(buf), src)) > 0) {
+ fwrite(buf, 1, n, tar);
+ }
+ fclose(src);
+
+ padding = (512 - (sz % 512)) % 512;
+ if (padding > 0) {
+ char pad[512] = {0};
+ fwrite(pad, 1, padding, tar);
+ }
+}
+
+static void
+pack_directory(FILE *tar, const char *dir_path, const char *prefix)
+{
+ DIR *dir;
+ struct dirent *de;
+ char path[1024];
+ char tar_name[1024];
+ struct stat st;
+
+ dir = opendir(dir_path);
+ if (!dir) {
+ return;
+ }
+ while ((de = readdir(dir))) {
+ if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) {
+ continue;
+ }
+ snprintf(path, sizeof(path), "%s/%s", dir_path, de->d_name);
+ snprintf(tar_name, sizeof(tar_name), "%s%s", prefix, de->d_name);
+
+ if (stat(path, &st) == 0) {
+ if (S_ISDIR(st.st_mode)) {
+ char new_prefix[1024];
+ snprintf(new_prefix, sizeof(new_prefix), "%s/", tar_name);
+ pack_directory(tar, path, new_prefix);
+ } else {
+ tar_add_file(tar, path, tar_name);
+ }
+ }
+ }
+ closedir(dir);
+}
+
+static void
+process_img_file(const char *root, const char *rel_path, struct file_list *fl, int num_threads, pthread_t *threads, struct thread_arg *args)
+{
+ char img_path[1024];
+ int img_fd;
+ struct stat st;
+ uint8_t *img_data;
+ struct img_header *hdr;
+ struct img_entry *entries;
+ uint32_t chunk;
+ int i;
+
+ if (!resolve_ci(root, rel_path, img_path, sizeof(img_path))) {
+ warnx("failed to resolve %s", rel_path);
+ return;
+ }
+
+ img_fd = open(img_path, O_RDONLY);
+ if (img_fd < 0) {
+ warn("failed to open %s", rel_path);
+ return;
+ }
+
+ if (fstat(img_fd, &st) < 0) {
+ close(img_fd);
+ return;
+ }
+
+ img_data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, img_fd, 0);
+ if (img_data == MAP_FAILED) {
+ close(img_fd);
+ return;
+ }
+
+ hdr = (struct img_header *)img_data;
+ if (memcmp(hdr->magic, "VER2", 4) != 0) {
+ munmap(img_data, st.st_size);
+ close(img_fd);
+ return;
+ }
+ entries = (struct img_entry *)(img_data + sizeof(struct img_header));
+
+ printf("build: processing %s (%u entries)...\n", rel_path, hdr->entries);
+
+ chunk = hdr->entries / num_threads;
+ for (i = 0; i < num_threads; i++) {
+ args[i].img_data = img_data;
+ args[i].entries = entries;
+ args[i].start = i * chunk;
+ args[i].end = (i == num_threads - 1) ? hdr->entries : (i + 1) * chunk;
+ args[i].fl = fl;
+ pthread_create(&threads[i], NULL, build_worker, &args[i]);
+ }
+
+ for (i = 0; i < num_threads; i++) {
+ pthread_join(threads[i], NULL);
+ }
+
+ munmap(img_data, st.st_size);
+ close(img_fd);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct file_list fl;
+ struct arena shared_arena;
+ FILE *map_out;
+ char img_path[1024];
+ int img_fd;
+ struct stat st;
+ uint8_t *img_data;
+ struct img_header *hdr;
+ struct img_entry *entries;
+ struct file_list missing_fl;
+ int num_threads;
+ pthread_t *threads;
+ struct thread_arg *args;
+ FILE *tar;
+
+ if (argc < 2) {
+ return (1);
+ }
+
+ index_directory_tree(argv[1]);
+
+ mkdir("assets", 0777);
+ mkdir("assets/models", 0777);
+ mkdir("assets/textures", 0777);
+
+ scan_existing_assets();
+ memset(&fl, 0, sizeof(fl));
+
+ map_out = fopen("assets/map.ipl", "w");
+ if (!map_out) {
+ err(1, "failed to create map.ipl");
+ }
+
+ process_gta_dat_ide(argv[1], "data/default.dat");
+ process_gta_dat_ide(argv[1], "data/gta.dat");
+
+ process_gta_dat_ipl(argv[1], "data/default.dat", map_out, &fl);
+ process_gta_dat_ipl(argv[1], "data/gta.dat", map_out, &fl);
+
+ if (!resolve_ci(argv[1], "MODELS/GTA3.IMG", img_path, sizeof(img_path))) {
+ errx(1, "failed to resolve GTA3.IMG");
+ }
+
+ img_fd = open(img_path, O_RDONLY);
+ if (img_fd < 0) {
+ err(1, "failed to open GTA3.IMG");
+ }
+
+ fstat(img_fd, &st);
+ img_data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, img_fd, 0);
+ if (img_data == MAP_FAILED) {
+ err(1, "mmap failed");
+ }
+
+ hdr = (struct img_header *)img_data;
+ if (memcmp(hdr->magic, "VER2", 4) != 0) {
+ errx(1, "invalid IMG");
+ }
+ entries = (struct img_entry *)(img_data + sizeof(struct img_header));
+
+ for (uint32_t i = 0; i < hdr->entries; i++) {
+ char name_lower[NAME_SZ + 1];
+ size_t j;
+ for (j = 0; j < sizeof(entries[i].name) && entries[i].name[j]; j++) {
+ name_lower[j] = tolower((unsigned char)entries[i].name[j]);
+ }
+ name_lower[j] = '\0';
+
+ if (strstr(name_lower, ".ipl") != NULL) {
+ uint32_t size = entries[i].size * 2048;
+ const uint8_t *buf = img_data + entries[i].offset * 2048;
+ if (size >= 4 && memcmp(buf, "bnry", 4) == 0) {
+ parse_binary_ipl(buf, size, map_out, &fl);
+ } else {
+ parse_text_ipl((char *)buf, size, map_out, &fl);
+ }
+ }
+ }
+ fclose(map_out);
+
+ memset(&missing_fl, 0, sizeof(missing_fl));
+ for (int i = 0; i < fl.count; i++) {
+ if (!asset_exists(fl.names[i])) {
+ for (uint32_t k = 0; k < hdr->entries; k++) {
+ char n[NAME_SZ + 1];
+ size_t x;
+ for (x = 0; x < 24 && entries[k].name[x]; x++) {
+ n[x] = tolower((unsigned char)entries[k].name[x]);
+ }
+ n[x] = '\0';
+ if (strcmp(n, fl.names[i]) == 0) {
+ list_add(&missing_fl, fl.names[i]);
+ break;
+ }
+ }
+ }
+ }
+ printf("Total map files: %d, missing/rebuilding: %d\n", fl.count, missing_fl.count);
+ fl = missing_fl;
+
+ shared_arena.size = 128 * 1024 * 1024;
+ shared_arena.mem = malloc(shared_arena.size);
+ process_particle_txd(argv[1], &shared_arena);
+ free(shared_arena.mem);
+
+ process_water(argv[1]);
+ process_timecyc(argv[1]);
+
+ num_threads = sysconf(_SC_NPROCESSORS_ONLN);
+ if (num_threads < 1) {
+ num_threads = 4;
+ }
+
+ threads = malloc(num_threads * sizeof(pthread_t));
+ args = malloc(num_threads * sizeof(struct thread_arg));
+
+ process_img_file(argv[1], "models/gta3.img", &fl, num_threads, threads, args);
+ process_img_file(argv[1], "models/gta_int.img", &fl, num_threads, threads, args);
+
+ free(threads);
+ free(args);
+
+ printf("Packing assets.tar...\n");
+ tar = fopen("assets.tar", "wb");
+ if (tar) {
+ pack_directory(tar, "assets", "assets/");
+ char empty[1024] = {0};
+ fwrite(empty, 1, sizeof(empty), tar);
+ fclose(tar);
+ system("rm -rf assets");
+ }
+
+ return (0);
+}
diff --git a/src/render.c b/src/render.c
@@ -0,0 +1,2271 @@
+#include <sys/ipc.h>
+#include <sys/mman.h>
+#include <sys/shm.h>
+#include <sys/stat.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/keysym.h>
+#include <X11/extensions/XShm.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <fcntl.h>
+#include <locale.h>
+#include <math.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <unistd.h>
+
+#define WIDTH 640
+#define HEIGHT 480
+#define MAX_CACHE 8192
+#define NAME_SZ 24
+#define MAX_DIST 250.0f
+#define GRID_SZ 64
+#define MAP_MIN -3000.0f
+#define MAP_MAX 3000.0f
+#define MAX_RENDER_TRIS 262144
+#define TAR_INDEX_SIZE 32768
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
+struct vec3 { float x, y, z; };
+struct mat4 { float m[4][4]; };
+
+struct timecyc_entry {
+ uint8_t amb[3], dir[3], sky_top[3], sky_bot[3], water[4];
+ float far_clp, fog_st;
+};
+
+struct sys_gfx {
+ Display *dpy;
+ Window win;
+ GC gc;
+ XImage *img;
+ XShmSegmentInfo shm;
+ uint32_t *pixels;
+ float *zbuffer;
+ Atom wm_delete;
+ struct mat4 proj;
+ char keys[512];
+ float game_time_sec, game_time_hours;
+ struct timecyc_entry weathers[32][8], cur_weather;
+ int num_weathers, game_weather;
+ float far_clip_override;
+ int in_chat, chat_len;
+ char chat_buf[128];
+ KeyCode key_w, key_a, key_s, key_d, key_t, key_esc;
+ KeyCode key_up, key_down, key_left, key_right;
+ XIC ic;
+ XFontSet font_set;
+};
+
+struct vertex { float x, y, z, w, u, v, r, g, b, a, f; };
+struct clip_vertex { float x, y, z, w, u, v, r, g, b, a, f; };
+
+struct texture {
+ uint32_t *pixels;
+ int w, h, has_alpha;
+ uint32_t *mips[4];
+ int mip_w[4];
+ int mip_h[4];
+};
+
+struct render_triangle {
+ struct vertex v0, v1, v2;
+ const struct texture *tex;
+ int is_water, mip;
+};
+
+struct ply_vertex {
+ float x, y, z;
+ float s, t;
+ uint8_t r, g, b, a;
+};
+
+struct mesh {
+ struct ply_vertex *ply_verts;
+ uint16_t *indices, *mat_ids;
+ uint32_t num_verts, num_tris, num_materials;
+ char (*raw_tex_names)[32];
+ struct texture **textures;
+ float spr_x, spr_y, spr_z, spr_r, total_area;
+};
+
+struct transform_array {
+ float *pos_x, *pos_y, *pos_z;
+ float *rot_qx, *rot_qy, *rot_qz, *rot_qw;
+ struct mat4 *world_matrices;
+ float *sphere_cx, *sphere_cy, *sphere_cz, *sphere_r;
+ int16_t *time_on, *time_off;
+ int count;
+};
+
+struct grid_cell { int *ids, count, cap; };
+
+struct asset_cache {
+ struct mesh meshes[MAX_CACHE];
+ struct texture textures[MAX_CACHE];
+ char mesh_names[MAX_CACHE][NAME_SZ], tex_names[MAX_CACHE][32];
+ int mesh_count, tex_count, mesh_hash[MAX_CACHE * 2], tex_hash[MAX_CACHE * 2];
+};
+
+struct draw_item { int id; float dist_sq; };
+struct water_face {
+ float x[4], y[4], z[4];
+ float u[4], v[4], h[4];
+ int num;
+};
+
+struct spin_barrier {
+ int count;
+ int threshold;
+ int cycle;
+};
+
+struct tar_header {
+ char name[100];
+ char mode[8];
+ char uid[8];
+ char gid[8];
+ char size[12];
+ char mtime[12];
+ char chksum[8];
+ char typeflag;
+ char linkname[100];
+ char magic[6];
+ char version[2];
+ char uname[32];
+ char gname[32];
+ char devmajor[8];
+ char devminor[8];
+ char prefix[155];
+ char pad[12];
+} __attribute__((packed));
+
+struct tar_index_entry {
+ char name[128];
+ const uint8_t *data;
+ size_t size;
+};
+
+struct tga_header {
+ uint8_t id_length;
+ uint8_t color_map_type;
+ uint8_t image_type;
+ uint16_t color_map_first;
+ uint16_t color_map_length;
+ uint8_t color_map_entry_size;
+ uint16_t x_origin;
+ uint16_t y_origin;
+ uint16_t width;
+ uint16_t height;
+ uint8_t pixel_depth;
+ uint8_t image_descriptor;
+} __attribute__((packed));
+
+static struct sys_gfx *g_sys;
+static struct transform_array g_transforms;
+static struct mesh **scene_meshes = NULL;
+static int scene_inst_count = 0, scene_inst_capacity = 0;
+static struct grid_cell scene_grid[GRID_SZ][GRID_SZ];
+static struct draw_item *draw_list = NULL;
+static int draw_cap = 0, draw_count = 0;
+static struct render_triangle render_list[MAX_RENDER_TRIS];
+static int render_count = 0;
+static struct water_face *water_faces = NULL;
+static uint32_t water_count = 0;
+static struct tar_index_entry tar_idx[TAR_INDEX_SIZE];
+static struct texture *water_tex = NULL, *sand_tex = NULL;
+static pthread_t *workers = NULL;
+static struct spin_barrier barrier_start, barrier_end;
+static int num_threads = 1, threads_running = 1;
+static struct clip_vertex *v_scratch = NULL;
+static size_t v_scratch_cap = 0;
+
+static uint8_t *g_tar_mapped = NULL;
+static size_t g_tar_size = 0;
+
+static inline void
+spin_barrier_init(struct spin_barrier *b, int count)
+{
+ b->count = 0;
+ b->threshold = count;
+ b->cycle = 0;
+}
+
+static inline void
+spin_barrier_wait(struct spin_barrier *b)
+{
+ int cycle;
+
+ cycle = __atomic_load_n(&b->cycle, __ATOMIC_ACQUIRE);
+ if (__atomic_add_fetch(&b->count, 1, __ATOMIC_ACQ_REL) == b->threshold) {
+ __atomic_store_n(&b->count, 0, __ATOMIC_RELEASE);
+ __atomic_store_n(&b->cycle, cycle + 1, __ATOMIC_RELEASE);
+ } else {
+ while (__atomic_load_n(&b->cycle, __ATOMIC_ACQUIRE) == cycle) {
+#if defined(__i386__) || defined(__x86_64__)
+ __builtin_ia32_pause();
+#else
+ __asm__ volatile("":::"memory");
+#endif
+ }
+ }
+}
+
+static size_t
+get_tar_size(const char *octal)
+{
+ size_t size = 0;
+ int i;
+
+ for (i = 0; i < 11; i++) {
+ if (octal[i] < '0' || octal[i] > '7') {
+ break;
+ }
+ size = size * 8 + (octal[i] - '0');
+ }
+ return (size);
+}
+
+static inline float
+edge_func(float ax, float ay, float bx, float by, float cx, float cy)
+{
+ return ((cx - ax) * (by - ay) - (cy - ay) * (bx - ax));
+}
+
+static void
+draw_triangle_band(const struct render_triangle *tri, int min_y_band, int max_y_band)
+{
+ struct vertex v0, v1, v2, sv0, sv1, sv2, tmp;
+ const struct texture *tex;
+ uint32_t *pixels;
+ float area, inv_area, dy12, dy20, dx12, dx20, dy01;
+ float p_x_start, p_y_start, w0_start, w1_start;
+ float dw0_dx, dw0_dy, dw1_dx, dw1_dy;
+ float u0_w, u1_w, u2_w, v0_w, v1_w, v2_w, f0_w, f1_w, f2_w;
+ float d_inv_w_dx, d_u_w_dx, d_v_w_dx, d_f_w_dx, d_r_dx, d_g_dx, d_b_dx;
+ float d_r_dx_255, d_g_dx_255, d_b_dx_255;
+ float inv_slope_long, inv_slope_short1, inv_slope_short2;
+ float row_y, x_long, x_short;
+ float dx, dy, row_w0, row_w1, row_w2;
+ float inv_w, u_w, v_w, f_w, r, g_val, b;
+ float wf, f, inv_f;
+ uint32_t sky_r, sky_g, sky_b, color, wa, fr, fg, fb, bg, tex_r, tex_g, tex_b;
+ uint8_t wa_tc, wr, wg, wb, tex_a;
+ int min_x, max_x, min_y, max_y, row_min_x, row_max_x;
+ int tw, th, tw_mask, th_mask, py, px, idx, ir, ig, ib, tx, ty;
+ int m;
+ float min_inv_w;
+
+ v0 = tri->v0;
+ v1 = tri->v1;
+ v2 = tri->v2;
+
+ /* prevent near-plane depth extrapolation artifacts */
+ min_inv_w = MIN(v0.w, MIN(v1.w, v2.w));
+ if (min_inv_w < 0.0001f) {
+ min_inv_w = 0.0001f;
+ }
+
+ area = edge_func(v0.x, v0.y, v1.x, v1.y, v2.x, v2.y);
+ if (fabsf(area) < 1e-5f) {
+ return;
+ }
+ inv_area = 1.0f / area;
+
+ dy12 = v2.y - v1.y;
+ dy20 = v0.y - v2.y;
+ dx12 = v2.x - v1.x;
+ dx20 = v0.x - v2.x;
+ dy01 = v1.y - v0.y;
+
+ min_x = (int)MIN(v0.x, MIN(v1.x, v2.x));
+ max_x = (int)MAX(v0.x, MAX(v1.x, v2.x));
+ min_y = (int)MIN(v0.y, MIN(v1.y, v2.y));
+ max_y = (int)MAX(v0.y, MAX(v1.y, v2.y));
+
+ if (min_x < 0) min_x = 0;
+ if (max_x >= WIDTH) max_x = WIDTH - 1;
+ if (min_y < min_y_band) min_y = min_y_band;
+ if (max_y > max_y_band) max_y = max_y_band;
+ if (min_y > max_y) {
+ return;
+ }
+
+ p_x_start = min_x + 0.5f;
+ p_y_start = min_y + 0.5f;
+ w0_start = ((p_x_start - v1.x) * dy12 - (p_y_start - v1.y) * dx12) * inv_area;
+ w1_start = ((p_x_start - v2.x) * dy20 - (p_y_start - v2.y) * dx20) * inv_area;
+
+ dw0_dx = dy12 * inv_area;
+ dw0_dy = -dx12 * inv_area;
+ dw1_dx = dy20 * inv_area;
+ dw1_dy = -dx20 * inv_area;
+
+ u0_w = v0.u * v0.w; u1_w = v1.u * v1.w; u2_w = v2.u * v2.w;
+ v0_w = v0.v * v0.w; v1_w = v1.v * v1.w; v2_w = v2.v * v2.w;
+ f0_w = v0.f * v0.w; f1_w = v1.f * v1.w; f2_w = v2.f * v2.w;
+
+ d_inv_w_dx = (dy12 * v0.w + dy20 * v1.w + dy01 * v2.w) * inv_area;
+ d_u_w_dx = (dy12 * u0_w + dy20 * u1_w + dy01 * u2_w) * inv_area;
+ d_v_w_dx = (dy12 * v0_w + dy20 * v1_w + dy01 * v2_w) * inv_area;
+ d_f_w_dx = (dy12 * f0_w + dy20 * f1_w + dy01 * f2_w) * inv_area;
+ d_r_dx = (dy12 * v0.r + dy20 * v1.r + dy01 * v2.r) * inv_area;
+ d_g_dx = (dy12 * v0.g + dy20 * v1.g + dy01 * v2.g) * inv_area;
+ d_b_dx = (dy12 * v0.b + dy20 * v1.b + dy01 * v2.b) * inv_area;
+
+ d_r_dx_255 = d_r_dx * 255.0f;
+ d_g_dx_255 = d_g_dx * 255.0f;
+ d_b_dx_255 = d_b_dx * 255.0f;
+
+ sky_r = g_sys->cur_weather.sky_bot[0];
+ sky_g = g_sys->cur_weather.sky_bot[1];
+ sky_b = g_sys->cur_weather.sky_bot[2];
+
+ sv0 = v0;
+ sv1 = v1;
+ sv2 = v2;
+ if (sv0.y > sv1.y) { tmp = sv0; sv0 = sv1; sv1 = tmp; }
+ if (sv0.y > sv2.y) { tmp = sv0; sv0 = sv2; sv2 = tmp; }
+ if (sv1.y > sv2.y) { tmp = sv1; sv1 = sv2; sv2 = tmp; }
+
+ inv_slope_long = 0.0f;
+ inv_slope_short1 = 0.0f;
+ inv_slope_short2 = 0.0f;
+
+ if (sv2.y != sv0.y) {
+ inv_slope_long = (sv2.x - sv0.x) / (sv2.y - sv0.y);
+ }
+ if (sv1.y != sv0.y) {
+ inv_slope_short1 = (sv1.x - sv0.x) / (sv1.y - sv0.y);
+ }
+ if (sv2.y != sv1.y) {
+ inv_slope_short2 = (sv2.x - sv1.x) / (sv2.y - sv1.y);
+ }
+
+ if (tri->is_water && tri->tex && (uintptr_t)tri->tex != (uintptr_t)-1) {
+ tex = tri->tex;
+ pixels = tex->pixels;
+ tw = tex->w;
+ th = tex->h;
+ tw_mask = tw - 1;
+ th_mask = th - 1;
+ wa_tc = g_sys->cur_weather.water[3];
+ wr = g_sys->cur_weather.water[0];
+ wg = g_sys->cur_weather.water[1];
+ wb = g_sys->cur_weather.water[2];
+
+ for (py = min_y; py <= max_y; py++) {
+ row_y = (float)py + 0.5f;
+ x_long = sv0.x;
+ if (sv2.y != sv0.y) {
+ x_long += (row_y - sv0.y) * inv_slope_long;
+ }
+
+ if (row_y < sv1.y) {
+ x_short = sv0.x;
+ if (sv1.y != sv0.y) {
+ x_short += (row_y - sv0.y) * inv_slope_short1;
+ }
+ } else {
+ x_short = sv1.x;
+ if (sv2.y != sv1.y) {
+ x_short += (row_y - sv1.y) * inv_slope_short2;
+ }
+ }
+
+ float x_min = (x_long < x_short) ? x_long : x_short;
+ float x_max = (x_long < x_short) ? x_short : x_long;
+
+ row_min_x = (int)ceilf(x_min - 0.5f);
+ row_max_x = (int)floorf(x_max - 0.5f);
+
+ if (row_min_x < min_x) row_min_x = min_x;
+ if (row_max_x > max_x) row_max_x = max_x;
+ if (row_min_x > row_max_x) {
+ continue;
+ }
+
+ dx = (row_min_x + 0.5f) - p_x_start;
+ dy = row_y - p_y_start;
+ row_w0 = w0_start + dw0_dx * dx + dw0_dy * dy;
+ row_w1 = w1_start + dw1_dx * dx + dw1_dy * dy;
+ row_w2 = 1.0f - row_w0 - row_w1;
+ inv_w = row_w0 * v0.w + row_w1 * v1.w + row_w2 * v2.w;
+ u_w = row_w0 * u0_w + row_w1 * u1_w + row_w2 * u2_w;
+ v_w = row_w0 * v0_w + row_w1 * v1_w + row_w2 * v2_w;
+ f_w = row_w0 * f0_w + row_w1 * f1_w + row_w2 * f2_w;
+ r = (row_w0 * v0.r + row_w1 * v1.r + row_w2 * v2.r) * 255.0f;
+ g_val = (row_w0 * v0.g + row_w1 * v1.g + row_w2 * v2.g) * 255.0f;
+ b = (row_w0 * v0.b + row_w1 * v1.b + row_w2 * v2.b) * 255.0f;
+
+ for (px = row_min_x; px <= row_max_x; px++) {
+ idx = py * WIDTH + px;
+ if (inv_w > g_sys->zbuffer[idx] && inv_w >= min_inv_w * 0.99f) {
+ ir = (int)r; ir = ir > 255 ? 255 : (ir < 0 ? 0 : ir);
+ ig = (int)g_val; ig = ig > 255 ? 255 : (ig < 0 ? 0 : ig);
+ ib = (int)b; ib = ib > 255 ? 255 : (ib < 0 ? 0 : ib);
+ wf = 1.0f / inv_w;
+ f = f_w * wf;
+ if (f > 1.0f) f = 1.0f;
+ if (f < 0.0f) f = 0.0f;
+ inv_f = 1.0f - f;
+ tx = (int)(u_w * wf * tw) & tw_mask;
+ ty = (int)(v_w * wf * th) & th_mask;
+ color = pixels[ty * tw + tx];
+ wa = (wa_tc * ((color >> 24) & 0xFF)) >> 8;
+ fr = ((((color >> 16) & 0xFF) * ir >> 8) * wr) >> 8;
+ fg = ((((color >> 8) & 0xFF) * ig >> 8) * wg) >> 8;
+ fb = ((color & 0xFF) * ib >> 8) * wb >> 8;
+ fr = (uint32_t)(fr * inv_f + sky_r * f);
+ fg = (uint32_t)(fg * inv_f + sky_g * f);
+ fb = (uint32_t)(fb * inv_f + sky_b * f);
+ bg = g_sys->pixels[idx];
+ g_sys->pixels[idx] = (0xFF << 24) |
+ (((fr * wa + ((bg >> 16) & 0xFF) * (255 - wa)) >> 8) << 16) |
+ (((fg * wa + ((bg >> 8) & 0xFF) * (255 - wa)) >> 8) << 8) |
+ ((fb * wa + (bg & 0xFF) * (255 - wa)) >> 8);
+ }
+ inv_w += d_inv_w_dx; u_w += d_u_w_dx; v_w += d_v_w_dx; f_w += d_f_w_dx;
+ r += d_r_dx_255; g_val += d_g_dx_255; b += d_b_dx_255;
+ }
+ }
+ } else if (tri->tex && (uintptr_t)tri->tex != (uintptr_t)-1) {
+ tex = tri->tex;
+ m = tri->mip;
+ if (m < 0) {
+ m = 0;
+ }
+ if (m > 3) {
+ m = 3;
+ }
+ pixels = tex->mips[m];
+ tw = tex->mip_w[m];
+ th = tex->mip_h[m];
+ tw_mask = tw - 1;
+ th_mask = th - 1;
+
+ for (py = min_y; py <= max_y; py++) {
+ row_y = (float)py + 0.5f;
+ x_long = sv0.x;
+ if (sv2.y != sv0.y) {
+ x_long += (row_y - sv0.y) * inv_slope_long;
+ }
+
+ if (row_y < sv1.y) {
+ x_short = sv0.x;
+ if (sv1.y != sv0.y) {
+ x_short += (row_y - sv0.y) * inv_slope_short1;
+ }
+ } else {
+ x_short = sv1.x;
+ if (sv2.y != sv1.y) {
+ x_short += (row_y - sv1.y) * inv_slope_short2;
+ }
+ }
+
+ float x_min = (x_long < x_short) ? x_long : x_short;
+ float x_max = (x_long < x_short) ? x_short : x_long;
+
+ row_min_x = (int)ceilf(x_min - 0.5f);
+ row_max_x = (int)floorf(x_max - 0.5f);
+
+ if (row_min_x < min_x) row_min_x = min_x;
+ if (row_max_x > max_x) row_max_x = max_x;
+ if (row_min_x > row_max_x) {
+ continue;
+ }
+
+ dx = (row_min_x + 0.5f) - p_x_start;
+ dy = row_y - p_y_start;
+ row_w0 = w0_start + dw0_dx * dx + dw0_dy * dy;
+ row_w1 = w1_start + dw1_dx * dx + dw1_dy * dy;
+ row_w2 = 1.0f - row_w0 - row_w1;
+ inv_w = row_w0 * v0.w + row_w1 * v1.w + row_w2 * v2.w;
+ u_w = row_w0 * u0_w + row_w1 * u1_w + row_w2 * u2_w;
+ v_w = row_w0 * v0_w + row_w1 * v1_w + row_w2 * v2_w;
+ f_w = row_w0 * f0_w + row_w1 * f1_w + row_w2 * f2_w;
+ r = (row_w0 * v0.r + row_w1 * v1.r + row_w2 * v2.r) * 255.0f;
+ g_val = (row_w0 * v0.g + row_w1 * v1.g + row_w2 * v2.g) * 255.0f;
+ b = (row_w0 * v0.b + row_w1 * v1.b + row_w2 * v2.b) * 255.0f;
+
+ for (px = row_min_x; px <= row_max_x; px++) {
+ idx = py * WIDTH + px;
+ if (inv_w > g_sys->zbuffer[idx] && inv_w >= min_inv_w * 0.99f) {
+ wf = 1.0f / inv_w;
+ tx = (int)(u_w * wf * tw) & tw_mask;
+ ty = (int)(v_w * wf * th) & th_mask;
+ color = pixels[ty * tw + tx];
+ tex_a = (color >> 24) & 0xFF;
+ if (tex_a > 127) {
+ ir = (int)r; ir = ir > 255 ? 255 : (ir < 0 ? 0 : ir);
+ ig = (int)g_val; ig = ig > 255 ? 255 : (ig < 0 ? 0 : ig);
+ ib = (int)b; ib = ib > 255 ? 255 : (ib < 0 ? 0 : ib);
+ f = f_w * wf;
+ if (f > 1.0f) f = 1.0f;
+ if (f < 0.0f) f = 0.0f;
+ inv_f = 1.0f - f;
+ tex_r = ((color >> 16) & 0xFF) * ir >> 8;
+ tex_g = ((color >> 8) & 0xFF) * ig >> 8;
+ tex_b = (color & 0xFF) * ib >> 8;
+ tex_r = (uint32_t)(tex_r * inv_f + sky_r * f);
+ tex_g = (uint32_t)(tex_g * inv_f + sky_g * f);
+ tex_b = (uint32_t)(tex_b * inv_f + sky_b * f);
+ g_sys->pixels[idx] = (tex_a << 24) |
+ (tex_r << 16) | (tex_g << 8) | tex_b;
+ g_sys->zbuffer[idx] = inv_w;
+ }
+ }
+ inv_w += d_inv_w_dx; u_w += d_u_w_dx; v_w += d_v_w_dx; f_w += d_f_w_dx;
+ r += d_r_dx_255; g_val += d_g_dx_255; b += d_b_dx_255;
+ }
+ }
+ } else {
+ for (py = min_y; py <= max_y; py++) {
+ row_y = (float)py + 0.5f;
+ x_long = sv0.x;
+ if (sv2.y != sv0.y) {
+ x_long += (row_y - sv0.y) * inv_slope_long;
+ }
+
+ if (row_y < sv1.y) {
+ x_short = sv0.x;
+ if (sv1.y != sv0.y) {
+ x_short += (row_y - sv0.y) * inv_slope_short1;
+ }
+ } else {
+ x_short = sv1.x;
+ if (sv2.y != sv1.y) {
+ x_short += (row_y - sv1.y) * inv_slope_short2;
+ }
+ }
+
+ float x_min = (x_long < x_short) ? x_long : x_short;
+ float x_max = (x_long < x_short) ? x_short : x_long;
+
+ row_min_x = (int)ceilf(x_min - 0.5f);
+ row_max_x = (int)floorf(x_max - 0.5f);
+
+ if (row_min_x < min_x) row_min_x = min_x;
+ if (row_max_x > max_x) row_max_x = max_x;
+ if (row_min_x > row_max_x) {
+ continue;
+ }
+
+ dx = (row_min_x + 0.5f) - p_x_start;
+ dy = row_y - p_y_start;
+ row_w0 = w0_start + dw0_dx * dx + dw0_dy * dy;
+ row_w1 = w1_start + dw1_dx * dx + dw1_dy * dy;
+ row_w2 = 1.0f - row_w0 - row_w1;
+ inv_w = row_w0 * v0.w + row_w1 * v1.w + row_w2 * v2.w;
+ f_w = row_w0 * f0_w + row_w1 * f1_w + row_w2 * f2_w;
+ r = (row_w0 * v0.r + row_w1 * v1.r + row_w2 * v2.r) * 255.0f;
+ g_val = (row_w0 * v0.g + row_w1 * v1.g + row_w2 * v2.g) * 255.0f;
+ b = (row_w0 * v0.b + row_w1 * v1.b + row_w2 * v2.b) * 255.0f;
+
+ for (px = row_min_x; px <= row_max_x; px++) {
+ idx = py * WIDTH + px;
+ if (inv_w > g_sys->zbuffer[idx] && inv_w >= min_inv_w * 0.99f) {
+ ir = (int)r; ir = ir > 255 ? 255 : (ir < 0 ? 0 : ir);
+ ig = (int)g_val; ig = ig > 255 ? 255 : (ig < 0 ? 0 : ig);
+ ib = (int)b; ib = ib > 255 ? 255 : (ib < 0 ? 0 : ib);
+ f = f_w / inv_w;
+ if (f > 1.0f) f = 1.0f;
+ if (f < 0.0f) f = 0.0f;
+ inv_f = 1.0f - f;
+ if (tri->tex == (void *)-1) {
+ bg = g_sys->pixels[idx];
+ wa = (g_sys->cur_weather.water[3] * 100) >> 8;
+ g_sys->pixels[idx] = (0xFF << 24) |
+ (((g_sys->cur_weather.water[0] * wa + ((bg >> 16) & 0xFF) * (255 - wa)) >> 8) << 16) |
+ (((g_sys->cur_weather.water[1] * wa + ((bg >> 8) & 0xFF) * (255 - wa)) >> 8) << 8) |
+ (((g_sys->cur_weather.water[2] * wa + (bg & 0xFF) * (255 - wa)) >> 8));
+ } else {
+ ir = (uint32_t)(ir * inv_f + sky_r * f);
+ ig = (uint32_t)(ig * inv_f + sky_g * f);
+ ib = (uint32_t)(ib * inv_f + sky_b * f);
+ g_sys->pixels[idx] = (0xFF << 24) |
+ (ir << 16) | (ig << 8) | ib;
+ }
+ g_sys->zbuffer[idx] = inv_w;
+ }
+ inv_w += d_inv_w_dx; f_w += d_f_w_dx;
+ r += d_r_dx * 255.0f; g_val += d_g_dx * 255.0f; b += d_b_dx * 255.0f;
+ }
+ }
+ }
+}
+
+static void *
+render_worker(void *arg)
+{
+ int id, start_y, end_y, i;
+
+ id = (int)(intptr_t)arg;
+ start_y = id * HEIGHT / num_threads;
+ end_y = (id == num_threads - 1) ? HEIGHT - 1 : (id + 1) * HEIGHT / num_threads - 1;
+
+ while (1) {
+ spin_barrier_wait(&barrier_start);
+ if (!threads_running) {
+ break;
+ }
+ for (i = 0; i < render_count; i++) {
+ draw_triangle_band(&render_list[i], start_y, end_y);
+ }
+ spin_barrier_wait(&barrier_end);
+ }
+ return (NULL);
+}
+
+static void
+gfx_init(struct sys_gfx *g)
+{
+ XIM im;
+ int scr;
+ XColor black;
+ Pixmap bm_no;
+ Cursor no_ptr;
+ char no_data[] = { 0,0,0,0,0,0,0,0 };
+ char **missing;
+ int nmissing;
+ char *def;
+ int i;
+
+ memset(g, 0, sizeof(*g));
+
+ setlocale(LC_ALL, "");
+ XSetLocaleModifiers("");
+
+ g->dpy = XOpenDisplay(NULL);
+ if (g->dpy == NULL) {
+ errx(1, "xopendisplay failed");
+ }
+ if (!XShmQueryExtension(g->dpy)) {
+ errx(1, "xshm extension not supported");
+ }
+ scr = DefaultScreen(g->dpy);
+ g->win = XCreateSimpleWindow(g->dpy, DefaultRootWindow(g->dpy), 0, 0,
+ WIDTH, HEIGHT, 0, 0, 0);
+ XSelectInput(g->dpy, g->win, KeyPressMask | KeyReleaseMask |
+ StructureNotifyMask | FocusChangeMask | PointerMotionMask);
+
+ im = XOpenIM(g->dpy, NULL, NULL, NULL);
+ g->ic = NULL;
+ if (im) {
+ g->ic = XCreateIC(im, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, XNClientWindow, g->win, NULL);
+ }
+ g->font_set = XCreateFontSet(g->dpy, "-misc-fixed-medium-r-normal--14-*,*",
+ &missing, &nmissing, &def);
+ if (nmissing > 0) {
+ XFreeStringList(missing);
+ }
+
+ g->gc = XCreateGC(g->dpy, g->win, 0, NULL);
+ g->img = XShmCreateImage(g->dpy, DefaultVisual(g->dpy, scr),
+ DefaultDepth(g->dpy, scr), ZPixmap, NULL, &g->shm, WIDTH, HEIGHT);
+ g->shm.shmid = shmget(IPC_PRIVATE, g->img->bytes_per_line * g->img->height,
+ IPC_CREAT | 0777);
+ g->shm.shmaddr = shmat(g->shm.shmid, 0, 0);
+ g->img->data = g->shm.shmaddr;
+ g->pixels = (uint32_t *)g->img->data;
+ g->shm.readOnly = False;
+ XShmAttach(g->dpy, &g->shm);
+ shmctl(g->shm.shmid, IPC_RMID, 0);
+ g->zbuffer = malloc(WIDTH * HEIGHT * sizeof(float));
+ g->wm_delete = XInternAtom(g->dpy, "WM_DELETE_WINDOW", False);
+ XSetWMProtocols(g->dpy, g->win, &g->wm_delete, 1);
+
+ memset(&black, 0, sizeof(black));
+ bm_no = XCreateBitmapFromData(g->dpy, g->win, no_data, 8, 8);
+ no_ptr = XCreatePixmapCursor(g->dpy, bm_no, bm_no, &black, &black, 0, 0);
+ XDefineCursor(g->dpy, g->win, no_ptr);
+ XFreeCursor(g->dpy, no_ptr);
+ XFreePixmap(g->dpy, bm_no);
+
+ g->key_w = XKeysymToKeycode(g->dpy, XK_w);
+ g->key_a = XKeysymToKeycode(g->dpy, XK_a);
+ g->key_s = XKeysymToKeycode(g->dpy, XK_s);
+ g->key_d = XKeysymToKeycode(g->dpy, XK_d);
+ g->key_t = XKeysymToKeycode(g->dpy, XK_t);
+ g->key_esc = XKeysymToKeycode(g->dpy, XK_Escape);
+ g->key_up = XKeysymToKeycode(g->dpy, XK_Up);
+ g->key_down = XKeysymToKeycode(g->dpy, XK_Down);
+ g->key_left = XKeysymToKeycode(g->dpy, XK_Left);
+ g->key_right = XKeysymToKeycode(g->dpy, XK_Right);
+
+ num_threads = sysconf(_SC_NPROCESSORS_ONLN);
+ if (num_threads < 1) {
+ num_threads = 4;
+ }
+ spin_barrier_init(&barrier_start, num_threads + 1);
+ spin_barrier_init(&barrier_end, num_threads + 1);
+ workers = malloc(num_threads * sizeof(pthread_t));
+ for (i = 0; i < num_threads; i++) {
+ pthread_create(&workers[i], NULL, render_worker, (void *)(intptr_t)i);
+ }
+
+ v_scratch_cap = 16384;
+ v_scratch = malloc(v_scratch_cap * sizeof(struct clip_vertex));
+}
+
+static void
+gfx_cleanup(struct sys_gfx *g)
+{
+ int i;
+
+ threads_running = 0;
+ spin_barrier_wait(&barrier_start);
+ if (g->font_set) {
+ XFreeFontSet(g->dpy, g->font_set);
+ }
+ for (i = 0; i < num_threads; i++) {
+ pthread_join(workers[i], NULL);
+ }
+ free(workers);
+ free(v_scratch);
+ XUngrabPointer(g->dpy, CurrentTime);
+ XShmDetach(g->dpy, &g->shm);
+ XDestroyImage(g->img);
+ shmdt(g->shm.shmaddr);
+ free(g->zbuffer);
+ XFreeGC(g->dpy, g->gc);
+ XDestroyWindow(g->dpy, g->win);
+ XCloseDisplay(g->dpy);
+}
+
+static void
+gfx_present(struct sys_gfx *g)
+{
+ char txt[256];
+
+ XShmPutImage(g->dpy, g->win, g->gc, g->img, 0, 0, 0, 0, WIDTH, HEIGHT, False);
+ if (g->in_chat) {
+ snprintf(txt, sizeof(txt), "> %s_", g->chat_buf);
+ XSetForeground(g->dpy, g->gc, 0x00FF00);
+ XSetBackground(g->dpy, g->gc, 0x000000);
+ if (g->font_set) {
+ Xutf8DrawImageString(g->dpy, g->win, g->font_set, g->gc,
+ 10, HEIGHT - 20, txt, strlen(txt));
+ } else {
+ XDrawImageString(g->dpy, g->win, g->gc,
+ 10, HEIGHT - 20, txt, strlen(txt));
+ }
+ }
+ XSync(g->dpy, False);
+}
+
+static unsigned int
+hash_str(const char *str)
+{
+ unsigned int hash = 5381;
+ int c;
+
+ while ((c = (unsigned char)*str++)) {
+ hash = ((hash << 5) + hash) + c;
+ }
+ return (hash);
+}
+
+static void
+mesh_load_ply(struct mesh *m, const uint8_t *data, size_t sz)
+{
+ const char *header_end;
+ const char *p;
+ const uint8_t *bin;
+ int temp_materials, idx, k;
+ char *dst;
+ const char *src;
+ uint16_t *mat;
+ int32_t *idx_ptr;
+ uint32_t i;
+
+ (void)sz;
+ header_end = strstr((const char *)data, "end_header\n");
+ if (!header_end) {
+ return;
+ }
+ header_end += 11;
+
+ memset(m, 0, sizeof(*m));
+
+ p = (const char *)data;
+ temp_materials = 0;
+ while (p < header_end) {
+ if (strncmp(p, "comment spr_cx", 14) == 0) m->spr_x = strtof(p + 14, NULL);
+ else if (strncmp(p, "comment spr_cy", 14) == 0) m->spr_y = strtof(p + 14, NULL);
+ else if (strncmp(p, "comment spr_cz", 14) == 0) m->spr_z = strtof(p + 14, NULL);
+ else if (strncmp(p, "comment spr_r", 13) == 0) m->spr_r = strtof(p + 13, NULL);
+ else if (strncmp(p, "element vertex", 14) == 0) m->num_verts = strtoul(p + 14, NULL, 10);
+ else if (strncmp(p, "element face", 12) == 0) m->num_tris = strtoul(p + 12, NULL, 10);
+ else if (strncmp(p, "comment texture", 15) == 0) temp_materials++;
+ p = strchr(p, '\n');
+ if (!p) break;
+ p++;
+ }
+
+ m->num_materials = temp_materials;
+ if (m->num_materials > 0) {
+ m->raw_tex_names = malloc(m->num_materials * 32);
+ p = (const char *)data;
+ idx = 0;
+ while (p < header_end) {
+ if (strncmp(p, "comment texture", 15) == 0) {
+ src = p + 16;
+ dst = m->raw_tex_names[idx];
+ k = 0;
+ while (src[k] != '\n' && src[k] != '\r' && k < 31) {
+ dst[k] = src[k];
+ k++;
+ }
+ dst[k] = '\0';
+ idx++;
+ }
+ p = strchr(p, '\n');
+ if (!p) break;
+ p++;
+ }
+ }
+
+ m->ply_verts = malloc(m->num_verts * sizeof(struct ply_vertex));
+ m->indices = malloc(m->num_tris * 3 * sizeof(uint16_t));
+ m->mat_ids = malloc(m->num_tris * sizeof(uint16_t));
+
+ bin = (const uint8_t *)header_end;
+
+ memcpy(m->ply_verts, bin, m->num_verts * sizeof(struct ply_vertex));
+ bin += m->num_verts * sizeof(struct ply_vertex);
+
+ for (i = 0; i < m->num_tris; i++) {
+ bin += 1;
+ idx_ptr = (int32_t *)bin;
+ m->indices[i*3+0] = idx_ptr[0];
+ m->indices[i*3+1] = idx_ptr[1];
+ m->indices[i*3+2] = idx_ptr[2];
+ bin += 12;
+
+ mat = (uint16_t *)bin;
+ m->mat_ids[i] = *mat;
+ bin += 2;
+ }
+
+ m->total_area = 0.0f;
+ for (i = 0; i < m->num_tris; i++) {
+ uint16_t i0 = m->indices[i*3], i1 = m->indices[i*3+1], i2 = m->indices[i*3+2];
+ float ux = m->ply_verts[i1].x - m->ply_verts[i0].x;
+ float uy = m->ply_verts[i1].y - m->ply_verts[i0].y;
+ float uz = m->ply_verts[i1].z - m->ply_verts[i0].z;
+ float vx = m->ply_verts[i2].x - m->ply_verts[i0].x;
+ float vy = m->ply_verts[i2].y - m->ply_verts[i0].y;
+ float vz = m->ply_verts[i2].z - m->ply_verts[i0].z;
+ float cx = uy * vz - uz * vy, cy = uz * vx - ux * vz, cz = ux * vy - uy * vx;
+ m->total_area += 0.5f * sqrtf(cx*cx + cy*cy + cz*cz);
+ }
+}
+
+static void
+tex_load_tga(struct texture *t, const uint8_t *data, size_t sz)
+{
+ struct tga_header *hdr;
+ const uint8_t *src_pixels;
+ uint32_t *dst, *curr_offset, *src;
+ int w, h, mw, mh, total_pixels, i, level;
+ int prev_w, prev_h, next_w, next_h, x, y;
+ uint32_t c00, c10, c01, c11, r, g, b, a;
+
+ memset(t, 0, sizeof(*t));
+ if (data == NULL || sz < sizeof(struct tga_header)) {
+ w = 2;
+ h = 2;
+ t->w = w;
+ t->h = h;
+ t->has_alpha = 0;
+ t->pixels = malloc((4 + 1 + 1 + 1) * sizeof(uint32_t));
+ if (t->pixels == NULL) {
+ return;
+ }
+ t->pixels[0] = 0xffa0a0a0;
+ t->pixels[1] = 0xff808080;
+ t->pixels[2] = 0xff808080;
+ t->pixels[3] = 0xffa0a0a0;
+
+ t->mips[0] = t->pixels;
+ t->mip_w[0] = 2;
+ t->mip_h[0] = 2;
+
+ for (i = 1; i < 4; i++) {
+ t->mips[i] = t->pixels + 4;
+ t->mip_w[i] = 1;
+ t->mip_h[i] = 1;
+ }
+ t->mips[1][0] = 0xff909090;
+ return;
+ }
+
+ hdr = (struct tga_header *)data;
+ w = hdr->width;
+ h = hdr->height;
+ t->w = w;
+ t->h = h;
+
+ mw = w;
+ mh = h;
+ total_pixels = 0;
+ for (i = 0; i < 4; i++) {
+ total_pixels += mw * mh;
+ mw = mw > 1 ? mw / 2 : 1;
+ mh = mh > 1 ? mh / 2 : 1;
+ }
+
+ t->pixels = malloc(total_pixels * sizeof(uint32_t));
+ if (t->pixels == NULL) {
+ return;
+ }
+ src_pixels = data + sizeof(struct tga_header) + hdr->id_length;
+
+ if (hdr->pixel_depth == 32) {
+ memcpy(t->pixels, src_pixels, w * h * 4);
+ t->has_alpha = 0;
+ for (i = 0; i < w * h; i++) {
+ a = (t->pixels[i] >> 24) & 0xFF;
+ if (a < 250) {
+ t->has_alpha = 1;
+ break;
+ }
+ }
+ } else if (hdr->pixel_depth == 24) {
+ t->has_alpha = 0;
+ dst = t->pixels;
+ for (i = 0; i < w * h; i++) {
+ dst[i] = (255U << 24) | (src_pixels[i*3+2] << 16) |
+ (src_pixels[i*3+1] << 8) | src_pixels[i*3+0];
+ }
+ }
+
+ t->mips[0] = t->pixels;
+ t->mip_w[0] = w;
+ t->mip_h[0] = h;
+
+ curr_offset = t->pixels + (w * h);
+ for (level = 1; level < 4; level++) {
+ prev_w = t->mips[level-1] ? t->mip_w[level-1] : 1;
+ prev_h = t->mips[level-1] ? t->mip_h[level-1] : 1;
+ next_w = prev_w > 1 ? prev_w / 2 : 1;
+ next_h = prev_h > 1 ? prev_h / 2 : 1;
+
+ t->mips[level] = curr_offset;
+ t->mip_w[level] = next_w;
+ t->mip_h[level] = next_h;
+
+ src = t->mips[level-1];
+ dst = t->mips[level];
+
+ if (prev_w > 1 && prev_h > 1) {
+ for (y = 0; y < next_h; y++) {
+ for (x = 0; x < next_w; x++) {
+ c00 = src[(y*2)*prev_w + (x*2)];
+ c10 = src[(y*2)*prev_w + (x*2+1)];
+ c01 = src[(y*2+1)*prev_w + (x*2)];
+ c11 = src[(y*2+1)*prev_w + (x*2+1)];
+
+ r = (((c00 >> 16) & 0xFF) + ((c10 >> 16) & 0xFF) +
+ ((c01 >> 16) & 0xFF) + ((c11 >> 16) & 0xFF)) / 4;
+ g = (((c00 >> 8) & 0xFF) + ((c10 >> 8) & 0xFF) +
+ ((c01 >> 8) & 0xFF) + ((c11 >> 8) & 0xFF)) / 4;
+ b = ((c00 & 0xFF) + (c10 & 0xFF) +
+ (c01 & 0xFF) + (c11 & 0xFF)) / 4;
+ a = (((c00 >> 24) & 0xFF) + ((c10 >> 24) & 0xFF) +
+ ((c01 >> 24) & 0xFF) + ((c11 >> 24) & 0xFF)) / 4;
+
+ dst[y * next_w + x] = (a << 24) | (r << 16) |
+ (g << 8) | b;
+ }
+ }
+ } else {
+ for (i = 0; i < next_w * next_h; i++) {
+ dst[i] = src[0];
+ }
+ }
+ curr_offset += next_w * next_h;
+ }
+}
+
+static uint32_t
+tar_hash(const char *str)
+{
+ uint32_t hash = 5381;
+ int c;
+
+ while ((c = (unsigned char)*str++)) {
+ hash = ((hash << 5) + hash) + tolower(c);
+ }
+
+ return (hash);
+}
+
+static const uint8_t *
+tar_lookup(const char *filename, size_t *out_size)
+{
+ uint32_t slot;
+
+ slot = tar_hash(filename) % TAR_INDEX_SIZE;
+ while (tar_idx[slot].data != NULL) {
+ if (strcasecmp(tar_idx[slot].name, filename) == 0) {
+ *out_size = tar_idx[slot].size;
+ return (tar_idx[slot].data);
+ }
+ slot = (slot + 1) % TAR_INDEX_SIZE;
+ }
+ return (NULL);
+}
+
+static struct texture *
+cache_get_tex(struct asset_cache *c, const char *name)
+{
+ unsigned int h;
+ int idx;
+ char tar_path[1024];
+ size_t sz = 0;
+ const uint8_t *data;
+
+ if (!name || name[0] == '\0') {
+ name = "missing";
+ }
+ h = hash_str(name) % (MAX_CACHE * 2);
+ while (c->tex_hash[h] != 0) {
+ idx = c->tex_hash[h] - 1;
+ if (strcasecmp(c->tex_names[idx], name) == 0) {
+ return (&c->textures[idx]);
+ }
+ h = (h + 1) % (MAX_CACHE * 2);
+ }
+ if (c->tex_count >= MAX_CACHE) {
+ return (NULL);
+ }
+ idx = c->tex_count++;
+ snprintf(c->tex_names[idx], 32, "%s", name);
+ c->tex_hash[h] = idx + 1;
+
+ snprintf(tar_path, sizeof(tar_path), "assets/textures/%s.tga", name);
+
+ data = tar_lookup(tar_path, &sz);
+ tex_load_tga(&c->textures[idx], data, sz);
+ return (&c->textures[idx]);
+}
+
+static struct mesh *
+cache_get_mesh(struct asset_cache *c, const char *name)
+{
+ unsigned int h;
+ int idx;
+ char tar_path[1024];
+ size_t sz = 0;
+ const uint8_t *data;
+ uint32_t i;
+
+ h = hash_str(name) % (MAX_CACHE * 2);
+ while (c->mesh_hash[h] != 0) {
+ idx = c->mesh_hash[h] - 1;
+ if (strcasecmp(c->mesh_names[idx], name) == 0) {
+ return (&c->meshes[idx]);
+ }
+ h = (h + 1) % (MAX_CACHE * 2);
+ }
+ if (c->mesh_count >= MAX_CACHE) {
+ return (NULL);
+ }
+ idx = c->mesh_count++;
+ snprintf(c->mesh_names[idx], NAME_SZ, "%s", name);
+ c->mesh_hash[h] = idx + 1;
+
+ snprintf(tar_path, sizeof(tar_path), "assets/models/%s.ply", name);
+
+ data = tar_lookup(tar_path, &sz);
+ if (data) {
+ mesh_load_ply(&c->meshes[idx], data, sz);
+ if (c->meshes[idx].num_materials > 0) {
+ c->meshes[idx].textures = malloc(c->meshes[idx].num_materials * sizeof(struct texture *));
+ for (i = 0; i < c->meshes[idx].num_materials; i++) {
+ c->meshes[idx].textures[i] = cache_get_tex(c, c->meshes[idx].raw_tex_names[i]);
+ }
+ }
+ }
+ return (&c->meshes[idx]);
+}
+
+static void
+mat_identity(struct mat4 *m)
+{
+ memset(m, 0, sizeof(*m));
+ m->m[0][0] = m->m[1][1] = m->m[2][2] = m->m[3][3] = 1.0f;
+}
+
+static inline void
+mat_mul(struct mat4 *dst, const struct mat4 *a, const struct mat4 *b)
+{
+ struct mat4 r;
+ int i;
+
+ for (i = 0; i < 4; i++) {
+ r.m[i][0] = a->m[i][0]*b->m[0][0] + a->m[i][1]*b->m[1][0] + a->m[i][2]*b->m[2][0] + a->m[i][3]*b->m[3][0];
+ r.m[i][1] = a->m[i][0]*b->m[0][1] + a->m[i][1]*b->m[1][1] + a->m[i][2]*b->m[2][1] + a->m[i][3]*b->m[3][1];
+ r.m[i][2] = a->m[i][0]*b->m[0][2] + a->m[i][1]*b->m[1][2] + a->m[i][2]*b->m[2][2] + a->m[i][3]*b->m[3][2];
+ r.m[i][3] = a->m[i][0]*b->m[0][3] + a->m[i][1]*b->m[1][3] + a->m[i][2]*b->m[2][3] + a->m[i][3]*b->m[3][3];
+ }
+ *dst = r;
+}
+
+static void
+mat_translate(struct mat4 *m, float x, float y, float z)
+{
+ mat_identity(m);
+ m->m[0][3] = x;
+ m->m[1][3] = y;
+ m->m[2][3] = z;
+}
+
+static void
+mat_from_quat(struct mat4 *m, float qx, float qy, float qz, float qw)
+{
+ float len;
+
+ qw = -qw;
+ len = sqrtf(qx * qx + qy * qy + qz * qz + qw * qw);
+ if (len > 0.0f) {
+ qx /= len; qy /= len; qz /= len; qw /= len;
+ }
+ mat_identity(m);
+ m->m[0][0] = 1.0f - 2.0f * (qy * qy + qz * qz);
+ m->m[0][1] = 2.0f * (qx * qy - qz * qw);
+ m->m[0][2] = 2.0f * (qx * qz + qy * qw);
+ m->m[1][0] = 2.0f * (qx * qy + qz * qw);
+ m->m[1][1] = 1.0f - 2.0f * (qx * qx + qz * qz);
+ m->m[1][2] = 2.0f * (qy * qz - qx * qw);
+ m->m[2][0] = 2.0f * (qx * qz - qy * qw);
+ m->m[2][1] = 2.0f * (qy * qz + qx * qw);
+ m->m[2][2] = 1.0f - 2.0f * (qx * qx + qy * qy);
+}
+
+static void
+mat_projection(struct mat4 *m, float fov, float aspect, float near, float far)
+{
+ float f;
+
+ f = 1.0f / tanf(fov * 0.5f * 3.14159265f / 180.0f);
+ memset(m, 0, sizeof(*m));
+ m->m[0][0] = f / aspect;
+ m->m[1][1] = f;
+ m->m[2][2] = (far + near) / (far - near);
+ m->m[2][3] = -(2.0f * far * near) / (far - near);
+ m->m[3][2] = 1.0f;
+}
+
+static void
+mat_view(struct mat4 *m, float cx, float cy, float cz, float cyaw, float cpitch)
+{
+ struct mat4 trans, rot_y, rot_x, rot;
+
+ mat_translate(&trans, -cx, -cy, -cz);
+ mat_identity(&rot_y);
+ rot_y.m[0][0] = cosf(-cyaw);
+ rot_y.m[0][2] = sinf(-cyaw);
+ rot_y.m[2][0] = -sinf(-cyaw);
+ rot_y.m[2][2] = cosf(-cyaw);
+ mat_identity(&rot_x);
+ rot_x.m[1][1] = cosf(cpitch);
+ rot_x.m[1][2] = -sinf(cpitch);
+ rot_x.m[2][1] = sinf(cpitch);
+ rot_x.m[2][2] = cosf(cpitch);
+ mat_mul(&rot, &rot_x, &rot_y);
+ mat_mul(m, &rot, &trans);
+}
+
+static inline void
+mat_transform(struct vec3 *dst, float *w_out, const struct mat4 *m, const struct vec3 *v)
+{
+ dst->x = m->m[0][0] * v->x + m->m[0][1] * v->y + m->m[0][2] * v->z + m->m[0][3];
+ dst->y = m->m[1][0] * v->x + m->m[1][1] * v->y + m->m[1][2] * v->z + m->m[1][3];
+ dst->z = m->m[2][0] * v->x + m->m[2][1] * v->y + m->m[2][2] * v->z + m->m[2][3];
+ *w_out = m->m[3][0] * v->x + m->m[3][1] * v->y + m->m[3][2] * v->z + m->m[3][3];
+}
+
+static void
+flush_render_list(void)
+{
+ if (render_count == 0) {
+ return;
+ }
+ spin_barrier_wait(&barrier_start);
+ spin_barrier_wait(&barrier_end);
+ render_count = 0;
+}
+
+static void
+project_and_push(const struct clip_vertex *c0, const struct clip_vertex *c1, const struct clip_vertex *c2, const struct texture *tex, int is_water, int mip)
+{
+ struct vertex v0, v1, v2;
+ float area;
+ int double_sided;
+ struct vertex tmp;
+
+ v0.w = 1.0f / c0->w; v0.x = (c0->x * v0.w + 1.0f) * WIDTH * 0.5f; v0.y = (1.0f - c0->y * v0.w) * HEIGHT * 0.5f;
+ v0.z = c0->w; v0.u = c0->u; v0.v = c0->v; v0.r = c0->r; v0.g = c0->g; v0.b = c0->b; v0.a = c0->a; v0.f = c0->f;
+ v1.w = 1.0f / c1->w; v1.x = (c1->x * v1.w + 1.0f) * WIDTH * 0.5f; v1.y = (1.0f - c1->y * v1.w) * HEIGHT * 0.5f;
+ v1.z = c1->w; v1.u = c1->u; v1.v = c1->v; v1.r = c1->r; v1.g = c1->g; v1.b = c1->b; v1.a = c1->a; v1.f = c1->f;
+ v2.w = 1.0f / c2->w; v2.x = (c2->x * v2.w + 1.0f) * WIDTH * 0.5f; v2.y = (1.0f - c2->y * v2.w) * HEIGHT * 0.5f;
+ v2.z = c2->w; v2.u = c2->u; v2.v = c2->v; v2.r = c2->r; v2.g = c2->g; v2.b = c2->b; v2.a = c2->a; v2.f = c2->f;
+
+ /* check backface */
+ area = edge_func(v0.x, v0.y, v1.x, v1.y, v2.x, v2.y);
+ double_sided = (tex && tex->has_alpha) || is_water;
+ if (area >= 0.0f) {
+ if (double_sided) {
+ tmp = v1;
+ v1 = v2;
+ v2 = tmp;
+ } else {
+ return;
+ }
+ }
+
+ if (render_count >= MAX_RENDER_TRIS) {
+ flush_render_list();
+ }
+ render_list[render_count].v0 = v0;
+ render_list[render_count].v1 = v1;
+ render_list[render_count].v2 = v2;
+ render_list[render_count].tex = tex;
+ render_list[render_count].is_water = is_water;
+ render_list[render_count].mip = mip;
+ render_count++;
+}
+
+static struct clip_vertex
+clip_intersect(const struct clip_vertex *a, const struct clip_vertex *b, float w_clip)
+{
+ struct clip_vertex out;
+ float t;
+
+ t = (w_clip - a->w) / (b->w - a->w);
+ out.x = a->x + t * (b->x - a->x);
+ out.y = a->y + t * (b->y - a->y);
+ out.z = a->z + t * (b->z - a->z);
+ out.w = w_clip;
+ out.u = a->u + t * (b->u - a->u);
+ out.v = a->v + t * (b->v - a->v);
+ out.r = a->r + t * (b->r - a->r);
+ out.g = a->g + t * (b->g - a->g);
+ out.b = a->b + t * (b->b - a->b);
+ out.a = a->a + t * (b->a - a->a);
+ out.f = a->f + t * (b->f - a->f);
+ return (out);
+}
+
+static void
+clip_and_push_triangle(const struct clip_vertex *c0, const struct clip_vertex *c1, const struct clip_vertex *c2, const struct texture *tex, int is_water, int mip)
+{
+ struct clip_vertex p[4];
+ const struct clip_vertex *v[3];
+ const struct clip_vertex *c, *n;
+ int len = 0;
+ int i;
+
+ v[0] = c0;
+ v[1] = c1;
+ v[2] = c2;
+
+ for (i = 0; i < 3; i++) {
+ c = v[i];
+ n = v[(i + 1) % 3];
+ if (c->w >= 0.1f) {
+ p[len++] = *c;
+ }
+ if ((c->w >= 0.1f) != (n->w >= 0.1f)) {
+ p[len++] = clip_intersect(c, n, 0.1f);
+ }
+ }
+ if (len == 3) {
+ project_and_push(&p[0], &p[1], &p[2], tex, is_water, mip);
+ } else if (len == 4) {
+ project_and_push(&p[0], &p[1], &p[2], tex, is_water, mip);
+ project_and_push(&p[0], &p[2], &p[3], tex, is_water, mip);
+ }
+}
+
+static void
+draw_mesh(const struct mesh *m, const struct mat4 *mvp, int is_seabed)
+{
+ struct clip_vertex *cv;
+ float ar, ag, ab, far_clp, fog_st, min_dist;
+ int mip;
+ float scale;
+ uint32_t i;
+ struct vec3 in, out;
+ float w, fog, d;
+
+ if (m->ply_verts == NULL || m->num_verts == 0) {
+ return;
+ }
+ if (m->num_verts > v_scratch_cap) {
+ v_scratch_cap = m->num_verts * 2;
+ v_scratch = realloc(v_scratch, v_scratch_cap * sizeof(struct clip_vertex));
+ }
+ cv = v_scratch;
+ ar = g_sys->cur_weather.amb[0] / 255.0f * 1.2f;
+ ag = g_sys->cur_weather.amb[1] / 255.0f * 1.2f;
+ ab = g_sys->cur_weather.amb[2] / 255.0f * 1.2f;
+ far_clp = g_sys->cur_weather.far_clp;
+ fog_st = g_sys->cur_weather.fog_st;
+ if (far_clp <= fog_st) {
+ far_clp = fog_st + 1.0f;
+ }
+ min_dist = 1e9f;
+
+ for (i = 0; i < m->num_verts; i++) {
+ in.x = m->ply_verts[i].x;
+ in.y = m->ply_verts[i].y;
+ in.z = m->ply_verts[i].z;
+ mat_transform(&out, &w, mvp, &in);
+ cv[i].x = out.x;
+ cv[i].y = out.y;
+ cv[i].z = out.z;
+ cv[i].w = w;
+ cv[i].u = m->ply_verts[i].s;
+ cv[i].v = m->ply_verts[i].t;
+ fog = (out.z - fog_st) / (far_clp - fog_st);
+ if (fog < 0.0f) {
+ fog = 0.0f;
+ } else if (fog > 1.0f) {
+ fog = 1.0f;
+ }
+ cv[i].f = fog;
+
+ /* override model pre-light colors with uniform seamless underwater tone */
+ if (is_seabed) {
+ cv[i].r = 0.25f * ar;
+ cv[i].g = 0.28f * ag;
+ cv[i].b = 0.30f * ab;
+ } else {
+ cv[i].r = (m->ply_verts[i].r / 255.0f) * ar;
+ cv[i].g = (m->ply_verts[i].g / 255.0f) * ag;
+ cv[i].b = (m->ply_verts[i].b / 255.0f) * ab;
+ }
+ cv[i].a = 1.0f;
+
+ d = sqrtf(out.x*out.x + out.y*out.y + out.z*out.z);
+ if (d < min_dist) {
+ min_dist = d;
+ }
+ }
+ mip = 0;
+ scale = sqrtf(m->total_area) * 0.05f;
+ if (scale < 0.2f) {
+ scale = 0.2f;
+ }
+ if (scale > 5.0f) {
+ scale = 5.0f;
+ }
+ if (min_dist > 150.0f * scale) {
+ mip = 3;
+ } else if (min_dist > 80.0f * scale) {
+ mip = 2;
+ } else if (min_dist > 30.0f * scale) {
+ mip = 1;
+ }
+
+ for (i = 0; i < m->num_tris; i++) {
+ uint16_t i0 = m->indices[i*3], i1 = m->indices[i*3+1], i2 = m->indices[i*3+2];
+ struct texture *tex;
+
+ if (cv[i0].w < 0.1f && cv[i1].w < 0.1f && cv[i2].w < 0.1f) {
+ continue;
+ }
+ if (cv[i0].w > far_clp && cv[i1].w > far_clp && cv[i2].w > far_clp) {
+ continue;
+ }
+ tex = (m->num_materials > 0 && m->textures) ? m->textures[m->mat_ids[i] < m->num_materials ? m->mat_ids[i] : 0] : NULL;
+ clip_and_push_triangle(&cv[i0], &cv[i1], &cv[i2], tex, 0, mip);
+ }
+}
+
+static int
+is_occluded(struct sys_gfx *g, float cx, float cy, float cz, float r)
+{
+ (void)g; (void)cx; (void)cy; (void)cz; (void)r;
+ return (0);
+}
+
+static int
+cmp_draw_item(const void *a, const void *b)
+{
+ float da = ((const struct draw_item *)a)->dist_sq;
+ float db = ((const struct draw_item *)b)->dist_sq;
+
+ if (da < db)
+ return (-1);
+ if (da > db)
+ return (1);
+ return (0);
+}
+
+static void
+draw_scene(struct sys_gfx *g, const struct mat4 *proj, const struct mat4 *view, float cam_x, float cam_y, float cam_z)
+{
+ float val, pitch, fov_y, fov_x, far_clp, dx, dy, dz, dist_sq, w, r;
+ int y_horiz, min_gx, max_gx, min_gz, max_gz, id, x, y, gx, gz, i;
+ uint8_t sr, sg, sb, hr, hg, hb;
+ uint32_t c;
+ struct mesh *m;
+ struct vec3 mc, cv;
+ struct mat4 mvp;
+
+ val = view->m[1][2];
+ if (val > 1.0f) {
+ val = 1.0f;
+ } else if (val < -1.0f) {
+ val = -1.0f;
+ }
+ pitch = asin(val);
+ y_horiz = HEIGHT / 2 + (int)(pitch * 400.0f);
+ sr = g->cur_weather.sky_top[0];
+ sg = g->cur_weather.sky_top[1];
+ sb = g->cur_weather.sky_top[2];
+ hr = g->cur_weather.sky_bot[0];
+ hg = g->cur_weather.sky_bot[1];
+ hb = g->cur_weather.sky_bot[2];
+
+ for (y = 0; y < HEIGHT; y++) {
+ c = (hr << 16) | (hg << 8) | hb;
+ if (y < y_horiz) {
+ float f = (float)y / (float)(y_horiz > 0 ? y_horiz : 1);
+ c = ((sr + (uint8_t)(f * (hr - sr))) << 16) | ((sg + (uint8_t)(f * (hg - sg))) << 8) | (sb + (uint8_t)(f * (hb - sb)));
+ }
+ for (x = 0; x < WIDTH; x++) {
+ g->pixels[y * WIDTH + x] = c;
+ }
+ }
+ far_clp = g->cur_weather.far_clp;
+ fov_y = tanf(60.0f * 0.5f * 3.14159f / 180.0f);
+ fov_x = fov_y * ((float)WIDTH / HEIGHT);
+ min_gx = (int)((cam_x - far_clp - MAP_MIN) / ((MAP_MAX - MAP_MIN) / GRID_SZ));
+ max_gx = (int)((cam_x + far_clp - MAP_MIN) / ((MAP_MAX - MAP_MIN) / GRID_SZ));
+ min_gz = (int)((cam_z - far_clp - MAP_MIN) / ((MAP_MAX - MAP_MIN) / GRID_SZ));
+ max_gz = (int)((cam_z + far_clp - MAP_MIN) / ((MAP_MAX - MAP_MIN) / GRID_SZ));
+
+ if (min_gx < 0) min_gx = 0;
+ if (max_gx >= GRID_SZ) max_gx = GRID_SZ - 1;
+ if (min_gz < 0) min_gz = 0;
+ if (max_gz >= GRID_SZ) max_gz = GRID_SZ - 1;
+
+ draw_count = 0;
+ for (gz = min_gz; gz <= max_gz; gz++) {
+ for (gx = min_gx; gx <= max_gx; gx++) {
+ for (i = 0; i < scene_grid[gx][gz].count; i++) {
+ id = scene_grid[gx][gz].ids[i];
+
+ /* check time-of-day visibility for timed objects */
+ int16_t t_on = g_transforms.time_on[id];
+ int16_t t_off = g_transforms.time_off[id];
+ if (t_on != t_off) {
+ float cur_h = g->game_time_hours;
+ int visible = 0;
+ if (t_on > t_off) {
+ if (cur_h >= t_on || cur_h < t_off) {
+ visible = 1;
+ }
+ } else {
+ if (cur_h >= t_on && cur_h < t_off) {
+ visible = 1;
+ }
+ }
+ if (!visible) {
+ continue;
+ }
+ }
+
+ dx = g_transforms.sphere_cx[id] - cam_x;
+ dy = g_transforms.sphere_cy[id] - cam_y;
+ dz = g_transforms.sphere_cz[id] - cam_z;
+ dist_sq = dx*dx + dy*dy + dz*dz;
+ if (dist_sq > (far_clp + g_transforms.sphere_r[id])*(far_clp + g_transforms.sphere_r[id])) {
+ continue;
+ }
+ if (draw_count >= draw_cap) {
+ draw_cap = draw_cap == 0 ? 1024 : draw_cap * 2;
+ draw_list = realloc(draw_list, draw_cap * sizeof(struct draw_item));
+ }
+ draw_list[draw_count].id = id;
+ draw_list[draw_count].dist_sq = dist_sq;
+ draw_count++;
+ }
+ }
+ }
+ qsort(draw_list, draw_count, sizeof(struct draw_item), cmp_draw_item);
+
+ for (i = 0; i < draw_count; i++) {
+ id = draw_list[i].id;
+ m = scene_meshes[id];
+ if (!m || !m->ply_verts) {
+ continue;
+ }
+ mc.x = g_transforms.sphere_cx[id];
+ mc.y = g_transforms.sphere_cy[id];
+ mc.z = g_transforms.sphere_cz[id];
+ r = g_transforms.sphere_r[id] * 1.05f;
+ mat_transform(&cv, &w, view, &mc);
+ if (cv.z + r < 0.1f || cv.z - r > far_clp || fabsf(cv.x) - r > cv.z * fov_x || fabsf(cv.y) - r > cv.z * fov_y) {
+ continue;
+ }
+ if (is_occluded(g, cv.x, cv.y, cv.z, r)) {
+ continue;
+ }
+ mat_mul(&mvp, view, &g_transforms.world_matrices[id]);
+ mat_mul(&mvp, proj, &mvp);
+ draw_mesh(m, &mvp, 0);
+ }
+ flush_render_list();
+}
+
+static void
+update_weather(struct sys_gfx *g)
+{
+ float t, snaps[8] = {0.0f, 5.0f, 6.0f, 7.0f, 12.0f, 19.0f, 20.0f, 22.0f};
+ int i0, i1;
+ float f;
+ struct timecyc_entry *w0, *w1;
+
+ if (g->num_weathers == 0) {
+ return;
+ }
+ if (g->game_weather >= g->num_weathers) {
+ g->game_weather = 0;
+ }
+ t = g->game_time_hours;
+ i0 = 7;
+ i1 = 0;
+ f = 0.0f;
+ if (t >= snaps[7] || t < snaps[0]) {
+ float wt = t;
+ if (wt < snaps[0]) {
+ wt += 24.0f;
+ }
+ f = (wt - snaps[7]) / (24.0f - snaps[7] + snaps[0]);
+ } else {
+ for (int i = 0; i < 7; i++) {
+ if (t >= snaps[i] && t < snaps[i+1]) {
+ i0 = i;
+ i1 = i + 1;
+ f = (t - snaps[i]) / (snaps[i+1] - snaps[i]);
+ break;
+ }
+ }
+ }
+ w0 = &g->weathers[g->game_weather][i0];
+ w1 = &g->weathers[g->game_weather][i1];
+ #define LERP(c) g->cur_weather.c = w0->c + f * (w1->c - w0->c)
+ LERP(amb[0]); LERP(amb[1]); LERP(amb[2]); LERP(sky_top[0]); LERP(sky_top[1]); LERP(sky_top[2]); LERP(sky_bot[0]); LERP(sky_bot[1]); LERP(sky_bot[2]);
+ LERP(water[0]); LERP(water[1]); LERP(water[2]); LERP(water[3]); LERP(far_clp); LERP(fog_st);
+ #undef LERP
+ if (g->far_clip_override > 10.0f) {
+ g->cur_weather.far_clp = g->far_clip_override;
+ }
+}
+
+static void
+draw_water(const struct mat4 *proj, const struct mat4 *view)
+{
+ struct clip_vertex cv[4];
+ struct mat4 mvp;
+ struct vec3 in, out;
+ float t, far_clp, fog_st, ar, ag, ab, w, fog, u_tex, v_tex;
+ float wave_height, wave_phase, wave_speed;
+ float flow_x, flow_z, len, dir_x, dir_z;
+ float dx, dy, dz, scroll_u, scroll_v;
+ uint32_t i;
+ int j;
+
+ mat_mul(&mvp, proj, view);
+ t = g_sys->game_time_sec;
+ far_clp = g_sys->cur_weather.far_clp;
+ fog_st = g_sys->cur_weather.fog_st;
+ if (far_clp <= fog_st) {
+ far_clp = fog_st + 1.0f;
+ }
+ ar = g_sys->cur_weather.amb[0] / 255.0f * 1.2f;
+ ag = g_sys->cur_weather.amb[1] / 255.0f * 1.2f;
+ ab = g_sys->cur_weather.amb[2] / 255.0f * 1.2f;
+
+ for (i = 0; i < water_count; i++) {
+ for (j = 0; j < water_faces[i].num; j++) {
+ flow_x = water_faces[i].u[j];
+ flow_z = water_faces[i].v[j];
+ wave_height = water_faces[i].h[j];
+
+ len = sqrtf(flow_x * flow_x + flow_z * flow_z);
+ dir_x = 0.7071f;
+ dir_z = 0.7071f;
+ if (len > 0.001f) {
+ dir_x = flow_x / len;
+ dir_z = flow_z / len;
+ }
+
+ wave_speed = (len > 0.01f) ? (len * 3.0f) : 1.5f;
+
+ wave_phase = t * wave_speed +
+ (water_faces[i].x[j] * dir_x + water_faces[i].y[j] * dir_z) * 0.04f;
+
+ dy = sinf(wave_phase) * wave_height;
+
+ dx = -cosf(wave_phase) * wave_height * 0.45f * dir_x;
+ dz = -cosf(wave_phase) * wave_height * 0.45f * dir_z;
+
+ in.x = water_faces[i].x[j] + dx;
+ in.y = water_faces[i].z[j] + dy;
+ in.z = water_faces[i].y[j] + dz;
+
+ mat_transform(&out, &w, &mvp, &in);
+ fog = (out.z - fog_st) / (far_clp - fog_st);
+ if (fog < 0.0f) {
+ fog = 0.0f;
+ } else if (fog > 1.0f) {
+ fog = 1.0f;
+ }
+
+ scroll_u = flow_x;
+ scroll_v = flow_z;
+ if (fabsf(scroll_u) < 0.001f && fabsf(scroll_v) < 0.001f) {
+ scroll_u = 0.006f;
+ scroll_v = 0.004f;
+ }
+
+ u_tex = (water_faces[i].x[j] - water_faces[i].x[0]) * 0.01f + t * scroll_u + dy * 0.03f;
+ v_tex = (water_faces[i].y[j] - water_faces[i].y[0]) * 0.01f + t * scroll_v + dy * 0.03f;
+
+ cv[j].x = out.x;
+ cv[j].y = out.y;
+ cv[j].z = out.z;
+ cv[j].w = w;
+ cv[j].u = u_tex;
+ cv[j].v = v_tex;
+ cv[j].r = ar;
+ cv[j].g = ag;
+ cv[j].b = ab;
+ cv[j].a = 1.0f;
+ cv[j].f = fog;
+ }
+ if (water_faces[i].num == 4) {
+ if (!(cv[0].w < 0.1f && cv[2].w < 0.1f && cv[1].w < 0.1f) && !(cv[0].w > far_clp && cv[2].w > far_clp && cv[1].w > far_clp)) {
+ clip_and_push_triangle(&cv[0], &cv[2], &cv[1], water_tex, 1, 0);
+ }
+ if (!(cv[2].w < 0.1f && cv[3].w < 0.1f && cv[1].w < 0.1f) && !(cv[2].w > far_clp && cv[3].w > far_clp && cv[1].w > far_clp)) {
+ clip_and_push_triangle(&cv[2], &cv[3], &cv[1], water_tex, 1, 0);
+ }
+ } else if (water_faces[i].num == 3) {
+ if (!(cv[0].w < 0.1f && cv[2].w < 0.1f && cv[1].w < 0.1f) && !(cv[0].w > far_clp && cv[2].w > far_clp && cv[1].w > far_clp)) {
+ clip_and_push_triangle(&cv[0], &cv[2], &cv[1], water_tex, 1, 0);
+ }
+ }
+ }
+ flush_render_list();
+}
+
+static void
+draw_ocean_sector(const struct mat4 *mvp, float x0, float x1, float z0, float z1, float height, float t, const struct texture *tex, int is_water)
+{
+ struct clip_vertex cv[4];
+ struct vec3 in, out;
+ float far_clp, fog_st, ar, ag, ab, w, fog, u_tex, v_tex;
+ float prelight_r, prelight_g, prelight_b;
+ float step = 500.0f;
+ float tx, tz, next_tx, next_tz;
+ float xs[4], zs[4];
+ float wave_height, wave_phase, dx, dy, dz;
+ float dir_x = 0.7071f, dir_z = 0.7071f;
+ int i;
+
+ far_clp = g_sys->cur_weather.far_clp;
+ fog_st = g_sys->cur_weather.fog_st;
+ if (far_clp <= fog_st) {
+ far_clp = fog_st + 1.0f;
+ }
+ ar = g_sys->cur_weather.amb[0] / 255.0f * 1.2f;
+ ag = g_sys->cur_weather.amb[1] / 255.0f * 1.2f;
+ ab = g_sys->cur_weather.amb[2] / 255.0f * 1.2f;
+
+ prelight_r = is_water ? 1.0f : 0.35f;
+ prelight_g = is_water ? 1.0f : 0.38f;
+ prelight_b = is_water ? 1.0f : 0.40f;
+
+ wave_height = is_water ? 0.8f : 0.0f;
+
+ for (tx = floorf(x0 / step) * step; tx < x1; tx += step) {
+ next_tx = tx + step;
+ if (next_tx > x1) {
+ next_tx = x1;
+ }
+ for (tz = floorf(z0 / step) * step; tz < z1; tz += step) {
+ next_tz = tz + step;
+ if (next_tz > z1) {
+ next_tz = z1;
+ }
+
+ xs[0] = tx; xs[1] = next_tx; xs[2] = next_tx; xs[3] = tx;
+ zs[0] = tz; zs[1] = tz; zs[2] = next_tz; zs[3] = next_tz;
+
+ for (i = 0; i < 4; i++) {
+ dy = 0.0f;
+ dx = 0.0f;
+ dz = 0.0f;
+
+ if (is_water) {
+ wave_phase = t * 1.5f + (xs[i] * dir_x + zs[i] * dir_z) * 0.04f;
+ dy = sinf(wave_phase) * wave_height;
+ dx = -cosf(wave_phase) * wave_height * 0.45f * dir_x;
+ dz = -cosf(wave_phase) * wave_height * 0.45f * dir_z;
+ }
+
+ in.x = xs[i] + dx;
+ in.y = height + dy;
+ in.z = zs[i] + dz;
+
+ mat_transform(&out, &w, mvp, &in);
+ fog = (out.z - fog_st) / (far_clp - fog_st);
+ if (fog < 0.0f) {
+ fog = 0.0f;
+ } else if (fog > 1.0f) {
+ fog = 1.0f;
+ }
+
+ u_tex = xs[i] * 0.01f;
+ v_tex = zs[i] * 0.01f;
+ if (is_water) {
+ u_tex += t * 0.008f + dy * 0.03f;
+ v_tex += t * 0.006f + dy * 0.03f;
+ }
+
+ cv[i] = (struct clip_vertex){
+ out.x, out.y, out.z, w, u_tex, v_tex,
+ ar * prelight_r, ag * prelight_g, ab * prelight_b,
+ 1.0f, fog
+ };
+ }
+ if (!(cv[0].w < 0.1f && cv[2].w < 0.1f && cv[1].w < 0.1f) && !(cv[0].w > far_clp && cv[2].w > far_clp && cv[1].w > far_clp)) {
+ clip_and_push_triangle(&cv[0], &cv[2], &cv[1], tex, is_water, 0);
+ }
+ if (!(cv[0].w < 0.1f && cv[3].w < 0.1f && cv[2].w < 0.1f) && !(cv[0].w > far_clp && cv[3].w > far_clp && cv[2].w > far_clp)) {
+ clip_and_push_triangle(&cv[0], &cv[3], &cv[2], tex, is_water, 0);
+ }
+ }
+ }
+}
+
+static void
+draw_infinite_ocean(const struct mat4 *proj, const struct mat4 *view, float cam_x, float cam_z, float t)
+{
+ struct mat4 mvp;
+ float far_clp, b;
+ float n_x0, n_x1, n_z0, n_z1;
+ float s_x0, s_x1, s_z0, s_z1;
+ float w_x0, w_x1, w_z0, w_z1;
+ float e_x0, e_x1, e_z0, e_z1;
+
+ mat_mul(&mvp, proj, view);
+ far_clp = g_sys->cur_weather.far_clp;
+ b = 2970.0f; /* 10m overlap to close seams */
+
+ /* north sector */
+ n_x0 = cam_x - far_clp * 1.5f;
+ n_x1 = cam_x + far_clp * 1.5f;
+ n_z0 = fmaxf(b, cam_z - far_clp * 1.5f);
+ n_z1 = cam_z + far_clp * 1.5f;
+ if (n_z1 > n_z0) {
+ draw_ocean_sector(&mvp, n_x0, n_x1, n_z0, n_z1, -65.0f, t, sand_tex, 0);
+ draw_ocean_sector(&mvp, n_x0, n_x1, n_z0, n_z1, 0.0f, t, water_tex, 1);
+ }
+
+ /* south sector */
+ s_x0 = cam_x - far_clp * 1.5f;
+ s_x1 = cam_x + far_clp * 1.5f;
+ s_z0 = cam_z - far_clp * 1.5f;
+ s_z1 = fminf(-b, cam_z + far_clp * 1.5f);
+ if (s_z1 > s_z0) {
+ draw_ocean_sector(&mvp, s_x0, s_x1, s_z0, s_z1, -65.0f, t, sand_tex, 0);
+ draw_ocean_sector(&mvp, s_x0, s_x1, s_z0, s_z1, 0.0f, t, water_tex, 1);
+ }
+
+ /* west sector */
+ w_x0 = cam_x - far_clp * 1.5f;
+ w_x1 = fminf(-b, cam_x + far_clp * 1.5f);
+ w_z0 = fmaxf(-b, cam_z - far_clp * 1.5f);
+ w_z1 = fminf(b, cam_z + far_clp * 1.5f);
+ if (w_x1 > w_x0 && w_z1 > w_z0) {
+ draw_ocean_sector(&mvp, w_x0, w_x1, w_z0, w_z1, -65.0f, t, sand_tex, 0);
+ draw_ocean_sector(&mvp, w_x0, w_x1, w_z0, w_z1, 0.0f, t, water_tex, 1);
+ }
+
+ /* east sector */
+ e_x0 = fmaxf(b, cam_x - far_clp * 1.5f);
+ e_x1 = cam_x + far_clp * 1.5f;
+ e_z0 = fmaxf(-b, cam_z - far_clp * 1.5f);
+ e_z1 = fminf(b, cam_z + far_clp * 1.5f);
+ if (e_x1 > e_x0 && e_z1 > e_z0) {
+ draw_ocean_sector(&mvp, e_x0, e_x1, e_z0, e_z1, -65.0f, t, sand_tex, 0);
+ draw_ocean_sector(&mvp, e_x0, e_x1, e_z0, e_z1, 0.0f, t, water_tex, 1);
+ }
+ flush_render_list();
+}
+
+static void
+update_camera(float *cx, float *cy, float *cz, float *cyaw, float *cpitch, const char *keys, int m_dx, int m_dy, float dt)
+{
+ float speed, rot_speed, cp, sp, cy_val, sy_val;
+
+ speed = 100.0f * dt;
+ rot_speed = 0.01f * dt;
+ if (keys[258]) *cyaw -= rot_speed;
+ if (keys[259]) *cyaw += rot_speed;
+ if (keys[256]) *cpitch += rot_speed;
+ if (keys[257]) *cpitch -= rot_speed;
+ *cyaw += m_dx * 0.002f;
+ *cpitch -= m_dy * 0.002f;
+ if (*cpitch > 1.4f) *cpitch = 1.4f;
+ if (*cpitch < -1.4f) *cpitch = -1.4f;
+ cp = cosf(*cpitch);
+ sp = sinf(*cpitch);
+ cy_val = cosf(*cyaw);
+ sy_val = sinf(*cyaw);
+ if (keys['w']) { *cx += sy_val * cp * speed; *cy += sp * speed; *cz += cy_val * cp * speed; }
+ if (keys['s']) { *cx -= sy_val * cp * speed; *cy -= sp * speed; *cz -= cy_val * cp * speed; }
+ if (keys['a']) { *cx -= cy_val * speed; *cz += sy_val * speed; }
+ if (keys['d']) { *cx += cy_val * speed; *cz -= sy_val * speed; }
+}
+
+static void
+scene_load(struct asset_cache *cache, const uint8_t *data, size_t sz)
+{
+ char *buf, *line, *next_line, *p, *end;
+ int in_inst, id, interior, idx, gx, gz;
+ char name[NAME_SZ];
+ float px, py, pz, qx, qy, qz, qw, w;
+ struct mesh *m;
+ struct mat4 rot, trans, sa2gl;
+ struct vec3 wc;
+ struct grid_cell *c;
+ int lod, time_on, time_off;
+
+ buf = malloc(sz + 1);
+ if (buf == NULL) {
+ return;
+ }
+ memcpy(buf, data, sz);
+ buf[sz] = '\0';
+
+ line = buf;
+ in_inst = 0;
+ while (line && *line) {
+ next_line = strchr(line, '\n');
+ if (next_line) {
+ *next_line = '\0';
+ next_line++;
+ }
+ p = line;
+ while (*p == ' ' || *p == '\t') {
+ p++;
+ }
+ if (*p == '\0' || *p == '#') {
+ line = next_line;
+ continue;
+ }
+ if (strncasecmp(p, "inst", 4) == 0 || strncasecmp(p, "tobj", 4) == 0) {
+ in_inst = 1;
+ line = next_line;
+ continue;
+ }
+ if (strncasecmp(p, "end", 3) == 0) {
+ in_inst = 0;
+ line = next_line;
+ continue;
+ }
+ if (in_inst) {
+ /* initialize fields to default values before sscanf */
+ lod = -1;
+ time_on = 0;
+ time_off = 0;
+
+ if (sscanf(p, "%d, %23[^,], %d, %f, %f, %f, %f, %f, %f, %f, %d, %d, %d",
+ &id, name, &interior, &px, &py, &pz, &qx, &qy, &qz, &qw, &lod, &time_on, &time_off) >= 10) {
+ end = name + strlen(name) - 1;
+ while (end > name && (*end == ' ' || *end == '\t')) {
+ *end = '\0';
+ end--;
+ }
+ for (p = name; *p; p++) {
+ *p = tolower((unsigned char)*p);
+ }
+
+ if (scene_inst_count >= scene_inst_capacity) {
+ scene_inst_capacity = scene_inst_capacity == 0 ? 512 : scene_inst_capacity * 2;
+ scene_meshes = realloc(scene_meshes, scene_inst_capacity * sizeof(struct mesh *));
+ g_transforms.pos_x = realloc(g_transforms.pos_x, scene_inst_capacity * sizeof(float));
+ g_transforms.pos_y = realloc(g_transforms.pos_y, scene_inst_capacity * sizeof(float));
+ g_transforms.pos_z = realloc(g_transforms.pos_z, scene_inst_capacity * sizeof(float));
+ g_transforms.rot_qx = realloc(g_transforms.rot_qx, scene_inst_capacity * sizeof(float));
+ g_transforms.rot_qy = realloc(g_transforms.rot_qy, scene_inst_capacity * sizeof(float));
+ g_transforms.rot_qz = realloc(g_transforms.rot_qz, scene_inst_capacity * sizeof(float));
+ g_transforms.rot_qw = realloc(g_transforms.rot_qw, scene_inst_capacity * sizeof(float));
+ g_transforms.world_matrices = realloc(g_transforms.world_matrices, scene_inst_capacity * sizeof(struct mat4));
+ g_transforms.sphere_cx = realloc(g_transforms.sphere_cx, scene_inst_capacity * sizeof(float));
+ g_transforms.sphere_cy = realloc(g_transforms.sphere_cy, scene_inst_capacity * sizeof(float));
+ g_transforms.sphere_cz = realloc(g_transforms.sphere_cz, scene_inst_capacity * sizeof(float));
+ g_transforms.sphere_r = realloc(g_transforms.sphere_r, scene_inst_capacity * sizeof(float));
+ g_transforms.time_on = realloc(g_transforms.time_on, scene_inst_capacity * sizeof(int16_t));
+ g_transforms.time_off = realloc(g_transforms.time_off, scene_inst_capacity * sizeof(int16_t));
+ }
+
+ m = cache_get_mesh(cache, name);
+ if (m) {
+ idx = scene_inst_count++;
+ scene_meshes[idx] = m;
+ g_transforms.pos_x[idx] = px;
+ g_transforms.pos_y[idx] = py;
+ g_transforms.pos_z[idx] = pz;
+ g_transforms.rot_qx[idx] = qx;
+ g_transforms.rot_qy[idx] = qy;
+ g_transforms.rot_qz[idx] = qz;
+ g_transforms.rot_qw[idx] = qw;
+
+ /* store limits for time checking */
+ g_transforms.time_on[idx] = (time_on >= 0) ? time_on : 0;
+ g_transforms.time_off[idx] = (time_off >= 0) ? time_off : 0;
+
+ mat_from_quat(&rot, qx, qy, qz, qw);
+ mat_translate(&trans, px, py, pz);
+ mat_mul(&g_transforms.world_matrices[idx], &trans, &rot);
+
+ memset(&sa2gl, 0, sizeof(sa2gl));
+ sa2gl.m[0][0] = sa2gl.m[1][2] = sa2gl.m[2][1] = sa2gl.m[3][3] = 1.0f;
+ mat_mul(&g_transforms.world_matrices[idx], &sa2gl, &g_transforms.world_matrices[idx]);
+
+ mat_transform(&wc, &w, &g_transforms.world_matrices[idx], &(struct vec3){m->spr_x, m->spr_y, m->spr_z});
+ g_transforms.sphere_cx[idx] = wc.x;
+ g_transforms.sphere_cy[idx] = wc.y;
+ g_transforms.sphere_cz[idx] = wc.z;
+ g_transforms.sphere_r[idx] = m->spr_r;
+
+ gx = (int)((wc.x - MAP_MIN) / ((MAP_MAX - MAP_MIN) / GRID_SZ));
+ gz = (int)((wc.z - MAP_MIN) / ((MAP_MAX - MAP_MIN) / GRID_SZ));
+ if (gx < 0) gx = 0; if (gx >= GRID_SZ) gx = GRID_SZ - 1;
+ if (gz < 0) gz = 0; if (gz >= GRID_SZ) gz = GRID_SZ - 1;
+
+ c = &scene_grid[gx][gz];
+ if (c->count >= c->cap) {
+ c->cap = c->cap == 0 ? 16 : c->cap * 2;
+ c->ids = realloc(c->ids, c->cap * sizeof(int));
+ }
+ c->ids[c->count++] = idx;
+ }
+ }
+ }
+ line = next_line;
+ }
+ free(buf);
+}
+
+static void
+tar_build_index(void)
+{
+ size_t offset = 0;
+ struct tar_header *h;
+ size_t file_sz;
+ uint32_t slot;
+
+ memset(tar_idx, 0, sizeof(tar_idx));
+
+ while (offset < g_tar_size) {
+ h = (struct tar_header *)(g_tar_mapped + offset);
+ if (h->name[0] == '\0') {
+ break;
+ }
+ file_sz = get_tar_size(h->size);
+ const uint8_t *file_data = g_tar_mapped + offset + 512;
+
+ slot = tar_hash(h->name) % TAR_INDEX_SIZE;
+ while (tar_idx[slot].data != NULL) {
+ if (strcasecmp(tar_idx[slot].name, h->name) == 0) {
+ goto next_file;
+ }
+ slot = (slot + 1) % TAR_INDEX_SIZE;
+ }
+
+ strncpy(tar_idx[slot].name, h->name, sizeof(tar_idx[slot].name) - 1);
+ tar_idx[slot].data = file_data;
+ tar_idx[slot].size = file_sz;
+
+ next_file:
+ offset += 512 + ((file_sz + 511) & ~511);
+ }
+}
+
+int
+main(void)
+{
+ struct sys_gfx g;
+ struct asset_cache *cache;
+ struct mat4 view;
+ struct timespec ts, t_start, t_end;
+ XEvent ev;
+ float cam_x = 2490.0f, cam_y = 15.0f, cam_z = -1660.0f, cam_yaw = -1.57f, cam_pitch = 0.0f, dt;
+ int running;
+ int tar_fd;
+ struct stat st;
+ size_t ipl_sz = 0;
+ const uint8_t *ipl_data;
+ size_t water_sz = 0;
+ const uint8_t *water_data;
+ size_t tc_sz = 0;
+ const uint8_t *tc_data;
+ int m_dx, m_dy, gz, gx, i;
+ int last_mx, last_my;
+ KeyCode code;
+
+ g_sys = &g;
+ gfx_init(&g);
+ cache = calloc(1, sizeof(struct asset_cache));
+ g.game_time_sec = 0.0f;
+ g.game_time_hours = 12.0f;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 16666666;
+
+ tar_fd = open("assets.tar", O_RDONLY);
+ if (tar_fd < 0) {
+ err(1, "failed to open assets.tar. run bin/build first");
+ }
+ fstat(tar_fd, &st);
+ g_tar_size = st.st_size;
+ g_tar_mapped = mmap(NULL, g_tar_size, PROT_READ, MAP_PRIVATE, tar_fd, 0);
+ if (g_tar_mapped == MAP_FAILED) {
+ err(1, "failed to mmap assets.tar");
+ }
+ tar_build_index();
+
+ ipl_data = tar_lookup("assets/map.ipl", &ipl_sz);
+ if (ipl_data) {
+ scene_load(cache, ipl_data, ipl_sz);
+ }
+
+ water_data = tar_lookup("assets/water.bin", &water_sz);
+ if (water_data && water_sz > 0) {
+ memcpy(&water_count, water_data, 4);
+ water_faces = malloc(water_count * sizeof(struct water_face));
+ memcpy(water_faces, water_data + 4, water_count * sizeof(struct water_face));
+ }
+
+ tc_data = tar_lookup("assets/timecyc.bin", &tc_sz);
+ if (tc_data && tc_sz > 0) {
+ memcpy(&g.num_weathers, tc_data, 4);
+ if (g.num_weathers > 32) {
+ g.num_weathers = 32;
+ }
+ memcpy(g.weathers, tc_data + 4, g.num_weathers * 8 * sizeof(struct timecyc_entry));
+ }
+
+ water_tex = cache_get_tex(cache, "waterclear256");
+ sand_tex = cache_get_tex(cache, "sand256");
+
+ XMapWindow(g.dpy, g.win);
+ clock_gettime(CLOCK_MONOTONIC, &t_start);
+ XWarpPointer(g.dpy, None, g.win, 0, 0, 0, 0, WIDTH / 2, HEIGHT / 2);
+ XGrabPointer(g.dpy, g.win, True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask, GrabModeAsync, GrabModeAsync, g.win, None, CurrentTime);
+ XSync(g.dpy, False);
+
+ running = 1;
+ while (running) {
+ last_mx = WIDTH / 2;
+ last_my = HEIGHT / 2;
+ m_dx = 0;
+ m_dy = 0;
+
+ while (XPending(g.dpy)) {
+ XNextEvent(g.dpy, &ev);
+
+ /* filter dead keys and system layout events */
+ if (XFilterEvent(&ev, None)) {
+ continue;
+ }
+
+ if (ev.type == ClientMessage && (Atom)ev.xclient.data.l[0] == g.wm_delete) {
+ running = 0;
+ }
+ if (ev.type == FocusIn && !g.in_chat) {
+ XGrabPointer(g.dpy, g.win, True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask, GrabModeAsync, GrabModeAsync, g.win, None, CurrentTime);
+ }
+ if (ev.type == MotionNotify) {
+ last_mx = ev.xmotion.x;
+ last_my = ev.xmotion.y;
+ }
+ if (ev.type == KeyPress) {
+ code = ev.xkey.keycode;
+ if (code == g.key_esc) {
+ if (g.in_chat) {
+ g.in_chat = 0;
+ XGrabPointer(g.dpy, g.win, True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask, GrabModeAsync, GrabModeAsync, g.win, None, CurrentTime);
+ } else {
+ running = 0;
+ }
+ }
+ if (g.in_chat) {
+ char str[32];
+ KeySym sym;
+ Status status;
+ int n = 0;
+
+ if (g_sys->ic) {
+ n = Xutf8LookupString(g_sys->ic, &ev.xkey, str, sizeof(str), &sym, &status);
+ } else {
+ n = XLookupString(&ev.xkey, str, sizeof(str), &sym, NULL);
+ }
+
+ if (sym == XK_Return) {
+ if (strncmp(g.chat_buf, "/st ", 4) == 0) {
+ g.game_time_hours = atof(g.chat_buf + 4);
+ } else if (strncmp(g.chat_buf, "/sw ", 4) == 0) {
+ g.game_weather = atoi(g.chat_buf + 4);
+ } else if (strncmp(g.chat_buf, "/sd ", 4) == 0) {
+ g.far_clip_override = atof(g.chat_buf + 4);
+ }
+ g.in_chat = 0;
+ XGrabPointer(g.dpy, g.win, True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask, GrabModeAsync, GrabModeAsync, g.win, None, CurrentTime);
+ } else if (sym == XK_BackSpace) {
+ if (g.chat_len > 0) {
+ while (g.chat_len > 0) {
+ g.chat_len--;
+ if ((g.chat_buf[g.chat_len] & 0xC0) != 0x80) {
+ break;
+ }
+ }
+ g.chat_buf[g.chat_len] = '\0';
+ }
+ } else if (n > 0 && (unsigned char)str[0] >= 32 && str[0] != 127 && g.chat_len + n < 127) {
+ memcpy(&g.chat_buf[g.chat_len], str, n);
+ g.chat_len += n;
+ g.chat_buf[g.chat_len] = '\0';
+ }
+ } else {
+ if (code == g.key_w) g.keys['w'] = 1;
+ else if (code == g.key_a) g.keys['a'] = 1;
+ else if (code == g.key_s) g.keys['s'] = 1;
+ else if (code == g.key_d) g.keys['d'] = 1;
+ else if (code == g.key_up) g.keys[256] = 1;
+ else if (code == g.key_down) g.keys[257] = 1;
+ else if (code == g.key_left) g.keys[258] = 1;
+ else if (code == g.key_right) g.keys[259] = 1;
+ else if (code == g.key_t) {
+ g.in_chat = 1;
+ g.chat_len = 0;
+ g.chat_buf[0] = '\0';
+ XUngrabPointer(g.dpy, CurrentTime);
+ }
+ }
+ }
+ if (ev.type == KeyRelease) {
+ code = ev.xkey.keycode;
+ if (code == g.key_w) g.keys['w'] = 0;
+ else if (code == g.key_a) g.keys['a'] = 0;
+ else if (code == g.key_s) g.keys['s'] = 0;
+ else if (code == g.key_d) g.keys['d'] = 0;
+ else if (code == g.key_up) g.keys[256] = 0;
+ else if (code == g.key_down) g.keys[257] = 0;
+ else if (code == g.key_left) g.keys[258] = 0;
+ else if (code == g.key_right) g.keys[259] = 0;
+ }
+ }
+
+ /* calculate single precise delta at the end of the frame */
+ m_dx = last_mx - WIDTH / 2;
+ m_dy = last_my - HEIGHT / 2;
+
+ if (m_dx != 0 || m_dy != 0) {
+ XWarpPointer(g.dpy, None, g.win, 0, 0, 0, 0, WIDTH / 2, HEIGHT / 2);
+ XFlush(g.dpy);
+ }
+
+ clock_gettime(CLOCK_MONOTONIC, &t_end);
+ dt = (t_end.tv_sec - t_start.tv_sec) + (t_end.tv_nsec - t_start.tv_nsec) * 1e-9f;
+ t_start = t_end;
+ if (dt > 0.1f) dt = 0.1f; if (dt < 0.001f) dt = 0.001f;
+
+ g.game_time_sec += dt;
+ g.game_time_hours += dt * (24.0f / 1440.0f);
+ if (g.game_time_hours >= 24.0f) {
+ g.game_time_hours -= 24.0f;
+ }
+
+ if (!g.in_chat) {
+ update_camera(&cam_x, &cam_y, &cam_z, &cam_yaw, &cam_pitch, g.keys, m_dx, m_dy, dt);
+ }
+
+ update_weather(&g);
+ mat_projection(&g.proj, 60.0f, (float)WIDTH / HEIGHT, 0.1f, g.cur_weather.far_clp);
+ memset(g.zbuffer, 0, WIDTH * HEIGHT * sizeof(float));
+
+ mat_view(&view, cam_x, cam_y, cam_z, cam_yaw, cam_pitch);
+ draw_scene(&g, &g.proj, &view, cam_x, cam_y, cam_z);
+ draw_water(&g.proj, &view);
+ draw_infinite_ocean(&g.proj, &view, cam_x, cam_z, g.game_time_sec);
+ gfx_present(&g);
+ }
+ munmap(g_tar_mapped, g_tar_size);
+ close(tar_fd);
+ gfx_cleanup(&g);
+
+ /* prevent memory leaks */
+ for (i = 0; i < cache->mesh_count; i++) {
+ free(cache->meshes[i].ply_verts);
+ free(cache->meshes[i].indices);
+ free(cache->meshes[i].mat_ids);
+ if (cache->meshes[i].num_materials > 0) {
+ free(cache->meshes[i].raw_tex_names);
+ free(cache->meshes[i].textures);
+ }
+ }
+ for (i = 0; i < cache->tex_count; i++) {
+ free(cache->textures[i].pixels);
+ }
+ free(cache);
+
+ for (gz = 0; gz < GRID_SZ; gz++) {
+ for (gx = 0; gx < GRID_SZ; gx++) {
+ free(scene_grid[gx][gz].ids);
+ }
+ }
+
+ free(g_transforms.pos_x);
+ free(g_transforms.pos_y);
+ free(g_transforms.pos_z);
+ free(g_transforms.rot_qx);
+ free(g_transforms.rot_qy);
+ free(g_transforms.rot_qz);
+ free(g_transforms.rot_qw);
+ free(g_transforms.world_matrices);
+ free(g_transforms.sphere_cx);
+ free(g_transforms.sphere_cy);
+ free(g_transforms.sphere_cz);
+ free(g_transforms.sphere_r);
+ free(g_transforms.time_on);
+ free(g_transforms.time_off);
+
+ free(scene_meshes);
+ free(water_faces);
+ free(draw_list);
+
+ return (0);
+}