/******
gserverlist.c
GameSpy C Engine SDK

Copyright 1999 GameSpy Industries, Inc

Suite E-204
2900 Bristol Street
Costa Mesa, CA 92626
(714)549-7689
Fax(714)549-0757

******

 Please see the GameSpy C Engine SDK documentation for more
 information

  The goaceng.h file has extensive comments on each of the public functions
  for this SDK. It also has a change history for the SDK.

******/
#include "nonport.h"
#include "goaceng.h"
#include "gserver.h"
#include "hashtable.h"
#include "gdarray.h"
#include "gutil.h"
#include "client.h"
#ifndef UNDER_CE
#include <assert.h>
#else
#define assert(a)
#endif
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h> /* FS: Needed for non-blocking sockets */
#include <errno.h> /* FS: Needed for non-blocking sockets */

#define SERVER_GROWBY 64
#define LAN_SEARCH_TIME 3000 /* 3 sec */
#define LIST_NUMKEYBUCKETS 500
#define LIST_NUMKEYCHAINS 4
#define MAX_INFO_FIELDS 20

static cvar_t	*cl_master_server_retries;
static cvar_t	*cl_master_server_port;
static cvar_t	*cl_master_server_ip;
static cvar_t	*cl_master_server_timeout;
static cvar_t	*cl_master_server_optout;

//todo: check state changes on error
typedef struct
{
	SOCKET s;
	GServer currentserver;
	goa_uint32 starttime;
	struct sockaddr_in saddr;
} UpdateInfo;

typedef enum {pi_fieldcount, pi_fields, pi_servers} GParseInfoState;

struct GServerListImplementation
{
	GServerListState state;
	DArray servers;
	UpdateInfo *updatelist; /* dynamic array of updateinfos */
	char gamename[32];
	char seckey[32];
	char enginename[32];
	int maxupdates;
	int nextupdate;
	int abortupdate;
	ListCallBackFn CallBackFn;
	void *instance;
	const char *sortkey;
	gbool sortascending;
	SOCKET slsocket;
	goa_uint32 lanstarttime;
	GQueryType querytype;
	HashTable keylist;
	GParseInfoState pistate;
};

static GServerList g_sortserverlist; //global serverlist for sorting info!!
static int totalRetry = 20; /* FS: Total retry attempts waiting for the gamespy validate stuff */

/* these correspond to the qt_ constants */
#define NUM_QUERYTYPES 6
static const char *querystrings[NUM_QUERYTYPES] = {"\\basic\\","\\info\\","\\rules\\",
								"\\players\\","\\info\\\\rules\\","\\status\\"};
static const int querylengths[NUM_QUERYTYPES] = {7,6,7,9,13,8};

void InitGamespy (void)
{
	cl_master_server_retries = Cvar_Get("cl_master_server_retries", "20", CVAR_ARCHIVE);
	cl_master_server_port = Cvar_Get("cl_master_server_port", "28900", CVAR_ARCHIVE);
	cl_master_server_ip = Cvar_Get("cl_master_server_ip", "maraakate.org", CVAR_ARCHIVE);
	cl_master_server_timeout = Cvar_Get("cl_master_server_timeout", "3000", CVAR_ARCHIVE);
	cl_master_server_optout = Cvar_Get("cl_master_server_optout", "0", CVAR_ARCHIVE);
}

/* FS: Set a socket to be non-blocking */
#ifdef _WIN32
#define TCP_BLOCKING_ERROR WSAEWOULDBLOCK
static int Set_Non_Blocking_Socket (SOCKET s) {
	u_long _true = true;

	return ioctlsocket( s, FIONBIO, &_true);
}

static __inline int Get_Last_Error(void) {
	return WSAGetLastError();
}
#else
#define TCP_BLOCKING_ERROR EWOULDBLOCK
static int Set_Non_Blocking_Socket (SOCKET s) {
	int _true = true;
	return ioctlsocket( s, FIONBIO, IOCTLARG_T &_true);
}

static __inline int Get_Last_Error(void) {
	return errno;
}
#endif

static void Close_TCP_Socket(SOCKET *s)
{
	closesocket(*s);
	*s = INVALID_SOCKET;
}

/* ServerListNew
----------------
Creates and returns a new (empty) GServerList. */
GServerList ServerListNew(const char *gamename, const char *enginename, const char *seckey,
			  int maxconcupdates, ListCallBackFn CallBackFn, int CallBackFnType, void *instance)
{
	GServerList list;

	list = (GServerList) malloc(sizeof(struct GServerListImplementation));
	assert(list != NULL);
	list->state = sl_idle;
	list->servers = ArrayNew(sizeof(GServer), SERVER_GROWBY, ServerFree);
	list->keylist = TableNew2(sizeof(char *),LIST_NUMKEYBUCKETS,LIST_NUMKEYCHAINS,GStringHash, GCaseInsensitiveCompare, GStringFree);
	list->maxupdates = maxconcupdates;
	list->updatelist = (UpdateInfo *)malloc(maxconcupdates * sizeof(UpdateInfo));
	memset(list->updatelist, 0, maxconcupdates * sizeof(UpdateInfo));
	assert(list->updatelist != NULL);
	strcpy(list->gamename, gamename);
	strcpy(list->seckey, seckey);
	strcpy(list->enginename, enginename);
	list->CallBackFn = CallBackFn;
	assert(CallBackFn != NULL);
	list->instance = instance;
	list->sortkey = "";
	SocketStartUp();
	return list;
}

/* ServerListFree
-----------------
Free a GServerList and all internal sturctures and servers */
void ServerListFree(GServerList serverlist)
{
	ArrayFree(serverlist->servers);
	serverlist->servers = NULL;
	TableFree(serverlist->keylist);
	free(serverlist->updatelist);
	serverlist->updatelist = NULL;

	free(serverlist);
	serverlist = NULL;
	SocketShutDown();
}

 //create update sockets and init structures
static GError InitUpdateList(GServerList serverlist)
{
	int i;

	for (i = 0 ; i < serverlist->maxupdates ; i++)
	{
		serverlist->updatelist[i].s = socket(AF_INET, SOCK_DGRAM,IPPROTO_UDP);
		if (serverlist->updatelist[i].s == INVALID_SOCKET)
		{
			//ran out of sockets, just cap maxupdates here, unless we don't have any
			if (i == 0)
				return GE_NOSOCKET;
			serverlist->maxupdates = i;
			return 0;
		}


		/* FS: Set non-blocking sockets */
		if (Set_Non_Blocking_Socket(serverlist->updatelist[i].s) == SOCKET_ERROR)
		{
//			Com_Printf("ERROR: InitUpdateList: ioctl FIOBNIO:%s\n", gspyi.net_strerror());
			return GE_NOSOCKET;
		}

		serverlist->updatelist[i].currentserver = NULL;
		serverlist->updatelist[i].starttime = 0;
	}
	return 0;
}

//free update sockets
static GError FreeUpdateList(GServerList serverlist) 
{
	int i;

#ifdef __DJGPP__
	Com_Printf("Freeing gamespy query sockets, please wait. . .\n"); /* FS: This can have a little bit of a delay in DOS, so give a warning about it */
#endif
	for (i = 0 ; i < serverlist->maxupdates ; i++)
	{
		closesocket(serverlist->updatelist[i].s);
	}
	return 0;
}

//create and connect a server list socket
static GError CreateServerListSocket(GServerList serverlist)
{
	struct   sockaddr_in saddr;
	struct hostent *hent;

	if (cl_master_server_ip->string[0] == '\0')
	{
		Com_Printf("Error: cl_master_server_ip is blank!  Setting to default: %s\n", CL_MASTER_ADDR);
		Cvar_Set("cl_master_server_ip", CL_MASTER_ADDR);
	}
	if (cl_master_server_port->intValue <= 0)
	{
		Com_Printf("Error: cl_master_server_port is invalid!  Setting to default: %s\n", CL_MASTER_PORT);
		Cvar_Set("cl_master_server_port", CL_MASTER_PORT);
	}

	serverlist->slsocket = socket ( AF_INET, SOCK_STREAM, IPPROTO_TCP );
	if (serverlist->slsocket == INVALID_SOCKET)
		return GE_NOSOCKET;

	saddr.sin_family = AF_INET;
	saddr.sin_port = htons((unsigned short)cl_master_server_port->intValue);
	saddr.sin_addr.s_addr = inet_addr(cl_master_server_ip->string);
	if (saddr.sin_addr.s_addr == INADDR_NONE)
	{
		hent = gethostbyname(cl_master_server_ip->string);

		if (!hent)
		{
			Close_TCP_Socket(&serverlist->slsocket);
			return GE_NODNS;
		}
		saddr.sin_addr.s_addr = *(in_addr_t *)hent->h_addr_list[0];
	}

	memset ( saddr.sin_zero, 0, 8 );

	if (connect ( serverlist->slsocket, (struct sockaddr *) &saddr, sizeof saddr ) != 0 )
	{
		Close_TCP_Socket(&serverlist->slsocket);
		return GE_NOSOCKET;
	}

	if(Set_Non_Blocking_Socket(serverlist->slsocket) == SOCKET_ERROR)
	{
		Com_Printf("ERROR: CreateServerListSocket: ioctl FIOBNIO:%s\n", NET_ErrorString());
		Close_TCP_Socket(&serverlist->slsocket);
		return GE_NOSOCKET;
	}

	totalRetry = bound(1,cl_master_server_retries->intValue,100);

	//else we are connected
	return 0;
}

//create and connect a server list socket
static GError CreateServerListLANSocket(GServerList serverlist)
{
	int optval = 1;

	serverlist->slsocket = socket ( AF_INET, SOCK_DGRAM, IPPROTO_UDP );
	if (serverlist->slsocket == INVALID_SOCKET)
		return GE_NOSOCKET;
	if (setsockopt(serverlist->slsocket, SOL_SOCKET, SO_BROADCAST, (char *)&optval, sizeof(optval)) != 0)
	{
		Com_Printf("ERROR: CreateServerListLANSocket: setsockopt SOL_SOCKET, SO_BROADCAST:%s\n", NET_ErrorString());
		Close_TCP_Socket(&serverlist->slsocket);
		return GE_NOSOCKET;
	}

	//else we are ready to broadcast
	return 0;
}

//trigger the callback and set the new mode
static void ServerListModeChange(GServerList serverlist, GServerListState newstate)
{
	serverlist->state = newstate;
	serverlist->CallBackFn(serverlist, LIST_STATECHANGED, serverlist->instance, NULL, NULL);
}

// validate us to the master and send a list request
#define SECURE "\\secure\\"
static GError SendListRequest(GServerList serverlist, const char *filter)
{
	char data[256], *ptr, result[64];
	int len;
	const char *modifier;
	const char *clientName = "OPT-OUT"; /* FS: Allow people to opt-out of me getting their username in list requests */
	int error = 0;
	int retry = 0;
	goa_uint32 sleepMs = 50;

retryRecv:
	len = recv(serverlist->slsocket, data, sizeof(data) - 1, 0);

	if (len == SOCKET_ERROR)
	{
		error = Get_Last_Error();

		if(error == TCP_BLOCKING_ERROR && (retry < totalRetry))
		{
			retry++;
			Com_DPrintf(DEVELOPER_MSG_GAMESPY, "Retrying Gamespy TCP Validate Handshake, Attempt %i of %i.\n", retry, totalRetry);
			msleep(sleepMs);
			sleepMs = sleepMs + 10;
			Sys_SendKeyEvents (); /* FS: Check for aborts */
			goto retryRecv;
		}
		else
		{
			Close_TCP_Socket(&serverlist->slsocket);
			return GE_NOCONNECT;
		}
	}
	data[len] = '\0'; //null terminate it
	Com_DPrintf(DEVELOPER_MSG_GAMESPY, "Gamespy validate server key: %s\n", data);

	ptr = strstr ( data, SECURE );
	if (!ptr)
		return GE_DATAERROR;
	ptr = ptr + strlen(SECURE);
	gs_encrypt ((uchar *) serverlist->seckey, 6, (uchar *)ptr, 6);
	gs_encode ((uchar *)ptr, 6, (uchar *) result);

	/* FS: Allow people to opt-out of me getting their username in list requests */
	if (!cl_master_server_optout->intValue)
		clientName = Cvar_VariableString("name");

	//validate to the master
	sprintf(data, "\\gamename\\%s\\gamever\\%s\\location\\0\\clientname\\%s\\validate\\%s\\final\\\\queryid\\1.1\\",
			serverlist->enginename, ENGINE_VERSION, clientName, result); //validate us
	Com_DPrintf(DEVELOPER_MSG_GAMESPY, "Gamespy validate to the master: %s\n", data);
	
	len = send ( serverlist->slsocket, data, (int)strlen(data), 0 );
	if (len == SOCKET_ERROR || len == 0)
	{
		Close_TCP_Socket(&serverlist->slsocket);
		return GE_NOCONNECT;
	}

	if (serverlist->querytype == qt_grouprooms)
	{
		modifier = "groups";
		serverlist->pistate = pi_fieldcount;
	} 
	else if (serverlist->querytype == qt_masterinfo)
	{
		modifier = "info2";
		serverlist->pistate = pi_fieldcount;
	}
	else
		modifier = "cmp";
	//send the list request
	if (filter)
		sprintf(data, "\\list\\%s\\gamename\\%s\\where\\%s\\final\\", modifier, serverlist->gamename, filter);
	else
		sprintf(data, "\\list\\%s\\gamename\\%s\\final\\", modifier, serverlist->gamename);
	len = send ( serverlist->slsocket, data, (int)strlen(data), 0 );
	if (len == SOCKET_ERROR || len == 0)
	{
		Close_TCP_Socket(&serverlist->slsocket);
		return GE_NOCONNECT;
	}

	ServerListModeChange(serverlist, sl_listxfer);
	return 0;
}

static GError SendBroadcastRequest(GServerList serverlist, int startport, int endport, int delta)
{
	struct   sockaddr_in saddr;
	short i;

	saddr.sin_family = AF_INET;
	saddr.sin_addr.s_addr = 0xFFFFFFFF; //broadcast
	for (i = startport ; i <= endport ; i += delta)
	{
		saddr.sin_port = htons(i);
		sendto(serverlist->slsocket, "\\status\\",8,0,(struct sockaddr *)&saddr,sizeof(saddr));
	}
	ServerListModeChange(serverlist, sl_lanlist);
	serverlist->lanstarttime = current_time();
	return 0;
}

//just wait for the server list to become idle
static void DoSyncLoop(GServerList serverlist)
{
	while (serverlist->state != sl_idle)
	{
		ServerListThink(serverlist);
		msleep(10);
	}
}

/* ServerListUpdate
-------------------
Start updating a GServerList without filters */
GError ServerListUpdate(GServerList serverlist, gbool async)
{
	return ServerListUpdate2(serverlist, async, NULL, qt_status);
}

/* ServerListUpdate2
-------------------------
Start updating a GServerList. */
GError ServerListUpdate2(GServerList serverlist, gbool async, const char *filter, GQueryType querytype)
{
	GError error;

	if (serverlist->state != sl_idle)
		return GE_BUSY;

	serverlist->querytype = querytype;
	error = CreateServerListSocket(serverlist);
	if (error) return error;
	error = SendListRequest(serverlist, filter);
	if (error) return error;
	if (querytype != qt_grouprooms && querytype != qt_masterinfo)
		error = InitUpdateList(serverlist);
	if (error) return error;
	serverlist->nextupdate = 0;
	serverlist->abortupdate = 0;
	if (!async)
		DoSyncLoop(serverlist);

	return 0;
}

/* ServerListLANUpdate
-------------------
Start updating a GServerList from servers on the LAN. */
GError ServerListLANUpdate(GServerList serverlist, gbool async, int startsearchport, int endsearchport, int searchdelta)
{
	GError error;

	assert(searchdelta > 0);

	if (serverlist->state != sl_idle)
		return GE_BUSY;

	error = InitUpdateList(serverlist);
	if (error) return error;
	error = CreateServerListLANSocket(serverlist);
	if (error) return error;
	error = SendBroadcastRequest(serverlist, startsearchport, endsearchport, searchdelta);
	if (error) return error;
	serverlist->nextupdate = 0;
	serverlist->abortupdate = 0;
	if (!async)
		DoSyncLoop(serverlist);

	return 0;
}


static GServer ServerListAddServerData(GServerList serverlist, char **fieldlist, int fieldcount, char *serverdata, GQueryType qtype)
{
	GServer server;
	server = ServerNewData(fieldlist, fieldcount, serverdata, qtype, serverlist->keylist);
	ArrayAppend(serverlist->servers,&server);
	return server;
}

//add the server to the list with the given ip, port
static GServer ServerListAddServer(GServerList serverlist, in_addr_t ip, unsigned short port, GQueryType qtype)
{
	GServer server;
	server =  ServerNew(ip, port, qtype, serverlist->keylist);
	ArrayAppend(serverlist->servers,&server);
	return server;
}

//add the server to the list with the given ip, port
static GServer ServerListInsertServer(GServerList serverlist, in_addr_t ip, unsigned short port, int pos, GQueryType qtype)
{
	GServer server;
	server =  ServerNew(ip, port, qtype, serverlist->keylist);
	ArrayInsertAt(serverlist->servers,&server,pos);
	return server;
}

//find the server in the list, returns -1 if it does not exist
static int ServerListFindServer(GServerList serverlist, in_addr_t ip, int port)
{
	int i;
	GServer server;

	for (i = 0; i < ArrayLength(serverlist->servers) ; i++)
	{
		server = *(GServer *)ArrayNth(serverlist->servers,i);
		if (port == ServerGetQueryPort(server) && ServerGetInetAddress(server)==ip)
		{
			return i;
		}
	}
	return -1;
}

//finds the server in the list of servers currently being queried
// returns -1 if it does not exist
static int ServerListFindServerInUpdateList(GServerList serverlist, GServer server)
{
	int i;

	for (i = 0 ; i < serverlist->maxupdates ; i++)
	{
		if (serverlist->updatelist[i].currentserver == server)
			return i;
	}
	return -1;
}

/* ServerListRemoveServer
-------------------------
Removes a single server from the list. Frees the memory associated with the GServer */
void ServerListRemoveServer(GServerList serverlist, const char *ip, int port)
{
	int currentindex = ServerListFindServer(serverlist, inet_addr(ip), port);
	int updateindex;

	if (currentindex == -1)
		return; //can't do anything, it doesn't exist

	//check to see whether we need to change the updatelist or move the nextupdate
	if (serverlist->state != sl_idle && serverlist->nextupdate > currentindex) 
	{
		GServer holdserver = *(GServer *)ArrayNth(serverlist->servers,currentindex);
		updateindex = ServerListFindServerInUpdateList(serverlist, holdserver);
		if (updateindex != -1) //is currently being queried, stop it
			serverlist->updatelist[updateindex].currentserver = NULL;
		serverlist->nextupdate--; //decrement the next update, since we are removing a server
	}
	ArrayDeleteAt(serverlist->servers, currentindex);
}

/* ServerListUpdate
-------------------
Adds an auxilliary (non-fetched) server to the update list.
If the engine is idle, the server is added and the engine started. */
GError ServerListAuxUpdate(GServerList serverlist, const char *ip, int port, gbool async, GQueryType querytype)
{
	GError error;
	int currentindex;
	int updateindex;
	in_addr_t real_ip = inet_addr(ip);

	//first, see if the server already exists
	currentindex = ServerListFindServer(serverlist,real_ip,port);

	//if we're idle, start things up
	if (serverlist->state == sl_idle)
	{
		//prepare as if we're going to do a normal list fetch,
		//but skip the call to SendListRequest().

		error = InitUpdateList(serverlist);
		if (error) return error;
		if (currentindex != -1) //we need to "move" this server to the end of the list
		{ //move the server to the end of the array
			GServer holdserver = *(GServer *)ArrayNth(serverlist->servers,currentindex);
			holdserver->ping = 9999;//clear the ping so it gets recalculated
			ArrayRemoveAt(serverlist->servers,currentindex);
			ArrayAppend(serverlist->servers,&holdserver);
		} else
		{	//add the aux server
			ServerListAddServer(serverlist, real_ip, (unsigned short)port, querytype);
		}
		
		serverlist->nextupdate = ArrayLength(serverlist->servers) - 1;
		serverlist->abortupdate = 0;

		//chane the mode straight to querying
		ServerListModeChange(serverlist, sl_querying);
		//is it's a sync call, do it until done
		if (!async)
			DoSyncLoop(serverlist);
	}
	else
	{
		//if we're in the middle of an update, we should
		//be able to just slip the aux server in for querying
		//ServerListAddServer(serverlist, ip, port);
		//crt -- make it the next server to be queried
		//note: this should NEVER be called in a different thread from think!!
		if (currentindex == -1) //it doesn't exist yet
			ServerListInsertServer(serverlist, real_ip, (unsigned short)port, serverlist->nextupdate, querytype);
		else 
		{ //it exists, find out whats happening to it
			GServer holdserver = *(GServer *)ArrayNth(serverlist->servers,currentindex);
			if (currentindex >= serverlist->nextupdate) //hasn't been queried yet!
				return 0; //it will be queried soon anyway
			holdserver->ping = 9999;//clear the ping so it gets recalculated
			updateindex = ServerListFindServerInUpdateList(serverlist, holdserver);
			if (updateindex != -1) //is currently being queried, stop it
				serverlist->updatelist[updateindex].currentserver = NULL;
			ArrayInsertAt(serverlist->servers,&holdserver, serverlist->nextupdate); //insert at new place
			ArrayRemoveAt(serverlist->servers,currentindex); //remove the old one
			serverlist->nextupdate--; //decrement the next update, since we are removing a server
		}
	}
	return 0;
}

static GError ServerListLANList(GServerList serverlist)
{
	struct timeval timeout = {0,0};
	fd_set set;
	char indata[1500];
	struct   sockaddr_in saddr;
	socklen_t saddrlen = sizeof(saddr);
	int error;

	while (1) //we break if the select fails
	{
		FD_ZERO(&set);
		FD_SET( serverlist->slsocket, &set);
		error = selectsocket(FD_SETSIZE, &set, NULL, NULL, &timeout);
		if (SOCKET_ERROR == error || 0 == error) //no data
			break;
		error = recvfrom(serverlist->slsocket, indata, sizeof(indata) - 1, 0, (struct sockaddr *)&saddr, &saddrlen );
		if (SOCKET_ERROR == error)
			continue;
		//we got data, add the server to the list to update
		if (strstr(indata,"\\final\\") != NULL)
			ServerListAddServer(serverlist,saddr.sin_addr.s_addr, ntohs(saddr.sin_port), qt_status);
	}
	if (current_time() - serverlist->lanstarttime > LAN_SEARCH_TIME) //done waiting for replies
	{
		closesocket(serverlist->slsocket);
		serverlist->slsocket = INVALID_SOCKET;
		ServerListModeChange(serverlist, sl_querying);
	}
	return 0;
}

//parses a \ delimited string and counts the number of characters to the Nth \ character
// e.g. \test\3\test2\2335 N = 3, returns 8
static int CountSlashOffset(char *data, int len, int slashcount)
{
	char *p = data;
	while (slashcount && p != data + len)
	{
		if (*p == '\\')
			slashcount--;
		p++;		
	}
	if (slashcount == 0)
		return p - data;
	else
		return -1;
}

#define FIELDCOUNT_LENGTH 12 //length of string \fieldcount\ --

//parses a server list with info in it, the format is:
// \fieldcount\N\field1\field2\...\fieldN\server1Field1\server1Field2\...
// \server1FieldN\server2Field1\server2Field2\...\serverXFieldN\final\ ---

static int ServerListParseInfoList(GServerList serverlist, char *data, int len)
{
	static char *fieldlist[MAX_INFO_FIELDS];
	char tempfield[64], *tempptr;
	static int fieldcount;
	int offset;
	int i;
	GServer server;
	
	switch (serverlist->pistate)
	{
	case pi_fieldcount:
		offset = CountSlashOffset(data, len, 3); //looking for \fieldcount\N\ --
		if (offset == -1)
			return 0;
		if (offset < FIELDCOUNT_LENGTH)
			return -1; //the data is incorrect!
		strncpy(tempfield, data + FIELDCOUNT_LENGTH, offset - FIELDCOUNT_LENGTH);
		tempfield[offset - FIELDCOUNT_LENGTH - 1] = 0;
		fieldcount = atoi(tempfield);
		if (fieldcount > MAX_INFO_FIELDS)
			return -1; //this won't work!
		serverlist->pistate = pi_fields;
		for (i = 0 ; i < fieldcount ; i++)
			fieldlist[i] = NULL; //make sure it gets null'd
		return offset - 1;//don't count the slash character..
		break;
	case pi_fields:
		offset = CountSlashOffset(data, len, 2); //looking for \fieldname\ --
		if (offset == -1)
			return 0;
		strncpy(tempfield, data + 1, offset - 2); //copy just "fieldname"
		tempfield[offset-2] = 0;
		tempptr = strdup(tempfield);
		TableEnter(serverlist->keylist,&tempptr);
		//find the place to insert it..
		for (i = 0; i < fieldcount ; i++)
			if (fieldlist[i] == NULL)
			{
				fieldlist[i] = tempptr;
				if (i == fieldcount - 1) //that's the last field, advance us!
				{
					serverlist->pistate = pi_servers;
				}
				return offset - 1;
			}		
		break;
	case pi_servers:
		offset = CountSlashOffset(data, len, fieldcount+1); //get the next set of fields
		if (offset == -1)
			return 0;
		//now add a server with the data as the string
		server = ServerListAddServerData(serverlist, fieldlist, fieldcount, data, serverlist->querytype);
		data[offset - 1] = '\\'; //make sure the \ char doesn't get nuked insde AddServerData
		serverlist->CallBackFn(serverlist, 
								LIST_PROGRESS, 
								serverlist->instance,
								server,
								0); //percent done
								
		return offset - 1;
		break;
	}
	return -1; //bad state!
}

//reads the server list from the socket and parses it
static GError ServerListReadList(GServerList serverlist)
{
	static char data[2048]; //static input buffer
	static int oldlen = 0;
	int len, i;
	char *p;
	in_addr_t ip;
	unsigned short port;
	int retry = 0;
	int error = 0;
	goa_uint32 sleepMs = 50;

	if(serverlist->abortupdate)
	{
		goto abort;
	}

retryRecv:
	len = recv(serverlist->slsocket, data + oldlen, sizeof(data) - oldlen - 1, 0);

	if (len == SOCKET_ERROR || len == 0)
	{
		error = Get_Last_Error();

		if(error == TCP_BLOCKING_ERROR && (retry < totalRetry))
		{
			retry++;
			Com_DPrintf(DEVELOPER_MSG_GAMESPY, "Retrying Gamespy TCP List RECV, Attempt %i of %i.\n", retry, totalRetry);
			msleep(sleepMs);
			sleepMs = sleepMs + 10;
			Sys_SendKeyEvents (); /* FS: Check for aborts */
			goto retryRecv;
		}
		else
		{
			Com_DPrintf(DEVELOPER_MSG_GAMESPY, "Error during TCP List RECV: %s\n", NET_ErrorString());
abort:
			Close_TCP_Socket(&serverlist->slsocket);
			serverlist->slsocket = INVALID_SOCKET;
			oldlen = 0; //clear data so it can be used again
			ServerListHalt(serverlist);
			ServerListModeChange(serverlist, sl_querying);
			return GE_NOCONNECT;
		}
	}

	oldlen += len;

	Com_DPrintf(DEVELOPER_MSG_GAMESPY, "List xfer data: %s\n", data);

	p = data;
	while (p - data <= oldlen - 6)
	{
		if (strncmp(p,"\\final\\",7) == 0 || serverlist->abortupdate)
		{
			closesocket(serverlist->slsocket);
			serverlist->slsocket = INVALID_SOCKET;
			oldlen = 0; //clear data so it can be used again
			if (serverlist->querytype == qt_grouprooms || serverlist->querytype == qt_masterinfo)
				ServerListModeChange(serverlist, sl_idle); //go idle if we are just getting a GR list
			else
				ServerListModeChange(serverlist, sl_querying);
			return 0; //get out!!
		}
		if (oldlen < 6) //no way it could be a full IP, quit
			break;
		if (serverlist->querytype == qt_grouprooms || serverlist->querytype == qt_masterinfo)
		{
			i = ServerListParseInfoList(serverlist, p,oldlen - (p - data));
			if (i < 0) //the data was in a bad format, abort!
				serverlist->abortupdate = 1;
			else if (i == 0) //not enough info yet, read more!
				break;
			else
				p += i;

		} else
		{
			memcpy(&ip,p,4);
			p += 4;
			memcpy(&port,p,2);
			p += 2;
			ServerListAddServer(serverlist, ip, ntohs(port), serverlist->querytype);
		}
	}
	oldlen = oldlen - (p - data);
	memmove(data,p,oldlen); //shift it over
	return 0;
}

//loop through pending queries and send out new ones

static GError ServerListQueryLoop(GServerList serverlist)
{
	int i, scount = 0, error, final;
	fd_set read_fd;
	struct timeval timeout = {0,0};
	char indata[1500];
	struct sockaddr_in saddr;
	socklen_t saddrlen = sizeof(saddr);
	int server_timeout = bound(100, cl_master_server_timeout->intValue, 9000); /* FS: Now a CVAR */
	static qboolean firsttime = true;
	GServer server;

//first, check for available data
	FD_ZERO(&read_fd);

	if(!firsttime)
	{
		for (i = 0 ; i < serverlist->maxupdates; i++)
		{
			FD_SET(serverlist->updatelist[i].s, &read_fd);
			selectsocket(serverlist->updatelist[i].s + 1, NULL, &read_fd, NULL, &timeout);

			if (FD_ISSET(serverlist->updatelist[i].s, &read_fd))
			{
				if (serverlist->updatelist[i].currentserver != NULL) //there is a server waiting
				{
					scount++;
				}
				else
				{
					continue;
				}

				error = recvfrom(serverlist->updatelist[i].s, indata, sizeof(indata) - 1, 0,(struct sockaddr *)&saddr, &saddrlen);

				if (saddr.sin_addr.s_addr != serverlist->updatelist[i].saddr.sin_addr.s_addr ||
						saddr.sin_port != serverlist->updatelist[i].saddr.sin_port)
					continue; //it wasn't from this server

				if (SOCKET_ERROR != error) //we got data
				{
					indata[error] = 0; //truncate and parse it
					final = (strstr(indata,"\\final\\") != NULL);
					server = serverlist->updatelist[i].currentserver;
					if (server->ping == 9999) //set the ping
					{
						server->ping = (short)(current_time() - serverlist->updatelist[i].starttime);
					}

					ServerParseKeyVals(server, indata); 
					if (final) //it's all done
					{
						serverlist->CallBackFn(serverlist, 
												LIST_PROGRESS, 
												serverlist->instance,
												server,
												(void *)(intptr_t)((serverlist->nextupdate * 100) / ArrayLength(serverlist->servers))); //percent done
						serverlist->updatelist[i].currentserver = NULL; //reuse the updatelist
					} 
					else
					{
						serverlist->updatelist[i].currentserver = NULL; //reuse the updatelist
					}
				}
				else
				{
					error = Get_Last_Error();
					if ((error == TCP_BLOCKING_ERROR) && (current_time() - serverlist->updatelist[i].starttime < server_timeout) )
					{
//						Com_DPrintf("Slow down cowboy..\n");
						continue;
					}
					else /* FS: If we didn't get WOULDBLOCK or if it just kept going past the server_timeout threshold then just give up. */
					{
						Com_DPrintf(DEVELOPER_MSG_GAMESPY, "Error during gamespy recv %s.  Removing server from list.\n", NET_ErrorString());
						serverlist->updatelist[i].currentserver = NULL; //reuse the updatelist
					}
				}
			}
		}
	}

	if ( (serverlist->abortupdate) || (serverlist->nextupdate >= ArrayLength(serverlist->servers) && scount == 0) ) 
	{ //we are done!!
		firsttime = true;
		FreeUpdateList(serverlist);
		ServerListModeChange(serverlist, sl_idle);
		return 0;
	}

	// now, send out queries on available sockets
	for (i = 0 ; i < serverlist->maxupdates && serverlist->nextupdate < ArrayLength(serverlist->servers) ; i++)
	{
		if (serverlist->updatelist[i].currentserver == NULL) //it's availalbe
		{
			server = *(GServer *)ArrayNth(serverlist->servers,serverlist->nextupdate);
			serverlist->nextupdate++;
			serverlist->updatelist[i].currentserver = server;
			serverlist->updatelist[i].saddr.sin_family = AF_INET;
			serverlist->updatelist[i].saddr.sin_addr.s_addr = inet_addr(ServerGetAddress(server));

			Com_DPrintf(DEVELOPER_MSG_GAMESPY, "Attempting to ping[%i]: %s:%i\n", i, ServerGetAddress(server), ServerGetQueryPort(server));

			serverlist->updatelist[i].saddr.sin_port = htons((short)ServerGetQueryPort(server));

			error = sendto(serverlist->updatelist[i].s,querystrings[server->querytype] ,querylengths[server->querytype],0,(struct sockaddr *)&serverlist->updatelist[i].saddr,sizeof(struct sockaddr_in));

			if (SOCKET_ERROR != error)
			{
				serverlist->updatelist[i].starttime = current_time();
			}
			else
			{
				Com_DPrintf(DEVELOPER_MSG_GAMESPY, "Error pinging server[%i] %s: %s\n", i, ServerGetAddress(server), NET_ErrorString());
				serverlist->updatelist[i].currentserver = NULL; // reuse the update index
			}
		}
	}

	firsttime = false;
	return 0;
}

/* ServerListThink
------------------
For use with Async Updates. This needs to be called every ~10ms for list processing and
updating to occur during async server list updates */
GError ServerListThink(GServerList serverlist)
{
	switch (serverlist->state)
	{
		case sl_idle:
			return 0;
		case sl_listxfer:
				 //read the data
				return ServerListReadList(serverlist);
		case sl_lanlist:
				return ServerListLANList(serverlist);
		case sl_querying: 
				//do some queries
				return ServerListQueryLoop(serverlist);
	}

	return 0;
}

/* ServerListHalt
-----------------
Halts the current update batch */
GError ServerListHalt(GServerList serverlist)
{
	if (serverlist->state != sl_idle)
		serverlist->abortupdate = 1;

	return 0;
}

/* ServerListClear
------------------
Clear and free all of the servers from the server list.
List must be in the sl_idle state */
GError ServerListClear(GServerList serverlist)
{
	if (serverlist->state != sl_idle)
		return GE_BUSY;
	//fastest way to clear is kill and recreate
	ArrayFree(serverlist->servers);
	serverlist->servers = ArrayNew(sizeof(GServer), SERVER_GROWBY, ServerFree);
	TableFree(serverlist->keylist);
	serverlist->keylist = TableNew2(sizeof(char *),LIST_NUMKEYBUCKETS,LIST_NUMKEYCHAINS,GStringHash, GCaseInsensitiveCompare, GStringFree);

	return 0;
}

/* ServerListState
------------------
Returns the current state of the server list */
GServerListState ServerListState(GServerList serverlist)
{
	return serverlist->state;
}

/* ServerListErrorDesc
----------------------
Returns a static string description of the specified error */
const char *ServerListErrorDesc(GServerList serverlist, GError error)
{
	switch (error)
	{
	case GE_NOERROR:
		return "";
	case GE_NOSOCKET:
		return "Unable to create socket";
	case GE_NODNS:
		return "Unable to resolve master";
	case GE_NOCONNECT:
		return "Connection to master reset";
	case GE_BUSY:
		return "Server List is busy";
	case GE_DATAERROR:
		return "Unexpected data in server list";
	}
	return "UNKNOWN ERROR CODE";
}

/* ServerListGetServer
----------------------
Returns the server at the specified index, or NULL if the index is out of bounds */
GServer ServerListGetServer(GServerList serverlist, int index)
{
	if (index < 0 || index >= ArrayLength(serverlist->servers))
		return NULL;
	return *(GServer *)ArrayNth(serverlist->servers,index);
}

/* ServerListCount
------------------
Returns the number of servers on the specified list. Indexing is 0 based, so
the actual server indexes are 0 <= valid index < Count */
int ServerListCount(GServerList serverlist)
{
	return ArrayLength(serverlist->servers);
}

/****
Comparision Functions
***/
static int IntKeyCompare(const void *entry1, const void *entry2)
{
	GServer server1 = *(GServer *)entry1, server2 = *(GServer *)entry2;
	int diff;
	diff = ServerGetIntValue(server1, g_sortserverlist->sortkey, 0) - 
			ServerGetIntValue(server2, g_sortserverlist->sortkey, 0);
	if (!g_sortserverlist->sortascending) 
		diff = -diff;
	return diff;
}

static int FloatKeyCompare(const void *entry1, const void *entry2)
{
	GServer server1 = *(GServer *)entry1, server2 = *(GServer *)entry2;
	double f = ServerGetFloatValue(server1, g_sortserverlist->sortkey, 0) - 
			ServerGetFloatValue(server2, g_sortserverlist->sortkey, 0);
	if (!g_sortserverlist->sortascending) 
		f = -f;
	if ((float)f > (float)0.0) 
		return 1;
	else if ((float)f < (float)0.0) 
		return -1; 
	else 
		return 0;
}

static int StrCaseKeyCompare(const void *entry1, const void *entry2)
{
	GServer server1 = *(GServer *)entry1, server2 = *(GServer *)entry2;
	int diff = strcmp(ServerGetStringValue(server1, g_sortserverlist->sortkey, ""),
				ServerGetStringValue(server2, g_sortserverlist->sortkey, ""));
	if (!g_sortserverlist->sortascending) 
		diff = -diff;
	return diff;
}

static int StrNoCaseKeyCompare(const void *entry1, const void *entry2)
{
	GServer server1 = *(GServer *)entry1, server2 = *(GServer *)entry2;
	int diff = strcasecmp(ServerGetStringValue(server1, g_sortserverlist->sortkey, ""),
				ServerGetStringValue(server2, g_sortserverlist->sortkey, ""));
	if (!g_sortserverlist->sortascending) 
		diff = -diff;
	return diff;
}

/* ServerListSort
-----------------
Sort the server list in either ascending or descending order using the 
specified comparemode.
sortkey can be a normal server key, or "ping" or "hostaddr" */
void ServerListSort(GServerList serverlist, gbool ascending, const char *sortkey, GCompareMode comparemode)
{
	ArrayCompareFn comparator;
	switch (comparemode)
	{
		case cm_int:
			comparator = IntKeyCompare;
			break;
		case cm_float:
			comparator = FloatKeyCompare;
			break;
		case cm_strcase:
			comparator = StrCaseKeyCompare;
			break;
		case cm_stricase:
			comparator = StrNoCaseKeyCompare;
			break;
		default:
			comparator = StrCaseKeyCompare;
	}
	serverlist->sortkey = sortkey;
	serverlist->sortascending = ascending;
	g_sortserverlist = serverlist;
	ArraySort(serverlist->servers,comparator);
}
