/* 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 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_filter_version; static cvar_t *mvd_stats_hack; static cvar_t *mvd_freeze_hack; udpClient_t *mvd_clients; mvd_player_t mvd_dummy; extern jmp_buf mvd_jmpbuf; /* ============================================================================== LAYOUTS ============================================================================== */ static void MVD_LayoutClients( udpClient_t *client ) { static const char header[] = "xv 16 yv 0 string2 \" Name RTT Status\""; char layout[MAX_STRING_CHARS]; char buffer[MAX_STRING_CHARS]; char status[MAX_QPATH]; size_t length, total; udpClient_t *cl; mvd_t *mvd = client->mvd; int y, i, prestep; // calculate prestep if( client->layout_cursor < 0 ) { client->layout_cursor = 0; } else if( client->layout_cursor ) { total = List_Count( &mvd->udpClients ); if( client->layout_cursor > total / 10 ) { client->layout_cursor = total / 10; } } prestep = client->layout_cursor * 10; memcpy( layout, header, sizeof( header ) - 1 ); total = sizeof( header ) - 1; y = 8; i = 0; LIST_FOR_EACH( udpClient_t, cl, &mvd->udpClients, entry ) { if( ++i < prestep ) { continue; } if( cl->cl->state < cs_spawned ) { continue; } if( cl->target ) { strcpy( status, "-> " ); strcpy( status + 3, cl->target->name ); } else { strcpy( status, "observing" ); } length = Com_sprintf( buffer, sizeof( buffer ), "yv %d string \"%3d %-15.15s %3d %s\"", y, i, cl->cl->name, cl->ping, status ); if( total + length >= sizeof( layout ) ) { break; } memcpy( layout + total, buffer, length ); total += length; y += 8; } layout[total] = 0; // send the layout MSG_WriteByte( svc_layout ); MSG_WriteData( layout, total + 1 ); SV_ClientAddMessage( client->cl, MSG_CLEAR ); client->layout_time = svs.realtime; } static void MVD_LayoutChannels( udpClient_t *client ) { static const char header[] = "xv 32 yv 8 picn inventory " "xv 240 yv 172 string2 " VERSION " " "xv 0 yv 32 cstring \"\020Channel Chooser\021\"" "xv 64 yv 48 string2 \"Name Map CL\"" "yv 56 string \"------------- ------- --\" xv 56 "; static const char nochans[] = "yv 64 string \" \""; char layout[MAX_STRING_CHARS]; char buffer[MAX_STRING_CHARS]; mvd_t *mvd; size_t length, total; int cursor, y; memcpy( layout, header, sizeof( header ) - 1 ); total = sizeof( header ) - 1; cursor = List_Count( &mvd_ready ); if( cursor ) { if( client->layout_cursor < 0 ) { client->layout_cursor = cursor - 1; } else if( client->layout_cursor > cursor - 1 ) { client->layout_cursor = 0; } y = 64; cursor = 0; LIST_FOR_EACH( mvd_t, mvd, &mvd_ready, ready ) { length = Com_sprintf( buffer, sizeof( buffer ), "yv %d string \"%c%-13.13s %-7.7s %d%s\" ", y, cursor == client->layout_cursor ? 0x8d : 0x20, mvd->name, mvd->mapname, List_Count( &mvd->udpClients ), mvd == client->mvd ? "+" : "" ); if( total + length >= sizeof( layout ) ) { break; } memcpy( layout + total, buffer, length ); total += length; y += 8; if( y > 172 ) { break; } cursor++; } } else { client->layout_cursor = 0; memcpy( layout + total, nochans, sizeof( nochans ) - 1 ); total += sizeof( nochans ) - 1; } layout[total] = 0; // send the layout MSG_WriteByte( svc_layout ); MSG_WriteData( layout, total + 1 ); SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR ); client->layout_time = svs.realtime; } #define MENU_ITEMS 10 #define YES "\xD9\xE5\xF3" #define NO "\xCE\xEF" static void MVD_LayoutMenu( udpClient_t *client ) { static const char format[] = "xv 32 yv 8 picn inventory " "xv 0 yv 32 cstring \"\020Main Menu\021\" xv 56 " "yv 48 string2 \"%c%s in-eyes mode\"" "yv 56 string2 \"%cShow scoreboard\"" "yv 64 string2 \"%cShow spectators (%d)\"" "yv 72 string2 \"%cShow channels (%d)\"" "yv 80 string2 \"%cLeave this channel\"" "yv 96 string \"%cIgnore spectator chat: %s\"" "yv 104 string \"%cIgnore connect msgs: %s\"" "yv 112 string \"%cIgnore player chat: %s\"" "yv 120 string \"%cIgnore player FOV: %s\"" "yv 128 string \" (use 'set uf %d u' in cfg)\"" "yv 144 string2 \"%cExit menu\"" "xv 240 yv 172 string2 " VERSION; char layout[MAX_STRING_CHARS]; char cur[MENU_ITEMS]; size_t total; if( client->layout_cursor < 0 ) { client->layout_cursor = MENU_ITEMS - 1; } else if( client->layout_cursor > MENU_ITEMS - 1 ) { client->layout_cursor = 0; } memset( cur, 0x20, sizeof( cur ) ); cur[client->layout_cursor] = 0x8d; total = Com_sprintf( layout, sizeof( layout ), format, cur[0], client->target ? "Leave" : "Enter", cur[1], cur[2], List_Count( &client->mvd->udpClients ), cur[3], List_Count( &mvd_ready ), cur[4], cur[5], ( client->uf & UF_MUTE_OBSERVERS ) ? YES : NO, cur[6], ( client->uf & UF_MUTE_MISC ) ? YES : NO, cur[7], ( client->uf & UF_MUTE_PLAYERS ) ? YES: NO, cur[8], ( client->uf & UF_LOCALFOV ) ? YES : NO, client->uf, cur[9] ); // send the layout MSG_WriteByte( svc_layout ); MSG_WriteData( layout, total + 1 ); SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR ); client->layout_time = svs.realtime; } static void MVD_LayoutScores( udpClient_t *client ) { mvd_t *mvd = client->mvd; char *layout = mvd->layout[0] ? mvd->layout : "xv 100 yv 60 string \"\""; // send the layout MSG_WriteByte( svc_layout ); MSG_WriteString( layout ); SV_ClientAddMessage( client->cl, MSG_CLEAR ); client->layout_time = svs.realtime; } static void MVD_LayoutFollow( udpClient_t *client ) { char *name = client->target ? client->target->name : ""; // send the layout MSG_WriteByte( svc_layout ); MSG_WriteString( va( "xv 0 yt 48 cstring \"%s\"", name ) ); SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR ); client->layout_time = svs.realtime; } static void MVD_SetDefaultLayout( udpClient_t *client ) { mvd_t *mvd = client->mvd; if( mvd == &mvd_waitingRoom ) { client->layout_type = LAYOUT_CHANNELS; } else if( mvd->intermission ) { client->layout_type = LAYOUT_SCORES; } else if( client->target && ( client->cl->protocol != PROTOCOL_VERSION_Q2PRO || client->cl->settings[CLS_RECORDING] ) ) { client->layout_type = LAYOUT_FOLLOW; } else { client->layout_type = LAYOUT_NONE; } // force an update client->layout_time = 0; client->layout_cursor = 0; } static void MVD_SetFollowLayout( udpClient_t *client ) { if( !client->layout_type ) { MVD_SetDefaultLayout( client ); } else if( client->layout_type == LAYOUT_FOLLOW ) { client->layout_time = 0; // force an update } } // this is the only function that actually writes layouts static void MVD_UpdateLayouts( mvd_t *mvd ) { udpClient_t *client; LIST_FOR_EACH( udpClient_t, client, &mvd->udpClients, entry ) { if( client->cl->state != cs_spawned ) { continue; } client->ps.stats[STAT_LAYOUTS] = client->layout_type ? 1 : 0; switch( client->layout_type ) { case LAYOUT_NONE: if( client->cl->protocol != PROTOCOL_VERSION_Q2PRO ) { break; } if( client->target && client->cl->settings[CLS_RECORDING] ) { client->layout_type = LAYOUT_FOLLOW; MVD_LayoutFollow( client ); } break; case LAYOUT_FOLLOW: if( !client->layout_time ) { MVD_LayoutFollow( client ); } break; case LAYOUT_SCORES: if( !client->layout_time ) { MVD_LayoutScores( client ); } break; case LAYOUT_MENU: if( !client->layout_time ) { MVD_LayoutMenu( client ); } break; case LAYOUT_CLIENTS: if( svs.realtime - client->layout_time > LAYOUT_MSEC ) { MVD_LayoutClients( client ); } break; case LAYOUT_CHANNELS: if( mvd_dirty || !client->layout_time ) { MVD_LayoutChannels( client ); } break; default: break; } } } /* ============================================================================== CHASE CAMERA ============================================================================== */ static void MVD_FollowStop( udpClient_t *client ) { mvd_t *mvd = client->mvd; mvd_cs_t *cs; int i; client->ps.viewangles[ROLL] = 0; for( i = 0; i < 3; i++ ) { client->ps.pmove.delta_angles[i] = ANGLE2SHORT( client->ps.viewangles[i] ) - client->lastcmd.angles[i]; } VectorClear( client->ps.kick_angles ); Vector4Clear( client->ps.blend ); client->ps.pmove.pm_flags = 0; client->ps.pmove.pm_type = mvd->pm_type; client->ps.rdflags = 0; client->ps.gunindex = 0; client->ps.fov = client->fov; for( cs = mvd->dummy->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 ); } client->clientNum = mvd->clientNum; client->oldtarget = client->target; client->target = NULL; if( client->layout_type == LAYOUT_FOLLOW ) { MVD_SetDefaultLayout( client ); } MVD_UpdateClient( client ); } static void MVD_FollowStart( udpClient_t *client, mvd_player_t *target ) { mvd_cs_t *cs; if( client->target == target ) { return; } client->oldtarget = client->target; client->target = target; // send delta configstrings for( cs = target->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 ); } SV_ClientPrintf( client->cl, PRINT_LOW, "[MVD] Following %s.\n", target->name ); MVD_SetFollowLayout( client ); MVD_UpdateClient( client ); } static void MVD_FollowFirst( udpClient_t *client ) { mvd_t *mvd = client->mvd; mvd_player_t *target; int i; // pick up the first active player for( i = 0; i < mvd->maxclients; i++ ) { target = &mvd->players[i]; if( target->inuse && target != mvd->dummy ) { MVD_FollowStart( client, target ); return; } } SV_ClientPrintf( client->cl, PRINT_MEDIUM, "[MVD] No players to follow.\n" ); } static void MVD_FollowLast( udpClient_t *client ) { mvd_t *mvd = client->mvd; mvd_player_t *target; int i; // pick up the last active player for( i = 0; i < mvd->maxclients; i++ ) { target = &mvd->players[ mvd->maxclients - i - 1 ]; if( target->inuse && target != mvd->dummy ) { MVD_FollowStart( client, target ); return; } } SV_ClientPrintf( client->cl, PRINT_MEDIUM, "[MVD] No players to follow.\n" ); } static void MVD_FollowNext( udpClient_t *client ) { mvd_t *mvd = client->mvd; mvd_player_t *target = client->target; if( !target ) { MVD_FollowFirst( client ); return; } do { if( target == mvd->players + mvd->maxclients - 1 ) { target = mvd->players; } else { target++; } if( target == client->target ) { return; } } while( !target->inuse || target == mvd->dummy ); MVD_FollowStart( client, target ); } static void MVD_FollowPrev( udpClient_t *client ) { mvd_t *mvd = client->mvd; mvd_player_t *target = client->target; if( !target ) { MVD_FollowLast( client ); return; } do { if( target == mvd->players ) { target = mvd->players + mvd->maxclients - 1; } else { target--; } if( target == client->target ) { return; } } while( !target->inuse || target == mvd->dummy ); MVD_FollowStart( client, target ); } static mvd_player_t *MVD_MostFollowed( mvd_t *mvd ) { int count[MAX_CLIENTS]; udpClient_t *other; mvd_player_t *player, *target = NULL; int i, maxcount = -1; memset( count, 0, sizeof( count ) ); LIST_FOR_EACH( udpClient_t, other, &mvd->udpClients, entry ) { if( other->cl->state == cs_spawned && other->target ) { count[ other->target - mvd->players ]++; } } for( i = 0, player = mvd->players; i < mvd->maxclients; i++, player++ ) { if( player->inuse && player != mvd->dummy && maxcount < count[i] ) { maxcount = count[i]; target = player; } } return target; } void MVD_UpdateClient( udpClient_t *client ) { mvd_t *mvd = client->mvd; mvd_player_t *target = client->target; int i; if( !target ) { // copy stats of the dummy MVD observer target = mvd->dummy; for( i = 0; i < MAX_STATS; i++ ) { client->ps.stats[i] = target->ps.stats[i]; } return; } if( !target->inuse ) { // player is no longer active MVD_FollowStop( client ); return; } // copy entire player state client->ps = target->ps; if( client->uf & UF_LOCALFOV ) { client->ps.fov = client->fov; } client->ps.pmove.pm_flags |= PMF_NO_PREDICTION; client->ps.pmove.pm_type = PM_FREEZE; client->clientNum = target - mvd->players; if( mvd_stats_hack->integer && target != mvd->dummy ) { // copy stats of the dummy MVD observer target = mvd->dummy; for( i = 0; i < MAX_STATS; i++ ) { if( mvd_stats_hack->integer & ( 1 << i ) ) { client->ps.stats[i] = target->ps.stats[i]; } } } } /* ============================================================================== SPECTATOR COMMANDS ============================================================================== */ void MVD_BroadcastPrintf( mvd_t *mvd, int level, int mask, const char *fmt, ... ) { va_list argptr; char text[MAXPRINTMSG]; size_t len; udpClient_t *other; client_t *cl; va_start( argptr, fmt ); len = Q_vsnprintf( text, sizeof( text ), fmt, argptr ); va_end( argptr ); if( level == PRINT_CHAT && mvd_filter_version->integer ) { char *s = strstr( text, "!version" ); if( s ) { s[6] = '0'; } } MSG_WriteByte( svc_print ); MSG_WriteByte( level ); MSG_WriteData( text, len + 1 ); LIST_FOR_EACH( udpClient_t, other, &mvd->udpClients, entry ) { cl = other->cl; if( cl->state < cs_spawned ) { continue; } if( level < cl->messagelevel ) { continue; } if( other->uf & mask ) { continue; } SV_ClientAddMessage( cl, MSG_RELIABLE ); } SZ_Clear( &msg_write ); } void MVD_SwitchChannel( udpClient_t *client, mvd_t *mvd ) { client_t *cl = client->cl; List_Remove( &client->entry ); List_Append( &mvd->udpClients, &client->entry ); client->mvd = mvd; client->begin_time = 0; client->target = client->oldtarget = NULL; cl->gamedir = mvd->gamedir; cl->mapname = mvd->mapname; cl->configstrings = ( char * )mvd->configstrings; cl->slot = mvd->clientNum; cl->cm = &mvd->cm; cl->pool = &mvd->pool; // needs to reconnect MSG_WriteByte( svc_stufftext ); MSG_WriteString( va( "changing map=%s; reconnect\n", mvd->mapname ) ); SV_ClientReset( client->cl ); SV_ClientAddMessage( client->cl, MSG_RELIABLE|MSG_CLEAR ); } void MVD_TrySwitchChannel( udpClient_t *client, mvd_t *mvd ) { if( mvd == client->mvd ) { SV_ClientPrintf( client->cl, PRINT_HIGH, mvd == &mvd_waitingRoom ? "[MVD] You are already in the Waiting Room.\n" : "[MVD] You are already on this channel.\n" ); return; // nothing to do } if( client->begin_time ) { if( client->begin_time > svs.realtime ) { client->begin_time = svs.realtime; } if( svs.realtime - client->begin_time < 2000 ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] You may not switch channels too soon.\n" ); return; } MVD_BroadcastPrintf( client->mvd, PRINT_MEDIUM, UF_MUTE_MISC, "[MVD] %s left the channel.\n", client->cl->name ); } MVD_SwitchChannel( client, mvd ); } static void MVD_Admin_f( udpClient_t *client ) { char *s = mvd_admin_password->string; if( client->admin ) { client->admin = qfalse; SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Lost admin status.\n" ); return; } if( !NET_IsLocalAddress( &client->cl->netchan->remote_address ) ) { if( Cmd_Argc() < 2 ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "Usage: %s \n", Cmd_Argv( 0 ) ); return; } if( !s[0] || strcmp( s, Cmd_Argv( 1 ) ) ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Invalid password.\n" ); return; } } client->admin = qtrue; SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Granted admin status.\n" ); } static void MVD_Say_f( udpClient_t *client ) { mvd_t *mvd = client->mvd; unsigned delta, delay = mvd_flood_waitdelay->value * 1000; unsigned treshold = mvd_flood_persecond->value * 1000; char *text; int i; if( mvd_flood_mute->integer && !client->admin ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Spectators may not talk on this server.\n" ); return; } if( client->uf & UF_MUTE_OBSERVERS ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Please turn off ignore mode first.\n" ); return; } if( client->floodTime ) { delta = svs.realtime - client->floodTime; if( delta < delay ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] You can't talk for %u more seconds.\n", ( delay - delta ) / 1000 ); return; } } Cvar_ClampInteger( mvd_flood_msgs, 0, FLOOD_SAMPLES - 1 ); i = client->floodHead - mvd_flood_msgs->integer - 1; if( i >= 0 ) { delta = svs.realtime - client->floodSamples[i & FLOOD_MASK]; if( delta < treshold ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] You can't talk for %u seconds.\n", delay / 1000 ); client->floodTime = svs.realtime; return; } } client->floodSamples[client->floodHead & FLOOD_MASK] = svs.realtime; client->floodHead++; text = Cmd_Args(); //text[128] = 0; // don't let it be too long MVD_BroadcastPrintf( mvd, PRINT_CHAT, client->admin ? 0 : UF_MUTE_OBSERVERS, "[MVD] %s: %s\n", client->cl->name, text ); } static void MVD_Observe_f( udpClient_t *client ) { if( client->mvd == &mvd_waitingRoom ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Please enter a channel first.\n" ); return; } if( client->mvd->intermission ) { return; } if( client->target ) { MVD_FollowStop( client ); } else if( client->oldtarget && client->oldtarget->inuse ) { MVD_FollowStart( client, client->oldtarget ); } else { MVD_FollowFirst( client ); } } static void MVD_Follow_f( udpClient_t *client ) { mvd_t *mvd = client->mvd; mvd_player_t *player; entity_state_t *ent; char *s; int i, mask; if( mvd == &mvd_waitingRoom ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Please enter a channel first.\n" ); return; } if( mvd->intermission ) { return; } if( Cmd_Argc() < 2 ) { MVD_Observe_f( client ); return; } s = Cmd_Argv( 1 ); if( s[0] == '!' ) { switch( s[1] ) { case 'q': mask = EF_QUAD; break; case 'i': mask = EF_PENT; break; case 'r': mask = EF_FLAG1; break; case 'b': mask = EF_FLAG2; break; case 'p': if( client->oldtarget && client->oldtarget->inuse ) { MVD_FollowStart( client, client->oldtarget ); } else { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Previous target is not active.\n" ); } return; default: SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Unknown target '%s'. Valid targets:\n" "q (quad), i (invulner), r (red flag), " "b (blue flag), p (previous target).\n", s + 1 ); return; } for( i = 0; i < mvd->maxclients; i++ ) { player = &mvd->players[i]; if( !player->inuse || player == mvd->dummy ) { continue; } ent = &mvd->edicts[ i + 1 ].s; if( ent->effects & mask ) { goto follow; } } SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] No players with '%s' powerup.\n", s + 1 ); return; } i = atoi( s ); if( i < 0 || i >= mvd->maxclients ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Player number %d is invalid.\n", i ); return; } player = &mvd->players[i]; if( !player->inuse || player == mvd->dummy ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Player %d is not active.\n", i ); return; } follow: MVD_FollowStart( client, player ); } static void MVD_Invuse_f( udpClient_t *client ) { mvd_t *mvd; int cursor = 0; int uf = client->uf; if( client->layout_type == LAYOUT_MENU ) { if( client->layout_cursor < 0 ) { client->layout_cursor = MENU_ITEMS - 1; } else if( client->layout_cursor > MENU_ITEMS - 1 ) { client->layout_cursor = 0; } switch( client->layout_cursor ) { case 0: MVD_SetDefaultLayout( client ); MVD_Observe_f( client ); return; case 1: client->layout_type = LAYOUT_SCORES; break; case 2: client->layout_type = LAYOUT_CLIENTS; break; case 3: client->layout_type = LAYOUT_CHANNELS; break; case 4: MVD_TrySwitchChannel( client, &mvd_waitingRoom ); return; case 5: client->uf ^= UF_MUTE_OBSERVERS; break; case 6: client->uf ^= UF_MUTE_MISC; break; case 7: client->uf ^= UF_MUTE_PLAYERS; break; case 8: client->uf ^= UF_LOCALFOV; break; case 9: MVD_SetDefaultLayout( client ); break; } if( uf != client->uf ) { SV_ClientCommand( client->cl, "set uf %d u\n", client->uf ); } client->layout_time = 0; return; } if( client->layout_type != LAYOUT_CHANNELS ) { return; } LIST_FOR_EACH( mvd_t, mvd, &mvd_channels, entry ) { if( !mvd->framenum ) { continue; } if( cursor == client->layout_cursor ) { MVD_TrySwitchChannel( client, mvd ); return; } cursor++; } } static void MVD_Join_f( udpClient_t *client ) { mvd_t *mvd; SV_BeginRedirect( RD_CLIENT ); mvd = MVD_SetChannel( 1 ); Com_EndRedirect(); if( !mvd ) { return; } if( mvd->state < MVD_WAITING ) { SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] This channel is not ready yet.\n" ); return; } MVD_TrySwitchChannel( client, mvd ); } static void MVD_GameClientCommand( edict_t *ent ) { udpClient_t *client = EDICT_MVDCL( ent ); char *cmd; cmd = Cmd_Argv( 0 ); if( !strcmp( cmd, "!mvdadmin" ) ) { MVD_Admin_f( client ); return; } if( !strcmp( cmd, "say" ) || !strcmp( cmd, "say_team" ) ) { MVD_Say_f( client ); return; } if( !strcmp( cmd, "follow" ) || !strcmp( cmd, "chase" ) ) { MVD_Follow_f( client ); return; } if( !strcmp( cmd, "observe" ) ) { MVD_Observe_f( client ); return; } if( !strcmp( cmd, "inven" ) || !strcmp( cmd, "menu" ) ) { if( client->layout_type == LAYOUT_MENU ) { MVD_SetDefaultLayout( client ); } else { client->layout_type = LAYOUT_MENU; client->layout_time = 0; } return; } if( !strcmp( cmd, "invnext" ) ) { if( client->layout_type >= LAYOUT_MENU ) { client->layout_cursor++; client->layout_time = 0; } else if( !client->mvd->intermission ) { MVD_FollowNext( client ); } return; } if( !strcmp( cmd, "invprev" ) ) { if( client->layout_type >= LAYOUT_MENU ) { client->layout_cursor--; client->layout_time = 0; } else if( !client->mvd->intermission ) { MVD_FollowPrev( client ); } return; } if( !strcmp( cmd, "invuse" ) ) { MVD_Invuse_f( client ); return; } if( !strcmp( cmd, "help" ) || !strncmp( cmd, "score", 5 ) ) { if( client->layout_type == LAYOUT_SCORES ) { MVD_SetDefaultLayout( client ); } else { client->layout_type = LAYOUT_SCORES; client->layout_time = 0; } return; } if( !strcmp( cmd, "putaway" ) ) { MVD_SetDefaultLayout( client ); return; } if( !strcmp( cmd, "channels" ) ) { client->layout_type = LAYOUT_CHANNELS; client->layout_time = 0; return; } if( !strcmp( cmd, "clients" ) ) { client->layout_type = LAYOUT_CLIENTS; client->layout_time = 0; return; } if( !strcmp( cmd, "join" ) ) { MVD_Join_f( client ); return; } if( !strcmp( cmd, "leave" ) ) { MVD_TrySwitchChannel( client, &mvd_waitingRoom ); return; } SV_ClientPrintf( client->cl, PRINT_HIGH, "[MVD] Unknown command: %s\n", cmd ); } /* ============================================================================== MISC GAME FUNCTIONS ============================================================================== */ void MVD_RemoveClient( client_t *client ) { int index = client - svs.udp_client_pool; udpClient_t *cl = &mvd_clients[index]; List_Remove( &cl->entry ); memset( cl, 0, sizeof( *cl ) ); cl->cl = client; } static void MVD_GameInit( void ) { mvd_t *mvd = &mvd_waitingRoom; edict_t *edicts; cvar_t *mvd_default_map; char buffer[MAX_QPATH]; unsigned checksum; const char *error; int i; Com_Printf( "----- MVD_GameInit -----\n" ); 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_filter_version = Cvar_Get( "mvd_filter_version", "0", 0 ); mvd_default_map = Cvar_Get( "mvd_default_map", "q2dm1", CVAR_LATCH ); mvd_stats_hack = Cvar_Get( "mvd_stats_hack", "0", 0 ); mvd_freeze_hack = Cvar_Get( "mvd_freeze_hack", "0", 0 ); Cvar_Get( "g_features", va( "%d", GMF_CLIENTNUM|GMF_PROPERINUSE ), CVAR_ROM ); 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.udp_client_pool[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; Com_sprintf( buffer, sizeof( buffer ), "maps/%s.bsp", mvd_default_map->string ); error = CM_LoadMapEx( &mvd->cm, buffer, CM_LOAD_CLIENT|CM_LOAD_ENTONLY, &checksum ); if( error ) { Com_WPrintf( "Couldn't load %s for the Waiting Room: %s\n", buffer, error ); Cvar_Reset( mvd_default_map ); strcpy( buffer, "maps/q2dm1.bsp" ); checksum = 80717714; VectorSet( mvd->spawnOrigin, 984, 192, 784 ); VectorSet( mvd->spawnAngles, 25, 72, 0 ); } else { // get the spectator spawn point MVD_ParseEntityString( mvd ); CM_FreeMap( &mvd->cm ); } strcpy( mvd->name, "Waiting Room" ); Cvar_VariableStringBuffer( "game", mvd->gamedir, sizeof( mvd->gamedir ) ); Q_strncpyz( mvd->mapname, mvd_default_map->string, sizeof( mvd->mapname ) ); List_Init( &mvd->udpClients ); strcpy( mvd->configstrings[CS_NAME], "Waiting Room" ); strcpy( mvd->configstrings[CS_SKY], "unit1_" ); strcpy( mvd->configstrings[CS_MAXCLIENTS], "8" ); sprintf( mvd->configstrings[CS_MAPCHECKSUM], "%d", checksum ); strcpy( mvd->configstrings[CS_MODELS + 1], buffer ); mvd->dummy = &mvd_dummy; mvd->pm_type = PM_FREEZE; // set serverinfo variables SV_InfoSet( "mapname", mvd->mapname ); // SV_InfoSet( "gamedir", "gtv" ); SV_InfoSet( "gamename", "gtv" ); SV_InfoSet( "gamedate", __DATE__ ); } static void MVD_GameShutdown( void ) { Com_Printf( "----- MVD_GameShutdown -----\n" ); MVD_Shutdown(); mvd_ge.edicts = NULL; mvd_ge.edict_size = 0; mvd_ge.num_edicts = 0; mvd_ge.max_edicts = 0; } static void MVD_GameSpawnEntities( const char *mapname, const char *entstring, const char *spawnpoint ) { } static void MVD_GameWriteGame( const char *filename, qboolean autosave ) { } static void MVD_GameReadGame( const char *filename ) { } static void MVD_GameWriteLevel( const char *filename ) { } static void MVD_GameReadLevel( const char *filename ) { } static qboolean MVD_GameClientConnect( edict_t *ent, char *userinfo ) { udpClient_t *client = EDICT_MVDCL( ent ); client_t *cl = client->cl; mvd_t *mvd; int count; // assign them to some channel 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; // override server state cl->gamedir = mvd->gamedir; cl->mapname = mvd->mapname; cl->configstrings = ( char * )mvd->configstrings; cl->slot = mvd->clientNum; cl->cm = &mvd->cm; cl->pool = &mvd->pool; return qtrue; } static void MVD_GameClientBegin( edict_t *ent ) { udpClient_t *client = EDICT_MVDCL( ent ); mvd_t *mvd = client->mvd; mvd_player_t *target; client->floodTime = 0; client->floodHead = 0; memset( &client->lastcmd, 0, sizeof( client->lastcmd ) ); memset( &client->ps, 0, sizeof( client->ps ) ); client->jump_held = qfalse; if( !client->begin_time ) { MVD_BroadcastPrintf( mvd, PRINT_MEDIUM, UF_MUTE_MISC, "[MVD] %s entered the channel\n", client->cl->name ); target = MVD_MostFollowed( mvd ); } else { target = client->target; } client->oldtarget = NULL; client->begin_time = svs.realtime; MVD_SetDefaultLayout( client ); if( mvd->intermission ) { // force them to chase dummy MVD client client->target = mvd->dummy; MVD_SetFollowLayout( client ); MVD_UpdateClient( client ); } else if( target && target->inuse ) { // start normal chase cam mode MVD_FollowStart( client, target ); } else { // spawn the spectator VectorScale( mvd->spawnOrigin, 8, client->ps.pmove.origin ); VectorCopy( mvd->spawnAngles, client->ps.viewangles ); MVD_FollowStop( client ); } } static void MVD_GameClientUserinfoChanged( edict_t *ent, char *userinfo ) { udpClient_t *client = EDICT_MVDCL( ent ); char *s; float fov; s = Info_ValueForKey( userinfo, "uf" ); client->uf = atoi( s ); s = Info_ValueForKey( userinfo, "fov" ); fov = atof( s ); if( fov < 1 ) { fov = 90; } else if( fov > 160 ) { fov = 160; } client->fov = fov; if( client->uf & UF_LOCALFOV ) { client->ps.fov = fov; } } void MVD_GameClientNameChanged( edict_t *ent, const char *name ) { udpClient_t *client = EDICT_MVDCL( ent ); client_t *cl = client->cl; if( client->begin_time ) { MVD_BroadcastPrintf( client->mvd, PRINT_MEDIUM, UF_MUTE_MISC, "[MVD] %s changed name to %s\n", cl->name, name ); } } // called early from SV_Drop to prevent multiple disconnect messages void MVD_GameClientDrop( edict_t *ent, const char *reason ) { udpClient_t *client = EDICT_MVDCL( ent ); client_t *cl = client->cl; if( client->begin_time ) { MVD_BroadcastPrintf( client->mvd, PRINT_MEDIUM, UF_MUTE_MISC, "[MVD] %s was dropped: %s\n", cl->name, reason ); client->begin_time = 0; } } static void MVD_GameClientDisconnect( edict_t *ent ) { udpClient_t *client = EDICT_MVDCL( ent ); client_t *cl = client->cl; if( client->begin_time ) { MVD_BroadcastPrintf( client->mvd, PRINT_MEDIUM, UF_MUTE_MISC, "[MVD] %s disconnected\n", cl->name ); client->begin_time = 0; } } 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( ( cmd->buttons & ~old->buttons ) & BUTTON_ATTACK ) { MVD_Observe_f( client ); } if( client->target ) { if( cmd->upmove >= 10 ) { if( !client->jump_held ) { if( !client->mvd->intermission ) { MVD_FollowNext( client ); } client->jump_held = qtrue; } } else { client->jump_held = qfalse; } } 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_IntermissionStart( mvd_t *mvd ) { udpClient_t *client; // set this early so MVD_SetDefaultLayout works mvd->intermission = qtrue; // force all clients to switch to the MVD dummy // and open the scoreboard LIST_FOR_EACH( udpClient_t, client, &mvd->udpClients, entry ) { if( client->cl->state != cs_spawned ) { continue; } client->oldtarget = client->target; client->target = mvd->dummy; MVD_SetFollowLayout( client ); } } static void MVD_IntermissionStop( mvd_t *mvd ) { udpClient_t *client; mvd_player_t *target; // set this early so MVD_SetDefaultLayout works mvd->intermission = qfalse; // force all clients to switch to previous mode // and close the scoreboard LIST_FOR_EACH( udpClient_t, client, &mvd->udpClients, entry ) { if( client->cl->state != cs_spawned ) { continue; } if( client->layout_type == LAYOUT_SCORES ) { client->layout_type = 0; } target = client->oldtarget; if( target && target->inuse ) { // start normal chase cam mode MVD_FollowStart( client, target ); } else { MVD_FollowStop( client ); } client->oldtarget = NULL; } } static void MVD_GameRunFrame( void ) { mvd_t *mvd, *next; udpClient_t *u; tcpClient_t *t; uint16_t length; LIST_FOR_EACH_SAFE( mvd_t, mvd, next, &mvd_ready, ready ) { if( setjmp( mvd_jmpbuf ) ) { continue; } // parse stream if( mvd->state < MVD_READING ) { goto update; } if( !MVD_Parse( mvd ) ) { goto update; } // check for intermission if( mvd_freeze_hack->integer ) { if( !mvd->intermission ) { if( mvd->dummy->ps.pmove.pm_type == PM_FREEZE ) { MVD_IntermissionStart( mvd ); } } else if( mvd->dummy->ps.pmove.pm_type != PM_FREEZE ) { MVD_IntermissionStop( mvd ); } } else if( mvd->intermission ) { MVD_IntermissionStop( mvd ); } // 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->demorecording ); FS_Write( msg_read.data, msg_read.cursize, mvd->demorecording ); } update: MVD_UpdateLayouts( mvd ); } MVD_UpdateLayouts( &mvd_waitingRoom ); mvd_dirty = qfalse; } static void MVD_GameServerCommand( void ) { } void MVD_PrepWorldFrame( void ) { mvd_t *mvd; edict_t *ent; int i; // reset events and old origins LIST_FOR_EACH( mvd_t, mvd, &mvd_ready, ready ) { for( i = 1, ent = &mvd->edicts[1]; i < mvd->pool.num_edicts; i++, ent++ ) { if( !ent->inuse ) { continue; } if( !( ent->s.renderfx & RF_BEAM ) ) { VectorCopy( ent->s.origin, ent->s.old_origin ); } ent->s.event = 0; } } } 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 };