// Emacs style mode select -*- C++ -*- //----------------------------------------------------------------------------- // // $Id:$ // // Copyright (C) 1993-1996 by id Software, Inc. // // This source is available for distribution and/or modification // only under the terms of the DOOM Source Code License as // published by id Software. All rights reserved. // // The source is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // FITNESS FOR A PARTICULAR PURPOSE. See the DOOM Source Code License // for more details. // // $Log:$ // // DESCRIPTION: // DOOM Network game communication and protocol, // all OS independent parts. // //----------------------------------------------------------------------------- #include #include "version.h" #include "m_alloc.h" #include "m_menu.h" #include "m_random.h" #include "i_system.h" #include "i_video.h" #include "i_net.h" #include "g_game.h" #include "doomdef.h" #include "doomstat.h" #include "c_console.h" #include "d_netinf.h" #include "cmdlib.h" #include "s_sound.h" #include "m_cheat.h" #include "p_effect.h" #include "p_local.h" #include "c_dispatch.h" #include "sbar.h" #include "gi.h" #include "m_misc.h" #include "gameconfigfile.h" #include "d_gui.h" #include "templates.h" #include "p_acs.h" #include "p_trace.h" #include "a_sharedglobal.h" int P_StartScript (AActor *who, line_t *where, int script, char *map, bool backSide, int arg0, int arg1, int arg2, int always, bool wantResultCode, bool net); //#define SIMULATEERRORS (RAND_MAX/3) #define SIMULATEERRORS 0 #define NCMD_EXIT 0x80 #define NCMD_RETRANSMIT 0x40 #define NCMD_SETUP 0x20 #define NCMD_MULTI 0x10 // multiple players in this packet #define NCMD_QUITTERS 0x08 // one or more players just quit (packet server only) #define NCMD_XTICS 0x03 // packet contains >2 tics #define NCMD_2TICS 0x02 // packet contains 2 tics #define NCMD_1TICS 0x01 // packet contains 1 tic #define NCMD_0TICS 0x00 // packet contains 0 tics // [RH] // New generic packet structure: // // Header: // One byte with above flags. // One byte with starttic // One byte with master's maketic (master -> slave only!) // If NCMD_RETRANSMIT set, one byte with retransmitfrom // If NCMD_XTICS set, one byte with number of tics (minus 3, so theoretically up to 258 tics in one packet) // If NCMD_QUITTERS, one byte with number of players followed by one byte with each player's consolenum // If NCMD_MULTI, one byte with number of players followed by one byte with each player's consolenum // - The first player's consolenum is not included in this list, because it always matches the sender // // For each tic: // Two bytes with consistancy check, followed by tic data // // Setup packets are different, and are described just before D_ArbitrateNetStart(). extern byte *demo_p; // [RH] Special "ticcmds" get recorded in demos extern char savedescription[SAVESTRINGSIZE]; extern FString savegamefile; extern short consistancy[MAXPLAYERS][BACKUPTICS]; doomcom_t doomcom; #define netbuffer (doomcom.data) enum { NET_PeerToPeer, NET_PacketServer }; BYTE NetMode = NET_PeerToPeer; // // NETWORKING // // gametic is the tic about to (or currently being) run // maketic is the tick that hasn't had control made for it yet // nettics[] has the maketics for all players // // a gametic cannot be run until nettics[] > gametic for all players // #define RESENDCOUNT 10 #define PL_DRONE 0x80 // bit flag in doomdata->player ticcmd_t localcmds[LOCALCMDTICS]; FDynamicBuffer NetSpecs[MAXPLAYERS][BACKUPTICS]; ticcmd_t netcmds[MAXPLAYERS][BACKUPTICS]; int nettics[MAXNETNODES]; BOOL nodeingame[MAXNETNODES]; // set false as nodes leave game bool nodejustleft[MAXNETNODES]; // set when a node just left BOOL remoteresend[MAXNETNODES]; // set when local needs tics int resendto[MAXNETNODES]; // set when remote needs tics int resendcount[MAXNETNODES]; unsigned int lastrecvtime[MAXPLAYERS]; // [RH] Used for pings unsigned int currrecvtime[MAXPLAYERS]; int nodeforplayer[MAXPLAYERS]; int playerfornode[MAXNETNODES]; int maketic; int skiptics; int ticdup; void D_ProcessEvents (void); void G_BuildTiccmd (ticcmd_t *cmd); void D_DoAdvanceDemo (void); static void SendSetup (DWORD playersdetected[MAXNETNODES], BYTE gotsetup[MAXNETNODES], int len); int reboundpacket; BYTE reboundstore[MAX_MSGLEN]; int frameon; int frameskip[4]; int oldnettics; int mastertics; static int entertic; static int oldentertics; extern BOOL advancedemo; CVAR (Bool, cl_capfps, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) // [RH] Special "ticcmds" get stored in here static struct TicSpecial { byte *streams[BACKUPTICS]; size_t used[BACKUPTICS]; byte *streamptr; size_t streamoffs; int specialsize; int lastmaketic; BOOL okay; TicSpecial () { int i; lastmaketic = -1; specialsize = 256; for (i = 0; i < BACKUPTICS; i++) streams[i] = NULL; for (i = 0; i < BACKUPTICS; i++) { streams[i] = (byte *)M_Malloc (256); used[i] = 0; } okay = true; } ~TicSpecial () { int i; for (i = 0; i < BACKUPTICS; i++) { if (streams[i]) { free (streams[i]); streams[i] = NULL; used[i] = 0; } } okay = false; } // Make more room for special commands. void GetMoreSpace () { int i; specialsize <<= 1; DPrintf ("Expanding special size to %d\n", specialsize); for (i = 0; i < BACKUPTICS; i++) streams[i] = (byte *)M_Realloc (streams[i], specialsize); streamptr = streams[(maketic/ticdup)%BACKUPTICS] + streamoffs; } void CheckSpace (size_t needed) { if (streamoffs >= specialsize - needed) GetMoreSpace (); streamoffs += needed; } void NewMakeTic () { int mt = maketic / ticdup; if (lastmaketic != -1) { if (lastmaketic == mt) return; used[lastmaketic%BACKUPTICS] = streamoffs; } lastmaketic = mt; streamptr = streams[mt%BACKUPTICS]; streamoffs = 0; } TicSpecial &operator << (byte it) { if (streamptr) { CheckSpace (1); WriteByte (it, &streamptr); } return *this; } TicSpecial &operator << (short it) { if (streamptr) { CheckSpace (2); WriteWord (it, &streamptr); } return *this; } TicSpecial &operator << (int it) { if (streamptr) { CheckSpace (4); WriteLong (it, &streamptr); } return *this; } TicSpecial &operator << (float it) { if (streamptr) { CheckSpace (4); WriteFloat (it, &streamptr); } return *this; } TicSpecial &operator << (const char *it) { if (streamptr) { CheckSpace (strlen (it) + 1); WriteString (it, &streamptr); } return *this; } } specials; void Net_ClearBuffers () { int i, j; memset (localcmds, 0, sizeof(localcmds)); memset (netcmds, 0, sizeof(netcmds)); memset (nettics, 0, sizeof(nettics)); memset (nodeingame, 0, sizeof(nodeingame)); memset (remoteresend, 0, sizeof(remoteresend)); memset (resendto, 0, sizeof(resendto)); memset (resendcount, 0, sizeof(resendcount)); memset (lastrecvtime, 0, sizeof(lastrecvtime)); memset (currrecvtime, 0, sizeof(currrecvtime)); memset (consistancy, 0, sizeof(consistancy)); nodeingame[0] = true; for (i = 0; i < MAXPLAYERS; i++) { for (j = 0; j < BACKUPTICS; j++) { NetSpecs[i][j].SetData (NULL, 0); } } oldentertics = entertic; gametic = 0; maketic = 0; } // // [RH] Rewritten to properly calculate the packet size // with our variable length commands. // int NetbufferSize () { if (netbuffer[0] & (NCMD_EXIT | NCMD_SETUP)) { return doomcom.datalength; } int k = 2, count, numtics; if (netbuffer[0] & NCMD_RETRANSMIT) k++; if (NetMode == NET_PacketServer && doomcom.remotenode == nodeforplayer[Net_Arbitrator]) k++; numtics = netbuffer[0] & NCMD_XTICS; if (numtics == 3) { numtics += netbuffer[k++]; } if (netbuffer[0] & NCMD_QUITTERS) { k += netbuffer[k] + 1; } if (netbuffer[0] & NCMD_MULTI) { count = netbuffer[k]; k += count; } else { count = 1; } // Need at least 3 bytes per tic per player if (doomcom.datalength < k + 3 * count * numtics) { return k + 3 * count * numtics; } byte *skipper = &netbuffer[k]; if ((netbuffer[0] & NCMD_EXIT) == 0) { while (count-- > 0) { SkipTicCmd (&skipper, numtics); } } return skipper - netbuffer; } // // // int ExpandTics (int low) { int delta; int mt = maketic / ticdup; delta = low - (mt&0xff); if (delta >= -64 && delta <= 64) return (mt&~0xff) + low; if (delta > 64) return (mt&~0xff) - 256 + low; if (delta < -64) return (mt&~0xff) + 256 + low; I_Error ("ExpandTics: strange value %i at maketic %i", low, maketic); return 0; } // // HSendPacket // void HSendPacket (int node, int len) { if (debugfile && node != 0) { int i, k, realretrans; if (netbuffer[0] & NCMD_SETUP) { fprintf (debugfile,"%i/%i send %i = SETUP [%3i]", gametic, maketic, node, len); for (i = 0; i < len; i++) fprintf (debugfile," %2x", ((byte *)netbuffer)[i]); } else if (netbuffer[0] & NCMD_EXIT) { fprintf (debugfile,"%i/%i send %i = EXIT [%3i]", gametic, maketic, node, len); for (i = 0; i < len; i++) fprintf (debugfile," %2x", ((byte *)netbuffer)[i]); } else { k = 2; if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator && node != 0) { k++; } if (netbuffer[0] & NCMD_RETRANSMIT) realretrans = ExpandTics (netbuffer[k++]); else realretrans = -1; int numtics = netbuffer[0] & 3; if (numtics == 3) numtics += netbuffer[k++]; fprintf (debugfile,"%i/%i send %i = (%i + %i, R %i) [%3i]", gametic, maketic, node, ExpandTics(netbuffer[1]), numtics, realretrans, len); for (i = 0; i < len; i++) fprintf (debugfile, "%c%2x", i==k?'|':' ', ((byte *)netbuffer)[i]); } fprintf (debugfile, " [[ "); for (i = 0; i < doomcom.numnodes; ++i) { if (nodeingame[i]) { fprintf (debugfile, "%d ", nettics[i]); } else { fprintf (debugfile, "--- "); } } fprintf (debugfile, "]]\n"); } if (node == 0) { memcpy (reboundstore, netbuffer, len); reboundpacket = len; return; } if (demoplayback) return; if (!netgame) I_Error ("Tried to transmit to another node"); #if SIMULATEERRORS if (rand() < SIMULATEERRORS) { if (debugfile) fprintf (debugfile, "Drop!\n"); return; } #endif doomcom.command = CMD_SEND; doomcom.remotenode = node; doomcom.datalength = len; I_NetCmd (); } // // HGetPacket // Returns false if no packet is waiting // BOOL HGetPacket (void) { if (reboundpacket) { memcpy (netbuffer, reboundstore, reboundpacket); doomcom.remotenode = 0; reboundpacket = 0; return true; } if (!netgame) return false; if (demoplayback) return false; doomcom.command = CMD_GET; I_NetCmd (); if (doomcom.remotenode == -1) return false; if (debugfile) { int i, k, realretrans; if (netbuffer[0] & NCMD_SETUP) { fprintf (debugfile,"%i/%i get %i = SETUP [%3i]", gametic, maketic, doomcom.remotenode, doomcom.datalength); for (i = 0; i < doomcom.datalength; i++) fprintf (debugfile, " %2x", ((byte *)netbuffer)[i]); fprintf (debugfile, "\n"); } else if (netbuffer[0] & NCMD_EXIT) { fprintf (debugfile,"%i/%i get %i = EXIT [%3i]", gametic, maketic, doomcom.remotenode, doomcom.datalength); for (i = 0; i < doomcom.datalength; i++) fprintf (debugfile, " %2x", ((byte *)netbuffer)[i]); fprintf (debugfile, "\n"); } else { k = 2; if (NetMode == NET_PacketServer && doomcom.remotenode == nodeforplayer[Net_Arbitrator]) { k++; } if (netbuffer[0] & NCMD_RETRANSMIT) realretrans = ExpandTics (netbuffer[k++]); else realretrans = -1; int numtics = netbuffer[0] & 3; if (numtics == 3) numtics += netbuffer[k++]; fprintf (debugfile,"%i/%i get %i = (%i + %i, R %i) [%3i]", gametic, maketic, doomcom.remotenode, ExpandTics(netbuffer[1]), numtics, realretrans, doomcom.datalength); for (i = 0; i < doomcom.datalength; i++) fprintf (debugfile, "%c%2x", i==k?'|':' ', ((byte *)netbuffer)[i]); if (numtics) fprintf (debugfile, " <<%4x>>\n", consistancy[playerfornode[doomcom.remotenode]][nettics[doomcom.remotenode]%BACKUPTICS] & 0xFFFF); else fprintf (debugfile, "\n"); } } if (doomcom.datalength != NetbufferSize ()) { if (debugfile) fprintf (debugfile,"---bad packet length %i (calculated %i)\n", doomcom.datalength, NetbufferSize()); return false; } return true; } void PlayerIsGone (int netnode, int netconsole) { int i; for (i = netnode + 1; i < doomcom.numnodes; ++i) { if (nodeingame[i]) break; } if (i == doomcom.numnodes) { doomcom.numnodes = netnode; } nodeingame[netnode] = false; playeringame[netconsole] = false; nodejustleft[netnode] = false; if (deathmatch) { Printf ("%s left the game with %d frags\n", players[netconsole].userinfo.netname, players[netconsole].fragcount); } else { Printf ("%s left the game\n", players[netconsole].userinfo.netname); } // [RH] Revert to your own view if spying through the player who left if (players[consoleplayer].camera == players[netconsole].mo) { players[consoleplayer].camera = players[consoleplayer].mo; if (StatusBar != NULL) { StatusBar->AttachToPlayer (&players[consoleplayer]); } } // [RH] Make the player disappear FBehavior::StaticStopMyScripts (players[netconsole].mo); if (players[netconsole].mo != NULL) { P_DisconnectEffect (players[netconsole].mo); players[netconsole].mo->Destroy (); players[netconsole].mo = NULL; players[netconsole].camera = NULL; } // [RH] Let the scripts know the player left FBehavior::StaticStartTypedScripts (SCRIPT_Disconnect, NULL, true, netconsole); if (netconsole == Net_Arbitrator) { bglobal.RemoveAllBots (true); Printf ("Removed all bots\n"); // Pick a new network arbitrator for (int i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] && !players[i].isbot) { Net_Arbitrator = i; Printf ("%s is the new arbitrator\n", players[i].userinfo.netname); break; } } if (debugfile && NetMode == NET_PacketServer) { if (Net_Arbitrator == consoleplayer) { fprintf (debugfile, "I am the new master!\n"); } else { fprintf (debugfile, "Node %d is the new master!\n", nodeforplayer[Net_Arbitrator]); } } } if (demorecording) { G_CheckDemoStatus (); //WriteByte (DEM_DROPPLAYER, &demo_p); //WriteByte ((byte)netconsole, &demo_p); } } // // GetPackets // void GetPackets (void) { int netconsole; int netnode; int realend; int realstart; int numtics; int retransmitfrom; int k; BYTE playerbytes[MAXNETNODES]; int numplayers; while ( HGetPacket() ) { if (netbuffer[0] & NCMD_SETUP) { if (consoleplayer == Net_Arbitrator) { // This player apparantly doesn't realise the game has started netbuffer[0] = NCMD_SETUP+3; HSendPacket (doomcom.remotenode, 1); } continue; // extra setup packet } netnode = doomcom.remotenode; netconsole = playerfornode[netnode] & ~PL_DRONE; // [RH] Get "ping" times lastrecvtime[netconsole] = currrecvtime[netconsole]; currrecvtime[netconsole] = I_MSTime (); // check for exiting the game if (netbuffer[0] & NCMD_EXIT) { if (!nodeingame[netnode]) continue; if (NetMode != NET_PacketServer || netconsole == Net_Arbitrator) { PlayerIsGone (netnode, netconsole); if (NetMode == NET_PacketServer) { BYTE *foo = &netbuffer[2]; for (int i = 0; i < MAXPLAYERS; ++i) { if (playeringame[i]) { int resend = ReadLong (&foo); if (i != consoleplayer) { resendto[nodeforplayer[i]] = resend; } } } } } else { nodeingame[netnode] = false; playeringame[netconsole] = false; nodejustleft[netnode] = true; } continue; } k = 2; if (NetMode == NET_PacketServer && netconsole == Net_Arbitrator && netconsole != consoleplayer) { mastertics = ExpandTics (netbuffer[k++]); } if (netbuffer[0] & NCMD_RETRANSMIT) { retransmitfrom = netbuffer[k++]; } else { retransmitfrom = 0; } numtics = (netbuffer[0] & NCMD_XTICS); if (numtics == 3) { numtics += netbuffer[k++]; } if (netbuffer[0] & NCMD_QUITTERS) { numplayers = netbuffer[k++]; for (int i = 0; i < numplayers; ++i) { PlayerIsGone (nodeforplayer[netbuffer[k]], netbuffer[k]); k++; } } playerbytes[0] = netconsole; if (netbuffer[0] & NCMD_MULTI) { numplayers = netbuffer[k++]; memcpy (playerbytes+1, &netbuffer[k], numplayers - 1); k += numplayers - 1; } else { numplayers = 1; } // to save bytes, only the low byte of tic numbers are sent // Figure out what the rest of the bytes are realstart = ExpandTics (netbuffer[1]); realend = (realstart + numtics); nodeforplayer[netconsole] = netnode; // check for retransmit request if (resendcount[netnode] <= 0 && (netbuffer[0] & NCMD_RETRANSMIT)) { resendto[netnode] = ExpandTics (retransmitfrom); if (debugfile) fprintf (debugfile,"retransmit from %i\n", resendto[netnode]); resendcount[netnode] = RESENDCOUNT; } else { resendcount[netnode]--; } // check for out of order / duplicated packet if (realend == nettics[netnode]) continue; if (realend < nettics[netnode]) { if (debugfile) fprintf (debugfile, "out of order packet (%i + %i)\n" , realstart, numtics); continue; } // check for a missed packet if (realstart > nettics[netnode]) { // stop processing until the other system resends the missed tics if (debugfile) fprintf (debugfile, "missed tics from %i (%i to %i)\n", netnode, nettics[netnode], realstart); remoteresend[netnode] = true; continue; } // update command store from the packet { byte *start; int i, tics; remoteresend[netnode] = false; start = &netbuffer[k]; for (i = 0; i < numplayers; ++i) { int node = !players[playerbytes[i]].isbot ? nodeforplayer[playerbytes[i]] : netnode; SkipTicCmd (&start, nettics[node] - realstart); for (tics = nettics[node]; tics < realend; tics++) ReadTicCmd (&start, playerbytes[i], tics); } // Update the number of tics received from each node. This must // be separate from the above loop in case the master is also // sending bot movements. If it's not separate, then the bots // will only move on the master, because the other players will // read the master's tics and then think they already got all // the tics for the bots and skip the bot tics included in the // packet. for (i = 0; i < numplayers; ++i) { if (!players[playerbytes[i]].isbot) { nettics[nodeforplayer[playerbytes[i]]] = realend; } } } } } void AdjustBots (int gameticdiv) { // [RH] This loop adjusts the bots' rotations for ticcmds that have // been already created but not yet executed. This way, the bot is still // able to create ticcmds that accurately reflect the state it wants to // be in even when gametic lags behind maketic. for (int i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] && players[i].isbot && players[i].mo) { players[i].savedyaw = players[i].mo->angle; players[i].savedpitch = players[i].mo->pitch; for (int j = gameticdiv; j < maketic/ticdup; j++) { players[i].mo->angle += (netcmds[i][j%BACKUPTICS].ucmd.yaw << 16) * ticdup; players[i].mo->pitch -= (netcmds[i][j%BACKUPTICS].ucmd.pitch << 16) * ticdup; } } } } void UnadjustBots () { for (int i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] && players[i].isbot && players[i].mo) { players[i].mo->angle = players[i].savedyaw; players[i].mo->pitch = players[i].savedpitch; } } } // // NetUpdate // Builds ticcmds for console player, // sends out a packet // int gametime; void NetUpdate (void) { int lowtic; int nowtime; int newtics; int i,j; int realstart; byte *cmddata; bool resendOnly; if (ticdup == 0) { return; } // check time nowtime = I_GetTime (false); newtics = nowtime - gametime; gametime = nowtime; if (newtics <= 0) // nothing new to update { GetPackets (); return; } if (skiptics <= newtics) { newtics -= skiptics; skiptics = 0; } else { skiptics -= newtics; newtics = 0; } // build new ticcmds for console player (and bots if I am the arbitrator) AdjustBots (gametic / ticdup); for (i = 0; i < newtics; i++) { I_StartTic (); D_ProcessEvents (); if ((maketic - gametic) / ticdup >= BACKUPTICS/2-1) break; // can't hold any more //Printf ("mk:%i ",maketic); G_BuildTiccmd (&localcmds[maketic % LOCALCMDTICS]); if (maketic % ticdup == 0) { //Added by MC: For some of that bot stuff. The main bot function. bglobal.Main ((maketic / ticdup) % BACKUPTICS); } maketic++; if (ticdup == 1 || maketic == 0) { Net_NewMakeTic (); } else { // Once ticdup tics have been collected, average their movements // and combine their buttons, since they will all be sent as a // single tic that gets duplicated ticdup times. Even with ticdup, // tics are still collected at the normal rate so that, with the // help of prediction, the game seems as responsive as normal. if (maketic % ticdup != 0) { int mod = maketic - maketic % ticdup; int j; // Update the buttons for all tics in this ticdup set as soon as // possible so that the prediction shows jumping as correctly as // possible. (If you press +jump in the middle of a ticdup set, // the jump will actually begin at the beginning of the set, not // in the middle.) for (j = maketic-2; j >= mod; --j) { localcmds[j % LOCALCMDTICS].ucmd.buttons |= localcmds[(j + 1) % LOCALCMDTICS].ucmd.buttons; } } else { // Average the ticcmds between these tics to get the // movement that is actually sent across the network. We // need to update them in all the localcmds slots that // are dupped so that prediction works properly. int mod = maketic - ticdup; int modp, j; int pitch = 0; int yaw = 0; int roll = 0; int forwardmove = 0; int sidemove = 0; int upmove = 0; for (j = 0; j < ticdup; ++j) { modp = (mod + j) % LOCALCMDTICS; pitch += localcmds[modp].ucmd.pitch; yaw += localcmds[modp].ucmd.yaw; roll += localcmds[modp].ucmd.roll; forwardmove += localcmds[modp].ucmd.forwardmove; sidemove += localcmds[modp].ucmd.sidemove; upmove += localcmds[modp].ucmd.upmove; } pitch /= ticdup; yaw /= ticdup; roll /= ticdup; forwardmove /= ticdup; sidemove /= ticdup; upmove /= ticdup; for (j = 0; j < ticdup; ++j) { modp = (mod + j) % LOCALCMDTICS; localcmds[modp].ucmd.pitch = pitch; localcmds[modp].ucmd.yaw = yaw; localcmds[modp].ucmd.roll = roll; localcmds[modp].ucmd.forwardmove = forwardmove; localcmds[modp].ucmd.sidemove = sidemove; localcmds[modp].ucmd.upmove = upmove; } Net_NewMakeTic (); } } } UnadjustBots (); if (singletics) return; // singletic update is synchronous // If maketic didn't cross a ticdup boundary, only send packets // to players waiting for resends. resendOnly = (maketic / ticdup) == (maketic - i) / ticdup; // send the packet to the other nodes int count = 1; int quitcount = 0; if (consoleplayer == Net_Arbitrator) { for (j = 0; j < MAXPLAYERS; j++) { if (playeringame[j]) { if (players[j].isbot || NetMode == NET_PacketServer) { count++; } } } if (NetMode == NET_PacketServer) { // The loop above added the local player to the count a second time, // and it also added the player being sent the packet to the count. count -= 2; for (j = 0; j < doomcom.numnodes; ++j) { if (nodejustleft[j]) { if (count == 0) { PlayerIsGone (j, playerfornode[j]); } else { quitcount++; } } } if (count == 0) { count = 1; } } } for (i = 0; i < doomcom.numnodes; i++) { BYTE playerbytes[MAXPLAYERS]; if (!nodeingame[i]) { continue; } if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator && i != nodeforplayer[Net_Arbitrator] && i != 0) { continue; } if (resendOnly && resendcount[i] <= 0 && !remoteresend[i] && nettics[i]) { continue; } int numtics; int k; lowtic = maketic / ticdup; netbuffer[0] = 0; netbuffer[1] = realstart = resendto[i]; k = 2; if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator && i != 0) { for (j = 1; j < doomcom.numnodes; ++j) { if (nodeingame[j] && nettics[j] < lowtic && j != i) { lowtic = nettics[j]; } } netbuffer[k++] = lowtic; } numtics = lowtic - realstart; if (numtics > BACKUPTICS) I_Error ("NetUpdate: Node %d missed too many tics", i); resendto[i] = MAX (0, lowtic - doomcom.extratics); if (numtics == 0 && resendOnly && !remoteresend[i] && nettics[i]) { continue; } if (remoteresend[i]) { netbuffer[0] |= NCMD_RETRANSMIT; netbuffer[k++] = nettics[i]; } if (numtics < 3) { netbuffer[0] |= numtics; } else { netbuffer[0] |= NCMD_XTICS; netbuffer[k++] = numtics - 3; } if (quitcount > 0) { netbuffer[0] |= NCMD_QUITTERS; netbuffer[k++] = quitcount; for (int l = 0; l < doomcom.numnodes; ++l) { if (nodejustleft[l]) { netbuffer[k++] = playerfornode[l]; } } } if (numtics > 0) { int l; if (count > 1 && i != 0 && consoleplayer == Net_Arbitrator) { netbuffer[0] |= NCMD_MULTI; netbuffer[k++] = count; for (l = 1, j = 0; j < MAXPLAYERS; j++) { if (playeringame[j] && j != playerfornode[i] && j != consoleplayer) { if (players[j].isbot || NetMode == NET_PacketServer) { playerbytes[l++] = j; netbuffer[k++] = j; } } } } cmddata = &netbuffer[k]; for (l = 0; l < count; ++l) { for (j = 0; j < numtics; j++) { int start = realstart + j, prev = start - 1; int localstart, localprev; localstart = (start * ticdup) % LOCALCMDTICS; localprev = (prev * ticdup) % LOCALCMDTICS; start %= BACKUPTICS; prev %= BACKUPTICS; // The local player has their tics sent first, followed by // the other players/bots. if (l == 0) { WriteWord (localcmds[localstart].consistancy, &cmddata); // [RH] Write out special "ticcmds" before real ticcmd if (specials.used[start]) { memcpy (cmddata, specials.streams[start], specials.used[start]); cmddata += specials.used[start]; } WriteUserCmdMessage (&localcmds[localstart].ucmd, localprev >= 0 ? &localcmds[localprev].ucmd : NULL, &cmddata); } else if (i != 0) { if (players[playerbytes[l]].isbot) { WriteWord (0, &cmddata); // fake consistancy word } else { int len; BYTE *spec; WriteWord (netcmds[playerbytes[l]][start].consistancy, &cmddata); spec = NetSpecs[playerbytes[l]][start].GetData (&len); if (spec != NULL) { memcpy (cmddata, spec, len); cmddata += len; } } WriteUserCmdMessage (&netcmds[playerbytes[l]][start].ucmd, prev >= 0 ? &netcmds[playerbytes[l]][prev].ucmd : NULL, &cmddata); } } } HSendPacket (i, cmddata - netbuffer); } else { HSendPacket (i, k); } } // listen for other packets GetPackets (); } // // CheckAbort // BOOL CheckAbort (void) { event_t *ev; BOOL res = false; PrintString (PRINT_HIGH, ""); // [RH] Give the console a chance to redraw itself // This WaitForTic is to avoid flooding the network with packets on startup. I_WaitForTic (I_GetTime (false) + TICRATE/4); I_StartTic (); for ( ; eventtail != eventhead ; eventtail = (eventtail+1)&(MAXEVENTS-1) ) { ev = &events[eventtail]; if (ev->type == EV_KeyDown && ev->data1 == KEY_ESCAPE) { res = true; break; } if (ev->type == EV_GUI_Event && (ev->subtype == EV_GUI_KeyDown || ev->subtype == EV_GUI_KeyRepeat) && ev->data1 == GK_ESCAPE) { res = true; break; } } eventhead = eventtail = 0; return res; } // // D_ArbitrateNetStart // // User info packets look like this: // // 0 One byte set to NCMD_SETUP or NCMD_SETUP+1 // If NCMD_SETUP+1, omit byte 7 // 1 One byte for the player's number // 2 One byte for the game version //3-7 A bit mask for each player the sender knows about // (the high bit of byte 7 indicates the game info was received) // 8 A stream of bytes with the user info // // The guests always send NCMD_SETUP packets, and the host always // sends NCMD_SETUP+1 packets. // // Game info packets look like this: // // 0 One byte set to NCMD_SETUP+2 // 1 One byte for ticdup setting // 2 One byte for extratics setting // 3 One byte for NetMode setting // 4 String with starting map's name // . Four bytes for the RNG seed // . Stream containing remaining game info // // Finished packet looks like this: // // 0 One byte set to NCMD_SETUP+3 // // Each machine sends user info packets to the host. The host sends user // info packets back to the other machines as well as game info packets. // Negotiation is done when all the guests have reported to the host that // they know about the other nodes. void D_ArbitrateNetStart (void) { int i, j; DWORD playersdetected[MAXNETNODES]; BYTE gotsetup[MAXNETNODES]; char *s; byte *stream; int node; bool allset = false; // Return right away if we're just playing with ourselves. if (doomcom.numnodes == 1) return; autostart = true; memset (playersdetected, 0, sizeof(playersdetected)); memset (gotsetup, 0, sizeof(gotsetup)); // Everyone know about themself playersdetected[0] = 1 << consoleplayer; // Assign nodes to players playerfornode[0] = consoleplayer; nodeforplayer[consoleplayer] = 0; if (consoleplayer == Net_Arbitrator) { for (i = 1; i < doomcom.numnodes; ++i) { playerfornode[i] = i; nodeforplayer[i] = i; } } else { playerfornode[1] = 0; nodeforplayer[0] = 1; for (i = 1; i < doomcom.numnodes; ++i) { if (i < consoleplayer) { playerfornode[i+1] = i; nodeforplayer[i] = i+1; } else if (i > consoleplayer) { playerfornode[i] = i; nodeforplayer[i] = i; } } } if (consoleplayer == Net_Arbitrator) { gotsetup[0] = 0x80; } while (!allset) { if (CheckAbort ()) I_FatalError ("Network game synchronization aborted."); I_WaitVBL (1); while (HGetPacket ()) { if (netbuffer[0] == NCMD_EXIT) { I_FatalError ("The game was aborted\n"); } if (doomcom.remotenode == 0) { continue; } if (netbuffer[0] == NCMD_SETUP || netbuffer[0] == NCMD_SETUP+1) // got user info { node = (netbuffer[0] == NCMD_SETUP) ? doomcom.remotenode : nodeforplayer[netbuffer[1]]; playersdetected[node] = (netbuffer[3] << 24) | (netbuffer[4] << 16) | (netbuffer[5] << 8) | netbuffer[6]; if (netbuffer[0] == NCMD_SETUP) { // Sent to host gotsetup[node] = netbuffer[7] & 0x80; stream = &netbuffer[8]; } else { // Sent from host stream = &netbuffer[7]; } if (!nodeingame[node]) { if (netbuffer[2] != NETGAMEVERSION) I_Error ("Different DOOM versions cannot play a net game!"); playeringame[netbuffer[1]] = true; nodeingame[node] = true; playersdetected[0] |= 1 << netbuffer[1]; D_ReadUserInfoStrings (netbuffer[1], &stream, false); Printf ("Found %s (node %d, player %d)\n", players[netbuffer[1]].userinfo.netname, node, netbuffer[1]+1); } } else if (netbuffer[0] == NCMD_SETUP+2) // got game info { gotsetup[0] = 0x80; ticdup = doomcom.ticdup = netbuffer[1]; doomcom.extratics = netbuffer[2]; NetMode = netbuffer[3]; stream = &netbuffer[4]; s = ReadString (&stream); strncpy (startmap, s, 8); delete[] s; rngseed = ReadLong (&stream); C_ReadCVars (&stream); } else if (netbuffer[0] == NCMD_SETUP+3) { allset = true; } } // If everybody already knows everything, it's time to go if (consoleplayer == Net_Arbitrator) { for (i = 0; i < doomcom.numnodes; ++i) if (playersdetected[i] != DWORD(1 << doomcom.numnodes) - 1 || !gotsetup[i]) break; if (i == doomcom.numnodes) break; } netbuffer[2] = NETGAMEVERSION; netbuffer[3] = playersdetected[0] >> 24; netbuffer[4] = playersdetected[0] >> 16; netbuffer[5] = playersdetected[0] >> 8; netbuffer[6] = playersdetected[0]; if (!allset && consoleplayer != Net_Arbitrator) { // Send user info for the local node netbuffer[0] = NCMD_SETUP; netbuffer[1] = consoleplayer; netbuffer[7] = gotsetup[0]; stream = &netbuffer[8]; D_WriteUserInfoStrings (consoleplayer, &stream, true); SendSetup (playersdetected, gotsetup, stream - netbuffer); } else { // Send user info for all nodes netbuffer[0] = NCMD_SETUP+1; netbuffer[2] = NETGAMEVERSION; for (i = 1; i < doomcom.numnodes; ++i) { for (j = 0; j < doomcom.numnodes; ++j) { // Send info about player j to player i? if (i != j && (playersdetected[0] & (1<ReadNetVars (); // [RH] Read network ServerInfo cvars D_ArbitrateNetStart (); } // read values out of doomcom ticdup = doomcom.ticdup; for (i = 0; i < doomcom.numplayers; i++) playeringame[i] = true; for (i = 0; i < doomcom.numnodes; i++) nodeingame[i] = true; Printf ("player %i of %i (%i nodes)\n", consoleplayer+1, doomcom.numplayers, doomcom.numnodes); } // // D_QuitNetGame // Called before quitting to leave a net game // without hanging the other players // void D_QuitNetGame (void) { int i, j, k; if (!netgame || !usergame || consoleplayer == -1 || demoplayback) return; // send a bunch of packets for security netbuffer[0] = NCMD_EXIT; netbuffer[1] = 0; k = 2; if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) { BYTE *foo = &netbuffer[2]; // Let the new arbitrator know what resendto counts to use for (i = 0; i < MAXPLAYERS; ++i) { if (playeringame[i] && i != consoleplayer) WriteLong (resendto[nodeforplayer[i]], &foo); } k = foo - netbuffer; } for (i = 0; i < 4; i++) { if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) { HSendPacket (nodeforplayer[Net_Arbitrator], 2); } else { for (j = 1; j < doomcom.numnodes; j++) if (nodeingame[j]) HSendPacket (j, k); } I_WaitVBL (1); } if (debugfile) fclose (debugfile); } // // TryRunTics // void TryRunTics (void) { int i; int lowtic; int realtics; int availabletics; int counts; int numplaying; // If paused, do not eat more CPU time than we need, because it // will all be wasted anyway. bool doWait = cl_capfps || r_NoInterpolate /*|| netgame*/; // get real tics if (doWait) { entertic = I_WaitForTic (oldentertics); } else { entertic = I_GetTime (false); } realtics = entertic - oldentertics; oldentertics = entertic; // get available tics NetUpdate (); lowtic = INT_MAX; numplaying = 0; for (i = 0; i < doomcom.numnodes; i++) { if (nodeingame[i]) { numplaying++; if (nettics[i] < lowtic) lowtic = nettics[i]; } } if (ticdup == 1) { availabletics = lowtic - gametic; } else { availabletics = lowtic - gametic / ticdup; } // decide how many tics to run if (realtics < availabletics-1) counts = realtics+1; else if (realtics < availabletics) counts = realtics; else counts = availabletics; if (counts == 0 && !doWait) { return; } if (counts < 1) counts = 1; frameon++; if (debugfile) fprintf (debugfile, "=======real: %i avail: %i game: %i\n", realtics, availabletics, counts); if (!demoplayback) { // ideally nettics[0] should be 1 - 3 tics above lowtic // if we are consistantly slower, speed up time // [RH] I had erroneously assumed frameskip[] had 4 entries // because there were 4 players, but that's not the case at // all. The game is comparing the lag behind the master for // four runs of TryRunTics. If our tic count is ahead of the // master all 4 times, the next run of NetUpdate will not // process any new input. If we have less input than the // master, the next run of NetUpdate will process extra tics // (because gametime gets decremented here). // the key player does not adapt if (consoleplayer != Net_Arbitrator) { // I'm not sure about this when using a packet server, because // if left unmodified from the P2P version, it can make the game // very jerky. The way I have it written right now basically means // that it won't adapt. Fortunately, player prediction helps // alleviate the lag somewhat. if (NetMode != NET_PacketServer) { mastertics = nettics[nodeforplayer[Net_Arbitrator]]; } if (nettics[0] <= mastertics) { gametime--; if (debugfile) fprintf (debugfile, "-"); } if (NetMode != NET_PacketServer) { frameskip[frameon&3] = (oldnettics > mastertics); } else { frameskip[frameon&3] = (oldnettics - mastertics) > 3; } if (frameskip[0] && frameskip[1] && frameskip[2] && frameskip[3]) { skiptics = 1; if (debugfile) fprintf (debugfile, "+"); } oldnettics = nettics[0]; } }// !demoplayback // wait for new tics if needed while (lowtic < gametic + counts) { NetUpdate (); lowtic = INT_MAX; for (i = 0; i < doomcom.numnodes; i++) if (nodeingame[i] && nettics[i] < lowtic) lowtic = nettics[i]; lowtic = lowtic * ticdup; if (lowtic < gametic) I_Error ("TryRunTics: lowtic < gametic"); // don't stay in here forever -- give the menu a chance to work if (I_GetTime (false) - entertic >= TICRATE/3) { C_Ticker (); M_Ticker (); return; } } // run the count tics while (counts--) { if (gametic > lowtic) { I_Error ("gametic>lowtic"); } if (advancedemo) { D_DoAdvanceDemo (); } if (debugfile) fprintf (debugfile, "run tic %d\n", gametic); DObject::BeginFrame (); C_Ticker (); M_Ticker (); I_GetTime (true); G_Ticker (); DObject::EndFrame (); gametic++; NetUpdate (); // check for new console commands } } void Net_NewMakeTic (void) { specials.NewMakeTic (); } void Net_WriteByte (byte it) { specials << it; } void Net_WriteWord (short it) { specials << it; } void Net_WriteLong (int it) { specials << it; } void Net_WriteFloat (float it) { specials << it; } void Net_WriteString (const char *it) { specials << it; } void Net_WriteBytes (const byte *block, int len) { while (len--) specials << *block++; } //========================================================================== // // Dynamic buffer interface // //========================================================================== FDynamicBuffer::FDynamicBuffer () { m_Data = NULL; m_Len = m_BufferLen = 0; } FDynamicBuffer::~FDynamicBuffer () { if (m_Data) { free (m_Data); m_Data = NULL; } m_Len = m_BufferLen = 0; } void FDynamicBuffer::SetData (const byte *data, int len) { if (len > m_BufferLen) { m_BufferLen = (len + 255) & ~255; m_Data = (byte *)M_Realloc (m_Data, m_BufferLen); } if (data) { m_Len = len; memcpy (m_Data, data, len); } else { len = 0; } } byte *FDynamicBuffer::GetData (int *len) { if (len) *len = m_Len; return m_Len ? m_Data : NULL; } // [RH] Execute a special "ticcmd". The type byte should // have already been read, and the stream is positioned // at the beginning of the command's actual data. void Net_DoCommand (int type, byte **stream, int player) { byte pos = 0; char *s = NULL; int i; switch (type) { case DEM_SAY: { const char *name = players[player].userinfo.netname; byte who = ReadByte (stream); s = ReadString (stream); if (((who & 1) == 0) || players[player].userinfo.team == TEAM_None) { // Said to everyone if (who & 2) { Printf (PRINT_CHAT, TEXTCOLOR_BOLD "* %s%s\n", name, s); } else { Printf (PRINT_CHAT, "%s: %s\n", name, s); } S_Sound (CHAN_VOICE, gameinfo.chatSound, 1, ATTN_NONE); } else if (players[player].userinfo.team == players[consoleplayer].userinfo.team) { // Said only to members of the player's team if (who & 2) { Printf (PRINT_TEAMCHAT, TEXTCOLOR_BOLD "* (%s)%s\n", name, s); } else { Printf (PRINT_TEAMCHAT, "(%s): %s\n", name, s); } S_Sound (CHAN_VOICE, gameinfo.chatSound, 1, ATTN_NONE); } } break; case DEM_MUSICCHANGE: s = ReadString (stream); S_ChangeMusic (s); break; case DEM_PRINT: s = ReadString (stream); Printf (s); break; case DEM_CENTERPRINT: s = ReadString (stream); C_MidPrint (s); break; case DEM_UINFCHANGED: D_ReadUserInfoStrings (player, stream, true); break; case DEM_SINFCHANGED: D_DoServerInfoChange (stream, false); break; case DEM_SINFCHANGEDXOR: D_DoServerInfoChange (stream, true); break; case DEM_GIVECHEAT: s = ReadString (stream); cht_Give (&players[player], s, ReadWord (stream)); break; case DEM_WARPCHEAT: { int x, y; x = ReadWord (stream); y = ReadWord (stream); P_TeleportMove (players[player].mo, x * 65536, y * 65536, ONFLOORZ, true); } break; case DEM_GENERICCHEAT: cht_DoCheat (&players[player], ReadByte (stream)); break; case DEM_CHANGEMAP2: pos = ReadByte (stream); /* intentional fall-through */ case DEM_CHANGEMAP: // Change to another map without disconnecting other players s = ReadString (stream); strncpy (level.nextmap, s, 8); // Using LEVEL_NOINTERMISSION tends to throw the game out of sync. // That was a long time ago. Maybe it works now? level.flags |= LEVEL_CHANGEMAPCHEAT; G_ExitLevel (pos, false); break; case DEM_SUICIDE: cht_Suicide (&players[player]); break; case DEM_ADDBOT: { byte num = ReadByte (stream); bglobal.DoAddBot (num, s = ReadString (stream)); } break; case DEM_KILLBOTS: bglobal.RemoveAllBots (true); Printf ("Removed all bots\n"); break; case DEM_CENTERVIEW: if (players[player].mo != NULL) { players[player].mo->pitch = 0; } break; case DEM_INVUSEALL: if (gamestate == GS_LEVEL && !paused) { AInventory *item = players[player].mo->Inventory; while (item != NULL) { AInventory *next = item->Inventory; if (item->ItemFlags & IF_INVBAR && !(item->IsKindOf(RUNTIME_CLASS(APuzzleItem)))) { players[player].mo->UseInventory (item); } item = next; } } break; case DEM_INVUSE: case DEM_INVDROP: { DWORD which = ReadLong (stream); if (gamestate == GS_LEVEL && !paused) { AInventory *item = players[player].mo->Inventory; while (item != NULL && item->InventoryID != which) { item = item->Inventory; } if (item != NULL) { if (type == DEM_INVUSE) { players[player].mo->UseInventory (item); } else { players[player].mo->DropInventory (item); } } } } break; case DEM_SUMMON: case DEM_SUMMONFRIEND: { const PClass *typeinfo; s = ReadString (stream); typeinfo = PClass::FindClass (s); if (typeinfo != NULL && typeinfo->ActorInfo != NULL) { AActor *source = players[player].mo; if (source != NULL) { if (GetDefaultByType (typeinfo)->flags & MF_MISSILE) { P_SpawnPlayerMissile (source, typeinfo); } else { const AActor *def = GetDefaultByType (typeinfo); AActor *spawned = Spawn (typeinfo, source->x + FixedMul (def->radius * 2 + source->radius, finecosine[source->angle>>ANGLETOFINESHIFT]), source->y + FixedMul (def->radius * 2 + source->radius, finesine[source->angle>>ANGLETOFINESHIFT]), source->z + 8 * FRACUNIT, ALLOW_REPLACE); if (spawned != NULL && type == DEM_SUMMONFRIEND) { spawned->FriendPlayer = player + 1; spawned->flags |= MF_FRIENDLY; spawned->LastHeard = players[player].mo; } } } } } break; case DEM_SPRAY: { FTraceResults trace; angle_t ang = players[player].mo->angle >> ANGLETOFINESHIFT; angle_t pitch = (angle_t)(players[player].mo->pitch) >> ANGLETOFINESHIFT; fixed_t vx = FixedMul (finecosine[pitch], finecosine[ang]); fixed_t vy = FixedMul (finecosine[pitch], finesine[ang]); fixed_t vz = -finesine[pitch]; s = ReadString (stream); if (Trace (players[player].mo->x, players[player].mo->y, players[player].mo->z + players[player].mo->height - (players[player].mo->height>>2), players[player].mo->Sector, vx, vy, vz, 172*FRACUNIT, 0, ML_BLOCKEVERYTHING, players[player].mo, trace, TRACE_NoSky)) { if (trace.HitType == TRACE_HitWall) { DImpactDecal::StaticCreate (s, trace.X, trace.Y, trace.Z, sides + trace.Line->sidenum[trace.Side]); } } } break; case DEM_PAUSE: if (gamestate == GS_LEVEL) { if (paused) { paused = 0; S_ResumeSound (); } else { paused = player + 1; S_PauseSound (false); } BorderNeedRefresh = screen->GetPageCount (); } break; case DEM_SAVEGAME: if (gamestate == GS_LEVEL) { s = ReadString (stream); savegamefile = s; delete[] s; s = ReadString (stream); memset (savedescription, 0, sizeof(savedescription)); strncpy (savedescription, s, sizeof(savedescription)); if (player != consoleplayer) { // Paths sent over the network will be valid for the system that sent // the save command. For other systems, the path needs to be changed. const char *fileonly = savegamefile.GetChars(); const char *slash = strrchr (fileonly, '\\'); if (slash != NULL) { fileonly = slash + 1; } slash = strrchr (fileonly, '/'); if (slash != NULL) { fileonly = slash + 1; } if (fileonly != savegamefile.GetChars()) { savegamefile = G_BuildSaveName (fileonly, -1); } } } gameaction = ga_savegame; break; case DEM_FOV: { float newfov = (float)ReadByte (stream); if (newfov != players[consoleplayer].DesiredFOV) { Printf ("FOV%s set to %g\n", consoleplayer == Net_Arbitrator ? " for everyone" : "", newfov); } for (i = 0; i < MAXPLAYERS; ++i) { if (playeringame[i]) { players[i].DesiredFOV = newfov; } } } break; case DEM_MYFOV: players[player].DesiredFOV = (float)ReadByte (stream); break; case DEM_RUNSCRIPT: { int snum = ReadWord (stream); int argn = ReadByte (stream); int arg[3] = { 0, 0, 0 }; for (i = 0; i < argn; ++i) { arg[i] = ReadLong (stream); } P_StartScript (players[player].mo, NULL, snum, level.mapname, false, arg[0], arg[1], arg[2], false, false, true); } break; case DEM_CROUCH: if (gamestate == GS_LEVEL && players[player].mo != NULL && players[player].health > 0 && !(players[player].oldbuttons & BT_JUMP)) { players[player].crouching = players[player].crouchdir<0? 1 : -1; } break; default: I_Error ("Unknown net command: %d", type); break; } if (s) delete[] s; } void Net_SkipCommand (int type, byte **stream) { BYTE t; size_t skip; switch (type) { case DEM_SAY: case DEM_ADDBOT: skip = strlen ((char *)(*stream + 1)) + 2; break; case DEM_GIVECHEAT: skip = strlen ((char *)(*stream)) + 3; break; case DEM_MUSICCHANGE: case DEM_PRINT: case DEM_CENTERPRINT: case DEM_UINFCHANGED: case DEM_CHANGEMAP: case DEM_SUMMON: case DEM_SUMMONFRIEND: case DEM_SPRAY: skip = strlen ((char *)(*stream)) + 1; break; case DEM_INVUSE: case DEM_INVDROP: case DEM_WARPCHEAT: skip = 4; break; case DEM_GENERICCHEAT: case DEM_DROPPLAYER: case DEM_FOV: case DEM_MYFOV: skip = 1; break; case DEM_SAVEGAME: skip = strlen ((char *)(*stream)) + 1; skip += strlen ((char *)(*stream) + skip) + 1; break; case DEM_SINFCHANGEDXOR: case DEM_SINFCHANGED: t = **stream; skip = 1 + (t & 63); if (type == DEM_SINFCHANGED) { switch (t >> 6) { case CVAR_Bool: skip += 1; break; case CVAR_Int: case CVAR_Float: skip += 4; break; case CVAR_String: skip += strlen ((char *)(*stream + skip)) + 1; break; } } else { skip += 1; } break; case DEM_RUNSCRIPT: skip = 3 + *(*stream + 2) * 4; break; default: return; } *stream += skip; } // [RH] List "ping" times CCMD (pings) { int i; for (i = 0; i < MAXPLAYERS; i++) if (playeringame[i]) Printf ("% 4d %s\n", currrecvtime[i] - lastrecvtime[i], players[i].userinfo.netname); }