online.c (20649B)
1 /* online.c - menus de juego en red: servidor (VPS) y red local. */ 2 #include <ctype.h> 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <string.h> 6 #include "plat.h" 7 #if !defined(_WIN32) && !defined(__EMSCRIPTEN__) 8 #include <arpa/inet.h> 9 #include <ifaddrs.h> 10 #include <netinet/in.h> 11 #endif 12 #include "app.h" 13 #include "net.h" 14 #include "app_config.h" 15 #include "sha256.h" 16 #include "../server/server.h" 17 18 #define DEFAULT_PORT 7373 19 20 static bool parse_hostport(const char *in, char *host, int hn, int *port) 21 { 22 *port = DEFAULT_PORT; 23 while (*in == ' ') in++; 24 if (!*in) return false; 25 if (*in == '[') { /* [ipv6]:puerto */ 26 const char *e = strchr(in, ']'); 27 if (!e) return false; 28 snprintf(host, (size_t)hn, "%.*s", (int)(e - in - 1), in + 1); 29 if (e[1] == ':') *port = atoi(e + 2); 30 return true; 31 } 32 const char *c = strrchr(in, ':'); 33 if (c && strchr(in, ':') == c) { 34 snprintf(host, (size_t)hn, "%.*s", (int)(c - in), in); 35 *port = atoi(c + 1); 36 } else snprintf(host, (size_t)hn, "%s", in); 37 if (*port <= 0 || *port > 65535) *port = DEFAULT_PORT; 38 return host[0] != 0; 39 } 40 41 #if defined(__EMSCRIPTEN__) 42 /* la web no tiene red local: ver los stubs de lan_host/lan_join al final */ 43 #elif defined(_WIN32) 44 static void local_ip(char *out, int n) 45 { 46 snprintf(out, (size_t)n, "?"); 47 char host[256]; 48 struct addrinfo hints, *res, *ai; 49 sock_init(); 50 if (gethostname(host, sizeof host)) return; 51 memset(&hints, 0, sizeof hints); 52 hints.ai_family = AF_INET; 53 if (getaddrinfo(host, 0, &hints, &res)) return; 54 for (ai = res; ai; ai = ai->ai_next) { 55 struct sockaddr_in *a = (struct sockaddr_in *)ai->ai_addr; 56 if ((ntohl(a->sin_addr.s_addr) >> 24) == 127) continue; 57 inet_ntop(AF_INET, &a->sin_addr, out, (socklen_t)n); 58 break; 59 } 60 freeaddrinfo(res); 61 } 62 #else 63 static void local_ip(char *out, int n) 64 { 65 snprintf(out, (size_t)n, "?"); 66 struct ifaddrs *ifs, *i; 67 if (getifaddrs(&ifs)) return; 68 for (i = ifs; i; i = i->ifa_next) { 69 if (!i->ifa_addr || i->ifa_addr->sa_family != AF_INET) continue; 70 struct sockaddr_in *a = (struct sockaddr_in *)i->ifa_addr; 71 if ((ntohl(a->sin_addr.s_addr) >> 24) == 127) continue; 72 const char *nm = i->ifa_name; 73 if (!strncmp(nm, "virbr", 5) || !strncmp(nm, "docker", 6) || !strncmp(nm, "veth", 4) || 74 !strncmp(nm, "br-", 3) || !strncmp(nm, "lxc", 3) || !strncmp(nm, "tun", 3) || !strncmp(nm, "wg", 2)) continue; 75 inet_ntop(AF_INET, &a->sin_addr, out, (socklen_t)n); 76 break; 77 } 78 freeifaddrs(ifs); 79 } 80 #endif 81 82 static void notice(Frontend *fe, Form *f, const char *msg, bool err) 83 { 84 form_msg(f, msg, err); 85 fe->draw_form(fe, f); 86 } 87 88 /* ------------------------------------------------------------ jugar */ 89 90 static bool play_remote(Frontend *fe, Settings *st, Net *n, Form *f, const char *hint) 91 { 92 static Session s; 93 char err[200]; 94 notice(fe, f, g_lang ? "Loading game..." : "Cargando partida...", false); 95 if (!net_enter_game(n, &s, err, sizeof err)) { 96 if (strstr(err, "wrong password")) snprintf(err, sizeof err, "%s", g_lang ? "That name is taken in this game, or the password is wrong" 97 : "Ese nombre ya está en la partida o la contraseña no es esa"); 98 else if (strstr(err, "game is full")) snprintf(err, sizeof err, "%s", g_lang ? "The game is full" : "La partida está completa"); 99 else if (strstr(err, "no such game")) snprintf(err, sizeof err, "%s", g_lang ? "There is no game with that code" : "No hay ninguna partida con ese código"); 100 form_msg(f, err, true); 101 return false; 102 } 103 char line[160]; 104 snprintf(line, sizeof line, g_lang ? "— game code: %s (share it to invite) —" : "— código de partida: %s (pasalo para invitar) —", net_code(n)); 105 sess_add_log(&s, line); 106 if (hint) sess_add_log(&s, hint); 107 s.handoff = false; 108 int again = app_play(fe, &s, st); 109 s.net = 0; 110 if (again == 1) { /* otra como esta: el servidor la crea y nos sienta en el asiento 0 */ 111 net_send(n, "AGAIN"); 112 return play_remote(fe, st, n, f, hint); 113 } 114 net_send(n, "LEAVE"); 115 return true; 116 } 117 118 /* ------------------------------------------------------------ crear */ 119 120 enum { C_BACK = 0, C_CREATE = 1, C_PLAYERS = 10, C_VARIANT, C_BOARD, C_AWAY, C_SETUP, C_SCEN, C_MAP, C_LEVEL, C_SEAT = 20 }; 121 static const char *LV_ES[] = { "Fáciles", "Medios", "Difíciles" }, *LV_EN[] = { "Easy", "Medium", "Hard" }; 122 static const int LV_WIRE[3] = { 0, 2, 1 }; /* CREATE/OPTS: 0 facil, 1 dificil (como antes), 2 medio */ 123 124 static const char *CH_NUM[] = { "2", "3", "4", "5", "6" }; 125 static const char *YN_ES[] = { "No", "Sí" }, *YN_EN[] = { "No", "Yes" }; 126 static const char *SK_ES[] = { "Persona", "Bot" }, *SK_EN[] = { "Person", "Bot" }; 127 static const char *BO_ES[] = { "Aleatorio", "Principiante" }, *BO_EN[] = { "Random", "Beginner" }; 128 /* si a alguien le toca y no juega en ese tiempo, el bot juega su turno */ 129 static const int AWAY_MIN[] = { 0, 60, 4 * 60, 12 * 60, 24 * 60, 48 * 60 }; 130 /* colocacion inicial: por turnos, por turnos con plazo corto, o la hace el bot por todos */ 131 static const int SETUP_V[] = { 0, 5, 15, 60, -1 }; 132 static const char *SU_ES[] = { "Por turnos", "Por turnos, 5 min c/u", "Por turnos, 15 min c/u", "Por turnos, 1 hora c/u", "Automática" }, 133 *SU_EN[] = { "In turns", "In turns, 5 min each", "In turns, 15 min each", "In turns, 1 hour each", "Automatic" }; 134 static const char *AW_ES[] = { "Nunca", "1 hora", "4 horas", "12 horas", "24 horas", "48 horas" }, 135 *AW_EN[] = { "Never", "1 hour", "4 hours", "12 hours", "24 hours", "48 hours" }; 136 137 static void create_change(Form *f, void *ctx) 138 { 139 (void)ctx; 140 int np = form_get(f, C_PLAYERS)->value + 2; 141 form_get(f, C_VARIANT)->hidden = np != 2; 142 Field *scen = form_get(f, C_SCEN); /* Navegantes: 3-4 jugadores */ 143 scen->hidden = np < 3 || np > 4; 144 if (scen->hidden) scen->value = 0; 145 form_get(f, C_BOARD)->hidden = np > 4 || scen->value; /* el de principiante es solo el clasico */ 146 Opts o; /* el campo de mapa, con los de esa familia */ 147 opts_default(&o, np, false); 148 opts_scenario(&o, scen->value); 149 Field *map = form_get(f, C_MAP); 150 map->choices = map_choices(opts_family(&o)); 151 map->hidden = !form_get(f, C_BOARD)->hidden && form_get(f, C_BOARD)->value == 1; 152 for (int i = 1; i < MAXP; i++) form_get(f, C_SEAT + i)->hidden = i >= np; 153 } 154 155 static bool create_form(Frontend *fe, int layout, char *cmd, int n) 156 { 157 Form f; 158 bool en = g_lang; 159 form_begin(&f, en ? "Create game" : "Crear partida"); 160 form_choice(&f, C_PLAYERS, en ? "Players" : "Jugadores", CH_NUM, MAXP - 1, 0); 161 form_choice(&f, C_VARIANT, en ? "2-player variant" : "Variante para 2", en ? YN_EN : YN_ES, 2, 1); 162 for (int i = 1; i < MAXP; i++) { 163 char l[32]; 164 snprintf(l, sizeof l, "%s %d", en ? "Seat" : "Asiento", i + 1); 165 form_choice(&f, C_SEAT + i, l, en ? SK_EN : SK_ES, 2, i == 1 ? 0 : 1); 166 } 167 form_choice(&f, C_SCEN, en ? "Scenario" : "Escenario", en ? SCEN_CH_EN : SCEN_CH_ES, SCEN__COUNT, 0); 168 form_choice(&f, C_BOARD, en ? "Board" : "Tablero", en ? BO_EN : BO_ES, 2, 0); 169 form_choice(&f, C_MAP, en ? "Map" : "Mapa", map_choices(BF_CLASSIC), NMAPS + 1, 1); 170 form_choice(&f, C_LEVEL, en ? "Bots" : "Bots", en ? LV_EN : LV_ES, 3, 2); 171 form_choice(&f, C_AWAY, en ? "Bot plays if away" : "Bot juega si no hay", en ? AW_EN : AW_ES, 6, 4); 172 form_choice(&f, C_SETUP, en ? "Initial placement" : "Colocación inicial", en ? SU_EN : SU_ES, 5, 2); 173 form_label(&f, en ? "Seat 1 is yours; 'Person' seats wait for players." : "El asiento 1 es tuyo; los de 'Persona' esperan gente."); 174 form_label(&f, en ? "If you don't play in time, a bot plays that turn." : "Si no jugás a tiempo, un bot juega ese turno por vos."); 175 form_button(&f, C_CREATE, en ? "Create" : "Crear"); 176 form_button(&f, C_BACK, en ? "Back" : "Volver"); 177 f.cur = f.n - 2; 178 for (;;) { 179 if (run_form(fe, &f, create_change, 0) != C_CREATE) return false; 180 int np = form_get(&f, C_PLAYERS)->value + 2; 181 bool v2p = np == 2 && form_get(&f, C_VARIANT)->value; 182 char seats[8] = "H"; 183 for (int i = 1; i < np; i++) seats[i] = form_get(&f, C_SEAT + i)->value ? 'B' : 'H'; 184 seats[np] = 0; 185 int scen = form_get(&f, C_SCEN)->hidden ? 0 : form_get(&f, C_SCEN)->value; 186 int beg = !form_get(&f, C_BOARD)->hidden && form_get(&f, C_BOARD)->value; 187 Opts o; /* quien crea mira el tablero y lo elige */ 188 opts_default(&o, np, v2p); 189 opts_scenario(&o, scen); 190 int mv = form_get(&f, C_MAP)->value; 191 o.map = (uint8_t)(mv > 0 ? mv - 1 : 0); 192 if (!beg) { 193 const char *nm[MAXP]; 194 static char lbl[MAXP][20]; 195 for (int i = 0; i < MAXP; i++) { 196 snprintf(lbl[i], sizeof lbl[i], "%s %d", seats[i] == 'B' ? "Bot" : en ? "Seat" : "Asiento", i + 1); 197 nm[i] = lbl[i]; 198 } 199 if (!map_preview(fe, layout, &o, mv == 0, nm)) continue; 200 } 201 snprintf(cmd, (size_t)n, "CREATE %d %d %d %d %s %d %d %d %d %lu %d", np, v2p, scen ? 13 : 10, beg, seats, 202 AWAY_MIN[form_get(&f, C_AWAY)->value], SETUP_V[form_get(&f, C_SETUP)->value], scen, beg ? 0 : o.map, 203 beg ? 0ul : (unsigned long)o.bseed, LV_WIRE[form_get(&f, C_LEVEL)->value]); 204 return true; 205 } 206 } 207 208 /* ------------------------------------------------------------ mis partidas */ 209 210 typedef struct { char code[8], token[33], label[96]; bool mine_turn, over; } GameRow; 211 212 /* Lee STAT ... STATEND. token: el de todas las filas (MINE) o NULL para buscarlo en sg. */ 213 static int read_stats(Net *n, GameRow *rows, int count, int max, const char *token, const SavedGame *sg, int k) 214 { 215 char line[SRV_LINE]; 216 while (net_read_line(n, line, sizeof line, 5000)) { 217 if (!strcmp(line, "STATEND")) break; 218 char code[16], names[200] = ""; 219 int seat, phase, turn, needs, winner, np; 220 if (sscanf(line, "STAT %15s %d %d %d %d %d %d %199[^\n]", code, &seat, &phase, &turn, &needs, &winner, &np, names) < 7) continue; 221 bool dup = false; 222 for (int i = 0; i < count; i++) if (!strcmp(rows[i].code, code)) dup = true; 223 if (dup || count >= max) continue; 224 GameRow *r = &rows[count++]; 225 snprintf(r->code, sizeof r->code, "%s", code); 226 r->token[0] = 0; 227 if (token) snprintf(r->token, sizeof r->token, "%s", token); 228 else for (int i = 0; i < k; i++) if (!strcmp(sg[i].code, code)) snprintf(r->token, sizeof r->token, "%s", sg[i].token); 229 for (char *p = names; *p; p++) if (*p == '|') *p = ','; 230 r->over = phase == PH_OVER; 231 r->mine_turn = needs; 232 const char *state = r->over ? (g_lang ? "finished" : "terminada") 233 : needs ? (g_lang ? "YOUR TURN" : "¡TE TOCA!") 234 : (g_lang ? "waiting" : "esperando"); 235 snprintf(r->label, sizeof r->label, "%s · %s %d · %s · %.40s", code, g_lang ? "turn" : "turno", turn + 1, state, names); 236 } 237 return count; 238 } 239 240 /* Con contrasena: las partidas de tu nombre + contrasena (las sabe el servidor, sirve 241 * desde cualquier compu). Sin contrasena: las guardadas en esta compu (red local y 242 * partidas viejas). */ 243 static int fetch_status(Net *n, const char *hostkey, const char *seatkey, GameRow *rows, int max) 244 { 245 int count = 0; 246 if (seatkey && seatkey[0]) { /* con contrasena, solo las tuyas: en la misma compu */ 247 net_send(n, "AUTH %s", seatkey); /* puede haber otros usuarios (o vos con otro nombre) */ 248 net_send(n, "MINE"); 249 return read_stats(n, rows, count, max, seatkey, 0, 0); 250 } 251 SavedGame sg[64]; 252 int k = saved_games(hostkey, sg, 64); 253 if (!k) return count; 254 char cmd[SRV_LINE] = "STATUS"; 255 int used = 6; 256 for (int i = 0; i < k && used < SRV_LINE - 60; i++) 257 used += snprintf(cmd + used, sizeof cmd - (size_t)used, " %s %s", sg[i].code, sg[i].token); 258 net_send(n, "%s", cmd); 259 return read_stats(n, rows, count, max, 0, sg, k); 260 } 261 262 static bool my_games(Frontend *fe, Settings *st, Net *n, const char *hostkey, const char *seatkey, Form *parent) 263 { 264 GameRow rows[16]; 265 int k = fetch_status(n, hostkey, seatkey, rows, 16); 266 if (!k) { form_msg(parent, g_lang ? "You have no games on this server" : "No tenés partidas en este servidor", true); return false; } 267 Form f; 268 form_begin(&f, g_lang ? "My games" : "Mis partidas"); 269 /* primero las que te tocan */ 270 for (int pass = 0; pass < 3; pass++) 271 for (int i = 0; i < k; i++) { 272 int want = rows[i].mine_turn && !rows[i].over ? 0 : !rows[i].over ? 1 : 2; 273 if (want == pass) form_button(&f, 100 + i, rows[i].label); 274 } 275 form_button(&f, 0, g_lang ? "Back" : "Volver"); 276 int r = run_form(fe, &f, 0, 0); 277 if (r < 100) return false; 278 GameRow *g = &rows[r - 100]; 279 net_send(n, "OPEN %s %s", g->code, g->token); 280 return play_remote(fe, st, n, parent, 0); 281 } 282 283 /* ------------------------------------------------------------ menu online */ 284 285 enum { O_BACK = 0, O_MINE = 1, O_CREATE, O_JOIN, O_SERVER = 10, O_NAME, O_PASS, O_CODE }; 286 287 /* Clave del asiento: hash de nombre + contrasena. El servidor nunca ve la contrasena, 288 * y con el mismo nombre y contrasena se vuelve al asiento desde cualquier compu. */ 289 static void seat_key(const char *name, const char *pass, char out[33]) 290 { 291 char buf[200], hex[65]; 292 int k = snprintf(buf, sizeof buf, "catan-asiento|"); 293 /* el nombre como lo guarda el servidor (clean_name): sin espacios en las puntas ni 294 * '|' ni controles, asi la llave que calcula el administrador coincide */ 295 while (*name == ' ') name++; 296 int k0 = k; 297 for (const char *p = name; *p && k < (int)sizeof buf - 1; p++) { 298 unsigned char ch = (unsigned char)*p; 299 if (ch < 0x20 || ch == '|' || ch == 0x7F) continue; 300 buf[k++] = (char)tolower(ch); 301 } 302 while (k > k0 && buf[k - 1] == ' ') k--; 303 k += snprintf(buf + k, sizeof buf - (size_t)k, "|%s", pass); 304 if (k > (int)sizeof buf - 1) k = (int)sizeof buf - 1; 305 sha256_hex(buf, (size_t)k, hex); 306 memcpy(out, hex, 32); 307 out[32] = 0; 308 } 309 310 bool online_menu(Frontend *fe, Settings *st, const char *server) 311 { 312 Form f; 313 bool en = g_lang; 314 form_begin(&f, en ? "Play online" : "Jugar online"); 315 form_text(&f, O_SERVER, en ? "Server" : "Servidor", server && *server ? server : APP_SERVER, 60); 316 #ifdef __EMSCRIPTEN__ 317 /* la web siempre habla con el servidor de la pagina (web/shell.html fija la URL del WebSocket) */ 318 form_get(&f, O_SERVER)->hidden = true; 319 snprintf(form_get(&f, O_SERVER)->text, sizeof form_get(&f, O_SERVER)->text, "%s", APP_SERVER); 320 #endif 321 form_text(&f, O_NAME, en ? "Your name" : "Tu nombre", st->name, 16); 322 form_text(&f, O_PASS, en ? "Password" : "Contraseña", "", 32)->secret = true; 323 form_button(&f, O_MINE, en ? "My games" : "Mis partidas"); 324 form_button(&f, O_CREATE, en ? "Create a game" : "Crear partida"); 325 form_text(&f, O_CODE, en ? "Game code" : "Código", "", 7); 326 form_button(&f, O_JOIN, en ? "Join with code" : "Unirse con el código"); 327 form_label(&f, en ? "No accounts: name + password are your key." : "Sin cuentas: nombre + contraseña son tu llave."); 328 form_label(&f, en ? "With them you see your games from any computer." : "Con ellos ves tus partidas desde cualquier compu."); 329 form_button(&f, O_BACK, en ? "Back" : "Volver"); 330 f.cur = 2; 331 for (;;) { 332 int r = run_form(fe, &f, 0, 0); 333 if (r == O_BACK || r == FORM_CANCEL) return false; 334 snprintf(st->server, sizeof st->server, "%s", form_get(&f, O_SERVER)->text); 335 snprintf(st->name, sizeof st->name, "%s", form_get(&f, O_NAME)->text); 336 settings_save(st); 337 char host[128], err[200], hostkey[160]; 338 int port; 339 if (!parse_hostport(st->server, host, sizeof host, &port)) { form_msg(&f, en ? "Invalid server" : "Servidor inválido", true); continue; } 340 snprintf(hostkey, sizeof hostkey, "%s:%d", host, port); 341 const char *pass = form_get(&f, O_PASS)->text; 342 if ((r == O_CREATE || r == O_JOIN) && !pass[0]) { 343 form_msg(&f, en ? "Choose a password (you'll need it to get back in)" : "Elegí una contraseña (la vas a necesitar para volver)", true); 344 continue; 345 } 346 char cmd[64] = "", key[33]; 347 seat_key(st->name, pass, key); 348 if (r == O_CREATE && !create_form(fe, st->layout, cmd, sizeof cmd)) continue; 349 if (r == O_JOIN) { 350 char *code = form_get(&f, O_CODE)->text; 351 for (char *p = code; *p; p++) *p = (char)toupper((unsigned char)*p); 352 if (strlen(code) != 6) { form_msg(&f, en ? "Enter the 6-letter code" : "Escribí el código de 6 letras", true); continue; } 353 snprintf(cmd, sizeof cmd, "JOIN %s", code); 354 } 355 notice(fe, &f, en ? "Connecting..." : "Conectando...", false); 356 Net *n = net_connect(host, port, st->name, 0, err, sizeof err); 357 if (!n) { form_msg(&f, err, true); continue; } 358 if (r == O_MINE) my_games(fe, st, n, hostkey, pass[0] ? key : 0, &f); 359 else { net_send(n, "AUTH %s", key); net_send(n, "%s", cmd); play_remote(fe, st, n, &f, 0); } 360 net_close(n); 361 } 362 } 363 364 /* ------------------------------------------------------------ red local */ 365 366 #ifdef __EMSCRIPTEN__ 367 bool lan_host(Frontend *fe, Settings *st, int port) { (void)fe; (void)st; (void)port; return false; } 368 bool lan_join(Frontend *fe, Settings *st, const char *host, int port) { (void)fe; (void)st; (void)host; (void)port; return false; } 369 #else 370 static void msg_form(Frontend *fe, const char *title, const char *msg) 371 { 372 Form f; 373 form_begin(&f, title); 374 form_label(&f, msg); 375 form_button(&f, 0, "OK"); 376 run_form(fe, &f, 0, 0); 377 } 378 379 bool lan_host(Frontend *fe, Settings *st, int port) 380 { 381 if (port <= 0) port = DEFAULT_PORT; 382 char dir[600], err[200]; 383 snprintf(dir, sizeof dir, "%s/lan", data_dir()); 384 Server *srv = srv_open(&catan_module, port, dir, true, err, sizeof err); 385 if (!srv) { msg_form(fe, g_lang ? "Error" : "Error", err); return false; } 386 Net *n = net_connect("127.0.0.1", port, st->name, srv, err, sizeof err); 387 if (!n) { srv_close(srv); msg_form(fe, "Error", err); return false; } 388 char ip[64], hint[160]; 389 local_ip(ip, sizeof ip); 390 snprintf(hint, sizeof hint, g_lang ? "— others join with: catan join %s %d —" : "— los demás entran con: catan join %s %d —", ip, port); 391 392 Form f; 393 form_begin(&f, g_lang ? "LAN game" : "Partida en red local"); 394 GameRow rows[8]; 395 int k = fetch_status(n, "lan", 0, rows, 8), live = 0; 396 for (int i = 0; i < k; i++) 397 if (!rows[i].over) { form_button(&f, 100 + i, rows[i].label); live++; } 398 form_button(&f, 1, g_lang ? "New game" : "Partida nueva"); 399 form_button(&f, 0, g_lang ? "Back" : "Volver"); 400 form_label(&f, hint + 2); 401 int r = live ? run_form(fe, &f, 0, 0) : 1; 402 bool ok = false; 403 if (r >= 100) { 404 net_send(n, "OPEN %s %s", rows[r - 100].code, rows[r - 100].token); 405 ok = play_remote(fe, st, n, &f, hint); 406 } else if (r == 1) { 407 char cmd[64]; 408 if (create_form(fe, st->layout, cmd, sizeof cmd)) { 409 net_send(n, "%s", cmd); 410 ok = play_remote(fe, st, n, &f, hint); 411 } 412 } 413 net_close(n); 414 return ok; 415 } 416 417 bool lan_join(Frontend *fe, Settings *st, const char *host, int port) 418 { 419 if (port <= 0) port = DEFAULT_PORT; 420 char err[200], key[160]; 421 Form f; 422 form_begin(&f, g_lang ? "Joining..." : "Uniéndose..."); 423 notice(fe, &f, g_lang ? "Connecting..." : "Conectando...", false); 424 Net *n = net_connect(host, port, st->name, 0, err, sizeof err); 425 if (!n) { msg_form(fe, "Error", err); return false; } 426 snprintf(key, sizeof key, "%s:%d", host, port); 427 GameRow rows[8]; 428 int k = fetch_status(n, key, 0, rows, 8); 429 bool ok = false; 430 for (int i = 0; i < k && !ok; i++) 431 if (!rows[i].over) { 432 net_send(n, "OPEN %s %s", rows[i].code, rows[i].token); 433 ok = play_remote(fe, st, n, &f, 0); 434 } 435 if (!ok) { 436 net_send(n, "JOIN -"); 437 ok = play_remote(fe, st, n, &f, 0); 438 if (!ok) msg_form(fe, "Error", f.msg); 439 } 440 net_close(n); 441 return ok; 442 } 443 #endif