// server.cpp: little more than enhanced multicaster // runs dedicated or as client coroutine #include "cube.h" enum { ST_EMPTY, ST_LOCAL, ST_TCPIP }; struct client // server side version of "dynent" type { int type; ENetPeer *peer; string hostname; string mapvote; string name; int modevote; }; vector clients; int maxclients = 8; string smapname; struct server_entity // server side version of "entity" type { bool spawned; int spawnsecs; }; vector sents; bool notgotitems = true; // true when map has changed and waiting for clients to send item int mode = 0; void restoreserverstate( vector &ents) // hack: called from savegame code, only works in SP { loopv(sents) { sents[i].spawned = ents[i].spawned; sents[i].spawnsecs = 0; }; }; int interm = 0, minremain = 0, mapend = 0; bool mapreload = false; char *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, char *reason); 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); uchar *start = packet->data; uchar *p = start + 2; putint(p, a); putint(p, b); *(ushort *)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(char *msg) { ENetPacket *packet = enet_packet_create( NULL, _MAXDEFSTR + 10, ENET_PACKET_FLAG_RELIABLE); uchar *start = packet->data; uchar *p = start + 2; putint(p, SV_SERVMSG); sendstring(msg, p); *(ushort *)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, char *reason) { printf("disconnecting client (%s) [%s]\n", clients[n].hostname, reason); enet_peer_disconnect(clients[n].peer); clients[n].type = ST_EMPTY; send2(true, -1, SV_CDIS, n); }; void resetitems() { sents.setsize(0); notgotitems = true; }; void pickup(uint i, int sec, int sender) // server side item pickup, acknowledge // first client that gets it { if (i >= (uint)sents.length()) return; if (sents[i].spawned) { sents[i].spawned = false; sents[i].spawnsecs = sec; send2(true, sender, SV_ITEMACC, i); }; }; void resetvotes() { loopv(clients) clients[i].mapvote[0] = 0; }; bool vote(char *map, int reqmode, int sender) { strcpy_s(clients[sender].mapvote, map); clients[sender].modevote = reqmode; int yes = 0, no = 0; loopv(clients) if (clients[i].type != ST_EMPTY) { if (clients[i].mapvote[0]) { if (strcmp(clients[i].mapvote, map) == 0 && clients[i].modevote == reqmode) yes++; else no++; } else no++; }; if (yes == 1 && no == 0) return true; // single player sprintf_sd(msg)("%s suggests %s on map %s (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(*(ushort *)packet->data) != packet->dataLength) { disconnect_client(sender, "packet length"); return; }; uchar *end = packet->data + packet->dataLength; uchar *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(); strcpy_s(clients[cn].name, text); sgetstr(); getint(p); break; case SV_MAPCHANGE: { sgetstr(); int reqmode = getint(p); if (reqmode < 0) reqmode = 0; if (smapname[0] && !mapreload && !vote(text, reqmode, sender)) return; mapreload = false; mode = reqmode; minremain = mode & 1 ? 15 : 10; mapend = lastsec + minremain * 60; interm = 0; strcpy_s(smapname, text); resetitems(); sender = -1; break; }; case SV_ITEMLIST: { int n; while ((n = getint(p)) != -1) if (notgotitems) { server_entity se = {false, 0}; while (sents.length() <= n) sents.add(se); 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.length() || clients[cn].type == ST_EMPTY) { disconnect_client(sender, "client num"); return; }; int size = msgsizelookup(type); assert(size != -1); loopi(size - 2) 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; case SV_EXT: // allows for new features that require no server // updates { for (int n = getint(p); n; n--) getint(p); break; }; default: { int size = msgsizelookup(type); if (size == -1) { disconnect_client(sender, "tag type"); return; }; loopi(size - 1) 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); uchar *start = packet->data; uchar *p = start + 2; putint(p, SV_INITS2C); putint(p, n); putint(p, PROTOCOL_VERSION); putint(p, smapname[0]); sendstring(serverpassword, p); putint(p, clients.length() > maxclients); if (smapname[0]) { putint(p, SV_MAPCHANGE); sendstring(smapname, p); putint(p, mode); putint(p, SV_ITEMLIST); loopv(sents) if (sents[i].spawned) putint(p, i); putint(p, -1); }; *(ushort *)start = ENET_HOST_TO_NET_16(p - start); enet_packet_resize(packet, p - start); send(n, packet); }; void multicast(ENetPacket *packet, int sender) { loopv(clients) { if (i == sender) continue; send(i, packet); }; }; void localclienttoserver(ENetPacket *packet) { process(packet, 0); if (!packet->referenceCount) enet_packet_destroy(packet); }; client & addclient() { loopv(clients) if (clients[i].type == ST_EMPTY) return clients[i]; return clients.add(); }; void checkintermission() { if (!minremain) { interm = lastsec + 10; mapend = lastsec + 1000; }; send2(true, -1, SV_TIMEUP, minremain--); }; void startintermission() { minremain = 0; checkintermission(); }; void resetserverifempty() { loopv(clients) if (clients[i].type != ST_EMPTY) return; clients.setsize(0); smapname[0] = 0; resetvotes(); resetitems(); mode = 0; mapreload = false; minremain = 10; mapend = lastsec + minremain * 60; interm = 0; }; int nonlocalclients = 0; int lastconnect = 0; void serverslice(int seconds, unsigned int timeout) // main server update, called from cube main loop in // sp, or dedicated server loop { loopv(sents) // spawn entities when timer reached { if (sents[i].spawnsecs && (sents[i].spawnsecs -= seconds - lastsec) <= 0) { sents[i].spawnsecs = 0; sents[i].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; loopv(clients) if (clients[i].type != ST_EMPTY) { send2(true, i, SV_MAPRELOAD, 0); // ask a client to trigger map reload mapreload = true; break; }; }; resetserverifempty(); if (!isdedicated) return; // below is network only int numplayers = 0; loopv(clients) if (clients[i].type != ST_EMPTY)++ numplayers; serverms(mode, numplayers, minremain, smapname, seconds, clients.length() >= maxclients); if (seconds - laststatus > 60) // display bandwidth stats, useful for server ops { nonlocalclients = 0; loopv(clients) if (clients[i].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 *)(&c - &clients[0]); char hn[1024]; strcpy_s( c.hostname, (enet_address_get_host(&c.peer->address, hn, sizeof(hn)) == 0) ? hn : "localhost"); printf("client connected (%s)\n", c.hostname); send_welcome(lastconnect = &c - &clients[0]); 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; printf("disconnected client (%s)\n", clients[(intptr_t)event.peer->data].hostname); clients[(intptr_t)event.peer->data].type = ST_EMPTY; send2(true, -1, SV_CDIS, (intptr_t)event.peer->data); event.peer->data = (void *)-1; break; }; if (numplayers > maxclients) { disconnect_client(lastconnect, "maxclients reached"); }; }; #ifndef _WIN32 fflush(stdout); #endif }; void cleanupserver() { if (serverhost) enet_host_destroy(serverhost); }; void localdisconnect() { loopv(clients) if (clients[i].type == ST_LOCAL) clients[i].type = ST_EMPTY; }; void localconnect() { client &c = addclient(); c.type = ST_LOCAL; strcpy_s(c.hostname, "local"); send_welcome(&c - &clients[0]); }; void initserver(bool dedicated, int uprate, char *sdesc, char *ip, char *master, char *passwd, int maxcl) { serverpassword = passwd; maxclients = maxcl; servermsinit(master ? master : "wouter.fov120.com/cube/masterserver/", sdesc, dedicated); if (isdedicated = dedicated) { ENetAddress address = {ENET_HOST_ANY, CUBE_SERVER_PORT}; if (*ip && enet_address_set_host(&address, ip) < 0) printf("WARNING: server ip not resolved"); serverhost = enet_host_create(&address, MAXCLIENTS, 0, uprate); if (!serverhost) fatal("could not create server host\n"); loopi(MAXCLIENTS) serverhost->peers[i].data = (void *)-1; }; resetserverifempty(); if (isdedicated) // do not return, this becomes main loop { #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 (;;) serverslice(/*enet_time_get_sec()*/ time(NULL), 5); }; };