juegos

Juegos de terminal de Pancho: Catan (TUI, GUI, web, servidor, PicoCalc), ajedrez, calculadora y minijuegos
git clone https://git.lu3dhn.xyz/juegos.git
Log | Files | Refs

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