/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static SNAPBUILD builder; static int64 game_start_time; static int current_tick = 0; static int browseinfo_gametype = -1; static int browseinfo_progression = -1; static int64 lastheartbeat; static NETADDR4 master_server; static char current_map[64]; static int current_map_crc; void *snap_new_item(int type, int id, int size) { dbg_assert(type >= 0 && type <=0xffff, "incorrect type"); dbg_assert(id >= 0 && id <=0xffff, "incorrect id"); return snapbuild_new_item(&builder, type, id, size); } typedef struct { short next; short state; /* 0 = free, 1 = alloced, 2 = timed */ int timeout; } SNAP_ID; static const int MAX_IDS = 16*1024; /* should be lowered */ static SNAP_ID snap_ids[16*1024]; static int snap_first_free_id; static int snap_first_timed_id; static int snap_last_timed_id; static int snap_id_usage; static int snap_id_inusage; static int snap_id_inited = 0; enum { SRVCLIENT_STATE_EMPTY = 0, SRVCLIENT_STATE_CONNECTING, SRVCLIENT_STATE_READY, SRVCLIENT_STATE_INGAME, SRVCLIENT_SNAPRATE_INIT=0, SRVCLIENT_SNAPRATE_FULL, SRVCLIENT_SNAPRATE_RECOVER }; typedef struct { int data[MAX_INPUT_SIZE]; int pred_tick; /* tick that the client predicted for the input */ int game_tick; /* the tick that was chosen for the input */ int64 timeleft; /* how much time in ms there were left before this should be applied */ } CLIENT_INPUT; /* */ typedef struct { /* connection state info */ int state; int latency; int snap_rate; int last_acked_snapshot; SNAPSTORAGE snapshots; CLIENT_INPUT inputs[200]; /* TODO: handle input better */ int current_input; char name[MAX_NAME_LENGTH]; char clan[MAX_CLANNAME_LENGTH]; int score; } CLIENT; static CLIENT clients[MAX_CLIENTS]; static NETSERVER *net; static void snap_init_id() { int i; for(i = 0; i < MAX_IDS; i++) { snap_ids[i].next = i+1; snap_ids[i].state = 0; } snap_ids[MAX_IDS-1].next = -1; snap_first_free_id = 0; snap_first_timed_id = -1; snap_last_timed_id = -1; snap_id_usage = 0; snap_id_inusage = 0; snap_id_inited = 1; } static void snap_remove_first_timeout() { int next_timed = snap_ids[snap_first_timed_id].next; /* add it to the free list */ snap_ids[snap_first_timed_id].next = snap_first_free_id; snap_ids[snap_first_timed_id].state = 0; snap_first_free_id = snap_first_timed_id; /* remove it from the timed list */ snap_first_timed_id = next_timed; if(snap_first_timed_id == -1) snap_last_timed_id = -1; snap_id_usage--; } int snap_new_id() { int id; int64 now = time_get(); dbg_assert(snap_id_inited == 1, "requesting id too soon"); /* process timed ids */ while(snap_first_timed_id != -1 && snap_ids[snap_first_timed_id].timeout < now) snap_remove_first_timeout(); id = snap_first_free_id; dbg_assert(id != -1, "id error"); snap_first_free_id = snap_ids[snap_first_free_id].next; snap_ids[id].state = 1; snap_id_usage++; snap_id_inusage++; return id; } void snap_timeout_ids() { /* process timed ids */ while(snap_first_timed_id != -1) snap_remove_first_timeout(); } void snap_free_id(int id) { dbg_assert(snap_ids[id].state == 1, "id is not alloced"); snap_id_inusage--; snap_ids[id].state = 2; snap_ids[id].timeout = time_get()+time_freq()*5; snap_ids[id].next = -1; if(snap_last_timed_id != -1) { snap_ids[snap_last_timed_id].next = id; snap_last_timed_id = id; } else { snap_first_timed_id = id; snap_last_timed_id = id; } } const char *server_clientname(int client_id) { if(client_id < 0 || client_id > MAX_CLIENTS || clients[client_id].state < SRVCLIENT_STATE_READY) return "(invalid client)"; return clients[client_id].name; } void server_setclientname(int client_id, const char *name) { if(client_id < 0 || client_id > MAX_CLIENTS || clients[client_id].state < SRVCLIENT_STATE_READY) return; strncpy(clients[client_id].name, name, MAX_NAME_LENGTH); } void server_setclientscore(int client_id, int score) { if(client_id < 0 || client_id > MAX_CLIENTS || clients[client_id].state < SRVCLIENT_STATE_READY) return; clients[client_id].score = score; } void server_setbrowseinfo(int game_type, int progression) { browseinfo_gametype = game_type; browseinfo_progression = progression; } void server_kick(int client_id, const char *reason) { if(clients[client_id].state != SRVCLIENT_STATE_EMPTY) netserver_drop(net, client_id, reason); } int server_tick() { return current_tick; } int64 server_tick_start_time(int tick) { return game_start_time + (time_freq()*tick)/SERVER_TICK_SPEED; } int server_tickspeed() { return SERVER_TICK_SPEED; } int server_init() { int i; for(i = 0; i < MAX_CLIENTS; i++) { clients[i].state = SRVCLIENT_STATE_EMPTY; clients[i].name[0] = 0; clients[i].clan[0] = 0; snapstorage_init(&clients[i].snapshots); } current_tick = 0; return 0; } int server_getclientinfo(int client_id, CLIENT_INFO *info) { dbg_assert(client_id >= 0 && client_id < MAX_CLIENTS, "client_id is not valid"); dbg_assert(info != 0, "info can not be null"); if(clients[client_id].state == SRVCLIENT_STATE_INGAME) { info->name = clients[client_id].name; info->latency = clients[client_id].latency; return 1; } return 0; } int server_send_msg(int client_id) { const MSG_INFO *info = msg_get_info(); NETPACKET packet; mem_zero(&packet, sizeof(NETPACKET)); packet.client_id = client_id; packet.data = info->data; packet.data_size = info->size; if(info->flags&MSGFLAG_VITAL) packet.flags = PACKETFLAG_VITAL; if(client_id == -1) { /* broadcast */ int i; for(i = 0; i < MAX_CLIENTS; i++) if(clients[i].state == SRVCLIENT_STATE_INGAME) { packet.client_id = i; netserver_send(net, &packet); } } else netserver_send(net, &packet); return 0; } static void server_do_snap() { int i, k; { static PERFORMACE_INFO scope = {"presnap", 0}; perf_start(&scope); mods_presnap(); perf_end(); } for(i = 0; i < MAX_CLIENTS; i++) { /* client must be ingame to recive snapshots */ if(clients[i].state != SRVCLIENT_STATE_INGAME) continue; /* this client is trying to recover, don't spam snapshots */ if(clients[i].snap_rate == SRVCLIENT_SNAPRATE_RECOVER && (server_tick()%50) != 0) continue; /* this client is trying to recover, don't spam snapshots */ if(clients[i].snap_rate == SRVCLIENT_SNAPRATE_INIT && (server_tick()%10) != 0) continue; { char data[MAX_SNAPSHOT_SIZE]; char deltadata[MAX_SNAPSHOT_SIZE]; char compdata[MAX_SNAPSHOT_SIZE]; int snapshot_size; int crc; static SNAPSHOT emptysnap; SNAPSHOT *deltashot = &emptysnap; int deltashot_size; int delta_tick = -1; int input_predtick = -1; int64 timeleft = 0; int deltasize; static PERFORMACE_INFO scope = {"build", 0}; perf_start(&scope); snapbuild_init(&builder); { static PERFORMACE_INFO scope = {"modsnap", 0}; perf_start(&scope); mods_snap(i); perf_end(); } /* finish snapshot */ snapshot_size = snapbuild_finish(&builder, data); crc = snapshot_crc((SNAPSHOT*)data); /* remove old snapshos */ /* keep 1 seconds worth of snapshots */ snapstorage_purge_until(&clients[i].snapshots, current_tick-SERVER_TICK_SPEED); /* save it the snapshot */ snapstorage_add(&clients[i].snapshots, current_tick, time_get(), snapshot_size, data); /* find snapshot that we can preform delta against */ emptysnap.data_size = 0; emptysnap.num_items = 0; { deltashot_size = snapstorage_get(&clients[i].snapshots, clients[i].last_acked_snapshot, 0, &deltashot); if(deltashot_size >= 0) delta_tick = clients[i].last_acked_snapshot; else { /* no acked package found, force client to recover rate */ if(clients[i].snap_rate == SRVCLIENT_SNAPRATE_FULL) clients[i].snap_rate = SRVCLIENT_SNAPRATE_RECOVER; } } for(k = 0; k < 200; k++) /* TODO: do this better */ { if(clients[i].inputs[k].game_tick == current_tick) { timeleft = clients[i].inputs[k].timeleft; input_predtick = clients[i].inputs[k].pred_tick; break; } } /* create delta */ { static PERFORMACE_INFO scope = {"delta", 0}; perf_start(&scope); deltasize = snapshot_create_delta(deltashot, (SNAPSHOT*)data, deltadata); perf_end(); } if(deltasize) { /* compress it */ unsigned char intdata[MAX_SNAPSHOT_SIZE]; int intsize; int snapshot_size; const int max_size = MAX_SNAPSHOT_PACKSIZE; int numpackets; int n, left; { static PERFORMACE_INFO scope = {"compress", 0}; perf_start(&scope); { static PERFORMACE_INFO scope = {"int", 0}; perf_start(&scope); intsize = intpack_compress(deltadata, deltasize, intdata); perf_end(); } { static PERFORMACE_INFO scope = {"zero", 0}; perf_start(&scope); snapshot_size = zerobit_compress(intdata, intsize, compdata); perf_end(); } perf_end(); } numpackets = (snapshot_size+max_size-1)/max_size; for(n = 0, left = snapshot_size; left; n++) { int chunk = left < max_size ? left : max_size; left -= chunk; if(numpackets == 1) msg_pack_start_system(NETMSG_SNAPSINGLE, 0); else msg_pack_start_system(NETMSG_SNAP, 0); msg_pack_int(current_tick); msg_pack_int(current_tick-delta_tick); /* compressed with */ msg_pack_int(input_predtick); msg_pack_int((timeleft*1000)/time_freq()); if(numpackets != 1) { msg_pack_int(numpackets); msg_pack_int(n); } msg_pack_int(crc); msg_pack_int(chunk); msg_pack_raw(&compdata[n*max_size], chunk); msg_pack_end(); server_send_msg(i); } } else { msg_pack_start_system(NETMSG_SNAPEMPTY, 0); msg_pack_int(current_tick); msg_pack_int(current_tick-delta_tick); /* compressed with */ msg_pack_int(input_predtick); msg_pack_int((timeleft*1000)/time_freq()); msg_pack_end(); server_send_msg(i); } perf_end(); } } mods_postsnap(); } static int new_client_callback(int cid, void *user) { int i; clients[cid].state = SRVCLIENT_STATE_CONNECTING; clients[cid].name[0] = 0; clients[cid].clan[0] = 0; /* reset input */ for(i = 0; i < 200; i++) { clients[cid].inputs[i].game_tick = -1; clients[cid].inputs[i].pred_tick = -1; } clients[cid].current_input = 0; snapstorage_purge_all(&clients[cid].snapshots); clients[cid].last_acked_snapshot = -1; clients[cid].snap_rate = SRVCLIENT_SNAPRATE_INIT; clients[cid].score = 0; return 0; } static int del_client_callback(int cid, void *user) { /* notify the mod about the drop */ if(clients[cid].state == SRVCLIENT_STATE_READY || clients[cid].state == SRVCLIENT_STATE_INGAME) { mods_client_drop(cid); } clients[cid].state = SRVCLIENT_STATE_EMPTY; clients[cid].name[0] = 0; clients[cid].clan[0] = 0; snapstorage_purge_all(&clients[cid].snapshots); return 0; } static void server_send_map(int cid) { msg_pack_start_system(NETMSG_MAP, MSGFLAG_VITAL); msg_pack_string(config.sv_map, 0); msg_pack_int(current_map_crc); msg_pack_end(); server_send_msg(cid); } static void server_send_heartbeat() { static unsigned char data[sizeof(SERVERBROWSE_HEARTBEAT) + 2]; unsigned short port = config.sv_port; NETPACKET packet; mem_copy(data, SERVERBROWSE_HEARTBEAT, sizeof(SERVERBROWSE_HEARTBEAT)); packet.client_id = -1; packet.address = master_server; packet.flags = PACKETFLAG_CONNLESS; packet.data_size = sizeof(SERVERBROWSE_HEARTBEAT) + 2; packet.data = &data; /* supply the set port that the master can use if it has problems */ if(config.sv_external_port) port = config.sv_external_port; data[sizeof(SERVERBROWSE_HEARTBEAT)] = port >> 8; data[sizeof(SERVERBROWSE_HEARTBEAT)+1] = port&0xff; netserver_send(net, &packet); } static void server_process_client_packet(NETPACKET *packet) { int cid = packet->client_id; int sys; int msg = msg_unpack_start(packet->data, packet->data_size, &sys); if(sys) { /* system message */ if(msg == NETMSG_INFO) { char version[64]; const char *password; strncpy(version, msg_unpack_string(), 64); if(strcmp(version, mods_net_version()) != 0) { /* OH FUCK! wrong version, drop him */ char reason[256]; sprintf(reason, "wrong version. server is running %s.", mods_net_version()); netserver_drop(net, cid, reason); return; } strncpy(clients[cid].name, msg_unpack_string(), MAX_NAME_LENGTH); strncpy(clients[cid].clan, msg_unpack_string(), MAX_CLANNAME_LENGTH); password = msg_unpack_string(); if(config.password[0] != 0 && strcmp(config.password, password) != 0) { /* wrong password */ netserver_drop(net, cid, "wrong password"); return; } server_send_map(cid); } else if(msg == NETMSG_READY) { if(clients[cid].state == SRVCLIENT_STATE_CONNECTING) { dbg_msg("server", "player is ready. cid=%x", cid); clients[cid].state = SRVCLIENT_STATE_READY; mods_connected(cid); } } else if(msg == NETMSG_ENTERGAME) { if(clients[cid].state != SRVCLIENT_STATE_INGAME) { dbg_msg("server", "player as entered the game. cid=%x", cid); clients[cid].state = SRVCLIENT_STATE_INGAME; mods_client_enter(cid); } } else if(msg == NETMSG_INPUT) { int tick, size, i; CLIENT_INPUT *input; int64 tagtime; clients[cid].last_acked_snapshot = msg_unpack_int(); if(clients[cid].last_acked_snapshot > 0) clients[cid].snap_rate = SRVCLIENT_SNAPRATE_FULL; if(snapstorage_get(&clients[cid].snapshots, clients[cid].last_acked_snapshot, &tagtime, 0) >= 0) clients[cid].latency = (int)(((time_get()-tagtime)*1000)/time_freq()); tick = msg_unpack_int(); size = msg_unpack_int(); input = &clients[cid].inputs[clients[cid].current_input]; input->timeleft = server_tick_start_time(tick)-time_get(); input->pred_tick = tick; if(tick <= server_tick()) tick = server_tick()+1; input->game_tick = tick; for(i = 0; i < size/4; i++) input->data[i] = msg_unpack_int(); clients[cid].current_input++; clients[cid].current_input %= 200; } else if(msg == NETMSG_CMD) { const char *pw = msg_unpack_string(); const char *cmd = msg_unpack_string(); if(config.rcon_password[0] != 0 && strcmp(pw, config.rcon_password) == 0) { dbg_msg("server", "cid=%d rcon='%s'", cid, cmd); config_set(cmd); } } else { dbg_msg("server", "strange message cid=%d msg=%d data_size=%d", cid, msg, packet->data_size); } } else { /* game message */ mods_message(msg, cid); } } static void server_send_serverinfo(NETADDR4 *addr) { NETPACKET packet; PACKER p; char buf[128]; /* count the players */ int c = 0; int i; for(i = 0; i < MAX_CLIENTS; i++) { if(clients[i].state != SRVCLIENT_STATE_EMPTY) c++; } packer_reset(&p); packer_add_raw(&p, SERVERBROWSE_INFO, sizeof(SERVERBROWSE_INFO)); packer_add_string(&p, mods_net_version(), 32); packer_add_string(&p, config.sv_name, 64); packer_add_string(&p, config.sv_map, 32); /* gametype */ sprintf(buf, "%d", browseinfo_gametype); packer_add_string(&p, buf, 2); /* flags */ i = 0; if(strlen(config.password)) i |= 1; sprintf(buf, "%d", i); packer_add_string(&p, buf, 2); /* progression */ sprintf(buf, "%d", browseinfo_progression); packer_add_string(&p, buf, 4); sprintf(buf, "%d", c); packer_add_string(&p, buf, 3); /* num players */ sprintf(buf, "%d", netserver_max_clients(net)); packer_add_string(&p, buf, 3); /* max players */ for(i = 0; i < MAX_CLIENTS; i++) { if(clients[i].state != SRVCLIENT_STATE_EMPTY) { packer_add_string(&p, clients[i].name, 48); /* player name */ sprintf(buf, "%d", clients[i].score); packer_add_string(&p, buf, 6); /* player score */ } } packet.client_id = -1; packet.address = *addr; packet.flags = PACKETFLAG_CONNLESS; packet.data_size = packer_size(&p); packet.data = packer_data(&p); netserver_send(net, &packet); } static void server_send_fwcheckresponse(NETADDR4 *addr) { NETPACKET packet; packet.client_id = -1; packet.address = *addr; packet.flags = PACKETFLAG_CONNLESS; packet.data_size = sizeof(SERVERBROWSE_FWRESPONSE); packet.data = SERVERBROWSE_FWRESPONSE; netserver_send(net, &packet); } static void server_pump_network() { NETPACKET packet; netserver_update(net); /* process packets */ while(netserver_recv(net, &packet)) { if(packet.client_id == -1) { /* stateless */ if(packet.data_size == sizeof(SERVERBROWSE_GETINFO) && memcmp(packet.data, SERVERBROWSE_GETINFO, sizeof(SERVERBROWSE_GETINFO)) == 0) { server_send_serverinfo(&packet.address); } else if(packet.data_size == sizeof(SERVERBROWSE_FWCHECK) && memcmp(packet.data, SERVERBROWSE_FWCHECK, sizeof(SERVERBROWSE_FWCHECK)) == 0) { server_send_fwcheckresponse(&packet.address); } else if(packet.data_size == sizeof(SERVERBROWSE_FWOK) && memcmp(packet.data, SERVERBROWSE_FWOK, sizeof(SERVERBROWSE_FWOK)) == 0) { if(config.debug) dbg_msg("server", "no firewall/nat problems detected"); } else if(packet.data_size == sizeof(SERVERBROWSE_FWERROR) && memcmp(packet.data, SERVERBROWSE_FWERROR, sizeof(SERVERBROWSE_FWERROR)) == 0) { dbg_msg("server", "ERROR: the master server reports that clients can not connect to this server."); dbg_msg("server", "ERROR: configure your firewall/nat to let trough udp on port %d.", config.sv_port); } } else server_process_client_packet(&packet); } } static int server_load_map(const char *mapname) { DATAFILE *df; char buf[512]; sprintf(buf, "data/maps/%s.map", mapname); df = datafile_load(buf); if(!df) return 0; /* reinit snapshot ids */ snap_timeout_ids(); /* get the crc of the map */ current_map_crc = datafile_crc(buf); dbg_msg("server", "%s crc is %08x", buf, current_map_crc); strcpy(current_map, mapname); map_set(df); return 1; } static int server_run() { NETADDR4 bindaddr; net_init(); snap_init_id(); /* load map */ if(!server_load_map(config.sv_map)) { dbg_msg("server", "failed to load map. mapname='%s'", config.sv_map); return -1; } /* start server */ if(strlen(config.sv_bindaddr) && net_host_lookup(config.sv_bindaddr, config.sv_port, &bindaddr) != 0) { /* sweet! */ } else { mem_zero(&bindaddr, sizeof(bindaddr)); bindaddr.port = config.sv_port; } net = netserver_open(bindaddr, config.sv_max_clients, 0); if(!net) { dbg_msg("server", "couldn't open socket. port might already be in use"); return -1; } netserver_set_callbacks(net, new_client_callback, del_client_callback, 0); dbg_msg("server", "server name is '%s'", config.sv_name); dbg_msg("server", "masterserver is '%s'", config.masterserver); if(net_host_lookup(config.masterserver, MASTERSERVER_PORT, &master_server) != 0) { /* TODO: fix me */ /*master_server = netaddr4(0, 0, 0, 0, 0); */ } mods_init(); dbg_msg("server", "version %s", mods_net_version()); /* start game */ { int64 time_per_heartbeat = time_freq() * 30; int64 reporttime = time_get(); int reportinterval = 3; lastheartbeat = 0; game_start_time = time_get(); if(config.debug) dbg_msg("server", "baseline memory usage %dk", mem_allocated()/1024); while(1) { static PERFORMACE_INFO rootscope = {"root", 0}; int64 t = time_get(); int new_ticks = 0; perf_start(&rootscope); /* load new map TODO: don't poll this */ if(strcmp(config.sv_map, current_map) != 0 || config.sv_map_reload) { config.sv_map_reload = 0; /* load map */ if(server_load_map(config.sv_map)) { int c; /* new map loaded */ mods_shutdown(); for(c = 0; c < MAX_CLIENTS; c++) { if(clients[c].state == SRVCLIENT_STATE_EMPTY) continue; server_send_map(c); clients[c].state = SRVCLIENT_STATE_CONNECTING; clients[c].last_acked_snapshot = -1; clients[c].snap_rate = SRVCLIENT_SNAPRATE_RECOVER; snapstorage_purge_all(&clients[c].snapshots); } game_start_time = time_get(); current_tick = 0; mods_init(); } else { dbg_msg("server", "failed to load map. mapname='%s'", config.sv_map); config_set_sv_map(&config, current_map); } } while(t > server_tick_start_time(current_tick+1)) { current_tick++; new_ticks++; /* apply new input */ { static PERFORMACE_INFO scope = {"input", 0}; int c, i; perf_start(&scope); for(c = 0; c < MAX_CLIENTS; c++) { if(clients[c].state == SRVCLIENT_STATE_EMPTY) continue; for(i = 0; i < 200; i++) { if(clients[c].inputs[i].game_tick == server_tick()) { mods_client_input(c, clients[c].inputs[i].data); break; } } } perf_end(); } /* progress game */ { static PERFORMACE_INFO scope = {"tick", 0}; perf_start(&scope); mods_tick(); perf_end(); } } /* snap game */ if(new_ticks) { if(config.sv_high_bandwidth || (current_tick%2) == 0) { static PERFORMACE_INFO scope = {"snap", 0}; perf_start(&scope); server_do_snap(); perf_end(); } } if(config.sv_sendheartbeats) { if (t > lastheartbeat+time_per_heartbeat) { server_send_heartbeat(); lastheartbeat = t+time_per_heartbeat; } } { static PERFORMACE_INFO scope = {"net", 0}; perf_start(&scope); server_pump_network(); perf_end(); } perf_end(); if(reporttime < time_get()) { if(config.debug) { static NETSTATS prev_stats; NETSTATS stats; netserver_stats(net, &stats); perf_next(); if(config.dbg_pref) perf_dump(&rootscope); dbg_msg("server", "send=%8d recv=%8d", (stats.send_bytes - prev_stats.send_bytes)/reportinterval, (stats.recv_bytes - prev_stats.recv_bytes)/reportinterval); prev_stats = stats; } reporttime += time_freq()*reportinterval; } if(config.dbg_hitch) { thread_sleep(config.dbg_hitch); config.dbg_hitch = 0; } else thread_sleep(1); } } mods_shutdown(); map_unload(); return 0; } int main(int argc, char **argv) { dbg_msg("server", "starting..."); engine_init("Teewars", argc, argv); server_run(); return 0; }