/*
Copyright (C) 2003-2006 Andrey Nazarov

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.

*/

//
// mvd_client.c
//

#include "server.h"
#include "mvd.h"
#include "version.h"

mvd_t			mvd;

// FIXME: these are used only for translating client_t into mvdClient_t
edict_t			mvd_edicts[MAX_CLIENTS + 1];

cmdbuf_t	mvd_buffer;
byte		mvd_buffer_text[MAX_STRING_CHARS];

cvar_t *mvd_buffer_size;
cvar_t *mvd_pause;

void MVD_DPrintf( const char *fmt, ... )
    __attribute__(( format( printf, 1, 2 ) ));

void MVD_DPrintf( const char *fmt, ... ) {
	va_list		argptr;
	char		text[MAXPRINTMSG];

	if( !mvd_debug->integer ) {
		return;
	}

	va_start( argptr, fmt );
	Q_vsnprintf( text, sizeof( text ), fmt, argptr );
	va_end( argptr );

	Com_Printf( S_COLOR_BLUE "%s", text );
}

void MVD_ClientCommand( const char *string ) {
    if( !mvd.netchan ) {
        return;
    }
    MSG_WriteByte( clc_stringcmd );
    MSG_WriteString( string );
    MSG_FlushTo( &mvd.netchan->message );
}

void MVD_ForwardToServer_f( void ) {
    if( mvd.state < MVD_CONNECTED ) {
        Com_Printf( "Can't \"%s\", not connected\n", Cmd_Argv( 0 ) );
        return;
    }

    if( mvd.demoplayback ) {
        return;
    }

    // don't forward the first argument
    if( Cmd_Argc() > 1 ) {
        MVD_ClientCommand( Cmd_RawArgs() );
    }
}

/* called initially at ClientBegin */
static void MVD_GameSetValidPos( mvdClient_t *client ) {
	mvdFrame_t *frame;
	player_state_t *ps;
	entityStateEx_t *ent;
	mvdGamestate_t *gs;
	int i, j;

	ps = &client->ps;
	memset( ps, 0, sizeof( *ps ) );

	if( client->admin ) {
		gs = &mvd.gamestates[mvd.gamestateSequence & GAMESTATE_MASK];
	} else {
		gs = &mvd.gamestates[mvd.activeGamestateSequence & GAMESTATE_MASK];
	}

	if( gs->spawnSet ) {
		VectorScale( gs->spawnOrigin, 8, ps->pmove.origin );
		VectorCopy( gs->spawnAngles, ps->viewangles );
	} else {
		frame = &mvd.frames[mvd.activePacketNum % mvd.frameBackup];
		for( i = 0; i < frame->numEntityStates; i++ ) {
			j = ( frame->firstEntityState + i ) % mvd.maxEntityStates;
			ent = &mvd.entityStates[j];
			if( ent->s.solid != 31 ) {
				VectorScale( ent->s.origin, 8, ps->pmove.origin );
				break;
			}
		}
		if( i == frame->numEntityStates ) {
			Com_WPrintf( "Don't know where to spawn %s\n", client->cl->name );
		}
	}

	ps->viewangles[ROLL] = 0;
	for( i = 0; i < 3; i++ ) {
		ps->pmove.delta_angles[i] = ANGLE2SHORT( ps->viewangles[i] ) -
			client->lastcmd.angles[i];
	}

	ps->fov = client->fov; 
	ps->pmove.pm_flags = client->pmflags;
	ps->pmove.pm_type = PM_SPECTATOR;

    client->clientNum = CLIENTNUM_NONE;
	client->savedClientNum = CLIENTNUM_NONE;
	client->followClientNum = CLIENTNUM_NONE;
	client->following = qfalse;
}

static void MVD_GameStartObserving( mvdClient_t *client ) {
	player_state_t *ps;
	mvdPlayer_t *oldplayer;
	int i, length;
	int savedLayouts;

	ps = &client->ps;
	ps->viewangles[ROLL] = 0;
	for( i = 0; i < 3; i++ ) {
		ps->pmove.delta_angles[i] = ANGLE2SHORT( ps->viewangles[i] ) -
			client->lastcmd.angles[i];
	}

	VectorClear( ps->kick_angles );

	ps->fov = client->fov; 
	ps->blend[0] = 0;
	ps->blend[1] = 0;
	ps->blend[2] = 0;
	ps->blend[3] = 0;
	ps->pmove.pm_flags = client->pmflags;
	ps->pmove.pm_type = PM_SPECTATOR;
	ps->rdflags = 0;
	ps->gunindex = 0;

	if( client->scoreboard <= SBOARD_FOLLOW ) {
		savedLayouts = 0;
	} else {
		savedLayouts = ps->stats[STAT_LAYOUTS];
	}
	for( i = 0; i < MAX_STATS; i++ ) {
		ps->stats[i] = 0;
	}
	ps->stats[STAT_HEALTH] = 100;
	ps->stats[STAT_LAYOUTS] = savedLayouts;

	if( client->following && client->followClientNum != CLIENTNUM_NONE ) {
		oldplayer = &mvd.players[client->followClientNum];
		for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
			if( oldplayer->configstrings[i] ) {
				MSG_WriteByte( svc_configstring );
				MSG_WriteShort( i );
				length = strlen( sv.configstrings[i] );
				if( length > MAX_QPATH ) {
					length = MAX_QPATH;
				}
				MSG_WriteData( sv.configstrings[i], length );
				MSG_WriteByte( 0 );
				SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR );
			}
		}
	}

    client->clientNum = CLIENTNUM_NONE;
	client->savedClientNum = client->followClientNum;
	client->followClientNum = CLIENTNUM_NONE;
	client->following = qfalse;
	client->savedFollowing = qfalse;

	SV_ClientPrintf( client->cl, PRINT_MEDIUM,
            "[MVD] Switched to freefloat mode.\n" );
}

void MVD_UpdateFollower( mvdClient_t *client, playerStateEx_t *src ) {
	player_state_t *ps = &client->ps;

	*ps = src->ps;
	if( client->cl->protocol == PROTOCOL_VERSION_Q2PRO ) {
		if( client->cl->settings[CLS_LOCALFOV] ) {
			ps->fov = client->fov;
		}
	} else if( mvd_custom_fov->integer ) {
		ps->fov = client->fov;
	}
	ps->pmove.pm_flags &= ~PMF_TELEPORT_BIT;
	ps->pmove.pm_flags |= client->pmflags | PMF_NO_PREDICTION;
	ps->pmove.pm_type = PM_FREEZE;
	if( !client->admin ) {
		if( client->scoreboard > SBOARD_FOLLOW ) {
			ps->stats[STAT_LAYOUTS] = 1;
		} else if( client->scoreboard == SBOARD_NONE ) {
			ps->stats[STAT_LAYOUTS] = 0;
		}
	}
    client->clientNum = src->clientNum;
}

static void MVD_UpdateLayoutClients( mvdClient_t *client ) {
	char layout[MAX_STRING_CHARS];
	char buffer[MAX_STRING_CHARS];
	char status[MAX_QPATH];
	static const char *header =
		"xl 32 yb -40 string2 \""APPLICATION" "VERSION"\" "
		"yb -32 string http://q2pro.sf.net "
		"xv 0 yv 0 string2 Name "
		"xv 152 string2 Ping "
		"xv 208 string2 Status ";
	int length, totalLength;
	client_t *cl;
	mvdClient_t *mvdcl;
    char name[MAX_CLIENT_NAME];
    char *s;
	int y;

	strcpy( layout, header );
	totalLength = strlen( layout );

	y = 8;
    FOR_EACH_CLIENT( cl ) {
		if( cl->state < cs_connected ) {
			continue;
		}
        if( cl->protocol == PROTOCOL_VERSION_MVD ) {
            continue;
        }
		if( cl->state == cs_spawned ) {
			mvdcl = ( mvdClient_t * )cl->edict->client;
			if( mvdcl->following ) {
				if( mvd.serverProtocol == PROTOCOL_VERSION_MVD ) {
                    s = sv.configstrings[CS_PLAYERSKINS + mvdcl->followClientNum];
                    Q_strncpyz( name, s, sizeof( name ) );
                    s = strchr( name, '\\' );
                    if( s ) {
                        *s = 0;
                    }
					Com_sprintf( status, sizeof( status ), "-> %s", name );
				} else {
					strcpy( status, "following" );
				}
			} else {
				strcpy( status, "observing" );
			}
		} else {
			strcpy( status, "connecting" );
		}
		length = Com_sprintf( buffer, sizeof( buffer ),
			"xv 0 yv %d string \"%.16s\" "
			"xv 152 string %d "
			"xv 208 string \"%s\" ",
			y, cl->name, cl->ping, status );
		if( totalLength + length > sizeof( layout ) - 1 ) {
			break;
		}
		strcpy( layout + totalLength, buffer );
		totalLength += length;
		y += 8;
	}

	MSG_WriteByte( svc_layout );
	MSG_WriteString( layout );
				
	SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR );

	client->ps.stats[STAT_LAYOUTS] = 1;
	client->layoutTime = sv.time + LAYOUT_MSEC;
}

static void MVD_UpdateLayoutScores( mvdClient_t *client ) {
	mvdPlayer_t *player;

	if( mvd.serverProtocol == PROTOCOL_VERSION_MVD ) {
		if( mvd.clientNum == CLIENTNUM_NONE ) {
			client->ps.stats[STAT_LAYOUTS] = 1;
			return;
		}
		player = &mvd.players[mvd.clientNum];
	} else {
		player = &mvd.players[0];
	}
	MSG_WriteByte( svc_layout );
	MSG_WriteString( player->layout );
	SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR );

	client->ps.stats[STAT_LAYOUTS] = 1;
}

static void MVD_SetDefaultLayout( mvdClient_t *client ) {
	/*if( mvd.serverProtocol == PROTOCOL_VERSION_MVD ) {
		client->scoreboard = SBOARD_FOLLOW;
		MVD_UpdateLayoutFollow( client );
	} else*/ {
		client->ps.stats[STAT_LAYOUTS] = 0;
		client->scoreboard = SBOARD_NONE;
	}
}

static void MVD_GameStartFollowing( mvdClient_t *client, int playerNum ) {
	mvdPlayer_t *player, *oldplayer;
	int i, j, length;
    char name[MAX_CLIENT_NAME];
    char *s;
	mvdFrame_t *frame;
	playerStateEx_t *ps = NULL;

	if( client->admin ) {
		frame = &mvd.frames[mvd.validPacketNum % mvd.frameBackup];
	} else {
		frame = &mvd.frames[mvd.activePacketNum % mvd.frameBackup];
	}

	if( !frame->numPlayerStates ) {
		SV_ClientPrintf( client->cl, PRINT_MEDIUM,
			"[MVD] No players to follow.\n" );
		if( client->following ) {
			MVD_GameStartObserving( client );
		}
		return;
	}

	// find the desired player in current frame
	for( i = 0; i < frame->numPlayerStates; i++ ) {
		j = ( frame->firstPlayerState + i ) % mvd.maxPlayerStates;
		ps = &mvd.playerStates[j];
		if( ps->number == playerNum ) {
			break;
		}
	}

	if( i == frame->numPlayerStates ) {
		// pick up the first player
		if( playerNum != CLIENTNUM_NONE ) {
			SV_ClientPrintf( client->cl, PRINT_MEDIUM,
				"[MVD] Player %d is not active.\n", playerNum );
		}
		j = frame->firstPlayerState % mvd.maxPlayerStates;
		ps = &mvd.playerStates[j];
		playerNum = ps->number;
	}

	if( client->following && playerNum == client->followClientNum ) {
		return;
	}

	player = &mvd.players[playerNum];
	oldplayer = NULL;
	if( client->following && client->followClientNum != CLIENTNUM_NONE ) {
		oldplayer = &mvd.players[client->followClientNum];
	}

	client->savedClientNum = client->followClientNum;
	client->followClientNum = playerNum;
	client->following = qtrue;

	client->pmflags ^= PMF_TELEPORT_BIT;
	MVD_UpdateFollower( client, ps );
    
	if( mvd.serverProtocol == PROTOCOL_VERSION_MVD ) {
        s = sv.configstrings[CS_PLAYERSKINS + playerNum];
        Q_strncpyz( name, s, sizeof( name ) );
        s = strchr( name, '\\' );
        if( s ) {
            *s = 0;
        }
	} else {
		strcpy( name, "the camera" );
	}

	SV_ClientPrintf( client->cl, PRINT_MEDIUM, "[MVD] Following %s.\n", name );

	// send delta configstrings
	for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
		if( player->configstrings[i] ) {
			MSG_WriteByte( svc_configstring );
			MSG_WriteShort( i );
			MSG_WriteString( player->configstrings[i] );
			SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR );
		} else if( oldplayer && oldplayer->configstrings[i] ) {
			MSG_WriteByte( svc_configstring );
			MSG_WriteShort( i );
			length = strlen( sv.configstrings[i] );
			if( length > MAX_QPATH ) {
				length = MAX_QPATH;
			}
			MSG_WriteData( sv.configstrings[i], length );
			MSG_WriteByte( 0 );
			SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR );
		}
	}

	// send layout
	if( client->scoreboard == SBOARD_FOLLOW ) {
		MSG_WriteByte( svc_layout );
		MSG_WriteString( player->layout );
		SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR );

		client->ps.stats[STAT_LAYOUTS] = ps->ps.stats[STAT_LAYOUTS] & 1;
	}
	
}

static void MVD_GameFollowCycle( mvdClient_t *client, int dir ) {
	mvdFrame_t *frame;
	playerStateEx_t *ps;
	int i, j;

	if( client->admin ) {
		frame = &mvd.frames[mvd.validPacketNum % mvd.frameBackup];
	} else {
		frame = &mvd.frames[mvd.activePacketNum % mvd.frameBackup];
	}

	if( !frame->numPlayerStates ) {
		return;
	}

	for( i = 0; i < frame->numPlayerStates; i++ ) {
		j = ( frame->firstPlayerState + i ) % mvd.maxPlayerStates;
		ps = &mvd.playerStates[j];
		if( ps->number == client->followClientNum ) {
			i += dir;
			break;
		}
	}

	i %= frame->numPlayerStates;
	j = ( frame->firstPlayerState + i ) % mvd.maxPlayerStates;
	ps = &mvd.playerStates[j];

	MVD_GameStartFollowing( client, ps->number );
}

static void MVD_SetActiveState( void ) {
	client_t *client;

	mvd.state = MVD_ACTIVE;

    FOR_EACH_CLIENT( client ) {
		if( client->state != cs_ready ) {
			continue;
		}
		Com_DPrintf( "Going from cs_ready to cs_spawned for %s\n",
                client->name );
		client->state = cs_spawned;
		client->sendTime = 0;
		client->surpressCount = 0;
		client->commandMsec = 1800;
		
		// call the game begin function
		sv_client = client;
		sv_player = client->edict;
		ge->ClientBegin( client->edict );
		sv_client = NULL;
		sv_player = NULL;
		
	}
}

typedef struct {
	byte *data;
	int mask;
	int offset;
	int last;
} mvdReadStruct_t;

int MVD_ReadByte( mvdReadStruct_t *msg ) {
	int	c, mask;
	byte *data;
	
	if( msg->offset + 1 > msg->last ) {
		c = -1;
	} else {
		data = msg->data;
		mask = msg->mask;
		c = ( unsigned char )data[msg->offset & mask];
	}
	msg->offset++;
	
	return c;
}

int MVD_ReadShort( mvdReadStruct_t *msg ) {
	int	c, mask, ofs;
	byte *data;
	
	if( msg->offset + 2 > msg->last ) {
		c = -1;
	} else {
		mask = msg->mask;
		data = msg->data;
		ofs = msg->offset;
		c = ( short )( data[ofs & mask]
		+ ( data[( ofs + 1 ) & mask] << 8 ) );
	}
	
	msg->offset += 2;
	
	return c;
}

int MVD_ReadLong( mvdReadStruct_t *msg ) {
	int	c, mask, ofs;
	byte *data;
	
	if( msg->offset + 4 > msg->last ) {
		c = -1;
	} else {
		mask = msg->mask;
		data = msg->data;
		ofs = msg->offset;
		c = data[ofs & mask]
		+ ( data[( ofs + 1 ) & mask] << 8 )
		+ ( data[( ofs + 2 ) & mask] << 16 )
		+ ( data[( ofs + 3 ) & mask] << 24 );
	}
	
	msg->offset += 4;
	
	return c;
}

char *MVD_ReadString( mvdReadStruct_t *msg ) {
	static char	string[MAX_NET_STRING];
	int		l,c;
	
	l = 0;
	do {
		c = MVD_ReadByte( msg );
		if( c == -1 || c == 0 ) {
			break;
		}
		if( c == 0xFF ) {
			c = '.';
		}
		string[l] = c;
		l++;
	} while( l < sizeof( string ) - 1 );
	
	string[l] = 0;
	
	return string;
}

void MVD_Multicast( qboolean unbuffered, vec3_t origin, multicast_t to ) {
	client_t	*client;
	byte		*mask;
	cleaf_t		*leaf;
	int			cluster;
	int			area1, area2;
	int			flags;
	vec3_t		org;
	mvdClient_t *mvdcl;
	player_state_t	*ps;
	mvdGamestate_t *gs;

	if( unbuffered ) {
		gs = &mvd.gamestates[mvd.gamestateSequence & GAMESTATE_MASK];
	} else {
		gs = &mvd.gamestates[mvd.activeGamestateSequence & GAMESTATE_MASK];
	}

	flags = 0;

	switch( to ) {
	case MULTICAST_ALL_R:
		flags |= MSG_RELIABLE;	// intentional fallthrough
	case MULTICAST_ALL:
		area1 = 0;
		cluster = 0;
		mask = NULL;
		break;

	case MULTICAST_PHS_R:
		flags |= MSG_RELIABLE;	// intentional fallthrough
	case MULTICAST_PHS:
		leaf = CM_PointLeaf( &gs->cm, origin );
		area1 = CM_LeafArea( leaf );
		cluster = CM_LeafCluster( leaf );
		mask = CM_ClusterPHS( &gs->cm, cluster );
		break;

	case MULTICAST_PVS_R:
		flags |= MSG_RELIABLE;	// intentional fallthrough
	case MULTICAST_PVS:
		leaf = CM_PointLeaf( &gs->cm, origin );
		area1 = CM_LeafArea( leaf );
		cluster = CM_LeafCluster( leaf );
		mask = CM_ClusterPVS( &gs->cm, cluster );
		break;

	default:
		mask = NULL;
		area1 = 0;
		Com_Error( ERR_DROP, "MVD_Multicast: bad to: %i", to );
	}

	// send the data to all relevent clients
    FOR_EACH_CLIENT( client ) {
		if( client->state < cs_connected ) {
			continue;
		}
        if( client->protocol == PROTOCOL_VERSION_MVD ) {
            continue;
        }
		mvdcl = ( mvdClient_t * )client->edict->client;
		if( mvdcl->admin != unbuffered ) {
			continue;
		}

		if( !( flags & MSG_RELIABLE ) ) {
			/* do not send unreliables to connecting clients */
			if( client->state != cs_spawned || client->download || client->nodata ) {
				continue; 
			}
		}

		if( mask ) {
			// find the client's PVS
			ps = &client->edict->client->ps;
			VectorMA( ps->viewoffset, 0.125f, ps->pmove.origin, org );
			leaf = CM_PointLeaf( &gs->cm, org );
			area2 = CM_LeafArea( leaf );
			if( !CM_AreasConnected( &gs->cm, area1, area2 ) )
				continue;
			cluster = CM_LeafCluster( leaf );
			if( !Q_IsBitSet( mask, cluster ) ) {
				continue;
			}
		}

		SV_ClientAddMessage( client, flags );
	}

	/* clear the buffer */
	SZ_Clear( &msg_write );

}

static void MVD_FrameParseMulticast( mvdReadStruct_t *msg ) {
	multicast_t to;
	int length;
    union {
        vec_t v[3];
        uint32 l[3];
    } origin;
	int i, c;

	length = MVD_ReadShort( msg );
	to = ( length >> 12 ) & 7;
	length &= 0xFFF;

	if( to > MULTICAST_PVS_R ) {
		Com_Error( ERR_DROP, "MVD_FrameParseMulticast: bad to" );
	}

	if( length < 1 ) {
		Com_Error( ERR_DROP, "MVD_FrameParseMulticast: bad length" );
	}

	if( to != MULTICAST_ALL && to != MULTICAST_ALL_R ) {
		origin.l[0] = MVD_ReadLong( msg );
		origin.l[1] = MVD_ReadLong( msg );
		origin.l[2] = MVD_ReadLong( msg );
	} else {
		VectorClear( origin.v );
	}

	if( msg->offset + length > msg->last ) {
		Com_Error( ERR_DROP,
                "MVD_FrameParseMulticast: read past end of message" );
	}

	/* copy it byte-to-byte */
	for( i = 0; i < length; i++ ) {
		c = MVD_ReadByte( msg );
		MSG_WriteByte( c );
	}

	MVD_Multicast( qfalse, origin.v, to );

}

static void MVD_FrameParseUnicast( mvdReadStruct_t *msg ) {
	int clientNum, length, last;
	int flags;
	client_t *client;
	mvdClient_t *gclient;
	mvdPlayer_t *player;
	int i, c, offset;
	char *s;
	qboolean gotLayout, wantLayout;

	flags = 0;
	clientNum = MVD_ReadByte( msg );
	if( clientNum & 0x80 ) {
		flags |= MSG_RELIABLE;
	}
	clientNum &= 0x7F;
	length = MVD_ReadShort( msg );
    
    if( mvd_debug->integer > 1 ) {
        Com_Printf( "Unicast to %d (%d bytes)\n", clientNum, length );
    }

	if( ( unsigned )clientNum >= mvd.maxPlayers ) {
		Com_Error( ERR_DROP, "MVD_FrameParseUnicast: bad clientNum" );
	}

	if( length < 1 ) {
		Com_Error( ERR_DROP, "MVD_FrameParseUnicast: bad length" );
	}

	last = msg->offset + length;
	if( last > msg->last ) {
		Com_Error( ERR_DROP, "MVD_FrameParseUnicast: read past end of message" );
	}

	player = &mvd.players[clientNum];

	gotLayout = qfalse;


	/* attempt to parse the datagram and find custom configstrings,
     * layouts, etc. give up if unknown command byte is encountered */
	while( 1 ) {
		if( msg->offset > last ) {
			Com_Error( ERR_DROP,
                    "MVD_FrameParseUnicast: read past end of message" );
		}

		if( msg->offset == last ) {
			break;
		}

		if( ( c = MVD_ReadByte( msg ) ) == -1 ) {
			break;
		}
        
        if( mvd_debug->integer > 1 ) {
            Com_Printf( "%s\n", MSG_ServerCommandString( c ) );
        }

		switch( c ) {
		case svc_layout:
			s = MVD_ReadString( msg );
			Q_strncpyz( player->layout, s, sizeof( player->layout ) );
			gotLayout = qtrue;
			break;
		case svc_configstring:
			i = MVD_ReadShort( msg );
			s = MVD_ReadString( msg );
			if( ( unsigned )i >= MAX_CONFIGSTRINGS ) {
				Com_Error( ERR_DROP,
                        "MVD_FrameParseUnicast: bad configstring index" );
			}
			if( player->configstrings[i] ) {
				Z_Free( player->configstrings[i] );
			}
			player->configstrings[i] = MVD_CopyString( s );
			MSG_WriteByte( svc_configstring );
			MSG_WriteShort( i );
			MSG_WriteString( s );
			break;
		case svc_print:
			i = MVD_ReadByte( msg );
			s = MVD_ReadString( msg );
			MSG_WriteByte( svc_print );
			MSG_WriteByte( i );
			MSG_WriteString( s );
			break;
		case svc_stufftext:
			s = MVD_ReadString( msg );
			MSG_WriteByte( svc_stufftext );
			MSG_WriteString( s );
			break;
		default:
			/* copy the rest byte-to-byte */
			MSG_WriteByte( c );
			for( offset = msg->offset; offset < last; offset++ ) {
				c = MVD_ReadByte( msg );
				MSG_WriteByte( c );
			}
			msg->offset = offset;
			goto breakOut;
		}
	}

breakOut:
	/* send to all relevant clients */
	wantLayout = qfalse;
    FOR_EACH_CLIENT( client ) {
		if( client->state < cs_spawned ) {
			continue;
		}
        if( client->protocol == PROTOCOL_VERSION_MVD ) {
            continue;
        }
		gclient = ( mvdClient_t * )client->edict->client;
		if( gclient->admin ) {
			continue;
		}
		if( gclient->scoreboard == SBOARD_SCORES ) {
			if( mvd.serverProtocol != PROTOCOL_VERSION_MVD ||
                    clientNum == mvd.clientNum )
            {
				wantLayout = qtrue;
			}
		}
		if( !gclient->following ) {
			if( mvd.serverProtocol == PROTOCOL_VERSION_MVD &&
                    clientNum == mvd.clientNum )
            {
				SV_ClientAddMessage( client, flags );
			}
			continue;
		}
		if( gclient->followClientNum == clientNum ) {
			SV_ClientAddMessage( client, flags );
			if( gclient->scoreboard == SBOARD_FOLLOW ) {
				wantLayout = qtrue;
			}
		}
	}

	SZ_Clear( &msg_write );

	if( !gotLayout || !wantLayout ) {
		return;
	}

	MSG_WriteByte( svc_layout );
	MSG_WriteString( player->layout );

    FOR_EACH_CLIENT( client ) {
		if( client->state < cs_spawned ) {
			continue;
		}
        if( client->protocol == PROTOCOL_VERSION_MVD ) {
            continue;
        }
		gclient = ( mvdClient_t * )client->edict->client;
		if( gclient->admin ) {
			continue;
		}
		if( gclient->scoreboard == SBOARD_SCORES ) {
			if( mvd.serverProtocol != PROTOCOL_VERSION_MVD ||
                    clientNum == mvd.clientNum )
            {
				SV_ClientAddMessage( client, flags );
				continue;
			}
		}
		if( gclient->followClientNum == clientNum ) {
			if( gclient->scoreboard == SBOARD_FOLLOW ) {
				SV_ClientAddMessage( client, flags );
			}
		}
	}

	SZ_Clear( &msg_write );
}

static void MVD_FrameParseConfigstring( mvdReadStruct_t *msg ) {
	int index, length;
	char *string;
	client_t *client;
	mvdClient_t *mvdcl;

	index = MVD_ReadShort( msg );
	string = MVD_ReadString( msg );

	if( ( unsigned )index >= MAX_CONFIGSTRINGS ) {
		Com_Error( ERR_DROP,
			"MVD_FrameParseConfigstring: bad index: %d\n", index );
	}

	length = strlen( string );

	if( sizeof( sv.configstrings[0] ) * index + length >
		sizeof( sv.configstrings ) - 1 )
	{
		Com_Error( ERR_DROP,
			"MVD_FrameParseConfigstring: oversize configstring: %d", index );
	}

	if( !strcmp( sv.configstrings[index], string ) ) {
		return;
	}

	strcpy( sv.configstrings[index], string );
	
	MSG_WriteByte( svc_configstring );
	MSG_WriteShort( index );
	MSG_WriteString( string );
	
    FOR_EACH_CLIENT( client ) {
		if( client->state < cs_connected ) {
			continue;
		}
        if( client->protocol == PROTOCOL_VERSION_MVD ) {
            continue;
        }
		mvdcl = ( mvdClient_t * )client->edict->client;
		if( mvdcl->admin ) {
			continue;
		}
		SV_ClientAddMessage( client, MSG_RELIABLE );
	}

	SZ_Clear( &msg_write );
}

static void MVD_FrameParseMessages( mvdReadStruct_t *msg ) {
	int cmd;

//
// parse the message
//
	while( 1 ) {
		if( msg->offset > msg->last ) {
			Com_Error( ERR_DROP,
                    "MVD_FrameParseMessages: read past end of message" );
		}

		if( ( cmd = MVD_ReadByte( msg ) ) == -1 ) {
			break;
		}

//		MVD_ShowSVC(cmd);

		switch( cmd ) {
		case svc_multicast:
			MVD_FrameParseMulticast( msg );
			break;
		case svc_unicast:
			MVD_FrameParseUnicast( msg );
			break;
		case svc_configstring:
			MVD_FrameParseConfigstring( msg );
			break;
		default:
			Com_Error( ERR_DROP,
                    "MVD_FrameParseMessages: illegible command: %d", cmd );
			break;
		}
	}
}

void MVD_TransitionGamestate( int sequence ) {
	mvdConfigstring_t *cs, *nextcs;
	mvdGamestate_t *gs;
	mvdPlayer_t *player, *last;
	client_t *client;
	mvdClient_t *mvdcl;
	int i, j;

	Com_Printf( "------- MVD_TransitionGamestate -------\n" );
	
	MVD_DPrintf( "Going from %d to %d (frame %d)\n", 
		mvd.activeGamestateSequence, sequence, mvd.activePacketNum );

	Cvar_Set( "timedemo", "0" );
	Cvar_Set( "sv_paused", "0" );

	if( sequence < mvd.activeGamestateSequence ) {
		Com_Error( ERR_DROP, "MVD_TransitionGamestate: sequence < active" );
	}

	/* free old gamestate */
	for( j = mvd.activeGamestateSequence; j < sequence; j++ ) {
		gs = &mvd.gamestates[j & GAMESTATE_MASK];

		for( i = 0; i < SV_BASELINES_CHUNKS; i++ ) {
			if( gs->baselines[i] ) {
				Z_Free( gs->baselines[i] );
			}
		}
		for( cs = gs->headCS; cs; cs = nextcs ) {
			nextcs = cs->next;
			Z_Free( cs );
		}
		CM_FreeMap( &gs->cm );
		memset( gs, 0, sizeof( *gs ) );
	}

	gs = &mvd.gamestates[sequence & GAMESTATE_MASK];
	mvd.activeGamestateSequence = sequence;

	/* reset player slots */
	last = mvd.players + mvd.maxPlayers;
	for( player = mvd.players; player != last; player++ ) {
		for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
			if( player->configstrings[i] ) {
				Z_Free( player->configstrings[i] );
			}
		}
		memset( player, 0, sizeof( *player ) );
	}

	MSG_WriteByte( svc_stufftext );
	MSG_WriteString( "changing\n" );

    FOR_EACH_CLIENT( client ) {
		if( client->state < cs_connected ) {
			continue;
		}
		if( client->protocol == PROTOCOL_VERSION_MVD ) {
			continue;
		}
		mvdcl = ( mvdClient_t * )client->edict->client;
		if( mvdcl->admin ) {
			continue;
		}

		memset( &mvdcl->lastcmd, 0, sizeof( mvdcl->lastcmd ) );

		client->state = cs_connected;
		client->lastframe = -1;
		client->sendTime = 0;
		client->surpressCount = 0;

		SV_ClientAddMessage( client, MSG_RELIABLE );
	}

	SZ_Clear( &msg_write );

    SV_SendAsyncPackets();

	memset( sv.configstrings, 0, sizeof( sv.configstrings ) );

	i = strlen( gs->mapname );
	if( i > 9 ) {
		Q_strncpyz( sv.name, gs->mapname + 5, sizeof( sv.name ) ); // skip "maps/"
		sv.name[i - 9] = 0; // cut off ".bsp"
	} else {
		Q_strncpyz( sv.name, gs->mapname, sizeof( sv.name ) );
	}

	Com_Printf( "Map         : %s\n", sv.name );
	Com_Printf( "Max players : %d\n", gs->maxclients );

	svs.spawncount = ( rand() | ( rand() << 16 ) ) ^ Sys_Realtime();
	svs.spawncount &= 0x7FFFFFFF;	// any partially connected client will be
									// restarted

	/* set up configstrings */
	for( cs = gs->headCS; cs; cs = cs->next ) {
		strcpy( sv.configstrings[cs->index], cs->string );
	}

	MSG_WriteByte( svc_stufftext );
	MSG_WriteString( "reconnect\n" );

    FOR_EACH_CLIENT( client ) {
		if( client->state < cs_connected ) {
			continue;
		}
		if( client->protocol == PROTOCOL_VERSION_MVD ) {
			continue;
		}
		mvdcl = ( mvdClient_t * )client->edict->client;
		if( mvdcl->admin ) {
			continue;
		}

		SV_ClientAddMessage( client, MSG_RELIABLE );
	}

	SZ_Clear( &msg_write );

	/* set serverinfo variable */
	Cvar_FullSet( "mapname", sv.name, CVAR_SERVERINFO|CVAR_NOSET, CVAR_SET_DIRECT );

	Cvar_SetInteger( "sv_running", ss_broadcast );
	Cvar_SetInteger( "sv_paused", 0 );

	sv.state = ss_broadcast;

	Com_Printf( "---------------------------------------\n" );
}

#define RESET_DELTA		16

static void MVD_DriftTime( void ) {
	int delta, drift;

	delta = mvd.serverPacketNum - mvd.activePacketNum + 1;
	drift = mvd_buffer_size->integer - delta;
	clamp( drift, -RESET_DELTA, RESET_DELTA );

	if( mvd_debug->integer > 2 ) {
		Com_Printf( "MVD_DriftTime: frame=%d delta=%d ",
                mvd.activePacketNum, delta );
		if( drift > 0 ) {
			Com_Printf( "[fast]\n" );
		} else if( drift < 0 ) {
			Com_Printf( "[slow]\n" );
		} else {
			Com_Printf( "\n" );
		}
	}

	sv.time += drift;
}

static void MVD_TransitionFrame( mvdFrame_t *frame ) {
	mvdReadStruct_t msg;
	mvdGamestate_t *gs;

	/* add unreliable datagram messages present in this frame */
	if( frame->numMessageBytes ) {
		msg.data = mvd.messageBytes;
		msg.mask = mvd.maxMessageBytes - 1;
		msg.offset = frame->firstMessageByte;
		msg.last = frame->firstMessageByte + frame->numMessageBytes;
		if( mvd_debug->integer > 1 ) {
			Com_Printf( "Adding %d bytes of unreliable messages\n",
					msg.last - msg.offset );
		}
		MVD_FrameParseMessages( &msg );
	}

	/* update areaportals */
	if( mvd.serverProtocol == PROTOCOL_VERSION_MVD ) {
		gs = &mvd.gamestates[mvd.activeGamestateSequence & GAMESTATE_MASK];
		CM_SetPortalStates( &gs->cm, frame->portalbytes, frame->numPortalBytes );
	}
}

/*
MVD delay buffer:
                              mvd_buffer_size
|          |<------------------------------------------------->|
|__________|__________|________________________________________|
|    16    |    16    |                                        |
|<-------->|<-------->|                                        |
A          B          C                                        D

If mvd.state == MVD_BUFFERING, do nothing until we are beyond point B.
If mvd.state == MVD_ACTIVE and we are between A and C, try to reach
point B by slowly drifting sv.time.
If we are ahead point C, restart buffering (with a 16 frames hysteresis).
If we are beyond point A, or we have run out of entityState or playerState
buffers, explicitly set mvd.activePacketNum at point B.

When playing back a demo, just run towards point D and do nothing else.
*/
static void MVD_SetNextFrame( void ) {
	mvdFrame_t		*frame;
	mvdReadStruct_t msg;
	int delta;

	if( mvd.state < MVD_BUFFERING ) {
		return;
	}

	delta = mvd.serverPacketNum - mvd.activePacketNum;
	if( !mvd.demoplayback ) {
		if( delta < mvd_buffer_size->integer - RESET_DELTA &&
                mvd.state == MVD_ACTIVE )
        {
			/* start buffering again */
			mvd.state = MVD_BUFFERING;
			MVD_DPrintf( "Restarted buffering at frame %d, delta is %d\n",
                    mvd.serverPacketNum, delta );
            return;
		}
		if( delta < mvd_buffer_size->integer && mvd.state == MVD_BUFFERING ) {
			MVD_DPrintf( "Still buffering at frame %d, delta is %d\n",
                    mvd.serverPacketNum, delta );
			return;
		}
	}

	if( delta > mvd.frameBackup - 1 ) {
		MVD_DPrintf( "MVD_SetNextFrame: frame %d is too old, delta is %d\n",
                mvd.activePacketNum, delta );
		mvd.activePacketNum = mvd.serverPacketNum - mvd_buffer_size->integer;
	}

	frame = NULL;
	while( mvd.activePacketNum < mvd.serverPacketNum ) {
        mvd.activePacketNum++;
		frame = &mvd.frames[mvd.activePacketNum % mvd.frameBackup];

		if( frame->serverPacketNum != mvd.activePacketNum ) {
			MVD_DPrintf( "MVD_SetNextFrame: %d: never received\n",
                    mvd.activePacketNum );
			continue;
		}

		/* got new gamestate? */
		if( frame->gamestateSequence != mvd.activeGamestateSequence ) {
			MVD_TransitionGamestate( frame->gamestateSequence );
		}

		/* does this frame contain svc_frame? */
		if( !( frame->flags & MFF_ACTIVE ) ) {
			continue;
		}

		if( !( frame->flags & MFF_VALID ) ) {
			MVD_DPrintf( "MVD_SetNextFrame: %d: invalid frame\n",
                    mvd.activePacketNum );
			continue;
		}

		if( mvd.nextEntityStates - frame->firstEntityState >
                mvd.maxEntityStates )
        {
			MVD_DPrintf( "MVD_SetNextFrame: %d: entityStates too old\n",
                    mvd.activePacketNum );
			continue;
		}
		if( mvd.nextPlayerStates - frame->firstPlayerState >
                mvd.maxPlayerStates )
        {
			MVD_DPrintf( "MVD_SetNextFrame: %d: playerStates too old\n",
                    mvd.activePacketNum );
			continue;
		}

		if( mvd.nextMessageBytes - frame->firstMessageByte >
                mvd.maxMessageBytes )
        {
			/* FIXME: use this frame anyway? */
			MVD_DPrintf( "MVD_SetNextFrame: %d: messages too old\n",
                    mvd.activePacketNum );
			continue;
		}

		if( !mvd.demoplayback ) {
			MVD_DriftTime();
		}

		/* advance server state to this frame */
        mvd.prevPacketNum = mvd.activePacketNum - 1;
		MVD_TransitionFrame( frame );

		/* getting a valid frame message ends the buffering process */
		if( mvd.state == MVD_BUFFERING ) {
			delta = mvd.serverPacketNum - mvd.activePacketNum;
			MVD_DPrintf( "Finished buffering at frame %d, delta is %d\n",
                    mvd.serverPacketNum, delta );
			MVD_SetActiveState();
		}

		break;
		
	}

	if( !frame ) {
		return;
	}

	/* add pending reliable datagram messages */
	msg.data = mvd.reliableMessageBytes;
	msg.mask = mvd.maxReliableMessageBytes - 1;
	msg.offset = mvd.lastReliableMessageBytes;
	msg.last = frame->firstReliableMessageByte + frame->numReliableMessageBytes;
	if( msg.last == msg.offset ) {
		return;
	}

	if( msg.last < msg.offset ) {
		if( mvd.demoplayback ) {
			/* this is normal if demo was just played backwards */
			return;
		}
		Com_Error( ERR_DROP,
                "MVD_SetNextFrame: reliable messages went backwards" );
	}

	if( mvd.nextReliableMessageBytes - mvd.lastReliableMessageBytes >
            mvd.maxReliableMessageBytes )
    {
		Com_Error( ERR_DROP, "MVD_SetNextFrame: dropped reliable messages" );
	}

	if( mvd_debug->integer > 1 ) {
		Com_Printf( "Adding %d bytes of reliable messages: %d --> %d\n",
			msg.last - msg.offset, msg.offset, msg.last );
	}
	MVD_FrameParseMessages( &msg );
	mvd.lastReliableMessageBytes = msg.last;
}

/* used for playing demo buffer backwards */
static void MVD_SetPrevFrame( void ) {
	mvdFrame_t		*frame;

	while( mvd.activePacketNum > 0 &&
            mvd.serverPacketNum - mvd.activePacketNum < mvd.frameBackup - 1 )
    {
		mvd.activePacketNum--;
		frame = &mvd.frames[mvd.activePacketNum % mvd.frameBackup];

		if( frame->serverPacketNum != mvd.activePacketNum ) {
			MVD_DPrintf( "MVD_SetPrevFrame: %d: never received\n",
                    mvd.activePacketNum );
			continue;
		}
		
		/* got new gamestate? */
		if( frame->gamestateSequence != mvd.activeGamestateSequence ) {
			MVD_DPrintf( "MVD_SetPrevFrame: different gamestateSequence\n" );
		    mvd.activePacketNum++;
			break;
		}

		/* does this frame contain svc_frame? */
		if( !( frame->flags & MFF_ACTIVE ) ) {
			continue;
		}

		if( !( frame->flags & MFF_VALID ) ) {
			MVD_DPrintf( "MVD_SetPrevFrame: %d: invalid frame\n",
                    mvd.activePacketNum );
			continue;
		}

		if( mvd.nextEntityStates - frame->firstEntityState >
                mvd.maxEntityStates )
        {
			MVD_DPrintf( "MVD_SetPrevFrame: %d: entityStates too old\n",
                    mvd.activePacketNum );
		    mvd.activePacketNum++;
			break;
		}
		if( mvd.nextPlayerStates - frame->firstPlayerState >
                mvd.maxPlayerStates )
        {
			MVD_DPrintf( "MVD_SetPrevFrame: %d: playerStates too old\n",
                    mvd.activePacketNum );
		    mvd.activePacketNum++;
			break;
		}

		if( mvd.nextMessageBytes - frame->firstMessageByte >
                mvd.maxMessageBytes )
        {
			MVD_DPrintf( "MVD_SetPrevFrame: %d: messages too old\n",
                    mvd.activePacketNum );
		    mvd.activePacketNum++;
			break;
		}

		/* advance server state to this frame */
        mvd.prevPacketNum = mvd.activePacketNum + 1;
		MVD_TransitionFrame( frame );
		break;
	}

}

void MVD_BackwardsDown_f( void ) {
	mvd.demobackwards = qtrue;
}

void MVD_BackwardsUp_f( void ) {
	mvd.demobackwards = qfalse;
}

static void MVD_GameInit( void ) {
	int i;

	Com_Printf( "----- MVD_GameInit -----\n" );

	mvd.clients = Z_TagMallocz( sizeof( mvdClient_t ) * sv_maxclients->integer,
            TAG_GAME );

	mvd.players = MVD_Mallocz( sizeof( mvdPlayer_t ) * mvd.maxPlayers );

	/* allocate delay buffers */
	Cvar_ClampInteger( mvd_buffer_size, RESET_DELTA, 1024 );

	mvd.frameBackup = mvd_buffer_size->integer + RESET_DELTA;
	Com_DPrintf( "Delay buffer of %d frames total\n", mvd.frameBackup );

	mvd.frames = MVD_Mallocz( sizeof( mvdFrame_t ) * mvd.frameBackup );
	mvd.maxEntityStates = MAX_EDICTS * mvd.frameBackup;
	mvd.maxPlayerStates = mvd.maxPlayers * mvd.frameBackup;

	mvd.entityStates = MVD_Mallocz( sizeof( entityStateEx_t ) *
            mvd.maxEntityStates );
	mvd.playerStates = MVD_Mallocz( sizeof( playerStateEx_t ) *
            mvd.maxPlayerStates );

	i = 256 * mvd.frameBackup; /* TODO */
	mvd.maxMessageBytes = Q_CeilPowerOfTwo( i );
	mvd.nextMessageBytes = 0;
	mvd.messageBytes = MVD_Mallocz( mvd.maxMessageBytes );

	i = 256 * mvd.maxPlayers * mvd.frameBackup; /* TODO */
	mvd.maxReliableMessageBytes = Q_CeilPowerOfTwo( i );
	mvd.nextReliableMessageBytes = 0;
	mvd.reliableMessageBytes = MVD_Mallocz( mvd.maxReliableMessageBytes );

	/* prepare client slots */
	for( i = 0; i < sv_maxclients->integer; i++ ) {
		mvd_edicts[i + 1].client = ( gclient_t * )&mvd.clients[i];
		mvd.clients[i].cl = &svs.clientpool[i];
	}
}

static void MVD_GameShutdown( void ) {
	Com_Printf( "----- MVD_GameShutdown -----\n" );

	mvd_ge.num_edicts = 0;
    MVD_Disconnect();
	
}

void MVD_GameSpawnEntities( char *mapname, char *entstring, char *spawnpoint ) {
}
static void MVD_GameWriteGame( char *filename, qboolean autosave ) {
}
static void MVD_GameReadGame( char *filename ) {
}
static void MVD_GameWriteLevel( char *filename ) {
}
static void MVD_GameReadLevel( char *filename ) {
}

static qboolean MVD_GameClientConnect( edict_t *ent, char *userinfo ) {
	mvdClient_t *client;

	client = ( mvdClient_t * )ent->client;

	if( client->cl->protocol != PROTOCOL_VERSION_MVD ) {
		SV_BroadcastPrintf( PRINT_HIGH, "[MVD] %s connected\n",
			Info_ValueForKey( userinfo, "name" ) );
	}
	return qtrue;
}

static void MVD_GameClientBegin( edict_t *ent ) {
	mvdClient_t *client;
	char *s;

	client = ( mvdClient_t * )ent->client;
	client->savedClientNum = CLIENTNUM_NONE;

	client->floodTime = 0;
	client->floodHead = 0;
	
	if( client->cl->protocol == PROTOCOL_VERSION_MVD ) {
		client->connected = qtrue;
		return;
	}

	if( !client->connected ) {
		SV_BroadcastPrintf( PRINT_HIGH, "[MVD] %s entered the server\n",
            client->cl->name );
		client->connected = qtrue;
	}

	if( mvd_motd->string[0] ) {
		s = Cmd_MacroExpandString( mvd_motd->string, qfalse );
		if( !s ) {
			Com_WPrintf( "Macro expansion of mvd_motd failed\n" );
			Cvar_Set( "mvd_motd", "" );
		} else {
			s = Q_UnescapeString( s );
			MSG_WriteByte( svc_centerprint );
			MSG_WriteString( s );
			SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR );
		}
	}

	MVD_GameSetValidPos( client );
    if( mvd.serverProtocol != PROTOCOL_VERSION_MVD ) {
    	MVD_GameStartFollowing( client, CLIENTNUM_NONE );
    }
	MVD_SetDefaultLayout( client );
}

static void MVD_GameClientUserinfoChanged( edict_t *ent, char *userinfo ) {
	mvdClient_t *client;
	float fov;

	client = ( mvdClient_t * )ent->client;
	fov = atof( Info_ValueForKey( userinfo, "fov" ) );
	if( fov < 1 ) {
		fov = 90;
	} else if( fov > 160 ) {
		fov = 160;
	}
	client->fov = fov;
	if( !client->following ) {
		client->ps.fov = fov;
	}
}

static void MVD_GameClientDisconnect( edict_t *ent ) {
	mvdClient_t *client;
	client_t *cl;

	client = ( mvdClient_t * )ent->client;
	cl = client->cl;
	memset( client, 0, sizeof( *client ) );
	client->cl = cl;

	if( client->cl->protocol != PROTOCOL_VERSION_MVD ) {
		SV_BroadcastPrintf( PRINT_HIGH, "[MVD] %s disconnected\n", cl->name );
	}
}

static void MVD_StartAdmin( mvdClient_t *client ) {
	int i, length;
	mvdFrame_t *src, *dst;
	playerStateEx_t *ps;

	src = &mvd.frames[mvd.activePacketNum % mvd.frameBackup];
	dst = &mvd.frames[mvd.validPacketNum % mvd.frameBackup];

	if( src->gamestateSequence != dst->gamestateSequence ) {
		MSG_WriteByte( svc_stufftext );
		MSG_WriteString( "changing; reconnect\n" );
		SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR );
		client->cl->state = cs_connected;
		client->admin = qtrue;
		return;
	}

	/* write delta configstrings */
	for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
		length = strlen( mvd.configstrings[i] );
		if( !length && sv.configstrings[i][0] ) {
			MSG_WriteByte( svc_configstring );
			MSG_WriteShort( i );
			MSG_WriteByte( 0 );
			continue;
		}
		if( length > MAX_QPATH ) {
			length = MAX_QPATH;
		}
		if( strncmp( sv.configstrings[i], mvd.configstrings[i], length ) ) {
			MSG_WriteByte( svc_configstring );
			MSG_WriteShort( i );
			MSG_WriteData( mvd.configstrings[i], length );
			MSG_WriteByte( 0 );
		}
	}

	ps = &mvd.playerStates[dst->firstPlayerState % mvd.maxPlayerStates];

#if 0
	client->delta_angles[0] = ANGLE2SHORT( ps->ps.viewangles[0] ) - ps->ps.pmove.delta_angles[0] - client->lastcmd.angles[0];
	client->delta_angles[1] = ANGLE2SHORT( ps->ps.viewangles[1] ) - ps->ps.pmove.delta_angles[1] - client->lastcmd.angles[1];
	client->delta_angles[2] = ANGLE2SHORT( ps->ps.viewangles[2] ) - ps->ps.pmove.delta_angles[2] - client->lastcmd.angles[2];
#endif

    client->admin = qtrue;
	client->pmflags ^= PMF_TELEPORT_BIT;
    if( mvd.serverProtocol < PROTOCOL_VERSION_MVD ) {
	//	client->scoreboard = SBOARD_SCORES;
	//	MVD_UpdateLayoutScores( client );
		MVD_GameStartFollowing( client, CLIENTNUM_NONE );
    }
	SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Granted admin status.\n" );
}

static void MVD_StopAdmin( mvdClient_t *client ) {
	int i, length;
	mvdFrame_t *src, *dst;

	src = &mvd.frames[mvd.validPacketNum % mvd.frameBackup];
	dst = &mvd.frames[mvd.activePacketNum % mvd.frameBackup];

	if( src->gamestateSequence != dst->gamestateSequence ) {
		MSG_WriteByte( svc_stufftext );
		MSG_WriteString( "changing; reconnect\n" );
		SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR );
		client->cl->state = cs_connected;
		client->admin = qfalse;
		return;
	}

	/* write delta configstrings */
	for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
		length = strlen( sv.configstrings[i] );
		if( !length && mvd.configstrings[i][0] ) {
			MSG_WriteByte( svc_configstring );
			MSG_WriteShort( i );
			MSG_WriteByte( 0 );
			continue;
		}
		if( length > MAX_QPATH ) {
			length = MAX_QPATH;
		}
		if( strncmp( mvd.configstrings[i], sv.configstrings[i], length ) ) {
			MSG_WriteByte( svc_configstring );
			MSG_WriteShort( i );
			MSG_WriteData( sv.configstrings[i], length );
			MSG_WriteByte( 0 );
		}
	}

    client->admin = qfalse;
	client->pmflags ^= PMF_TELEPORT_BIT;
	SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Lost admin status.\n" );
}

static void MVD_GameClientCommand( edict_t *ent ) {
	mvdClient_t *client;
	char *cmd;
	int clientNum, i;

	if( mvd.state < MVD_PRIMED ) {
		return;
	}

	client = ( mvdClient_t * )ent->client;
	cmd = Cmd_Argv( 0 );
	if( !Q_stricmp( cmd, "!mvdadmin" ) ) {
		if( mvd.demoplayback || client->cl->protocol == PROTOCOL_VERSION_MVD ) {
			return;
		}
        if( client->admin ) {
			MVD_StopAdmin( client );
            return;
        }
		if( !NET_IsLocalAddress( &client->cl->netchan->remote_address ) ) {
			if( Cmd_Argc() < 2 ) {
				SV_ClientPrintf( client->cl, PRINT_HIGH, "Usage: %s <password>\n",
						Cmd_Argv( 0 ) );
				return;
			}
			if( !mvd_admin_password->string[0]
					|| strcmp( mvd_admin_password->string, Cmd_Argv( 1 ) ) )
			{
				SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Invalid password.\n" );
				return;
			}
		}
		MVD_StartAdmin( client );
		return;
	}

    if( client->admin ) {
        MVD_ClientCommand( Cmd_RawString() );
        return;
    }
    
	if( !Q_stricmp( cmd, "say" ) || !Q_stricmp( cmd, "say_team" ) ) {
		if( client->floodTime > sv.time ) {
			SV_ClientPrintf( client->cl, PRINT_HIGH,
				"[MVD] You can't talk for %d more seconds.\n",
				( client->floodTime - sv.time ) / 1000 );
			return;
		}
		Cvar_ClampInteger( mvd_flood_msgs, 0, FLOOD_SAMPLES - 1 );
		i = client->floodHead - mvd_flood_msgs->integer - 1;
		if( i >= 0 ) {
			Cvar_ClampValue( mvd_flood_persecond, 0, 60 );
			if( sv.time - client->floodSamples[i & FLOOD_MASK] <
				mvd_flood_persecond->value * 1000 )
			{
				Cvar_ClampValue( mvd_flood_waitdelay, 0, 60 );
				SV_ClientPrintf( client->cl, PRINT_HIGH,
					"[MVD] You can't talk for %d seconds.\n",
					mvd_flood_waitdelay->integer );
				client->floodTime = sv.time + mvd_flood_waitdelay->value * 1000;
				return;
			}
		}
		SV_BroadcastPrintf( PRINT_CHAT, "[MVD] %s: %s\n", client->cl->name,
                Cmd_Args() );
		client->floodSamples[client->floodHead & FLOOD_MASK] = sv.time;
		client->floodHead++;
		return;
	}
	if( !Q_stricmp( cmd, "playernext" ) ) {
		MVD_GameFollowCycle( client, 1 );
		return;
	}
	if( !Q_stricmp( cmd, "playerprev" ) ) {
		MVD_GameFollowCycle( client, -1 );
		return;
	}
	if( !Q_stricmp( cmd, "playertoggle" ) ) {
		MVD_GameStartFollowing( client, client->savedClientNum );
		return;
	}
	if( !Q_stricmp( cmd, "follow" ) ) {
		if( Cmd_Argc() < 2 ) {
			if( !client->following ) {
				MVD_GameStartFollowing( client, client->savedClientNum );
			}
			return;
		}
		clientNum = atoi( Cmd_Argv( 1 ) );
		MVD_GameStartFollowing( client, clientNum );
		return;
	}
	if( !Q_stricmp( cmd, "observe" ) ) {
		if( client->following ) {
			MVD_GameStartObserving( client );
		}
		return;
	}
	if( !Q_stricmp( cmd, "inven" ) ) {
		if( client->scoreboard == SBOARD_CLIENTS ) {
			MVD_SetDefaultLayout( client );
		} else {
			client->scoreboard = SBOARD_CLIENTS;
			MVD_UpdateLayoutClients( client );
		}
		return;
	}
	if( !Q_stricmp( cmd, "help" ) ) {
		if( client->scoreboard == SBOARD_SCORES ) {
			MVD_SetDefaultLayout( client );
		} else {
			client->scoreboard = SBOARD_SCORES;
			MVD_UpdateLayoutScores( client );
		}
		return;
	}

	if( !Q_stricmp( cmd, "putaway" ) ) {
		MVD_SetDefaultLayout( client );
		return;
	}
	
	SV_ClientPrintf( client->cl, PRINT_LOW, "[MVD] unknown command '%s'\n", cmd );
}

trace_t MVD_Trace( vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end ) {
	return SV_Trace( start, mins, maxs, end, NULL, MASK_PLAYERSOLID );
}

static void MVD_GameClientThink( edict_t *ent, usercmd_t *cmd ) {
	mvdClient_t *client;
	pmove_t pm;
	qboolean modeChange, playerChange;

	client = ( mvdClient_t * )ent->client;
    if( client->admin && mvd.serverProtocol < PROTOCOL_VERSION_MVD ) {
       // VectorAdd( cmd->angles, client->delta_angles, mvd.cmd.angles );
		VectorCopy( cmd->angles, mvd.cmd.angles );
        mvd.cmd.forwardmove += cmd->forwardmove;
        mvd.cmd.sidemove += cmd->sidemove;
        mvd.cmd.upmove += cmd->upmove;
        mvd.cmd.buttons |= cmd->buttons;
        mvd.cmd.msec += cmd->msec;
	    client->lastcmd = *cmd;
        return;
    }
	modeChange = !( client->lastcmd.buttons & BUTTON_ATTACK ) && ( cmd->buttons & BUTTON_ATTACK );
	playerChange = !client->lastcmd.upmove && cmd->upmove;
	client->lastcmd = *cmd;

	if( modeChange ) {
		if( client->following ) {
			MVD_GameStartObserving( client );
		} else {
			MVD_GameStartFollowing( client, client->savedClientNum );
		}
	}

	if( client->following ) {
		if( playerChange ) {
			MVD_GameFollowCycle( client, 1 );
		}
		return;
	}
	
	memset( &pm, 0, sizeof( pm ) );
	pm.trace = MVD_Trace;
	pm.pointcontents = SV_PointContents;
	pm.s = client->ps.pmove;
	pm.cmd = *cmd;

	PF_Pmove( &pm );

	client->ps.pmove = pm.s;
	VectorCopy( pm.viewangles, client->ps.viewangles );
}

static void MVD_GameRunFrame( void ) {
	client_t *client;
	mvdClient_t *mvdcl;
	mvdFrame_t *frames[2], *frame;
	playerStateEx_t *ps;
	int i, j;

	if( mvd.demoplayback ) {
		/* run backwards, until we run out of buffers */
		if( mvd.demobackwards ) {
			MVD_SetPrevFrame();
			goto update;
		}

		/* if paused, do not run forward */
		if( mvd_pause->integer ) {
			goto update;
		}

		/* feed in next demo frame, if necessarry */
		while( mvd.demofile && mvd.activePacketNum >= mvd.serverPacketNum ) {
			if( !MVD_ParseNextMessage() ) {
				Com_Printf( "MVD finished, closing demofile.\n" );
				FS_FCloseFile( mvd.demofile );
				mvd.demofile = 0;
				if( mvd_nextserver->integer ) {
					SV_Nextserver();
				}
				return;
			}
		}
	}

	if( mvd.state < MVD_CONNECTED ) {
		return;
	}

	MVD_SetNextFrame();

update:
	frames[0] = &mvd.frames[mvd.activePacketNum % mvd.frameBackup];
	frames[1] = &mvd.frames[mvd.validPacketNum % mvd.frameBackup];

	/* update clients */
    FOR_EACH_CLIENT( client ) {
		if( client->state < cs_spawned ) {
			continue;
		}
        if( client->protocol == PROTOCOL_VERSION_MVD ) {
            continue;
        }
		mvdcl = ( mvdClient_t * )client->edict->client;
		if( mvdcl->scoreboard == SBOARD_CLIENTS && mvdcl->layoutTime < sv.time && !mvdcl->admin ) {
			MVD_UpdateLayoutClients( mvdcl );
		}
		if( !mvdcl->following ) {
			continue;
		}
		frame = frames[mvdcl->admin];
		for( i = 0; i < frame->numPlayerStates; i++ ) {
			j = ( frame->firstPlayerState + i ) % mvd.maxPlayerStates;
			ps = &mvd.playerStates[j];
			if( ps->number == mvdcl->followClientNum ) {
				MVD_UpdateFollower( mvdcl, ps );
				break;
			}
		}
		if( i == frame->numPlayerStates ) {
			/* player not present in current frame */
			MVD_GameStartObserving( mvdcl );
			mvdcl->savedFollowing = qtrue;
		}
		
	}
}

static void MVD_GameServerCommand( void ) {
}

game_export_t mvd_ge = {
	GAME_API_VERSION,

	MVD_GameInit,
	MVD_GameShutdown,

	MVD_GameSpawnEntities,

	MVD_GameWriteGame,
	MVD_GameReadGame,

	MVD_GameWriteLevel,
	MVD_GameReadLevel,

	MVD_GameClientConnect,
	MVD_GameClientBegin,
	MVD_GameClientUserinfoChanged,
	MVD_GameClientDisconnect,
	MVD_GameClientCommand,
	MVD_GameClientThink,

	MVD_GameRunFrame,

	MVD_GameServerCommand,

	( edict_t * )mvd_edicts,
	sizeof( mvd_edicts[0] ),
	MAX_CLIENTS + 1,
	MAX_CLIENTS + 1
};

void MVD_StreamedWrite( sizebuf_t *msgbuf, int bytesToSkip ) {
	int		len, swlen;

	// the first several bytes are just packet sequencing stuff
	len = msgbuf->cursize - bytesToSkip;
	if( len < 1 ) {
		return; // don't write bad messages
	}

	swlen = LittleLong( len );
	FS_Write( &swlen, 4, mvd.demofile );
	FS_Write( msgbuf->data + bytesToSkip, len, mvd.demofile );
}

void MVD_StreamedStop_f( void ) {
	int length;

	if( !mvd.demorecording ) {
		Com_Printf( "Not recording a streamed demo.\n" );
		return;
	}

	length = -1;
	FS_Write( &length, 4, mvd.demofile );
	FS_FCloseFile( mvd.demofile );

	mvd.demofile = 0;
	mvd.demorecording = qfalse;
	mvd.demowaiting = qfalse;

	Com_Printf( "Streamed demo recording completed.\n" );
}

void MVD_StreamedRecord_f( void ) {
	mvdGamestate_t *gs;
	entityStateEx_t *ent;
	char *string;
	int i, j, length;
	char buffer[MAX_QPATH];
	char *name;
	fileHandle_t demofile;
    qboolean compressed = qfalse;
    
	i = 1;
    string = Cmd_Argv( i );
	if( !strcmp( string, "-c" ) || !strcmp( string, "--compressed" ) ) {
		compressed = qtrue;
		i++;
	}

	if( i >= Cmd_Argc() ) {
		Com_Printf( "Usage: %s [-c|--compressed] [/]<filename>\n",
                Cmd_Argv( 0 ) );
		return;
	}

	if( mvd.demorecording ) {
		Com_Printf( "Already recording a streamed demo.\n" );
		return;
	}

	if( mvd.state < MVD_CONNECTED || mvd.demoplayback ) {
		Com_Printf( "You must be connected to record.\n" );
		return;
	}

	//
	// open the demo file
	//
	name = Cmd_Argv( i );
	if( name[0] == '/' ) {
		Q_strncpyz( buffer, name + 1, sizeof( buffer ) );
	} else {
		Com_sprintf( buffer, sizeof( buffer ), "demos/%s", name );
        switch( mvd.serverProtocol ) {
        case PROTOCOL_VERSION_MVD:
            string = ".mvd2";
            break;
        case PROTOCOL_VERSION_Q2PRO:
            string = ".dm_36";
            break;
        case PROTOCOL_VERSION_R1Q2:
            string = ".dm_35";
            break;
        default:
            string = ".dm2";
            break;
        }
		COM_DefaultExtension( buffer, string, sizeof( buffer ) );
	}
	if( compressed ) {
		Q_strcat( buffer, sizeof( buffer ), ".gz" );
	}

	FS_FOpenFile( buffer, &demofile, FS_MODE_WRITE );
	if( !demofile ) {
		Com_EPrintf( "Couldn't open %s for writing\n", buffer );
		return;
	}
	
	Com_Printf( "Recording streamed demo to %s\n", buffer );
	//Com_WPrintf( "Streamed demos are currently BROKEN!\n" );

	mvd.demofile = demofile;
	mvd.demorecording = qtrue;
	mvd.demowaiting = qtrue;

	gs = &mvd.gamestates[mvd.gamestateSequence & GAMESTATE_MASK];


	//
	// write out messages to hold the startup information
	//

	// send the serverdata
	MSG_WriteByte( svc_serverdata );
	MSG_WriteLong( mvd.serverProtocol );
	MSG_WriteLong( 0x10000 + mvd.servercount );
    MSG_WriteByte( ATR_DEMO );	/* demos are always attract loops */
	string = Cvar_VariableString( "gamedir" );
    MSG_WriteString( string );
    MSG_WriteShort( mvd.clientNum );
    MSG_WriteString( gs->fullname );

	// protocol-specific stuff
	switch( mvd.serverProtocol ) {
	case PROTOCOL_VERSION_R1Q2:
        MSG_WriteByte( 0 );		/* enhanced */
        MSG_WriteShort( PROTOCOL_VERSION_R1Q2_MINOR );
        MSG_WriteByte( 0 );		/* advanced deltas */
        MSG_WriteByte( 0 );     /* strafeHack */
		break;
	case PROTOCOL_VERSION_Q2PRO:
        MSG_WriteShort( PROTOCOL_VERSION_Q2PRO_MINOR );
        MSG_WriteByte( GT_DEATHMATCH );
        MSG_WriteByte( 0 );     /* strafeHack */
		MSG_WriteByte( 0 );		//atu QWMod
		break;
	case PROTOCOL_VERSION_MVD:
		MSG_WriteShort( PROTOCOL_VERSION_MVD_MINOR );
		break;
	default:
		break;
	}

    // configstrings
	for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
		string = mvd.configstrings[i];
		if( !string || !string[0] ) {
            continue;
        }

		length = strlen( string );
		if( length > MAX_QPATH ) {
			length = MAX_QPATH;
		}
			
		if( mvd.serverProtocol < PROTOCOL_VERSION_Q2PRO ) {
			if( msg_write.cursize + length + 4 > MAX_PACKETLEN ) {
				MVD_StreamedWrite( &msg_write, 0 );
				SZ_Clear( &msg_write );
			}
		}
        MSG_WriteByte( svc_configstring );
        MSG_WriteShort( i );
        MSG_WriteData( string, length );
		MSG_WriteByte( 0 );
	}

    // baselines
	for( i = 0; i < SV_BASELINES_CHUNKS; i++ ) {
		if( !gs->baselines[i] ) {
			continue;
		}
		for( j = 0; j < SV_BASELINES_PER_CHUNK; j++ ) {
			ent = &gs->baselines[i][j];
			if( !ent->s.number ) {
				continue;
			}
			if( mvd.serverProtocol < PROTOCOL_VERSION_Q2PRO ) {
				if( msg_write.cursize + 64 > MAX_PACKETLEN ) {
					MVD_StreamedWrite( &msg_write, 0 );
					SZ_Clear( &msg_write );
				}
			}
			MSG_WriteByte( svc_spawnbaseline );
			MSG_WriteDeltaEntity( NULL, &ent->s, MSG_ES_FORCE );
		}
	}

	MSG_WriteByte( svc_stufftext );
	MSG_WriteString( "precache\n" );

	MVD_StreamedWrite( &msg_write, 0 );
	SZ_Clear( &msg_write );
}

void MVD_Jump_f( void ) {
	int percent, delta, savedPacketNum;
	mvdFrame_t *frame;
	mvdClient_t *gclient;
	client_t *client;

	if( !mvd.demoplayback ) {
		Com_Printf( "Not playing back a MVD.\n" );
		return;
	}

	if( Cmd_Argc() < 2 ) {
		Com_Printf( "Usage: %s <percent>\n", Cmd_Argv( 0 ) );
		return;
	}

	percent = atoi( Cmd_Argv( 1 ) );
	if( percent < 0 || percent > 100 ) {
		Com_Printf( "Percent should be in the [0, 100] range\n" );
		return;
	}

    savedPacketNum = mvd.activePacketNum;

	delta = ( 100 - percent ) * ( mvd.frameBackup - 1 ) / 100;
	mvd.activePacketNum = mvd.serverPacketNum - delta - 1;
    if( mvd.activePacketNum < 0 ) {
        mvd.activePacketNum = 0;
    }

	frame = &mvd.frames[mvd.activePacketNum % mvd.frameBackup];

	while( mvd.activePacketNum < mvd.serverPacketNum ) {
		mvd.activePacketNum++;
		frame = &mvd.frames[mvd.activePacketNum % mvd.frameBackup];
        
        if( frame->serverPacketNum != mvd.activePacketNum ) {
			MVD_DPrintf( "MVD_Jump_f: %d: never received\n",
                    mvd.activePacketNum );
            continue;
        }
        
		/* got new gamestate? */
		if( frame->gamestateSequence != mvd.activeGamestateSequence ) {
			MVD_DPrintf( "MVD_Jump_f: %d: different gamestateSequence\n",
                    mvd.activePacketNum );
			continue;
		}

		/* does this frame contain svc_frame? */
		if( !( frame->flags & MFF_ACTIVE ) ) {
			continue;
		}

		if( !( frame->flags & MFF_VALID ) ) {
			MVD_DPrintf( "MVD_Jump_f: %d: invalid frame\n",
                    mvd.activePacketNum );
			continue;
		}

		if( mvd.nextEntityStates - frame->firstEntityState >
                mvd.maxEntityStates )
        {
			MVD_DPrintf( "MVD_Jump_f: %d: entityStates too old\n",
                    mvd.activePacketNum );
			continue;
		}
		if( mvd.nextPlayerStates - frame->firstPlayerState >
                mvd.maxPlayerStates )
        {
			MVD_DPrintf( "MVD_Jump_f: %d: playerStates too old\n",
                    mvd.activePacketNum );
			continue;
		}

		if( mvd.nextMessageBytes - frame->firstMessageByte >
                mvd.maxMessageBytes )
        {
			MVD_DPrintf( "MVD_Jump_f: %d: messages too old\n",
                    mvd.activePacketNum );
			continue;
		}

        break;
	}

    if( mvd.activePacketNum == savedPacketNum ) {
        return;
    }
    
    /* advance server state to this frame */
    mvd.prevPacketNum = mvd.activePacketNum;
    MVD_TransitionFrame( frame );
    
    /*for( i = 1; i < ge->num_edicts; i++, ent++ ) {
        ent = EDICT_NUM( i );
        ent->s.event = EV_OTHER_TELEPORT;
        if( !( ent->s.renderfx & RF_BEAM ) ) {
            VectorCopy( ent->s.origin, ent->s.old_origin );
        }
    }*/

    FOR_EACH_CLIENT( client ) {
		if( client->state < cs_spawned ) {
			continue;
		}
        if( client->protocol == PROTOCOL_VERSION_MVD ) {
            continue;
        }
		gclient = ( mvdClient_t * )client->edict->client;
		if( !gclient->following ) {
			continue;
		}
		gclient->pmflags ^= PMF_TELEPORT_BIT;
	}
}

#ifndef DEDICATED_ONLY
/* called by the client code */
qboolean MVD_GetDemoPercent( int *percent, int *bufferPercent ) {
	int delta;

	if( !mvd.demoplayback ) {
		return qfalse;
	}
	
	delta = mvd.serverPacketNum - mvd.activePacketNum;
	*bufferPercent = 100 - delta * 100 / ( mvd.frameBackup - 1 );
	*percent = mvd.demofilePercent;

	return qtrue;
}
#endif

const char *MVD_Play_g( const char *partial, int state ) {
	return Com_FileNameGeneratorByFilter( "demos", "*.mvd2;*.mvd2.gz", partial, qfalse, state );
}

void MVD_Play_f( void ) {
	char *name;
	char buffer[MAX_QPATH];
	fileHandle_t f;
	int length;

	if( Cmd_Argc() < 2 ) {
		Com_Printf( "Usage: %s [/]<filename>\n", Cmd_Argv( 0 ) );
		return;
	}

	name = Cmd_Argv( 1 );
	if( name[0] == '/' ) {
		Q_strncpyz( buffer, name + 1, sizeof( buffer ) );
	} else {
		Com_sprintf( buffer, sizeof( buffer ), "demos/%s", name );
		COM_DefaultExtension( buffer, ".mvd2", sizeof( buffer ) );
	}
	FS_FOpenFile( buffer, &f, FS_MODE_READ );
	if( !f ) {
		Com_Printf( "Couldn't open '%s'\n", buffer );
		return;
	}

	if( !MVD_ReadNextMessage( f ) ) {
		Com_Printf( "Couldn't read the first message from '%s'.\n"
			"Demo is possibly broken or truncated.\n", buffer );
		FS_FCloseFile( f );
		return;	
	}

	SV_Shutdown( "Server restarted\n", KILL_RESTART );

	mvd.demofile = f;
	mvd.demoplayback = qtrue;
	mvd.state = MVD_CONNECTED;

	mvd.serverPacketNum++;
	MVD_ParseMessage();

	do {
		if( !MVD_ParseNextMessage() ) {
			Com_WPrintf( "Premature end of MVD.\n" );
			SV_Nextserver();
			return;
		}
		MVD_SetNextFrame();
		if( mvd.state < MVD_CONNECTED ) {
			return;
		}
	} while( mvd.state < MVD_ACTIVE );

    if( dedicated->integer && !sv_nextserver->string[0] ) {
    	Cvar_Set( "nextserver", va( "mvdplay /%s", buffer ) );
    }

	length = FS_GetFileLengthNoCache( mvd.demofile );
	mvd.demofileFrameOffset = FS_Tell( mvd.demofile );
	mvd.demofileSize = length - mvd.demofileFrameOffset;
	strcpy( mvd.demopath, buffer );
}

static void MVD_ConnectionlessPacket( void ) {
    char	*c, *s;
	netchan_t *netchan;
    netchan_type_t type;
	int i, j, k;

    MSG_BeginReading();
    MSG_ReadLong();	// skip the -1

    s = MSG_ReadStringLine();

    Cmd_TokenizeString( s, qfalse );

    c = Cmd_Argv( 0 );

	Com_DPrintf( "[MVD] ClientPacket: %s: %s\n", NET_AdrToString( &net_from ), s );

    /* challenge from the server we are connecting to */
    if ( !strcmp( c, "challenge" ) ) {
		qboolean proto35 = qfalse, proto36 = qfalse, proto37 = qfalse;

        if ( mvd.state < MVD_CHALLENGING ) {
            Com_DPrintf( "[MVD] Challenge received while not connecting.  Ignored.\n" );
            return;
        }
        if ( !NET_IsEqualBaseAdr( &net_from, &mvd.serverAddress ) ) {
            Com_DPrintf( "[MVD] Challenge from different address.  Ignored.\n" );
            return;
        }
        if ( mvd.state > MVD_CHALLENGING ) {
            Com_DPrintf( "[MVD] Dup challenge received.  Ignored.\n" );
            return;
        }

		/* parse additional parameters */
        j = Cmd_Argc();
		for( i = 2; i < j; i++ ) {
			s = Cmd_Argv( i );
			if( !strncmp( s, "p=", 2 ) ) {
				s += 2;
				while( *s ) {
					k = atoi( s );
					if( k == PROTOCOL_VERSION_R1Q2 ) {
						proto35 = qtrue;
					} else if( k == PROTOCOL_VERSION_Q2PRO ) {
						proto36 = qtrue;
					} else if( k == PROTOCOL_VERSION_MVD ) {
						proto37 = qtrue;
					}
					s = strchr( s, ',' );
					if( s == NULL ) {
						break;
					}
					s++;
				}
			}
        }

		/* select the 'best' protocol available, unless told otherwise */
		if( mvd.serverProtocol == 0 ||
			( mvd.serverProtocol == PROTOCOL_VERSION_R1Q2 && !proto35 ) ||
			( mvd.serverProtocol == PROTOCOL_VERSION_Q2PRO && !proto36 ) ||
			( mvd.serverProtocol == PROTOCOL_VERSION_MVD && !proto37 ) )
		{
			if( proto37 ) {
				mvd.serverProtocol = PROTOCOL_VERSION_MVD;
			} else if( proto36 ) {
				mvd.serverProtocol = PROTOCOL_VERSION_Q2PRO;
			} else if( proto35 ) {
				mvd.serverProtocol = PROTOCOL_VERSION_R1Q2;
			} else {
				mvd.serverProtocol = PROTOCOL_VERSION_DEFAULT;
			}
		}
		Com_DPrintf( "[MVD] Selected protocol %d\n", mvd.serverProtocol );

        mvd.challenge = atoi( Cmd_Argv( 1 ) );
        mvd.state = MVD_CONNECTING;
        mvd.connectTime = -9999;
        mvd.connectCount = 0;
		return;
	}

	if ( !strcmp( c, "client_connect" ) ) {
        if ( mvd.state < MVD_CONNECTING ) {
            Com_DPrintf( "[MVD] Connect received while not connecting.  Ignored.\n" );
            return;
        }
        if ( !NET_IsEqualBaseAdr( &net_from, &mvd.serverAddress ) ) {
            Com_DPrintf( "[MVD] Connect from different address.  Ignored.\n" );
            return;
        }
        if ( mvd.state > MVD_CONNECTING ) {
            Com_DPrintf( "[MVD] Dup connect received.  Ignored.\n" );
            return;
        }

        if( mvd.serverProtocol < PROTOCOL_VERSION_Q2PRO ) {
            type = NETCHAN_OLD;
        } else {
            type = NETCHAN_NEW;
        }

		/* parse additional parameters */
        j = Cmd_Argc();
		for( i = 1; i < j; i++ ) {
			s = Cmd_Argv( i );
			if( !strncmp( s, "nc=", 3 ) ) {
				s += 3;
				if( *s ) {
                    type = atoi( s );
                    if( type != NETCHAN_OLD && type != NETCHAN_NEW ) {
			            Com_Error( ERR_DISCONNECT,
                            "[MVD] Server returned invalid netchan type" );
                    }
				}
			}
        }

		netchan = Netchan_Setup( NS_CLIENT, type, &mvd.serverAddress,
                mvd.quakePort, 1024, mvd.serverProtocol );

		Com_Printf( "[MVD] Connection to %s established (protocol %d).\n",
			NET_AdrToString( &netchan->remote_address ), mvd.serverProtocol );

		mvd.state = MVD_CONNECTED;
		mvd.netchan = netchan;
		MVD_ClientCommand( "new" );
		return;
	}

	if ( !strcmp( c, "print" ) ) {
		if ( ( mvd.state == MVD_CHALLENGING || mvd.state == MVD_CONNECTING ) &&
            NET_IsEqualBaseAdr( &net_from, &mvd.serverAddress ) )
		{
			s = MSG_ReadString();
			Com_Error( ERR_DISCONNECT, "[MVD] %s", s );
		}
		return;
	}
}

void MVD_PacketEvent( int ret ) {
	int bytesToSkip;

	if( !mvd_running->integer ) {
		return;
	}

    //
    // remote command packet
    //
    if ( ret == 1 && *( int * )msg_read.data == -1 ) {
        MVD_ConnectionlessPacket();
        return;
    }

	if ( mvd.state < MVD_CONNECTED )
        return;		// dump it if not connected

    if ( ret == 1 && msg_read.cursize < 8 ) {
        Com_DPrintf( "[MVD] %s: runt packet\n", NET_AdrToString( &net_from ) );
        return;
    }

	if ( !mvd.netchan )
        return;

    //
    // packet from server
    //
	if ( !NET_IsEqualAdr( &net_from, &mvd.netchan->remote_address ) ) {
        Com_DPrintf( "[MVD] %s: sequenced packet without connection\n",
                NET_AdrToString( &net_from ) );
        return;
    }

	if( ret == -1 ) {
		Com_Error( ERR_DISCONNECT, "[MVD] Connection reset by peer" );
	}

    if ( !mvd.netchan->Process( mvd.netchan ) )
        return;		// wasn't accepted for some reason

	bytesToSkip = msg_read.readcount;

	mvd.serverPacketNum = mvd.netchan->incoming_sequence;
    MVD_ParseMessage();

	if( mvd.demorecording && !mvd.demowaiting ) {
        MVD_StreamedWrite( &msg_read, bytesToSkip );
	}
}

void MVD_CheckForResend( void ) {
    char tail[32];
	int ret;

	if( mvd.realtime - mvd.connectTime < 3000 ) {
		return;
	}
	mvd.connectTime = mvd.realtime;
	mvd.connectCount++;

	if( mvd.state < MVD_CONNECTING ) {
        Com_Printf( "[MVD] Requesting challenge... %i\n", mvd.connectCount );
        ret = Netchan_OutOfBandPrint( NS_CLIENT, &mvd.serverAddress,
                "getchallenge\n" );
		if( ret == -1 ) {
			Com_Error( ERR_DISCONNECT, "[MVD] %s to %s\n", Sys_NetErrorString(),
                    NET_AdrToString( &mvd.serverAddress ) );
		}
        return;
	}

    Com_Printf( "[MVD] Requesting connection... %i\n", mvd.connectCount );

    mvd.quakePort = net_qport->integer;
	if( mvd.serverProtocol != PROTOCOL_VERSION_DEFAULT ) {
        Com_sprintf( tail, sizeof( tail ), " %d",
                net_maxmsglen->integer );
        if( mvd.serverProtocol >= PROTOCOL_VERSION_Q2PRO ) {
            strcat( tail, net_chantype->integer ? " 1" : " 0" );
        }
        mvd.quakePort &= 0xFF;
	} else {
        tail[0] = 0;
	}

    ret = Netchan_OutOfBandPrint( NS_CLIENT, &mvd.serverAddress,
		"connect %i %i %i \"%s\"%s\n", mvd.serverProtocol, mvd.quakePort,
            mvd.challenge, Cvar_Userinfo(), tail );
	if( ret == -1 ) {
		Com_Error( ERR_DISCONNECT, "[MVD] %s to %s\n", Sys_NetErrorString(),
                NET_AdrToString( &mvd.serverAddress ) );
	}
}

void MVD_ClientFrame( int msec ) {
	int checksumIndex;

	if( !mvd.state ) {
		return;
	}

	Cbuf_ExecuteEx( &mvd_buffer );

	mvd.realtime += msec;

	if( mvd.state < MVD_CONNECTED ) {
		MVD_CheckForResend();
		return;
	}

	if( !mvd.netchan ) {
		return;
	}

    //
    // check timeout
    //
    if( Sys_Milliseconds() - mvd.netchan->last_received > mvd_timeout->value * 1000 ) {
        // timeoutcount saves debugger
        if ( ++mvd.timeoutcount > 5 ) {
            Com_Error( ERR_DISCONNECT, "[MVD] Server connection timed out." );
        }
    } else {
        mvd.timeoutcount = 0;
    }

	if( cvar_infoModified & CVAR_USERINFO ) {
		MSG_WriteByte( clc_userinfo );
		MSG_WriteString( Cvar_Userinfo() );
		MSG_FlushTo( &mvd.netchan->message );
		Com_DPrintf( "[MVD] Sending userinfo update\n" );
	}

	if( mvd.state < MVD_BUFFERING ) {
		if( mvd.netchan->ShouldUpdate( mvd.netchan ) ) {
			mvd.netchan->Transmit( mvd.netchan, 0, NULL );
		}
		return;
	}

	if( mvd.realtime - mvd.sendtime < 33 &&
		!mvd.netchan->ShouldUpdate( mvd.netchan ) )
	{
		return;
	}

    if( !mvd.cmd.msec ) {
        mvd.cmd.msec = mvd.realtime - mvd.sendtime;
    }
    if( mvd.cmd.msec > 250 ) {
        mvd.cmd.msec = 100;
    }
	
    MSG_WriteByte( clc_move );

    // save the position for a checksum byte
    checksumIndex = 0;
    if( mvd.serverProtocol == PROTOCOL_VERSION_DEFAULT ) {
        checksumIndex = msg_write.cursize;
        SZ_GetSpace( &msg_write, 1 );
    }
    if( mvd.validPacketNum != mvd.serverPacketNum || mvd.demowaiting ) {
        MSG_WriteLong( -1 );
    } else {
        MSG_WriteLong( mvd.lastServerFrame );
    }

    MSG_WriteDeltaUsercmd( NULL, &mvd.cmd );
    MSG_WriteByte( 0 );

    MSG_WriteByte( 0 );
    MSG_WriteByte( mvd.cmd.msec );
    MSG_WriteByte( 0 );

    MSG_WriteByte( 0 );
    MSG_WriteByte( mvd.cmd.msec );
    MSG_WriteByte( 0 );

    if( mvd.serverProtocol == PROTOCOL_VERSION_DEFAULT ) {
        // calculate a checksum over the move commands
        msg_write.data[checksumIndex] = COM_BlockSequenceCRCByte(
            msg_write.data + checksumIndex + 1,
            msg_write.cursize - checksumIndex - 1,
            mvd.netchan->outgoing_sequence );
    }
        
    mvd.cmd.forwardmove = 0;
    mvd.cmd.sidemove = 0;
    mvd.cmd.upmove = 0;
    mvd.cmd.buttons = 0;
    mvd.cmd.msec = 0;

	mvd.sendtime = mvd.realtime;

	mvd.netchan->Transmit( mvd.netchan, msg_write.cursize, msg_write.data );

	//Com_Printf( "Sent packet\n" );

	SZ_Clear( &msg_write );

}

void MVD_Connect_f( void ) {
	netadr_t adr;
	char *s;
    int protocol;

    if ( Cmd_Argc() < 2 ) {
usage:
        Com_Printf( "Usage: %s <server> [protocol]\n"
					"Supported protocols: %d, %d, %d and %d\n",
            Cmd_Argv( 0 ),
			PROTOCOL_VERSION_DEFAULT,
			PROTOCOL_VERSION_R1Q2,
			PROTOCOL_VERSION_Q2PRO,
            PROTOCOL_VERSION_MVD );
        return;
    }

	protocol = 0;
	if( Cmd_Argc() > 2 ) {
		protocol = atoi( Cmd_Argv( 2 ) );
		if( protocol < PROTOCOL_VERSION_DEFAULT ||
			protocol > PROTOCOL_VERSION_MVD )
		{
			goto usage;
		}
	}

	s = Cmd_Argv( 1 );
	if( !NET_StringToAdr( s, &adr ) ) {
		Com_Printf( "Bad server address: %s\n", s );
		return;
	}
	if( !adr.port ) {
		adr.port = BigShort( PORT_SERVER );
	}

	SV_Shutdown( "Server restarted\n", KILL_RESTART );

	NET_Config( NET_CLIENT );

	Cvar_SetInteger( "mvd_running", 1 );

	mvd.serverAddress = adr;
    mvd.serverProtocol = protocol;
	mvd.connectCount = 0;
	mvd.connectTime = -9999;
	mvd.state = MVD_CHALLENGING;
}

void MVD_Disconnect_f( void ) {
	if( mvd.state ) {
		Com_Error( ERR_SILENT, "[MVD] Disconnected from server.\n" );
	}
	Com_Printf( "Not connected to a MVD server.\n" );
}

void MVD_Disconnect( void ) {
	int i;
	mvdGamestate_t *gs;

	if( mvd.demorecording ) {
		MVD_StreamedStop_f();
	}

	if( mvd.netchan ) {
        MSG_WriteByte( clc_stringcmd );
        MSG_WriteString( "disconnect" );

        mvd.netchan->Transmit( mvd.netchan, msg_write.cursize, msg_write.data );
        mvd.netchan->Transmit( mvd.netchan, msg_write.cursize, msg_write.data );
        mvd.netchan->Transmit( mvd.netchan, msg_write.cursize, msg_write.data );

        SZ_Clear( &msg_write );

		Netchan_Close( mvd.netchan );
	}

	if( mvd.clients ) {
		Z_Free( mvd.clients );
	}

	Z_FreeTags( TAG_MVD );
	if( mvd.demofile ) {
		FS_FCloseFile( mvd.demofile );
	}

	for( i = 0; i < MAX_GAMESTATES; i++ ) {
		gs = &mvd.gamestates[i];
		if( gs->cm.cache ) {
			CM_FreeMap( &gs->cm );
		}
	}

	if( mvd.state ) {
		memset( &mvd, 0, sizeof( mvd ) );
	}

	Cvar_SetInteger( "mvd_running", 0 );
}



