/* * CL_ParseServerMessage */ void CL_ParseServerMessage( msg_t *msg ) { int cmd; if( cl_shownet->integer == 1 ) { Com_Printf( "%i ", msg->cursize ); } else if( cl_shownet->integer >= 2 ) { Com_Printf( "------------------\n" ); } // parse the message while( 1 ) { if( msg->readcount > msg->cursize ) { Com_Error( ERR_DROP, "CL_ParseServerMessage: Bad server message" ); break; } cmd = MSG_ReadByte( msg ); if( cl_debug_serverCmd->integer & 4 ) { if( cmd == -1 ) Com_Printf( "%3i:CMD %i %s\n", msg->readcount-1, cmd, "EOF" ); else Com_Printf( "%3i:CMD %i %s\n", msg->readcount-1, cmd, !svc_strings[cmd] ? "bad" : svc_strings[cmd] ); } if( cmd == -1 ) { SHOWNET( msg, "END OF MESSAGE" ); break; } if( cl_shownet->integer >= 2 ) { if( !svc_strings[cmd] ) Com_Printf( "%3i:BAD CMD %i\n", msg->readcount-1, cmd ); else SHOWNET( msg, svc_strings[cmd] ); } // other commands switch( cmd ) { default: Com_Error( ERR_DROP, "CL_ParseServerMessage: Illegible server message" ); break; case svc_nop: // Com_Printf( "svc_nop\n" ); break; case svc_servercmd: if( !cls.reliable ) { int cmdNum = MSG_ReadLong( msg ); if( cmdNum < 0 ) { Com_Error( ERR_DROP, "CL_ParseServerMessage: Invalid cmdNum value received: %i\n", cmdNum ); return; } if( cmdNum <= cls.lastExecutedServerCommand ) { MSG_ReadString( msg ); // read but ignore break; } cls.lastExecutedServerCommand = cmdNum; } // fall through case svc_servercs: // configstrings from demo files. they don't have acknowledge CL_ParseServerCommand( msg ); break; case svc_serverdata: if( cls.state == CA_HANDSHAKE ) { Cbuf_Execute(); // make sure any stuffed commands are done CL_ParseServerData( msg ); } else { return; // ignore rest of the packet (serverdata is always sent alone) } break; case svc_spawnbaseline: CL_ParseBaseline( msg ); break; case svc_download: CL_ParseDownload( msg ); break; case svc_clcack: if( cls.reliable ) { Com_Error( ERR_DROP, "CL_ParseServerMessage: clack message for reliable client\n" ); return; } cls.reliableAcknowledge = (unsigned)MSG_ReadLong( msg ); cls.ucmdAcknowledged = (unsigned)MSG_ReadLong( msg ); if( cl_debug_serverCmd->integer & 4 ) Com_Printf( "svc_clcack:reliable cmd ack:%i ucmdack:%i\n", cls.reliableAcknowledge, cls.ucmdAcknowledged ); break; case svc_frame: CL_ParseFrame( msg ); break; case svc_demoinfo: assert( cls.demo.playing ); { size_t meta_data_maxsize; MSG_ReadLong( msg ); MSG_ReadLong( msg ); cls.demo.meta_data_realsize = (size_t)MSG_ReadLong( msg ); meta_data_maxsize = (size_t)MSG_ReadLong( msg ); // sanity check if( cls.demo.meta_data_realsize > meta_data_maxsize ) { cls.demo.meta_data_realsize = meta_data_maxsize; } if( cls.demo.meta_data_realsize > sizeof( cls.demo.meta_data ) ) { cls.demo.meta_data_realsize = sizeof( cls.demo.meta_data ); } MSG_ReadData( msg, cls.demo.meta_data, cls.demo.meta_data_realsize ); MSG_SkipData( msg, meta_data_maxsize - cls.demo.meta_data_realsize ); } break; case svc_playerinfo: case svc_packetentities: case svc_match: Com_Error( ERR_DROP, "Out of place frame data" ); break; case svc_extension: if( 1 ) { int ext, len; ext = MSG_ReadByte( msg ); // extension id MSG_ReadByte( msg ); // version number len = MSG_ReadShort( msg ); // command length switch( ext ) { default: // unsupported MSG_SkipData( msg, len ); break; } } break; } } CL_AddNetgraph(); // // if recording demos, copy the message out // // // we don't know if it is ok to save a demo message until // after we have parsed the frame // if( cls.demo.recording && !cls.demo.waiting ) CL_WriteDemoMessage( msg ); }
/* =================== CL_WritePacket Create and send the command packet to the server Including both the reliable commands and the usercmds During normal gameplay, a client packet will contain something like: 4 sequence number 2 qport 4 serverid 4 acknowledged sequence number 4 clc.serverCommandSequence <optional reliable commands> 1 clc_move or clc_moveNoDelta 1 command count <count * usercmds> =================== */ void CL_WritePacket( void ) { msg_t buf; byte data[MAX_MSGLEN]; int i, j; usercmd_t *cmd, *oldcmd; usercmd_t nullcmd; int packetNum; int oldPacketNum; int count, key; // don't send anything if playing back a demo if ( clc.demoplaying || clc.state == CA_CINEMATIC ) { return; } Com_Memset( &nullcmd, 0, sizeof(nullcmd) ); oldcmd = &nullcmd; MSG_Init( &buf, data, sizeof(data) ); MSG_Bitstream( &buf ); // write the current serverId so the server // can tell if this is from the current gameState MSG_WriteLong( &buf, cl.serverId ); // write the last message we received, which can // be used for delta compression, and is also used // to tell if we dropped a gamestate MSG_WriteLong( &buf, clc.serverMessageSequence ); // write the last reliable message we received MSG_WriteLong( &buf, clc.serverCommandSequence ); // write any unacknowledged clientCommands for ( i = clc.reliableAcknowledge + 1 ; i <= clc.reliableSequence ; i++ ) { MSG_WriteByte( &buf, clc_clientCommand ); MSG_WriteLong( &buf, i ); MSG_WriteString( &buf, clc.reliableCommands[ i & (MAX_RELIABLE_COMMANDS-1) ] ); } // we want to send all the usercmds that were generated in the last // few packet, so even if a couple packets are dropped in a row, // all the cmds will make it to the server if ( cl_packetdup->integer < 0 ) { Cvar_Set( "cl_packetdup", "0" ); } else if ( cl_packetdup->integer > 5 ) { Cvar_Set( "cl_packetdup", "5" ); } oldPacketNum = (clc.netchan.outgoingSequence - 1 - cl_packetdup->integer) & PACKET_MASK; count = cl.cmdNumber - cl.outPackets[ oldPacketNum ].p_cmdNumber; if ( count > MAX_PACKET_USERCMDS ) { count = MAX_PACKET_USERCMDS; Com_Printf("MAX_PACKET_USERCMDS\n"); } #ifdef USE_VOIP if (clc.voipOutgoingDataSize > 0) { if((clc.voipFlags & VOIP_SPATIAL) || Com_IsVoipTarget(clc.voipTargets, sizeof(clc.voipTargets), -1)) { MSG_WriteByte (&buf, clc_voip); MSG_WriteByte (&buf, clc.voipOutgoingGeneration); MSG_WriteLong (&buf, clc.voipOutgoingSequence); MSG_WriteByte (&buf, clc.voipOutgoingDataFrames); MSG_WriteData (&buf, clc.voipTargets, sizeof(clc.voipTargets)); MSG_WriteByte(&buf, clc.voipFlags); MSG_WriteShort (&buf, clc.voipOutgoingDataSize); MSG_WriteData (&buf, clc.voipOutgoingData, clc.voipOutgoingDataSize); // If we're recording a demo, we have to fake a server packet with // this VoIP data so it gets to disk; the server doesn't send it // back to us, and we might as well eliminate concerns about dropped // and misordered packets here. if(clc.demorecording && !clc.demowaiting) { const int voipSize = clc.voipOutgoingDataSize; msg_t fakemsg; byte fakedata[MAX_MSGLEN]; MSG_Init (&fakemsg, fakedata, sizeof (fakedata)); MSG_Bitstream (&fakemsg); MSG_WriteLong (&fakemsg, clc.reliableAcknowledge); MSG_WriteByte (&fakemsg, svc_voip); MSG_WriteShort (&fakemsg, clc.clientNum); MSG_WriteByte (&fakemsg, clc.voipOutgoingGeneration); MSG_WriteLong (&fakemsg, clc.voipOutgoingSequence); MSG_WriteByte (&fakemsg, clc.voipOutgoingDataFrames); MSG_WriteShort (&fakemsg, clc.voipOutgoingDataSize ); MSG_WriteBits (&fakemsg, clc.voipFlags, VOIP_FLAGCNT); MSG_WriteData (&fakemsg, clc.voipOutgoingData, voipSize); MSG_WriteByte (&fakemsg, svc_EOF); CL_WriteDemoMessage (&fakemsg, 0); } clc.voipOutgoingSequence += clc.voipOutgoingDataFrames; clc.voipOutgoingDataSize = 0; clc.voipOutgoingDataFrames = 0; } else { // We have data, but no targets. Silently discard all data clc.voipOutgoingDataSize = 0; clc.voipOutgoingDataFrames = 0; } } #endif if ( count >= 1 ) { if ( cl_showSend->integer ) { Com_Printf( "(%i)", count ); } // begin a client move command if ( cl_nodelta->integer || !cl.snap.valid || clc.demowaiting || clc.serverMessageSequence != cl.snap.messageNum ) { MSG_WriteByte (&buf, clc_moveNoDelta); } else { MSG_WriteByte (&buf, clc_move); } // write the command count MSG_WriteByte( &buf, count ); // use the checksum feed in the key key = clc.checksumFeed; // also use the message acknowledge key ^= clc.serverMessageSequence; // also use the last acknowledged server command in the key key ^= MSG_HashKey(clc.serverCommands[ clc.serverCommandSequence & (MAX_RELIABLE_COMMANDS-1) ], 32); // write all the commands, including the predicted command for ( i = 0 ; i < count ; i++ ) { j = (cl.cmdNumber - count + i + 1) & CMD_MASK; cmd = &cl.cmds[j]; MSG_WriteDeltaUsercmdKey (&buf, key, oldcmd, cmd); oldcmd = cmd; } } // // deliver the message // packetNum = clc.netchan.outgoingSequence & PACKET_MASK; cl.outPackets[ packetNum ].p_realtime = cls.realtime; cl.outPackets[ packetNum ].p_serverTime = oldcmd->serverTime; cl.outPackets[ packetNum ].p_cmdNumber = cl.cmdNumber; clc.lastPacketSentTime = cls.realtime; if ( cl_showSend->integer ) { Com_Printf( "%i ", buf.cursize ); } CL_Netchan_Transmit (&clc.netchan, &buf); }
/* ===================== CL_ParseServerMessage ===================== */ void CL_ParseServerMessage( sizebuf_t *msg ) { char *s; int i, j, cmd; int param1, param2; int bufStart; cls_message_debug.parsing = true; // begin parsing starting_count = BF_GetNumBytesRead( msg ); // updates each frame // parse the message while( 1 ) { if( BF_CheckOverflow( msg )) { Host_Error( "CL_ParseServerMessage: overflow!\n" ); return; } // mark start position bufStart = BF_GetNumBytesRead( msg ); // end of message if( BF_GetNumBitsLeft( msg ) < 8 ) break; cmd = BF_ReadByte( msg ); // record command for debugging spew on parse problem CL_Parse_RecordCommand( cmd, bufStart ); // other commands switch( cmd ) { case svc_bad: Host_Error( "svc_bad\n" ); break; case svc_nop: // this does nothing break; case svc_disconnect: CL_Drop (); Host_AbortCurrentFrame (); break; case svc_changing: if( BF_ReadOneBit( msg )) { cls.changelevel = true; S_StopAllSounds(); if( cls.demoplayback ) { SCR_BeginLoadingPlaque( cl.background ); cls.changedemo = true; } } else MsgDev( D_INFO, "Server disconnected, reconnecting\n" ); CL_ClearState (); CL_InitEdicts (); // re-arrange edicts if( cls.demoplayback ) { cl.background = (cls.demonum != -1) ? true : false; cls.state = ca_connected; } else cls.state = ca_connecting; cls.connect_time = MAX_HEARTBEAT; // CL_CheckForResend() will fire immediately break; case svc_setview: cl.refdef.viewentity = BF_ReadWord( msg ); break; case svc_sound: CL_ParseSoundPacket( msg, false ); break; case svc_time: // shuffle timestamps cl.mtime[1] = cl.mtime[0]; cl.mtime[0] = BF_ReadFloat( msg ); break; case svc_print: i = BF_ReadByte( msg ); MsgDev( D_INFO, "^6%s", BF_ReadString( msg )); if( i == PRINT_CHAT ) S_StartLocalSound( "common/menu2.wav", VOL_NORM, false ); break; case svc_stufftext: s = BF_ReadString( msg ); Cbuf_AddText( s ); break; case svc_lightstyle: CL_ParseLightStyle( msg ); break; case svc_setangle: CL_ParseSetAngle( msg ); break; case svc_serverdata: Cbuf_Execute(); // make sure any stuffed commands are done CL_ParseServerData( msg ); break; case svc_addangle: CL_ParseAddAngle( msg ); break; case svc_clientdata: CL_ParseClientData( msg ); break; case svc_packetentities: CL_ParsePacketEntities( msg, false ); break; case svc_deltapacketentities: CL_ParsePacketEntities( msg, true ); break; case svc_updatepings: CL_UpdateUserPings( msg ); break; case svc_usermessage: CL_RegisterUserMessage( msg ); break; case svc_particle: CL_ParseParticles( msg ); break; case svc_restoresound: CL_ParseRestoreSoundPacket( msg ); break; case svc_spawnstatic: CL_ParseStaticEntity( msg ); break; case svc_ambientsound: CL_ParseSoundPacket( msg, true ); break; case svc_crosshairangle: CL_ParseCrosshairAngle( msg ); break; case svc_spawnbaseline: CL_ParseBaseline( msg ); break; case svc_temp_entity: CL_ParseTempEntity( msg ); break; case svc_setpause: cl.refdef.paused = ( BF_ReadOneBit( msg ) != 0 ); break; case svc_deltamovevars: CL_ParseMovevars( msg ); break; case svc_customization: CL_ParseCustomization( msg ); break; case svc_centerprint: CL_CenterPrint( BF_ReadString( msg ), 0.25f ); break; case svc_event: CL_ParseEvent( msg ); break; case svc_event_reliable: CL_ParseReliableEvent( msg ); break; case svc_updateuserinfo: CL_UpdateUserinfo( msg ); break; case svc_intermission: cl.refdef.intermission = true; break; case svc_modelindex: CL_PrecacheModel( msg ); break; case svc_soundindex: CL_PrecacheSound( msg ); break; case svc_soundfade: CL_ParseSoundFade( msg ); break; case svc_cdtrack: param1 = BF_ReadByte( msg ); param1 = bound( 1, param1, MAX_CDTRACKS ); // tracknum param2 = BF_ReadByte( msg ); param2 = bound( 1, param2, MAX_CDTRACKS ); // loopnum S_StartBackgroundTrack( clgame.cdtracks[param1-1], clgame.cdtracks[param2-1], 0 ); break; case svc_serverinfo: CL_ServerInfo( msg ); break; case svc_eventindex: CL_PrecacheEvent( msg ); break; case svc_deltatable: Delta_ParseTableField( msg ); break; case svc_weaponanim: param1 = BF_ReadByte( msg ); // iAnim param2 = BF_ReadByte( msg ); // body CL_WeaponAnim( param1, param2 ); break; case svc_bspdecal: CL_ParseStaticDecal( msg ); break; case svc_roomtype: param1 = BF_ReadShort( msg ); Cvar_SetFloat( "room_type", param1 ); break; case svc_chokecount: i = BF_ReadByte( msg ); j = cls.netchan.incoming_acknowledged - 1; for( ; i > 0 && j > cls.netchan.outgoing_sequence - CL_UPDATE_BACKUP; j-- ) { if( cl.frames[j & CL_UPDATE_MASK].receivedtime != -3.0 ) { cl.frames[j & CL_UPDATE_MASK].receivedtime = -2.0; i--; } } break; case svc_resourcelist: CL_ParseResourceList( msg ); break; case svc_director: CL_ParseDirector( msg ); break; case svc_studiodecal: CL_ParseStudioDecal( msg ); break; case svc_querycvarvalue: CL_ParseCvarValue( msg ); break; case svc_querycvarvalue2: CL_ParseCvarValue2( msg ); break; default: CL_ParseUserMessage( msg, cmd ); break; } } cls_message_debug.parsing = false; // done // we don't know if it is ok to save a demo message until // after we have parsed the frame if( !cls.demoplayback ) { if( cls.demorecording && !cls.demowaiting ) { CL_WriteDemoMessage( false, starting_count, msg ); } else if( cls.state != ca_active ) { CL_WriteDemoMessage( true, starting_count, msg ); } } }
/** * Handles recording and playback of demos, on top of NET_ code */ int CL_GetMessage(void) { int r = 0, i; float f; if (cls.demoplayback) { // decide if it is time to grab the next message if (cls.signon == SIGNONS) // allways grab until fully connected { if (cls.timedemo) { if (host_framecount == cls.td_lastframe) return 0; // allready read this frame's message cls.td_lastframe = host_framecount; // if this is the second frame, grab the real td_starttime // so the bogus time on the first frame doesn't count if (host_framecount == cls.td_startframe + 1) cls.td_starttime = realtime; } else if (/* cl.time > 0 && */ cl.time <= cl.mtime[0]) { return 0; // don't need another message yet } } // get the next message fread(&net_message.cursize, 4, 1, cls.demofile); VectorCopy(cl.mviewangles[0], cl.mviewangles[1]); for (i = 0; i < 3; i++) { r = fread(&f, 4, 1, cls.demofile); cl.mviewangles[0][i] = LittleFloat(f); } net_message.cursize = LittleLong(net_message.cursize); if (net_message.cursize > MAX_MSGLEN) { Sys_Error("Demo message > MAX_MSGLEN"); return 0; } r = fread(net_message.data, net_message.cursize, 1, cls.demofile); if (r != 1) { CL_StopPlayback(); return 0; } return 1; } while (1) { r = NET_GetMessage(cls.netcon); if (r != 1 && r != 2) return r; // discard nop keepalive message if (net_message.cursize == 1 && net_message.data[0] == svc_nop) Con_Printf("<-- server to client keepalive\n"); else break; } if (cls.demorecording) CL_WriteDemoMessage(); return r; }
/* =================== CL_WritePacket Create and send the command packet to the server Including both the reliable commands and the usercmds A client packet will contain something like: 4 sequence number 2 qport 4 serverid 4 acknowledged sequence number 4 clc.serverCommandSequence <optional reliable commands> 1 clc_move or clc_moveNoDelta 1 command count <count * usercmds> =================== */ void CL_WritePacket() { msg_t buf; byte data[ MAX_MSGLEN ]; int i, j; usercmd_t *cmd, *oldcmd; usercmd_t nullcmd; int packetNum; int oldPacketNum; int count; // don't send anything if playing back a demo if ( clc.demoplaying || cls.state == CA_CINEMATIC ) { return; } memset( &nullcmd, 0, sizeof( nullcmd ) ); oldcmd = &nullcmd; MSG_Init( &buf, data, sizeof( data ) ); MSG_Bitstream( &buf ); // write the current serverId so the server // can tell if this is from the current gameState MSG_WriteLong( &buf, cl.serverId ); // write the last message we received, which can // be used for delta compression, and is also used // to tell if we dropped a gamestate MSG_WriteLong( &buf, clc.serverMessageSequence ); // write the last reliable message we received MSG_WriteLong( &buf, clc.serverCommandSequence ); // write any unacknowledged clientCommands // NOTE TTimo: if you verbose this, you will see that there are quite a few duplicates // typically several unacknowledged cp or userinfo commands stacked up for ( i = clc.reliableAcknowledge + 1; i <= clc.reliableSequence; i++ ) { MSG_WriteByte( &buf, clc_clientCommand ); MSG_WriteLong( &buf, i ); MSG_WriteString( &buf, clc.reliableCommands[ i & ( MAX_RELIABLE_COMMANDS - 1 ) ] ); } // we want to send all the usercmds that were generated in the last // few packet, so even if a couple packets are dropped in a row, // all the cmds will make it to the server if ( cl_packetdup->integer < 0 ) { Cvar_Set( "cl_packetdup", "0" ); } else if ( cl_packetdup->integer > 5 ) { Cvar_Set( "cl_packetdup", "5" ); } oldPacketNum = ( clc.netchan.outgoingSequence - 1 - cl_packetdup->integer ) & PACKET_MASK; count = cl.cmdNumber - cl.outPackets[ oldPacketNum ].p_cmdNumber; if ( count > MAX_PACKET_USERCMDS ) { count = MAX_PACKET_USERCMDS; Com_Printf( "MAX_PACKET_USERCMDS" ); } #ifdef USE_VOIP if ( clc.voipOutgoingDataSize > 0 ) { if ( ( clc.voipFlags & VOIP_SPATIAL ) || Com_IsVoipTarget( clc.voipTargets, sizeof( clc.voipTargets ), -1 ) ) { MSG_WriteByte( &buf, clc_voip ); MSG_WriteByte( &buf, clc.voipOutgoingGeneration ); MSG_WriteLong( &buf, clc.voipOutgoingSequence ); MSG_WriteByte( &buf, clc.voipOutgoingDataFrames ); MSG_WriteData( &buf, clc.voipTargets, sizeof( clc.voipTargets ) ); MSG_WriteByte( &buf, clc.voipFlags ); MSG_WriteShort( &buf, clc.voipOutgoingDataSize ); MSG_WriteData( &buf, clc.voipOutgoingData, clc.voipOutgoingDataSize ); // If we're recording a demo, we have to fake a server packet with // this VoIP data so it gets to disk; the server doesn't send it // back to us, and we might as well eliminate concerns about dropped // and misordered packets here. if ( clc.demorecording && !clc.demowaiting ) { const int voipSize = clc.voipOutgoingDataSize; msg_t fakemsg; byte fakedata[ MAX_MSGLEN ]; MSG_Init( &fakemsg, fakedata, sizeof( fakedata ) ); MSG_Bitstream( &fakemsg ); MSG_WriteLong( &fakemsg, clc.reliableAcknowledge ); MSG_WriteByte( &fakemsg, svc_voip ); MSG_WriteShort( &fakemsg, clc.clientNum ); MSG_WriteByte( &fakemsg, clc.voipOutgoingGeneration ); MSG_WriteLong( &fakemsg, clc.voipOutgoingSequence ); MSG_WriteByte( &fakemsg, clc.voipOutgoingDataFrames ); MSG_WriteShort( &fakemsg, clc.voipOutgoingDataSize ); MSG_WriteBits( &fakemsg, clc.voipFlags, VOIP_FLAGCNT ); MSG_WriteData( &fakemsg, clc.voipOutgoingData, voipSize ); MSG_WriteByte( &fakemsg, svc_EOF ); CL_WriteDemoMessage( &fakemsg, 0 ); } clc.voipOutgoingSequence += clc.voipOutgoingDataFrames; clc.voipOutgoingDataSize = 0; clc.voipOutgoingDataFrames = 0; } else { // We have data, but no targets. Silently discard all data clc.voipOutgoingDataSize = 0; clc.voipOutgoingDataFrames = 0; } } #endif if ( count >= 1 ) { if ( cl_showSend->integer ) { Com_Printf( "(%i)", count ); } // begin a client move command if ( cl_nodelta->integer || !cl.snap.valid || clc.demowaiting || clc.serverMessageSequence != cl.snap.messageNum ) { MSG_WriteByte( &buf, clc_moveNoDelta ); } else { MSG_WriteByte( &buf, clc_move ); } // write the command count MSG_WriteByte( &buf, count ); // write all the commands, including the predicted command for ( i = 0; i < count; i++ ) { j = ( cl.cmdNumber - count + i + 1 ) & CMD_MASK; cmd = &cl.cmds[ j ]; MSG_WriteDeltaUsercmd( &buf, oldcmd, cmd ); oldcmd = cmd; } } // // deliver the message // packetNum = clc.netchan.outgoingSequence & PACKET_MASK; cl.outPackets[ packetNum ].p_realtime = cls.realtime; cl.outPackets[ packetNum ].p_serverTime = oldcmd->serverTime; cl.outPackets[ packetNum ].p_cmdNumber = cl.cmdNumber; clc.lastPacketSentTime = cls.realtime; if ( cl_showSend->integer ) { Com_Printf("%i ", buf.cursize ); } MSG_WriteByte( &buf, clc_EOF ); CL_WriteBinaryMessage( &buf ); Netchan_Transmit( &clc.netchan, buf.cursize, buf.data ); // clients never really should have messages large enough // to fragment, but in case they do, fire them all off // at once // TTimo: this causes a packet burst, which is bad karma for winsock // added a WARNING message, we'll see if there are legit situations where this happens while ( clc.netchan.unsentFragments ) { if ( cl_showSend->integer ) { Com_Printf( "WARNING: unsent fragments (not supposed to happen!)" ); } Netchan_TransmitNextFragment( &clc.netchan ); } }