/*
Copyright (C) 1996-1997 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the included (GNU.txt) GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/


//connection notes
//The connection is like http.
//The stream starts with a small header.
//The header is a list of 'key: value' pairs, separated by new lines.
//The header ends with a totally blank line.
//to record an mvd from telnet or somesuch, you would use:
//"QTV\nRAW: 1\n\n"

//VERSION: a list of the different qtv protocols supported. Multiple versions can be specified. The first is assumed to be the preferred version.
//RAW: if non-zero, send only a raw mvd with no additional markup anywhere (for telnet use). Doesn't work with challenge-based auth, so will only be accepted when proxy passwords are not required.
//AUTH: specifies an auth method, the exact specs varies based on the method
//		PLAIN: the password is sent as a PASSWORD line
//		MD4: the server responds with an "AUTH: MD4\n" line as well as a "CHALLENGE: somerandomchallengestring\n" line, the client sends a new 'initial' request with CHALLENGE: MD4\nRESPONSE: hexbasedmd4checksumhere\n"
//		etc: same idea as md4
//		CCITT: same as md4, but using the CRC stuff common to all quake engines. should not be used.
//		if the supported/allowed auth methods don't match, the connection is silently dropped.
//SOURCE: which stream to play from, DEFAULT is special. Without qualifiers, it's assumed to be a tcp address.
//COMPRESSION: Suggests a compression method (multiple are allowed). You'll get a COMPRESSION response, and compression will begin with the binary data.
//SOURCELIST: Asks for a list of active sources from the proxy.
//DEMOLIST:	Asks for a list of available mvd demos.

//Response:
//if using RAW, there will be no header or anything
//Otherwise you'll get a QTVSV %f response (%f being the protocol version being used)
//same structure, terminated by a \n
//AUTH: Server requires auth before proceeding. If you don't support the method the server says, then, urm, the server shouldn't have suggested it.
//CHALLENGE: used with auth
//COMPRESSION: Method of compression used. Compression begins with the raw data after the connection process.
//ASOURCE: names a source
//ADEMO: gives a demo file name




#include "qtv.h"
#include <string.h>

#include "bsd_string.h"

#ifndef _WIN32
#include <dirent.h>
#include <signal.h>
#endif

#include <stddef.h>
#ifdef UNIXSOCKETS
#include <sys/un.h>
#endif

#define RECONNECT_TIME (1000*30)
#define RECONNECT_TIME_DEMO (1000*5)
#define UDPRECONNECT_TIME (1000)
#define STATUSPOLL_TIME 1000*30
#define PINGSINTERVAL_TIME (1000*5)
#define UDPTIMEOUT_LENGTH (1000*20)
#define UDPPACKETINTERVAL (1000/72)

void Net_SendConnectionMVD(sv_t *qtv, oproxy_t *prox);
void Net_QueueUpstream(sv_t *qtv, int size, char *buffer);


qboolean	NET_StringToAddr (char *s, netadr_t *sadr, int defaultport)
{
	struct hostent	*h;
	char	*colon;
	char	copy[128];

	memset (sadr, 0, sizeof(netadr_t));

#ifdef USEIPX
	if ((strlen(s) >= 23) && (s[8] == ':') && (s[21] == ':'))	// check for an IPX address
	{
		unsigned int val;

		((struct sockaddr_ipx *)sadr)->sa_family = AF_IPX;
		copy[2] = 0;
		DO(0, sa_netnum[0]);
		DO(2, sa_netnum[1]);
		DO(4, sa_netnum[2]);
		DO(6, sa_netnum[3]);
		DO(9, sa_nodenum[0]);
		DO(11, sa_nodenum[1]);
		DO(13, sa_nodenum[2]);
		DO(15, sa_nodenum[3]);
		DO(17, sa_nodenum[4]);
		DO(19, sa_nodenum[5]);
		sscanf (&s[22], "%u", &val);
		((struct sockaddr_ipx *)sadr)->sa_socket = htons((unsigned short)val);
	}
	else
#endif
#if 0//def IPPROTO_IPV6
		if (getaddrinfo)
	{//ipv6 method (can return ipv4 addresses too)
		struct addrinfo *addrinfo, *pos;
		struct addrinfo udp6hint;
		int error;
		char *port;
		char dupbase[256];
		int len;

		memset(&udp6hint, 0, sizeof(udp6hint));
		udp6hint.ai_family = 0;//Any... we check for AF_INET6 or 4
		udp6hint.ai_socktype = SOCK_DGRAM;
		udp6hint.ai_protocol = IPPROTO_UDP;

		port = s + strlen(s);
		while(port >= s)
		{
			if (*port == ':')
			break;
			port--;
		}

		if (port == s)
			port = NULL;
		if (port)
		{
			len = port - s;
			if (len > sizeof(dupbase))
				len = sizeof(dupbase);
			strlcpy(dupbase, s, len);
			error = getaddrinfo(dupbase, port+1, &udp6hint, &addrinfo);
		}
		else
			error = EAI_NONAME;
		if (error)	//failed, try string with no port.
			error = getaddrinfo(s, NULL, &udp6hint, &addrinfo);	//remember, this func will return any address family that could be using the udp protocol... (ip4 or ip6)
		if (error)
		{
			return false;
		}
		((struct sockaddr*)sadr)->sa_family = 0;
		for (pos = addrinfo; pos; pos = pos->ai_next)
		{
			switch(pos->ai_family)
			{
			case AF_INET6:
				if (((struct sockaddr_in *)sadr)->sin_family == AF_INET6)
					break;	//first one should be best...
				//fallthrough
			case AF_INET:
				memcpy(sadr, addrinfo->ai_addr, addrinfo->ai_addrlen);
				if (pos->ai_family == AF_INET)
					goto dblbreak;	//don't try finding any more, this is quake, they probably prefer ip4...
				break;
			}
		}
dblbreak:
		pfreeaddrinfo (addrinfo);
		if (!((struct sockaddr*)sadr)->sa_family)	//none suitablefound
			return false;
	}
	else
#endif
	{ //old fashioned method
		struct sockaddr_in *sin = (struct sockaddr_in *)sadr->sockaddr;
		sin->sin_family = AF_INET;

		sin->sin_port = htons(defaultport);

		strcpy (copy, s);
		// strip off a trailing :port if present
		for (colon = copy ; *colon ; colon++)
			if (*colon == ':')
			{
				*colon = 0;
				sin->sin_port = htons((short)atoi(colon+1));
			}

		if (copy[0] >= '0' && copy[0] <= '9')	//this is the wrong way to test. a server name may start with a number.
		{
			*(int *)&sin->sin_addr = inet_addr(copy);
		}
		else
		{
			if (! (h = gethostbyname(copy)) )
				return 0;
			if (h->h_addrtype != AF_INET)
				return 0;
			*(int *)&sin->sin_addr = *(int *)h->h_addr_list[0];
		}
	}

	return true;
}

qboolean Net_CompareAddress(netadr_t *s1, netadr_t *s2, int qp1, int qp2)
{
	struct sockaddr *g1=(void*)s1->sockaddr, *g2=(void*)s2->sockaddr;
	if (g1->sa_family != g2->sa_family)
		return false;
	switch(g1->sa_family)
	{
	default:
		return true;
	case AF_INET:
		{
			struct sockaddr_in *i1=(void*)s1->sockaddr, *i2=(void*)s2->sockaddr;
			if (*(unsigned int*)&i1->sin_addr != *(unsigned int*)&i2->sin_addr)
				return false;
			if (i1->sin_port != i2->sin_port && qp1 != qp2)	//allow qports to match instead of ports, if required.
				return false;
			return true;
		}

	case AF_INET6:
		{
			struct sockaddr_in6 *i1=(void*)s1->sockaddr, *i2=(void*)s2->sockaddr;
			if (((unsigned int*)&i1->sin6_addr)[0] != ((unsigned int*)&i2->sin6_addr)[0])
				return false;
			if (((unsigned int*)&i1->sin6_addr)[1] != ((unsigned int*)&i2->sin6_addr)[1])
				return false;
			if (((unsigned int*)&i1->sin6_addr)[2] != ((unsigned int*)&i2->sin6_addr)[2])
				return false;
			if (((unsigned int*)&i1->sin6_addr)[3] != ((unsigned int*)&i2->sin6_addr)[3])
				return false;
			if (i1->sin6_port != i2->sin6_port && qp1 != qp2)	//allow qports to match instead of ports, if required.
				return false;
			return true;
		}
	}
	return false;
}

void Net_TCPListen(cluster_t *cluster, int port, int socketid)
{
	SOCKET sock;

	union
	{
		struct sockaddr		s;
		struct sockaddr_in	ipv4;
		struct sockaddr_in6	ipv6;
#ifdef UNIXSOCKETS
		struct sockaddr_un	un;
#endif
	} address;
	int prot;
	int addrsize;
	int _true = true;
//	int fromlen;

	unsigned long nonblocking = true;
	unsigned long v6only = false;
	const char *famname;

	switch(socketid)
	{
#ifdef UNIXSOCKETS
	case SG_UNIX:
		prot = AF_UNIX;
		memset(&address.un, 0, sizeof(address.un));
		address.un.sun_family = prot;
		memcpy(address.un.sun_path, "\0qtv", 4);
		addrsize = offsetof(struct sockaddr_un, sun_path[4]);
		famname = "unix";
		break;
#endif
	case SG_IPV6:
		prot = AF_INET6;
		memset(&address.ipv6, 0, sizeof(address.ipv6));
		address.ipv6.sin6_family = prot;
		address.ipv6.sin6_port = htons((u_short)port);
		addrsize = sizeof(struct sockaddr_in6);
		if (v6only)
			famname = "tcp6";
		else
			famname = "tcp";
		break;
	case SG_IPV4:
		prot = AF_INET;
		address.ipv4.sin_family = prot;
		address.ipv4.sin_addr.s_addr = INADDR_ANY;
		address.ipv4.sin_port = htons((u_short)port);
		addrsize = sizeof(struct sockaddr_in);
		famname = "tcp4";
		break;
	default:
		return;	//some kind of error. avoid unintialised warnings.
	}

	if (socketid==SG_IPV4 && !v6only && cluster->tcpsocket[SG_IPV6] != INVALID_SOCKET)
	{	//if we already have a hybrid ipv6 socket, don't bother with ipv4 too
		int sz = sizeof(v6only);
		if (getsockopt(cluster->tcpsocket[1], IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, &sz) == 0 && !v6only)
			port = 0;
	}

	if (cluster->tcpsocket[socketid] != INVALID_SOCKET)
	{
		closesocket(cluster->tcpsocket[socketid]);
		cluster->tcpsocket[socketid] = INVALID_SOCKET;

		Sys_Printf(cluster, "closed %s port\n", famname);
	}
	if (!port)
		return;

	if ((sock = socket (prot, SOCK_STREAM, 0)) == INVALID_SOCKET)
	{
		cluster->tcpsocket[socketid] = INVALID_SOCKET;
		return;
	}

	if (ioctlsocket (sock, FIONBIO, &nonblocking) == -1)
	{
		cluster->tcpsocket[socketid] = INVALID_SOCKET;
		closesocket(sock);
		return;
	}

#ifdef _WIN32 
	//win32 is so fucked up
//	setsockopt(newsocket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char *)&_true, sizeof(_true));
#endif
	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&_true, sizeof(_true));


	if (socketid == SG_IPV6)
	{
		if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, sizeof(v6only)) == -1)
			v6only = true;
		if (v6only)
			famname = "tcp6";
		else
			famname = "tcp";
	}

	if (bind (sock, &address.s, addrsize) == -1)
	{
		printf("socket bind error %i (%s)\n", qerrno, strerror(qerrno));
		closesocket(sock);
		return;
	}

	listen(sock, 2);	//don't listen for too many clients at once.

	Sys_Printf(cluster, "opened %s port %i\n", famname, port);

	cluster->tcpsocket[socketid] = sock;
}

char *strchrrev(char *str, char chr)
{
	char *firstchar = str;
	for (str = str + strlen(str)-1; str>=firstchar; str--)
		if (*str == chr)
			return str;

	return NULL;
}

void Net_SendQTVConnectionRequest(sv_t *qtv, char *authmethod, char *challenge)
{
	char *at;
	char *str;
	char hash[512];

	//due to mvdsv sucking and stuff, we try using raw connections where possibleso that we don't end up expecting a header.
	//at some point, this will be forced to 1
	qtv->parsingqtvheader = true;//!!*qtv->connectpassword;
	qtv->buffersize = 0;
	qtv->forwardpoint = 0;

	str =	"QTV\n";			Net_QueueUpstream(qtv, strlen(str), str);
	str =	"VERSION: 1\n";		Net_QueueUpstream(qtv, strlen(str), str);

	if (qtv->serverquery)
	{
		if (qtv->serverquery == 2)
		{
			str =	"DEMOLIST\n";		Net_QueueUpstream(qtv, strlen(str), str);
		}
		else
		{
			str =	"SOURCELIST\n";		Net_QueueUpstream(qtv, strlen(str), str);
		}
	}
	else
	{

		at = strchrrev(qtv->server, '@');
		if (at)
		{
			*at = '\0';
			str =	"SOURCE: ";		Net_QueueUpstream(qtv, strlen(str), str);

			if (strncmp(qtv->server, "tcp:", 4))
			{
				str = qtv->server;
				Net_QueueUpstream(qtv, strlen(str), str);
			}
			else
			{
				str = strchr(qtv->server, ':');
				if (str)
				{
					str++;
					Net_QueueUpstream(qtv, strlen(str), str);
				}
			}

			str =	"\n";			Net_QueueUpstream(qtv, strlen(str), str);
			*at = '@';
		}
		else
		{
			str =	"RECEIVE\n";	Net_QueueUpstream(qtv, strlen(str), str);
		}

		if (!qtv->parsingqtvheader)
		{
			str =	"RAW: 1\n";		Net_QueueUpstream(qtv, strlen(str), str);
		}
		else
		{
			if (authmethod)
			{
				if (!strcmp(authmethod, "PLAIN"))
				{
					str = "AUTH: PLAIN\n";	Net_QueueUpstream(qtv, strlen(str), str);
					str = "PASSWORD: \"";	Net_QueueUpstream(qtv, strlen(str), str);
					str = qtv->connectpassword;	Net_QueueUpstream(qtv, strlen(str), str);
					str = "\"\n";			Net_QueueUpstream(qtv, strlen(str), str);
				}
				/*else if (challenge && strlen(challenge)>=32 && !strcmp(authmethod, "CCITT"))
				{
					unsigned short crcvalue;
					str = "AUTH: CCITT\n";	Net_QueueUpstream(qtv, strlen(str), str);
					str = "PASSWORD: \"";	Net_QueueUpstream(qtv, strlen(str), str);

					snprintf(hash, sizeof(hash), "%s%s", challenge, qtv->connectpassword);
					crcvalue = QCRC_Block(hash, strlen(hash));
					sprintf(hash, "0x%X", (unsigned int)QCRC_Value(crcvalue));

					str = hash;				Net_QueueUpstream(qtv, strlen(str), str);
					str = "\"\n";			Net_QueueUpstream(qtv, strlen(str), str);
				}*/
				else if (challenge && strlen(challenge)>=8 && !strcmp(authmethod, "MD4"))
				{
					unsigned int md4sum[4];
					str = "AUTH: MD4\n";	Net_QueueUpstream(qtv, strlen(str), str);
					str = "PASSWORD: \"";	Net_QueueUpstream(qtv, strlen(str), str);

					snprintf(hash, sizeof(hash), "%s%s", challenge, qtv->connectpassword);
					Com_BlockFullChecksum (hash, strlen(hash), (unsigned char*)md4sum);
					sprintf(hash, "%X%X%X%X", md4sum[0], md4sum[1], md4sum[2], md4sum[3]);

					str = hash;				Net_QueueUpstream(qtv, strlen(str), str);
					str = "\"\n";			Net_QueueUpstream(qtv, strlen(str), str);
				}
				else if (challenge && strlen(challenge)>=8 && !strcmp(authmethod, "SHA1"))
				{
					unsigned char digest[20];
					str = "AUTH: SHA1\n";	Net_QueueUpstream(qtv, strlen(str), str);
					str = "PASSWORD: \"";	Net_QueueUpstream(qtv, strlen(str), str);

					snprintf(hash, sizeof(hash), "%s%s", challenge, qtv->connectpassword);
					CalcHash(&hash_sha1, (unsigned char*)digest, sizeof(digest), hash, strlen(hash));
					tobase64(hash, sizeof(hash), digest, hash_sha1.digestsize);

					str = hash;				Net_QueueUpstream(qtv, strlen(str), str);
					str = "\"\n";			Net_QueueUpstream(qtv, strlen(str), str);
				}
				else if (!strcmp(authmethod, "NONE"))
				{
					str = "AUTH: NONE\n";	Net_QueueUpstream(qtv, strlen(str), str);
					str = "PASSWORD: \n";	Net_QueueUpstream(qtv, strlen(str), str);
				}
				else
				{
					qtv->errored = ERR_PERMANENT;
					qtv->upstreambuffersize = 0;
					Sys_Printf(qtv->cluster, "Auth method %s was not usable\n", authmethod);
					return;
				}
			}
			else
			{
				str = "AUTH: SHA1\n";		Net_QueueUpstream(qtv, strlen(str), str);
				str = "AUTH: MD4\n";		Net_QueueUpstream(qtv, strlen(str), str);
//				str = "AUTH: CCITT\n";		Net_QueueUpstream(qtv, strlen(str), str);
				str = "AUTH: PLAIN\n";		Net_QueueUpstream(qtv, strlen(str), str);
				str = "AUTH: NONE\n";		Net_QueueUpstream(qtv, strlen(str), str);
			}
		}
	}
	str =	"\n";		Net_QueueUpstream(qtv, strlen(str), str);
}

qboolean Net_ConnectToTCPServer(sv_t *qtv, char *ip)
{
	int err;
	netadr_t from;
	unsigned long nonblocking = true;
	int afam;
	int pfam;
	int asz;

	if (!NET_StringToAddr(ip, &qtv->serveraddress, 27500))
	{
		Sys_Printf(qtv->cluster, "Stream %i: Unable to resolve %s\n", qtv->streamid, ip);
		strcpy(qtv->status, "Unable to resolve server\n");
		return false;
	}
	afam = ((struct sockaddr*)&qtv->serveraddress.sockaddr)->sa_family;
	pfam = ((afam==AF_INET6)?PF_INET6:PF_INET);
	asz = ((afam==AF_INET6)?sizeof(struct sockaddr_in6):sizeof(struct sockaddr_in));
	qtv->sourcesock = socket(pfam, SOCK_STREAM, IPPROTO_TCP);
	if (qtv->sourcesock == INVALID_SOCKET)
	{
		strcpy(qtv->status, "Network error\n");
		return false;
	}

	if (afam == AF_INET6)
	{
		qboolean v6only = true;
		setsockopt(qtv->sourcesock, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, sizeof(v6only));
	}

	memset(&from, 0, sizeof(from));
	((struct sockaddr*)&from)->sa_family = afam;
	if (bind(qtv->sourcesock, (struct sockaddr *)&from, sizeof(from)) == -1)
	{
		closesocket(qtv->sourcesock);
		qtv->sourcesock = INVALID_SOCKET;
		strcpy(qtv->status, "Network error\n");
		return false;
	}

	if (ioctlsocket (qtv->sourcesock, FIONBIO, &nonblocking) == -1)
	{
		closesocket(qtv->sourcesock);
		qtv->sourcesock = INVALID_SOCKET;
		strcpy(qtv->status, "Network error\n");
		return false;
	}

	if (connect(qtv->sourcesock, (struct sockaddr *)&qtv->serveraddress.sockaddr, asz) == INVALID_SOCKET)
	{
		err = qerrno;
		if (err != NET_EINPROGRESS && err != NET_EAGAIN && err != NET_EWOULDBLOCK)	//bsd sockets are meant to return EINPROGRESS, but some winsock drivers use EWOULDBLOCK instead. *sigh*...
		{
			closesocket(qtv->sourcesock);
			qtv->sourcesock = INVALID_SOCKET;
			strcpy(qtv->status, "Connection failed\n");
			return false;
		}
	}

	//make sure the buffers are empty. we could have disconnected prematurly
	qtv->upstreambuffersize = 0;
	qtv->buffersize = 0;
	qtv->forwardpoint = 0;

	//read the notes at the start of this file for what these text strings mean
	Net_SendQTVConnectionRequest(qtv, NULL, NULL);
	return true;
}
qboolean Net_ConnectToUDPServer(sv_t *qtv, char *ip)
{
	netadr_t from;
	unsigned long nonblocking = true;
	int afam, pfam, asz;

	if (!NET_StringToAddr(ip, &qtv->serveraddress, 27500))
	{
		Sys_Printf(qtv->cluster, "Stream %i: Unable to resolve %s\n", qtv->streamid, ip);
		return false;
	}
	afam = ((struct sockaddr*)&qtv->serveraddress.sockaddr)->sa_family;
	pfam = ((afam==AF_INET6)?PF_INET6:PF_INET);
	asz = ((afam==AF_INET6)?sizeof(struct sockaddr_in6):sizeof(struct sockaddr_in));
	qtv->sourcesock = socket(pfam, SOCK_DGRAM, IPPROTO_UDP);
	if (qtv->sourcesock == INVALID_SOCKET)
		return false;

	if (afam == AF_INET6)
	{
		qboolean v6only = true;
		setsockopt(qtv->sourcesock, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, sizeof(v6only));
	}

	memset(&from, 0, sizeof(from));
	((struct sockaddr*)&from)->sa_family = afam;
	if (bind(qtv->sourcesock, (struct sockaddr *)&from, asz) == -1)
	{
		closesocket(qtv->sourcesock);
		qtv->sourcesock = INVALID_SOCKET;
		return false;
	}

	if (ioctlsocket (qtv->sourcesock, FIONBIO, &nonblocking) == -1)
	{
		closesocket(qtv->sourcesock);
		qtv->sourcesock = INVALID_SOCKET;
		return false;
	}

	qtv->qport = Sys_Milliseconds()*1000+Sys_Milliseconds();

	return true;
}

qboolean DemoFilenameIsOkay(sv_t* qtv, char *fname, char *dir)
{
	int len;

	if (dir)
	{
		if (strchr(dir, '/'))
			return false;	//unix path seperator
		if (strchr(dir, '\\'))
			return false;	//windows path seperator
		if (strchr(dir, ':'))
			return false;	//mac path seperator
		if (*(dir) == '.')
			return false;
	}

	if (strchr(fname, '/'))
		return false;	//unix path seperator
	if (strchr(fname, '\\'))
		return false;	//windows path seperator
	if (strchr(fname, ':'))
		return false;	//mac path seperator
	if (*(fname) == '.')
		return false;

	//now make certain that the last four characters are '.mvd' and not something like '.cfg' perhaps
	len = strlen(fname);
	if (len < 5)
		return false;
	if (strcmp(fname+len-4, ".mvd"))
		return false;

	return true;

/*
	if (strchr(fname, '\\'))
	{
		char *s;
		Con_Printf("Warning: \\ characters in filename %s\n", fname);
		while((s = strchr(fname, '\\')))
			*s = '/';
	}

	if (strstr(fname, ".."))
	{
		Con_Printf("Error: '..' characters in filename %s\n", fname);
	}
	else if (fname[0] == '/')
	{
		Con_Printf("Error: absolute path in filename %s\n", fname);
	}
	else if (strstr(fname, ":")) //win32 drive seperator (or mac path seperator, but / works there and they're used to it)
	{
		Con_Printf("Error: absolute path in filename %s\n", fname);
	}
	else
		return false;
	return true;
*/
}

qboolean Net_ConnectToDemoServer(sv_t* qtv, char* ip, char* dir)
{
	char fullname[512];
	qtv->sourcesock = INVALID_SOCKET;
	if (DemoFilenameIsOkay(qtv, ip, dir))
	{
		if (!dir)
			snprintf(fullname, sizeof(fullname), "%s%s", qtv->cluster->demodir, ip);
		else
			snprintf(fullname, sizeof(fullname), "%s%s/%s", qtv->cluster->demodir, dir, ip);
		qtv->sourcefile = fopen(fullname, "rb");
	}
	else
		qtv->sourcefile = NULL;

	if (qtv->sourcefile)
	{
		char smallbuffer[17];
		fseek(qtv->sourcefile, 0, SEEK_END);
		qtv->filelength = ftell(qtv->sourcefile);

		//attempt to detect the end of the file
		fseek(qtv->sourcefile, 0-(long)sizeof(smallbuffer), SEEK_CUR);
		fread(smallbuffer, 1, 17, qtv->sourcefile);
		//0 is the time
		if (smallbuffer[1] == dem_all || smallbuffer[1] == dem_read) //mvdsv changed it to read...
		{
			//2,3,4,5 are the length
			if (smallbuffer[6] == svc_disconnect)
			{
				if (!strcmp(smallbuffer+7, "EndOfDemo"))
				{
					qtv->filelength -= 17;
				}
			}
		}

		fseek(qtv->sourcefile, 0, SEEK_SET);
		return true;
	}

	if (!dir)
		Sys_Printf(qtv->cluster, "Stream %i: Unable to open file %s\n", qtv->streamid, ip);
	else
		Sys_Printf(qtv->cluster, "Stream %i: Unable to open file %s in directory %s\n", qtv->streamid, ip, dir);
	return false;
}

qboolean Net_ConnectToDemoDirServer(sv_t* qtv, char *ip)
{
	char fullname[512];
	qtv->sourcesock = INVALID_SOCKET;
	snprintf(fullname, sizeof(fullname), "%s%s", qtv->cluster->demodir, ip);

	#ifdef _WIN32
	// TODO: code for Windows directories goes here
	// TODO: possible to do this without copy-pasting the entire GNU/Linux code below?
	// TODO: also, what about MAC OS X?
	Sys_Printf(qtv->cluster, "Windows support coming soon!\n");
	return false;
	#else
	{
		DIR *dir;
		struct dirent* ent;

		dir = opendir(fullname);
		if (dir)
		{
			char demoname[512];
			int current_demo = 0;
			int file_count = 0;
			int random_number = 1; // always this value if the directory contains one file

			// count the files, important for determining a random demo file
			while ((ent = readdir(dir)) != NULL)
			{
				int len;

				// only count files neding in .mvd
				len = strlen(ent->d_name);
				if (len < 5)
				{
					continue;
				}
				if (strcmp(ent->d_name+len-4, ".mvd"))
				{
					continue;
				}

				if (ent->d_type == DT_REG && *(ent->d_name) != '.')
					file_count++;	// only add non-hidden and regular files
			}

			if (file_count == 0)
			{
				// empty directory
				Sys_Printf(qtv->cluster, "Stream %i: Error: Directory has no demos.\n", qtv->streamid);
				closedir(dir);
				return false;
			}

			closedir(dir);
			dir = opendir(fullname);

			// FIXME: not sure if srand should only be called once somewhere?
			// FIXME: this is not really shuffling the demos, but does introduce some variety
			if (file_count > 1)
			{
				//srand(time(NULL));
				while ((random_number = rand()%file_count + 1) == qtv->last_random_number);
				qtv->last_random_number = random_number;
			}

			while (1) {
				int len;

				ent = readdir(dir);
				if (!ent)
				{
					// reached the end of the directory, shouldn't happen
					Sys_Printf(qtv->cluster, "Stream %i: Error: Reached end of directory (%s%s)\n", qtv->streamid, qtv->cluster->demodir, ip);
					closedir(dir);
					return false;
				}

				if (ent->d_type != DT_REG || *(ent->d_name) == '.')
				{
					continue;	// ignore hidden and non-regular files
				}

				//now make certain that the last four characters are '.mvd' and not something like '.cfg' perhaps
				len = strlen(ent->d_name);
				if (len < 5)
				{
					continue;
				}
				if (strcmp((ent->d_name)+len-4, ".mvd"))
				{
					continue;
				}

				if (++current_demo != random_number)
					continue;

				snprintf(demoname, sizeof(demoname), "%s/%s", ip, ent->d_name);
				qtv->sourcefile = fopen(demoname, "rb");
				closedir(dir);
				if (Net_ConnectToDemoServer(qtv, ent->d_name, ip) == true)
				{
					return true;
				}
				else
				{
					return false;
				}
			}
			closedir(dir);
		}
		else
		{
			Sys_Printf(qtv->cluster, "Stream %i: Unable to open directory %s\n", qtv->streamid, qtv->cluster->demodir);
			return false;
		}
	}
	#endif

	return false;
}

/*figures out the ip to connect to, and decides the protocol for it*/
char *Net_DiagnoseProtocol(sv_t *qtv)
{
	char *at;
	sourcetype_t type = SRC_BAD;
	char *ip = qtv->server;

	if (!strncmp(ip, "udp:", 4))
	{
		type = SRC_UDP;
		ip += 4;
	}
	else if (!strncmp(ip, "tcp:", 4))
	{
		type = SRC_TCP;
		ip += 4;
	}
	else if (!strncmp(ip, "demo:", 5))
	{
		type = SRC_DEMO;
		ip += 5;
	}
	else if (!strncmp(ip, "file:", 5))
	{
		type = SRC_DEMO;
		ip += 5;
	}
	else if (!strncmp(ip, "dir:", 4))
	{
		type = SRC_DEMODIR;
		ip += 4;
	}

	at = strchrrev(ip, '@');
	if (at && (type == SRC_DEMO || type == SRC_DEMODIR || type == SRC_TCP))
	{
		if (type == SRC_DEMO || type == SRC_DEMODIR)
			type = SRC_TCP;
		ip = at+1;
	}

	qtv->sourcetype = type;
	return ip;
}

qboolean Net_ConnectToServer(sv_t *qtv)
{
	char *ip = Net_DiagnoseProtocol(qtv);

	qtv->usequakeworldprotocols = false;
	qtv->pext1 = 0;
	qtv->pext2 = 0;

	if (qtv->sourcetype == SRC_DEMO || qtv->sourcetype == SRC_DEMODIR)
	{
		qtv->nextconnectattempt = qtv->curtime + RECONNECT_TIME_DEMO;	//wait half a minuite before trying to reconnect
	}
	else
		qtv->nextconnectattempt = qtv->curtime + RECONNECT_TIME;	//wait half a minuite before trying to reconnect

	switch(	qtv->sourcetype)
	{
	case SRC_DEMO:
		return Net_ConnectToDemoServer(qtv, ip, NULL);


	case SRC_DEMODIR:
		return Net_ConnectToDemoDirServer(qtv, ip);


	case SRC_UDP:
		qtv->usequakeworldprotocols = true;
		return Net_ConnectToUDPServer(qtv, ip);

	case SRC_TCP:
		return Net_ConnectToTCPServer(qtv, ip);

	default:
		Sys_Printf(qtv->cluster, "Unknown source type %s\n", ip);
		return false;
	}
}

void Net_QueueUpstream(sv_t *qtv, int size, char *buffer)
{
	if (qtv->usequakeworldprotocols)
		return;

	if (qtv->upstreambuffersize + size > sizeof(qtv->upstreambuffer))
	{
		Sys_Printf(qtv->cluster, "Stream %i: Upstream queue overflowed for %s\n", qtv->streamid, qtv->server);
		strcpy(qtv->status, "Upstream overflow");
		qtv->errored = ERR_RECONNECT;
		return;
	}
	memcpy(qtv->upstreambuffer + qtv->upstreambuffersize, buffer, size);
	qtv->upstreambuffersize += size;
}

qboolean Net_WriteUpstream(sv_t *qtv)
{
	int len;

	if (qtv->upstreambuffersize && qtv->sourcesock != INVALID_SOCKET)
	{
		len = send(qtv->sourcesock, qtv->upstreambuffer, qtv->upstreambuffersize, 0);
		if (len == 0)
			return false;
		if (len < 0)
		{
			int err = qerrno;
			if (err != NET_EWOULDBLOCK && err != NET_EAGAIN && err != NET_ENOTCONN)
			{
				if (err)
				{
					Sys_Printf(qtv->cluster, "Stream %i: Error: source socket error %i (%s)\n", qtv->streamid, err, strerror(err));
					strcpy(qtv->status, "Network error\n");
				}
				else
				{
					Sys_Printf(qtv->cluster, "Stream %i: Error: server %s disconnected\n", qtv->streamid, qtv->server);
					strcpy(qtv->status, "Server disconnected");
				}
				qtv->errored = ERR_RECONNECT;	//if the server is down, we'll detect it on reconnect
			}
			return false;
		}
		qtv->upstreambuffersize -= len;
		memmove(qtv->upstreambuffer, qtv->upstreambuffer + len, qtv->upstreambuffersize);
	}
	return true;
}

void SV_SendUpstream(sv_t *qtv, netmsg_t *nm)
{
	char size[2];
	int ffs = nm->cursize+2;	//qq fucked up and made his upstream inconsistent with downstream. ffs.

	size[0] = (ffs&0x00ff)>>0;
	size[1] = (ffs&0xff00)>>8;
	Net_QueueUpstream(qtv, 2, size);
	Net_QueueUpstream(qtv, nm->cursize, nm->data);
	Net_WriteUpstream(qtv);	//try and flush it
}

int SV_SayToUpstream(sv_t *qtv, char *message)
{
	char buffer[1024];
	netmsg_t nm;

	if (!qtv->upstreamacceptschat)
	{
#ifndef _MSC_VER
#warning This is incomplete!
#endif
		//Sys_Printf(qtv->cluster, "not forwarding say\n");
		return 0;
	}

	InitNetMsg(&nm, buffer, sizeof(buffer));

	WriteByte(&nm, qtv_clc_stringcmd);
	WriteString2(&nm, "say ");
	WriteString(&nm, message);
	SV_SendUpstream(qtv, &nm);

	return 1;
}

void SV_SayToViewers(sv_t *qtv, char *message)
{
	Fwd_SayToDownstream(qtv, message);
#ifndef _MSC_VER
	#warning Send to viewers here too
#endif
}

//This function 1: parses the 'don't delay' packets in the stream
//              2: returns the length of continuous data (that is, whole-packet bytes that have not been truncated by the networking layer)
//                 this means we know that the client proxies have valid data, at least from our side.
int SV_EarlyParse(sv_t *qtv, unsigned char *buffer, int remaining)
{
	int lengthofs;
	int length;
	int available = 0;
	while(1)
	{
		if (remaining < 2)
			return available;

		//buffer[0] is time

		switch (buffer[1]&dem_mask)
		{
		case dem_set:
			lengthofs = 0;	//to silence gcc, nothing more
			break;
		case dem_multiple:
			lengthofs = 6;
			break;
		default:
			lengthofs = 2;
			break;
		}

		if (lengthofs > 0)
		{
			if (lengthofs+4 > remaining)
				return available;

			length = (buffer[lengthofs]<<0) + (buffer[lengthofs+1]<<8) + (buffer[lengthofs+2]<<16) + (buffer[lengthofs+3]<<24);

			length += lengthofs+4;
			if (length > MAX_MSGLEN)
				printf("Probably corrupt mvd (length %i)\n", length);
		}
		else
			length = 10;

		if (remaining < length)
			return available;

		if ((buffer[1]&dem_mask) == dem_all && (buffer[1] & ~dem_mask) && qtv->sourcetype != SRC_DEMO)	//dem_qtvdata
		{
			ParseMessage(qtv, buffer+lengthofs+4, length - (lengthofs+4), buffer[1], 0xffffffff);
		}

		remaining -= length;
		available += length;
		buffer += length;
	}
}

qboolean Net_ReadStream(sv_t *qtv)
{
	int maxreadable;
	int read;
	void *buffer;
	int err;

	maxreadable = MAX_PROXY_BUFFER - qtv->buffersize;
	if (!maxreadable)
		return true;	//this is bad!
	buffer = qtv->buffer + qtv->buffersize;

	if (qtv->sourcefile)
	{
		if (maxreadable > PREFERRED_PROXY_BUFFER-qtv->buffersize)
			maxreadable = PREFERRED_PROXY_BUFFER-qtv->buffersize;
		if (maxreadable<=0)
			return true;

		//reuse read a little...
		read = ftell(qtv->sourcefile);
		if (read+maxreadable > qtv->filelength)
			maxreadable = qtv->filelength-read;	//clamp to the end of the file
								//even if that 'end' is before the svc_disconnect

		read = fread(buffer, 1, maxreadable, qtv->sourcefile);
	}
	else
	{
		unsigned int errsize;
		errsize = sizeof(err);
		err = 0;
		getsockopt(qtv->sourcesock, SOL_SOCKET, SO_ERROR, (char*)&err, &errsize);
		if (err == NET_ECONNREFUSED)
		{
			Sys_Printf(qtv->cluster, "Stream %i: Error: server %s refused connection\n", qtv->streamid, qtv->server);
			closesocket(qtv->sourcesock);
			qtv->sourcesock = INVALID_SOCKET;
			qtv->upstreambuffersize = 0;	//probably contains initial connection request info
			return false;
		}

		read = recv(qtv->sourcesock, buffer, maxreadable, 0);
	}
	if (read > 0)
	{
		qtv->buffersize += read;
		if (!qtv->cluster->lateforward && !qtv->parsingqtvheader)	//qtv header being the auth part of the connection rather than the stream
		{
			int forwardable;
			//this has the effect of not only parsing early packets, but also saying how much complete data there is.
			forwardable = SV_EarlyParse(qtv, qtv->buffer+qtv->forwardpoint, qtv->buffersize - qtv->forwardpoint);
			if (forwardable > 0)
			{
				SV_ForwardStream(qtv, qtv->buffer+qtv->forwardpoint, forwardable);
				qtv->forwardpoint += forwardable;
			}
		}
	}
	else
	{
		if (read == 0)
			err = 0;
		else
			err = qerrno;
		if (read == 0 || (err != NET_EWOULDBLOCK && err != NET_EAGAIN && err != NET_ENOTCONN))	//ENOTCONN can be returned whilst waiting for a connect to finish.
		{
			if (qtv->sourcefile)
				Sys_Printf(qtv->cluster, "Stream %i: Error: End of file\n", qtv->streamid);
			else if (read)
				Sys_Printf(qtv->cluster, "Stream %i: Error: source socket error %i (%s)\n", qtv->streamid, qerrno, strerror(qerrno));
			else
				Sys_Printf(qtv->cluster, "Stream %i: Error: server %s disconnected\n", qtv->streamid, qtv->server);
			if (qtv->sourcesock != INVALID_SOCKET)
			{
				closesocket(qtv->sourcesock);
				qtv->sourcesock = INVALID_SOCKET;
			}
			if (qtv->sourcefile)
			{
				fclose(qtv->sourcefile);
				qtv->sourcefile = NULL;
			}
			return false;
		}
	}
	return true;
}

unsigned int Sys_Milliseconds(void)
{
#ifdef _WIN32
	#ifdef _MSC_VER
		#pragma comment(lib, "winmm.lib")
	#endif

#if 0
	static firsttime = 1;
	static starttime;
	if (firsttime)
	{
		starttime = timeGetTime() + 1000*20;
		firsttime = 0;
	}
	return timeGetTime() - starttime;
#endif



	return timeGetTime();
#else
	//assume every other system follows standards.
	unsigned int t;
	struct timeval tv;

	gettimeofday(&tv, NULL);
	t = ((unsigned int)tv.tv_sec)*1000 + (((unsigned int)tv.tv_usec)/1000);
	return t;
#endif
}
/*
void NetSleep(sv_t *tv)
{
	int m;
	struct timeval timeout;
	fd_set socketset;

	FD_ZERO(&socketset);
	m = 0;
	if (tv->sourcesock != INVALID_SOCKET)
	{
		FD_SET(tv->sourcesock, &socketset);
		if (tv->sourcesock >= m)
			m = tv->sourcesock+1;
	}
	if (tv->qwdsocket != INVALID_SOCKET)
	{
		FD_SET(tv->qwdsocket, &socketset);
		if (tv->sourcesock >= m)
			m = tv->sourcesock+1;
	}

#ifndef _WIN32
	#ifndef STDIN
		#define STDIN 0
	#endif
	FD_SET(STDIN, &socketset);
	if (STDIN >= m)
		m = STDIN+1;
#endif

	timeout.tv_sec = 100/1000;
	timeout.tv_usec = (100%1000)*1000;

	select(m, &socketset, NULL, NULL, &timeout);

#ifdef _WIN32
	for (;;)
	{
		char buffer[8192];
		char *result;
		char c;

		if (!kbhit())
			break;
		c = getch();

		if (c == '\n' || c == '\r')
		{
			printf("\n");
			if (tv->inputlength)
			{
				tv->commandinput[tv->inputlength] = '\0';
				result = Rcon_Command(tv->cluster, tv, tv->commandinput, buffer, sizeof(buffer), true);
				printf("%s", result);
				tv->inputlength = 0;
				tv->commandinput[0] = '\0';
			}
		}
		else if (c == '\b')
		{
			if (tv->inputlength > 0)
			{
				tv->inputlength--;
				tv->commandinput[tv->inputlength] = '\0';
			}
		}
		else
		{
			if (tv->inputlength < sizeof(tv->commandinput)-1)
			{
				tv->commandinput[tv->inputlength++] = c;
				tv->commandinput[tv->inputlength] = '\0';
			}
		}if (FD_ISSET(STDIN, &socketset))
		printf("\r%s \b", tv->commandinput);
	}
#else
	if (FD_ISSET(STDIN, &socketset))
	{
		char buffer[8192];
		char *result;
		tv->inputlength = read (0, tv->commandinput, sizeof(tv->commandinput));
		if (tv->inputlength >= 1)
		{
			tv->commandinput[tv->inputlength-1] = 0;        // rip off the /n and terminate

			if (tv->inputlength)
			{
				tv->commandinput[tv->inputlength] = '\0';
				result = Rcon_Command(tv, tv->commandinput, buffer, sizeof(buffer), true);
				printf("%s", result);
				tv->inputlength = 0;
				tv->commandinput[0] = '\0';
			}
		}
	}
#endif
}
*/

void Trim(char *s)
{
	char *f;
	f = s;
	while(*f <= ' ' && *f > '\0')
		f++;
	while(*f > ' ')
		*s++ = *f++;
	*s = '\0';
}

qboolean QTV_ConnectStream(sv_t *qtv, char *serverurl)
{
	if (qtv->sourcesock != INVALID_SOCKET)
	{
		closesocket(qtv->sourcesock);
		qtv->sourcesock = INVALID_SOCKET;
	}

	if (qtv->sourcefile)
	{
		fclose(qtv->sourcefile);
		qtv->sourcefile = NULL;
	}

	memcpy(qtv->server, serverurl, sizeof(qtv->server)-1);
	if (qtv->autodisconnect == AD_STATUSPOLL && !qtv->numviewers && !qtv->proxies)
	{
		char *ip;
		ip = Net_DiagnoseProtocol(qtv);
		if (!NET_StringToAddr(ip, &qtv->serveraddress, 27500))
		{
			Sys_Printf(qtv->cluster, "Stream %i: Unable to resolve %s\n", qtv->streamid, ip);
			return false;
		}
		return true;
	}

	*qtv->map.serverinfo = '\0';
	Info_SetValueForStarKey(qtv->map.serverinfo, "*version",	"FTEQTV",	sizeof(qtv->map.serverinfo));
	Info_SetValueForStarKey(qtv->map.serverinfo, "*qtv",		QTV_VERSION_STRING,	sizeof(qtv->map.serverinfo));
	Info_SetValueForStarKey(qtv->map.serverinfo, "hostname",	qtv->cluster->hostname,	sizeof(qtv->map.serverinfo));
	Info_SetValueForStarKey(qtv->map.serverinfo, "maxclients",	"99",	sizeof(qtv->map.serverinfo));
	if (!strncmp(qtv->server, "file:", 5))
		Info_SetValueForStarKey(qtv->map.serverinfo, "server",		"file",	sizeof(qtv->map.serverinfo));
	else
		Info_SetValueForStarKey(qtv->map.serverinfo, "server",		qtv->server,	sizeof(qtv->map.serverinfo));

	if (qtv->autodisconnect == AD_REVERSECONNECT)
	{	//added because of paranoia rather than need. Should never occur.
		printf("bug: autoclose==2\n");
		strcpy(qtv->status, "Network error\n");
		return false;
	}
	else if (!Net_ConnectToServer(qtv))
	{
		Sys_Printf(qtv->cluster, "Stream %i: Couldn't connect (%s)\n", qtv->streamid, qtv->server);
		return false;
	}

	if (qtv->sourcesock == INVALID_SOCKET)
	{
		qtv->parsetime = Sys_Milliseconds();
//		Sys_Printf(qtv->cluster, "Stream %i: Playing from file\n", qtv->streamid);
	}
	else
	{
		qtv->parsetime = Sys_Milliseconds() + qtv->cluster->anticheattime;
	}
	return true;
}

void QTV_CleanupMap(sv_t *qtv)
{
	int i;

	//free the bsp
	BSP_Free(qtv->map.bsp);
	qtv->map.bsp = NULL;

	//clean up entity state
	for (i = 0; i < ENTITY_FRAMES; i++)
	{
		if (qtv->map.frame[i].ents)
		{
			free(qtv->map.frame[i].ents);
			qtv->map.frame[i].ents = NULL;
		}
		if (qtv->map.frame[i].entnums)
		{
			free(qtv->map.frame[i].entnums);
			qtv->map.frame[i].entnums = NULL;
		}
	}
	memset(&qtv->map, 0, sizeof(qtv->map));
}

void QTV_DisconnectFromSource(sv_t *qtv)
{
		// close the source handle
	if (qtv->sourcesock != INVALID_SOCKET)
	{
		if (qtv->usequakeworldprotocols)
		{
			char dying[] = {clc_stringcmd, 'd', 'r', 'o', 'p', '\0'};
			Netchan_Transmit (qtv->cluster, &qtv->netchan, sizeof(dying), dying);
			Netchan_Transmit (qtv->cluster, &qtv->netchan, sizeof(dying), dying);
			Netchan_Transmit (qtv->cluster, &qtv->netchan, sizeof(dying), dying);
		}
		closesocket(qtv->sourcesock);
		qtv->sourcesock = INVALID_SOCKET;
	}
	if (qtv->sourcefile)
	{
		fclose(qtv->sourcefile);
		qtv->sourcefile = NULL;
	}

	//cancel downloads
	if (qtv->downloadfile)
	{
		fclose(qtv->downloadfile);
		qtv->downloadfile = NULL;
		unlink(qtv->downloadname);
		*qtv->downloadname = '\0';
	}
}

void QTV_Cleanup(sv_t *qtv, qboolean leaveadmins)
{	//disconnects the stream
	viewer_t *v;
	cluster_t *cluster;
	oproxy_t *prox;
	oproxy_t *old;

	cluster = qtv->cluster;

	//set connected viewers to a different stream
	if (cluster->viewserver == qtv)
		cluster->viewserver = NULL;
	for (v = cluster->viewers; v; v = v->next)
	{
	//warning fixme: honour leaveadmins
		if (v->server == qtv)
		{	//they were watching this one
			QW_SetViewersServer(qtv->cluster, v, NULL);
			QW_SetMenu(v, MENU_NONE);
			QTV_SayCommand(cluster, v->server, v, "menu");
			QW_PrintfToViewer(v, "Stream %s is closing\n", qtv->server);
		}
	}

	QTV_DisconnectFromSource(qtv);

	QTV_CleanupMap(qtv);

	//boot connected downstream proxies
	for (prox = qtv->proxies; prox; )
	{
		if (prox->file)
			fclose(prox->file);
		if (prox->sock != INVALID_SOCKET)
			closesocket(prox->sock);
		old = prox;
		prox = prox->next;
		free(old);
		cluster->numproxies--;
	}
}

void QTV_ShutdownStream(sv_t *qtv)
{
	sv_t *peer;
	cluster_t *cluster;
	Sys_Printf(qtv->cluster, "Stream %i: Closing source %s\n", qtv->streamid, qtv->server);

	QTV_Cleanup(qtv, false);

	//unlink it
	cluster = qtv->cluster;
	if (cluster->servers == qtv)
		cluster->servers = qtv->next;
	else
	{
		for (peer = cluster->servers; peer->next; peer = peer->next)
		{
			if (peer->next == qtv)
			{
				peer->next = qtv->next;
				break;
			}
		}
	}

	free(qtv);
	cluster->numservers--;
}









void SendClientCommand(sv_t *qtv, char *fmt, ...)
{
	va_list		argptr;
	char buf[1024];

	va_start (argptr, fmt);
	vsnprintf (buf, sizeof(buf), fmt, argptr);
	va_end (argptr);

	WriteByte(&qtv->netchan.message, clc_stringcmd);
	WriteString(&qtv->netchan.message, buf);
}






void ChooseFavoriteTrack(sv_t *tv)
{
	int frags, best, pnum;
	char buffer[64];

	frags = -10000;
	best = -1;
	if (tv->controller || tv->proxyplayer)
		best = tv->map.trackplayer;
	else
	{
		for (pnum = 0; pnum < MAX_CLIENTS; pnum++)
		{
			if (*tv->map.players[pnum].userinfo && !atoi(Info_ValueForKey(tv->map.players[pnum].userinfo, "*spectator", buffer, sizeof(buffer))))
			{
				if (tv->map.thisplayer == pnum)
					continue;
				if (frags < tv->map.players[pnum].frags)
				{
					best = pnum;
					frags = tv->map.players[pnum].frags;
				}
			}
		}
	}
	if (best != tv->map.trackplayer)
	{
		SendClientCommand (tv, "ptrack %i\n", best);
		tv->map.trackplayer = best;

		if (tv->usequakeworldprotocols)
			QW_StreamStuffcmd(tv->cluster, tv, "track %i\n", best);
	}
}






static const unsigned char chktbl[1024] = {
0x78,0xd2,0x94,0xe3,0x41,0xec,0xd6,0xd5,0xcb,0xfc,0xdb,0x8a,0x4b,0xcc,0x85,0x01,
0x23,0xd2,0xe5,0xf2,0x29,0xa7,0x45,0x94,0x4a,0x62,0xe3,0xa5,0x6f,0x3f,0xe1,0x7a,
0x64,0xed,0x5c,0x99,0x29,0x87,0xa8,0x78,0x59,0x0d,0xaa,0x0f,0x25,0x0a,0x5c,0x58,
0xfb,0x00,0xa7,0xa8,0x8a,0x1d,0x86,0x80,0xc5,0x1f,0xd2,0x28,0x69,0x71,0x58,0xc3,
0x51,0x90,0xe1,0xf8,0x6a,0xf3,0x8f,0xb0,0x68,0xdf,0x95,0x40,0x5c,0xe4,0x24,0x6b,
0x29,0x19,0x71,0x3f,0x42,0x63,0x6c,0x48,0xe7,0xad,0xa8,0x4b,0x91,0x8f,0x42,0x36,
0x34,0xe7,0x32,0x55,0x59,0x2d,0x36,0x38,0x38,0x59,0x9b,0x08,0x16,0x4d,0x8d,0xf8,
0x0a,0xa4,0x52,0x01,0xbb,0x52,0xa9,0xfd,0x40,0x18,0x97,0x37,0xff,0xc9,0x82,0x27,
0xb2,0x64,0x60,0xce,0x00,0xd9,0x04,0xf0,0x9e,0x99,0xbd,0xce,0x8f,0x90,0x4a,0xdd,
0xe1,0xec,0x19,0x14,0xb1,0xfb,0xca,0x1e,0x98,0x0f,0xd4,0xcb,0x80,0xd6,0x05,0x63,
0xfd,0xa0,0x74,0xa6,0x86,0xf6,0x19,0x98,0x76,0x27,0x68,0xf7,0xe9,0x09,0x9a,0xf2,
0x2e,0x42,0xe1,0xbe,0x64,0x48,0x2a,0x74,0x30,0xbb,0x07,0xcc,0x1f,0xd4,0x91,0x9d,
0xac,0x55,0x53,0x25,0xb9,0x64,0xf7,0x58,0x4c,0x34,0x16,0xbc,0xf6,0x12,0x2b,0x65,
0x68,0x25,0x2e,0x29,0x1f,0xbb,0xb9,0xee,0x6d,0x0c,0x8e,0xbb,0xd2,0x5f,0x1d,0x8f,
0xc1,0x39,0xf9,0x8d,0xc0,0x39,0x75,0xcf,0x25,0x17,0xbe,0x96,0xaf,0x98,0x9f,0x5f,
0x65,0x15,0xc4,0x62,0xf8,0x55,0xfc,0xab,0x54,0xcf,0xdc,0x14,0x06,0xc8,0xfc,0x42,
0xd3,0xf0,0xad,0x10,0x08,0xcd,0xd4,0x11,0xbb,0xca,0x67,0xc6,0x48,0x5f,0x9d,0x59,
0xe3,0xe8,0x53,0x67,0x27,0x2d,0x34,0x9e,0x9e,0x24,0x29,0xdb,0x69,0x99,0x86,0xf9,
0x20,0xb5,0xbb,0x5b,0xb0,0xf9,0xc3,0x67,0xad,0x1c,0x9c,0xf7,0xcc,0xef,0xce,0x69,
0xe0,0x26,0x8f,0x79,0xbd,0xca,0x10,0x17,0xda,0xa9,0x88,0x57,0x9b,0x15,0x24,0xba,
0x84,0xd0,0xeb,0x4d,0x14,0xf5,0xfc,0xe6,0x51,0x6c,0x6f,0x64,0x6b,0x73,0xec,0x85,
0xf1,0x6f,0xe1,0x67,0x25,0x10,0x77,0x32,0x9e,0x85,0x6e,0x69,0xb1,0x83,0x00,0xe4,
0x13,0xa4,0x45,0x34,0x3b,0x40,0xff,0x41,0x82,0x89,0x79,0x57,0xfd,0xd2,0x8e,0xe8,
0xfc,0x1d,0x19,0x21,0x12,0x00,0xd7,0x66,0xe5,0xc7,0x10,0x1d,0xcb,0x75,0xe8,0xfa,
0xb6,0xee,0x7b,0x2f,0x1a,0x25,0x24,0xb9,0x9f,0x1d,0x78,0xfb,0x84,0xd0,0x17,0x05,
0x71,0xb3,0xc8,0x18,0xff,0x62,0xee,0xed,0x53,0xab,0x78,0xd3,0x65,0x2d,0xbb,0xc7,
0xc1,0xe7,0x70,0xa2,0x43,0x2c,0x7c,0xc7,0x16,0x04,0xd2,0x45,0xd5,0x6b,0x6c,0x7a,
0x5e,0xa1,0x50,0x2e,0x31,0x5b,0xcc,0xe8,0x65,0x8b,0x16,0x85,0xbf,0x82,0x83,0xfb,
0xde,0x9f,0x36,0x48,0x32,0x79,0xd6,0x9b,0xfb,0x52,0x45,0xbf,0x43,0xf7,0x0b,0x0b,
0x19,0x19,0x31,0xc3,0x85,0xec,0x1d,0x8c,0x20,0xf0,0x3a,0xfa,0x80,0x4d,0x2c,0x7d,
0xac,0x60,0x09,0xc0,0x40,0xee,0xb9,0xeb,0x13,0x5b,0xe8,0x2b,0xb1,0x20,0xf0,0xce,
0x4c,0xbd,0xc6,0x04,0x86,0x70,0xc6,0x33,0xc3,0x15,0x0f,0x65,0x19,0xfd,0xc2,0xd3
};


unsigned char	COM_BlockSequenceCRCByte (void *base, int length, int sequence)
{
	unsigned short crc;
	const unsigned char	*p;
	unsigned char chkb[60 + 4];

	p = chktbl + (sequence % (sizeof(chktbl) - 4));

	if (length > 60)
		length = 60;
	memcpy (chkb, base, length);

	chkb[length] = (sequence & 0xff) ^ p[0];
	chkb[length+1] = p[1];
	chkb[length+2] = ((sequence>>8) & 0xff) ^ p[2];
	chkb[length+3] = p[3];

	length += 4;

	crc = QCRC_Block(chkb, length);

	crc &= 0xff;

	return crc;
}
void SetMoveCRC(sv_t *qtv, netmsg_t *msg)
{
	char *outbyte;
	outbyte = (char*)msg->data + msg->startpos+1;

	*outbyte = COM_BlockSequenceCRCByte(
				outbyte+1, msg->cursize - (msg->startpos+2),
				qtv->netchan.outgoing_sequence);
}





void QTV_ParseQWStream(sv_t *qtv)
{
	char buffer[1500];
	netadr_t from;
	unsigned int fromlen;
	int readlen;
	netmsg_t msg;
	fromlen = sizeof(from.sockaddr);	//bug: this won't work on (free)bsd

	for (;;)
	{
		from.tcpcon = NULL;
		readlen = recvfrom(qtv->sourcesock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&from.sockaddr, &fromlen);
		if (readlen < 0)
		{
			//FIXME: Check for error
			break;
		}
		if (readlen > sizeof(buffer)-1)
			break;	//oversized!

		buffer[readlen] = 0;
		if (*(int*)buffer == -1)
		{
			if (buffer[4] == 'c')
			{	//got a challenge
				strcpy(qtv->status, "Attemping connection\n");
				qtv->challenge = atoi(buffer+5);
				if (qtv->controller)
					sprintf(buffer, "connect %i %i %i \"%s\\*qtv\\1\\Qizmo\\2.9 notimer\"", 28, qtv->qport, qtv->challenge, qtv->controller->userinfo);
				else if (qtv->proxyplayer)
					sprintf(buffer, "connect %i %i %i \"%s\\name\\%s\"", 28, qtv->qport, qtv->challenge, "\\*ver\\fteqtv\\spectator\\0\\rate\\10000", qtv->cluster->hostname);
				else
					sprintf(buffer, "connect %i %i %i \"%s\\name\\%s\"", 28, qtv->qport, qtv->challenge, "\\*ver\\fteqtv\\spectator\\1\\rate\\10000", qtv->cluster->hostname);
				Netchan_OutOfBandSocket(qtv->cluster, qtv->sourcesock, &qtv->serveraddress, strlen(buffer), buffer);
				continue;
			}
			if (buffer[4] == 'n')
			{
				strlcpy(qtv->status, buffer+5, sizeof(qtv->status));
				Sys_Printf(qtv->cluster, "%s: %s", qtv->server, buffer+5);
				continue;
			}
			if (buffer[4] == 'j')
			{
				strcpy(qtv->status, "Waiting for gamestate\n");
				Netchan_Setup(qtv->sourcesock, &qtv->netchan, qtv->serveraddress, qtv->qport, true);

				qtv->map.trackplayer = -1;

				qtv->isconnected = true;
				qtv->timeout = qtv->curtime + UDPTIMEOUT_LENGTH;
				SendClientCommand(qtv, "new");
				Sys_Printf(qtv->cluster, "Stream %i: Connected!\n", qtv->streamid);
				continue;
			}
			Sys_Printf(qtv->cluster, "Stream %i: %s: unrecognized connectionless packet:\n%s\n", qtv->streamid, qtv->server, buffer+4);
			continue;
		}
		memset(&msg, 0, sizeof(msg));
		msg.cursize = readlen;
		msg.data = buffer;
		msg.maxsize = readlen;
		qtv->timeout = qtv->curtime + UDPTIMEOUT_LENGTH;
		if (!Netchan_Process(&qtv->netchan, &msg))
			continue;
		ParseMessage(qtv, (char*)msg.data + msg.readpos, msg.cursize - msg.readpos, dem_all, -1);

		qtv->oldpackettime = qtv->nextpackettime;
		qtv->nextpackettime = qtv->parsetime;
		qtv->parsetime = qtv->curtime;

		if (qtv->simtime < qtv->oldpackettime)
			qtv->simtime = qtv->oldpackettime;	//too old

		if (qtv->controller)
		{
			qtv->controller->maysend = true;
//if (qtv->controller->netchan.outgoing_sequence != qtv->controller->netchan.incoming_sequence)
//printf("bug is here\n");
		}
	}
}

#ifdef COMMENTARY
#include <speex/speex.h>
#endif

void QTV_CollectCommentry(sv_t *qtv)
{
#define usespeex 0
#ifdef COMMENTARY
	int samps;
	unsigned char buffer[8192+6];
	unsigned char *uchar;
	signed char *schar;
	int bytesleft;
	if (!qtv->comentrycapture)
	{
		if (0)
		{
//			if (usespeex)
//				qtv->comentrycapture = SND_InitCapture(11025, 16);
//			else
				qtv->comentrycapture = SND_InitCapture(11025, 8);
		}
		return;
	}

	while(1)
	{
		//the protocol WILL be different. Don't add compatibility for this code.
		buffer[0] = 0;
		buffer[1] = dem_audio;
		buffer[2] = 255;
		buffer[3] = 255;
		buffer[4] = 8;
		buffer[5] = 11*5;

	/*	if (usespeex)
		{

			SpeexBits bits;
			void *enc_state;

			int frame_size;

			spx_int16_t pcmdata[8192/2];

			samps=qtv->comentrycapture->update(qtv->comentrycapture, 2048, (char*)pcmdata);


			speex_bits_init(&bits);

			enc_state = speex_encoder_init(&speex_nb_mode);


			speex_encoder_ctl(enc_state,SPEEX_GET_FRAME_SIZE,&frame_size);


			speex_bits_reset(&bits);

			speex_encode_int(enc_state, (spx_int16_t*)pcmdata, &bits);

			samps = speex_bits_write(&bits, buffer+6, sizeof(buffer)-6);


			speex_bits_destroy(&bits);

			speex_encoder_destroy(enc_state);

		}
		else*/
		{
			samps=qtv->comentrycapture->update(qtv->comentrycapture, 2048, buffer+6);

			bytesleft = samps;
			schar = buffer+6;
			uchar = buffer+6;
			while(bytesleft-->0)
			{
				*schar++ = *uchar++ - 128;
			}
		}

		buffer[2] = samps&255;
		buffer[3] = samps>>8;

		if (samps)
			SV_ForwardStream(qtv, buffer, 6 + samps);

		if (samps < 64)
			break;
	}
#endif
}

void QTV_Run(sv_t *qtv)
{
	int from;
	int to;
	int lengthofs;
	unsigned int length;
	unsigned char *buffer;
	int oldcurtime;
	int packettime;

	if (qtv->numviewers == 0 && qtv->proxies == NULL)
	{
		if (qtv->autodisconnect == AD_WHENEMPTY)
		{
			Sys_Printf(qtv->cluster, "Stream %i: %s became inactive\n", qtv->streamid, qtv->server);
			qtv->errored = ERR_DROP;
		}
		else if (qtv->autodisconnect == AD_STATUSPOLL && qtv->isconnected)
		{
			/*switch to status polling instead of packet spamming*/
			qtv->errored = ERR_RECONNECT;
		}
	}
	if (qtv->errored)
	{
		if (qtv->errored == ERR_DISABLED)
		{
			//this keeps any connected proxies ticking over.
			//probably we should drop them instead - the connection will only be revived if one of them reconnects.
			SV_ForwardStream(qtv, NULL, 0);
			return;
		}
		else if (qtv->errored == ERR_PERMANENT)
		{
			QTV_Cleanup(qtv, false);	//frees various pieces of context
			qtv->errored = ERR_DISABLED;
			return;
		}
		else if (qtv->errored == ERR_DROP)
		{
			QTV_ShutdownStream(qtv);	//destroys the stream
			return;
		}
	}



//we will read out as many packets as we can until we're up to date
//note: this can cause real issues when we're overloaded for any length of time
//each new packet comes with a leading msec byte (msecs from last packet)
//then a type, an optional destination mask, and a 4byte size.
//the 4 byte size is probably excessive, a short would do.
//some of the types have thier destination mask encoded inside the type byte, yielding 8 types, and 32 max players.


//if we've no got enough data to read a new packet, we print a message and wait an extra two seconds. this will add a pause, connected clients will get the same pause, and we'll just try to buffer more of the game before playing.
//we'll stay 2 secs behind when the tcp stream catches up, however. This could be bad especially with long up-time.
//All timings are in msecs, which is in keeping with the mvd times, but means we might have issues after 72 or so days.
//the following if statement will reset the parse timer. It might cause the game to play too soon, the buffersize checks in the rest of the function will hopefully put it back to something sensible.

	oldcurtime = qtv->curtime;
	qtv->curtime = Sys_Milliseconds();
	if (oldcurtime > qtv->curtime)
	{
		Sys_Printf(qtv->cluster, "Time wrapped\n");
		qtv->parsetime = qtv->curtime;
	}


	if (qtv->errored == ERR_PAUSED)
	{
		if (!qtv->parsingconnectiondata)
			qtv->parsetime = qtv->curtime;
	}

	if (qtv->errored == ERR_RECONNECT)
	{
		qtv->buffersize = 0;
		qtv->forwardpoint = 0;
		QTV_DisconnectFromSource(qtv);
		qtv->isconnected = 0;
		qtv->errored = ERR_NONE;
		qtv->nextconnectattempt = qtv->curtime;	//make the reconnect happen _now_
	}


	if (qtv->sourcetype == SRC_UDP)
	{
		qtv->simtime += qtv->curtime - oldcurtime;

		if (qtv->simtime > qtv->nextpackettime)
			qtv->simtime = qtv->nextpackettime;	//too old

		if (!qtv->isconnected && (qtv->curtime >= qtv->nextconnectattempt || qtv->curtime < qtv->nextconnectattempt - (UDPRECONNECT_TIME+STATUSPOLL_TIME)))
		{
			if (qtv->errored == ERR_DISABLED)
			{
				strcpy(qtv->status, "Given up connecting\n");
			}
			else if (qtv->autodisconnect == AD_STATUSPOLL)
			{
				QTV_DisconnectFromSource(qtv);
				Netchan_OutOfBand(qtv->cluster, qtv->serveraddress, 13, "status\n");
				qtv->nextconnectattempt = qtv->curtime + STATUSPOLL_TIME;
				return;
			}
			else
			{
				strcpy(qtv->status, "Attemping challenge\n");
				if (qtv->sourcesock == INVALID_SOCKET && !qtv->sourcefile)
				{
					if (!QTV_ConnectStream(qtv, qtv->server))	//reconnect it
					{
						qtv->errored = ERR_PERMANENT;
					}
				}
				if (qtv->errored == ERR_NONE)
					Netchan_OutOfBandSocket(qtv->cluster, qtv->sourcesock, &qtv->serveraddress, 13, "getchallenge\n");
			}
			qtv->nextconnectattempt = qtv->curtime + UDPRECONNECT_TIME;
		}
		if (qtv->sourcesock == INVALID_SOCKET && !qtv->sourcefile)
			return;

		QTV_ParseQWStream(qtv);

		if (qtv->isconnected)
		{
			char buffer[128];
			netmsg_t msg;
			memset(&msg, 0, sizeof(msg));
			msg.data = buffer;
			msg.maxsize = sizeof(buffer);

			if (qtv->curtime >= qtv->timeout || qtv->curtime < qtv->timeout - UDPTIMEOUT_LENGTH*2)
			{
				Sys_Printf(qtv->cluster, "Stream %i: Timeout\n", qtv->streamid);
				qtv->isconnected = false;
				return;
			}

			if (qtv->controller && !qtv->controller->netchan.isnqprotocol)
			{
				qtv->netchan.outgoing_sequence = qtv->controller->netchan.incoming_sequence;
				if (qtv->maysend)
				{
					qtv->maysend = false;
					qtv->packetratelimiter = qtv->curtime;
				}
				else
					qtv->packetratelimiter = qtv->curtime + 1;
			}
			else
			{
				if (qtv->curtime < qtv->packetratelimiter - UDPPACKETINTERVAL*2)
					qtv->packetratelimiter = qtv->curtime;
			}
			if (qtv->curtime >= qtv->packetratelimiter)
			{
				if (qtv->curtime >= qtv->nextsendpings || qtv->curtime < qtv->nextsendpings - PINGSINTERVAL_TIME*2)
				{
					qtv->nextsendpings = qtv->curtime + PINGSINTERVAL_TIME;
					SendClientCommand(qtv, "pings\n");

				}
				ChooseFavoriteTrack(qtv);

				//if we froze somehow, don't speedcheat by a burst of 10000+ packets while we were frozen in a debugger or disk spinup or whatever
				if (qtv->packetratelimiter < qtv->curtime - UDPPACKETINTERVAL*2)
					qtv->packetratelimiter = qtv->curtime;

				if (qtv->map.trackplayer >= 0)
				{
					qtv->packetratelimiter += UDPPACKETINTERVAL;

					WriteByte(&msg, clc_tmove);
					WriteShort(&msg, qtv->map.players[qtv->map.trackplayer].current.origin[0]);
					WriteShort(&msg, qtv->map.players[qtv->map.trackplayer].current.origin[1]);
					WriteShort(&msg, qtv->map.players[qtv->map.trackplayer].current.origin[2]);
				}
				else if (qtv->controller)
				{
					qtv->packetratelimiter += UDPPACKETINTERVAL;

					if (qtv->controller->netchan.isnqprotocol)
					{
						memcpy(&qtv->controller->ucmds[0], &qtv->controller->ucmds[1], sizeof(qtv->controller->ucmds[0]));
						memcpy(&qtv->controller->ucmds[1], &qtv->controller->ucmds[2], sizeof(qtv->controller->ucmds[0]));
						qtv->controller->ucmds[2].msec = UDPPACKETINTERVAL;
					}

					WriteByte(&msg, clc_tmove);
					WriteShort(&msg, qtv->controller->origin[0]);
					WriteShort(&msg, qtv->controller->origin[1]);
					WriteShort(&msg, qtv->controller->origin[2]);

/*					qtv->controller->ucmds[0].angles[1] = qtv->curtime*120;
					qtv->controller->ucmds[1].angles[1] = qtv->curtime*120;
					qtv->controller->ucmds[2].angles[1] = qtv->curtime*120;
*/
					msg.startpos = msg.cursize;
					WriteByte(&msg, clc_move);
					WriteByte(&msg, 0);
					WriteByte(&msg, 0);
					WriteDeltaUsercmd(&msg, &nullcmd, &qtv->controller->ucmds[0]);
					WriteDeltaUsercmd(&msg, &qtv->controller->ucmds[0], &qtv->controller->ucmds[1]);
					WriteDeltaUsercmd(&msg, &qtv->controller->ucmds[1], &qtv->controller->ucmds[2]);

					SetMoveCRC(qtv, &msg);
				}
				else if (qtv->proxyplayer || qtv->map.trackplayer < 0)
				{
					usercmd_t *cmd[3];
					cmd[0] = &qtv->proxyplayerucmds[(qtv->proxyplayerucmdnum-2)%3];
					cmd[1] = &qtv->proxyplayerucmds[(qtv->proxyplayerucmdnum-1)%3];
					cmd[2] = &qtv->proxyplayerucmds[(qtv->proxyplayerucmdnum-0)%3];

					cmd[2]->angles[0] = (qtv->proxyplayerangles[0]/360)*0x10000;
					cmd[2]->angles[1] = (qtv->proxyplayerangles[1]/360)*0x10000;
					cmd[2]->angles[2] = (qtv->proxyplayerangles[2]/360)*0x10000;
					cmd[2]->buttons = qtv->proxyplayerbuttons & 255;
					cmd[2]->forwardmove = (qtv->proxyplayerbuttons & (1<<8))?800:0 + (qtv->proxyplayerbuttons & (1<<9))?-800:0;
					cmd[2]->sidemove = (qtv->proxyplayerbuttons & (1<<11))?800:0 + (qtv->proxyplayerbuttons & (1<<10))?-800:0;
					cmd[2]->msec = qtv->curtime - qtv->packetratelimiter;
					cmd[2]->impulse = qtv->proxyplayerimpulse;
					if (cmd[2]->msec < 13)
						cmd[2]->msec = 13;
					qtv->packetratelimiter += cmd[2]->msec;
					qtv->proxyplayerimpulse = 0;


					msg.startpos = msg.cursize;
					WriteByte(&msg, clc_move);
					WriteByte(&msg, 0);
					WriteByte(&msg, 0);
					WriteDeltaUsercmd(&msg, &nullcmd, cmd[0]);
					WriteDeltaUsercmd(&msg, cmd[0], cmd[1]);
					WriteDeltaUsercmd(&msg, cmd[1], cmd[2]);
					qtv->proxyplayerucmdnum++;

					SetMoveCRC(qtv, &msg);
				}

				to = qtv->netchan.outgoing_sequence & (ENTITY_FRAMES-1);
				from = qtv->netchan.incoming_sequence & (ENTITY_FRAMES-1);
				if (qtv->map.frame[from].numents)
				{
					//remember which one we came from
					qtv->map.frame[to].oldframe = from;

					WriteByte(&msg, clc_delta);
					WriteByte(&msg, qtv->map.frame[to].oldframe);	//let the server know
				}
				else
					qtv->map.frame[to].oldframe = -1;

				qtv->map.frame[to].numents = 0;

				Netchan_Transmit(qtv->cluster, &qtv->netchan, msg.cursize, msg.data);
			}
		}
		return;
	}
	else
		qtv->simtime = qtv->curtime;


	if (qtv->sourcesock == INVALID_SOCKET && !qtv->sourcefile)
	{
		if (qtv->errored == ERR_DISABLED)
			return;

		if (qtv->sourcetype == SRC_DEMODIR || qtv->curtime >= qtv->nextconnectattempt || qtv->curtime < qtv->nextconnectattempt - RECONNECT_TIME*2)
		{
			if (qtv->autodisconnect == AD_REVERSECONNECT)	//2 means a reverse connection
			{
				qtv->errored = ERR_DROP;
				return;
			}
			if (!QTV_ConnectStream(qtv, qtv->server))	//reconnect it
			{
				qtv->errored = ERR_PERMANENT;
				return;
			}
		}
	}


//	SV_FindProxies(qtv->tcpsocket, qtv->cluster, qtv);	//look for any other proxies wanting to muscle in on the action.

	if (qtv->sourcefile || qtv->sourcesock != INVALID_SOCKET)
	{
		if (!Net_ReadStream(qtv))
		{	//if we have an error reading it
			//if it's valid, give up
			//what should we do here?
			//obviously, we need to keep reading the stream to keep things smooth
		}

		Net_WriteUpstream(qtv);
	}


	if (qtv->parsingqtvheader)
	{
		float svversion;
		int length;
		char *start;
		char *nl;
		char *colon;
		char *end;
		char value[128];
		char challenge[128];
		char authmethod[128];

//		qtv->buffer[qtv->buffersize] = 0;
//		Sys_Printf(qtv->cluster, "msg: ---%s---\n", qtv->buffer);

		*authmethod = 0;

		qtv->parsetime = qtv->curtime;
		length = qtv->buffersize;
		if (length > 6)
			length = 6;
		if (ustrncmp(qtv->buffer, "QTVSV ", length))
		{
			Sys_Printf(qtv->cluster, "Stream %i: Server is not a QTV server (or is incompatible)\n", qtv->streamid);
//printf("%i, %s\n", qtv->buffersize, qtv->buffer);
			qtv->errored = ERR_PERMANENT;
			return;
		}
		if (length < 6)
			return;	//not ready yet
		end = (char*)qtv->buffer + qtv->buffersize - 1;
		for (nl = (char*)qtv->buffer; nl < end; nl++)
		{
			if (nl[0] == '\n' && nl[1] == '\n')
				break;
		}
		if (nl == end)
			return;	//we need more header still

		//we now have a complete packet.

		svversion = atof((char*)qtv->buffer + 6);
		if ((int)svversion != 1)
		{
			Sys_Printf(qtv->cluster, "Stream %i: QTV server doesn't support a compatible protocol version (returned %i)\n", qtv->streamid, atoi((char*)qtv->buffer + 6));
			qtv->errored = ERR_PERMANENT;
			return;
		}


		qtv->upstreamacceptschat = svversion>=1.1;
		qtv->upstreamacceptsdownload = svversion>=1.1;

		length = (nl - (char*)qtv->buffer) + 2;
		end = nl;
		nl[1] = '\0';
		start = strchr((char*)qtv->buffer, '\n')+1;

		while((nl = strchr(start, '\n')))
		{
			*nl = '\0';
			colon = strchr(start, ':');
			if (colon)
			{
				*colon = '\0';
				colon++;
				while (*colon == ' ')
					colon++;
				COM_ParseToken(colon, value, sizeof(value), NULL);
			}
			else
			{
				colon = "";
				*value = '\0';
			}


			//read the notes at the top of this file for which messages to expect
			if (!strcmp(start, "AUTH"))
				strcpy(authmethod, value);
			else if (!strcmp(start, "CHALLENGE"))
				strcpy(challenge, colon);
			else if (!strcmp(start, "COMPRESSION"))
			{	//we don't support compression, we didn't ask for it.
				Sys_Printf(qtv->cluster, "Stream %i: QTV server wrongly used compression\n", qtv->streamid);
				qtv->errored = ERR_PERMANENT;
				return;
			}
			else if (!strcmp(start, "PERROR"))
			{
				Sys_Printf(qtv->cluster, "\nStream %i: Server PERROR from %s: %s\n\n", qtv->streamid, qtv->server, colon);
				qtv->errored = ERR_PERMANENT;
				qtv->buffersize = 0;
				qtv->forwardpoint = 0;
				return;
			}
			else if (!strcmp(start, "TERROR") || !strcmp(start, "ERROR"))
			{	//we don't support compression, we didn't ask for it.
				Sys_Printf(qtv->cluster, "\nStream %i: Server TERROR from %s: %s\n\n", qtv->streamid, qtv->server, colon);
				qtv->buffersize = 0;
				qtv->forwardpoint = 0;

				if (qtv->autodisconnect == AD_WHENEMPTY || qtv->autodisconnect == AD_REVERSECONNECT)
					qtv->errored = ERR_DROP;	//if its a user registered stream, drop it immediatly
				else
				{	//otherwise close the socket (this will result in a timeout and reconnect)
					if (qtv->sourcesock != INVALID_SOCKET)
					{
						closesocket(qtv->sourcesock);
						qtv->sourcesock = INVALID_SOCKET;
					}
				}
				return;
			}
			else if (!strcmp(start, "ASOURCE"))
			{
				Sys_Printf(qtv->cluster, "SRC: %s\n", colon);
			}
			else if (!strcmp(start, "ADEMO"))
			{
				int size;
				size = atoi(colon);
				colon = strchr(colon, ':');
				if (!colon)
					colon = "";
				else
					colon = colon+1;
				while(*colon == ' ')
					colon++;
				if (size > 1024*1024)
					Sys_Printf(qtv->cluster, "DEMO: (%3imb) %s\n", size/(1024*1024), colon);
				else
					Sys_Printf(qtv->cluster, "DEMO: (%3ikb) %s\n", size/1024, colon);
			}
			else if (!strcmp(start, "PRINT"))
			{
				Sys_Printf(qtv->cluster, "Stream %i: QTV server: %s\n", qtv->streamid, colon);
			}
			else if (!strcmp(start, "BEGIN"))
			{
				qtv->parsingqtvheader = false;
			}
			else
			{
				Sys_Printf(qtv->cluster, "DBG: QTV server responded with a %s key\n", start);
			}

			start = nl+1;
		}

		qtv->buffersize -= length;
		memmove(qtv->buffer, qtv->buffer + length, qtv->buffersize);

		if (qtv->serverquery)
		{
			Sys_Printf(qtv->cluster, "End of list\n");
			qtv->errored = ERR_DROP;
			qtv->buffersize = 0;
			qtv->forwardpoint = 0;
			return;
		}
		else if (*authmethod)
		{	//we need to send a challenge response now.
			Net_SendQTVConnectionRequest(qtv, authmethod, challenge);
			return;
		}
		else if (qtv->parsingqtvheader)
		{
			Sys_Printf(qtv->cluster, "Stream %i: QTV server sent no begin command - assuming incompatible\n\n", qtv->streamid);
			qtv->errored = ERR_PERMANENT;
			qtv->buffersize = 0;
			qtv->forwardpoint = 0;
			return;
		}

		qtv->parsetime = Sys_Milliseconds() + qtv->cluster->anticheattime;
		if (!qtv->usequakeworldprotocols)
			Sys_Printf(qtv->cluster, "Stream %i: Connection established, buffering for %g seconds\n", qtv->streamid, qtv->cluster->anticheattime/1000.0f);

		SV_ForwardStream(qtv, qtv->buffer, qtv->forwardpoint);
	}

	QTV_CollectCommentry(qtv);

	while (qtv->curtime >= qtv->parsetime)
	{
		if (qtv->buffersize < 2)
		{	//not enough stuff to play.
			if (qtv->parsetime < qtv->curtime)
			{
				qtv->parsetime = qtv->curtime + qtv->cluster->tooslowdelay;
//				if (qtv->sourcefile || qtv->sourcesock != INVALID_SOCKET)
//					QTV_Printf(qtv, "Stream %i: Not enough buffered\n", qtv->streamid);
			}
			break;
		}

		buffer = qtv->buffer;

		switch (qtv->buffer[1]&dem_mask)
		{
		case dem_set:
			length = 10;
			if (qtv->buffersize < length)
			{	//not enough stuff to play.
				qtv->parsetime = qtv->curtime + qtv->cluster->tooslowdelay;
//				if (qtv->sourcefile || qtv->sourcesock != INVALID_SOCKET)
//					QTV_Printf(qtv, "Stream %i: Not enough buffered\n", qtv->streamid);
				continue;
			}
			qtv->parsetime += buffer[0];	//well this was pointless

			if (qtv->forwardpoint < length)	//we're about to destroy this data, so it had better be forwarded by now!
			{
				SV_ForwardStream(qtv, qtv->buffer, length);
				qtv->forwardpoint += length;
			}

			memmove(qtv->buffer, qtv->buffer+10, qtv->buffersize-(length));
			qtv->buffersize -= length;
			qtv->forwardpoint -= length;
			continue;
		case dem_multiple:
			lengthofs = 6;
			break;
		default:
			lengthofs = 2;
			break;
		}

		if (qtv->buffersize < lengthofs+4)
		{	//the size parameter doesn't fit.
//			if (qtv->sourcefile || qtv->sourcesock != INVALID_SOCKET)
//				QTV_Printf(qtv, "Stream %i: Not enough buffered\n", qtv->streamid);
			qtv->parsetime = qtv->curtime + qtv->cluster->tooslowdelay;
			break;
		}


		length = (buffer[lengthofs]<<0) + (buffer[lengthofs+1]<<8) + (buffer[lengthofs+2]<<16) + (buffer[lengthofs+3]<<24);
		if (length > MAX_MSGLEN)
		{	//THIS SHOULDN'T HAPPEN!
			//Blame the upstream proxy!
			Sys_Printf(qtv->cluster, "Stream %i: Warning: corrupt input packet (%i bytes) too big! Flushing and reconnecting!\n", qtv->streamid, length);
			if (qtv->sourcefile)
			{
				fclose(qtv->sourcefile);
				qtv->sourcefile = NULL;
			}
			else
			{
				closesocket(qtv->sourcesock);
				qtv->sourcesock = INVALID_SOCKET;
			}
			qtv->buffersize = 0;
			qtv->forwardpoint = 0;
			break;
		}

		if (length+lengthofs+4 > qtv->buffersize)
		{
//			if (qtv->sourcefile || qtv->sourcesock != INVALID_SOCKET)
//				QTV_Printf(qtv, "Stream %i: Not enough buffered\n", qtv->streamid);
			qtv->parsetime = qtv->curtime + qtv->cluster->tooslowdelay;	//add two seconds
			break;	//can't parse it yet.
		}

//		if (qtv->sourcesock != INVALID_SOCKET)
//		{
//			QTV_Printf(qtv, "Forcing demo speed to play at 100% speed\n");
//			qtv->parsespeed = 1000;	//no speeding up/slowing down routed demos
//		}

		packettime = buffer[0];
		if (qtv->parsespeed>0)
			packettime = ((1000*packettime) / qtv->parsespeed);
		qtv->nextpackettime = qtv->parsetime + packettime;

		if (qtv->nextpackettime < qtv->curtime)
		{
			switch(qtv->buffer[1]&dem_mask)
			{
			case dem_multiple:
				ParseMessage(qtv, buffer+lengthofs+4, length, qtv->buffer[1]&dem_mask, (buffer[lengthofs-4]<<0) + (buffer[lengthofs-3]<<8) + (buffer[lengthofs-2]<<16) + (buffer[lengthofs-1]<<24));
				break;
			case dem_single:
			case dem_stats:
				ParseMessage(qtv, buffer+lengthofs+4, length, qtv->buffer[1]&dem_mask, 1<<(qtv->buffer[1]>>3));
				break;
			case dem_all:
				if (qtv->buffer[1] & ~dem_mask)	//dem_qtvdata
					if (qtv->sourcetype != SRC_DEMO)
						break;
				//fallthrough
			case dem_read:
				ParseMessage(qtv, buffer+lengthofs+4, length, qtv->buffer[1], 0xffffffff);
				break;
			default:
				Sys_Printf(qtv->cluster, "Message type %i\n", qtv->buffer[1]&dem_mask);
				break;
			}

			length = lengthofs+4+length;	//make length be the length of the entire packet

			qtv->oldpackettime = qtv->curtime;

			if (qtv->buffersize)
			{	//svc_disconnect can flush our input buffer (to prevent the EndOfDemo part from interfering)

				if (qtv->forwardpoint < length)	//we're about to destroy this data, so it had better be forwarded by now!
				{
					SV_ForwardStream(qtv, qtv->buffer, length);
					qtv->forwardpoint += length;
				}

				memmove(qtv->buffer, qtv->buffer+length, qtv->buffersize-(length));
				qtv->buffersize -= length;
				qtv->forwardpoint -= length;
			}

			if (qtv->sourcefile)
			{
				Net_ReadStream(qtv);
				qtv->nextconnectattempt = qtv->curtime + RECONNECT_TIME_DEMO;
			}
			else
				qtv->nextconnectattempt = qtv->curtime + RECONNECT_TIME;

			qtv->parsetime += packettime;
		}
		else
			break;
	}
}

sv_t *QTV_NewServerConnection(cluster_t *cluster, int newstreamid, char *server, char *password, qboolean force, enum autodisconnect_e autoclose, qboolean noduplicates, qboolean query)
{
	sv_t *qtv;

	if (noduplicates)
	{
		for (qtv = cluster->servers; qtv; qtv = qtv->next)
		{
			if (!strcmp(qtv->server, server))
			{	//if the stream detected some permanent/config error, try reconnecting again (of course this only happens when someone tries using the stream)
//warning review this logic
				if (qtv->errored == ERR_DISABLED)
				{
					if (!(!QTV_ConnectStream(qtv, server) && !force))	//try and wake it up
						qtv->errored = ERR_NONE;
				}
				return qtv;
			}
		}
	}
	if (!newstreamid)	//no fixed id? generate a default id
		newstreamid = 100;
	//make sure it doesn't conflict
	for(;;newstreamid++)
	{
		for (qtv = cluster->servers; qtv; qtv = qtv->next)
		{
			if (qtv->streamid == newstreamid)
				break;
		}
		if (!qtv)
			break;
	}

	if (autoclose)
		if (cluster->nouserconnects)
			return NULL;

	qtv = malloc(sizeof(sv_t));
	if (!qtv)
		return NULL;

	memset(qtv, 0, sizeof(*qtv));
	//set up a default config
//	qtv->tcplistenportnum = PROX_DEFAULTLISTENPORT;
	strcpy(qtv->server, PROX_DEFAULTSERVER);

	memcpy(qtv->connectpassword, password, sizeof(qtv->connectpassword)-1);

//	qtv->tcpsocket = INVALID_SOCKET;
	qtv->sourcesock = INVALID_SOCKET;
	qtv->autodisconnect = autoclose;
	qtv->parsingconnectiondata = true;
	qtv->serverquery = query;
	qtv->silentstream = true;
	qtv->parsespeed = 1000;

	qtv->streamid = newstreamid;

	qtv->cluster = cluster;
	qtv->next = cluster->servers;

	if (autoclose != AD_REVERSECONNECT)	//2 means reverse connection (don't ever try reconnecting)
	{
		if (!QTV_ConnectStream(qtv, server) && !force)
		{
			QTV_Cleanup(qtv, false);
			free(qtv);
			return NULL;
		}
	}
	cluster->servers = qtv;
	cluster->numservers++;

	return qtv;
}
