// serverbrowser.cpp: eihrul's concurrent resolver, and server browser window management #include "cube.h" #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, "ResolverThread", &rt); } SDL_UnlockMutex(resolvermutex); } void resolverstop(resolverthread &rt) { SDL_LockMutex(resolvermutex); if(rt.query) { SDL_DetachThread(rt.thread); rt.thread = SDL_CreateThread(resolverloop, "ResolverThread", &rt); } rt.query = NULL; rt.starttime = 0; SDL_UnlockMutex(resolvermutex); } void resolverclear() { if(resolverthreads.empty()) return; SDL_LockMutex(resolvermutex); resolverqueries.shrink(0); resolverresults.shrink(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(); defformatstring(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; } defformatstring(text)("connecting to %s:%d... (esc to abort)", hostname, address.port); 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, "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; 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() { 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, int weight = 0) { serverinfo *si = new serverinfo; si->address.host = ip; si->address.port = CUBE_SERVINFO_PORT(port); si->msweight = weight; if(ip!=ENET_HOST_ANY) si->resolved = serverinfo::RESOLVED; if(name) copystring(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, int serverport, int weight) { if(serverport <= 0) serverport = CUBE_DEFAULT_SERVER_PORT; loopv(servers) if(strcmp(servers[i]->name, servername)==0 && servers[i]->port == serverport) return; newserver(servername, ENET_HOST_ANY, serverport, weight); } VARP(servpingrate, 1000, 5000, 60000); VARP(maxservpings, 0, 10, 1000); VAR(searchlan, 0, 1, 2); #define PINGBUFSIZE 100 static int pingbuf[PINGBUFSIZE], curpingbuf = 0; int chooseextping(serverinfo *si) { if(si->getinfo != EXTPING_NOP) return si->getinfo; if(!si->uplinkqual_age || totalmillis - si->uplinkqual_age > 60 * 1000) return EXTPING_UPLINKSTATS; return EXTPING_NOP; } void pingservers(bool issearch, serverinfo *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] = onlyconnected ? 0 : totalmillis; putint(p, curpingbuf + 1); // offset by 1 to avoid extinfo trigger int baselen = p.length(); if(onlyconnected) { serverinfo *si = onlyconnected; //p.len = baselen; if(si->getinfo == EXTPING_SERVERINFO) { putint(p, EXTPING_SERVERINFO); const char *silang; if(strlen(lang) != 2) silang = "en"; else silang = lang; loopi(2) putint(p, silang[i]); } else putint(p, issearch ? EXTPING_NAMELIST : chooseextping(si)); 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, issearch ? EXTPING_NAMELIST : chooseextping(&si)); 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_UPLINKSTATS); 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; } } } } #define MAXINFOLINELEN 100 // including color codes 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]; if(pingtm) si->ping = totalmillis - pingtm; int query = getint(p); switch(query) { // cleanup additional query info case EXTPING_SERVERINFO: loopi(2) getint(p); break; } 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, behindpath(text), FTXT__MAPNAME); getstring(text, p); filtertext(si->sdesc, text, FTXT__SERVDESC); copystring(si->description, si->sdesc); si->maxclients = getint(p); if(p.remaining()) { si->pongflags = getint(p); if(p.remaining() && getint(p) == query) { switch(query) { #define RESETINFOLINES() si->infotexts.setsize(0); \ ucharbuf q(si->textdata, sizeof(si->textdata)) #define ADDINFOLINE(msg) si->infotexts.add((char *)si->textdata + q.length()); \ sendstring(msg, q) case EXTPING_NAMELIST: { si->playernames.setsize(0); ucharbuf q(si->namedata, sizeof(si->namedata)); loopi(si->numplayers) { getstring(text, p); filtertext(text, text, FTXT__PLAYERNAME); if(text[0] && !p.overread()) { si->playernames.add((const char *)si->namedata + q.length()); sendstring(text, q); } else break; } break; } case EXTPING_SERVERINFO: { RESETINFOLINES(); getstring(text, p); if(strlen(text) != 2) { ADDINFOLINE("this server does not provide additional information"); break; } strcpy(si->lang, text); while(p.remaining()) { getstring(text, p); if(*text && !p.overread()) { text[MAXINFOLINELEN] = '\0'; cutcolorstring(text, 100); ADDINFOLINE(strcmp(text, ".") ? text : ""); } else break; } break; } case EXTPING_MAPROT: { RESETINFOLINES(); int n = getint(p); ADDINFOLINE("\f1server map rotation:"); ADDINFOLINE(""); while(p.remaining()) { getstring(text, p); filtertext(text, behindpath(text), FTXT__MAPNAME); if(*text && !p.overread()) { text[MAXINFOLINELEN] = '\0'; loopi(n) concatformatstring(text, ", %d", getint(p)); ADDINFOLINE(text ); } else break; } break; } case EXTPING_UPLINKSTATS: { si->uplinkqual_age = totalmillis; if(si->maxclients > 3) { int maxs = 0, maxc = 0, ts, tc; loopi(si->maxclients - 3) { ts = tc = si->uplinkstats[i + 4] = p.get(); if(si->maxclients < 8 || i > 2) { ts &= 0xF0; tc &= 0x0F; if(ts > maxs) maxs = ts; if(ts > 0x40 && tc > maxc) maxc = tc; // spent time = 2 ^ ((ts >> 4) - 1) * 30 sec, so 0x50 is 8..15 minutes } } if(maxs < 0x90) maxc -= 2; // go easy on fresh started servers if(maxs < 0x50) si->uplinkqual = 3; else if(maxc < 3) si->uplinkqual = 5; // choke_percentage = 1 / 2 ^ (15 - tc), so 0x03 is 0.02% else if(maxc < 6) si->uplinkqual = 4; // 0.2% else if(maxc == 6) si->uplinkqual = 3; else if(maxc < 9) si->uplinkqual = 2; // 1.6% (yep, that IS bad) else si->uplinkqual = 1; } if(si->getinfo == EXTPING_UPLINKSTATS) { RESETINFOLINES(); if(si->maxclients < 4) { ADDINFOLINE("server is too small for uplink quality statistics"); } else { ADDINFOLINE("\f1server uplink quality and usage statistics:"); ADDINFOLINE(""); ADDINFOLINE("players:\terrors/time"); for(int i = 4; i <= si->maxclients; i++) { defformatstring(msg)(" %d\t", i); loopj(15) concatformatstring(msg, "\a%c ", '0' + ((si->uplinkstats[i] & 0x0F) > j) + 2 * ((si->uplinkstats[i] & 0xF0) > (j << 4))); concatformatstring(msg, "\t\t [%02X]", si->uplinkstats[i]); ADDINFOLINE(msg); } ADDINFOLINE(""); ADDINFOLINE("\f4red bar: error rate, green bar: time played"); } } break; } #undef RESETINFOLINES #undef ADDINFOLINE } } } else { si->pongflags = 0; } if(si->getinfo == query) si->getinfo = EXTPING_NOP; 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"; else if(si->pongflags & (1 << PONGFLAG_BLACKLIST)) sp = "you are blacklisted on this server"; else if(mm) sp = mmfullname(mm); formatstring(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); VAR(shownamesinbrowser, 0, 0, 1); VARP(showminremain, 0, 1, 1); VARP(showallplayersnumber, 0, 1, 1); VARP(serversortpreferofficial, 0, 1, 1); void serversortprepare() { loopv(servers) { serverinfo &si = *servers[i]; // basic group weights: used(700) - empty(500) - unusable(200) if(si.protocol != PROTOCOL_VERSION) si.weight += 200; else if(!si.numplayers) si.weight += 500; else { si.weight += 700; if(serversortpreferofficial && securemapcheck(si.map, false)) si.weight += 100; } si.weight += si.msweight; } } int sicompare(serverinfo **ap, serverinfo **bp) { serverinfo *a = *ap, *b = *bp; int dir = serversortdir ? -1 : 1; if(a->weight > b->weight) return -dir; if(a->weight < b->weight) return dir; enet_uint32 ai = ENET_NET_TO_HOST_32(a->address.host), bi = ENET_NET_TO_HOST_32(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, FTXT__SERVDESC); filtertext(bd, b->sdesc, FTXT__SERVDESC); 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, *serverinfomenu = NULL; vector namelists; string cursearch, cursearchuc; void searchnickname(const char *name) { if(!name || !name[0]) return; copystring(cursearch, name); copystring(cursearchuc, name); strtoupper(cursearchuc); showmenu("search"); } COMMAND(searchnickname, "s"); VAR(showallservers, 0, 1, 1); bool matchplayername(const char *name) { static string nameuc; copystring(nameuc, name); strtoupper(nameuc); return strstr(nameuc, cursearchuc) != NULL; } VARP(serverbrowserhideip, 0, 0, 2); VARP(serverbrowserhidefavtag, 0, 1, 2); VAR(showweights, 0, 0, 1); VARP(hidefavicons, 0, 0, 1); vector favcats; const char *fc_als[] = { "weight", "tag", "desc", "red", "green", "blue", "alpha", "keys", "ignore", "image" }; enum { FC_WEIGHT = 0, FC_TAG, FC_DESC, FC_RED, FC_GREEN, FC_BLUE, FC_ALPHA, FC_KEYS, FC_IGNORE, FC_IMAGE, FC_NUM }; VARF(showonlyfavourites, 0, 0, 100, { if(showonlyfavourites > favcats.length()) { conoutf("showonlyfavourites: %d out of range (0..%d)", showonlyfavourites, favcats.length()); showonlyfavourites = 0; } }); const char *favcatargname(const char *refdes, int par) { static string text[3]; static int i = 0; if(par < 0 || par >= FC_NUM) return NULL; i = (i + 1) % 3; formatstring(text[i])("sbfavourite_%s_%s", refdes, fc_als[par]); return text[i]; } void addfavcategory(const char *refdes) { string text, val; char alx[FC_NUM]; if(!refdes) { intret(0); return; } filtertext(text, refdes, FTXT__FAVCATEGORY); if(!text[0]) { intret(0); return; } loopv(favcats) if(!strcmp(favcats[i], text)) { intret(i + 1); return; } favcats.add(newstring(text)); bool oldpersist = persistidents; persistidents = false; // only keep changed values loopi(FC_NUM) alx[i] = getalias(favcatargname(text, i)) ? 1 : 0; loopi(3) { formatstring(val)("%d", *text & (1 << i) ? 90 : 10); if(!alx[i + FC_RED]) alias(favcatargname(text, i + FC_RED), val); } formatstring(val)("favourites %d", favcats.length()); const int defk[] = { FC_WEIGHT, FC_DESC, FC_TAG, FC_KEYS, FC_IGNORE, FC_ALPHA }; const char *defv[] = { "0", val, refdes, "", "", "20" }; loopi(sizeof(defk)/sizeof(defk[0])) { if(!alx[defk[i]]) alias(favcatargname(text, defk[i]), defv[i]); } persistidents = oldpersist; intret(favcats.length()); } void listfavcats() { const char *str = conc((const char **)favcats.getbuf(), favcats.length(), true); result(str); delete [] str; } bool favcatcheckkey(serverinfo &si, const char *key) { // IP:port, #gamemode ,%mapname, desc string text, keyuc; if(isdigit(*key)) // IP { formatstring(text)("%s:%d", si.name, si.port); return !strncmp(text, key, strlen(key)); } else if(si.address.host != ENET_HOST_ANY && si.ping != 9999) switch(*key) { case '#': return si.map[0] && si.mode == atoi(key + 1); case '%': strtoupper(text, si.map); strtoupper(keyuc, key + 1); return si.map[0] && key[1] && strstr(text, keyuc); case '>': return si.ping > atoi(key + 1); case '!': return !favcatcheckkey(si, key + 1); case '+': return si.map[0] && favcatcheckkey(si, key + 1); case '$': if(key[1]) { formatstring(text)("%s \"%s\" %d %d %d %d %d \"%s\" %d %d", key + 1, si.map, si.mode, si.ping, si.minremain, si.numplayers, si.maxclients, si.name, si.port, si.pongflags); filtertext(text, text, FTXT_NOCOLOR|FTXT_NOWHITE|FTXT_ALLOWBLANKS); int cnt = 0; for(const char *p = text; (p = strchr(p, '\"')); p++) cnt++; return cnt == 4 && execute(text); } break; default: filtertext(text, si.sdesc, FTXT_NOCOLOR|FTXT_NOWHITE|FTXT_ALLOWBLANKS|(showonlyfavourites > 0 ? FTXT_TOUPPER : 0)); strtoupper(keyuc, key); return *key && strstr(text, showonlyfavourites > 0 ? keyuc : key); } return false; } const char *favcatcheck(serverinfo &si, const char *ckeys, char *autokeys = NULL) { if(!ckeys) return NULL; static char *nkeys = NULL; const char *sep = " \t\n\r"; char *keys = newstring(ckeys), *b, *k = strtok_r(keys, sep, &b); bool res = false; DELETEA(nkeys); nkeys = newstring(strlen(ckeys)); nkeys[0] = '\0'; while(k) { if(favcatcheckkey(si, k)) { res = true; if(autokeys && !(isdigit(*k) && strchr(k, ':'))) concatformatstring(autokeys, *autokeys ? " %s" : "%s", k); } else { if(*nkeys) strcat(nkeys, " "); strcat(nkeys, k); } k = strtok_r(NULL, sep, &b); } delete[] keys; return res ? nkeys : NULL; } void serverbrowseralternativeviews(int shiftdirection) { if(searchlan == 2) return; const char *ckeys = getalias("serverbrowseraltviews"); if(!ckeys) return; const char *sep = " \t\n\r"; char *keys = newstring(ckeys), *b, *k = strtok_r(keys, sep, &b); vector cats; cats.add(0); while(k) { loopv(favcats) if(!strcmp(favcats[i], k)) cats.add(i + 1); k = strtok_r(NULL, sep, &b); } delete[] keys; if(cats.length()) { loopv(cats) { if(cats[i] == showonlyfavourites) { showonlyfavourites = cats[(i + shiftdirection + cats.length()) % cats.length()]; return; } } } showonlyfavourites = 0; } vector favcattags; bool assignserverfavourites() { int alxn[FC_NUM]; const char *alx[FC_NUM], *sep = " \t\n\r"; favcattags.setsize(0); bool res = false; loopv(servers) { servers[i]->favcat = -1; servers[i]->weight = 0; } loopvj(favcats) { loopi(FC_NUM) { alx[i] = getalias(favcatargname(favcats[j], i)); alxn[i] = alx[i] ? atoi(alx[i]) : 0; } favcattags.add(alx[FC_TAG]); bool showonlythiscat = j == showonlyfavourites - 1; char *keys = newstring(alx[FC_KEYS]), *b, *k = strtok_r(keys, sep, &b); while(k) { loopv(servers) { serverinfo &si = *servers[i]; if(si.address.host == ENET_HOST_ANY || si.ping == 9999 || si.protocol != PROTOCOL_VERSION) continue; if((!alxn[FC_IGNORE] || showonlythiscat) && favcatcheckkey(si, k)) { si.weight += alxn[FC_WEIGHT]; res = true; if(si.favcat == -1 || showonlythiscat) { si.favcat = j; if(alxn[FC_ALPHA]) { if(!si.bgcolor) si.bgcolor = new color; new (si.bgcolor) color(((float)alxn[FC_RED])/100, ((float)alxn[FC_GREEN])/100, ((float)alxn[FC_BLUE])/100, ((float)alxn[FC_ALPHA])/100); } else DELETEP(si.bgcolor) } } } k = strtok_r(NULL, sep, &b); } delete[] keys; } loopv(servers) if(servers[i]->favcat == -1) { DELETEA(servers[i]->bgcolor); } return res; } COMMAND(addfavcategory, "s"); COMMAND(listfavcats, ""); static serverinfo *lastselectedserver = NULL; static bool pinglastselected = false; void refreshservers(void *menu, bool init) { static int servermenumillis; static bool usedselect = false; static string title; bool issearch = menu == searchmenu; bool isinfo = menu == serverinfomenu; bool isscoreboard = menu == NULL; if(!sk) { closemenu("serverbrowser"); showmenu("enter password"); } 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(isinfo) { if(lastselectedserver) { bool found = false; loopv(servers) if(lastselectedserver == servers[i]) { found = true; break; } if(!found) lastselectedserver = NULL; } menureset(menu); static string infotext; static char dummy = '\0'; if(lastselectedserver) { serverinfo &si = *lastselectedserver; formatstring(si.full)("%s:%d %s", si.name, si.port, si.sdesc); menuitemmanual(menu, si.full); menuitemmanual(menu, &dummy); if(si.infotexts.length() && !pinglastselected) { infotext[0] = '\0'; loopv(si.infotexts) menuimagemanual(menu, NULL, "bargraphs", si.infotexts[i]); } else { copystring(infotext, "-- waiting for server response --"); if(si.getinfo == EXTPING_NOP) si.getinfo = EXTPING_SERVERINFO; if(init || pinglastselected || totalmillis - lastinfo >= servpingrate) pingservers(false, lastselectedserver); pinglastselected = false; } } else copystring(infotext, " -- no server selected --"); if(*infotext) menuitemmanual(menu, infotext); return; } if((init && issearch) || totalmillis - lastinfo >= (servpingrate * (issearch ? 2 : 1))/(maxservpings ? max(1, (servers.length() + maxservpings - 1) / maxservpings) : 1)) pingservers(issearch, isscoreboard ? curserver : NULL); if(!init && menu)// && servers.inrange(((gmenu *)menu)->menusel)) { serverinfo *foundserver = NULL; loopv(servers) if(servers[i]->menuline_from == ((gmenu *)menu)->menusel && servers[i]->menuline_to > servers[i]->menuline_from) { foundserver = servers[i]; break; } if(foundserver) { if((usedselect || ((gmenu *)menu)->menusel > 0)) oldsel = foundserver; lastselectedserver = foundserver; } } if(isscoreboard) lastselectedserver = curserver; bool showfavtag = (assignserverfavourites() || !serverbrowserhidefavtag) && serverbrowserhidefavtag != 2; serversortprepare(); servers.sort(sicompare); if(menu) { bool showmr = showminremain || serversort == SBS_MINREM; menureset(menu); menuheader(menu, NULL, NULL); string text; int curnl = 0; int allplayers = 0; bool sbconnectexists = identexists("sbconnect"); loopv(servers) { serverinfo &si = *servers[i]; si.menuline_to = si.menuline_from = ((gmenu *)menu)->items.length(); if((!showallservers && si.lastpingmillis <= servermenumillis) || (si.maxclients > MAXCLIENTSONMASTER && searchlan<2) ) continue; // no pong yet or forbidden int banned = ((si.pongflags >> PONGFLAG_BANNED) & 1) | ((si.pongflags >> (PONGFLAG_BLACKLIST - 1)) & 2); bool showthisone = !(banned && showonlygoodservers) && !(showonlyfavourites > 0 && si.favcat != showonlyfavourites - 1); bool serverfull = si.numplayers >= si.maxclients; int mmode = (si.pongflags >> PONGFLAG_MASTERMODE) & MM_MASK; char basecolor = banned ? '4' : (curserver == servers[i] ? '1' : '5'); char plnumcolor = serverfull ? '2' : (mmode != MM_OPEN ? '1' : basecolor); const char *favimage = NULL; if(si.address.host != ENET_HOST_ANY && si.ping != 9999) { if(si.protocol!=PROTOCOL_VERSION) { if(!showonlygoodservers) formatstring(si.full)("%s:%d [%s]", si.name, si.port, si.protocol<0 ? "modded version" : (si.protocol -1) favimage = getalias(favcatargname(favcats[si.favcat], FC_IMAGE)); copystring(text, si.favcat > -1 && !favimage ? favcattags[si.favcat] : ""); if(showweights) concatformatstring(text, "(%d)", si.weight); if(showfavtag && !favimage) formatstring(si.full)("\fs%s\fr\t", text); else copystring(si.full, showfavtag ? "\t" : ""); concatformatstring(si.full, "\fs\f%c%s\t\fs\f%c%d/%d\fr\t\a%c ", basecolor, colorping(si.ping), plnumcolor, si.numplayers, si.maxclients, '0' + si.uplinkqual); if(si.map[0]) { concatformatstring(si.full, "%s, %s", si.map, modestr(si.mode, modeacronyms > 0)); if(showmr) concatformatstring(si.full, ", (%d)", si.minremain); } else concatformatstring(si.full, "empty"); concatstring(si.full, ": "); if(serverbrowserhideip < 2) concatformatstring(si.full, "\fs%s%s:%d\fr", serverbrowserhideip == 1 ? "\f4" : "", si.name, si.port); concatformatstring(si.full, "\fr %s", si.sdesc); } } else { if(!showonlygoodservers) formatstring(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) { cutcolorstring(si.full, 105); // cut off too long server descriptions cutcolorstring(si.description, 100); if(sbconnectexists) { filtertext(text, si.sdesc, FTXT_NOCOLOR|FTXT_NOWHITE|FTXT_ALLOWBLANKS); for(char *p = text; (p = strchr(p, '\"')); *p++ = ' '); formatstring(si.cmd)("sbconnect %s %d %d %d %d %d", si.name, si.port, serverfull ? 1 : 0, 0, mmode, banned); text[max(int(MAXSTRLEN - strlen(si.cmd) - 10), 0)] = '\0'; concatformatstring(si.cmd, " \"%s\"", text); } else formatstring(si.cmd)("connect %s %d", si.name, si.port); menuimagemanual(menu, favimage, "serverquality", si.full, si.cmd, si.bgcolor, si.description); if(!issearch && servers[i] == oldsel) { ((gmenu *)menu)->menusel = ((gmenu *)menu)->items.length() - 1; usedselect = true; } bool showplayernames = shownamesinbrowser && !issearch && (servers[i] == oldsel || (((gmenu *)menu)->menusel == 0 && servers[i] == lastselectedserver)); if(showplayernames) si.getinfo = EXTPING_NAMELIST; if((showplayernames && 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]; copystring(t, showfavtag ? "\t\t" : "\t"); } concatformatstring(t, " \t\fs%s%s\fr", !issearch || matchplayername(si.playernames[j]) ? "" : "\f4" ,si.playernames[j]); cur++; if(cur == 4) { menuitemmanual(menu, t, NULL, NULL, NULL); cur = 0; } } if(cur) menuitemmanual(menu, t, NULL, NULL, NULL); } } si.menuline_to = ((gmenu *)menu)->items.length(); if(!(showonlyfavourites > 0 && (servers[i]->favcat != showonlyfavourites - 1))) allplayers += servers[i]->numplayers; } static const char *titles[NUMSERVSORT] = { "%s\fs\f0ping\fr\t%s plr\tserver%s%s", // 0: ping "%sping\t\fs\f0%s plr\fr\tserver%s%s", // 1: player number "%sping\t%s plr\tserver (\fs\f0max players\fr)%s%s", // 2: maxplayers "%sping\t%s plr\fs\f0\fr\tserver (\fs\f0minutes remaining\fr)%s%s", // 3: minutes remaining "%sping\t%s plr\tserver (\fs\f0map\fr)%s%s", // 4: map "%sping\t%s plr\tserver (\fs\f0game mode\fr)%s%s", // 5: mode "%sping\t%s plr\tserver (\fs\f0IP\fr)%s%s", // 6: IP "%sping\t%s plr\tserver (\fs\f0description\fr)%s%s" // 7: description }; defformatstring(allplrs)("%d", allplayers); formatstring(title)(titles[serversort], showfavtag ? "fav\t" : "", !issearch && showallplayersnumber ? allplrs : "", issearch ? " search results for \f3" : " (F1: Help/Settings)", issearch ? cursearch : ""); menutitlemanual(menu, title); static string notfoundmsg, headermsg; notfoundmsg[0] = '\0'; if(issearch) { if(curnl == 0) formatstring(notfoundmsg)("\t\tpattern \fs\f3%s\fr not found.", cursearch); } if(showonlyfavourites && favcats.inrange(showonlyfavourites - 1)) { const char *desc = getalias(favcatargname(favcats[showonlyfavourites - 1], FC_DESC)); formatstring(headermsg)("Servers in category \f2%s", desc ? desc : favcattags[showonlyfavourites - 1]); menuheader(menu, headermsg, NULL); if(!((gmenu *)menu)->items.length() && !issearch) formatstring(notfoundmsg)("\t(no servers in this category)"); } if(*notfoundmsg) menuitemmanual(menu, notfoundmsg, NULL, NULL, NULL); } } bool serverskey(void *menu, int code, bool isdown) { int fk[] = { SDLK_1, SDLK_2, SDLK_3, SDLK_4, SDLK_5, SDLK_6, SDLK_7, SDLK_8, SDLK_9, SDLK_0 }, gogetem = 1; if(!isdown) return false; loopi(sizeof(fk)/sizeof(fk[0])) if(code == fk[i] && favcats.inrange(i)) { int sel = ((gmenu *)menu)->menusel; loopvj(servers) if(menu && (servers[j]->menuline_from <= sel && servers[j]->menuline_to > sel)) { string ak; ak[0] = '\0'; const char *keyalias = favcatargname(favcats[i], FC_KEYS), *key = getalias(keyalias), *rest = favcatcheck(*servers[j], key, ak), *desc = getalias(favcatargname(favcats[i], FC_DESC)); if(!desc) desc = ""; if(*ak) { // server was automatically added to this favourite group, don't remove conoutf("server \"\fs%s\fr\" is in category '\fs%s\fr' because of key '%s', please remove manually", servers[j]->sdesc, desc, ak); } else if(rest) { // remove from favourite group conoutf("removing server \"\fs%s\fr\" from favourites category '\fs%s\fr' (rest '%s')", servers[j]->sdesc, desc, rest); alias(keyalias, rest); } else { // add IP:port to group defformatstring(text)("%s:%d", servers[j]->name, servers[j]->port); if(key && *key) { char *newkey = newstring(key, strlen(text) + 1 + strlen(key)); strcat(newkey, " "); strcat(newkey, text); alias(keyalias, newkey); delete[] newkey; } else alias(keyalias, text); conoutf("adding server \"\fs%s\fr\" to favourites category '\fs%s\fr' (new '%s')", servers[j]->sdesc, desc, getalias(keyalias)); } return true; } } switch(code) { case SDLK_LEFT: serversort = (serversort+NUMSERVSORT-1) % NUMSERVSORT; return true; case SDLK_RIGHT: serversort = (serversort+1) % NUMSERVSORT; return true; case SDLK_LEFTBRACKET: serverbrowseralternativeviews(-1); return true; case SDLK_RIGHTBRACKET: serverbrowseralternativeviews(1); return true; case SDLK_F5: updatefrommaster(&gogetem); return true; case SDLK_F6: serversortdir = serversortdir ? 0 : 1; return true; case SDLK_F9: showmenu("serverinfo"); 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_F4: showmenu("edit favourites"); return true; case SDLK_F7: showonlygoodservers = showonlygoodservers ? 0 : 1; return true; case SDLK_F8: showminremain = showminremain ? 0 : 1; return true; } return false; } bool serverinfokey(void *menu, int code, bool isdown) { if(!isdown) return false; switch(code) { case SDLK_F5: if(lastselectedserver) lastselectedserver->getinfo = EXTPING_SERVERINFO; pinglastselected = true; return true; case SDLK_F1: showmenu("serverinfo help"); // feel free to actually write this menu ;) return true; case SDLK_F2: // yep: undocumented :) if(lastselectedserver) lastselectedserver->getinfo = EXTPING_UPLINKSTATS; pinglastselected = true; break; case SDLK_m: if(lastselectedserver) lastselectedserver->getinfo = EXTPING_MAPROT; pinglastselected = true; break; case SDLK_c: lastselectedserver = getconnectedserverinfo(); pinglastselected = true; break; } return false; } void clearservers() { resolverclear(); servers.deletecontents(); } #define RETRIEVELIMIT 5000 int progress_callback_retrieveservers(void *data, float progress) { if(progress < 0) show_out_of_renderloop_progress(progress + 1.0f, "waiting for response (esc to abort)"); else show_out_of_renderloop_progress(min(progress, 1.0f), (const char *)data); return interceptkey(SDLK_ESCAPE) ? 1 : 0; } VARP(mastertype, 0, 0, 1); // 0: TCP direct, 1: HTTP proxy void retrieveservers(vector &data) { if(mastertype) { httpget h; defformatstring(progresstext)("resolving %s", mastername); h.callbackfunc = progress_callback_retrieveservers; h.callbackdata = progresstext; show_out_of_renderloop_progress(0.01f, progresstext); if(h.set_host(mastername)) { formatstring(progresstext)("retrieving servers from %s:%d... (esc to abort)", mastername, masterport); defformatstring(url)("/retrieve.do?action=list&name=%s&version=%d&build=%d", urlencode(player1->name, true), AC_VERSION, getbuildtype()|(1<<16)); h.outvec = (vector *) &data; // ouch... show_out_of_renderloop_progress(0, progresstext); int got = h.get(url, RETRIEVELIMIT, RETRIEVELIMIT); if(got < 0 || h.response != 200) data.setsize(0); h.outvec = NULL; // must not be cleaned up by httpget if(data.length()) data.add('\0'); } else { conoutf("failed to resolve host %s", mastername); } } else { ENetSocket sock = connectmaster(); if(sock == ENET_SOCKET_NULL) { conoutf("Master server is not replying."); return; } defformatstring(text)("retrieving servers from %s:%d... (esc to abort)", mastername, masterport); show_out_of_renderloop_progress(0, text); int starttime = SDL_GetTicks(), timeout = 0; defformatstring(request)("list %s %d %d\n", player1->name, AC_VERSION, getbuildtype()); const char *req = request; int reqlen = strlen(req); ENetBuffer buf; while(reqlen > 0) { enet_uint32 events = ENET_SOCKET_WAIT_SEND; if(enet_socket_wait(sock, &events, 250) >= 0 && events) { buf.data = (void *)req; buf.dataLength = reqlen; int sent = enet_socket_send(sock, NULL, &buf, 1); if(sent < 0) break; req += sent; reqlen -= sent; if(reqlen <= 0) break; } timeout = SDL_GetTicks() - starttime; show_out_of_renderloop_progress(min(float(timeout)/RETRIEVELIMIT, 1.0f), text); if(interceptkey(SDLK_ESCAPE)) timeout = RETRIEVELIMIT + 1; if(timeout > RETRIEVELIMIT) break; } if(reqlen <= 0) for(;;) { enet_uint32 events = ENET_SOCKET_WAIT_RECEIVE; if(enet_socket_wait(sock, &events, 250) >= 0 && events) { if(data.length() >= data.capacity()) data.reserve(4096); buf.data = data.getbuf() + data.length(); buf.dataLength = data.capacity() - data.length(); int recv = enet_socket_receive(sock, NULL, &buf, 1); if(recv <= 0) break; data.advance(recv); } timeout = SDL_GetTicks() - starttime; show_out_of_renderloop_progress(min(float(timeout)/RETRIEVELIMIT, 1.0f), text); if(interceptkey(SDLK_ESCAPE)) timeout = RETRIEVELIMIT + 1; if(timeout > RETRIEVELIMIT) break; } if(data.length()) data.add('\0'); enet_socket_destroy(sock); } } VARP(masterupdatefrequency, 1, 60*60, 24*60*60); VAR(msctrl, 1, 1, INT_MAX); void updatefrommaster(int *force) { static int lastupdate = 0; if(!*force && lastupdate && totalmillis-lastupdate data; retrieveservers(data); if(data.empty()) conoutf("Master server is not replying. \f1Get more information at http://masterserver.cubers.net/"); else { // preserve currently connected server from deletion serverinfo *curserver = getconnectedserverinfo(); string curname; if(curserver) copystring(curname, curserver->name); clearservers(); execute(data.getbuf()); if(curserver) addserver(curname, curserver->port, curserver->msweight); lastupdate = totalmillis; } } COMMANDF(addserver, "sii", (char *n, int *p, int *w) { addserver(n, *p, *w); }); COMMAND(clearservers, ""); COMMAND(updatefrommaster, "i"); void writeservercfg() { stream *f = openfile(path("config/servers.cfg", true), "w"); if(!f) return; f->printf("// servers connected to are added here automatically\n"); loopvrev(servers) { f->printf("\naddserver %s %d", servers[i]->name, servers[i]->port); if(servers[i]->msweight) f->printf(" %d", servers[i]->msweight); } f->printf("\n"); delete f; }