// serverbrowser.cpp: eihrul's concurrent resolver, and server browser window management #include "pch.h" #include "cube.h" #ifdef __APPLE__ #include #endif #include "SDL_thread.h" extern bool isdedicated; struct resolverthread { SDL_Thread *thread; const char *query; int starttime; }; struct resolverresult { const char *query; ENetAddress address; }; vector resolverthreads; vector resolverqueries; vector resolverresults; SDL_mutex *resolvermutex; SDL_cond *querycond, *resultcond; #define RESOLVERTHREADS 1 #define RESOLVERLIMIT 3000 int resolverloop(void * data) { resolverthread *rt = (resolverthread *)data; SDL_LockMutex(resolvermutex); SDL_Thread *thread = rt->thread; SDL_UnlockMutex(resolvermutex); if(!thread || SDL_GetThreadID(thread) != SDL_ThreadID()) return 0; while(thread == rt->thread) { SDL_LockMutex(resolvermutex); while(resolverqueries.empty()) SDL_CondWait(querycond, resolvermutex); rt->query = resolverqueries.pop(); rt->starttime = totalmillis; SDL_UnlockMutex(resolvermutex); ENetAddress address = { ENET_HOST_ANY, ENET_PORT_ANY }; enet_address_set_host(&address, rt->query); SDL_LockMutex(resolvermutex); if(rt->query && thread == rt->thread) { resolverresult &rr = resolverresults.add(); rr.query = rt->query; rr.address = address; rt->query = NULL; rt->starttime = 0; SDL_CondSignal(resultcond); } SDL_UnlockMutex(resolvermutex); } return 0; } void resolverinit() { resolvermutex = SDL_CreateMutex(); querycond = SDL_CreateCond(); resultcond = SDL_CreateCond(); SDL_LockMutex(resolvermutex); loopi(RESOLVERTHREADS) { resolverthread &rt = resolverthreads.add(); rt.query = NULL; rt.starttime = 0; rt.thread = SDL_CreateThread(resolverloop, &rt); } SDL_UnlockMutex(resolvermutex); } void resolverstop(resolverthread &rt) { SDL_LockMutex(resolvermutex); if(rt.query) { #ifndef __APPLE__ SDL_KillThread(rt.thread); #endif rt.thread = SDL_CreateThread(resolverloop, &rt); } rt.query = NULL; rt.starttime = 0; SDL_UnlockMutex(resolvermutex); } void resolverclear() { if(resolverthreads.empty()) return; SDL_LockMutex(resolvermutex); resolverqueries.setsize(0); resolverresults.setsize(0); loopv(resolverthreads) { resolverthread &rt = resolverthreads[i]; resolverstop(rt); } SDL_UnlockMutex(resolvermutex); } void resolverquery(const char *name) { if(resolverthreads.empty()) resolverinit(); SDL_LockMutex(resolvermutex); resolverqueries.add(name); SDL_CondSignal(querycond); SDL_UnlockMutex(resolvermutex); } bool resolvercheck(const char **name, ENetAddress *address) { bool resolved = false; SDL_LockMutex(resolvermutex); if(!resolverresults.empty()) { resolverresult &rr = resolverresults.pop(); *name = rr.query; address->host = rr.address.host; resolved = true; } else loopv(resolverthreads) { resolverthread &rt = resolverthreads[i]; if(rt.query && totalmillis - rt.starttime > RESOLVERLIMIT) { resolverstop(rt); *name = rt.query; resolved = true; } } SDL_UnlockMutex(resolvermutex); return resolved; } extern bool isdedicated; bool resolverwait(const char *name, ENetAddress *address) { if(isdedicated) return enet_address_set_host(address, name) >= 0; if(resolverthreads.empty()) resolverinit(); s_sprintfd(text)("resolving %s... (esc to abort)", name); show_out_of_renderloop_progress(0, text); SDL_LockMutex(resolvermutex); resolverqueries.add(name); SDL_CondSignal(querycond); int starttime = SDL_GetTicks(), timeout = 0; bool resolved = false; for(;;) { SDL_CondWaitTimeout(resultcond, resolvermutex, 250); loopv(resolverresults) if(resolverresults[i].query == name) { address->host = resolverresults[i].address.host; resolverresults.remove(i); resolved = true; break; } if(resolved) break; timeout = SDL_GetTicks() - starttime; show_out_of_renderloop_progress(min(float(timeout)/RESOLVERLIMIT, 1.0f), text); SDL_Event event; while(SDL_PollEvent(&event)) { if(event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) timeout = RESOLVERLIMIT + 1; } if(timeout > RESOLVERLIMIT) break; } if(!resolved && timeout > RESOLVERLIMIT) { loopv(resolverthreads) { resolverthread &rt = resolverthreads[i]; if(rt.query == name) { resolverstop(rt); break; } } } SDL_UnlockMutex(resolvermutex); return resolved; } SDL_Thread *connthread = NULL; SDL_mutex *connmutex = NULL; SDL_cond *conncond = NULL; struct connectdata { ENetSocket sock; ENetAddress address; int result; }; // do this in a thread to prevent timeouts // could set timeouts on sockets, but this is more reliable and gives more control int connectthread(void *data) { SDL_LockMutex(connmutex); if(!connthread || SDL_GetThreadID(connthread) != SDL_ThreadID()) { SDL_UnlockMutex(connmutex); return 0; } connectdata cd = *(connectdata *)data; SDL_UnlockMutex(connmutex); int result = enet_socket_connect(cd.sock, &cd.address); SDL_LockMutex(connmutex); if(!connthread || SDL_GetThreadID(connthread) != SDL_ThreadID()) { enet_socket_destroy(cd.sock); SDL_UnlockMutex(connmutex); return 0; } ((connectdata *)data)->result = result; SDL_CondSignal(conncond); SDL_UnlockMutex(connmutex); return 0; } #define CONNLIMIT 20000 int connectwithtimeout(ENetSocket sock, const char *hostname, ENetAddress &address) { if(isdedicated) { int result = enet_socket_connect(sock, &address); if(result<0) enet_socket_destroy(sock); return result; } s_sprintfd(text)("connecting to %s... (esc to abort)", hostname); show_out_of_renderloop_progress(0, text); if(!connmutex) connmutex = SDL_CreateMutex(); if(!conncond) conncond = SDL_CreateCond(); SDL_LockMutex(connmutex); connectdata cd = { sock, address, -1 }; connthread = SDL_CreateThread(connectthread, &cd); int starttime = SDL_GetTicks(), timeout = 0; for(;;) { if(!SDL_CondWaitTimeout(conncond, connmutex, 250)) { if(cd.result<0) enet_socket_destroy(sock); break; } timeout = SDL_GetTicks() - starttime; show_out_of_renderloop_progress(min(float(timeout)/CONNLIMIT, 1.0f), text); SDL_Event event; while(SDL_PollEvent(&event)) { if(event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) timeout = CONNLIMIT + 1; } if(timeout > CONNLIMIT) break; } /* thread will actually timeout eventually if its still trying to connect * so just leave it (and let it destroy socket) instead of causing problems on some platforms by killing it */ connthread = NULL; SDL_UnlockMutex(connmutex); return cd.result; } vector servers; ENetSocket pingsock = ENET_SOCKET_NULL; int lastinfo = 0; char *getservername(int n) { return servers[n]->name; } serverinfo *findserverinfo(ENetAddress address) { loopv(servers) if(servers[i]->address.host == address.host && servers[i]->port == address.port) return servers[i]; return NULL; } serverinfo *getconnectedserverinfo() { extern ENetPeer *curpeer; if(!curpeer) return NULL; return findserverinfo(curpeer->address); } static serverinfo *newserver(const char *name, uint ip = ENET_HOST_ANY, int port = CUBE_DEFAULT_SERVER_PORT) { serverinfo *si = new serverinfo; si->address.host = ip; si->address.port = CUBE_SERVINFO_PORT(port); if(ip!=ENET_HOST_ANY) si->resolved = serverinfo::RESOLVED; if(name) s_strcpy(si->name, name); else if(ip==ENET_HOST_ANY || enet_address_get_host_ip(&si->address, si->name, sizeof(si->name)) < 0) { delete si; return NULL; } si->port = port; servers.insert(0, si); return si; } void addserver(const char *servername, const char *serverport) { int port = atoi(serverport); if(port == 0) port = CUBE_DEFAULT_SERVER_PORT; loopv(servers) if(strcmp(servers[i]->name, servername)==0 && servers[i]->port == port) return; newserver(servername, ENET_HOST_ANY, port); } VARP(servpingrate, 1000, 5000, 60000); VARP(maxservpings, 0, 0, 1000); VAR(searchlan, 0, 1, 2); #define PINGBUFSIZE 100 static int pingbuf[PINGBUFSIZE], curpingbuf = 0; void pingservers(bool issearch, bool onlyconnected) { if(pingsock == ENET_SOCKET_NULL) { pingsock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM); if(pingsock == ENET_SOCKET_NULL) { lastinfo = totalmillis; return; } enet_socket_set_option(pingsock, ENET_SOCKOPT_NONBLOCK, 1); enet_socket_set_option(pingsock, ENET_SOCKOPT_BROADCAST, 1); } ENetBuffer buf; static uchar ping[MAXTRANS]; ucharbuf p(ping, sizeof(ping)); curpingbuf = (curpingbuf + 1) % PINGBUFSIZE; pingbuf[(curpingbuf + PINGBUFSIZE / 2) % PINGBUFSIZE] = 0; pingbuf[curpingbuf] = totalmillis; putint(p, curpingbuf + 1); // offset by 1 to avoid extinfo trigger int baselen = p.length(); if(onlyconnected) { serverinfo *si = getconnectedserverinfo(); if(si) { //p.len = baselen; putint(p, si->getnames || issearch ? EXTPING_NAMELIST : EXTPING_NOP); buf.data = ping; buf.dataLength = p.length(); enet_socket_send(pingsock, &si->address, &buf, 1); } } else if(searchlan < 2) { static int lastping = 0; if(lastping >= servers.length()) lastping = 0; loopi(maxservpings ? min(servers.length(), maxservpings) : servers.length()) { serverinfo &si = *servers[lastping]; if(++lastping >= servers.length()) lastping = 0; if(si.address.host == ENET_HOST_ANY) continue; p.len = baselen; putint(p, si.getnames || issearch ? EXTPING_NAMELIST : EXTPING_NOP); buf.data = ping; buf.dataLength = p.length(); enet_socket_send(pingsock, &si.address, &buf, 1); } } if(searchlan && !onlyconnected) { ENetAddress address; address.host = ENET_HOST_BROADCAST; address.port = CUBE_SERVINFO_PORT_LAN; p.len = baselen; putint(p, issearch ? EXTPING_NAMELIST : EXTPING_NOP); buf.data = ping; buf.dataLength = p.length(); enet_socket_send(pingsock, &address, &buf, 1); } lastinfo = totalmillis; } void checkresolver() { int resolving = 0; loopv(servers) { serverinfo &si = *servers[i]; if(si.resolved == serverinfo::RESOLVED) continue; if(si.address.host == ENET_HOST_ANY) { if(si.resolved == serverinfo::UNRESOLVED) { si.resolved = serverinfo::RESOLVING; resolverquery(si.name); } resolving++; } } if(!resolving) return; const char *name = NULL; ENetAddress addr = { ENET_HOST_ANY, ENET_PORT_ANY }; while(resolvercheck(&name, &addr)) { loopv(servers) { serverinfo &si = *servers[i]; if(name == si.name) { si.resolved = serverinfo::RESOLVED; si.address.host = addr.host; addr.host = ENET_HOST_ANY; break; } } } } void checkpings() { if(pingsock == ENET_SOCKET_NULL) return; enet_uint32 events = ENET_SOCKET_WAIT_RECEIVE; ENetBuffer buf; ENetAddress addr; static uchar ping[MAXTRANS]; static char text[MAXTRANS]; buf.data = ping; buf.dataLength = sizeof(ping); while(enet_socket_wait(pingsock, &events, 0) >= 0 && events) { int len = enet_socket_receive(pingsock, &addr, &buf, 1); if(len <= 0) return; serverinfo *si = NULL; loopv(servers) if(addr.host == servers[i]->address.host && addr.port == servers[i]->address.port) { si = servers[i]; break; } if(!si && searchlan) si = newserver(NULL, addr.host, CUBE_SERVINFO_TO_SERV_PORT(addr.port)); if(!si) continue; ucharbuf p(ping, len); si->lastpingmillis = totalmillis; int pingtm = pingbuf[(getint(p) - 1) % PINGBUFSIZE]; si->ping = pingtm ? totalmillis - pingtm : 9997; int query = getint(p); si->protocol = getint(p); if(si->protocol!=PROTOCOL_VERSION) si->ping = 9998; si->mode = getint(p); si->numplayers = getint(p); si->minremain = getint(p); getstring(text, p); filtertext(si->map, text, 1); getstring(text, p); filterservdesc(si->sdesc, text); s_strcpy(si->description, si->sdesc); si->maxclients = getint(p); if(p.remaining()) { si->pongflags = getint(p); if(p.remaining() && getint(p) == query) { switch(query) { case EXTPING_NAMELIST: { si->playernames.setsizenodelete(0); ucharbuf q(si->namedata, sizeof(si->namedata)); loopi(si->numplayers) { getstring(text, p); filtertext(text, text, 0); if(text[0] && !p.overread()) { si->playernames.add((const char *)si->namedata + q.length()); sendstring(text, q); } else break; } break; } } } } else { si->pongflags = 0; } if(si->pongflags > 0) { const char *sp = ""; int mm = si->pongflags >> PONGFLAG_MASTERMODE; if(si->pongflags & (1 << PONGFLAG_BANNED)) sp = "you are banned from this server"; if(si->pongflags & (1 << PONGFLAG_BLACKLIST)) sp = "you are blacklisted on this server"; else if(si->pongflags & (1 << PONGFLAG_PASSWORD)) sp = "this server is password-protected"; else if(mm) sp = mmfullname(mm); s_sprintf(si->description)("%s \f1(%s)", si->sdesc, sp); } } } enum { SBS_PING = 0, SBS_NUMPL, SBS_MAXPL, SBS_MINREM, SBS_MAP, SBS_MODE, SBS_IP, SBS_DESC, NUMSERVSORT }; VARP(serversort, 0, 0, NUMSERVSORT-1); VARP(serversortdir, 0, 0, 1); VARP(showonlygoodservers, 0, 0, 1); VARP(shownamesinbrowser, 0, 0, 1); VARP(showminremain, 0, 0, 1); int sicompare(serverinfo **ap, serverinfo **bp) { serverinfo *a = *ap, *b = *bp; int dir = serversortdir ? -1 : 1; if((a->protocol==PROTOCOL_VERSION) > (b->protocol==PROTOCOL_VERSION)) return -dir; if((b->protocol==PROTOCOL_VERSION) > (a->protocol==PROTOCOL_VERSION)) return dir; if(!a->numplayers && b->numplayers) return dir; if(a->numplayers && !b->numplayers) return -dir; enet_uint32 ai = ntohl(a->address.host), bi = ntohl(b->address.host); int ips = ai < bi ? -dir : (ai > bi ? dir : 0); // most steady base sorting switch(serversort) { case SBS_NUMPL: // player number if(a->numplayers < b->numplayers) return dir; if(a->numplayers > b->numplayers) return -dir; break; case SBS_MAXPL: // maxplayers if(a->maxclients < b->maxclients) return dir; if(a->maxclients > b->maxclients) return -dir; break; case SBS_MINREM: // minutes remaining if(a->minremain < b->minremain) return dir; if(a->minremain > b->minremain) return -dir; break; case SBS_DESC: // description { static string ad, bd; filtertext(ad, a->sdesc); filtertext(bd, b->sdesc); if(!ad[0] && bd[0]) return dir; if(ad[0] && !bd[0]) return -dir; int mdir = dir * strcasecmp(ad, bd); if(mdir) return mdir; break; } case SBS_IP: // IP if(ips) return ips; break; case SBS_MAP: // map { int mdir = dir * strcasecmp(a->map, b->map); if(mdir) return mdir; break; } case SBS_MODE: // mode { const char *am = modestr(a->mode, modeacronyms > 0), *bm = modestr(b->mode, modeacronyms > 0); int mdir = dir * strcasecmp(am, bm); if(mdir) return mdir; break; } } if(serversort == SBS_PING || (!a->numplayers && !b->numplayers)) // ping { if(a->ping > b->ping) return dir; if(a->ping < b->ping) return -dir; } if(ips) return ips; if(a->port > b->port) return dir; else return -dir; } void *servmenu = NULL, *searchmenu = NULL; vector namelists; string cursearch, cursearchuc; void searchnickname(const char *name) { if(!name || !name[0]) return; s_strcpy(cursearch, name); s_strcpy(cursearchuc, name); for(char *t = cursearchuc; *t; t++) *t = toupper(*t); showmenu("search"); } COMMAND(searchnickname, ARG_1STR); VAR(showallservers, 0, 1, 1); bool matchplayername(const char *name) { static string nameuc; s_strcpy(nameuc, name); for(char *t = nameuc; *t; t++) *t = toupper(*t); return strstr(nameuc, cursearchuc) != NULL; } void refreshservers(void *menu, bool init) { static int servermenumillis; static bool usedselect = false; static string title; bool issearch = menu == searchmenu; serverinfo *curserver = getconnectedserverinfo(), *oldsel = NULL; if(init) { loopi(PINGBUFSIZE) if(pingbuf[i] < totalmillis) pingbuf[i] = 0; if(resolverthreads.empty()) resolverinit(); else resolverclear(); loopv(servers) resolverquery(servers[i]->name); servermenumillis = totalmillis; usedselect = false; if(menu && curserver) oldsel = curserver; } checkresolver(); checkpings(); if((init && issearch) || totalmillis - lastinfo >= (servpingrate * (issearch ? 2 : 1))/(maxservpings ? (servers.length() + maxservpings - 1) / maxservpings : 1)) pingservers(issearch, menu == NULL); if(!init && menu && servers.inrange(((gmenu *)menu)->menusel) && (usedselect || ((gmenu *)menu)->menusel > 0)) { loopv(servers) if(servers[i]->menuline == ((gmenu *)menu)->menusel) oldsel = servers[i]; } servers.sort(sicompare); if(menu) { static const char *titles[NUMSERVSORT] = { "\fs\f0ping\fr\tplr\tserver%s%s", // 0: ping "ping\t\fs\f0plr\fr\tserver%s%s", // 1: player number "ping\tplr\tserver (\fs\f0max players\fr)%s%s", // 2: maxplayers "ping\tplr\fs\f0\fr\tserver (\fs\f0minutes remaining\fr)%s%s", // 3: minutes remaining "ping\tplr\tserver (\fs\f0map\fr)%s%s", // 4: map "ping\tplr\tserver (\fs\f0game mode\fr)%s%s", // 5: mode "ping\tplr\tserver (\fs\f0IP\fr)%s%s", // 6: IP "ping\tplr\tserver (\fs\f0description\fr)%s%s" // 7: description }; bool showmr = showminremain || serversort == SBS_MINREM; s_sprintf(title)(titles[serversort], issearch ? " search results for \f3" : " (F1: Help)", issearch ? cursearch : ""); menutitle(menu, title); menureset(menu); string text; int curnl = 0, showedservers = 0; bool sbconnectexists = identexists("sbconnect"); loopv(servers) { serverinfo &si = *servers[i]; if(!showallservers && si.lastpingmillis < servermenumillis) continue; // no pong yet int banned = ((si.pongflags >> PONGFLAG_BANNED) & 1) | ((si.pongflags >> (PONGFLAG_BLACKLIST - 1)) & 2); bool showthisone = !(banned && showonlygoodservers); bool serverfull = si.numplayers >= si.maxclients; bool needspasswd = (si.pongflags & (1 << PONGFLAG_PASSWORD)) > 0; bool isprivate = (si.pongflags >> PONGFLAG_MASTERMODE) > 0; char basecolor = banned ? '4' : (curserver == servers[i] ? '1' : '5'); char plnumcolor = serverfull ? '2' : (needspasswd ? '3' : (isprivate ? '1' : basecolor)); if(si.address.host != ENET_HOST_ANY && si.ping != 9999) { if(si.protocol!=PROTOCOL_VERSION) { if(!showonlygoodservers) s_sprintf(si.full)("%s:%d [%s protocol]", si.name, si.port, si.protocol 0), text, si.name, si.port, si.sdesc); else s_sprintf(si.full)("\fs\f%c%d\t\fs\f%c%d/%d\fr\tempty: %s:%d\fr %s", basecolor, si.ping, plnumcolor, si.numplayers, si.maxclients, si.name, si.port, si.sdesc); } } else { if(!showonlygoodservers) s_sprintf(si.full)(si.address.host != ENET_HOST_ANY ? "%s:%d [waiting for server response]" : "%s:%d [unknown host]", si.name, si.port); else showthisone = false; } if(issearch && showthisone) { bool found = false; loopvj(si.playernames) if(matchplayername(si.playernames[j])) { found = true; break; }; if(!found) showthisone = false; } if(showthisone) { si.full[88] = 0; // cut off too long server descriptions si.description[75] = 0; if(sbconnectexists) { filtertext(text, si.sdesc); for(char *p = text; (p = strchr(p, '\"')); *p++ = ' '); text[30] = '\0'; s_sprintf(si.cmd)("sbconnect %s %d %d %d %d %d \"%s\"", si.name, si.port, serverfull ?1:0, needspasswd ?1:0, isprivate ?1:0, banned, text); } else s_sprintf(si.cmd)("connect %s %d", si.name, si.port); menumanual(menu, si.full, si.cmd, NULL, si.description); if(!issearch && servers[i] == oldsel) { ((gmenu *)menu)->menusel = showedservers; usedselect = true; si.getnames = shownamesinbrowser ? 1 : 0; } si.menuline = showedservers++; if((shownamesinbrowser && servers[i] == oldsel && si.playernames.length()) || issearch) { int cur = 0; char *t = NULL; loopvj(si.playernames) { if(cur == 0) { if(namelists.length() < ++curnl) namelists.add(newstringbuf()); t = namelists[curnl - 1]; s_strcpy(t, "\t"); } if(!issearch || matchplayername(si.playernames[j])) { s_strcat(t, " \t"); s_strcat(t, si.playernames[j]); cur++; }; if(cur == 4) { menumanual(menu, t, NULL, NULL, NULL); cur = 0; } } if(cur) menumanual(menu, t, NULL, NULL, NULL); } } else si.menuline = -1; } if(issearch && curnl == 0) { static string notfoundmsg; s_sprintf(notfoundmsg)("\t\tpattern \fs\f3%s\fr not found.", cursearch); menumanual(menu, notfoundmsg, NULL, NULL, NULL); } } } bool serverskey(void *menu, int code, bool isdown, int unicode) { if(!isdown) return false; switch(code) { case SDLK_LEFT: serversort = (serversort+NUMSERVSORT-1) % NUMSERVSORT; return true; case SDLK_RIGHT: serversort = (serversort+1) % NUMSERVSORT; return true; case SDLK_F5: updatefrommaster(1); return true; case SDLK_F6: serversortdir = serversortdir ? 0 : 1; return true; } if(menu == searchmenu) return false; switch(code) { case SDLK_F1: showmenu("serverbrowser help"); return true; case SDLK_F2: shownamesinbrowser = shownamesinbrowser ? 0 : 1; return true; case SDLK_F3: showmenu("search player"); return true; case SDLK_F7: showonlygoodservers = showonlygoodservers ? 0 : 1; return true; case SDLK_F8: showminremain = showminremain ? 0 : 1; return true; } return false; } void clearservers() { resolverclear(); servers.deletecontentsp(); } VARP(masterupdatefrequency, 1, 60*60, 24*60*60); void updatefrommaster(int force) { static int lastupdate = 0; if(!force && lastupdate && totalmillis-lastupdate") || strstr((char *)reply, "")) conoutf("master server not replying"); else { // preserve currently connected server from deletion serverinfo *curserver = getconnectedserverinfo(); string curname, curport; if(curserver) { s_strcpy(curname, curserver->name); s_sprintf(curport)("%d", curserver->port); } clearservers(); execute((char *)reply); if(curserver) addserver(curname, curport); lastupdate = totalmillis; } } COMMAND(addserver, ARG_2STR); COMMAND(clearservers, ARG_NONE); COMMAND(updatefrommaster, ARG_1INT); void writeservercfg() { FILE *f = openfile(path("config/servers.cfg", true), "w"); if(!f) return; fprintf(f, "// servers connected to are added here automatically\n\n"); loopvrev(servers) fprintf(f, "addserver %s %d\n", servers[i]->name, servers[i]->port); fclose(f); }