// SPDX-FileCopyrightText: 2024 Lizzy Fleckenstein // // SPDX-License-Identifier: AGPL-3.0-or-later #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "array.h" #include "content.h" #include "peer.h" #include "str.h" #include "ser.h" typedef enum { MAP_VALLEY, MAP_ISLAND, MAP_MOUNTAIN, MAP_CAVE, MAP_NUM, } map_type; typedef struct { const char *filename; str name; map_type type; uint32_t width; uint32_t height; int32_t off_x; int32_t off_y; int8_t z; // caves: -1 node *nodes; } map; typedef struct { bool auth; str name; uint64_t id; int32_t x, y; int8_t z; peer conn; } player; typedef struct { str motd; str passphrase; array(map) maps; arraybuf(player) players; uint64_t entity_id; } game; node *map_local_node(map *m, int32_t x, int32_t y) { if (x < 0 || y < 0 || (uint32_t) x >= m->width || (uint32_t) y >= m->height) return NULL; return &m->nodes[x*m->height+y]; } node *map_node(game *g, int32_t x, int32_t y, int8_t z) { for (size_t i = 0; i < g->maps.len; i++) { map *m = &g->maps.data[i]; if (m->z != z) continue; node *n = map_local_node(m, x - m->off_x, y - m->off_y); if (n == NULL || !n->present) continue; return n; } return NULL; } void map_load_node(map *m, uint32_t x, uint32_t y, uint32_t color) { node *n = map_local_node(m, x, y); n->present = color != 0xffffff; if (!n->present) return; n->z = 0; n->col = u32_color(color); switch (color) { case 0x00880d: n->type = N_GRASS; break; // TODO: color case 0x595959: n->type = N_ROCK; n->z = 1; break; case 0x484848: n->type = N_ROCK; n->z = 2; break; case 0x373737: n->type = N_ROCK; n->z = 3; break; case 0x00ffe8: n->type = N_MOUNTAIN_FLOWER; break; case 0xa09700: n->type = N_PATH; break; case 0x015100: n->type = N_NEEDLE_TREE; break; case 0x00c6ff: n->type = N_WATER; n->z = -1; break; case 0x5c3b12: n->type = N_PLANK; break; case 0xff9de2: n->type = N_VALLEY_FLOWER; break; // TODO: color case 0x016300: n->type = N_BIG_TREE; break; default: fprintf(stderr, "invalid color in map %*s at %"PRIu32" %"PRIu32": %06x\n", PSTR(m->name), x, y, color); n->present = false; break; } } bool map_load(map *m) { #define TRY(expr, ...) if (!(expr)) { \ fprintf(stderr, __VA_ARGS__); \ if (file != NULL) fclose(file); \ png_destroy_read_struct(&png, &info, NULL); \ return false; } FILE *file = NULL; png_structp png = NULL; png_infop info = NULL; TRY((file = fopen(m->filename, "r")) != NULL, "failed to open %s\n", m->filename) TRY(png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL), "png_create_read_struct failed\n") TRY(info = png_create_info_struct(png), "png_create_info_struct failed\n") png_init_io(png, file); png_read_info(png, info); png_uint_32 width = png_get_image_width(png, info); png_uint_32 height = png_get_image_height(png, info); png_byte color_type = png_get_color_type(png, info); TRY(width == m->width, "%s: width mismatch\n", m->filename) TRY(height == m->height, "%s: height mismatch\n", m->filename) TRY(color_type == PNG_COLOR_TYPE_RGB, "%s: color type is not RGB\n", m->filename) m->nodes = malloc(m->width * m->height * sizeof *m->nodes); // uint32_t colors[100] = {0}; png_uint_32 pitch = png_get_rowbytes(png, info); png_byte row[pitch]; for (png_uint_32 y = 0; y < height; y++) { png_read_row(png, row, NULL); for (png_uint_32 x = 0; x < width; x++) { png_bytep p = &row[x*3]; uint32_t col = color_u32((color) { p[0], p[1], p[2] }); /* for (size_t i = 0; i < 100; i++) { if (colors[i] == color) { break; } else if (colors[i] == 0) { colors[i] = color; printf("#%06x\n", color); break; } }*/ map_load_node(m, x, y, col); } } fclose(file); png_destroy_read_struct(&png, &info, NULL); return true; #undef TRY } int net_listen(const char *host, const char *port) { struct addrinfo *info = NULL, hints = {0}; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; int err; if ((err = getaddrinfo(host, port, &hints, &info))) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); return -1; } #define TRY(op, val) if ((val) < 0) { perror(op); freeaddrinfo(info); return -1; } int fd; TRY("socket", fd = socket(info->ai_family, info->ai_socktype, info->ai_protocol)) int flag = 1; TRY("setsockopt", setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void *) &flag, sizeof flag)) TRY("bind", bind(fd, info->ai_addr, info->ai_addrlen)) TRY("listen", listen(fd, 3)) #undef TRY freeaddrinfo(info); return fd; } void ser_color(strbuf *w, color c) { ser_u8(w, c.r); ser_u8(w, c.g); ser_u8(w, c.b); } void ser_node(strbuf *w, node *n) { bool present = n != NULL && n->present; ser_bool(w, present); if (!present) return; ser_i8(w, n->z); ser_color(w, n->col); } bool player_auth(player *p) { return p->auth; } #define SEND_PKT(TYPE, PEERS, NPEERS, FILTER, ...) \ { strbuf pkt = NILSBUF; \ ser_pkt_type(&pkt, TYPE); \ __VA_ARGS__ \ for (size_t i = 0; i < NPEERS; i++) { \ if (FILTER(&(PEERS)[i]) && !peer_send(&(PEERS)[i].conn, pkt.data, pkt.len)) \ fprintf(stderr, "failed to send " #TYPE " to %*s\n", PSTR((PEERS)[i].name));} \ free(pkt.data); } void send_players(game *g) { SEND_PKT(CPKT_PLAYERS, g->players.data, g->players.len, player_auth, ser_u16(&pkt, g->players.len); for (size_t i = 0; i < g->players.len; i++) { player *p = &g->players.data[i]; if (!p->auth) continue; ser_str(&pkt, p->name); ser_u64(&pkt, p->id); } ) } // send_nudes when void send_nodes(player *p, game *g, int8_t z, int32_t x, int32_t y, uint32_t w, uint32_t h) { SEND_PKT(CPKT_NODES, p, 1,, ser_i8(&pkt, z); ser_i32(&pkt, x); ser_i32(&pkt, y); ser_u32(&pkt, w); ser_u32(&pkt, h); for (uint32_t xi = 0; xi < w; xi++) for (uint32_t yi = 0; yi < w; yi++) ser_node(&pkt, map_node(g, x+xi, y+yi, z)); ) } void player_free(player *p) { peer_free(&p->conn); if (p->auth) free(p->name.data); } void player_remove(player *p, game *g) { player_free(p); bool auth = p->auth; ARR_REMOVE(g->players, p); if (auth) send_players(g); } void game_exit(game *g, int ret) { free(g->motd.data); free(g->passphrase.data); for (size_t i = 0; i < g->maps.len; i++) { free(g->maps.data[i].name.data); free(g->maps.data[i].nodes); } free(g->maps.data); for (size_t i = 0; i < g->players.len; i++) player_free(&g->players.data[i]); free(g->players.data); exit(ret); } bool handle_hi(str pkt, player *p, game *g) { str name, pass; if (! (deser_str(&pkt, &name) && deser_str(&pkt, &pass))) return false; if (str_cmp(g->passphrase, pass) != 0) { fprintf(stderr, "wrong passphrase from %*s\n", PSTR(name)); // TODO: log ip ? SEND_PKT(CPKT_FAIL, p, 1,, ser_fail_reason(&pkt, FAIL_WRONG_PASS);) return true; // valid pkt, but invalid passphrase } for (size_t i = 0; i < g->players.len; i++) { player *p2 = &g->players.data[i]; if (p2->auth && str_cmp(p2->name, name) == 0) { SEND_PKT(CPKT_FAIL, p, 1,, ser_fail_reason(&pkt, FAIL_ALREADY_ONLINE);) return true; } } p->auth = true; p->name = str_clone(name); p->x = p->y = 0; p->z = 0; SEND_PKT(CPKT_HI, p, 1,, ser_str(&pkt, g->motd);) send_players(g); uint32_t range = 10; send_nodes(p, g, p->z, p->x-range, p->y-range, range*2, range*2); return true; } bool process_pkt(str pkt, player *p, game *g) { pkt_type type; if (!deser_pkt_type(&pkt, &type)) return false; if ((type == SPKT_HI) == p->auth) return false; switch (type) { case SPKT_HI: return handle_hi(pkt, p, g); default: return false; } } int main() { game g = {0}; g.entity_id = 1; g.motd = str_clone(S("Welcome to test server")); g.passphrase = str_clone(S("")); g.maps.len = 1; g.maps.data = malloc(g.maps.len * sizeof *g.maps.data); g.maps.data[0] = (map) { .filename = "assets/map_valley.png", .name = str_clone(S("Valley")), .type = MAP_VALLEY, .off_x = -50, .off_y = -50, .z = 0, .width = 100, .height = 100, .nodes = NULL, }; if (!map_load(&g.maps.data[0])) game_exit(&g, EXIT_FAILURE); int accept_fd = net_listen("0.0.0.0", "4560"); if (accept_fd < 0) game_exit(&g, EXIT_FAILURE); for (;;) { struct pollfd fds[g.players.len + 1]; for (size_t i = 0; i < g.players.len;) { player *p = &g.players.data[i]; if (p->conn.disco) { printf("player disco\n"); player_remove(p, &g); continue; } fds[i++] = peer_prepare(&p->conn); } fds[g.players.len].fd = accept_fd; fds[g.players.len].events = POLLIN; if (poll(fds, g.players.len + 1, -1) < 0) { switch (errno) { case EINTR: continue; default: perror("poll"); continue; } } for (size_t i = 0; i < g.players.len; i++) { player *p = &g.players.data[i]; if (!peer_ready(&p->conn, fds[i])) continue; if (p->conn.disco) continue; str pkt = { p->conn.in.len, (char *) p->conn.in.buffer }; if (!process_pkt(pkt, p, &g)) { // TODO: maybe inform client about failure? not sure // FIXME: hexdumping a gazillon bytes to stderr might be an issue fprintf(stderr, "invalid pkt from %*s: ", PSTR(p->name)); for (size_t i = 0; i < pkt.len; i++) fprintf(stderr, "%02x%c", (uint8_t) pkt.data[i], i+1 == pkt.len ? '\n' : ' '); } } if (fds[g.players.len].revents) { int socket = accept(accept_fd, NULL, NULL); // TODO: save ip if (socket < 0) { perror("accept"); continue; } if (fcntl(socket, F_SETFL, fcntl(socket, F_GETFL, 0) | O_NONBLOCK) < 0) { close(socket); perror("fcntl"); continue; } printf("new player\n"); player *p = ARR_APPEND(g.players); p->auth = false; p->id = g.entity_id++; p->name = S("unauthenticated"); peer_init(&p->conn, socket); } } }