// 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 "array.h" #include "content.h" #include "net.h" #include "str.h" #include "ser.h" #include "sig.h" #include "chunk.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; int8_t z; // caves: -1 chunk chunk; } map; typedef struct { bool auth; str name; entity_id id; vec2 pos; int8_t z; peer conn; } player; typedef struct { int accept_fd; str motd; str passphrase; array(map) maps; arraybuf(player) players; entity_id next_entity; } game; node *map_node(game *g, vec2 p, 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 = chunk_index_abs(m->chunk, p); if (n == NULL || !n->present) continue; return n; } return NULL; } bool silly_noise(vec2 pos, uint32_t grid, double sillyness, int seed) { int64_t x = (int64_t) pos.x+INT_MAX+1; int64_t y = (int64_t) pos.y+INT_MAX+1; if ((y/grid) % 2 == 0) x += grid/2; x += round(noise2d(x/grid, y/grid, 0, seed) * sillyness); y += round(noise2d(y/grid, x/grid, 0, seed) * sillyness); return x % grid == 0 && y % grid == 0; #undef SMOD } void map_load_node(map *m, uvec2 v, color col) { uint32_t ucol = color_to_u32(col); vec2 pos = vec2_add(m->chunk.bounds.pos, CVEC2(v)); node *n = chunk_index(m->chunk, v); *n = (node) { 0 }; n->present = ucol != 0xffffff; if (!n->present) return; switch (ucol) { case 0x00880d: n->type = N_GRASS; n->variant = silly_noise(pos, 3, 1.0, 0); break; 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; n->variant = FLOWER_DANDELION; n->z = 3; break; case 0x001b51: n->type = N_MOUNTAIN_FLOWER; n->variant = FLOWER_DANDELION; n->z = 2; break; case 0xa09700: n->type = N_PATH; break; case 0x015100: n->type = N_NEEDLE_TREE; break; case 0x00c6ff: n->type = N_WATER; n->variant = silly_noise(pos, 3, 0.7, 1); n->z = -1; break; case 0x5c3b12: n->type = N_PLANK; break; case 0xff9de2: n->type = N_VALLEY_FLOWER; n->variant = rand() % FLOWER_COUNT; break; case 0x016300: n->type = N_BIG_TREE; break; case 0xfce84e: n->type = N_SAND; n->variant = silly_noise(pos, 4, 1.0, 1); break; default: fprintf(stderr, "invalid color in map %.*s at %"PRIu32" %"PRIu32": %06x\n", PSTR(m->name), v.x, v.y, ucol); 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->chunk.bounds.size.x, "%s: width mismatch\n", m->filename) TRY(height == m->chunk.bounds.size.y, "%s: height mismatch\n", m->filename) TRY(color_type == PNG_COLOR_TYPE_RGB, "%s: color type is not RGB\n", m->filename) // 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]; /* 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, UVEC2(x, y), (color) { p[0], p[1], p[2] }); } } fclose(file); png_destroy_read_struct(&png, &info, NULL); return true; #undef TRY } void send_players(game *g) { for (size_t i = 0; i < g->players.len; i++) { player *to = &g->players.data[i]; if (to->auth) SEND_PKT(to->conn, CPKT_PLAYERS, 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_entity_id(&pkt, p->id); } ) } } // send_nudes when void send_nodes(player *p, game *g, box2 bounds, int8_t z) { SEND_PKT(p->conn, CPKT_NODES, ser_box2(&pkt, bounds); for (uint32_t x = 0; x < bounds.size.x; x++) for (uint32_t y = 0; y < bounds.size.y; y++) ser_node(&pkt, map_node(g, vec2_add(bounds.pos, VEC2(x, y)), 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].chunk.data); } 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_eq(g->passphrase, pass)) { printf("wrong passphrase from %.*s\n", PSTR(name)); // TODO: log ip ? SEND_PKT(p->conn, CPKT_FAIL, 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_eq(p2->name, name)) { SEND_PKT(p->conn, CPKT_FAIL, ser_fail_reason(&pkt, FAIL_ALREADY_ONLINE);) return true; } } printf("player authenticated as %.*s\n", PSTR(name)); p->auth = true; p->name = str_clone(name); p->pos.x = p->pos.y = 0; p->z = 0; SEND_PKT(p->conn, CPKT_HI, ser_str(&pkt, g->motd);) send_players(g); send_nodes(p, g, box2_around(p->pos, SIGHT_RANGE), p->z); return true; } bool handle_move(str pkt, player *p, [[maybe_unused]] game *g) { dir d; if (!deser_dir(&pkt, &d)) return false; vec2 v = dir_to_vec2(d); if (vec2_iszero(v)) return false; // yolo p->pos = vec2_add(v, p->pos); SEND_PKT(p->conn, CPKT_ENTITY, ser_u16(&pkt, 1); ser_entity_id(&pkt, p->id); ser_entity_cmd(&pkt, ENTITY_MOVE); ser_vec2(&pkt, p->pos); ) // TODO: only send newly visible nodes send_nodes(p, g, box2_around(p->pos, SIGHT_RANGE), p->z); return true; } bool handle_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); case SPKT_MOVE: return handle_move(pkt, p, g); default: return false; } } int main() { signal_setup(); game g = {0}; g.accept_fd = -1; g.next_entity = 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, .z = 0, .chunk = chunk_alloc((box2) { { -50, -50 }, { 100, 100 } }), }; if (!map_load(&g.maps.data[0])) game_exit(&g, EXIT_FAILURE); if ((g.accept_fd = socket_create("0.0.0.0", "4560", true)) < 0) game_exit(&g, EXIT_FAILURE); for (;;) { struct pollfd fds[g.players.len + 1]; for (size_t i = 0; i < g.players.len; i++) fds[i] = peer_prepare(&g.players.data[i].conn); fds[g.players.len].fd = g.accept_fd; fds[g.players.len].events = POLLIN; if (poll(fds, g.players.len + 1, -1) < 0) { switch (errno) { case EINTR: break; default: perror("poll"); continue; } } if (signal_stop) game_exit(&g, EXIT_SUCCESS); for (size_t i = 0; i < g.players.len; i++) { player *p = &g.players.data[i]; str pkt = peer_recv(&p->conn, fds[i]); if (p->conn.disco) { printf("player %.*s disconnected\n", PSTR(p->name)); player_remove(p, &g); goto cont; } if (pkt.len > 0 && !handle_pkt(pkt, p, &g)) invalid_pkt(&p->conn, pkt); } if (fds[g.players.len].revents) { int socket = socket_accept(g.accept_fd); if (socket < 0) continue; printf("new player\n"); player *p = ARR_APPEND(g.players); p->auth = false; p->id = g.next_entity++; p->name = S("(unauthenticated)"); peer_init(&p->conn, socket, &p->name); } cont: continue; } }