// 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" #define MAYBE(T) \ typedef struct { bool present; T value; } maybe_##T; \ maybe_##T some_##T(T value) { return (maybe_##T) { true, value }; } \ maybe_##T none_##T = { 0 }; typedef enum { MAP_VALLEY, MAP_ISLAND, MAP_MOUNTAIN, MAP_CAVE, MAP_NUM, } map_type; typedef struct { entity base; int8_t z; } srv_entity; typedef struct { vec2 xy; int8_t z; } pos; MAYBE(pos) #define FPOS "(%"PRIi32" ,%"PRIi32", %"PRIi8")" #define PPOS(pos) (pos).xy.x, (pos).xy.y, (pos).z 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; peer conn; } player; typedef struct { int accept_fd; str motd; str passphrase; array(map) maps; arraybuf(player) players; arraybuf(srv_entity) entities; entity_id next_entity; } game; player *player_from_id(game *g, entity_id id) { ARR_ITER(g->players, p) if (p->id == id) return p; return NULL; } player *player_from_name(game *g, str name) { ARR_ITER(g->players, p) if (p->auth && str_eq(p->name, name)) return p; return NULL; } srv_entity *entity_from_id(game *g, entity_id id) { ARR_ITER(g->entities, e) if (e->base.id == id) return e; return NULL; } bool pos_eq(pos a, pos b) { return vec2_eq(a.xy, b.xy) && a.z == b.z; } pos entity_pos(srv_entity *e) { return (pos) { e->base.pos, e->z }; } box2 player_sight(vec2 center) { return box2_around(center, SIGHT_RANGE); } void ser_entity_update(strbuf *w, srv_entity *e, entity_cmd cmd) { ser_entity_id(w, e->base.id); ser_entity_cmd(w, cmd); switch (cmd) { case ENTITY_ADD: ser_entity_type(w, e->base.type); ser_vec2(w, e->base.pos); switch (e->base.type) { case ENTITY_FLOWER: ser_flower_type(w, e->base.flower); break; default: break; } break; case ENTITY_MOVE: ser_vec2(w, e->base.pos); break; case ENTITY_REMOVE: break; case ENTITY_CMD_COUNT: break; // unreachable } } static maybe_pos moved_get_pos(srv_entity *e, srv_entity *moved, maybe_pos p_moved) { return (e == moved) ? p_moved : some_pos(entity_pos(e)); } static bool moved_can_see(srv_entity *a, srv_entity *b, srv_entity *moved, maybe_pos p_moved) { maybe_pos p_a = moved_get_pos(a, moved, p_moved); maybe_pos p_b = moved_get_pos(b, moved, p_moved); return p_a.present && p_b.present && p_a.value.z == p_b.value.z && box2_contains(player_sight(p_a.value.xy), p_b.value.xy); } static entity_cmd moved_check_update(srv_entity *observer, srv_entity *target, srv_entity *moved, maybe_pos before, maybe_pos after) { bool see_before = moved_can_see(observer, target, moved, before); bool see_after = moved_can_see(observer, target, moved, after); if (see_before && !see_after) return ENTITY_REMOVE; else if (!see_before && see_after) return ENTITY_ADD; else if (see_before && see_after && target == moved) return ENTITY_MOVE; else return ENTITY_CMD_COUNT; } void moved_entity(game *g, srv_entity *moved, maybe_pos before, maybe_pos after) { ARR_ITER(g->players, p) if (p->auth) { srv_entity *observer = entity_from_id(g, p->id); if (observer == moved) { SEND_PKT(p->conn, CPKT_ENTITY, ARR_ITER(g->entities, target) { entity_cmd cmd = moved_check_update(observer, target, moved, before, after); if (cmd != ENTITY_CMD_COUNT) ser_entity_update(&pkt, target, cmd); } ) } else { entity_cmd cmd = moved_check_update(observer, moved, moved, before, after); if (cmd != ENTITY_CMD_COUNT) SEND_PKT(p->conn, CPKT_ENTITY, ser_entity_update(&pkt, moved, cmd);) } } } node *map_node(game *g, vec2 p, int8_t z) { ARR_ITER(g->maps, m) { 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; } srv_entity *spawn_entity(game *g, entity_type type, pos p) { srv_entity *e = ARR_APPEND(g->entities); e->base.id = g->next_entity++; e->base.type = type; e->base.pos = p.xy; e->z = p.z; return e; } void remove_entity(game *g, srv_entity *e) { moved_entity(g, e, some_pos(entity_pos(e)), none_pos); ARR_REMOVE(g->entities, e); } void map_load_node(game *g, map *m, uvec2 v, color col) { uint32_t ucol = color_to_u32(col); pos p = { vec2_add(m->chunk.bounds.pos, CVEC2(v)), m->z }; node *n = chunk_index(m->chunk, v); *n = (node) { 0 }; n->present = ucol != 0xffffff; if (!n->present) return; switch (ucol) { case 0xff9de2: spawn_entity(g, ENTITY_FLOWER, p)->base.flower = rand() % FLOWER_COUNT; [[fallthrough]]; case 0x00880d: n->type = N_GRASS; n->variant = silly_noise(p.xy, 3, 1.0, 0); break; case 0x595959: n->type = N_ROCK; n->z = 1; break; case 0x001b51: spawn_entity(g, ENTITY_FLOWER, p)->base.flower = FLOWER_DANDELION; [[fallthrough]]; case 0x484848: n->type = N_ROCK; n->z = 2; break; case 0x00ffe8: spawn_entity(g, ENTITY_FLOWER, p)->base.flower = FLOWER_DANDELION; [[fallthrough]]; case 0x373737: n->type = N_ROCK; n->z = 3; 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(p.xy, 3, 0.7, 1); n->z = -1; break; case 0x5c3b12: n->type = N_PLANK; break; case 0x016300: n->type = N_BIG_TREE; break; case 0xfce84e: n->type = N_SAND; n->variant = silly_noise(p.xy, 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(game *g, 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(g, 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) { ARR_ITER(g->players, to) if (to->auth) { SEND_PKT(to->conn, CPKT_PLAYERS, ser_u16(&pkt, g->players.len); ARR_ITER(g->players, p) if (p->auth) { 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_cleanup(player *p) { peer_free(&p->conn); if (p->auth) free(p->name.data); } void player_remove(player *p, game *g) { player_cleanup(p); bool auth = p->auth; entity_id id = p->id; ARR_REMOVE(g->players, p); if (auth) { remove_entity(g, entity_from_id(g, id)); send_players(g); } } void game_exit(game *g, int ret) { free(g->motd.data); free(g->passphrase.data); ARR_ITER(g->maps, m) { free(m->name.data); free(m->chunk.data); } free(g->maps.data); ARR_ITER(g->players, p) player_cleanup(p); 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 } if (player_from_name(g, 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); srv_entity *e = ARR_APPEND(g->entities); e->base.id = p->id; e->base.type = ENTITY_PLAYER; e->base.pos.x = e->base.pos.y = 0; e->z = 0; SEND_PKT(p->conn, CPKT_HI, ser_str(&pkt, g->motd);) send_players(g); pos ent_p = entity_pos(e); moved_entity(g, e, none_pos, some_pos(ent_p)); send_nodes(p, g, player_sight(ent_p.xy), ent_p.z); return true; } static str flower_name(flower_type t) { switch (t) { case FLOWER_ROSE: return S("rose"); case FLOWER_HIBISCUS: return S("hibiscus"); case FLOWER_SUNFLOWER: return S("sunflower"); case FLOWER_TULIP: return S("tulip"); case FLOWER_DANDELION: return S("dandelion"); case FLOWER_COUNT: break; } return S("???"); } bool handle_move(str pkt, player *p, game *g) { dir d; if (!deser_dir(&pkt, &d)) return false; vec2 v = dir_to_vec2(d); if (vec2_iszero(v)) return false; srv_entity *e = entity_from_id(g, p->id); // yolo pos old_pos = entity_pos(e); e->base.pos = vec2_add(v, e->base.pos); pos new_pos = entity_pos(e); moved_entity(g, e, some_pos(old_pos), some_pos(new_pos)); // TODO: only send newly visible nodes send_nodes(p, g, player_sight(new_pos.xy), new_pos.z); e = NULL; // will be invalidated! // epic flower collision ARR_ITER(g->entities, iter) { if (pos_eq(new_pos, entity_pos(iter)) && iter->base.type == ENTITY_FLOWER) { printf("player %.*s collected flower of type %.*s at "FPOS"\n", PSTR(p->name), PSTR(flower_name(iter->base.flower)), PPOS(new_pos)); // TODO: add item to inventory remove_entity(g, iter); iter--; } } 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, &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; } }