diff options
Diffstat (limited to 'src/engine/server/es_server.c')
| -rw-r--r-- | src/engine/server/es_server.c | 901 |
1 files changed, 901 insertions, 0 deletions
diff --git a/src/engine/server/es_server.c b/src/engine/server/es_server.c new file mode 100644 index 00000000..00100b10 --- /dev/null +++ b/src/engine/server/es_server.c @@ -0,0 +1,901 @@ +/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */ +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +#include <engine/e_system.h> +#include <engine/e_config.h> +#include <engine/e_engine.h> +#include <engine/e_interface.h> + +#include <engine/e_protocol.h> +#include <engine/e_snapshot.h> + +#include <engine/e_compression.h> + +#include <engine/e_network.h> +#include <engine/e_config.h> +#include <engine/e_packer.h> +#include <engine/e_datafile.h> + +#include <mastersrv/mastersrv.h> + +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 +}; + +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 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]; +} 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_setbrowseinfo(int game_type, int progression) +{ + browseinfo_gametype = game_type; + browseinfo_progression = progression; +} + +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; + mods_presnap(); + + for(i = 0; i < MAX_CLIENTS; i++) + { + if(clients[i].state == SRVCLIENT_STATE_INGAME) + { + 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; + + snapbuild_init(&builder); + mods_snap(i); + + /* 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; + } + + 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 */ + deltasize = snapshot_create_delta(deltashot, (SNAPSHOT*)data, deltadata); + + if(deltasize) + { + /* compress it */ + unsigned char intdata[MAX_SNAPSHOT_SIZE]; + int intsize = intpack_compress(deltadata, deltasize, intdata); + int snapshot_size = zerobit_compress(intdata, intsize, compdata); + const int max_size = MAX_SNAPSHOT_PACKSIZE; + int numpackets = (snapshot_size+max_size-1)/max_size; + int n, left; + + 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); + } + } + } + + 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; + 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(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()) + { + /* TODO: how should we handle this */ + dbg_msg("server", "input got in late for=%d cur=%d", 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 */ + packer_add_string(&p, "0", 6); /* 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; + + int64 simulationtime = 0; + int64 snaptime = 0; + int64 networktime = 0; + int64 totaltime = 0; + + lastheartbeat = 0; + game_start_time = time_get(); + + if(config.debug) + dbg_msg("server", "baseline memory usage %dk", mem_allocated()/1024); + + while(1) + { + int64 t = time_get(); + /* 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; + 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); + } + } + + if(t > server_tick_start_time(current_tick+1)) + { + current_tick++; + + /* apply new input */ + { + int c, i; + 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; + } + } + } + } + + /* progress game */ + { + int64 start = time_get(); + mods_tick(); + simulationtime += time_get()-start; + } + + /* snap game */ + if(config.sv_bandwidth_mode == 0 || + (config.sv_bandwidth_mode == 1 && current_tick%2) || + (config.sv_bandwidth_mode == 2 && (current_tick%3) == 0 )) + /* if(current_tick&1) */ + { + int64 start = time_get(); + server_do_snap(); + snaptime += time_get()-start; + } + } + + if(config.sv_sendheartbeats) + { + if (t > lastheartbeat+time_per_heartbeat) + { + server_send_heartbeat(); + lastheartbeat = t+time_per_heartbeat; + } + } + + { + int64 start = time_get(); + server_pump_network(); + networktime += time_get()-start; + } + + if(reporttime < time_get()) + { + if(config.debug) + { + static NETSTATS prev_stats; + NETSTATS stats; + netserver_stats(net, &stats); + dbg_msg("server", "sim=%.02fms snap=%.02fms net=%.02fms tot=%.02fms load=%.02f%%", + (simulationtime/reportinterval)/(double)time_freq()*1000, + (snaptime/reportinterval)/(double)time_freq()*1000, + (networktime/reportinterval)/(double)time_freq()*1000, + (totaltime/reportinterval)/(double)time_freq()*1000, + (totaltime)/reportinterval/(double)time_freq()*100.0f); + + 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; + } + + simulationtime = 0; + snaptime = 0; + networktime = 0; + totaltime = 0; + + reporttime += time_freq()*reportinterval; + } + totaltime += time_get()-t; + 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; +} + |