/********************************************************************/ /* */ /* L I QQ U U I DD W W A RR 555 */ /* L I Q Q U U I D D W W A A R R 5 */ /* L I Q Q U U I D D W W W AAA RR 55 */ /* L I Q Q U U I D D WW WW A A R R 5 */ /* LLL I Q Q U I DD W W A A R R 55 */ /* */ /* b */ /* bb y y */ /* b b yyy */ /* bb y */ /* yy */ /* */ /* U U FFF O O TTT */ /* U U F O O O O T */ /* U U TIRET FF O O O O T */ /* U U F O O O O T */ /* U F O O T */ /* */ /********************************************************************/ /*****************************************************************************/ /* Liquid War is a multiplayer wargame */ /* Copyright (C) 1998-2007 Christian Mauduit */ /* */ /* This program is free software; you can redistribute it and/or modify */ /* it under the terms of the GNU General Public License as published by */ /* the Free Software Foundation; either version 2 of the License, or */ /* (at your option) any later version. */ /* */ /* This program is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ /* GNU General Public License for more details. */ /* */ /* You should have received a copy of the GNU General Public License */ /* along with this program; if not, write to the Free Software */ /* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ /* */ /* Liquid War homepage : http://www.ufoot.org/liquidwar/v5 */ /* Contact author : ufoot@ufoot.org */ /*****************************************************************************/ /********************************************************************/ /* name : srvcont.c */ /* content : global controler of the network channels */ /* date de modif : 3 mai 98 */ /********************************************************************/ /*==================================================================*/ /* includes */ /*==================================================================*/ #include #include #include #include "srvcont.h" #include "srvchan.h" #include "sockgen.h" #include "sock2gen.h" #include "log.h" #include "server.h" #include "netkey.h" #include "srvtime.h" #include "startinf.h" #include "thrdgen.h" #include "macro.h" #include "execgen.h" /*==================================================================*/ /* structs */ /*==================================================================*/ typedef struct { int sock; char ip[LW_SOCK_IP_SIZE]; int port; int *thread_running; char password[PASSWORD_SIZE + 1]; } LW_SRVCONT_REJECT_DATA; /*==================================================================*/ /* constants */ /*==================================================================*/ #define LW_SRVCONT_INCREASE_LAG_LIMIT 10 #define LW_SRVCONT_DECREASE_LAG_LIMIT 20 #define LW_SRVCONT_DISPLAY_STATE_DELAY 60 #define LW_SRVCONT_REGISTER_DELAY 90 /*==================================================================*/ /* static functions */ /*==================================================================*/ static void reset (LW_SRVCONT * cont); static int distribute_teams (LW_SRVCONT * cont); static void recalc_nb_teams (LW_SRVCONT * cont); static void find_chan_and_team_by_server_id (LW_SRVCONT * cont, int server_id, int *chan, int *team); static int send_lag_key_presses (LW_SRVCONT * cont, int lag); static int reject_new_clients (LW_SRVCONT * cont, char *password); static void reject_new_clients_callback (void *args); static void disconnect_client (LW_SRVCONT * cont, int i, char *reason); /*------------------------------------------------------------------*/ /* * Resets the LW_SRVCONT struct */ static void reset (LW_SRVCONT * cont) { memset (cont, 0, sizeof (LW_SRVCONT)); cont->sock = -1; cont->nb_teams = 0; cont->nb_chans = 0; } /*------------------------------------------------------------------*/ /* * Affects consistent values to all the teams */ static int distribute_teams (LW_SRVCONT * cont) { int result = 1; int i, j, k; int i_orig, j_orig; int asm_algorithm; /* * The first step is to attribute a unique number to each * local team for each channel. */ k = 0; for (i = 0; i < cont->nb_chans; ++i) { for (j = 0; j < NB_TEAMS; ++j) { if (cont->chan[i].team[j].active) { cont->chan[i].team[j].server_id = k++; } } } /* * Now k should be equal to the number of available teams, * or it means there's a serious error... */ if (k != cont->nb_teams) { log_println_str ("Unable to attribute teams!"); result = 0; } /* * The second step is to define which teams in each channel * will be the "network" teams. */ for (i = 0; i < cont->nb_chans; ++i) { for (k = 0; k < cont->nb_teams; ++k) { if (lw_srvchan_find_team_by_server_id (&(cont->chan[i]), k) < 0) { j = lw_srvchan_find_first_free_team (&(cont->chan[i])); if (j >= 0) { cont->chan[i].team[j].active = 1; cont->chan[i].team[j].network = 1; cont->chan[i].team[j].server_id = k; } else { result = 0; log_println_str ("Unable to find a free team!"); } } } } /* * Third step, we copy the team names and other parameters in all * the teams structures. This is redundant information but it * will save useless CPU waste later... */ for (i = 0; i < cont->nb_chans; ++i) { for (j = 0; j < NB_TEAMS; ++j) { if (cont->chan[i].team[j].active) { find_chan_and_team_by_server_id (cont, cont->chan[i].team[j]. server_id, &i_orig, &j_orig); if (i_orig >= 0 && j_orig >= 0) { /* * Now we check that we're not uselessely copying * stuff from the same struct. It's a waste of time * and can cause some bugs depending on the implementation * of the LW_MACRO_STRCPY macro. */ if (i_orig != i || j_orig != j) { LW_MACRO_STRCPY (cont->chan[i].team[j].name, cont->chan[i_orig].team[j_orig].name); cont->chan[i].team[j].control_type = cont->chan[i_orig].team[j_orig].control_type; } } else { result = 0; log_println_str ("Unable to find a free team!"); } } } } /* * Last step, we check if the "asm_algorithm" parameter should be * enabled. Indeed, it must be enabled on _all_ computers or on * none of them... */ asm_algorithm = 1; for (i = 0; i < cont->nb_chans; ++i) { asm_algorithm = (cont->chan[i].config.asm_algorithm && asm_algorithm); } for (i = 0; i < cont->nb_chans; ++i) { cont->chan[i].config.asm_algorithm = asm_algorithm; } return result; } /*------------------------------------------------------------------*/ /* * Calculates the cont->nb_teams value */ static void recalc_nb_teams (LW_SRVCONT * cont) { int i, j; cont->nb_teams = 0; for (i = 0; i < cont->nb_chans; ++i) { for (j = 0; j < NB_TEAMS; ++j) { if (cont->chan[i].team[j].active) { cont->nb_teams++; } } } } /*------------------------------------------------------------------*/ /* * Gets the chan and team index for a given server id */ static void find_chan_and_team_by_server_id (LW_SRVCONT * cont, int server_id, int *chan, int *team) { int i; int j; *chan = *team = -1; for (i = 0; i < cont->nb_chans; ++i) { for (j = 0; j < NB_TEAMS; ++j) { if (cont->chan[i].team[j].active && (cont->chan[i].team[j].server_id == server_id) && !(cont->chan[i].team[j].network)) { (*chan) = i; (*team) = j; } } } } /*------------------------------------------------------------------*/ /* * Prepares the LW_TEAMSTARTINFO structures */ static void prepare_team_start_info (LW_TEAMSTARTINFO * team_start_info, LW_SRVCONT * cont) { int i, j, k; for (i = 0; i < NB_TEAMS; ++i) { lw_teamstartinfo_reset (team_start_info + i); } k = 0; for (i = 0; i < cont->nb_chans; ++i) { for (j = 0; j < NB_TEAMS; ++j) { if (cont->chan[i].team[j].active && k < NB_TEAMS) { team_start_info[k].active = 1; team_start_info[k].start = cont->chan[i].start; LW_MACRO_STRCPY (team_start_info[k].name, cont->chan[i].team[j].name); k++; } } } } /*==================================================================*/ /* global functions */ /*==================================================================*/ /*------------------------------------------------------------------*/ /* * Waits for a given number of teams to connect */ int lw_srvcont_wait_teams (LW_SRVCONT * cont, LW_WWWSRV_DATA * www_data, int nb_teams, int port, char *password, char *callback) { int free_teams = 0; int result = 0; int i; int start; int started = 0; LW_TEAMSTARTINFO team_start_info[NB_TEAMS]; int cur_time, last_time; int display_waiting_for = 1; LW_CHAT_HISTORY chat; LW_CHAT_MESSAGE chat_mess; reset (cont); cur_time = last_time = lw_srvtime_seconds (); lw_chat_reset (&chat); /* * We register the server on the metaserver for the * first time, with 0 active teams. */ www_data->busy_players = cont->nb_teams; www_data->max_players = nb_teams; www_data->log = 1; lw_wwwsrv_register (www_data); if (lw_sock_listen (&(cont->sock), port)) { log_print_str ("Listening on port "); log_print_int (port); log_println_str ("..."); log_flush (); /* * Now we just wait until there are enough teams connected */ while (!started) { free_teams = nb_teams - cont->nb_teams; if (display_waiting_for) { log_print_str ("Waiting for "); log_print_int (free_teams); if (free_teams > 1) { log_print_str (" teams..."); } else { log_print_str (" team..."); } log_println (); display_waiting_for = 0; } if (lw_srvchan_wait_teams (&(cont->chan[cont->nb_chans]), &free_teams, cont->sock, &(cont->chan[cont->nb_chans].map), &(cont->chan[cont->nb_chans].config), password)) { if (cont->nb_teams == 0) { /* * OK, this guy is the first one to connect on * the server, so we'll fire the "callback" script/program */ if (callback && strlen (callback)) { log_print_str ("Executing \""); log_print_str (callback); log_println_str ("\""); lw_exec_cmd (callback); } } cont->nb_teams = nb_teams - free_teams; ++(cont->nb_chans); display_waiting_for = 1; } /* * Now we check if me must force a game start */ start = 1; for (i = 0; i < cont->nb_chans; ++i) { start = start && cont->chan[i].start; } if (cont->nb_teams >= 2 && (start || cont->nb_teams >= nb_teams)) { if (cont->nb_teams < nb_teams) { /* * OK, all the players have pressed the "start" button and there * are at least 2 playing teams -> we start the game! */ nb_teams = cont->nb_teams; log_print_str ("Starting game with only "); log_print_int (cont->nb_teams); log_println_str (" teams"); } free_teams = 0; } /* * The official start signal is: free_teams is set to 0. * We use an extra flag "started" in case free_teams would * be recalculated */ if (free_teams <= 0) { free_teams = 0; started = 1; } /* * We replicate chat messages */ while (lw_chat_pop (&chat, &chat_mess)) { for (i = 0; i < cont->nb_chans; ++i) { lw_chat_push (&(cont->chan[i].chat), chat_mess.author, chat_mess.content); } } /* * now we send feedback to all the connected teams */ prepare_team_start_info (team_start_info, cont); /* * loop with a step of -1 to handle correcty the * possible disconnection of players */ for (i = cont->nb_chans - 1; i >= 0; --i) { if (!lw_srvchan_keepalive (&(cont->chan[i]), free_teams, team_start_info, &chat)) { /* * There's an error with this client, so * we're simply going to disconnect it... */ disconnect_client (cont, i, "keepalive failed"); display_waiting_for = 1; } } recalc_nb_teams (cont); www_data->busy_players = cont->nb_teams; www_data->max_players = nb_teams; /* * We register the server again since the number of * connected teams might have changed */ cur_time = lw_srvtime_seconds (); if (cur_time - last_time > LW_SRVCONT_REGISTER_DELAY || display_waiting_for) { www_data->log = 0; lw_wwwsrv_register (www_data); last_time = cur_time; } } /* * For now the server is a "blocking" server which is never statisfied * until the good number of players has connected itself, so if we * get here it means the operation has been successfull */ result = 1; } else { log_print_str ("Unable to bind socket on port "); log_print_int (port); log_println ("!"); /* * We consider this a fatal error and quit the program * right away. */ lw_server_exit (1); } return result; } /*------------------------------------------------------------------*/ /* * Tells the teams about what the other teams are */ int lw_srvcont_tell_who (LW_SRVCONT * cont) { int i; int result = 0; if (distribute_teams (cont)) { result = 1; for (i = cont->nb_chans - 1; i >= 0; --i) { if (!lw_srvchan_tell_who (&(cont->chan[i]), cont->chan[0].map, &(cont->chan[0].config))) { /* * There's been an error exchanging data with this team * so we decide to "close" it. */ disconnect_client (cont, i, "can't send game info"); } } /* * We start the game only if there are more than 1 channels opened... */ if (cont->nb_chans > 0) { result = 1; } } return result; } /*------------------------------------------------------------------*/ /* * Sends a final OK message to all the clients */ int lw_srvcont_final_ok (LW_SRVCONT * cont) { int i; int result = 0; for (i = cont->nb_chans - 1; i >= 0; --i) { if (!lw_srvchan_final_ok (&(cont->chan[i]))) { disconnect_client (cont, i, "can't send final ok"); } } /* * We start the game only if there are more than 1 channels opened... */ if (cont->nb_chans > 0) { result = 1; } if (result) { log_print_str ("Game start"); log_println (); } else { log_print_str ("Unable to start game"); log_println (); } return result; } /*------------------------------------------------------------------*/ /* * Sends a serie of blank key presses to create an artificial lag. * This lag is usefull, since this way, a few game cycles will * have to be run before a real key press comes back to a given * player. This leaves times for other players to send their own * key presses and this way the game can run smoothly. */ static int send_lag_key_presses (LW_SRVCONT * cont, int lag) { int result = 1; int i, j; LW_NETKEY netkey; lw_netkey_reset (&netkey); for (j = 0; j < lag && result; ++j) { for (i = cont->nb_chans - 1; i >= 0; --i) { /* * We put the checksum to 1, to inform the clients that * everything is OK */ cont->chan[i].checksum_send = 1; if (!lw_srvchan_send_keys (&(cont->chan[i]), &netkey)) { disconnect_client (cont, i, "error sending lag keys"); } } } return result; } /*------------------------------------------------------------------*/ /* * Checks if the checksums from the clients are all the same */ int check_checksum (LW_SRVCONT * cont) { int i, j; int ok = 1; int nb_diff; for (i = 0; i < cont->nb_chans; ++i) { cont->chan[i].checksum_send = 1; nb_diff = 0; /* * We do not check checksums if there's only 1 connected client. */ if (cont->nb_chans >= 2) { for (j = 0; j < cont->nb_chans; ++j) { if (cont->chan[i].checksum_recv != cont->chan[j].checksum_recv) { ok = 0; nb_diff++; } } /* * Now if this channel has a different checksum * than everyone else, we consider there's an error * on it. But if there's at least one channel that * has the same checksum, we consider it's OK * (for this channel only of course...) */ if (nb_diff >= cont->nb_chans - 1) { cont->chan[i].checksum_send = 0; log_print_str ("Checksum error on client \""); log_print_str (cont->chan[i].ip); log_print_str (":"); log_print_int (cont->chan[i].port); log_print_str ("\" ("); log_print_int (cont->chan[i].checksum_recv); log_print_str (")"); log_println (); } } } return ok; } /*------------------------------------------------------------------*/ /* * Sends a final OK message to all the clients */ int lw_srvcont_replicate_keys (LW_SRVCONT * cont, LW_WWWSRV_DATA * www_data, int lag, char *password) { int result = 1; LW_NETKEY netkey; int i; int increase_lag_counter = 0; int decrease_lag_counter = 0; int all_clients_request_increase_lag; int all_clients_request_decrease_lag; int a_client_requests_increase_lag; int a_client_requests_decrease_lag; int rounds = 0; int last_rounds = 0; int average_lag = 0; time_t start, start2, end; double elapsed; int lag_key_presses_to_send = 0; int skip_keys; www_data->log = 1; lw_wwwsrv_register (www_data); lag_key_presses_to_send = lag; start2 = start = lw_srvtime_seconds (); while (cont->nb_chans > 0) { lw_netkey_reset (&netkey); /* * We still listen on the listening socket, to reject * newcomers since the game already started */ reject_new_clients (cont, password); all_clients_request_increase_lag = 1; all_clients_request_decrease_lag = 1; a_client_requests_increase_lag = 0; a_client_requests_decrease_lag = 0; /* * First we get the informations from all the players */ for (i = cont->nb_chans - 1; i >= 0; --i) { if (lw_srvchan_recv_keys (&(cont->chan[i]), &netkey)) { /* * if at least a client does not request a lag * increase, then we don't do it */ if (netkey.cmd == LW_NETKEY_CMD_INCREASE_LAG) { a_client_requests_increase_lag = 1; } else { all_clients_request_increase_lag = 0; } /* * if at least a client does not request a lag * decrease, then we don't do it */ if (netkey.cmd == LW_NETKEY_CMD_DECREASE_LAG) { a_client_requests_decrease_lag = 1; } else { all_clients_request_decrease_lag = 0; } } else { /* * There's been an error receiving data from this team * so we decide to "close" it. */ disconnect_client (cont, i, "no more keys"); } } /* * we reset the cmd and arg fields */ netkey.cmd = LW_NETKEY_CMD_NONE; netkey.arg = 0; /* * We check if the checksums are OK */ check_checksum (cont); /* * we use a counter system, so that all client must answer * "yes we want a lag increase" several times in a row * before we actually update the lag */ if (all_clients_request_increase_lag) { increase_lag_counter++; } else { increase_lag_counter = 0; } /* * we use a counter system, so that all client must answer * "yes we want a lag decrease" several times in a row * before we actually update the lag */ if (a_client_requests_decrease_lag) { decrease_lag_counter++; } else { decrease_lag_counter = 0; } /* * If we need to increase the lag, we send blank key presses */ if (increase_lag_counter >= LW_SRVCONT_INCREASE_LAG_LIMIT && lag_key_presses_to_send == 0) { increase_lag_counter = 0; lag++; lag_key_presses_to_send++; } /* * If we need to decrease the lag, we simply do not send * the key presses... */ skip_keys = 0; if (decrease_lag_counter >= LW_SRVCONT_DECREASE_LAG_LIMIT) { decrease_lag_counter = 0; lag--; if (lag_key_presses_to_send > 0) { /* * We cancel the planified lag keys */ lag_key_presses_to_send--; } else { /* * No lag keys planned, we skip the current keys */ skip_keys = 1; } } if (!skip_keys) { /* * Now we send the information to all the players */ for (i = cont->nb_chans - 1; i >= 0; --i) { if (!lw_srvchan_send_keys (&(cont->chan[i]), &netkey)) { /* * There's been an error receiving data from this team * so we decide to "close" it. */ disconnect_client (cont, i, "error sending keys"); } } } /* * We send the lag key presses 1 by 1. This is a way * to synchronize correctly at the beginning of a game. */ if (lag_key_presses_to_send > 0) { lag_key_presses_to_send--; send_lag_key_presses (cont, 1); } rounds++; last_rounds++; average_lag += lag; end = lw_srvtime_seconds (); elapsed = end - start; if (((int) elapsed) > LW_SRVCONT_DISPLAY_STATE_DELAY) { if (rounds <= 0) { rounds = 1; } if (last_rounds <= 0) { last_rounds = 1; } log_print_int (rounds); log_print_str (" rounds, "); log_print_int (last_rounds / ((int) elapsed)); log_print_str (" rounds/sec, average lag is "); log_print_int (average_lag / last_rounds); log_println (); start = end; last_rounds = 0; average_lag = 0; } if (end - start2 > LW_SRVCONT_REGISTER_DELAY) { start2 += LW_SRVCONT_REGISTER_DELAY; www_data->log = 0; lw_wwwsrv_register (www_data); } } return result; } /*------------------------------------------------------------------*/ /* * Closes everything and frees the resources */ void lw_srvcont_close (LW_SRVCONT * cont) { int i; lw_sock_close (&(cont->sock)); log_print_str ("Game over"); log_println (); for (i = 0; i < cont->nb_chans; ++i) { lw_srvchan_close (&(cont->chan[i])); } reset (cont); } /*------------------------------------------------------------------*/ /* * Launches a thread which will handle new connections while * other players are playing. */ static int reject_new_clients (LW_SRVCONT * cont, char *password) { int result = 0; static LW_SRVCONT_REJECT_DATA data; static int thread_running = 0; /* * If there's already a thread answering to a client, we do nothing, * for we can answer later, and it makes programming easier and * safer. In any case, the answer we're going to give to the client * is: "there's no room for you guy" so it's not that bad to be * a little late when answering. */ if (!thread_running) { data.sock = -1; data.ip[0] = '\0'; data.port = 0; data.thread_running = &thread_running; strncpy (data.password, password, PASSWORD_SIZE); data.password[PASSWORD_SIZE] = '\0'; if (lw_sock_accept (&(data.sock), data.ip, &(data.port), cont->sock, 0)) { thread_running = 1; result = lw_thread_start (reject_new_clients_callback, (void *) &data); if (!result) { lw_sock_close (&(data.sock)); thread_running = 0; } } else { /* * privacy is set to 0, we do not tell anyone about this server */ result = 1; } } else { result = 1; } return result; } /*------------------------------------------------------------------*/ /* * Main function of the server thread which tries to connect * on the metaserver */ static void reject_new_clients_callback (void *args) { LW_SRVCONT_REJECT_DATA data; int ret; int ret_mess = 1; char message[LW_SOCK_MESSAGE_SIZE]; LW_NETMESS *mess; int program = 0; int version = 0; int password = 0; int free_received = 0; int normal_quit = 0; memcpy (&data, args, sizeof (LW_SRVCONT_REJECT_DATA)); log_print_str ("Connection from \""); log_print_str (data.ip); log_print_str (":"); log_print_int (data.port); log_print_str ("\""); log_println (); log_flush (); while ((!free_received) && ret_mess && ((ret = lw_sock_recv_str (&(data.sock), message)) >= 0)) { if (ret > 0) { mess = lw_netmess_read (message); if (mess != NULL) { /* * switch to handle the different possible messages */ switch (mess->code) { case LW_NETMESS_CODE_PING: ret_mess = lw_srvchan_handle_ping (data.sock, mess); break; case LW_NETMESS_CODE_ECHO: ret_mess = lw_srvchan_handle_echo (data.sock, mess); break; case LW_NETMESS_CODE_PROGRAM: ret_mess = lw_srvchan_handle_program (data.sock, mess, &program); break; case LW_NETMESS_CODE_VERSION: ret_mess = lw_srvchan_handle_version (data.sock, mess, &version); break; case LW_NETMESS_CODE_PASSWORD: ret_mess = lw_srvchan_handle_password (data.sock, mess, &password, data.password); break; case LW_NETMESS_CODE_FREE: ret_mess = lw_srvchan_handle_free (data.sock, mess, 0, &free_received); break; case LW_NETMESS_CODE_QUIT: ret_mess = lw_srvchan_handle_quit (data.sock, mess, &normal_quit); break; default: ret_mess = lw_srvchan_handle_unknown (data.sock, mess); break; } /* * lw_netmess_free must *not* be forgetted */ lw_netmess_free (mess); if (!ret_mess) { log_print_str ("Error processing \""); log_print_str (message); log_print_str ("\""); log_println (); } } else { log_println_str ("Serious memory problem 8-("); ret_mess = 0; } } } log_print_str ("Refusing \""); log_print_str (data.ip); log_print_str (":"); log_print_int (data.port); log_print_str ("\""); if (normal_quit) { /* * If the client quitted with a normal "QUIT" message, then * we mention in the log that it was probably simply pinging. */ log_print_str (" (probably a ping)"); } log_println (); lw_sock_close (&(data.sock)); (*(data.thread_running)) = 0; } /*------------------------------------------------------------------*/ /* * Disconnects a client. This code supposes that the caller function * is executing a loop with a step of -1 (ie a "reverse" loop). */ static void disconnect_client (LW_SRVCONT * cont, int i, char *reason) { int j; log_print_str ("Disconnecting \""); log_print_str (cont->chan[i].ip); log_print_str (":"); log_print_int (cont->chan[i].port); log_print_str ("\" ("); if (cont->chan[i].normal_quit) { /* * The client decided to quit, therefore this is not * an error, we mark it as such */ log_print_str ("decided to quit"); } else { log_print_str (reason); } log_println_str (")"); lw_srvchan_close (&(cont->chan[i])); for (j = i + 1; j < cont->nb_chans; ++j) { cont->chan[j - 1] = cont->chan[j]; } cont->nb_chans--; }