summary refs log tree commit diff homepage
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.gitignore5
-rw-r--r--LICENSE13
-rwxr-xr-xclient.c325
-rwxr-xr-xhelp.c187
-rwxr-xr-xserver.c272
-rw-r--r--torus.h78
6 files changed, 880 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..39fd7f6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+client
+help
+server
+torus.dat
+torus.sock
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..558ab00
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright © 2017, June McEnroe <june@causal.agency>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/client.c b/client.c
new file mode 100755
index 0000000..775d296
--- /dev/null
+++ b/client.c
@@ -0,0 +1,325 @@
+#if 0
+exec cc -Wall -Wextra -pedantic $@ -lcurses -o client $0
+#endif
+
+#include <ctype.h>
+#include <curses.h>
+#include <err.h>
+#include <errno.h>
+#include <poll.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+#undef COLOR_BLACK
+#undef COLOR_RED
+#undef COLOR_GREEN
+#undef COLOR_YELLOW
+#undef COLOR_BLUE
+#undef COLOR_MAGENTA
+#undef COLOR_CYAN
+#undef COLOR_WHITE
+#include "torus.h"
+
+#define ESC (0x1B)
+#define DEL (0x7F)
+
+#define CH_COLOR(ch) (ch & A_BOLD ? COLOR_BRIGHT | ch >> 8 & 0xFF : ch >> 8 & 0xFF)
+
+static int client;
+
+static void clientMessage(const struct ClientMessage *msg) {
+    ssize_t len = send(client, msg, sizeof(*msg), 0);
+    if (len < 0) err(EX_IOERR, "send");
+}
+
+static void clientMove(int8_t dx, int8_t dy) {
+    struct ClientMessage msg = { .type = CLIENT_MOVE };
+    msg.data.m.dx = dx;
+    msg.data.m.dy = dy;
+    clientMessage(&msg);
+}
+
+static void clientColor(uint8_t color) {
+    struct ClientMessage msg = { .type = CLIENT_COLOR };
+    msg.data.c = color;
+    clientMessage(&msg);
+}
+
+static void clientPut(char cell) {
+    struct ClientMessage msg = { .type = CLIENT_PUT };
+    msg.data.p = cell;
+    clientMessage(&msg);
+}
+
+static enum {
+    MODE_NORMAL,
+    MODE_INSERT,
+    MODE_REPLACE,
+    MODE_DRAW,
+} mode;
+static struct {
+    int8_t dx;
+    int8_t dy;
+    uint8_t len;
+} insert;
+static char drawChar;
+
+static void insertMode(int8_t dx, int8_t dy) {
+    mode = MODE_INSERT;
+    insert.dx = dx;
+    insert.dy = dy;
+    insert.len = 0;
+}
+
+static void swapCell(int8_t dx, int8_t dy) {
+    int sy, sx;
+    getyx(stdscr, sy, sx);
+
+    move(sy + dy, sx + dx);
+    char swapCell = inch() & 0x7F;
+    uint8_t swapColor = CH_COLOR(inch());
+
+    move(sy, sx);
+    char cell = inch() & 0x7F;
+    uint8_t color = CH_COLOR(inch());
+
+    clientColor(swapColor);
+    clientPut(swapCell);
+    clientMove(dx, dy);
+    clientColor(color);
+    clientPut(cell);
+}
+
+static void readInput(void) {
+    int c = getch();
+
+    if (mode == MODE_INSERT) {
+        if (c == ESC) {
+            mode = MODE_NORMAL;
+            clientMove(-insert.dx, -insert.dy);
+        } else if (!insert.dx && !insert.dy) {
+            switch (c) {
+                case 'h': insertMode(-1,  0); break;
+                case 'j': insertMode( 0,  1); break;
+                case 'k': insertMode( 0, -1); break;
+                case 'l': insertMode( 1,  0); break;
+                case 'y': insertMode(-1, -1); break;
+                case 'u': insertMode( 1, -1); break;
+                case 'b': insertMode(-1,  1); break;
+                case 'n': insertMode( 1,  1); break;
+            }
+        } else if (c == '\b' || c == DEL) {
+            clientMove(-insert.dx, -insert.dy);
+            clientPut(' ');
+            insert.len--;
+        } else if (c == '\n') {
+            clientMove(insert.dy, insert.dx);
+            for (uint8_t i = 0; i < insert.len; ++i) {
+                clientMove(-insert.dx, -insert.dy);
+            }
+            insert.len = 0;
+        } else if (isprint(c)) {
+            clientPut(c);
+            clientMove(insert.dx, insert.dy);
+            insert.len++;
+        }
+        return;
+    }
+
+    if (mode == MODE_REPLACE) {
+        if (isprint(c)) clientPut(c);
+        mode = MODE_NORMAL;
+        return;
+    }
+
+    if (mode == MODE_DRAW && !drawChar) {
+        if (c == ESC) mode = MODE_NORMAL;
+        if (isprint(c)) {
+            drawChar = c;
+            clientPut(c);
+        }
+        return;
+    }
+
+    switch (c) {
+        case ESC: mode = MODE_NORMAL; break;
+
+        case 'q': endwin(); exit(EX_OK);
+
+        case 'a': clientMove(1, 0); // fallthrough
+        case 'i': insertMode(1, 0); break;
+        case 'I': insertMode(0, 0); break;
+        case 'r': mode = MODE_REPLACE; break;
+        case 'R': mode = MODE_DRAW; drawChar = 0; break;
+        case 'x': clientPut(' '); break;
+        case '~': clientPut(inch() & 0x7F); clientMove(1, 0); break;
+
+        case 'h': clientMove(-1,  0); break;
+        case 'j': clientMove( 0,  1); break;
+        case 'k': clientMove( 0, -1); break;
+        case 'l': clientMove( 1,  0); break;
+        case 'y': clientMove(-1, -1); break;
+        case 'u': clientMove( 1, -1); break;
+        case 'b': clientMove(-1,  1); break;
+        case 'n': clientMove( 1,  1); break;
+
+        case 'H': swapCell(-1,  0); break;
+        case 'J': swapCell( 0,  1); break;
+        case 'K': swapCell( 0, -1); break;
+        case 'L': swapCell( 1,  0); break;
+        case 'Y': swapCell(-1, -1); break;
+        case 'U': swapCell( 1, -1); break;
+        case 'B': swapCell(-1,  1); break;
+        case 'N': swapCell( 1,  1); break;
+
+        case '1': clientColor(COLOR_RED); break;
+        case '2': clientColor(COLOR_GREEN); break;
+        case '3': clientColor(COLOR_YELLOW); break;
+        case '4': clientColor(COLOR_BLUE); break;
+        case '5': clientColor(COLOR_MAGENTA); break;
+        case '6': clientColor(COLOR_CYAN); break;
+        case '7': clientColor(COLOR_WHITE); break;
+
+        case '!': clientColor(COLOR_BRIGHT | COLOR_RED); break;
+        case '@': clientColor(COLOR_BRIGHT | COLOR_GREEN); break;
+        case '#': clientColor(COLOR_BRIGHT | COLOR_YELLOW); break;
+        case '$': clientColor(COLOR_BRIGHT | COLOR_BLUE); break;
+        case '%': clientColor(COLOR_BRIGHT | COLOR_MAGENTA); break;
+        case '^': clientColor(COLOR_BRIGHT | COLOR_CYAN); break;
+        case '&': clientColor(COLOR_BRIGHT | COLOR_WHITE); break;
+
+        case KEY_LEFT: clientMove(-1,  0); break;
+        case KEY_DOWN: clientMove( 0,  1); break;
+        case KEY_UP: clientMove( 0, -1); break;
+        case KEY_RIGHT: clientMove( 1,  0); break;
+    }
+
+    if (mode == MODE_DRAW && drawChar) clientPut(drawChar);
+}
+
+static void serverPut(uint8_t x, uint8_t y, uint8_t color, char cell) {
+    int attrs = COLOR_PAIR(color & ~COLOR_BRIGHT);
+    if (color & COLOR_BRIGHT) attrs |= A_BOLD;
+    mvaddch(y, x, attrs | cell);
+}
+
+static void serverTile(void) {
+    struct Tile tile;
+    ssize_t len = recv(client, &tile, sizeof(tile), 0);
+    if (len < 0) err(EX_IOERR, "recv");
+    if (len < (ssize_t)sizeof(tile)) {
+        errx(EX_PROTOCOL, "This tile isn't big enough...");
+    }
+
+    for (int y = 0; y < CELL_ROWS; ++y) {
+        for (int x = 0; x < CELL_COLS; ++x) {
+            serverPut(x, y, tile.colors[y][x], tile.cells[y][x]);
+        }
+    }
+}
+
+static void readMessage(void) {
+    struct ServerMessage msg;
+    ssize_t len = recv(client, &msg, sizeof(msg), 0);
+    if (len < 0) err(EX_IOERR, "recv");
+    if (len < (ssize_t)sizeof(msg)) errx(EX_PROTOCOL, "A message was cut short.");
+
+    int sy, sx;
+    getyx(stdscr, sy, sx);
+
+    switch (msg.type) {
+        case SERVER_TILE:
+            serverTile();
+            break;
+
+        case SERVER_MOVE:
+            move(msg.data.m.cellY, msg.data.m.cellX);
+            refresh();
+            return;
+
+        case SERVER_PUT:
+            serverPut(
+                msg.data.p.cellX,
+                msg.data.p.cellY,
+                msg.data.p.color,
+                msg.data.p.cell
+            );
+            break;
+
+        default:
+            errx(EX_PROTOCOL, "I don't know what %d means!", msg.type);
+    }
+
+    move(sy, sx);
+    refresh();
+}
+
+static void drawBorder(void) {
+    if (LINES < CELL_ROWS || COLS < CELL_COLS) {
+        endwin();
+        fprintf(stderr, "Sorry, your terminal is too small!\n");
+        fprintf(stderr, "It needs to be at least 80x25 characters.\n");
+        exit(EX_CONFIG);
+    }
+    if (LINES > CELL_ROWS) {
+        mvhline(CELL_ROWS, 0, 0, CELL_COLS);
+    }
+    if (COLS > CELL_COLS) {
+        mvvline(0, CELL_COLS, 0, CELL_ROWS);
+    }
+    if (LINES > CELL_ROWS && COLS > CELL_COLS) {
+        mvaddch(CELL_ROWS, CELL_COLS, ACS_LRCORNER);
+    }
+}
+
+static void initColors(void) {
+    if (!has_colors()) {
+        endwin();
+        fprintf(stderr, "Sorry, your terminal doesn't support colors!\n");
+        fprintf(stderr, "I only need 16, I promise.\n");
+        exit(EX_CONFIG);
+    }
+    start_color();
+    for (int fg = COLOR_RED; fg < COLOR_BRIGHT; ++fg) {
+        init_pair(fg, fg, COLOR_BLACK);
+    }
+}
+
+int main() {
+    client = socket(PF_LOCAL, SOCK_STREAM, 0);
+    if (client < 0) err(EX_OSERR, "socket");
+
+    struct sockaddr_un addr = {
+        .sun_family = AF_LOCAL,
+        .sun_path = "torus.sock",
+    };
+    int error = connect(client, (struct sockaddr *)&addr, sizeof(addr));
+    if (error) err(EX_IOERR, "torus.sock");
+
+    initscr();
+    cbreak();
+    noecho();
+    keypad(stdscr, true);
+    set_escdelay(100);
+
+    initColors();
+    drawBorder();
+
+    struct pollfd fds[2] = {
+        { .fd = STDIN_FILENO, .events = POLLIN },
+        { .fd = client, .events = POLLIN },
+    };
+    for (;;) {
+        int nfds = poll(fds, 2, -1);
+        if (nfds < 0 && errno == EINTR) continue;
+        if (nfds < 0) err(EX_IOERR, "poll");
+
+        if (fds[0].revents) readInput();
+        if (fds[1].revents) readMessage();
+    }
+}
diff --git a/help.c b/help.c
new file mode 100755
index 0000000..8360143
--- /dev/null
+++ b/help.c
@@ -0,0 +1,187 @@
+#if 0
+exec cc -Wall -Wextra -pedantic $@ -o help $0
+#endif
+
+#include <err.h>
+#include <stdint.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+#include "torus.h"
+
+static int client;
+
+static void clientMessage(const struct ClientMessage *msg) {
+    ssize_t len = send(client, msg, sizeof(*msg), 0);
+    if (len < 0) err(EX_IOERR, "send");
+}
+
+static void clientMove(int8_t dx, int8_t dy) {
+    struct ClientMessage msg = { .type = CLIENT_MOVE };
+    msg.data.m.dx = dx;
+    msg.data.m.dy = dy;
+    clientMessage(&msg);
+}
+
+static void clientColor(uint8_t color) {
+    struct ClientMessage msg = { .type = CLIENT_COLOR };
+    msg.data.c = color;
+    clientMessage(&msg);
+}
+
+static void clientPut(char cell) {
+    struct ClientMessage msg = { .type = CLIENT_PUT };
+    msg.data.p = cell;
+    clientMessage(&msg);
+}
+
+#define DELAY (100000)
+
+static void clear(uint8_t width, uint8_t height) {
+    uint8_t x = 0;
+    for (uint8_t y = 0; y < height; ++y) {
+        if (x) {
+            for (; x > 0; --x) {
+                clientPut(' ');
+                clientMove(-1, 0);
+                usleep(DELAY / 10);
+            }
+            clientPut(' ');
+            x = 0;
+        } else {
+            for (; x < width; ++x) {
+                clientPut(' ');
+                clientMove(1, 0);
+                usleep(DELAY / 10);
+            }
+            clientPut(' ');
+            x = width;
+        }
+        clientMove(0, 1);
+    }
+    clientMove(-x, -height);
+}
+
+static void white(void) {
+    clientColor(COLOR_WHITE);
+}
+
+static void brite(void) {
+    clientColor(COLOR_BRIGHT | COLOR_WHITE);
+}
+
+static int8_t lineLen;
+
+static void string(const char *str) {
+    for (; *str; ++str) {
+        clientPut(*str);
+        clientMove(1, 0);
+        lineLen++;
+        usleep(DELAY);
+    }
+}
+
+static void enter(void) {
+    clientMove(-lineLen, 1);
+    lineLen = 0;
+    usleep(DELAY);
+}
+
+static void mvPut(int8_t dx, int8_t dy, char cell) {
+    clientMove(dx, dy);
+    clientPut(cell);
+    usleep(DELAY);
+}
+
+int main() {
+    client = socket(PF_LOCAL, SOCK_STREAM, 0);
+    if (client < 0) err(EX_OSERR, "socket");
+
+    struct sockaddr_un addr = {
+        .sun_family = AF_LOCAL,
+        .sun_path = "torus.sock",
+    };
+    int error = connect(client, (struct sockaddr *)&addr, sizeof(addr));
+    if (error) err(EX_IOERR, "torus.sock");
+
+    pid_t pid = fork();
+    if (pid < 0) err(EX_OSERR, "fork");
+
+    if (!pid) {
+        for (;;) {
+            char buf[4096];
+            ssize_t len = recv(client, buf, sizeof(buf), 0);
+            if (len < 0) err(EX_IOERR, "recv");
+            if (!len) return EX_OK;
+        }
+    }
+
+    clientMove(-CELL_INIT_X, -CELL_INIT_Y);
+
+    for (;;) {
+        clear(28, 11);
+        clientMove(2, 1);
+
+        white(); string("Welcome to ");
+        brite(); string("ascii.town");
+        white(); string("!");
+        enter();
+
+        white(); mvPut( 2,  4, 'o');
+        white(); mvPut( 1,  0, '-');
+        brite(); mvPut( 1,  0, 'l');
+        white(); mvPut(-1,  1, '\\');
+        brite(); mvPut( 1,  1, 'n');
+        white(); mvPut(-2, -1, '|');
+        brite(); mvPut( 0,  1, 'j');
+        white(); mvPut(-1, -1, '/');
+        brite(); mvPut(-1,  1, 'b');
+        white(); mvPut( 1, -2, '-');
+        brite(); mvPut(-1,  0, 'h');
+        white(); mvPut( 1, -1, '\\');
+        brite(); mvPut(-1, -1, 'y');
+        white(); mvPut( 2,  1, '|');
+        brite(); mvPut( 0, -1, 'k');
+        white(); mvPut( 1,  1, '/');
+        brite(); mvPut( 1, -1, 'u');
+
+        clientMove(5, 0);
+        brite(); string("q");
+        white(); string(" quit");
+        enter();
+        brite(); string("i");
+        white(); string(" insert");
+        enter();
+        brite(); string("r");
+        white(); string(" replace");
+        enter();
+        brite(); string("R");
+        white(); string(" draw");
+        enter();
+        brite(); string("~");
+        white(); string(" color");
+        enter();
+
+        clientMove(13, -6);
+        clientColor(COLOR_RED);     mvPut(0, 0, '1');
+        clientColor(COLOR_GREEN);   mvPut(0, 1, '2');
+        clientColor(COLOR_YELLOW);  mvPut(0, 1, '3');
+        clientColor(COLOR_BLUE);    mvPut(0, 1, '4');
+        clientColor(COLOR_MAGENTA); mvPut(0, 1, '5');
+        clientColor(COLOR_CYAN);    mvPut(0, 1, '6');
+        clientColor(COLOR_WHITE);   mvPut(0, 1, '7');
+        clientColor(COLOR_BRIGHT | COLOR_WHITE);   mvPut(2,  0, '&');
+        clientColor(COLOR_BRIGHT | COLOR_CYAN);    mvPut(0, -1, '^');
+        clientColor(COLOR_BRIGHT | COLOR_MAGENTA); mvPut(0, -1, '%');
+        clientColor(COLOR_BRIGHT | COLOR_BLUE);    mvPut(0, -1, '$');
+        clientColor(COLOR_BRIGHT | COLOR_YELLOW);  mvPut(0, -1, '#');
+        clientColor(COLOR_BRIGHT | COLOR_GREEN);   mvPut(0, -1, '@');
+        clientColor(COLOR_BRIGHT | COLOR_RED);     mvPut(0, -1, '!');
+
+        clientMove(-26, -3);
+
+        sleep(30);
+    }
+}
diff --git a/server.c b/server.c
new file mode 100755
index 0000000..7b16f33
--- /dev/null
+++ b/server.c
@@ -0,0 +1,272 @@
+#if 0
+exec cc -Wall -Wextra -pedantic $@ -o server $0
+#endif
+
+#include <sys/types.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/event.h>
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/un.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+#include "torus.h"
+
+static struct Tile *tiles;
+
+static void tilesMap(void) {
+    int fd = open("torus.dat", O_CREAT | O_RDWR, 0644);
+    if (fd < 0) err(EX_IOERR, "torus.dat");
+
+    int error = ftruncate(fd, TILES_SIZE);
+    if (error) err(EX_IOERR, "ftruncate");
+
+    tiles = mmap(NULL, TILES_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+    if (tiles == MAP_FAILED) err(EX_OSERR, "mmap");
+}
+
+static struct Tile *tileGet(uint32_t tileX, uint32_t tileY) {
+    struct Tile *tile = &tiles[tileY * TILE_ROWS + tileX];
+    if (!tile->present) {
+        memset(tile->cells, ' ', CELLS_SIZE);
+        memset(tile->colors, COLOR_WHITE, CELLS_SIZE);
+        tile->present = true;
+    }
+    return tile;
+}
+
+static struct Client {
+    int fd;
+
+    uint32_t tileX;
+    uint32_t tileY;
+    uint8_t cellX;
+    uint8_t cellY;
+    uint8_t color;
+
+    struct Client *prev;
+    struct Client *next;
+} *clientHead;
+
+static struct Client *clientAdd(int fd) {
+    struct Client *client = malloc(sizeof(*client));
+    if (!client) err(EX_OSERR, "malloc");
+
+    client->fd = fd;
+    client->tileX = TILE_INIT_X;
+    client->tileY = TILE_INIT_Y;
+    client->cellX = CELL_INIT_X;
+    client->cellY = CELL_INIT_Y;
+    client->color = COLOR_WHITE;
+
+    client->prev = NULL;
+    if (clientHead) {
+        clientHead->prev = client;
+        client->next = clientHead;
+    } else {
+        client->next = NULL;
+    }
+    clientHead = client;
+
+    return client;
+}
+
+static void clientRemove(struct Client *client) {
+    if (client->prev) client->prev->next = client->next;
+    if (client->next) client->next->prev = client->prev;
+    if (clientHead == client) clientHead = client->next;
+    close(client->fd);
+    free(client);
+}
+
+static bool clientSend(struct Client *client, const struct ServerMessage *msg) {
+    ssize_t len = send(client->fd, msg, sizeof(*msg), 0);
+    if (len < 0) {
+        clientRemove(client);
+        return false;
+    }
+
+    if (msg->type == SERVER_TILE) {
+        struct Tile *tile = tileGet(client->tileX, client->tileY);
+        len = send(client->fd, tile, sizeof(*tile), 0);
+        if (len < 0) {
+            clientRemove(client);
+            return false;
+        }
+    }
+
+    return true;
+}
+
+static bool clientCast(struct Client *origin, const struct ServerMessage *msg) {
+    uint32_t tileX = origin->tileX;
+    uint32_t tileY = origin->tileY;
+
+    bool success = clientSend(origin, msg);
+
+    for (struct Client *client = clientHead; client; client = client->next) {
+        if (client == origin) continue;
+        if (client->tileX != tileX) continue;
+        if (client->tileY != tileY) continue;
+
+        struct Client *next = client->next;
+        if (!clientSend(client, msg)) {
+            client = next;
+            if (!client) break;
+        }
+    }
+
+    return success;
+}
+
+static bool clientMove(struct Client *client, int8_t dx, uint8_t dy) {
+    struct Client old = *client;
+
+    client->cellX += dx;
+    client->cellY += dy;
+
+    // TODO: Handle moves greater than 1 in either direction.
+    if (client->cellX == CELL_COLS) { client->tileX++; client->cellX = 0; }
+    if (client->cellX == UINT8_MAX) { client->tileX--; client->cellX = CELL_COLS - 1; }
+    if (client->cellY == CELL_ROWS) { client->tileY++; client->cellY = 0; }
+    if (client->cellY == UINT8_MAX) { client->tileY--; client->cellY = CELL_ROWS - 1; }
+
+    if (client->tileX == TILE_COLS)  client->tileX = 0;
+    if (client->tileX == UINT32_MAX) client->tileX = TILE_COLS - 1;
+    if (client->tileY == TILE_ROWS)  client->tileY = 0;
+    if (client->tileY == UINT32_MAX) client->tileY = TILE_ROWS - 1;
+
+    struct ServerMessage msg = { .type = SERVER_MOVE };
+    msg.data.m.cellX = client->cellX;
+    msg.data.m.cellY = client->cellY;
+    if (!clientSend(client, &msg)) return false;
+
+    if (client->tileX != old.tileX || client->tileY != old.tileY) {
+        msg.type = SERVER_TILE;
+        return clientSend(client, &msg);
+    }
+
+    return true;
+}
+
+static bool clientPut(struct Client *client, char cell) {
+    struct Tile *tile = tileGet(client->tileX, client->tileY);
+    tile->colors[client->cellY][client->cellX] = client->color;
+    tile->cells[client->cellY][client->cellX] = cell;
+
+    struct ServerMessage msg = { .type = SERVER_PUT };
+    msg.data.p.cellX = client->cellX;
+    msg.data.p.cellY = client->cellY;
+    msg.data.p.color = client->color;
+    msg.data.p.cell = cell;
+    return clientCast(client, &msg);
+}
+
+int main() {
+    int error;
+
+    signal(SIGPIPE, SIG_IGN);
+
+    tilesMap();
+
+    int server = socket(PF_LOCAL, SOCK_STREAM, 0);
+    if (server < 0) err(EX_OSERR, "socket");
+
+    error = unlink("torus.sock");
+    if (error && errno != ENOENT) err(EX_IOERR, "torus.sock");
+
+    struct sockaddr_un addr = {
+        .sun_family = AF_LOCAL,
+        .sun_path = "torus.sock",
+    };
+    error = bind(server, (struct sockaddr *)&addr, sizeof(addr));
+    if (error) err(EX_IOERR, "torus.sock");
+
+    error = listen(server, 0);
+    if (error) err(EX_OSERR, "listen");
+
+    int kq = kqueue();
+    if (kq < 0) err(EX_OSERR, "kqueue");
+
+    struct kevent event = {
+        .ident = server,
+        .filter = EVFILT_READ,
+        .flags = EV_ADD,
+        .fflags = 0,
+        .data = 0,
+        .udata = NULL,
+    };
+    int nevents = kevent(kq, &event, 1, NULL, 0, NULL);
+    if (nevents < 0) err(EX_OSERR, "kevent");
+
+    for (;;) {
+        nevents = kevent(kq, NULL, 0, &event, 1, NULL);
+        if (nevents < 0) err(EX_IOERR, "kevent");
+        if (!nevents) continue;
+
+        if (!event.udata) {
+            int fd = accept(server, NULL, NULL);
+            if (fd < 0) err(EX_IOERR, "accept");
+            fcntl(fd, F_SETFL, O_NONBLOCK);
+
+            struct Client *client = clientAdd(fd);
+
+            struct kevent event = {
+                .ident = fd,
+                .filter = EVFILT_READ,
+                .flags = EV_ADD,
+                .fflags = 0,
+                .data = 0,
+                .udata = client,
+            };
+            nevents = kevent(kq, &event, 1, NULL, 0, NULL);
+            if (nevents < 0) err(EX_OSERR, "kevent");
+
+            struct ServerMessage msg = { .type = SERVER_TILE };
+            if (!clientSend(client, &msg)) continue;
+            clientMove(client, 0, 0);
+
+            continue;
+        }
+
+        struct Client *client = event.udata;
+        if (event.flags & EV_EOF) {
+            clientRemove(client);
+            continue;
+        }
+
+        struct ClientMessage msg;
+        ssize_t len = recv(client->fd, &msg, sizeof(msg), 0);
+        if (len != sizeof(msg)) {
+            clientRemove(client);
+            continue;
+        }
+
+        switch (msg.type) {
+            case CLIENT_MOVE:
+                clientMove(client, msg.data.m.dx, msg.data.m.dy);
+                break;
+
+            case CLIENT_COLOR:
+                client->color = msg.data.c;
+                break;
+
+            case CLIENT_PUT:
+                clientPut(client, msg.data.p);
+                break;
+
+            default:
+                clientRemove(client);
+        }
+    }
+}
diff --git a/torus.h b/torus.h
new file mode 100644
index 0000000..23b4708
--- /dev/null
+++ b/torus.h
@@ -0,0 +1,78 @@
+#include <stdbool.h>
+#include <stdint.h>
+#include <assert.h>
+
+#define ALIGNED(x) __attribute__((aligned(x)))
+
+enum {
+    COLOR_BLACK,
+    COLOR_RED,
+    COLOR_GREEN,
+    COLOR_YELLOW,
+    COLOR_BLUE,
+    COLOR_MAGENTA,
+    COLOR_CYAN,
+    COLOR_WHITE,
+    COLOR_BRIGHT,
+};
+
+#define CELL_ROWS (25)
+#define CELL_COLS (80)
+#define CELLS_SIZE (sizeof(char[CELL_ROWS][CELL_COLS]))
+
+#define CELL_INIT_X (CELL_COLS / 2)
+#define CELL_INIT_Y (CELL_ROWS / 2)
+
+struct Tile {
+    bool present;
+    char cells[CELL_ROWS][CELL_COLS] ALIGNED(16);
+    uint8_t colors[CELL_ROWS][CELL_COLS] ALIGNED(16);
+} ALIGNED(4096);
+static_assert(sizeof(struct Tile) == 4096, "struct Tile is page-sized");
+
+#define TILE_ROWS (512)
+#define TILE_COLS (512)
+#define TILES_SIZE (sizeof(struct Tile[TILE_ROWS][TILE_COLS]))
+
+#define TILE_INIT_X (0)
+#define TILE_INIT_Y (0)
+
+enum ServerMessageType {
+    SERVER_TILE,
+    SERVER_MOVE,
+    SERVER_PUT,
+};
+
+struct ServerMessage {
+    enum ServerMessageType type;
+    union {
+        struct {
+            uint8_t cellX;
+            uint8_t cellY;
+        } m;
+        struct {
+            uint8_t cellX;
+            uint8_t cellY;
+            uint8_t color;
+            char cell;
+        } p;
+    } data;
+};
+
+enum ClientMessageType {
+    CLIENT_MOVE,
+    CLIENT_COLOR,
+    CLIENT_PUT,
+};
+
+struct ClientMessage {
+    enum ClientMessageType type;
+    union {
+        struct {
+            int8_t dx;
+            int8_t dy;
+        } m;
+        uint8_t c;
+        char p;
+    } data;
+};