summaryrefslogtreecommitdiff
path: root/source/mvd_game.c
diff options
context:
space:
mode:
Diffstat (limited to 'source/mvd_game.c')
-rw-r--r--source/mvd_game.c949
1 files changed, 949 insertions, 0 deletions
diff --git a/source/mvd_game.c b/source/mvd_game.c
new file mode 100644
index 0000000..847b537
--- /dev/null
+++ b/source/mvd_game.c
@@ -0,0 +1,949 @@
+/*
+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_game.c
+//
+
+#include "sv_local.h"
+#include "mvd_local.h"
+#include <setjmp.h>
+
+#define FOR_EACH_PLAYER( mvd, ps, frame ) \
+ for( i = 0, ps = &(mvd)->playerStates[(frame)->firstPlayer & MVD_PLAYERS_MASK]; \
+ i < (frame)->numPlayers; \
+ i++, ps = &(mvd)->playerStates[((frame)->firstPlayer + i ) & MVD_PLAYERS_MASK] )
+
+static cvar_t *mvd_motd;
+static cvar_t *mvd_admin_password;
+static cvar_t *mvd_flood_msgs;
+static cvar_t *mvd_flood_persecond;
+static cvar_t *mvd_flood_waitdelay;
+static cvar_t *mvd_flood_mute;
+static cvar_t *mvd_custom_fov;
+
+udpClient_t *mvd_clients;
+
+extern jmp_buf mvd_jmpbuf;
+
+/* called initially at ClientBegin */
+static void MVD_SetValidPos( udpClient_t *client ) {
+ player_state_t *ps;
+ entityStateEx_t *ent;
+ mvd_t *mvd = client->mvd;
+ mvdFrame_t *frame = &mvd->frames[mvd->framenum & MVD_UPDATE_MASK];
+ int i, j;
+
+ ps = &client->ps;
+ memset( ps, 0, sizeof( *ps ) );
+
+ if( mvd->spawnSet ) {
+ VectorScale( mvd->spawnOrigin, 8, ps->pmove.origin );
+ VectorCopy( mvd->spawnAngles, ps->viewangles );
+ } else {
+ for( i = 0; i < frame->numEntities; i++ ) {
+ j = ( frame->firstEntity + i ) & MVD_ENTITIES_MASK;
+ ent = &mvd->entityStates[j];
+ if( ent->s.solid != 31 ) {
+ VectorScale( ent->s.origin, 8, ps->pmove.origin );
+ break;
+ }
+ }
+ if( i == frame->numEntities ) {
+ 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 = mvd == &mvd_waitingRoom ? PM_FREEZE : PM_SPECTATOR;
+
+ client->clientNum = CLIENTNUM_NONE;
+ client->savedClientNum = CLIENTNUM_NONE;
+ client->followClientNum = CLIENTNUM_NONE;
+ client->following = qfalse;
+}
+
+static void MVD_StartObserving( udpClient_t *client ) {
+ player_state_t *ps;
+ player_state_t *state;
+ mvdPlayer_t *player;
+ int i;
+ int savedLayouts;
+ mvdConfigstring_t *cs;
+ mvd_t *mvd = client->mvd;
+ mvdFrame_t *frame = &mvd->frames[mvd->framenum & MVD_UPDATE_MASK];
+
+ 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 = mvd == &mvd_waitingRoom ? PM_FREEZE : 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;
+
+ FOR_EACH_PLAYER( mvd, state, frame ) {
+ if( PPS_NUM( state ) == mvd->clientNum ) {
+ for( i = 0; i < MAX_STATS; i++ ) {
+ ps->stats[i] = state->stats[i];
+ }
+ break;
+ }
+ }
+
+ player = &mvd->players[mvd->clientNum];
+ for( cs = player->configstrings; cs; cs = cs->next ) {
+ MSG_WriteByte( svc_configstring );
+ MSG_WriteShort( cs->index );
+ MSG_WriteString( cs->string );
+ SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR );
+ }
+
+ ps->stats[STAT_LAYOUTS] = savedLayouts;
+
+ 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( udpClient_t *client, player_state_t *src ) {
+ player_state_t *ps = &client->ps;
+
+ memcpy( ps, src, sizeof( *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->scoreboard > SBOARD_FOLLOW ) {
+ ps->stats[STAT_LAYOUTS] = 1;
+ } else if( client->scoreboard == SBOARD_NONE ) {
+ ps->stats[STAT_LAYOUTS] = 0;
+ }
+ client->clientNum = PPS_NUM( src );
+}
+
+static void MVD_UpdateLayoutClients( udpClient_t *client ) {
+ char layout[MAX_STRING_CHARS];
+ char buffer[MAX_STRING_CHARS];
+ char status[MAX_QPATH];
+ int length, totalLength;
+ udpClient_t *cl;
+ mvd_t *mvd = client->mvd;
+ char name[16];
+ char *s;
+ int y;
+
+ totalLength = Com_sprintf( layout, sizeof( layout ),
+ //"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 " );
+
+ y = 8;
+ LIST_FOR_EACH( udpClient_t, cl, &mvd->udpClients, entry ) {
+ if( cl->cl->state < cs_spawned ) {
+ strcpy( status, "connecting" );
+ } else if( cl->following ) {
+ s = mvd->configstrings[CS_PLAYERSKINS + cl->followClientNum];
+ Q_strncpyz( name, s, sizeof( name ) );
+ s = strchr( name, '\\' );
+ if( s ) {
+ *s = 0;
+ }
+ Com_sprintf( status, sizeof( status ), "-> %s", name );
+ } else {
+ strcpy( status, "observing" );
+ }
+ length = Com_sprintf( buffer, sizeof( buffer ),
+ "xv 0 yv %d string \"%.16s\" "
+ "xv 152 string %d "
+ "xv 208 string \"%s\" ",
+ y, cl->cl->name, cl->ping, status );
+ if( totalLength + length > sizeof( layout ) - 1 ) {
+ length = Com_sprintf( buffer, sizeof( buffer ),
+ "xv 0 yv %d string <...>", y );
+ 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_UpdateLayoutChannels( udpClient_t *client ) {
+ char layout[MAX_STRING_CHARS];
+ char buffer[MAX_STRING_CHARS];
+ mvd_t *mvd;
+ int length, totalLength, cursor, y;
+
+ strcpy( layout, "xv 32 yv 8 picn inventory "
+ "xv 64 yv 32 string2 \"Channel Map CL\" "
+ "yv 40 string \"------------- ------- --\" "
+ "xl 32 yb -40 string2 \""APPLICATION" "VERSION"\" "
+ "yb -32 string http://q2pro.sf.net/ " );
+ totalLength = strlen( layout );
+
+ cursor = List_Count( &mvd_ready );
+ if( cursor ) {
+ clamp( client->cursor, 0, cursor - 1 );
+
+ y = 48;
+ cursor = 0;
+ LIST_FOR_EACH( mvd_t, mvd, &mvd_ready, ready ) {
+ length = Com_sprintf( buffer, sizeof( buffer ),
+ "xv 56 yv %d string \"%c%-13.13s %-7.7s %d%s\" ", y,
+ cursor == client->cursor ? 0x8d : 0x20,
+ mvd->name, mvd->mapname,
+ List_Count( &mvd->udpClients ),
+ mvd == client->mvd ? "+" : "" );
+ if( totalLength + length > sizeof( layout ) - 1 ) {
+ break;
+ }
+ strcpy( layout + totalLength, buffer );
+ totalLength += length;
+ y += 8;
+
+ cursor++;
+ }
+ } else {
+ client->cursor = 0;
+ strcat( layout, "xv 56 yv 48 string \" <no channels>\" " );
+ }
+
+ 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( udpClient_t *client ) {
+ mvd_t *mvd = client->mvd;
+ mvdPlayer_t *player= &mvd->players[mvd->clientNum];
+
+ 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( udpClient_t *client ) {
+ if( client->mvd == &mvd_waitingRoom ) {
+ client->scoreboard = SBOARD_CHANNELS;
+ client->cursor = 0;
+ MVD_UpdateLayoutChannels( client );
+ } else {
+ client->ps.stats[STAT_LAYOUTS] = 0;
+ client->scoreboard = SBOARD_NONE;
+ }
+}
+
+static void MVD_StartFollowing( udpClient_t *client, int playerNum ) {
+ mvdPlayer_t *player;
+ int i;
+ char name[MAX_CLIENT_NAME];
+ char *s;
+ player_state_t *ps = NULL;
+ mvd_t *mvd = client->mvd;
+ mvdFrame_t *frame = &mvd->frames[mvd->framenum & MVD_UPDATE_MASK];
+ mvdConfigstring_t *cs;
+
+ // find the desired player in current frame
+ FOR_EACH_PLAYER( mvd, ps, frame ) {
+ if( PPS_NUM( ps ) == playerNum ) {
+ break;
+ }
+ }
+
+ if( i == frame->numPlayers ) {
+ // pick up the first player
+ FOR_EACH_PLAYER( mvd, ps, frame ) {
+ if( PPS_NUM( ps ) != mvd->clientNum ) {
+ break;
+ }
+ }
+ if( i == frame->numPlayers ) {
+ SV_ClientPrintf( client->cl, PRINT_MEDIUM,
+ "[MVD] No players to follow.\n" );
+ if( client->following ) {
+ MVD_StartObserving( client );
+ }
+ return;
+ }
+
+ if( playerNum != CLIENTNUM_NONE ) {
+ SV_ClientPrintf( client->cl, PRINT_MEDIUM,
+ "[MVD] Player %d is not active.\n", playerNum );
+ }
+ //j = frame->firstPlayer % mvd.maxPlayerStates;
+ //ps = &mvd.playerStates[j];
+ playerNum = PPS_NUM( ps );
+ }
+
+ if( client->following && playerNum == client->followClientNum ) {
+ return;
+ }
+
+ player = &mvd->players[playerNum];
+
+ client->savedClientNum = client->followClientNum;
+ client->followClientNum = playerNum;
+ client->following = qtrue;
+
+ client->pmflags ^= PMF_TELEPORT_BIT;
+ MVD_UpdateFollower( client, ps );
+
+ s = mvd->configstrings[CS_PLAYERSKINS + playerNum];
+ Q_strncpyz( name, s, sizeof( name ) );
+ s = strchr( name, '\\' );
+ if( s ) {
+ *s = 0;
+ }
+
+ SV_ClientPrintf( client->cl, PRINT_MEDIUM, "[MVD] Following %s.\n", name );
+
+ // send delta configstrings
+ for( cs = player->configstrings; cs; cs = cs->next ) {
+ MSG_WriteByte( svc_configstring );
+ MSG_WriteShort( cs->index );
+ MSG_WriteString( cs->string );
+ 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->stats[STAT_LAYOUTS] & 1;
+ }
+}
+
+static void MVD_FollowCycle( udpClient_t *client, int dir ) {
+ mvd_t *mvd = client->mvd;
+ mvdFrame_t *frame = &mvd->frames[mvd->framenum & MVD_UPDATE_MASK];
+ player_state_t *ps;
+ int i, j;
+ int playerNum;
+
+ if( !frame->numPlayers ) {
+ return;
+ }
+
+ for( i = 0; i < frame->numPlayers; i++ ) {
+ j = ( frame->firstPlayer + i ) & MVD_PLAYERS_MASK;
+ ps = &mvd->playerStates[j];
+ playerNum = PPS_NUM( ps );
+ if( playerNum == client->followClientNum ) {
+ i += dir;
+ break;
+ }
+ }
+
+ i %= frame->numPlayers;
+ for( ; i < frame->numPlayers; i++ ) {
+ j = ( frame->firstPlayer + i ) & MVD_PLAYERS_MASK;
+ ps = &mvd->playerStates[j];
+ playerNum = PPS_NUM( ps );
+ if( playerNum != mvd->clientNum ) {
+ MVD_StartFollowing( client, playerNum );
+ }
+ }
+}
+
+static void MVD_UpdateClient( udpClient_t *client ) {
+ mvd_t *mvd = client->mvd;
+ mvdFrame_t *frame = &mvd->frames[mvd->framenum & MVD_UPDATE_MASK];
+ int i;
+ player_state_t *ps;
+
+ if( !client->following ) {
+ /* find the dummy MVD client */
+ FOR_EACH_PLAYER( mvd, ps, frame ) {
+ if( PPS_NUM( ps ) == mvd->clientNum ) {
+ for( i = 0; i < STAT_LAYOUTS; i++ ) {
+ client->ps.stats[i] = ps->stats[i];
+ }
+ for( i = STAT_LAYOUTS + 1; i < MAX_STATS; i++ ) {
+ client->ps.stats[i] = ps->stats[i];
+ }
+ break;
+ }
+ }
+ return;
+ }
+
+ /* find the player */
+ FOR_EACH_PLAYER( mvd, ps, frame ) {
+ if( PPS_NUM( ps ) == client->followClientNum ) {
+ MVD_UpdateFollower( client, ps );
+ return;
+ }
+ }
+
+ /* player is no longer present */
+ MVD_StartObserving( client );
+ client->savedFollowing = qtrue;
+}
+
+void MVD_Update( mvd_t *mvd ) {
+ udpClient_t *client;
+
+ /* update clients */
+ LIST_FOR_EACH( udpClient_t, client, &mvd->udpClients, entry ) {
+ if( client->cl->state != cs_spawned ) {
+ continue;
+ }
+ if( client->scoreboard == SBOARD_CLIENTS &&
+ client->layoutTime < sv.time )
+ {
+ MVD_UpdateLayoutClients( client );
+ }
+ }
+}
+
+
+static void MVD_GameInit( void ) {
+ mvd_t *mvd = &mvd_waitingRoom;
+ edict_t *edicts;
+ int i;
+
+ Com_Printf( "----- MVD_GameInit -----\n" );
+
+ mvd_motd = Cvar_Get( "mvd_motd", "Hello ${sv_client}!\\n"
+ "This is an experimental MVD server!", 0 );
+ mvd_admin_password = Cvar_Get( "mvd_admin_password", "", CVAR_PRIVATE );
+ mvd_flood_msgs = Cvar_Get( "flood_msgs", "4", 0 );
+ mvd_flood_persecond = Cvar_Get( "flood_persecond", "4", 0 ); // FIXME: rename this
+ mvd_flood_waitdelay = Cvar_Get( "flood_waitdelay", "10", 0 );
+ mvd_flood_mute = Cvar_Get( "flood_mute", "0", 0 );
+ mvd_custom_fov = Cvar_Get( "mvd_custom_fov", "1", 0 );
+
+ Z_TagReserve( ( sizeof( edict_t ) +
+ sizeof( udpClient_t ) ) * sv_maxclients->integer +
+ sizeof( edict_t ), TAG_GAME );
+ mvd_clients = Z_ReservedAllocz( sizeof( udpClient_t ) *
+ sv_maxclients->integer );
+ edicts = Z_ReservedAllocz( sizeof( edict_t ) *
+ ( sv_maxclients->integer + 1 ) );
+
+ for( i = 0; i < sv_maxclients->integer; i++ ) {
+ mvd_clients[i].cl = &svs.clientpool[i];
+ edicts[i + 1].client = ( gclient_t * )&mvd_clients[i];
+ }
+
+ mvd_ge.edicts = edicts;
+ mvd_ge.edict_size = sizeof( edict_t );
+ mvd_ge.num_edicts = sv_maxclients->integer + 1;
+ mvd_ge.max_edicts = sv_maxclients->integer + 1;
+
+ strcpy( mvd->name, "Waiting Room" );
+ List_Init( &mvd->udpClients );
+
+ strcpy( mvd->configstrings[CS_NAME], "Waiting Room" );
+ strcpy( mvd->configstrings[CS_MAXCLIENTS], "8" );
+ strcpy( mvd->configstrings[CS_MAPCHECKSUM], "80717714" );
+ strcpy( mvd->configstrings[CS_MODELS + 1], "maps/q2dm1.bsp" );
+
+ mvd->spawnSet = qtrue;
+ VectorSet( mvd->spawnOrigin, 984, 192, 784 );
+ VectorSet( mvd->spawnAngles, 25, 72, 0 );
+}
+
+static void MVD_GameShutdown( void ) {
+ mvd_t *mvd, *next;
+
+ Com_Printf( "----- MVD_GameShutdown -----\n" );
+
+ LIST_FOR_EACH_SAFE( mvd_t, mvd, next, &mvd_channels, entry ) {
+ MVD_Disconnect( mvd );
+ MVD_ClearState( mvd );
+ MVD_Free( mvd );
+ }
+
+ List_Init( &mvd_channels );
+ List_Init( &mvd_ready );
+
+ if( mvd_clients ) {
+ Z_Free( mvd_clients );
+ mvd_clients = NULL;
+ }
+
+ mvd_ge.edicts = NULL;
+ mvd_ge.edict_size = 0;
+ mvd_ge.num_edicts = 0;
+ mvd_ge.max_edicts = 0;
+
+ Z_LeakTest( TAG_MVD );
+}
+
+static 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 ) {
+ udpClient_t *client = EDICT_MVDCL( ent );
+ mvd_t *mvd = client->mvd;
+ int count;
+
+ if( !mvd ) {
+ count = List_Count( &mvd_ready );
+ if( count == 1 ) {
+ mvd = LIST_FIRST( mvd_t, &mvd_ready, ready );
+ } else {
+ mvd = &mvd_waitingRoom;
+ }
+ List_Append( &mvd->udpClients, &client->entry );
+ client->mvd = mvd;
+ }
+
+ return qtrue;
+}
+
+static void MVD_GameClientBegin( edict_t *ent ) {
+ udpClient_t *client = EDICT_MVDCL( ent );
+ char *s;
+
+ client->savedClientNum = CLIENTNUM_NONE;
+
+ client->floodTime = 0;
+ client->floodHead = 0;
+ memset( &client->lastcmd, 0, sizeof( client->lastcmd ) );
+
+ if( !client->connected ) {
+ SV_BroadcastPrintf( PRINT_MEDIUM, "[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_SetValidPos( client );
+ MVD_StartObserving( client );
+ MVD_SetDefaultLayout( client );
+}
+
+static void MVD_GameClientUserinfoChanged( edict_t *ent, char *userinfo ) {
+ udpClient_t *client = EDICT_MVDCL( ent );
+ float fov;
+
+ 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 ) {
+ udpClient_t *client = EDICT_MVDCL( ent );
+ client_t *cl = client->cl;
+
+ if( client->connected ) {
+ SV_BroadcastPrintf( PRINT_MEDIUM,
+ "[MVD] %s left the server\n", cl->name );
+ }
+
+ List_Delete( &client->entry );
+
+ memset( client, 0, sizeof( *client ) );
+ client->cl = cl;
+}
+
+void MVD_SwitchChannel( udpClient_t *client, mvd_t *mvd ) {
+ if( mvd == client->mvd ) {
+ return; /* nothing to do */
+ }
+
+ List_Delete( &client->entry );
+ List_Append( &mvd->udpClients, &client->entry );
+ client->mvd = mvd;
+
+ /* needs to reconnect */
+ MSG_WriteByte( svc_stufftext );
+ MSG_WriteString( "changing; reconnect\n" );
+ SV_ClientReset( client->cl );
+ SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR );
+}
+
+static void MVD_StartAdmin( udpClient_t *client ) {
+ client->admin = qtrue;
+ SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Granted admin status.\n" );
+}
+
+static void MVD_StopAdmin( udpClient_t *client ) {
+ client->admin = qfalse;
+ SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Lost admin status.\n" );
+}
+
+static void MVD_GameClientCommand( edict_t *ent ) {
+ udpClient_t *client = EDICT_MVDCL( ent );
+ mvd_t *mvd = client->mvd;
+ client_t *cl;
+ char *cmd;
+ int clientNum, cursor, i;
+
+ cl = client->cl;
+ cmd = Cmd_Argv( 0 );
+ if( !strcmp( cmd, "!mvdadmin" ) ) {
+ if( mvd->demoplayback ) {
+ return;
+ }
+ if( client->admin ) {
+ MVD_StopAdmin( client );
+ return;
+ }
+ if( !NET_IsLocalAddress( &cl->netchan->remote_address ) ) {
+ if( Cmd_Argc() < 2 ) {
+ SV_ClientPrintf( 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( cl, PRINT_HIGH, "[MVD] Invalid password.\n" );
+ return;
+ }
+ }
+ MVD_StartAdmin( client );
+ return;
+ }
+
+ if( !strcmp( cmd, "say" ) || !strcmp( cmd, "say_team" ) ) {
+ if( mvd_flood_mute->integer ) {
+ SV_ClientPrintf( cl, PRINT_HIGH,
+ "[MVD] Sorry, spectator chat is muted.\n" );
+ return;
+ }
+ if( client->floodTime > sv.time ) {
+ SV_ClientPrintf( 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( 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, "{%s}: %s\n", cl->name, Cmd_Args() );
+ client->floodSamples[client->floodHead & FLOOD_MASK] = sv.time;
+ client->floodHead++;
+ return;
+ }
+ if( !strcmp( cmd, "playernext" ) ) {
+ MVD_FollowCycle( client, 1 );
+ return;
+ }
+ if( !strcmp( cmd, "playerprev" ) ) {
+ MVD_FollowCycle( client, -1 );
+ return;
+ }
+ if( !strcmp( cmd, "playertoggle" ) ) {
+ MVD_StartFollowing( client, client->savedClientNum );
+ return;
+ }
+ if( !strcmp( cmd, "follow" ) ) {
+ if( Cmd_Argc() < 2 ) {
+ if( !client->following ) {
+ MVD_StartFollowing( client, client->savedClientNum );
+ }
+ return;
+ }
+ clientNum = atoi( Cmd_Argv( 1 ) );
+ MVD_StartFollowing( client, clientNum );
+ return;
+ }
+ if( !strcmp( cmd, "observe" ) ) {
+ if( client->following ) {
+ MVD_StartObserving( client );
+ }
+ return;
+ }
+ if( !strcmp( cmd, "inven" ) ) {
+ if( client->scoreboard == SBOARD_CLIENTS ) {
+ MVD_SetDefaultLayout( client );
+ } else {
+ client->scoreboard = SBOARD_CLIENTS;
+ MVD_UpdateLayoutClients( client );
+ }
+ return;
+ }
+ if( !strcmp( cmd, "invnext" ) ) {
+ if( client->scoreboard == SBOARD_CHANNELS ) {
+ client->cursor++;
+ MVD_UpdateLayoutChannels( client );
+ }
+ return;
+ }
+ if( !strcmp( cmd, "invprev" ) ) {
+ if( client->scoreboard == SBOARD_CHANNELS ) {
+ client->cursor--;
+ MVD_UpdateLayoutChannels( client );
+ }
+ return;
+ }
+ if( !strcmp( cmd, "invuse" ) ) {
+ if( client->scoreboard == SBOARD_CHANNELS ) {
+ cursor = 0;
+ LIST_FOR_EACH( mvd_t, mvd, &mvd_channels, entry ) {
+ if( !mvd->framenum ) {
+ continue;
+ }
+ if( cursor == client->cursor ) {
+ MVD_SwitchChannel( client, mvd );
+ return;
+ }
+ cursor++;
+ }
+ }
+ return;
+ }
+ if( !strcmp( cmd, "help" ) ) {
+ if( client->scoreboard == SBOARD_SCORES ) {
+ MVD_SetDefaultLayout( client );
+ } else {
+ client->scoreboard = SBOARD_SCORES;
+ MVD_UpdateLayoutScores( client );
+ }
+ return;
+ }
+
+ if( !strcmp( cmd, "putaway" ) ) {
+ MVD_SetDefaultLayout( client );
+ return;
+ }
+ if( !strcmp( cmd, "channels" ) ) {
+ client->scoreboard = SBOARD_CHANNELS;
+ MVD_UpdateLayoutChannels( client );
+ return;
+ }
+
+ SV_ClientPrintf( cl, PRINT_LOW, "[MVD] unknown command '%s'\n", cmd );
+}
+
+static trace_t MVD_Trace( vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end ) {
+ trace_t trace;
+
+ memset( &trace, 0, sizeof( trace ) );
+ VectorCopy( end, trace.endpos );
+ trace.fraction = 1;
+
+ return trace;
+}
+
+static int MVD_PointContents( vec3_t p ) {
+ return 0;
+}
+
+static void MVD_GameClientThink( edict_t *ent, usercmd_t *cmd ) {
+ udpClient_t *client = EDICT_MVDCL( ent );
+ usercmd_t *old = &client->lastcmd;
+ pmove_t pm;
+
+ if( !( old->buttons & BUTTON_ATTACK ) && ( cmd->buttons & BUTTON_ATTACK ) ) {
+ if( client->following ) {
+ MVD_StartObserving( client );
+ } else {
+ MVD_StartFollowing( client, client->savedClientNum );
+ }
+ }
+
+ if( client->following ) {
+ if( !old->upmove && cmd->upmove ) {
+ MVD_FollowCycle( client, 1 );
+ }
+ } else {
+ memset( &pm, 0, sizeof( pm ) );
+ pm.trace = MVD_Trace;
+ pm.pointcontents = MVD_PointContents;
+ pm.s = client->ps.pmove;
+ pm.cmd = *cmd;
+
+ PF_Pmove( &pm );
+
+ client->ps.pmove = pm.s;
+ if( pm.s.pm_type != PM_FREEZE ) {
+ VectorCopy( pm.viewangles, client->ps.viewangles );
+ }
+ }
+
+ *old = *cmd;
+}
+
+static void MVD_GameRunFrame( void ) {
+ mvd_t *mvd, *next;
+ udpClient_t *u;
+ tcpClient_t *t;
+ uint16 length;
+
+ LIST_FOR_EACH_SAFE( mvd_t, mvd, next, &mvd_ready, ready ) {
+ if( setjmp( mvd_jmpbuf ) ) {
+ continue;
+ }
+
+ MVD_Update( mvd );
+
+ // parse stream
+ if( mvd->state < MVD_READING ) {
+ continue;
+ }
+
+ if( !MVD_Parse( mvd ) ) {
+ continue;
+ }
+
+ // update UDP clients
+ LIST_FOR_EACH( udpClient_t, u, &mvd->udpClients, entry ) {
+ if( u->cl->state == cs_spawned ) {
+ MVD_UpdateClient( u );
+ }
+ }
+
+ // send this message to TCP clients
+ length = LittleShort( msg_read.cursize );
+ LIST_FOR_EACH( tcpClient_t, t, &mvd->tcpClients, mvdEntry ) {
+ if( t->state == cs_spawned ) {
+ SV_HttpWrite( t, &length, 2 );
+ SV_HttpWrite( t, msg_read.data, msg_read.cursize );
+#if USE_ZLIB
+ t->noflush++;
+#endif
+ }
+ }
+
+ // write this message to demofile
+ if( mvd->demorecording ) {
+ FS_Write( &length, 2, mvd->demofile );
+ FS_Write( msg_read.data, msg_read.cursize, mvd->demofile );
+ }
+ }
+}
+
+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
+};
+