/*
 * Copyright (C) 1997-2001 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 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.
 *
 */
/* cl_parse.c  -- parse a message received from the server */

#include "client.h"

char   *svc_strings[256] =
{
	"svc_bad",

	"svc_muzzleflash",
	"svc_muzzleflash2",
	"svc_temp_entity",
	"svc_layout",
	"svc_inventory",

	"svc_nop",
	"svc_disconnect",
	"svc_reconnect",
	"svc_sound",
	"svc_print",
	"svc_stufftext",
	"svc_serverdata",
	"svc_configstring",
	"svc_spawnbaseline",
	"svc_centerprint",
	"svc_download",
	"svc_playerinfo",
	"svc_packetentities",
	"svc_deltapacketentities",
	"svc_frame"
};

/*
 * ===========================================================================
 */

void
CL_DownloadFileName(char *dest, int destlen, char *fn)
{
	if (strncmp(fn, "players", 7) == 0)
		Com_sprintf(dest, destlen, "%s/%s", BASEDIRNAME, fn);
	else
		Com_sprintf(dest, destlen, "%s/%s", FS_Gamedir(), fn);
}

/*
 * =============== CL_CheckOrDownloadFile
 *
 * Returns true if the file exists, otherwise it attempts to start a download
 * from the server. ===============
 */
qboolean
CL_CheckOrDownloadFile(char *filename)
{
	FILE           *fp;
	char		name[MAX_OSPATH];
	char           *ptr;

	if (strstr(filename, "..")) {
		Com_Printf("Refusing to download a path with ..\n");
		return true;
	}
	/* fix backslashes */
	while ((ptr = strchr(filename, '\\'))) {
		*ptr = '/';
	}

	if (FS_LoadFile(filename, NULL) != -1) {	/* it exists, no need to download */
		return true;
	}
	Q_strncpyz(cls.downloadname, filename, sizeof(cls.downloadname));

	/* download to a temp name, and only rename */
	/* to the real name when done, so if interrupted */
	/* a runt file wont be left */
	COM_StripExtension(cls.downloadname, cls.downloadtempname);
	strcat(cls.downloadtempname, ".tmp");

	/* ZOID */

	/*
	 * check to see if we already have a tmp for this file, if so, try to
	 * resume
	 */
	/* open the file if not opened yet */
	CL_DownloadFileName(name, sizeof(name), cls.downloadtempname);

	/* FS_CreatePath (name); */

	fp = fopen(name, "r+b");
	if (fp) {		/* it exists */
		int		len;

		fseek(fp, 0, SEEK_END);
		len = ftell(fp);

		cls.download = fp;

		/* give the server an offset to start the download */
		Com_Printf("Resuming %s\n", cls.downloadname);
		MSG_WriteByte(&cls.netchan.message, clc_stringcmd);
		MSG_WriteString(&cls.netchan.message,
		    va("download %s %i", cls.downloadname, len));
	} else {
		Com_Printf("Downloading %s\n", cls.downloadname);
		MSG_WriteByte(&cls.netchan.message, clc_stringcmd);
		MSG_WriteString(&cls.netchan.message,
		    va("download %s", cls.downloadname));
	}

	cls.downloadnumber++;

	return false;
}

/*
 * =============== CL_Download_f
 *
 * Request a download from the server ===============
 */
void
CL_Download_f(void)
{
	char	filename[MAX_OSPATH];

	if (Cmd_Argc() != 2) {
		Com_Printf("Usage: download <filename>\n");
		return;
	}
	Com_sprintf(filename, sizeof(filename), "%s", Cmd_Argv(1));

	if (strstr(filename, "..")) {
		Com_Printf("Refusing to download a path with ..\n");
		return;
	}
	if (FS_LoadFile(filename, NULL) != -1) {	/* it exists, no need to download */
		Com_Printf("File already exists.\n");
		return;
	}
	Q_strncpyz(cls.downloadname, filename, sizeof(cls.downloadname));
	Com_Printf("Downloading %s\n", cls.downloadname);

	/* download to a temp name, and only rename */
	/* to the real name when done, so if interrupted */
	/* a runt file wont be left */
	COM_StripExtension(cls.downloadname, cls.downloadtempname);
	strcat(cls.downloadtempname, ".tmp");

	MSG_WriteByte(&cls.netchan.message, clc_stringcmd);
	MSG_WriteString(&cls.netchan.message,
	    va("download %s", cls.downloadname));

	cls.downloadnumber++;
}

/*
 * ====================== CL_RegisterSounds ======================
 */
void
CL_RegisterSounds(void)
{
	int		i;

	S_BeginRegistration();
	CL_RegisterTEntSounds();
	for (i = 1; i < MAX_SOUNDS; i++) {
		if (!cl.configstrings[CS_SOUNDS + i][0])
			break;
		cl.sound_precache[i] = S_RegisterSound(cl.configstrings[CS_SOUNDS + i]);
		Sys_SendKeyEvents();	/* pump message loop */
	}
	S_EndRegistration();
}


/*
 * ===================== CL_ParseDownload
 *
 * A download message has been received from the server =====================
 */
void
CL_ParseDownload(void)
{
	int		size, percent;
	char		name[MAX_OSPATH];
	int		r;

	/* read the data */
	size = MSG_ReadShort(&net_message);
	percent = MSG_ReadByte(&net_message);
	if (size == -1) {
		Com_Printf("Server does not have this file.\n");
		if (cls.download) {

			/*
			 * if here, we tried to resume a file but the server
			 * said no
			 */
			fclose(cls.download);
			cls.download = NULL;
		}
		CL_RequestNextDownload();
		return;
	}
	/* open the file if not opened yet */
	if (!cls.download) {
		CL_DownloadFileName(name, sizeof(name), cls.downloadtempname);

		FS_CreatePath(name);

		cls.download = fopen(name, "wb");
		if (!cls.download) {
			net_message.readcount += size;
			Com_Printf("Failed to open %s\n", cls.downloadtempname);
			CL_RequestNextDownload();
			return;
		}
	}
	fwrite(net_message.data + net_message.readcount, 1, size, cls.download);
	net_message.readcount += size;

	if (percent != 100) {
		/* request next block */
		/* change display routines by zoid */
#if 0
		Com_Printf(".");
		if (10 * (percent / 10) != cls.downloadpercent) {
			cls.downloadpercent = 10 * (percent / 10);
			Com_Printf("%i%%", cls.downloadpercent);
		}
#endif
		cls.downloadpercent = percent;

		MSG_WriteByte(&cls.netchan.message, clc_stringcmd);
		SZ_Print(&cls.netchan.message, "nextdl");
	} else {
		char	oldn[MAX_OSPATH];
		char	newn[MAX_OSPATH];

		/* Com_Printf ("100%%\n"); */

		fclose(cls.download);

		/* rename the temp file to it's final name */
		CL_DownloadFileName(oldn, sizeof(oldn), cls.downloadtempname);
		CL_DownloadFileName(newn, sizeof(newn), cls.downloadname);
		r = rename(oldn, newn);
		if (r)
			Com_Printf("failed to rename.\n");

		cls.download = NULL;
		cls.downloadpercent = 0;

		/* get another file if needed */

		CL_RequestNextDownload();
	}
}


/*
 * =====================================================================
 *
 * SERVER CONNECTING MESSAGES
 *
 * =====================================================================
 */

/*
 * ================== CL_ParseServerData ==================
 */
void
CL_ParseServerData(void)
{
	extern cvar_t  *fs_gamedirvar;
	char           *str;
	int		i;

	Com_DPrintf("Serverdata packet received.\n");
	
	/* wipe the client_state_t struct */
	CL_ClearState();
	cls.state = ca_connected;

	/* parse protocol version number */
	i = MSG_ReadLong(&net_message);
	cls.serverProtocol = i;

	/* BIG HACK to let demos from release work with the 3.0x patch!!! */
	if (Com_ServerState() && PROTOCOL_VERSION == 34) {
	} else if (i != PROTOCOL_VERSION)
		Com_Error(ERR_DROP, "Server returned version %i, not %i", i, PROTOCOL_VERSION);

	cl.servercount = MSG_ReadLong(&net_message);
	cl.attractloop = MSG_ReadByte(&net_message);

	/* game directory */
	str = MSG_ReadString(&net_message);
	Q_strncpyz(cl.gamedir, str, sizeof(cl.gamedir));

	/* set gamedir */
	if ((*str && (!fs_gamedirvar->string || !*fs_gamedirvar->string || strcmp(fs_gamedirvar->string, str))) || (!*str && (fs_gamedirvar->string || *fs_gamedirvar->string)))
		Cvar_Set("game", str);

	/* parse player entity number */
	cl.playernum = MSG_ReadShort(&net_message);

	/* get the full level name */
	str = MSG_ReadString(&net_message);

	if (cl.playernum == -1) {	/* playing a cinematic or showing a
					 * pic, not a level */
		SCR_PlayCinematic(str);
	} else {

		/*
		 * seperate the printfs so the server message can have a
		 * color
		 */
		Com_Printf("\n\n\35\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\37\n\n");
		Com_Printf("%c%s\n", 2, str);

		/* need to prep refresh at next oportunity */
		cl.refresh_prepped = false;
	}
}

/*
 * ================== CL_ParseBaseline ==================
 */
void
CL_ParseBaseline(void)
{
	entity_state_t *es;
	unsigned	bits;
	int		newnum;
	entity_state_t	nullstate;

	memset(&nullstate, 0, sizeof(nullstate));

	newnum = CL_ParseEntityBits(&bits);
	es = &cl_entities[newnum].baseline;
	CL_ParseDelta(&nullstate, es, newnum, bits);
}


/*
 * ================ CL_LoadClientinfo
 *
 * ================
 */
void
CL_LoadClientinfo(clientinfo_t * ci, char *s)
{
	int		i;
	char           *t;
	char		model_name[MAX_QPATH];
	char		skin_name[MAX_QPATH];
	char		model_filename[MAX_QPATH];
	char		skin_filename[MAX_QPATH];
	char		weapon_filename[MAX_QPATH];

	Q_strncpyz(ci->cinfo, s, sizeof(ci->cinfo));

	/* sku avoid buffer overflow */
	s = ci->cinfo;

	/* isolate the player's name */
	Q_strncpyz(ci->name, s, sizeof(ci->name));
	t = strchr(s, '\\');
	if (t) {
		ci->name[t - s] = 0;
		s = t + 1;
	}
	if (cl_noskins->value || *s == 0) {
		Com_sprintf(model_filename, sizeof(model_filename), "players/male/tris.md2");
		Com_sprintf(weapon_filename, sizeof(weapon_filename), "players/male/weapon.md2");
		Com_sprintf(skin_filename, sizeof(skin_filename), "players/male/grunt.pcx");
		Com_sprintf(ci->iconname, sizeof(ci->iconname), "/players/male/grunt_i.pcx");
		ci->model = re.RegisterModel(model_filename);
		memset(ci->weaponmodel, 0, sizeof(ci->weaponmodel));
		ci->weaponmodel[0] = re.RegisterModel(weapon_filename);
		ci->skin = re.RegisterSkin(skin_filename);
		ci->icon = re.RegisterPic(ci->iconname);
	} else {
		/* isolate the model name */
		Q_strncpyz(model_name, s, sizeof(model_name));
		t = strchr(model_name, '/');
		if (!t)
			t = model_name;
		*t = 0;

		/* isolate the skin name */
		Q_strncpyz(skin_name, s + strlen(model_name) + 1, sizeof(skin_name));

		/* model file */
		Com_sprintf(model_filename, sizeof(model_filename), "players/%s/tris.md2", model_name);
		ci->model = re.RegisterModel(model_filename);
		if (!ci->model) {
			Q_strncpyz(model_name, "male", sizeof(model_name));
			Com_sprintf(model_filename, sizeof(model_filename), "players/male/tris.md2");
			ci->model = re.RegisterModel(model_filename);
		}
		/* skin file */
		Com_sprintf(skin_filename, sizeof(skin_filename), "players/%s/%s.pcx", model_name, skin_name);
		ci->skin = re.RegisterSkin(skin_filename);

		/* if we don't have the skin and the model wasn't male, */
		/* see if the male has it (this is for CTF's skins) */
		if (!ci->skin && Q_stricmp(model_name, "male")) {
			/* change model to male */
			Q_strncpyz(model_name, "male", sizeof(model_name));
			Com_sprintf(model_filename, sizeof(model_filename), "players/male/tris.md2");
			ci->model = re.RegisterModel(model_filename);

			/* see if the skin exists for the male model */
			Com_sprintf(skin_filename, sizeof(skin_filename), "players/%s/%s.pcx", model_name, skin_name);
			ci->skin = re.RegisterSkin(skin_filename);
		}

		/*
		 * if we still don't have a skin, it means that the male
		 * model didn't have
		 */
		/* it, so default to grunt */
		if (!ci->skin) {
			/* see if the skin exists for the male model */
			Com_sprintf(skin_filename, sizeof(skin_filename), "players/%s/grunt.pcx", model_name, skin_name);
			ci->skin = re.RegisterSkin(skin_filename);
		}
		/* weapon file */
		for (i = 0; i < num_cl_weaponmodels; i++) {
			Com_sprintf(weapon_filename, sizeof(weapon_filename), "players/%s/%s", model_name, cl_weaponmodels[i]);
			ci->weaponmodel[i] = re.RegisterModel(weapon_filename);
			if (!ci->weaponmodel[i] && strcmp(model_name, "cyborg") == 0) {
				/* try male */
				Com_sprintf(weapon_filename, sizeof(weapon_filename), "players/male/%s", cl_weaponmodels[i]);
				ci->weaponmodel[i] = re.RegisterModel(weapon_filename);
			}
			if (!cl_vwep->value)
				break;	/* only one when vwep is off */
		}

		/* icon file */
		Com_sprintf(ci->iconname, sizeof(ci->iconname), "/players/%s/%s_i.pcx", model_name, skin_name);
		ci->icon = re.RegisterPic(ci->iconname);
	}

	/* must have loaded all data types to be valud */
	if (!ci->skin || !ci->icon || !ci->model || !ci->weaponmodel[0]) {
		ci->skin = NULL;
		ci->icon = NULL;
		ci->model = NULL;
		ci->weaponmodel[0] = NULL;
		return;
	}
}

/*
 * ================ CL_ParseClientinfo
 *
 * Load the skin, icon, and model for a client ================
 */
void
CL_ParseClientinfo(int player)
{
	char           *s;
	clientinfo_t   *ci;

	s = cl.configstrings[player + CS_PLAYERSKINS];

	ci = &cl.clientinfo[player];

	CL_LoadClientinfo(ci, s);
}


/*
 * ================ CL_ParseConfigString ================
 */
void
CL_ParseConfigString(void)
{
	int		i;
	char           *s;
	char		olds[MAX_QPATH];
	int		length;

	i = MSG_ReadShort(&net_message);
	if (i < 0 || i >= MAX_CONFIGSTRINGS)
		Com_Error(ERR_DROP, "configstring > MAX_CONFIGSTRINGS");
	s = MSG_ReadString(&net_message);

	Q_strncpyz(olds, cl.configstrings[i], sizeof(olds));

	/* sku - avoid potentional buffer overflow vulnerability */
	length = strlen(s);
	if (length > sizeof(cl.configstrings) - sizeof(cl.configstrings[0]) * i - 1) {
		Com_Error(ERR_DROP, "CL_ParseConfigString: oversize configstring");
	}
	strcpy(cl.configstrings[i], s);

	/* do something apropriate  */

	if (i >= CS_LIGHTS && i < CS_LIGHTS + MAX_LIGHTSTYLES)
		CL_SetLightstyle(i - CS_LIGHTS);
	else if (i == CS_CDTRACK) {
		if (cl.refresh_prepped)
			CDAudio_Play(atoi(cl.configstrings[CS_CDTRACK]), true);
	} else if (i >= CS_MODELS && i < CS_MODELS + MAX_MODELS) {
		if (cl.refresh_prepped) {
			cl.model_draw[i - CS_MODELS] = re.RegisterModel(cl.configstrings[i]);
			if (cl.configstrings[i][0] == '*')
				cl.model_clip[i - CS_MODELS] = CM_InlineModel(cl.configstrings[i]);
			else
				cl.model_clip[i - CS_MODELS] = NULL;
		}
	} else if (i >= CS_SOUNDS && i < CS_SOUNDS + MAX_MODELS) {
		if (cl.refresh_prepped)
			cl.sound_precache[i - CS_SOUNDS] = S_RegisterSound(cl.configstrings[i]);
	} else if (i >= CS_IMAGES && i < CS_IMAGES + MAX_MODELS) {
		if (cl.refresh_prepped)
			cl.image_precache[i - CS_IMAGES] = re.RegisterPic(cl.configstrings[i]);
	} else if (i >= CS_PLAYERSKINS && i < CS_PLAYERSKINS + MAX_CLIENTS) {
		if (cl.refresh_prepped && strcmp(olds, s))
			CL_ParseClientinfo(i - CS_PLAYERSKINS);
	}
}


/*
 * =====================================================================
 *
 * ACTION MESSAGES
 *
 * =====================================================================
 */

/*
 * ================== CL_ParseStartSoundPacket ==================
 */
void
CL_ParseStartSoundPacket(void)
{
	vec3_t		pos_v;
	float          *pos;
	int		channel, ent;
	int		sound_num;
	float		volume;
	float		attenuation;
	int		flags;
	float		ofs;

	flags = MSG_ReadByte(&net_message);
	sound_num = MSG_ReadByte(&net_message);

	if (flags & SND_VOLUME)
		volume = MSG_ReadByte(&net_message) / 255.0;
	else
		volume = DEFAULT_SOUND_PACKET_VOLUME;

	if (flags & SND_ATTENUATION)
		attenuation = MSG_ReadByte(&net_message) / 64.0;
	else
		attenuation = DEFAULT_SOUND_PACKET_ATTENUATION;

	if (flags & SND_OFFSET)
		ofs = MSG_ReadByte(&net_message) / 1000.0;
	else
		ofs = 0;

	if (flags & SND_ENT) {	/* entity reletive */
		channel = MSG_ReadShort(&net_message);
		ent = channel >> 3;
		if (ent > MAX_EDICTS)
			Com_Error(ERR_DROP, "CL_ParseStartSoundPacket: ent = %i", ent);

		channel &= 7;
	} else {
		ent = 0;
		channel = 0;
	}

	if (flags & SND_POS) {	/* positioned in space */
		MSG_ReadPos(&net_message, pos_v);

		pos = pos_v;
	} else			/* use entity number */
		pos = NULL;

	if (!cl.sound_precache[sound_num])
		return;

	S_StartSound(pos, ent, channel, cl.sound_precache[sound_num], volume, attenuation, ofs);
}


void
SHOWNET(char *s)
{
	if (cl_shownet->value >= 2)
		Com_Printf("%3i:%s\n", net_message.readcount - 1, s);
}

/* NiceAss: highlight people's names */
char           *
CL_Highlight(char *s, int msg)
{
	static char	highlighted[512];
	char           *start = 0, *t;
	int		i, client = 0;

	Q_strncpyz(highlighted, s, sizeof(highlighted));

	if (cl_highlight->value == 0)
		return highlighted;

	if (msg == PRINT_CHAT && cl_highlight->value == 1) {
		/* see which client said this. */
		/* name with match closest to start of string */
		/* is the one we go with. (Incase someone named bob  */
		/* says "joe" and joe is also in the server,  */
		/* it'll go with bob.) */
		for (i = 0; i < MAX_CLIENTS; i++) {
			if (cl.clientinfo[i].name[0]) {
				char           *tmp = strstr(highlighted, cl.clientinfo[i].name);

				if (tmp && (tmp < start || start == 0)) {
					client = i;
					start = tmp;
				}
			}
		}

		/* if name is found.... */
		if (start) {
			/* skip the name */
			start += strlen(cl.clientinfo[client].name);
			/* walk to a space (after the colon) */
			while (*(start) != ' ' && *(start) != 0)
				start++;

			if (*start != 0) {
				char           *t;

				for (t = highlighted; t < start; t++)
					*t |= 128;
			}
		}
	}
	if ((msg != PRINT_CHAT && cl_highlight->value == 2) || cl_highlight->value == 1) {
		/* highlight peoples names */
		for (i = 0; i < MAX_CLIENTS; i++) {
			if (cl.clientinfo[i].name[0] && strlen(cl.clientinfo[i].name) > 1) {
				char           *tmp = strstr(highlighted, cl.clientinfo[i].name);

				if (tmp) {
					for (t = tmp; t < tmp + strlen(cl.clientinfo[i].name); t++)
						*t |= 128;
				}
			}
		}
	}
	return highlighted;
}

/*
 * ===================== CL_ParseServerMessage =====================
 */
NiceAss_Chat_t	ChatMessages;
void
CL_ParseServerMessage(void)
{
	int		cmd, i, j;
	char           *s, *s3;
	struct tm      *ntime;
	char		stime[24], Tmp[512], *Tmp2 = NULL;
	time_t		l_time;
	time(&l_time);
	ntime = localtime(&l_time);
	
	if (cl_drawtimestamps->value) {
		strftime(stime, sizeof(stime), "%H:%M", ntime);
	}

	/* if recording demos, copy the message out */
	if (cl_shownet->value == 1)
		Com_Printf("%i ", net_message.cursize);
	else if (cl_shownet->value >= 2)
		Com_Printf("------------------\n");


	/* parse the message */
	while (1) {
		if (net_message.readcount > net_message.cursize) {
			Com_Error(ERR_DROP, "CL_ParseServerMessage: Bad server message");
			break;
		}
		cmd = MSG_ReadByte(&net_message);

		if (cmd == -1) {
			SHOWNET("END OF MESSAGE");
			break;
		}
		if (cl_shownet->value >= 2) {
			if (!svc_strings[cmd])
				Com_Printf("%3i:BAD CMD %i\n", net_message.readcount - 1, cmd);
			else
				SHOWNET(svc_strings[cmd]);
		}
		/* other commands */
		switch (cmd) {
		default:
			Com_Error(ERR_DROP, "CL_ParseServerMessage: Illegible server message\n");
			break;

		case svc_nop:
			/* Com_Printf ("svc_nop\n"); */
			break;

		case svc_disconnect:
			Com_Error(ERR_DISCONNECT, "Server disconnected\n");
			break;

		case svc_reconnect:
			Com_Printf("Server disconnected, reconnecting\n");
			if (cls.download) {
				/* ZOID, close download */
				fclose(cls.download);
				cls.download = NULL;
			}
			cls.state = ca_connecting;
			cls.connect_time = -99999;	/* CL_CheckForResend() will fire immediately */
			break;

		case svc_print:
			i = MSG_ReadByte(&net_message);
			s = MSG_ReadString(&net_message);
			if (i == PRINT_CHAT) {
				S_StartLocalSound("misc/talk.wav");
				if (cl_highlight->value != 1)
					con.ormask = 128;
			}
			s3 = CL_Highlight(s, i);
			if (cl_drawtimestamps->value) {
				Com_Printf("[%s] %s", stime, s3);
			}
			else {
				Com_Printf ("%s", s3); 
			}
			con.ormask = 0;
			if (i == PRINT_CHAT) {
				Q_strncpyz(ChatMessages.chathud[0], ChatMessages.chathud[1],
						sizeof(ChatMessages.chathud[0]));
				Q_strncpyz(ChatMessages.chathud[1], ChatMessages.chathud[2],
						sizeof(ChatMessages.chathud[0]));
				Q_strncpyz(ChatMessages.chathud[2], ChatMessages.chathud[3],
						sizeof(ChatMessages.chathud[0]));
				Q_strncpyz(ChatMessages.chathud[3], s3, 
						sizeof(ChatMessages.chathud[0]));

				/*
				 * Overwrite some odd character that
				 * shouldn't be there
				 */
				ChatMessages.chathud[3][strlen(ChatMessages.chathud[3]) - 1] = '\0';

				/* flip the last bit which is highlighting */
				if (cl_highlight->value == 1)
					for (j = 0; ChatMessages.chathud[3][j] != 0; j++)
						ChatMessages.chathud[3][j] ^= 128;

			}
			/* Start NiceAss: */
			if (i == PRINT_CHAT && (cl.time - ChatMessages.echoTime > 120000 || ChatMessages.echoTime == 0)) {
				Q_strncpyz(Tmp, s, sizeof(Tmp));
				Q_strlwr(Tmp);
				Tmp2 = strstr(Tmp, "!chatsay");

				if (Tmp2) {

					/*
					 * Make sure "!chatsay" isn't in the
					 * name by checking if a : comes
					 * after it
					 */
					if (!strstr(Tmp2, ":")) {
						ChatMessages.echoTime = cl.time;
						ChatMessages.echoDelayTime = cl.time + rand() % 750;	/* 0 to 0.75 second
													 * delay */
					}
				}
			}
			break;

		case svc_centerprint:
			SCR_CenterPrint(MSG_ReadString(&net_message));
			break;

		case svc_stufftext:
			s = MSG_ReadString(&net_message);
			Com_DPrintf("stufftext: %s\n", s);
			Cbuf_AddText(s);
			break;

		case svc_serverdata:
			Cbuf_Execute();	/* make sure any stuffed commands are done */
			CL_ParseServerData();
			break;

		case svc_configstring:
			CL_ParseConfigString();
			break;

		case svc_sound:
			CL_ParseStartSoundPacket();
			break;

		case svc_spawnbaseline:
			CL_ParseBaseline();
			break;

		case svc_temp_entity:
			CL_ParseTEnt();
			break;

		case svc_muzzleflash:
			CL_ParseMuzzleFlash();
			break;

		case svc_muzzleflash2:
			CL_ParseMuzzleFlash2();
			break;

		case svc_download:
			CL_ParseDownload();
			break;

		case svc_frame:
			CL_ParseFrame();
			break;

		case svc_inventory:
			CL_ParseInventory();
			break;

		case svc_layout:
			s = MSG_ReadString(&net_message);
			Q_strncpyz(cl.layout, s, sizeof(cl.layout));
			break;

		case svc_playerinfo:
		case svc_packetentities:
		case svc_deltapacketentities:
			Com_Error(ERR_DROP, "Out of place frame data");
			break;
		}
	}

	CL_AddNetgraph();

	/* we don't know if it is ok to save a demo message until */
	/* after we have parsed the frame */
	if (cls.demorecording && !cls.demowaiting)
		CL_WriteDemoMessage();

}
