// server.cpp: little more than enhanced multicaster // runs dedicated or as client coroutine #include "cube.h" #import "Client.h" #import "Entity.h" #import "ServerEntity.h" enum { ST_EMPTY, ST_LOCAL, ST_TCPIP }; static OFMutableArray *clients; int maxclients = 8; static OFString *smapname; static OFMutableArray *sents; // true when map has changed and waiting for clients to send item bool notgotitems = true; int mode = 0; // hack: called from savegame code, only works in SP void restoreserverstate(OFArray *ents) { [sents enumerateObjectsUsingBlock: ^ (ServerEntity *e, size_t i, bool *stop) { e.spawned = ents[i].spawned; e.spawnsecs = 0; }]; } int interm = 0, minremain = 0, mapend = 0; bool mapreload = false; static OFString *serverpassword = @""; bool isdedicated; ENetHost *serverhost = NULL; int bsend = 0, brec = 0, laststatus = 0, lastsec = 0; #define MAXOBUF 100000 void process(ENetPacket *packet, int sender); void multicast(ENetPacket *packet, int sender); void disconnect_client(int n, OFString *reason); static void send_(int n, ENetPacket *packet) { if (!packet) return; switch (clients[n].type) { case ST_TCPIP: enet_peer_send(clients[n].peer, 0, packet); bsend += packet->dataLength; break; case ST_LOCAL: localservertoclient(packet->data, packet->dataLength); break; } } void send2(bool rel, int cn, int a, int b) { ENetPacket *packet = enet_packet_create(NULL, 32, rel ? ENET_PACKET_FLAG_RELIABLE : 0); unsigned char *start = packet->data; unsigned char *p = start + 2; putint(&p, a); putint(&p, b); *(unsigned short *)start = ENET_HOST_TO_NET_16(p - start); enet_packet_resize(packet, p - start); if (cn < 0) process(packet, -1); else send_(cn, packet); if (packet->referenceCount == 0) enet_packet_destroy(packet); } void sendservmsg(OFString *msg) { ENetPacket *packet = enet_packet_create( NULL, _MAXDEFSTR + 10, ENET_PACKET_FLAG_RELIABLE); unsigned char *start = packet->data; unsigned char *p = start + 2; putint(&p, SV_SERVMSG); sendstring(msg, &p); *(unsigned short *)start = ENET_HOST_TO_NET_16(p - start); enet_packet_resize(packet, p - start); multicast(packet, -1); if (packet->referenceCount == 0) enet_packet_destroy(packet); } void disconnect_client(int n, OFString *reason) { [OFStdOut writeFormat: @"disconnecting client (%@) [%@]\n", clients[n].hostname, reason]; enet_peer_disconnect(clients[n].peer, 0); clients[n].type = ST_EMPTY; send2(true, -1, SV_CDIS, n); } void resetitems() { [sents removeAllObjects]; notgotitems = true; } // server side item pickup, acknowledge first client that gets it static void pickup(size_t i, int sec, int sender) { if (i >= sents.count) return; if (sents[i].spawned) { sents[i].spawned = false; sents[i].spawnsecs = sec; send2(true, sender, SV_ITEMACC, i); } } void resetvotes() { for (Client *client in clients) client.mapvote = @""; } bool vote(OFString *map, int reqmode, int sender) { clients[sender].mapvote = map; clients[sender].modevote = reqmode; int yes = 0, no = 0; for (Client *client in clients) { if (client.type != ST_EMPTY) { if (client.mapvote.length > 0) { if ([client.mapvote isEqual: map] && client.modevote == reqmode) yes++; else no++; } else no++; } } if (yes == 1 && no == 0) return true; // single player OFString *msg = [OFString stringWithFormat: @"%@ suggests %@ on map %@ (set map to vote)", clients[sender].name, modestr(reqmode), map]; sendservmsg(msg); if (yes / (float)(yes + no) <= 0.5f) return false; sendservmsg(@"vote passed"); resetvotes(); return true; } // server side processing of updates: does very little and most state is tracked // client only could be extended to move more gameplay to server (at expense of // lag) void process(ENetPacket *packet, int sender) // sender may be -1 { if (ENET_NET_TO_HOST_16(*(unsigned short *)packet->data) != packet->dataLength) { disconnect_client(sender, @"packet length"); return; } unsigned char *end = packet->data + packet->dataLength; unsigned char *p = packet->data + 2; char text[MAXTRANS]; int cn = -1, type; while (p < end) { switch ((type = getint(&p))) { case SV_TEXT: sgetstr(); break; case SV_INITC2S: sgetstr(); clients[cn].name = @(text); sgetstr(); getint(&p); break; case SV_MAPCHANGE: { sgetstr(); int reqmode = getint(&p); if (reqmode < 0) reqmode = 0; if (smapname.length > 0 && !mapreload && !vote(@(text), reqmode, sender)) return; mapreload = false; mode = reqmode; minremain = mode & 1 ? 15 : 10; mapend = lastsec + minremain * 60; interm = 0; smapname = @(text); resetitems(); sender = -1; break; } case SV_ITEMLIST: { int n; while ((n = getint(&p)) != -1) if (notgotitems) { while (sents.count <= n) [sents addObject: [ServerEntity entity]]; sents[n].spawned = true; } notgotitems = false; break; } case SV_ITEMPICKUP: { int n = getint(&p); pickup(n, getint(&p), sender); break; } case SV_PING: send2(false, cn, SV_PONG, getint(&p)); break; case SV_POS: { cn = getint(&p); if (cn < 0 || cn >= clients.count || clients[cn].type == ST_EMPTY) { disconnect_client(sender, @"client num"); return; } int size = msgsizelookup(type); assert(size != -1); for (int i = 0; i < size - 2; i++) getint(&p); break; } case SV_SENDMAP: { sgetstr(); int mapsize = getint(&p); sendmaps(sender, @(text), mapsize, p); return; } case SV_RECVMAP: send_(sender, recvmap(sender)); return; // allows for new features that require no server updates case SV_EXT: for (int n = getint(&p); n; n--) getint(&p); break; default: { int size = msgsizelookup(type); if (size == -1) { disconnect_client(sender, @"tag type"); return; } for (int i = 0; i < size - 1; i++) getint(&p); } } } if (p > end) { disconnect_client(sender, @"end of packet"); return; } multicast(packet, sender); } void send_welcome(int n) { ENetPacket *packet = enet_packet_create(NULL, MAXTRANS, ENET_PACKET_FLAG_RELIABLE); unsigned char *start = packet->data; __block unsigned char *p = start + 2; putint(&p, SV_INITS2C); putint(&p, n); putint(&p, PROTOCOL_VERSION); putint(&p, *smapname.UTF8String); sendstring(serverpassword, &p); putint(&p, clients.count > maxclients); if (smapname.length > 0) { putint(&p, SV_MAPCHANGE); sendstring(smapname, &p); putint(&p, mode); putint(&p, SV_ITEMLIST); [sents enumerateObjectsUsingBlock: ^ (ServerEntity *e, size_t i, bool *stop) { if (e.spawned) putint(&p, i); }]; putint(&p, -1); } *(unsigned short *)start = ENET_HOST_TO_NET_16(p - start); enet_packet_resize(packet, p - start); send_(n, packet); } void multicast(ENetPacket *packet, int sender) { size_t count = clients.count; for (size_t i = 0; i < count; i++) if (i != sender) send_(i, packet); } void localclienttoserver(ENetPacket *packet) { process(packet, 0); if (packet->referenceCount == 0) enet_packet_destroy(packet); } Client * addclient() { for (Client *client in clients) if (client.type == ST_EMPTY) return client; Client *client = [Client client]; if (clients == nil) clients = [[OFMutableArray alloc] init]; [clients addObject: client]; return client; } void checkintermission() { if (!minremain) { interm = lastsec + 10; mapend = lastsec + 1000; } send2(true, -1, SV_TIMEUP, minremain--); } void startintermission() { minremain = 0; checkintermission(); } void resetserverifempty() { for (Client *client in clients) if (client.type != ST_EMPTY) return; [clients removeAllObjects]; smapname = @""; resetvotes(); resetitems(); mode = 0; mapreload = false; minremain = 10; mapend = lastsec + minremain * 60; interm = 0; } int nonlocalclients = 0; int lastconnect = 0; // main server update, called from cube main loop in sp, or dedicated server // loop void serverslice(int seconds, unsigned int timeout) { // spawn entities when timer reached [sents enumerateObjectsUsingBlock: ^ (ServerEntity *e, size_t i, bool *stop) { if (e.spawnsecs && (e.spawnsecs -= seconds - lastsec) <= 0) { e.spawnsecs = 0; e.spawned = true; send2(true, -1, SV_ITEMSPAWN, i); } }]; lastsec = seconds; if ((mode > 1 || (mode == 0 && nonlocalclients)) && seconds > mapend - minremain * 60) checkintermission(); if (interm && seconds > interm) { interm = 0; [clients enumerateObjectsUsingBlock: ^ (Client *client, size_t i, bool *stop) { if (client.type != ST_EMPTY) { // ask a client to trigger map reload send2(true, i, SV_MAPRELOAD, 0); mapreload = true; *stop = true; return; } }]; } resetserverifempty(); if (!isdedicated) return; // below is network only int numplayers = 0; for (Client *client in clients) if (client.type != ST_EMPTY) numplayers++; serverms(mode, numplayers, minremain, smapname, seconds, clients.count >= maxclients); // display bandwidth stats, useful for server ops if (seconds - laststatus > 60) { nonlocalclients = 0; for (Client *client in clients) if (client.type == ST_TCPIP) nonlocalclients++; laststatus = seconds; if (nonlocalclients || bsend || brec) printf("status: %d remote clients, %.1f send, %.1f rec " "(K/sec)\n", nonlocalclients, bsend / 60.0f / 1024, brec / 60.0f / 1024); bsend = brec = 0; } ENetEvent event; if (enet_host_service(serverhost, &event, timeout) > 0) { switch (event.type) { case ENET_EVENT_TYPE_CONNECT: { Client *c = addclient(); c.type = ST_TCPIP; c.peer = event.peer; c.peer->data = (void *)(clients.count - 1); char hn[1024]; c.hostname = (enet_address_get_host( &c.peer->address, hn, sizeof(hn)) == 0 ? @(hn) : @"localhost"); [OFStdOut writeFormat: @"client connected (%@)\n", c.hostname]; send_welcome(lastconnect = clients.count - 1); break; } case ENET_EVENT_TYPE_RECEIVE: brec += event.packet->dataLength; process(event.packet, (intptr_t)event.peer->data); if (event.packet->referenceCount == 0) enet_packet_destroy(event.packet); break; case ENET_EVENT_TYPE_DISCONNECT: if ((intptr_t)event.peer->data < 0) break; [OFStdOut writeFormat: @"disconnected client (%@)\n", clients[(size_t)event.peer->data].hostname]; clients[(size_t)event.peer->data].type = ST_EMPTY; send2(true, -1, SV_CDIS, (intptr_t)event.peer->data); event.peer->data = (void *)-1; break; case ENET_EVENT_TYPE_NONE: break; } if (numplayers > maxclients) disconnect_client(lastconnect, @"maxclients reached"); } #ifndef _WIN32 fflush(stdout); #endif } void cleanupserver() { if (serverhost) enet_host_destroy(serverhost); } void localdisconnect() { for (Client *client in clients) if (client.type == ST_LOCAL) client.type = ST_EMPTY; } void localconnect() { Client *c = addclient(); c.type = ST_LOCAL; c.hostname = @"local"; send_welcome(clients.count - 1); } void initserver(bool dedicated, int uprate, OFString *sdesc, OFString *ip, OFString *master, OFString *passwd, int maxcl) { serverpassword = passwd; maxclients = maxcl; sents = [[OFMutableArray alloc] init]; servermsinit(master ? master : @"wouter.fov120.com/cube/masterserver/", sdesc, dedicated); if ((isdedicated = dedicated)) { ENetAddress address = { ENET_HOST_ANY, CUBE_SERVER_PORT }; if (ip.length > 0 && enet_address_set_host(&address, ip.UTF8String) < 0) printf("WARNING: server ip not resolved"); serverhost = enet_host_create(&address, MAXCLIENTS, 0, 0, uprate); if (!serverhost) fatal(@"could not create server host\n"); for (int i = 0; i < MAXCLIENTS; i++) serverhost->peers[i].data = (void *)-1; } resetserverifempty(); // do not return, this becomes main loop if (isdedicated) { #ifdef _WIN32 SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); #endif printf("dedicated server started, waiting for " "clients...\nCtrl-C to exit\n\n"); atexit(cleanupserver); atexit(enet_deinitialize); for (;;) @autoreleasepool { serverslice( /*enet_time_get_sec()*/ time(NULL), 5); } } }