/*
	sv_user.c

	server code for moving users

	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 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:

		Free Software Foundation, Inc.
		59 Temple Place - Suite 330
		Boston, MA  02111-1307, USA

*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#ifdef HAVE_STRING_H
# include <string.h>
#endif
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif

#include <math.h>
#include <stdarg.h>
#include <stdlib.h>
#include <ctype.h>

#include "QF/cbuf.h"
#include "QF/idparse.h"
#include "QF/checksum.h"
#include "QF/clip_hull.h"
#include "QF/cmd.h"
#include "QF/cvar.h"
#include "QF/dstring.h"
#include "QF/hash.h"
#include "QF/msg.h"
#include "QF/quakefs.h"
#include "QF/sys.h"
#include "QF/va.h"

#include "compat.h"

#include "qw/msg_ucmd.h"
#include "qw/msg_ucmd.h"

#include "qw/bothdefs.h"
#include "qw/pmove.h"
#include "qw/include/server.h"
#include "qw/include/sv_gib.h"
#include "qw/include/sv_progs.h"
#include "qw/include/sv_recorder.h"
#include "world.h"

typedef struct ucmd_s {
	const char *name;
	void        (*func) (void *userdata);
	unsigned    no_redirect:1;
	unsigned    overridable:1;
	unsigned    freeable:1;
	void		*userdata;
	void		(*on_free) (void *userdata);
} ucmd_t;

edict_t    *sv_player;

usercmd_t   cmd;

int sv_maxrate;
static cvar_t sv_maxrate_cvar = {
	.name = "sv_maxrate",
	.description =
		"Maximum allowable rate",
	.default_value = "10000",
	.flags = CVAR_SERVERINFO,
	.value = { .type = &cexpr_int, .value = &sv_maxrate },
};
										// capped)
int sv_antilag;
static cvar_t sv_antilag_cvar = {
	.name = "sv_antilag",
	.description =
		"Attempt to backdate impacts to compensate for lag. 0=completely off. "
		"1=mod-controlled. 2=forced, which might break certain uses of "
		"traceline.",
	.default_value = "1",
	.flags = CVAR_SERVERINFO,
	.value = { .type = &cexpr_int, .value = &sv_antilag },
};
float sv_antilag_frac;
static cvar_t sv_antilag_frac_cvar = {
	.name = "sv_antilag_frac",
	.description =
		"FIXME something to do with sv_antilag",
	.default_value = "1",
	.flags = CVAR_SERVERINFO,
	.value = { .type = &cexpr_float, .value = &sv_antilag_frac },
};

float sv_accelerate;
static cvar_t sv_accelerate_cvar = {
	.name = "sv_accelerate",
	.description =
		"None",
	.default_value = "10",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_float, .value = &sv_accelerate },
};
float sv_airaccelerate;
static cvar_t sv_airaccelerate_cvar = {
	.name = "sv_airaccelerate",
	.description =
		"Sets how quickly the players accelerate in air",
	.default_value = "0.7",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_float, .value = &sv_airaccelerate },
};
float sv_maxspeed;
static cvar_t sv_maxspeed_cvar = {
	.name = "sv_maxspeed",
	.description =
		"None",
	.default_value = "320",
	.flags = CVAR_SERVERINFO,
	.value = { .type = &cexpr_float, .value = &sv_maxspeed },
};
float sv_spectatormaxspeed;
static cvar_t sv_spectatormaxspeed_cvar = {
	.name = "sv_spectatormaxspeed",
	.description =
		"Sets the maximum speed a spectator can move",
	.default_value = "500",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_float, .value = &sv_spectatormaxspeed },
};
float sv_wateraccelerate;
static cvar_t sv_wateraccelerate_cvar = {
	.name = "sv_wateraccelerate",
	.description =
		"Sets the water acceleration value",
	.default_value = "10",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_float, .value = &sv_wateraccelerate },
};
float sv_waterfriction;
static cvar_t sv_waterfriction_cvar = {
	.name = "sv_waterfriction",
	.description =
		"Sets the water friction value",
	.default_value = "4",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_float, .value = &sv_waterfriction },
};

int sv_allowfake;
static cvar_t sv_allowfake_cvar = {
	.name = "sv_allowfake",
	.description =
		"Allow 'fake' messages (FuhQuake $\\). 1 = always, 2 = only say_team",
	.default_value = "2",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_int, .value = &sv_allowfake },
};

float cl_rollspeed;
static cvar_t cl_rollspeed_cvar = {
	.name = "cl_rollspeed",
	.description =
		"How quickly you straighten out after strafing",
	.default_value = "200",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_float, .value = &cl_rollspeed },
};
float cl_rollangle;
static cvar_t cl_rollangle_cvar = {
	.name = "cl_rollangle",
	.description =
		"How much your screen tilts when strafing",
	.default_value = "2.0",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_float, .value = &cl_rollangle },
};
int sv_spectalk;
static cvar_t sv_spectalk_cvar = {
	.name = "sv_spectalk",
	.description =
		"Toggles the ability of spectators to talk to players",
	.default_value = "1",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_int, .value = &sv_spectalk },
};

int sv_kickfake;
static cvar_t sv_kickfake_cvar = {
	.name = "sv_kickfake",
	.description =
		"Kick users sending to send fake talk messages",
	.default_value = "0",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_int, .value = &sv_kickfake },
};

int sv_mapcheck;
static cvar_t sv_mapcheck_cvar = {
	.name = "sv_mapcheck",
	.description =
		"Toggle the use of map checksumming to check for players who edit maps"
		" to cheat",
	.default_value = "1",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_int, .value = &sv_mapcheck },
};

int sv_timecheck_mode;
static cvar_t sv_timecheck_mode_cvar = {
	.name = "sv_timecheck_mode",
	.description =
		"select between timekick (0, default) and timecheck (1)",
	.default_value = "0",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_int, .value = &sv_timecheck_mode },
};
int sv_timekick;
static cvar_t sv_timekick_cvar = {
	.name = "sv_timekick",
	.description =
		"Time cheat protection",
	.default_value = "3",
	.flags = CVAR_SERVERINFO,
	.value = { .type = &cexpr_int, .value = &sv_timekick },
};
float sv_timekick_fuzz;
static cvar_t sv_timekick_fuzz_cvar = {
	.name = "sv_timekick_fuzz",
	.description =
		"Time cheat \"fuzz factor\" in milliseconds",
	.default_value = "30",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_float, .value = &sv_timekick_fuzz },
};
float sv_timekick_interval;
static cvar_t sv_timekick_interval_cvar = {
	.name = "sv_timekick_interval",
	.description =
		"Time cheat check interval in seconds",
	.default_value = "30",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_float, .value = &sv_timekick_interval },
};
int sv_timecheck_fuzz;
static cvar_t sv_timecheck_fuzz_cvar = {
	.name = "sv_timecheck_fuzz",
	.description =
		"Milliseconds of tolerance before time cheat throttling kicks in.",
	.default_value = "250",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_int, .value = &sv_timecheck_fuzz },
};
int sv_timecheck_decay;
static cvar_t sv_timecheck_decay_cvar = {
	.name = "sv_timecheck_decay",
	.description =
		"Rate at which time inaccuracies are \"forgiven\".",
	.default_value = "2",
	.flags = CVAR_NONE,
	.value = { .type = &cexpr_int, .value = &sv_timecheck_decay },
};

char *sv_http_url_base;
static cvar_t sv_http_url_base_cvar = {
	.name = "sv_http_url_base",
	.description =
		"set to base url for http redirects of downloaded files",
	.default_value = "",
	.flags = CVAR_NONE,
	.value = { .type = 0, .value = &sv_http_url_base },
};

static void OutofBandPrintf (netadr_t where, const char *fmt, ...) __attribute__ ((format (PRINTF, 2, 3)));

//	USER STRINGCMD EXECUTION host_client and sv_player will be valid.

void
SV_WriteWorldVars (netchan_t *netchan)
{

	// send full levelname
	MSG_WriteString (&netchan->message,
					 PR_GetString (&sv_pr_state,
								   SVstring (sv.edicts, message)));

	// send the movevars
	MSG_WriteFloat (&netchan->message, movevars.gravity);
	MSG_WriteFloat (&netchan->message, movevars.stopspeed);
	MSG_WriteFloat (&netchan->message, movevars.maxspeed);
	MSG_WriteFloat (&netchan->message, movevars.spectatormaxspeed);
	MSG_WriteFloat (&netchan->message, movevars.accelerate);
	MSG_WriteFloat (&netchan->message, movevars.airaccelerate);
	MSG_WriteFloat (&netchan->message, movevars.wateraccelerate);
	MSG_WriteFloat (&netchan->message, movevars.friction);
	MSG_WriteFloat (&netchan->message, movevars.waterfriction);
	MSG_WriteFloat (&netchan->message, movevars.entgravity);

	// send music
	MSG_WriteByte (&netchan->message, svc_cdtrack);
	MSG_WriteByte (&netchan->message, SVfloat (sv.edicts, sounds));

	// send server info string
	MSG_WriteByte (&netchan->message, svc_stufftext);
	MSG_WriteString (&netchan->message,
					 va (0, "fullserverinfo \"%s\"\n",
						 Info_MakeString (svs.info, 0)));
}

/*
	SV_New_f

	Sends the first message from the server to a connected client.
	This will be sent on the initial connection and upon each server load.
*/
static void
SV_New_f (void *unused)
{
	int         playernum;
	const char *gamedir;

	if (host_client->state == cs_spawned)
		return;

	gamedir = Info_ValueForKey (svs.info, "*gamedir");
	if (!gamedir[0])
		gamedir = "qw";

	host_client->state = cs_connected;
	host_client->connection_started = realtime;

	// send the info about the new client to all connected clients

// NOTE:  This doesn't go through MSG_ReliableWrite since it's before the
// user spawns.  These functions are written to not overflow
	if (host_client->backbuf.num_backbuf) {
		SV_Printf ("WARNING %s: [SV_New] Back buffered (%d), clearing\n",
					host_client->name, host_client->netchan.message.cursize);
		host_client->backbuf.num_backbuf = 0;
		SZ_Clear (&host_client->netchan.message);
	}

	// send the serverdata
	MSG_WriteByte (&host_client->netchan.message, svc_serverdata);
	MSG_WriteLong (&host_client->netchan.message, PROTOCOL_VERSION);
	MSG_WriteLong (&host_client->netchan.message, svs.spawncount);
	MSG_WriteString (&host_client->netchan.message, gamedir);

	playernum = NUM_FOR_EDICT (&sv_pr_state, host_client->edict) - 1;
	if (host_client->spectator)
		playernum |= 128;
	MSG_WriteByte (&host_client->netchan.message, playernum);

	SV_WriteWorldVars (&host_client->netchan);

	// Trigger GIB connection event
	if (sv_client_connect_e->func)
		GIB_Event_Callback (sv_client_connect_e, 1,
							va (0, "%u", host_client->userid));
}

void
SV_WriteSoundlist (netchan_t *netchan, int n)
{
	const char **s;

	MSG_WriteByte (&netchan->message, svc_soundlist);
	MSG_WriteByte (&netchan->message, n);
	for (s = sv.sound_precache + 1 + n;
		 *s && netchan->message.cursize < (MAX_MSGLEN / 2);
		 s++, n++)
		MSG_WriteString (&netchan->message, *s);

	MSG_WriteByte (&netchan->message, 0);

	// next msg
	if (*s)
		MSG_WriteByte (&netchan->message, n);
	else
		MSG_WriteByte (&netchan->message, 0);
}

static void
SV_Soundlist_f (void *unused)
{
	unsigned    n;

	if (host_client->state != cs_connected) {
		SV_Printf ("soundlist not valid -- already spawned\n");
		return;
	}
	// handle the case of a level changing while a client was connecting
	if (atoi (Cmd_Argv (1)) != svs.spawncount) {
		SV_Printf ("SV_Soundlist_f from different level\n");
		SV_New_f (0);
		return;
	}

	n = atoi (Cmd_Argv (2));
	if (n >= MAX_SOUNDS) {
		SV_Printf ("SV_Soundlist_f: Invalid soundlist index\n");
		SV_New_f (0);
		return;
	}
// NOTE:  This doesn't go through MSG_ReliableWrite since it's before the
// user spawns.  These functions are written to not overflow
	if (host_client->backbuf.num_backbuf) {
		SV_Printf ("WARNING %s: [SV_Soundlist] Back buffered (%d), clearing",
					host_client->name, host_client->netchan.message.cursize);
		host_client->backbuf.num_backbuf = 0;
		SZ_Clear (&host_client->netchan.message);
	}

	SV_WriteSoundlist (&host_client->netchan, n);
}

void
SV_WriteModellist (netchan_t *netchan, int n)
{
	const char **s;

	MSG_WriteByte (&netchan->message, svc_modellist);
	MSG_WriteByte (&netchan->message, n);
	for (s = sv.model_precache + 1 + n;
		 *s && netchan->message.cursize < (MAX_MSGLEN / 2);
		 s++, n++)
		MSG_WriteString (&netchan->message, *s);
	MSG_WriteByte (&netchan->message, 0);

	// next msg
	if (*s)
		MSG_WriteByte (&netchan->message, n);
	else
		MSG_WriteByte (&netchan->message, 0);
}

static void
SV_Modellist_f (void *unused)
{
	unsigned    n;

	if (host_client->state != cs_connected) {
		SV_Printf ("modellist not valid -- already spawned\n");
		return;
	}
	// handle the case of a level changing while a client was connecting
	if (atoi (Cmd_Argv (1)) != svs.spawncount) {
		SV_Printf ("SV_Modellist_f from different level\n");
		SV_New_f (0);
		return;
	}

	n = atoi (Cmd_Argv (2));
	if (n >= MAX_MODELS) {
		SV_Printf ("SV_Modellist_f: Invalid modellist index\n");
		SV_New_f (0);
		return;
	}
// NOTE:  This doesn't go through MSG_ReliableWrite since it's before the
// user spawns.  These functions are written to not overflow
	if (host_client->backbuf.num_backbuf) {
		SV_Printf ("WARNING %s: [SV_Modellist] Back buffered (%d0, clearing",
					host_client->name, host_client->netchan.message.cursize);
		host_client->backbuf.num_backbuf = 0;
		SZ_Clear (&host_client->netchan.message);
	}

	SV_WriteModellist (&host_client->netchan, n);
}

static void
SV_PreSpawn_f (void *unused)
{
	const char *command;
	int         buf, size;
	unsigned    check;
	sizebuf_t  *msg;

	if (host_client->state != cs_connected) {
		SV_Printf ("prespawn not valid -- already spawned\n");
		return;
	}
	// handle the case of a level changing while a client was connecting
	if (atoi (Cmd_Argv (1)) != svs.spawncount) {
		SV_Printf ("SV_PreSpawn_f from different level\n");
		SV_New_f (0);
		return;
	}

	buf = atoi (Cmd_Argv (2));
	if (buf >= sv.num_signon_buffers)
		buf = 0;

	if (!buf) {
		// should be three numbers following containing checksums
		check = atoi (Cmd_Argv (3));

//      Sys_MaskPrintf (SYS_dev, , "Client check = %d\n", check);

		if (sv_mapcheck && check != sv.worldmodel->brush.checksum &&
			check != sv.worldmodel->brush.checksum2) {
			SV_ClientPrintf (1, host_client, PRINT_HIGH, "Map model file does "
							 "not match (%s), %i != %i/%i.\n"
							 "You may need a new version of the map, or the "
							 "proper install files.\n",
							 sv.modelname, check,
							 sv.worldmodel->brush.checksum,
							 sv.worldmodel->brush.checksum2);
			SV_DropClient (host_client);
			return;
		}
	}

	host_client->prespawned = true;

	if (buf == sv.num_signon_buffers - 1)
		command = va (0, "cmd spawn %i 0\n", svs.spawncount);
	else
		command = va (0, "cmd prespawn %i %i\n", svs.spawncount, buf + 1);

	size = sv.signon_buffer_size[buf] + 1 + strlen (command) + 1;

	MSG_ReliableCheckBlock (&host_client->backbuf, size);
	if (host_client->backbuf.num_backbuf)
		msg = &host_client->backbuf.backbuf;
	else
		msg = &host_client->netchan.message;

	SZ_Write (msg, sv.signon_buffers[buf], sv.signon_buffer_size[buf]);

	MSG_WriteByte (msg, svc_stufftext);
	MSG_WriteString (msg, command);
}

void
SV_Spawn (client_t *client)
{
	edict_t    *ent;

	// set up the edict
	ent = client->edict;

	memset (&E_fld (ent, 0), 0, sv_pr_state.progs->entityfields * 4);
	SVfloat (ent, colormap) = NUM_FOR_EDICT (&sv_pr_state, ent);
	SVfloat (ent, team) = 0; // FIXME
	SVstring (ent, netname) = PR_SetString (&sv_pr_state, client->name);

	client->entgravity = 1.0;
	if (sv_fields.gravity != -1)
		SVfloat (ent, gravity) = 1.0;
	client->maxspeed = sv_maxspeed;
	if (sv_fields.maxspeed != -1)
		SVfloat (ent, maxspeed) = sv_maxspeed;
}

void
SV_WriteSpawn1 (backbuf_t *backbuf, int n)
{
	int         i;
	client_t   *client;

	// normally this could overflow, but no need to check due to backbuf
	for (i = n, client = svs.clients + n; i < MAX_CLIENTS; i++, client++)
		SV_FullClientUpdateToClient (client, backbuf);

	// send all current light styles
	for (i = 0; i < MAX_LIGHTSTYLES; i++) {
		MSG_ReliableWrite_Begin (backbuf, svc_lightstyle,
								 3 + (sv.lightstyles[i] ?
									  strlen (sv.lightstyles[i]) : 1));
		MSG_ReliableWrite_Byte (backbuf, (char) i);
		MSG_ReliableWrite_String (backbuf, sv.lightstyles[i]);
	}
}

void
SV_WriteSpawn2 (backbuf_t *backbuf)
{

	MSG_ReliableWrite_Begin (backbuf, svc_updatestatlong, 6);
	MSG_ReliableWrite_Byte (backbuf, STAT_TOTALSECRETS);
	MSG_ReliableWrite_Long (backbuf, *sv_globals.total_secrets);

	MSG_ReliableWrite_Begin (backbuf, svc_updatestatlong, 6);
	MSG_ReliableWrite_Byte (backbuf, STAT_TOTALMONSTERS);
	MSG_ReliableWrite_Long (backbuf, *sv_globals.total_monsters);

	MSG_ReliableWrite_Begin (backbuf, svc_updatestatlong, 6);
	MSG_ReliableWrite_Byte (backbuf, STAT_SECRETS);
	MSG_ReliableWrite_Long (backbuf, *sv_globals.found_secrets);

	MSG_ReliableWrite_Begin (backbuf, svc_updatestatlong, 6);
	MSG_ReliableWrite_Byte (backbuf, STAT_MONSTERS);
	MSG_ReliableWrite_Long (backbuf, *sv_globals.killed_monsters);
}

static void
SV_Spawn_f (void *unused)
{
	int         n;

	if (host_client->state != cs_connected) {
		SV_Printf ("Spawn not valid -- already spawned\n");
		return;
	}
// handle the case of a level changing while a client was connecting
	if (atoi (Cmd_Argv (1)) != svs.spawncount) {
		SV_Printf ("SV_Spawn_f from different level\n");
		SV_New_f (0);
		return;
	}
// make sure they're not trying to cheat by spawning without prespawning
	if (host_client->prespawned == false) {
		SV_BroadcastPrintf (PRINT_HIGH, "%s has been kicked for trying to "
							"spawn before prespawning!\n", host_client->name);
		SV_DropClient (host_client);
		return;
	}

	n = atoi (Cmd_Argv (2));

	// make sure n is valid
	if (n < 0 || n > MAX_CLIENTS) {
		SV_Printf ("SV_Spawn_f invalid client start\n");
		SV_New_f (0);
		return;
	}

	host_client->spawned = true;

	// send all current names, colors, and frag counts
	// FIXME: is this a good thing?
	SZ_Clear (&host_client->netchan.message);

	// send current status of all other players
	SV_WriteSpawn1 (&host_client->backbuf, n);

	SV_Spawn (host_client);

	SV_WriteSpawn2 (&host_client->backbuf);

//
// force stats to be updated
//
	memset (host_client->stats, 0, sizeof (host_client->stats));

	// get the client to check and download skins
	// when that is completed, a begin command will be issued
	MSG_ReliableWrite_Begin (&host_client->backbuf, svc_stufftext, 8);
	MSG_ReliableWrite_String (&host_client->backbuf, "skins\n");
}

static void
SV_SpawnSpectator (void)
{
	VectorZero (SVvector (sv_player, origin));
	VectorZero (SVvector (sv_player, view_ofs));
	SVvector (sv_player, view_ofs)[2] = 22;

	// search for an info_playerstart to spawn the spectator at
	for (unsigned i = MAX_CLIENTS - 1; i < sv.num_edicts; i++) {
		edict_t    *e = EDICT_NUM (&sv_pr_state, i);
		if (!strcmp (PR_GetString (&sv_pr_state, SVstring (e, classname)),
					 "info_player_start")) {
			VectorCopy (SVvector (e, origin), SVvector (sv_player, origin));
			return;
		}
	}

}

static void
SV_Begin_f (void *unused)
{
	unsigned int	pmodel = 0, emodel = 0;
	int				i;

	if (host_client->state != cs_connected)
		return;							// don't begin again

	host_client->state = cs_spawned;

	// handle the case of a level changing while a client was connecting
	if (atoi (Cmd_Argv (1)) != svs.spawncount) {
		SV_Printf ("SV_Begin_f from different level\n");
		SV_New_f (0);
		return;
	}

	// make sure they're not trying to cheat by beginning without spawning
	if (host_client->spawned == false) {
		SV_BroadcastPrintf (PRINT_HIGH, "%s has been kicked for trying to "
							"begin before spawning!\n"
							"Have a nice day!\n", // 1 string!
							host_client->name);
		SV_DropClient (host_client);
		return;
	}

	if (host_client->spectator) {
		SV_SpawnSpectator ();

		if (sv_funcs.SpectatorConnect) {
			// copy spawn parms out of the client_t
			for (i = 0; i < NUM_SPAWN_PARMS; i++)
				sv_globals.parms[i] = host_client->spawn_parms[i];

			// call the spawn function
			*sv_globals.time = sv.time;
			*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, sv_player);
			PR_ExecuteProgram (&sv_pr_state, sv_funcs.SpectatorConnect);
		}
	} else {
		// copy spawn parms out of the client_t
		for (i = 0; i < NUM_SPAWN_PARMS; i++)
			sv_globals.parms[i] = host_client->spawn_parms[i];

		// call the spawn function
		*sv_globals.time = sv.time;
		*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, sv_player);
		PR_ExecuteProgram (&sv_pr_state, sv_funcs.ClientConnect);

		// actually spawn the player
		*sv_globals.time = sv.time;
		*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, sv_player);
		PR_ExecuteProgram (&sv_pr_state, sv_funcs.PutClientInServer);
	}

	// clear the net statistics, because connecting gives a bogus picture
	host_client->last_check = -1;
	host_client->msecs = 0;
	host_client->netchan.frame_latency = 0;
	host_client->netchan.frame_rate = 0;
	host_client->netchan.drop_count = 0;
	host_client->netchan.good_count = 0;

	// check he's not cheating
	pmodel = atoi (Info_ValueForKey (host_client->userinfo, "pmodel"));
	emodel = atoi (Info_ValueForKey (host_client->userinfo, "emodel"));

	if (pmodel != sv.model_player_checksum || emodel
		!= sv.eyes_player_checksum)
		SV_BroadcastPrintf (PRINT_HIGH, "%s WARNING: non standard "
							"player/eyes model detected\n", host_client->name);

	// if we are paused, tell the client
	if (sv.paused) {
		MSG_ReliableWrite_Begin (&host_client->backbuf, svc_setpause, 2);
		MSG_ReliableWrite_Byte (&host_client->backbuf, sv.paused);
		SV_ClientPrintf (1, host_client, PRINT_HIGH, "Server is paused.\n");
	}
#if 0
// send a fixangle over the reliable channel to make sure it gets there
// Never send a roll angle, because savegames can catch the server
// in a state where it is expecting the client to correct the angle
// and it won't happen if the game was just loaded, so you wind up
// with a permanent head tilt
	ent = EDICT_NUM (&sv_pr_state, 1 + (host_client - svs.clients));
	MSG_WriteByte (&host_client->netchan.message, svc_setangle);
	for (i = 0; i < 2; i++)
		MSG_WriteAngle (&host_client->netchan.message,
						SVvector (ent, angles)[i]);
	MSG_WriteAngle (&host_client->netchan.message, 0);
#endif

	// Trigger GIB events
	if (sv_client_spawn_e->func)
		GIB_Event_Callback (sv_client_spawn_e, 1, va (0, "%u",
													  host_client->userid));
}

//=============================================================================

static void
SV_NextDownload_f (void *unused)
{
	byte        buffer[768];		// FIXME protocol limit? could be bigger?
	int         percent, size;
	size_t      r;

	if (!host_client->download)
		return;

	r = host_client->downloadsize - host_client->downloadcount;
	if (r > sizeof (buffer))
		r = sizeof (buffer);
	r = Qread (host_client->download, buffer, r);
	MSG_ReliableWrite_Begin (&host_client->backbuf, svc_download, 6 + r);
	MSG_ReliableWrite_Short (&host_client->backbuf, r);

	host_client->downloadcount += r;
	size = host_client->downloadsize;
	if (!size)
		size = 1;
	percent = host_client->downloadcount * 100 / size;
	MSG_ReliableWrite_Byte (&host_client->backbuf, percent);
	MSG_ReliableWrite_SZ (&host_client->backbuf, buffer, r);

	if (host_client->downloadcount != host_client->downloadsize)
		return;

	Qclose (host_client->download);
	host_client->download = NULL;

}

static void
SV_NextUpload (void)
{
	int		percent, size;

	if (!host_client->upload) {
		SV_ClientPrintf (1, host_client, PRINT_HIGH, "Upload denied\n");
		MSG_ReliableWrite_Begin (&host_client->backbuf, svc_stufftext, 8);
		MSG_ReliableWrite_String (&host_client->backbuf, "stopul");

		// suck out rest of packet
		size = MSG_ReadShort (net_message);
		MSG_ReadByte (net_message);
		net_message->readcount += size;
		return;
	}

	size = MSG_ReadShort (net_message);
	percent = MSG_ReadByte (net_message);

	if (!host_client->upload_started) {
		host_client->upload_started = 1;
		SV_Printf ("Receiving %s from %d...\n", host_client->uploadfn->str,
					host_client->userid);
		if (host_client->remote_snap)
			OutofBandPrintf (host_client->snap_from,
							 "Server receiving %s from %d...\n",
							 host_client->uploadfn->str, host_client->userid);
	}

	Qwrite (host_client->upload, net_message->message->data +
			net_message->readcount, size);
	net_message->readcount += size;

	Sys_MaskPrintf (SYS_dev, "UPLOAD: %d received\n", size);

	if (percent != 100) {
		MSG_ReliableWrite_Begin (&host_client->backbuf, svc_stufftext, 8);
		MSG_ReliableWrite_String (&host_client->backbuf, "nextul\n");
	} else {
		Qclose (host_client->upload);
		host_client->upload = NULL;

		SV_Printf ("%s upload completed.\n", host_client->uploadfn->str);

		if (host_client->remote_snap) {
			char	*p;

			if ((p = strchr (host_client->uploadfn->str, '/')) != NULL)
				p++;
			else
				p = host_client->uploadfn->str;
			OutofBandPrintf (host_client->snap_from, "%s upload completed.\n"
							 "To download, enter:\ndownload %s\n",
							 host_client->uploadfn->str, p);
		}
		dstring_delete (host_client->uploadfn);
		host_client->uploadfn = 0;
		host_client->upload_started = 0;
	}

}

static void
SV_BeginDownload_f (void *unused)
{
	const char *name;
	int			http, zip;
	QFile	   *file;

	name = Cmd_Argv (1);
// hacked by zoid to allow more conrol over download
	// first off, no .. or global allow check
	if (strstr (name, "..") || !allow_download
		// leading dot is no good
		|| *name == '.'
		// next up, skin check
		|| (strncmp (name, "skins/", 6) == 0 && !allow_download_skins)
		// now models
		|| (strncmp (name, "progs/", 6) == 0 &&
			!allow_download_models)
		// now sounds
		|| (strncmp (name, "sound/", 6) == 0 &&
			!allow_download_sounds)
		// now maps (note special case for maps, must not be in pak)
		|| (strncmp (name, "maps/", 5) == 0 && !allow_download_maps)
		// MUST be in a subdirectory
		|| !strstr (name, "/")) {		// don't allow anything with .. path
		MSG_ReliableWrite_Begin (&host_client->backbuf, svc_download, 4);
		MSG_ReliableWrite_Short (&host_client->backbuf, -1);
		MSG_ReliableWrite_Byte (&host_client->backbuf, 0);
		return;
	}

	if (host_client->download) {
		Qclose (host_client->download);
		host_client->download = NULL;
	}

	zip = strchr (Info_ValueForKey (host_client->userinfo, "*cap"), 'z') != 0;
	http = sv_http_url_base[0]
			&& strchr (Info_ValueForKey (host_client->userinfo, "*cap"), 'h');

	file = _QFS_FOpenFile (name, !zip);

	host_client->download = file;
	host_client->downloadsize = file ? Qfilesize (file) : 0;
	host_client->downloadcount = 0;

	if (!host_client->download
		// ZOID: special check for maps, if it came from a pak file, don't
		// allow download
		|| (strncmp (name, "maps/", 5) == 0 && qfs_foundfile.in_pak)) {
		if (host_client->download) {
			Qclose (host_client->download);
			host_client->download = NULL;
		}

		SV_Printf ("Couldn't download %s to %s\n", name, host_client->name);
		MSG_ReliableWrite_Begin (&host_client->backbuf, svc_download, 4);
		MSG_ReliableWrite_Short (&host_client->backbuf, DL_NOFILE);
		MSG_ReliableWrite_Byte (&host_client->backbuf, 0);
		return;
	}

	if (http) {
		int         size;
		int         ren = zip && strcmp (qfs_foundfile.realname, name);
		SV_Printf ("http redirect: %s/%s\n", sv_http_url_base,
				   qfs_foundfile.realname);
		size = ren ? strlen (qfs_foundfile.realname) * 2 : strlen (name);
		size += strlen (sv_http_url_base) + 7;
		MSG_ReliableWrite_Begin (&host_client->backbuf, svc_download, size);
		MSG_ReliableWrite_Short (&host_client->backbuf, DL_HTTP);
		MSG_ReliableWrite_Byte (&host_client->backbuf, 0);
		MSG_ReliableWrite_String (&host_client->backbuf,
								  va (0, "%s/%s", sv_http_url_base,
									  ren ? qfs_foundfile.realname : name));
		MSG_ReliableWrite_String (&host_client->backbuf,
								  ren ? qfs_foundfile.realname : "");
		if (host_client->download) {
			Qclose (host_client->download);
			host_client->download = NULL;
		}
	} else {
		if (zip && strcmp (qfs_foundfile.realname, name)) {
			SV_Printf ("download renamed to %s\n", qfs_foundfile.realname);
			MSG_ReliableWrite_Begin (&host_client->backbuf, svc_download,
									 strlen (qfs_foundfile.realname) + 5);
			MSG_ReliableWrite_Short (&host_client->backbuf, DL_RENAME);
			MSG_ReliableWrite_Byte (&host_client->backbuf, 0);
			MSG_ReliableWrite_String (&host_client->backbuf,
									  qfs_foundfile.realname);
			MSG_Reliable_FinishWrite (&host_client->backbuf);
		}
	}

	SV_NextDownload_f (0);
	SV_Printf ("Downloading %s to %s\n", name, host_client->name);
}

//=============================================================================

static void
SV_Say (bool team)
{
	char       *i, *p;
	dstring_t  *text;
	const char *t1 = 0, *t2, *type;
	client_t   *client;
	int			tmp, j, cls = 0;
	sizebuf_t  *dbuf;

	if (Cmd_Argc () < 2)
		return;

	if (host_client->state < cs_connected)
		return;

	if (team)
		t1 = Info_ValueForKey (host_client->userinfo, "team");

	if (fp_messages) {
		if (!sv.paused && realtime < host_client->lockedtill) {
			SV_ClientPrintf (1, host_client, PRINT_CHAT,
							 "You can't talk for %d more seconds\n",
							 (int) (host_client->lockedtill - realtime));
			return;
		}
		tmp = host_client->whensaidhead - fp_messages + 1;
		if (tmp < 0)
			tmp = 10 + tmp;
		if (!sv.paused && host_client->whensaid[tmp]
			&& (realtime - host_client->whensaid[tmp] < fp_persecond)) {
			host_client->lockedtill = realtime + fp_secondsdead;
			if (fp_msg[0])
				SV_ClientPrintf (1, host_client, PRINT_CHAT,
								 "FloodProt: %s\n", fp_msg);
			else
				SV_ClientPrintf (1, host_client, PRINT_CHAT,
								 "FloodProt: You can't talk for %d seconds.\n",
								 fp_secondsdead);
			return;
		}
		host_client->whensaidhead++;
		if (host_client->whensaidhead > 9)
			host_client->whensaidhead = 0;
		host_client->whensaid[host_client->whensaidhead] = realtime;
	}

	p = Hunk_TempAlloc (0, strlen (Cmd_Args (1)) + 1);
	strcpy (p, Cmd_Args (1));

	if (*p == '"') {
		p++;
		p[strlen (p) - 1] = 0;
	}

	if (!sv_allowfake || (!team && sv_allowfake == 2)) {
		for (i = p; *i; i++) {
			if (*i == 13) { // ^M
				if (sv_kickfake) {
						SV_BroadcastPrintf (PRINT_HIGH, "%s was kicked for "
											"attempting to fake messages\n",
											host_client->name);
						SV_ClientPrintf (1, host_client, PRINT_HIGH, "You were kicked "
										 "for attempting to fake messages\n");
						SV_DropClient (host_client);
						return;
				} else
					*i = '#';
			}
		}
	}

	if (sv_funcs.ChatMessage) {
		PR_PushFrame (&sv_pr_state);
		PR_RESET_PARAMS (&sv_pr_state);
		P_STRING (&sv_pr_state, 0) = PR_SetTempString (&sv_pr_state, p);
		P_FLOAT (&sv_pr_state, 1) = (float) team;

		*sv_globals.time = sv.time;
		*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, sv_player);
		PR_ExecuteProgram (&sv_pr_state, sv_funcs.ChatMessage);
		PR_PopFrame (&sv_pr_state);
		if (R_FLOAT (&sv_pr_state))
			return;
	}

	text = dstring_new ();
	if (host_client->spectator && (!sv_spectalk || team)) {
		type = "2";
		dsprintf (text, "[SPEC] %s: ", host_client->name);
	} else if (team) {
		type = "1";
		dsprintf (text, "(%s): ", host_client->name);
	} else {
		type = "0";
		dsprintf (text, "%s: ", host_client->name);
	}

	if (sv_chat_e->func)
		GIB_Event_Callback (sv_chat_e, 2, va (0, "%i", host_client->userid), p,
							type);

	dstring_appendstr (text, p);
	dstring_appendstr (text, "\n");

	SV_Printf ("%s", text->str);

	for (j = 0, client = svs.clients; j < MAX_CLIENTS; j++, client++) {
		if (client->state < cs_connected)	// Clients connecting can hear. //FIXME record to mvd?
			continue;
		if (host_client->spectator && !sv_spectalk)
			if (!client->spectator)
				continue;

		if (team) {
			// the spectator team
			if (host_client->spectator) {
				if (!client->spectator)
					continue;
			} else {
				t2 = Info_ValueForKey (client->userinfo, "team");
				if (strcmp (t1, t2) || client->spectator)
					continue;			// on different teams
			}
		}
		cls |= 1 << j;
		SV_ClientPrintf (0, client, PRINT_CHAT, "%s", text->str);
	}

	if (!sv.recorders || !cls) {
		dstring_delete (text);
		return;
	}
	// non-team messages should be seen allways, even if not tracking any
	// player
	if (!team && ((host_client->spectator && sv_spectalk)
				  || !host_client->spectator)) {
		dbuf = SVR_WriteBegin (dem_all, 0, strlen (text->str) + 3);
	} else {
		dbuf = SVR_WriteBegin (dem_multiple, cls, strlen (text->str) + 3);
	}
	MSG_WriteByte (dbuf, svc_print);
	MSG_WriteByte (dbuf, PRINT_CHAT);
	MSG_WriteString (dbuf, text->str);
	dstring_delete (text);
}

static void
SV_Say_f (void *unused)
{
	SV_Say (false);
}

static void
SV_Say_Team_f (void *unused)
{
	SV_Say (true);
}

//============================================================================

/*
	SV_Pings_f

	The client is showing the scoreboard, so send new ping times for all
	clients
*/
static void
SV_Pings_f (void *unused)
{
	client_t   *client;
	int         j;

	for (j = 0, client = svs.clients; j < MAX_CLIENTS; j++, client++) {
		if (client->state != cs_spawned && client->state != cs_server)
			continue;

		MSG_ReliableWrite_Begin (&host_client->backbuf, svc_updateping, 4);
		MSG_ReliableWrite_Byte (&host_client->backbuf, j);
		MSG_ReliableWrite_Short (&host_client->backbuf, SV_CalcPing (client));
		MSG_ReliableWrite_Begin (&host_client->backbuf, svc_updatepl, 4);
		MSG_ReliableWrite_Byte (&host_client->backbuf, j);
		MSG_ReliableWrite_Byte (&host_client->backbuf, client->lossage);
	}
}

static void
SV_Kill_f (void *unused)
{
	if (SVfloat (sv_player, health) <= 0) {
		SV_BeginRedirect (RD_CLIENT);
		SV_ClientPrintf (1, host_client, PRINT_HIGH,
						 "Can't suicide -- already dead!\n");
		SV_EndRedirect ();
		return;
	}

	*sv_globals.time = sv.time;
	*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, sv_player);
	PR_ExecuteProgram (&sv_pr_state, sv_funcs.ClientKill);
}

void
SV_TogglePause (const char *msg)
{
	client_t   *cl;
	int         i;

	sv.paused ^= 1;
	net_nochoke = sv.paused;

	if (msg)
		SV_BroadcastPrintf (PRINT_HIGH, "%s", msg);

	// send notification to all clients
	for (i = 0, cl = svs.clients; i < MAX_CLIENTS; i++, cl++) {
		if (cl->state < cs_zombie)
			continue;
		MSG_ReliableWrite_Begin (&cl->backbuf, svc_setpause, 2);
		MSG_ReliableWrite_Byte (&cl->backbuf, sv.paused);
	}
}

static void
SV_Pause_f (void *unused)
{
	char			st[sizeof (host_client->name) + 32];
	static double	lastpausetime;
	double			currenttime;

	currenttime = Sys_DoubleTime ();

	if (lastpausetime + 1 > currenttime) {
		SV_ClientPrintf (1, host_client, PRINT_HIGH,
						 "Pause flood not allowed.\n");
		return;
	}

	lastpausetime = currenttime;

	if (!pausable) {
		SV_ClientPrintf (1, host_client, PRINT_HIGH, "Pause not allowed.\n");
		return;
	}

	if (host_client->spectator) {
		SV_ClientPrintf (1, host_client, PRINT_HIGH,
						 "Spectators can not pause.\n");
		return;
	}

	if (!sv.paused)
		snprintf (st, sizeof (st), "%s paused the game\n", host_client->name);
	else
		snprintf (st, sizeof (st), "%s unpaused the game\n",
				  host_client->name);

	SV_TogglePause (st);
}

/*
	SV_Drop_f

	The client is going to disconnect, so remove the connection immediately
*/
static void
SV_Drop_f (void *unused)
{
	SV_EndRedirect ();
	if (!host_client->spectator)
		SV_BroadcastPrintf (PRINT_HIGH, "%s dropped\n", host_client->name);
	SV_DropClient (host_client);
}

/*
	SV_PTrack_f

	Change the bandwidth estimate for a client
*/
static void
SV_PTrack_f (void *unused)
{
	edict_t    *ent, *tent;
	int         i;

	if (!host_client->spectator)
		return;

	if (Cmd_Argc () != 2) {
		// turn off tracking
		host_client->spec_track = 0;
		ent = EDICT_NUM (&sv_pr_state, host_client - svs.clients + 1);
		tent = EDICT_NUM (&sv_pr_state, 0);
		SVentity (ent, goalentity) = EDICT_TO_PROG (&sv_pr_state, tent);
		return;
	}

	i = atoi (Cmd_Argv (1));
	if (i < 0 || i >= MAX_CLIENTS
		|| (svs.clients[i].state != cs_spawned
			&& svs.clients[i].state != cs_server)
		|| svs.clients[i].spectator) {
		SV_ClientPrintf (1, host_client, PRINT_HIGH,
						 "Invalid client to track\n");
		host_client->spec_track = 0;
		ent = EDICT_NUM (&sv_pr_state, host_client - svs.clients + 1);
		tent = EDICT_NUM (&sv_pr_state, 0);
		SVentity (ent, goalentity) = EDICT_TO_PROG (&sv_pr_state, tent);
		return;
	}
	host_client->spec_track = i + 1;	// now tracking

	ent = EDICT_NUM (&sv_pr_state, host_client - svs.clients + 1);
	tent = EDICT_NUM (&sv_pr_state, i + 1);
	SVentity (ent, goalentity) = EDICT_TO_PROG (&sv_pr_state, tent);
}

/*
	SV_Rate_f

	Change the bandwidth estimate for a client
*/
static void
SV_Rate_f (void *unused)
{
	int		rate;

	if (Cmd_Argc () != 2) {
		SV_ClientPrintf (1, host_client, PRINT_HIGH, "Current rate is %i\n",
						 (int) (1.0 / host_client->netchan.rate + 0.5));
		return;
	}

	rate = atoi (Cmd_Argv (1));
	if (sv_maxrate) {
		rate = bound (500, rate, sv_maxrate);
	} else {
		rate = max (500, rate);
	}

	SV_ClientPrintf (1, host_client, PRINT_HIGH, "Net rate set to %i\n", rate);
	host_client->netchan.rate = 1.0 / rate;
}

/*
	SV_Msg_f

	Change the message level for a client
*/
static void
SV_Msg_f (void *unused)
{
	if (Cmd_Argc () != 2) {
		SV_ClientPrintf (1, host_client, PRINT_HIGH,
						 "Current msg level is %i\n",
						 host_client->messagelevel);
		return;
	}

	host_client->messagelevel = atoi (Cmd_Argv (1));

	SV_ClientPrintf (1, host_client, PRINT_HIGH, "Msg level set to %i\n",
					 host_client->messagelevel);
}

void
SV_SetUserinfo (client_t *client, const char *key, const char *value)
{
	char       *oldvalue = 0;
	int         send_changes = 1;

	if (sv_setinfo_e->func || sv_funcs.UserInfoChanged)
		oldvalue = strdup (Info_ValueForKey (client->userinfo, key));
	if (!Info_SetValueForKey (client->userinfo, key, value,
							  !sv_highchars)) {
		// key hasn't changed
		if (oldvalue)
			free (oldvalue);
		return;
	}

	// process any changed values
	SV_ExtractFromUserinfo (client);

	// trigger a GIB event
	if (sv_setinfo_e->func)
		GIB_Event_Callback (sv_setinfo_e, 4, va (0, "%d", client->userid),
							key, oldvalue, value);

	if (sv_funcs.UserInfoChanged) {
		PR_PushFrame (&sv_pr_state);
		*sv_globals.time = sv.time;
		*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, client->edict);
		PR_RESET_PARAMS (&sv_pr_state);
		P_STRING (&sv_pr_state, 0) = PR_SetTempString (&sv_pr_state, key);
		P_STRING (&sv_pr_state, 1) = PR_SetTempString (&sv_pr_state, oldvalue);
		P_STRING (&sv_pr_state, 2) = PR_SetTempString (&sv_pr_state, value);
		sv_pr_state.pr_argc = 3;
		PR_ExecuteProgram (&sv_pr_state, sv_funcs.UserInfoChanged);
		PR_PopFrame (&sv_pr_state);
		send_changes = !R_FLOAT (&sv_pr_state);
	}

	if (oldvalue)
		free (oldvalue);

	if (send_changes && Info_FilterForKey (key, client_info_filters)) {
		MSG_WriteByte (&sv.reliable_datagram, svc_setinfo);
		MSG_WriteByte (&sv.reliable_datagram, client - svs.clients);
		MSG_WriteString (&sv.reliable_datagram, key);
		MSG_WriteString (&sv.reliable_datagram, value);
	}
}

/*
	SV_SetInfo_f

	Allow clients to change userinfo
*/
static void
SV_SetInfo_f (void *unused)
{
	const char *key;
	const char *value;

	if (Cmd_Argc () == 1) {
		SV_Printf ("User info settings:\n");
		Info_Print (host_client->userinfo);
		return;
	}

	if (Cmd_Argc () != 3) {
		SV_Printf ("usage: setinfo [ <key> <value> ]\n");
		return;
	}

	if (Cmd_Argv (1)[0] == '*')
		return;							// don't set priveledged values

	key = Cmd_Argv (1);
	value = Cmd_Argv (2);

	if (sv_funcs.UserInfoCallback) {
		PR_PushFrame (&sv_pr_state);
		*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, sv_player);
		PR_RESET_PARAMS (&sv_pr_state);
		P_STRING (&sv_pr_state, 0) = PR_SetTempString (&sv_pr_state, key);
		P_STRING (&sv_pr_state, 1) = PR_SetTempString (&sv_pr_state, value);
		sv_pr_state.pr_argc = 2;
		PR_ExecuteProgram (&sv_pr_state, sv_funcs.UserInfoCallback);
		PR_PopFrame (&sv_pr_state);
		if (R_FLOAT (&sv_pr_state))
			return;
	}

	SV_SetUserinfo (host_client, key, value);
}

/*
	SV_ShowServerinfo_f

	Dump serverinfo into a string
*/
static void
SV_ShowServerinfo_f (void *unused)
{
	Info_Print (svs.info);
}

static void
SV_NoSnap_f (void *unused)
{
	if (host_client->uploadfn) {
		dstring_delete (host_client->uploadfn);
		host_client->uploadfn = 0;
		SV_BroadcastPrintf (PRINT_HIGH, "%s refused remote screenshot\n",
							host_client->name);
	}
}

ucmd_t      ucmds[] = {
	{"new",			SV_New_f,			0, 0},
	{"modellist",	SV_Modellist_f,		0, 0},
	{"soundlist",	SV_Soundlist_f,		0, 0},
	{"prespawn",	SV_PreSpawn_f,		0, 0},
	{"spawn",		SV_Spawn_f,			0, 0},
	{"begin",		SV_Begin_f,			1, 0},

	{"drop",		SV_Drop_f,			0, 0},
	{"pings",		SV_Pings_f,			0, 0},

// issued by hand at client consoles
	{"rate",		SV_Rate_f,			0, 0},
	{"kill",		SV_Kill_f,			1, 1},
	{"pause",		SV_Pause_f,			1, 0},
	{"msg",			SV_Msg_f,			0, 0},

	{"say",			SV_Say_f,			1, 1},
	{"say_team",	SV_Say_Team_f,		1, 1},

	{"setinfo",		SV_SetInfo_f,		1, 0},

	{"serverinfo",	SV_ShowServerinfo_f,0, 0},

	{"download",	SV_BeginDownload_f, 1, 0},
	{"nextdl",		SV_NextDownload_f,	0, 0},

	{"ptrack",		SV_PTrack_f,		0, 1},		// ZOID - used with autocam

	{"snap",		SV_NoSnap_f,		0, 0},

};

static hashtab_t *ucmd_table;
int (*ucmd_unknown)(void);

static void
call_qc_hook (void *qc_hook)
{
	*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, sv_player);
	PR_ExecuteProgram (&sv_pr_state, (pr_func_t) (intptr_t) qc_hook);
}

static const char *
ucmds_getkey (const void *_a, void *unused)
{
	ucmd_t *a = (ucmd_t*)_a;
	return a->name;
}

static void
ucmds_free (void *_c, void *unused)
{
	ucmd_t *c = (ucmd_t*)_c;
	if (c->freeable) {
		if (c->on_free)
			c->on_free (c->userdata);
		free ((char *)c->name);
		free (c);
	}
}

/*
	hash and compare functions: user commands are accessed by name but
	removed by pointer (because they are overridable and multiple commands
	can be stored under the same command name) so we define a get_hash
	functions that hashes the key and a compare function that compares the
	pointer.
*/

static uintptr_t
ucmd_get_hash (const void *_a, void *data)
{
	ucmd_t *a = (ucmd_t*)_a;
	return Hash_String (a->name);
}

static int
ucmd_compare (const void *a, const void *b, void *data)
{
	return a == b;
}

/*
	SV_AddUserCommand

	Adds a new user command.  Returns a pointer to the command object (to be
	used by SV_RemoveUserCommand) if succesful, NULL if the command already
	exists and is not overridable.
*/
void *
SV_AddUserCommand (const char *name, void (*func) (void *userdata), int flags,
				   void *userdata, void (*on_free) (void *userdata))
{
	ucmd_t *cmd;

	cmd = Hash_Find (ucmd_table, name);
	if (cmd && !cmd->overridable)
		return NULL;

	cmd = calloc (1, sizeof (ucmd_t));
	cmd->freeable = 1;
	cmd->name = strdup (name);
	cmd->func = func;
	cmd->no_redirect = (flags & UCMD_NO_REDIRECT) ? 1 : 0;
	cmd->overridable = (flags & UCMD_OVERRIDABLE) ? 1 : 0;
	cmd->userdata = userdata;
	cmd->on_free = on_free;
	Hash_Add (ucmd_table, cmd);

	return cmd;
}

/*
	Removes a user command added with SV_AddUserCommand.  Returns true if
	successful, false if not.
*/
int
SV_RemoveUserCommand (void *cmd)
{
	void *ele = Hash_DelElement(ucmd_table, cmd);

	if (!ele)
		return 0;

	Hash_Free (ucmd_table, ele);
	return 1;
}

static void
PF_SV_AddUserCommand (progs_t *pr, void *data)
{
	const char *name = P_GSTRING (pr, 0);
	ucmd_t     *cmd;
	cmd = SV_AddUserCommand (name, call_qc_hook,
							 P_INT (pr, 2) ? UCMD_NO_REDIRECT : 0,
							 (void *) (intptr_t) P_FUNCTION (pr, 1),
							 NULL);

	if (!cmd)
		SV_Printf ("%s already a user command\n", name);
}

void
SV_SetupUserCommands (void)
{
	size_t      i;

	Hash_FlushTable (ucmd_table);
	for (i = 0; i < sizeof (ucmds) / sizeof (ucmds[0]); i++)
		Hash_Add (ucmd_table, &ucmds[i]);
}

/*
	SV_ExecuteUserCommand

	Uhh...execute user command. :)
*/
void
SV_ExecuteUserCommand (const char *s)
{
	ucmd_t     *u;

	COM_TokenizeString (s, sv_args);
	cmd_args = sv_args;
	sv_player = host_client->edict;

	u = (ucmd_t*) Hash_Find (ucmd_table, sv_args->argv[0]->str);

	if (!u) {
		if (!ucmd_unknown || !ucmd_unknown ()) {
			SV_BeginRedirect (RD_CLIENT);
			SV_Printf ("Bad user command: %s\n", sv_args->argv[0]->str);
			SV_EndRedirect ();
		}
	} else {
		if (!u->no_redirect)
			SV_BeginRedirect (RD_CLIENT);
		u->func (u->userdata);
		if (!u->no_redirect)
			SV_EndRedirect ();
	}
}

// USER CMD EXECUTION =========================================================

/*
	SV_CalcRoll

	Used by view and sv_user
*/
static float
SV_CalcRoll (vec3_t angles, vec3_t velocity)
{
	vec3_t		forward, right, up;
	float		side, sign, value;

	AngleVectors (angles, forward, right, up);
	side = DotProduct (velocity, right);
	sign = side < 0 ? -1 : 1;
	side = fabs (side);

	value = cl_rollangle;

	if (side < cl_rollspeed)
		side = side * value / cl_rollspeed;
	else
		side = value;

	return side * sign;

}

//============================================================================

vec3_t	pmove_mins, pmove_maxs;

static void
AddLinksToPmove (areanode_t *node)
{
	edict_t    *check;
	pr_uint_t   pl, i;
	link_t     *l, *next;
	physent_t  *pe;

	pl = EDICT_TO_PROG (&sv_pr_state, sv_player);

	// touch linked edicts
	for (l = node->solid_edicts.next; l != &node->solid_edicts; l = next) {
		next = l->next;
		check = EDICT_FROM_AREA (l);

		if (SVentity (check, owner) == pl)
			continue;					// player's own missile
		if (SVfloat (check, solid) == SOLID_BSP
			|| SVfloat (check, solid) == SOLID_BBOX
			|| SVfloat (check, solid) == SOLID_SLIDEBOX) {
			if (check == sv_player)
				continue;

			for (i = 0; i < 3; i++)
				if (SVvector (check, absmin)[i] > pmove_maxs[i]
					|| SVvector (check, absmax)[i] < pmove_mins[i])
					break;
			if (i != 3)
				continue;
			if (pmove.numphysent == MAX_PHYSENTS)
				return;
			pe = &pmove.physents[pmove.numphysent];
			pmove.numphysent++;

			VectorCopy (SVvector (check, origin), pe->origin);
			VectorCopy (SVvector (check, angles), pe->angles);
			pe->info = NUM_FOR_EDICT (&sv_pr_state, check);

			if (sv_fields.rotated_bbox != -1
				&& SVint (check, rotated_bbox)) {
				int h = SVint (check, rotated_bbox) - 1;

				pe->hull = pf_hull_list[h]->hulls[1];
			} else {
				pe->hull = 0;
				if (SVfloat (check, solid) == SOLID_BSP) {
					pe->model = sv.models[(int) (SVfloat (check, modelindex))];
				} else {
					pe->model = NULL;
					VectorCopy (SVvector (check, mins), pe->mins);
					VectorCopy (SVvector (check, maxs), pe->maxs);
				}
			}
		}
	}

	// recurse down both sides
	if (node->axis == -1)
		return;

	if (pmove_maxs[node->axis] > node->dist)
		AddLinksToPmove (node->children[0]);

	if (pmove_mins[node->axis] < node->dist)
		AddLinksToPmove (node->children[1]);
}

byte        playertouch[(MAX_EDICTS + 7) / 8];

/*
	SV_PreRunCmd

	Done before running a player command.  Clears the touch array
*/
void
SV_PreRunCmd (void)
{
	memset (playertouch, 0, sizeof (playertouch));
}

static void
adjust_usecs (usercmd_t *ucmd)
{
	int         passed;

	if (host_client->last_check == -1.0)
		return;
	passed = (int) ((realtime - host_client->last_check) * 1000.0);
	host_client->msecs += passed - ucmd->msec;
	if (host_client->msecs >= 0) {
		host_client->msecs -= sv_timecheck_decay;
	} else {
		host_client->msecs += sv_timecheck_decay;
	}
	if (abs (host_client->msecs) > sv_timecheck_fuzz) {
		int         fuzz = sv_timecheck_fuzz;
		host_client->msecs = bound (-fuzz, host_client->msecs, fuzz);
		ucmd->msec = passed;
	}
	host_client->last_check = realtime;
}

static void
check_usecs (usercmd_t *ucmd)
{
	double      tmp_time;
	int         tmp_time1;

	host_client->msecs += ucmd->msec;
	if (host_client->spectator)
		return;
	if (host_client->last_check == -1.0)
		return;
	tmp_time = realtime - host_client->last_check;
	if (tmp_time < sv_timekick_interval)
		return;
	host_client->last_check = realtime;
	tmp_time1 = tmp_time * (1000 + sv_timekick_fuzz);
	if (host_client->msecs >= tmp_time1) {
		host_client->msec_cheating++;
		SV_BroadcastPrintf (PRINT_HIGH, "%s thinks there are %d ms "
							"in %d seconds (Strike %d/%d)\n",
							host_client->name, host_client->msecs,
							(int) tmp_time, host_client->msec_cheating,
							sv_timekick);
		if (host_client->msec_cheating >= sv_timekick) {
			SV_BroadcastPrintf (PRINT_HIGH, "Strike %d for %s!!\n",
								host_client->msec_cheating, host_client->name);
			SV_BroadcastPrintf (PRINT_HIGH, "Please see "
								"http://www.quakeforge.net/speed_cheat.php "
								"for information on QuakeForge's time cheat "
								"protection. That page explains how some may "
								"be cheating without knowing it.\n");
			SV_DropClient (host_client);
		}
	}
	host_client->msecs = 0;
}

void
SV_RunCmd (usercmd_t *ucmd, bool inside)
{
	int			oldmsec, i, n;
	edict_t    *ent;

	if (!inside) {
		if (sv_timecheck_mode) {
			adjust_usecs (ucmd);
		} else {
			check_usecs (ucmd);
		}
		if (host_client->last_check == -1.0)
			host_client->last_check = realtime;
	}

	cmd = *ucmd;

	// chop up very long commands
	if (cmd.msec > 50) {
		oldmsec = ucmd->msec;
		cmd.msec = oldmsec / 2;
		SV_RunCmd (&cmd, 1);
		cmd.msec = oldmsec / 2;
		cmd.impulse = 0;
		SV_RunCmd (&cmd, 1);
		return;
	}

	if (!SVfloat (sv_player, fixangle))
		VectorCopy (ucmd->angles, SVvector (sv_player, v_angle));

	SVfloat (sv_player, button0) = ucmd->buttons & 1;
// 1999-10-29 +USE fix by Maddes  start
	if (!nouse) {
		SVfloat (sv_player, button1) = (ucmd->buttons & 4) >> 2;
	}
// 1999-10-29 +USE fix by Maddes  end
	SVfloat (sv_player, button2) = (ucmd->buttons & 2) >> 1;
	if (ucmd->impulse)
		SVfloat (sv_player, impulse) = ucmd->impulse;
	if (host_client->cuff_time > realtime)
		SVfloat (sv_player, button0) = SVfloat (sv_player, impulse) = 0;

// angles
// show 1/3 the pitch angle and all the roll angle
	if (SVfloat (sv_player, health) > 0) {		//FIXME hardcoded mod info
		if (!SVfloat (sv_player, fixangle)) {
			SVvector (sv_player, angles)[PITCH] =
				-SVvector (sv_player, v_angle)[PITCH] / 3;
			SVvector (sv_player, angles)[YAW] =
				SVvector (sv_player, v_angle)[YAW];
		}
		SVvector (sv_player, angles)[ROLL] =
			SV_CalcRoll (SVvector (sv_player, angles),
						 SVvector (sv_player, velocity)) * 4;
	}

	sv_frametime = min (0.1, ucmd->msec * 0.001);

	if (!host_client->spectator) {
		*sv_globals.frametime = sv_frametime;
		*sv_globals.time = sv.time;
		*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, sv_player);
		PR_ExecuteProgram (&sv_pr_state, sv_funcs.PlayerPreThink);

		SV_RunThink (sv_player);
	}

	for (i = 0; i < 3; i++)
		pmove.origin[i] = SVvector (sv_player, origin)[i]
			+ (SVvector (sv_player, mins)[i] - player_mins[i]);
	VectorCopy (SVvector (sv_player, velocity), pmove.velocity);
	VectorCopy (SVvector (sv_player, v_angle), pmove.angles);

	pmove.flying = SVfloat (sv_player, movetype) == MOVETYPE_FLY;
	pmove.spectator = host_client->spectator;
	pmove.waterjumptime = SVfloat (sv_player, teleport_time);
	if (pmove.waterjumptime)
		pmove.waterjumptime -= sv.time;
	pmove.numphysent = 1;
	pmove.physents[0].model = sv.worldmodel;
	pmove.cmd = *ucmd;
	pmove.dead = SVfloat (sv_player, health) <= 0;
	pmove.oldbuttons = host_client->oldbuttons;
	pmove.oldonground = host_client->oldonground;

	movevars.entgravity = host_client->entgravity;
	movevars.maxspeed = host_client->maxspeed;

	for (i = 0; i < 3; i++) {
		pmove_mins[i] = pmove.origin[i] - 256;
		pmove_maxs[i] = pmove.origin[i] + 256;
	}

#if 0
	AddAllEntsToPmove ();
#else
	AddLinksToPmove (sv_areanodes);
#endif

#if 0
	{
		int		before, after;

		before = PM_TestPlayerPosition (pmove.origin);
		PlayerMove ();
		after = PM_TestPlayerPosition (pmove.origin);

		if (SVfloat (sv_player, health) > 0 && before && !after)
			SV_Printf ("player %s got stuck in playermove!!!!\n",
						host_client->name);
	}
#else
	PlayerMove ();
#endif

	host_client->oldbuttons = pmove.oldbuttons;
	host_client->oldonground = pmove.oldonground;
	if (pmove.waterjumptime > 0)
		pmove.waterjumptime += sv.time;
	else
		pmove.waterjumptime = 0;
	SVfloat (sv_player, teleport_time) = pmove.waterjumptime;
	SVfloat (sv_player, waterlevel) = waterlevel;
	SVfloat (sv_player, watertype) = watertype;
	if (onground != -1) {
		SVfloat (sv_player, flags) = (int) SVfloat (sv_player, flags) |
			FL_ONGROUND;
		SVentity (sv_player, groundentity) =
			EDICT_TO_PROG (&sv_pr_state,
						   EDICT_NUM (&sv_pr_state,
									  pmove.physents[onground].info));
	} else {
		SVfloat (sv_player, flags) =
			(int) SVfloat (sv_player, flags) & ~FL_ONGROUND;
	}
	for (i = 0; i < 3; i++)
		SVvector (sv_player, origin)[i] =
			pmove.origin[i] - (SVvector (sv_player, mins)[i] - player_mins[i]);

#if 0
	// truncate velocity the same way the net protocol will
	for (i = 0; i < 3; i++)
		SVvector (sv_player, velocity)[i] = (int) pmove.velocity[i];
#else
	VectorCopy (pmove.velocity, SVvector (sv_player, velocity));
#endif

	VectorCopy (pmove.angles, SVvector (sv_player, v_angle));

	if (!host_client->spectator) {
		// link into place and touch triggers
		SV_LinkEdict (sv_player, true);

		// touch other objects
		for (i = 0; i < pmove.numtouch; i++) {
			n = pmove.touchindex[i]->info;
			ent = EDICT_NUM (&sv_pr_state, n);
			if (!SVfunc (ent, touch) || (playertouch[n / 8] & (1 << (n % 8))))
				continue;
			sv_pr_touch (ent, sv_player);
			playertouch[n / 8] |= 1 << (n % 8);
		}
	}
}

/*
	SV_PostRunCmd

	Done after running a player command.
*/
void
SV_PostRunCmd (void)
{
	// run post-think

	if (!host_client->spectator) {
		*sv_globals.time = sv.time;
		*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, sv_player);
		PR_ExecuteProgram (&sv_pr_state, sv_funcs.PlayerPostThink);
		SV_RunNewmis ();
	} else if (sv_funcs.SpectatorThink) {
		*sv_globals.time = sv.time;
		*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, sv_player);
		PR_ExecuteProgram (&sv_pr_state, sv_funcs.SpectatorThink);
	}
}

/*
	SV_ExecuteClientMessage

	The current net_message is parsed for the given client
*/
void
SV_ExecuteClientMessage (client_t *cl)
{
	byte        checksum, calculatedChecksum;
	const char *s;
	client_frame_t *frame;
	int         checksumIndex, seq_hash, c;
	usercmd_t   oldest, oldcmd, newcmd;
	bool        move_issued = false;	// allow only one move command
	vec3_t      o;

	// make sure the reply sequence number matches the incoming
	// sequence number
	if (cl->netchan.incoming_sequence >= cl->netchan.outgoing_sequence)
		cl->netchan.outgoing_sequence = cl->netchan.incoming_sequence;
	else
		cl->send_message = false;		// don't reply, sequences have slipped

	// setup delta information
	cl->delta.cur_frame = cl->netchan.incoming_acknowledged & UPDATE_MASK;
	cl->delta.out_frame = cl->netchan.outgoing_sequence & UPDATE_MASK;
	cl->delta.in_frame = cl->netchan.incoming_sequence & UPDATE_MASK;

	// calc ping time
	frame = &cl->delta.frames[cl->delta.cur_frame];
	frame->ping_time = realtime - frame->senttime;

	cl->laggedents_count = 0;
	if (sv_antilag) {
		int         i;

		for (i = 0; i < MAX_CLIENTS; i++) {
			cl->laggedents[i].present = frame->playerpresent[i];
			if (cl->laggedents[i].present) {
				VectorCopy(frame->playerpositions[i],
						   cl->laggedents[i].laggedpos);
			}
		}
		cl->laggedents_count = MAX_CLIENTS;
		cl->laggedents_frac = sv_antilag_frac;
	}

	// save time for ping calculations
	cl->delta.frames[cl->delta.out_frame].senttime = realtime;
	cl->delta.frames[cl->delta.out_frame].ping_time = -1;

	host_client = cl;
	sv_player = host_client->edict;

//	seq_hash = (cl->netchan.incoming_sequence & 0xffff) ; // ^ QW_CHECK_HASH;
	seq_hash = cl->netchan.incoming_sequence;

	// mark time so clients will know how much to predict other players
	cl->localtime = sv.time;
	cl->delta.delta_sequence = -1;			// no delta unless requested
	while (1) {
		if (net_message->badread) {
			SV_Printf ("SV_ReadClientMessage: badread\n");
			SV_DropClient (cl);
			return;
		}

		c = MSG_ReadByte (net_message);
		if (c == -1)
			return;						// Ender: Patched :)

		switch (c) {
			default:
				SV_Printf ("SV_ReadClientMessage: unknown command char\n");
				SV_DropClient (cl);
				return;

			case clc_nop:
				break;

			case clc_delta:
				cl->delta.delta_sequence = MSG_ReadByte (net_message);
				break;

			case clc_move:
				if (move_issued)
					return;				// someone is trying to cheat...

				move_issued = true;

				checksumIndex = MSG_GetReadCount (net_message);
				checksum = (byte) MSG_ReadByte (net_message);

				// read loss percentage
				cl->lossage = MSG_ReadByte (net_message);

				MSG_ReadDeltaUsercmd (net_message, &nullcmd, &oldest);
				MSG_ReadDeltaUsercmd (net_message, &oldest, &oldcmd);
				MSG_ReadDeltaUsercmd (net_message, &oldcmd, &newcmd);

				if (cl->state != cs_spawned)
					break;

				// if the checksum fails, ignore the rest of the packet
				calculatedChecksum =
					COM_BlockSequenceCRCByte (net_message->message->data +
											  checksumIndex + 1,
											  MSG_GetReadCount (net_message) -
											  checksumIndex - 1, seq_hash);

				if (calculatedChecksum != checksum) {
					Sys_MaskPrintf (SYS_dev,
									"Failed command checksum for %s(%d) "
									"(%d != %d)\n",
									cl->name, cl->netchan.incoming_sequence,
									checksum, calculatedChecksum);
					return;
				}

				if (!sv.paused) {
					SV_PreRunCmd ();

					if (cl->netchan.net_drop < 20) {
						while (cl->netchan.net_drop > 2) {
							SV_RunCmd (&cl->lastcmd, 0);
							cl->netchan.net_drop--;
						}
						if (cl->netchan.net_drop > 1)
							SV_RunCmd (&oldest, 0);
						if (cl->netchan.net_drop > 0)
							SV_RunCmd (&oldcmd, 0);
					}
					SV_RunCmd (&newcmd, 0);

					SV_PostRunCmd ();
				}

				cl->lastcmd = newcmd;
				cl->lastcmd.buttons = 0;	// avoid multiple fires on lag
				break;

			case clc_stringcmd:
				s = MSG_ReadString (net_message);
				SV_ExecuteUserCommand (s);
				break;

			case clc_tmove:
				MSG_ReadCoordV (net_message, o);
				// allowed for only spectators
				if (host_client->spectator) {
					VectorCopy (o, SVvector (sv_player, origin));
					SV_LinkEdict (sv_player, false);
				}
				break;

			case clc_upload:
				SV_NextUpload ();
				break;
		}
	}
}

#define bi(x,np,params...) {#x, PF_##x, -1, np, {params}}
#define p(type) PR_PARAM(type)
#define P(a, s) { .size = (s), .alignment = BITOP_LOG2 (a), }
static builtin_t builtins[] = {
	bi(SV_AddUserCommand, 3, p(string), p(func), p(int)),
	{0}
};

static void
SV_MaxRate_f (void *data, const cvar_t *cvar)
{
	client_t   *cl;
	int         maxrate = sv_maxrate;
	int         i, rate = 2500;
	const char *val;

	Cvar_Info (data, cvar);
	for (i = 0, cl = svs.clients; i < MAX_CLIENTS; i++, cl++) {
		if (!cl->userinfo)
			continue;
		val = Info_ValueForKey (cl->userinfo, "rate");
		if (strlen (val)) {
			rate = atoi (val);

			if (maxrate) {
				rate = bound (500, rate, maxrate);
			} else {
				rate = max (500, rate);
			}
			cl->netchan.rate = 1.0 / rate;
		}
		SV_ClientPrintf (1, cl, PRINT_HIGH, "Net rate set to %i\n", rate);
	}
}

void
SV_UserInit (void)
{
	ucmd_table = Hash_NewTable (251, ucmds_getkey, ucmds_free, 0, 0);
	Hash_SetHashCompare (ucmd_table, ucmd_get_hash, ucmd_compare);
	PR_RegisterBuiltins (&sv_pr_state, builtins, 0);
	Cvar_Register (&cl_rollspeed_cvar, 0, 0);
	Cvar_Register (&cl_rollangle_cvar, 0, 0);

	Cvar_Register (&sv_antilag_cvar, Cvar_Info, &sv_antilag);
	Cvar_Register (&sv_antilag_frac_cvar, Cvar_Info, &sv_antilag_frac);

	Cvar_Register (&sv_allowfake_cvar, 0, 0);
	Cvar_Register (&sv_spectalk_cvar, 0, 0);
	Cvar_Register (&sv_mapcheck_cvar, 0, 0);
	Cvar_Register (&sv_timecheck_mode_cvar, 0, 0);
	Cvar_Register (&sv_timekick_cvar, Cvar_Info, &sv_timekick);
	Cvar_Register (&sv_timekick_fuzz_cvar, 0, 0);
	Cvar_Register (&sv_timekick_interval_cvar, 0, 0);
	Cvar_Register (&sv_timecheck_fuzz_cvar, 0, 0);
	Cvar_Register (&sv_timecheck_decay_cvar, 0, 0);
	Cvar_Register (&sv_kickfake_cvar, 0, 0);
	Cvar_Register (&sv_http_url_base_cvar, 0, 0);
	Cvar_Register (&sv_maxrate_cvar, SV_MaxRate_f, 0);
	Cvar_Register (&sv_maxspeed_cvar, Cvar_Info, &sv_maxspeed);
	Cvar_Register (&sv_spectatormaxspeed_cvar, 0, 0);
	Cvar_Register (&sv_accelerate_cvar, 0, 0);
	Cvar_Register (&sv_airaccelerate_cvar, 0, 0);
	Cvar_Register (&sv_wateraccelerate_cvar, 0, 0);
	Cvar_Register (&sv_waterfriction_cvar, 0, 0);
}

static void
OutofBandPrintf (netadr_t where, const char *fmt, ...)
{
	// 0 for davsprintf
	static const char header[] = {0xff, 0xff, 0xff, 0xff, A2C_PRINT, 0};
	static dstring_t *send = 0;
	va_list		argptr;
	int         len;

	if (!send)
		send = dstring_new ();
	send->size = sizeof (header);
	dstring_adjust (send);
	memcpy (send->str, header, sizeof (header));
	va_start (argptr, fmt);
	davsprintf (send, fmt, argptr);
	va_end (argptr);

	len = min (send->size, 1024);
	Netchan_SendPacket (len, send->str, where);
}