1457 lines
51 KiB
C++
1457 lines
51 KiB
C++
// 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<resolverthread> resolverthreads;
|
|
vector<const char *> resolverqueries;
|
|
vector<resolverresult> 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.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, &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<serverinfo *> 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()
|
|
{
|
|
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, 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";
|
|
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);
|
|
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<char *> 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<char *> 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<int> 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<const char *> 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;
|
|
|
|
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>MAXCL && 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;
|
|
bool needspasswd = (si.pongflags & (1 << PONGFLAG_PASSWORD)) > 0;
|
|
int mmode = (si.pongflags >> PONGFLAG_MASTERMODE) & MM_MASK;
|
|
char basecolor = banned ? '4' : (curserver == servers[i] ? '1' : '5');
|
|
char plnumcolor = serverfull ? '2' : (needspasswd ? '3' : (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<PROTOCOL_VERSION ? "older protocol" : "newer protocol"));
|
|
else showthisone = false;
|
|
}
|
|
else
|
|
{
|
|
if(!hidefavicons && !showweights && showfavtag && si.favcat > -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, needspasswd ? 1 : 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;
|
|
if(shownamesinbrowser) si.getinfo = EXTPING_NAMELIST;
|
|
}
|
|
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];
|
|
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 unicode)
|
|
{
|
|
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, int unicode)
|
|
{
|
|
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
|
|
|
|
extern char *global_name;
|
|
bool cllock = false, clfail = false;
|
|
|
|
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, 1, 1); // 0: TCP direct, 1: HTTP proxy
|
|
|
|
void retrieveservers(vector<char> &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(global_name, true), AC_VERSION, getbuildtype()|(1<<16));
|
|
h.outvec = (vector<uchar> *) &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');
|
|
clfail = false;
|
|
}
|
|
else
|
|
{
|
|
conoutf("failed to resolve host %s", mastername);
|
|
clfail = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ENetSocket sock = connectmaster();
|
|
if(sock == ENET_SOCKET_NULL)
|
|
{
|
|
conoutf("Master server is not replying.");
|
|
clfail = true;
|
|
return;
|
|
}
|
|
clfail = false;
|
|
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", global_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(lastupdate==0) cllock = true;
|
|
if(!*force && lastupdate && totalmillis-lastupdate<masterupdatefrequency*1000) return;
|
|
|
|
vector<char> data;
|
|
retrieveservers(data);
|
|
|
|
if(data.empty())
|
|
{
|
|
if (!clfail) conoutf("Master server is not replying. \f1Get more information at http://masterserver.cubers.net/");
|
|
cllock = !clfail;
|
|
}
|
|
else
|
|
{
|
|
// preserve currently connected server from deletion
|
|
serverinfo *curserver = getconnectedserverinfo();
|
|
string curname;
|
|
if(curserver) copystring(curname, curserver->name);
|
|
|
|
clearservers();
|
|
if(!strncmp(data.getbuf(), "addserver", 9)) cllock = false; // the ms could reply other thing... but currently, this is useless
|
|
if(!cllock )
|
|
{
|
|
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;
|
|
}
|