//----------------------------------------------------------------------------- // // Copyright 1993-1996 id Software // Copyright 1999-2016 Randy Heit // Copyright 2002-2016 Christoph Oelckers // // 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 3 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, see http://www.gnu.org/licenses/ // //----------------------------------------------------------------------------- // // DESCRIPTION: // DOOM Network game communication and protocol, // all OS independent parts. // //----------------------------------------------------------------------------- #include #define __STDC_FORMAT_MACROS #include #include "version.h" #include "menu/menu.h" #include "i_video.h" #include "i_net.h" #include "g_game.h" #include "c_console.h" #include "d_netinf.h" #include "d_net.h" #include "cmdlib.h" #include "m_cheat.h" #include "p_local.h" #include "c_dispatch.h" #include "sbar.h" #include "gi.h" #include "m_misc.h" #include "gameconfigfile.h" #include "p_acs.h" #include "p_trace.h" #include "a_sharedglobal.h" #include "st_start.h" #include "teaminfo.h" #include "p_conversation.h" #include "d_event.h" #include "p_enemy.h" #include "m_argv.h" #include "p_lnspec.h" #include "p_spec.h" #include "hardware.h" #include "r_utility.h" #include "a_keys.h" #include "intermission/intermission.h" #include "g_levellocals.h" #include "actorinlines.h" #include "events.h" #include "i_time.h" #include "i_system.h" #include "vm.h" EXTERN_CVAR (Int, disableautosave) EXTERN_CVAR (Int, autosavecount) //#define SIMULATEERRORS (RAND_MAX/3) #define SIMULATEERRORS 0 extern uint8_t *demo_p; // [RH] Special "ticcmds" get recorded in demos extern FString savedescription; extern FString savegamefile; extern short consistancy[MAXPLAYERS][BACKUPTICS]; doomcom_t doomcom; #define netbuffer (doomcom.data) enum { NET_PeerToPeer, NET_PacketServer }; uint8_t 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]; uint64_t lastrecvtime[MAXPLAYERS]; // [RH] Used for pings uint64_t currrecvtime[MAXPLAYERS]; uint64_t lastglobalrecvtime; // Identify the last time a packet was received. bool hadlate; int netdelay[MAXNETNODES][BACKUPTICS]; // Used for storing network delay times. int lastaverage; 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 (uint32_t playersdetected[MAXNETNODES], uint8_t gotsetup[MAXNETNODES], int len); static void RunScript(uint8_t **stream, AActor *pawn, int snum, int argn, int always); int reboundpacket; uint8_t reboundstore[MAX_MSGLEN]; int frameon; int frameskip[4]; int oldnettics; int mastertics; static int entertic; static int oldentertics; extern bool advancedemo; CUSTOM_CVAR (Bool, cl_capfps, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) { // Do not use the separate FPS limit timer if we are limiting FPS with this. if (self) { I_SetFPSLimit(0); } else { I_SetFPSLimit(-1); } } CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) CUSTOM_CVAR(Int, net_extratic, 0, CVAR_SERVERINFO | CVAR_NOSAVE) { if (self < 0) { self = 0; } else if (self > 2) { self = 2; } } #ifdef _DEBUG CVAR(Int, net_fakelatency, 0, 0); struct PacketStore { int timer; doomcom_t message; }; static TArray InBuffer; static TArray OutBuffer; #endif // [RH] Special "ticcmds" get stored in here static struct TicSpecial { uint8_t *streams[BACKUPTICS]; size_t used[BACKUPTICS]; uint8_t *streamptr; size_t streamoffs; size_t 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] = (uint8_t *)M_Malloc (256); used[i] = 0; } okay = true; } ~TicSpecial () { int i; for (i = 0; i < BACKUPTICS; i++) { if (streams[i]) { M_Free (streams[i]); streams[i] = NULL; used[i] = 0; } } okay = false; } // Make more room for special commands. void GetMoreSpace (size_t needed) { int i; specialsize = MAX(specialsize * 2, needed + 30); DPrintf (DMSG_NOTIFY, "Expanding special size to %zu\n", specialsize); for (i = 0; i < BACKUPTICS; i++) streams[i] = (uint8_t *)M_Realloc (streams[i], specialsize); streamptr = streams[(maketic/ticdup)%BACKUPTICS] + streamoffs; } void CheckSpace (size_t needed) { if (streamoffs + needed >= specialsize) GetMoreSpace (streamoffs + needed); 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 << (uint8_t 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 (nodeforplayer, 0, sizeof(nodeforplayer)); memset (playerfornode, 0, sizeof(playerfornode)); 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; lastglobalrecvtime = 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; } // Network delay byte k++; 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; } uint8_t *skipper = &netbuffer[k]; if ((netbuffer[0] & NCMD_EXIT) == 0) { while (count-- > 0) { SkipTicCmd (&skipper, numtics); } } return int(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", ((uint8_t *)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", ((uint8_t *)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?'|':' ', ((uint8_t *)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; #ifdef _DEBUG if (net_fakelatency / 2 > 0) { PacketStore store; store.message = doomcom; store.timer = I_GetTime() + ((net_fakelatency / 2) / (1000 / TICRATE)); OutBuffer.Push(store); } else I_NetCmd(); for (unsigned int i = 0; i < OutBuffer.Size(); i++) { if (OutBuffer[i].timer <= I_GetTime()) { doomcom = OutBuffer[i].message; I_NetCmd(); OutBuffer.Delete(i); i = -1; } } #else I_NetCmd(); #endif } // // 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 (); #ifdef _DEBUG if (net_fakelatency / 2 > 0 && doomcom.remotenode != -1) { PacketStore store; store.message = doomcom; store.timer = I_GetTime() + ((net_fakelatency / 2) / (1000 / TICRATE)); InBuffer.Push(store); doomcom.remotenode = -1; } if (doomcom.remotenode == -1) { bool gotmessage = false; for (unsigned int i = 0; i < InBuffer.Size(); i++) { if (InBuffer[i].timer <= I_GetTime()) { doomcom = InBuffer[i].message; InBuffer.Delete(i); gotmessage = true; break; } } if (!gotmessage) return false; } #else if (doomcom.remotenode == -1) { return false; } #endif 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", ((uint8_t *)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", ((uint8_t *)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?'|':' ', ((uint8_t *)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 ()) { Printf("Bad packet length %i (calculated %i)\n", 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; if (nodeingame[netnode]) { for (i = netnode + 1; i < doomcom.numnodes; ++i) { if (nodeingame[i]) break; } if (i == doomcom.numnodes) { doomcom.numnodes = netnode; } if (playeringame[netconsole]) { players[netconsole].playerstate = PST_GONE; } nodeingame[netnode] = false; nodejustleft[netnode] = false; } else if (nodejustleft[netnode]) // Packet Server { if (netnode + 1 == doomcom.numnodes) { doomcom.numnodes = netnode; } if (playeringame[netconsole]) { players[netconsole].playerstate = PST_GONE; } nodejustleft[netnode] = false; } else return; if (netconsole == Net_Arbitrator) { // Pick a new network arbitrator for (int i = 0; i < MAXPLAYERS; i++) { if (i != netconsole && playeringame[i] && players[i].Bot == NULL) { Net_Arbitrator = i; players[i].settings_controller = true; Printf("%s is the new arbitrator\n", players[i].userinfo.GetName()); 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 ((uint8_t)netconsole, &demo_p); } } // // GetPackets // void GetPackets (void) { int netconsole; int netnode; int realend; int realstart; int numtics; int retransmitfrom; int k; uint8_t 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 - totally useless, since it's bound to the frequency // packets go out at. 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) { uint8_t *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; 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++; } } // Pull current network delay from node netdelay[netnode][(nettics[netnode]+1) % BACKUPTICS] = netbuffer[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 { uint8_t *start; int i, tics; remoteresend[netnode] = false; start = &netbuffer[k]; for (i = 0; i < numplayers; ++i) { int node = nodeforplayer[playerbytes[i]]; SkipTicCmd (&start, nettics[node] - realstart); for (tics = nettics[node]; tics < realend; tics++) ReadTicCmd (&start, playerbytes[i], tics); nettics[nodeforplayer[playerbytes[i]]] = realend; } } } } // // 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; uint8_t *cmddata; bool resendOnly; GC::CheckGC(); if (ticdup == 0) { return; } // check time nowtime = I_GetTime (); 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 for (i = 0; i < newtics; i++) { I_StartTic (); D_ProcessEvents (); if (pauseext || (maketic - gametic) / ticdup >= BACKUPTICS/2-1) break; // can't hold any more //Printf ("mk:%i ",maketic); G_BuildTiccmd (&localcmds[maketic % LOCALCMDTICS]); 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 (); } } } if (singletics) return; // singletic update is synchronous if (demoplayback) { resendto[0] = nettics[0] = (maketic / ticdup); return; // Don't touch netcmd data while playing a demo, as it'll already exist. } // 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) { if (NetMode == NET_PacketServer) { for (j = 0; j < MAXPLAYERS; j++) { if (playeringame[j] && players[j].Bot == NULL) { count++; } } // 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++) { uint8_t 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 = MAX(0, lowtic - realstart); if (numtics > BACKUPTICS) I_Error ("NetUpdate: Node %d missed too many tics", i); switch (net_extratic) { case 0: default: resendto[i] = lowtic; break; case 1: resendto[i] = MAX(0, lowtic - 1); break; case 2: resendto[i] = nettics[i]; break; } 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]; } } } // Send current network delay // The number of tics we just made should be removed from the count. netbuffer[k++] = ((maketic - numtics - gametic) / ticdup); if (numtics > 0) { int l; if (count > 1 && i != 0 && consoleplayer == Net_Arbitrator) { netbuffer[0] |= NCMD_MULTI; netbuffer[k++] = count; if (NetMode == NET_PacketServer) { for (l = 1, j = 0; j < MAXPLAYERS; j++) { if (playeringame[j] && players[j].Bot == NULL && j != playerfornode[i] && j != consoleplayer) { 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. 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) { int len; uint8_t *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, int(cmddata - netbuffer)); } else { HSendPacket (i, k); } } // listen for other packets GetPackets (); if (!resendOnly) { // 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_PeerToPeer) { int totalavg = 0; if (net_ticbalance) { // Try to guess ahead the time it takes to send responses to the slowest node int nodeavg = 0, arbavg = 0; for (j = 0; j < BACKUPTICS; j++) { arbavg += netdelay[nodeforplayer[Net_Arbitrator]][j]; nodeavg += netdelay[0][j]; } arbavg /= BACKUPTICS; nodeavg /= BACKUPTICS; // We shouldn't adapt if we are already the arbitrator isn't what we are waiting for, otherwise it just adds more latency if (arbavg > nodeavg) { lastaverage = totalavg = ((arbavg + nodeavg) / 2); } else { // Allow room to guess two tics ahead if (nodeavg > (arbavg + 2) && lastaverage > 0) lastaverage--; totalavg = lastaverage; } } mastertics = nettics[nodeforplayer[Net_Arbitrator]] + totalavg; } if (nettics[0] <= mastertics) { gametime--; if (debugfile) fprintf(debugfile, "-"); } if (NetMode != NET_PacketServer) { frameskip[(maketic / ticdup) & 3] = (oldnettics > mastertics); } else { frameskip[(maketic / ticdup) & 3] = (oldnettics - mastertics) > 3; } if (frameskip[0] && frameskip[1] && frameskip[2] && frameskip[3]) { skiptics = 1; if (debugfile) fprintf(debugfile, "+"); } oldnettics = nettics[0]; } } } // // 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 9 // 1 One byte for the player's number //2-4 Three bytes for the game version (255,high byte,low byte) //5-8 A bit mask for each player the sender knows about // 9 The high bit is set if the sender got the game info // 10 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 NetMode setting // 3 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. struct ArbitrateData { uint32_t playersdetected[MAXNETNODES]; uint8_t gotsetup[MAXNETNODES]; }; bool DoArbitrate (void *userdata) { ArbitrateData *data = (ArbitrateData *)userdata; char *s; uint8_t *stream; int version; int node; int i, j; while (HGetPacket ()) { if (netbuffer[0] == NCMD_EXIT) { I_FatalError ("The game was aborted."); } 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]]; data->playersdetected[node] = (netbuffer[5] << 24) | (netbuffer[6] << 16) | (netbuffer[7] << 8) | netbuffer[8]; if (netbuffer[0] == NCMD_SETUP) { // Sent to host data->gotsetup[node] = netbuffer[9] & 0x80; stream = &netbuffer[10]; } else { // Sent from host stream = &netbuffer[9]; } D_ReadUserInfoStrings (netbuffer[1], &stream, false); if (!nodeingame[node]) { version = (netbuffer[2] << 16) | (netbuffer[3] << 8) | netbuffer[4]; if (version != (0xFF0000 | NETGAMEVERSION)) { I_Error ("Different " GAMENAME " versions cannot play a net game"); } playeringame[netbuffer[1]] = true; nodeingame[node] = true; data->playersdetected[0] |= 1 << netbuffer[1]; StartScreen->NetMessage ("Found %s (node %d, player %d)", players[netbuffer[1]].userinfo.GetName(), node, netbuffer[1]+1); } } else if (netbuffer[0] == NCMD_SETUP+2) // got game info { data->gotsetup[0] = 0x80; ticdup = doomcom.ticdup = netbuffer[1]; NetMode = netbuffer[2]; stream = &netbuffer[3]; s = ReadString (&stream); startmap = s; delete[] s; rngseed = ReadLong (&stream); C_ReadCVars (&stream); } else if (netbuffer[0] == NCMD_SETUP+3) { return true; } } // If everybody already knows everything, it's time to go if (consoleplayer == Net_Arbitrator) { for (i = 0; i < doomcom.numnodes; ++i) if (data->playersdetected[i] != uint32_t(1 << doomcom.numnodes) - 1 || !data->gotsetup[i]) break; if (i == doomcom.numnodes) return true; } netbuffer[2] = 255; netbuffer[3] = (NETGAMEVERSION >> 8) & 255; netbuffer[4] = NETGAMEVERSION & 255; netbuffer[5] = data->playersdetected[0] >> 24; netbuffer[6] = data->playersdetected[0] >> 16; netbuffer[7] = data->playersdetected[0] >> 8; netbuffer[8] = data->playersdetected[0]; if (consoleplayer != Net_Arbitrator) { // Send user info for the local node netbuffer[0] = NCMD_SETUP; netbuffer[1] = consoleplayer; netbuffer[9] = data->gotsetup[0]; stream = &netbuffer[10]; D_WriteUserInfoStrings (consoleplayer, &stream, true); SendSetup (data->playersdetected, data->gotsetup, int(stream - netbuffer)); } else { // Send user info for all nodes netbuffer[0] = NCMD_SETUP+1; for (i = 1; i < doomcom.numnodes; ++i) { for (j = 0; j < doomcom.numnodes; ++j) { // Send info about player j to player i? if ((data->playersdetected[0] & (1<playersdetected[i] & (1<playersdetected, data->gotsetup, int(stream - netbuffer)); } return false; } void D_ArbitrateNetStart (void) { ArbitrateData data; int i; // Return right away if we're just playing with ourselves. if (doomcom.numnodes == 1) return; autostart = true; memset (data.playersdetected, 0, sizeof(data.playersdetected)); memset (data.gotsetup, 0, sizeof(data.gotsetup)); // The arbitrator knows about himself, but the other players must // be told about themselves, in case the host had to adjust their // userinfo (e.g. assign them to a different team). if (consoleplayer == Net_Arbitrator) { data.playersdetected[0] = 1 << consoleplayer; } // Assign nodes to players. The local player is always node 0. // If the local player is not the host, then the host is node 1. // Any remaining players are assigned node numbers in the order // they were detected. 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) { data.gotsetup[0] = 0x80; } StartScreen->NetInit ("Exchanging game information", 1); if (!StartScreen->NetLoop (DoArbitrate, &data)) { exit (0); } if (consoleplayer == Net_Arbitrator) { netbuffer[0] = NCMD_SETUP+3; SendSetup (data.playersdetected, data.gotsetup, 1); } if (debugfile) { for (i = 0; i < doomcom.numnodes; ++i) { fprintf (debugfile, "player %d is on node %d\n", i, nodeforplayer[i]); } } StartScreen->NetDone(); } static void SendSetup (uint32_t playersdetected[MAXNETNODES], uint8_t gotsetup[MAXNETNODES], int len) { if (consoleplayer != Net_Arbitrator) { if (playersdetected[1] & (1 << consoleplayer)) { HSendPacket (1, 10); } else { HSendPacket (1, len); } } else { for (int i = 1; i < doomcom.numnodes; ++i) { if (!gotsetup[i] || netbuffer[0] == NCMD_SETUP+3) { HSendPacket (i, len); } } } } // // D_CheckNetGame // Works out player numbers among the net participants // void D_CheckNetGame (void) { const char *v; int i; for (i = 0; i < MAXNETNODES; i++) { nodeingame[i] = false; nettics[i] = 0; remoteresend[i] = false; // set when local needs tics resendto[i] = 0; // which tic to start sending } // Packet server has proven to be rather slow over the internet. Print a warning about it. v = Args->CheckValue("-netmode"); if (v != NULL && (atoi(v) != 0)) { Printf(TEXTCOLOR_YELLOW "Notice: Using PacketServer (netmode 1) over the internet is prone to running too slow on some internet configurations." "\nIf the game is running well below expected speeds, use netmode 0 (P2P) instead.\n"); } // I_InitNetwork sets doomcom and netgame if (I_InitNetwork ()) { // For now, stop auto selecting PacketServer, as it's more likely to cause confusion. //NetMode = NET_PacketServer; } if (doomcom.id != DOOMCOM_ID) { I_FatalError ("Doomcom buffer invalid!"); } players[0].settings_controller = true; consoleplayer = doomcom.consoleplayer; if (consoleplayer == Net_Arbitrator) { v = Args->CheckValue("-netmode"); if (v != NULL) { NetMode = atoi(v) != 0 ? NET_PacketServer : NET_PeerToPeer; } if (doomcom.numnodes > 1) { Printf("Selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode. (%s)\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server", v != NULL ? "forced" : "auto"); } if (Args->CheckParm("-extratic")) { net_extratic = 1; } } // [RH] Setup user info D_SetupUserInfo (); if (Args->CheckParm ("-debugfile")) { char filename[20]; mysnprintf (filename, countof(filename), "debug%i.txt", consoleplayer); Printf ("debug output to: %s\n", filename); debugfile = fopen (filename, "w"); } if (netgame) { GameConfig->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; if (consoleplayer != Net_Arbitrator && doomcom.numnodes > 1) { Printf("Arbitrator selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode.\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server"); } if (!batchrun) 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) { uint8_t *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 = int(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. if (pauseext) r_NoInterpolate = true; bool doWait = cl_capfps || r_NoInterpolate /*|| netgame*/; // get real tics if (doWait) { entertic = I_WaitForTic (oldentertics); } else { entertic = I_GetTime (); } realtics = entertic - oldentertics; oldentertics = entertic; // get available tics NetUpdate (); if (pauseext) return; 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; // Uncapped framerate needs seprate checks if (counts == 0 && !doWait) { // Check possible stall conditions Net_CheckLastReceived(counts); if (realtics >= 1) { C_Ticker(); M_Ticker(); // Repredict the player for new buffered movement P_UnPredictPlayer(); P_PredictPlayer(&players[consoleplayer]); } return; } if (counts < 1) counts = 1; if (debugfile) fprintf (debugfile, "=======real: %i avail: %i game: %i\n", realtics, availabletics, counts); // 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"); // Check possible stall conditions Net_CheckLastReceived (counts); // Update time returned by I_GetTime, but only if we are stuck in this loop if (lowtic < gametic + counts) I_SetFrameTime(); // don't stay in here forever -- give the menu a chance to work if (I_GetTime () - entertic >= 1) { C_Ticker (); M_Ticker (); // Repredict the player for new buffered movement P_UnPredictPlayer(); P_PredictPlayer(&players[consoleplayer]); return; } } //Tic lowtic is high enough to process this gametic. Clear all possible waiting info hadlate = false; for (i = 0; i < MAXPLAYERS; i++) players[i].waiting = false; lastglobalrecvtime = I_GetTime (); //Update the last time the game tic'd over // run the count tics if (counts > 0) { P_UnPredictPlayer(); while (counts--) { if (gametic > lowtic) { I_Error ("gametic>lowtic"); } if (advancedemo) { D_DoAdvanceDemo (); } if (debugfile) fprintf (debugfile, "run tic %d\n", gametic); C_Ticker (); M_Ticker (); G_Ticker(); gametic++; NetUpdate (); // check for new console commands } P_PredictPlayer(&players[consoleplayer]); S_UpdateSounds (players[consoleplayer].camera); // move positional sounds } } void Net_CheckLastReceived (int counts) { // [Ed850] Check to see the last time a packet was received. // If it's longer then 3 seconds, a node has likely stalled. if (I_GetTime() - lastglobalrecvtime >= TICRATE * 3) { lastglobalrecvtime = I_GetTime(); //Bump the count if (NetMode == NET_PeerToPeer || consoleplayer == Net_Arbitrator) { //Keep the local node in the for loop so we can still log any cases where the local node is /somehow/ late. //However, we don't send a resend request for sanity reasons. for (int i = 0; i < doomcom.numnodes; i++) { if (nodeingame[i] && nettics[i] <= gametic + counts) { if (debugfile && !players[playerfornode[i]].waiting) fprintf(debugfile, "%i is slow (%i to %i)\n", i, nettics[i], gametic + counts); //Send resend request to the late node. Also mark the node as waiting to display it in the hud. if (i != 0) remoteresend[i] = players[playerfornode[i]].waiting = hadlate = true; } else players[playerfornode[i]].waiting = false; } } else { //Send a resend request to the Arbitrator, as it's obvious we are stuck here. if (debugfile && !players[Net_Arbitrator].waiting) fprintf(debugfile, "Arbitrator is slow (%i to %i)\n", nettics[nodeforplayer[Net_Arbitrator]], gametic + counts); //Send resend request to the Arbitrator. Also mark the Arbitrator as waiting to display it in the hud. remoteresend[nodeforplayer[Net_Arbitrator]] = players[Net_Arbitrator].waiting = hadlate = true; } } } void Net_NewMakeTic (void) { specials.NewMakeTic (); } void Net_WriteByte (uint8_t 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 uint8_t *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) { M_Free (m_Data); m_Data = NULL; } m_Len = m_BufferLen = 0; } void FDynamicBuffer::SetData (const uint8_t *data, int len) { if (len > m_BufferLen) { m_BufferLen = (len + 255) & ~255; m_Data = (uint8_t *)M_Realloc (m_Data, m_BufferLen); } if (data != NULL) { m_Len = len; memcpy (m_Data, data, len); } else { m_Len = 0; } } uint8_t *FDynamicBuffer::GetData (int *len) { if (len) *len = m_Len; return m_Len ? m_Data : NULL; } static int RemoveClass(FLevelLocals *Level, const PClass *cls) { AActor *actor; int removecount = 0; bool player = false; auto iterator = Level->GetThinkerIterator(cls->TypeName); while ((actor = iterator.Next())) { if (actor->IsA(cls)) { // [MC]Do not remove LIVE players. if (actor->player != NULL) { player = true; continue; } // [SP] Don't remove owned inventory objects. if (!actor->IsMapActor()) { continue; } removecount++; actor->ClearCounters(); actor->Destroy(); } } if (player) Printf("Cannot remove live players!\n"); return removecount; } // [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, uint8_t **stream, int player) { uint8_t pos = 0; char *s = NULL; int i; switch (type) { case DEM_SAY: { const char *name = players[player].userinfo.GetName(); uint8_t who = ReadByte (stream); s = ReadString (stream); CleanseString (s); if (((who & 1) == 0) || players[player].userinfo.GetTeam() == TEAM_NONE) { // Said to everyone if (who & 2) { Printf (PRINT_CHAT, TEXTCOLOR_BOLD "* %s" TEXTCOLOR_BOLD "%s" TEXTCOLOR_BOLD "\n", name, s); } else { Printf (PRINT_CHAT, "%s" TEXTCOLOR_CHAT ": %s" TEXTCOLOR_CHAT "\n", name, s); } S_Sound (CHAN_VOICE | CHAN_UI, gameinfo.chatSound, 1, ATTN_NONE); } else if (players[player].userinfo.GetTeam() == players[consoleplayer].userinfo.GetTeam()) { // Said only to members of the player's team if (who & 2) { Printf (PRINT_TEAMCHAT, TEXTCOLOR_BOLD "* (%s" TEXTCOLOR_BOLD ")%s" TEXTCOLOR_BOLD "\n", name, s); } else { Printf (PRINT_TEAMCHAT, "(%s" TEXTCOLOR_TEAMCHAT "): %s" TEXTCOLOR_TEAMCHAT "\n", name, s); } S_Sound (CHAN_VOICE | CHAN_UI, gameinfo.chatSound, 1, ATTN_NONE); } } break; case DEM_MUSICCHANGE: s = ReadString (stream); S_ChangeMusic (s); break; case DEM_PRINT: s = ReadString (stream); Printf ("%s", s); break; case DEM_CENTERPRINT: s = ReadString (stream); C_MidPrint (SmallFont, 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, ReadLong (stream)); break; case DEM_TAKECHEAT: s = ReadString (stream); cht_Take (&players[player], s, ReadLong (stream)); break; case DEM_SETINV: s = ReadString(stream); i = ReadLong(stream); cht_SetInv(&players[player], s, i, !!ReadByte(stream)); break; case DEM_WARPCHEAT: { int x, y, z; x = ReadWord (stream); y = ReadWord (stream); z = ReadWord (stream); P_TeleportMove (players[player].mo, DVector3(x, y, z), 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); // Using LEVEL_NOINTERMISSION tends to throw the game out of sync. // That was a long time ago. Maybe it works now? primaryLevel->flags |= LEVEL_CHANGEMAPCHEAT; primaryLevel->ChangeLevel(s, pos, 0); break; case DEM_SUICIDE: cht_Suicide (&players[player]); break; case DEM_ADDBOT: primaryLevel->BotInfo.TryAddBot (primaryLevel, stream, player); break; case DEM_KILLBOTS: primaryLevel->BotInfo.RemoveAllBots (primaryLevel, true); Printf ("Removed all bots\n"); break; case DEM_CENTERVIEW: players[player].centering = true; break; case DEM_INVUSEALL: if (gamestate == GS_LEVEL && !paused) { AActor *item = players[player].mo->Inventory; auto pitype = PClass::FindActor(NAME_PuzzleItem); while (item != nullptr) { AActor *next = item->Inventory; IFVIRTUALPTRNAME(item, NAME_Inventory, UseAll) { VMValue param[] = { item, players[player].mo }; VMCall(func, param, 2, nullptr, 0); } item = next; } } break; case DEM_INVUSE: case DEM_INVDROP: { uint32_t which = ReadLong (stream); int amt = -1; if (type == DEM_INVDROP) amt = ReadLong(stream); if (gamestate == GS_LEVEL && !paused && players[player].playerstate != PST_DEAD) { auto 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, amt); } } } } break; case DEM_SUMMON: case DEM_SUMMONFRIEND: case DEM_SUMMONFOE: case DEM_SUMMONMBF: case DEM_SUMMON2: case DEM_SUMMONFRIEND2: case DEM_SUMMONFOE2: { PClassActor *typeinfo; int angle = 0; int16_t tid = 0; uint8_t special = 0; int args[5]; s = ReadString (stream); if (type >= DEM_SUMMON2 && type <= DEM_SUMMONFOE2) { angle = ReadWord(stream); tid = ReadWord(stream); special = ReadByte(stream); for(i = 0; i < 5; i++) args[i] = ReadLong(stream); } typeinfo = PClass::FindActor(s); if (typeinfo != NULL) { AActor *source = players[player].mo; if (source != NULL) { if (GetDefaultByType (typeinfo)->flags & MF_MISSILE) { P_SpawnPlayerMissile (source, 0, 0, 0, typeinfo, source->Angles.Yaw); } else { const AActor *def = GetDefaultByType (typeinfo); DVector3 spawnpos = source->Vec3Angle(def->radius * 2 + source->radius, source->Angles.Yaw, 8.); AActor *spawned = Spawn (primaryLevel, typeinfo, spawnpos, ALLOW_REPLACE); if (spawned != NULL) { if (type == DEM_SUMMONFRIEND || type == DEM_SUMMONFRIEND2 || type == DEM_SUMMONMBF) { if (spawned->CountsAsKill()) { primaryLevel->total_monsters--; } spawned->FriendPlayer = player + 1; spawned->flags |= MF_FRIENDLY; spawned->LastHeard = players[player].mo; spawned->health = spawned->SpawnHealth(); if (type == DEM_SUMMONMBF) spawned->flags3 |= MF3_NOBLOCKMONST; } else if (type == DEM_SUMMONFOE || type == DEM_SUMMONFOE2) { spawned->FriendPlayer = 0; spawned->flags &= ~MF_FRIENDLY; spawned->health = spawned->SpawnHealth(); } if (type >= DEM_SUMMON2 && type <= DEM_SUMMONFOE2) { spawned->Angles.Yaw = source->Angles.Yaw - angle; spawned->tid = tid; spawned->special = special; for(i = 0; i < 5; i++) { spawned->args[i] = args[i]; } if(tid) spawned->AddToHash(); } } } } } } break; case DEM_SPRAY: s = ReadString(stream); SprayDecal(players[player].mo, s); break; case DEM_MDK: s = ReadString(stream); cht_DoMDK(&players[player], s); break; case DEM_PAUSE: if (gamestate == GS_LEVEL) { if (paused) { paused = 0; S_ResumeSound (false); } else { paused = player + 1; S_PauseSound (false, false); } } break; case DEM_SAVEGAME: if (gamestate == GS_LEVEL) { s = ReadString (stream); savegamefile = s; delete[] s; s = ReadString (stream); savedescription = s; 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_CHECKAUTOSAVE: // Do not autosave in multiplayer games or when dead. // For demo playback, DEM_DOAUTOSAVE already exists in the demo if the // autosave happened. And if it doesn't, we must not generate it. if (multiplayer || demoplayback || players[consoleplayer].playerstate != PST_LIVE || disableautosave >= 2 || autosavecount == 0) { break; } Net_WriteByte (DEM_DOAUTOSAVE); break; case DEM_DOAUTOSAVE: gameaction = ga_autosave; break; case DEM_FOV: { float newfov = ReadFloat (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 = ReadFloat (stream); break; case DEM_RUNSCRIPT: case DEM_RUNSCRIPT2: { int snum = ReadWord (stream); int argn = ReadByte (stream); RunScript(stream, players[player].mo, snum, argn, (type == DEM_RUNSCRIPT2) ? ACS_ALWAYS : 0); } break; case DEM_RUNNAMEDSCRIPT: { s = ReadString(stream); int argn = ReadByte(stream); RunScript(stream, players[player].mo, -FName(s), argn & 127, (argn & 128) ? ACS_ALWAYS : 0); } break; case DEM_RUNSPECIAL: { int snum = ReadWord(stream); int argn = ReadByte(stream); int arg[5] = { 0, 0, 0, 0, 0 }; for (i = 0; i < argn; ++i) { int argval = ReadLong(stream); if ((unsigned)i < countof(arg)) { arg[i] = argval; } } if (!CheckCheatmode(player == consoleplayer)) { P_ExecuteSpecial(primaryLevel, snum, NULL, players[player].mo, false, arg[0], arg[1], arg[2], arg[3], arg[4]); } } break; case DEM_CROUCH: if (gamestate == GS_LEVEL && players[player].mo != NULL && players[player].health > 0 && !(players[player].oldbuttons & BT_JUMP) && !P_IsPlayerTotallyFrozen(&players[player])) { players[player].crouching = players[player].crouchdir < 0 ? 1 : -1; } break; case DEM_MORPHEX: { s = ReadString (stream); FString msg = cht_Morph (players + player, PClass::FindActor (s), false); if (player == consoleplayer) { Printf ("%s\n", msg[0] != '\0' ? msg.GetChars() : "Morph failed."); } } break; case DEM_ADDCONTROLLER: { uint8_t playernum = ReadByte (stream); players[playernum].settings_controller = true; if (consoleplayer == playernum || consoleplayer == Net_Arbitrator) Printf ("%s has been added to the controller list.\n", players[playernum].userinfo.GetName()); } break; case DEM_DELCONTROLLER: { uint8_t playernum = ReadByte (stream); players[playernum].settings_controller = false; if (consoleplayer == playernum || consoleplayer == Net_Arbitrator) Printf ("%s has been removed from the controller list.\n", players[playernum].userinfo.GetName()); } break; case DEM_KILLCLASSCHEAT: { s = ReadString (stream); int killcount = 0; PClassActor *cls = PClass::FindActor(s); if (cls != NULL) { killcount = primaryLevel->Massacre(false, cls->TypeName); PClassActor *cls_rep = cls->GetReplacement(primaryLevel); if (cls != cls_rep) { killcount += primaryLevel->Massacre(false, cls_rep->TypeName); } Printf ("Killed %d monsters of type %s.\n",killcount, s); } else { Printf ("%s is not an actor class.\n", s); } } break; case DEM_REMOVE: { s = ReadString(stream); int removecount = 0; PClassActor *cls = PClass::FindActor(s); if (cls != NULL && cls->IsDescendantOf(RUNTIME_CLASS(AActor))) { removecount = RemoveClass(primaryLevel, cls); const PClass *cls_rep = cls->GetReplacement(primaryLevel); if (cls != cls_rep) { removecount += RemoveClass(primaryLevel, cls_rep); } Printf("Removed %d actors of type %s.\n", removecount, s); } else { Printf("%s is not an actor class.\n", s); } } break; case DEM_CONVREPLY: case DEM_CONVCLOSE: case DEM_CONVNULL: P_ConversationCommand (type, player, stream); break; case DEM_SETSLOT: case DEM_SETSLOTPNUM: { int pnum; if (type == DEM_SETSLOTPNUM) { pnum = ReadByte(stream); } else { pnum = player; } unsigned int slot = ReadByte(stream); int count = ReadByte(stream); if (slot < NUM_WEAPON_SLOTS) { players[pnum].weapons.ClearSlot(slot); } for(i = 0; i < count; ++i) { PClassActor *wpn = Net_ReadWeapon(stream); players[pnum].weapons.AddSlot(slot, wpn, pnum == consoleplayer); } } break; case DEM_ADDSLOT: { int slot = ReadByte(stream); PClassActor *wpn = Net_ReadWeapon(stream); players[player].weapons.AddSlot(slot, wpn, player == consoleplayer); } break; case DEM_ADDSLOTDEFAULT: { int slot = ReadByte(stream); PClassActor *wpn = Net_ReadWeapon(stream); players[player].weapons.AddSlotDefault(slot, wpn, player == consoleplayer); } break; case DEM_SETPITCHLIMIT: players[player].MinPitch = -(double)ReadByte(stream); // up players[player].MaxPitch = (double)ReadByte(stream); // down break; case DEM_ADVANCEINTER: F_AdvanceIntermission(); break; case DEM_REVERTCAMERA: players[player].camera = players[player].mo; break; case DEM_FINISHGAME: // Simulate an end-of-game action primaryLevel->ChangeLevel(NULL, 0, 0); break; case DEM_NETEVENT: { s = ReadString(stream); int argn = ReadByte(stream); int arg[3] = { 0, 0, 0 }; for (int i = 0; i < 3; i++) arg[i] = ReadLong(stream); bool manual = !!ReadByte(stream); primaryLevel->localEventManager->Console(player, s, arg[0], arg[1], arg[2], manual); } break; default: I_Error ("Unknown net command: %d", type); break; } if (s) delete[] s; } // Used by DEM_RUNSCRIPT, DEM_RUNSCRIPT2, and DEM_RUNNAMEDSCRIPT static void RunScript(uint8_t **stream, AActor *pawn, int snum, int argn, int always) { int arg[4] = { 0, 0, 0, 0 }; int i; for (i = 0; i < argn; ++i) { int argval = ReadLong(stream); if ((unsigned)i < countof(arg)) { arg[i] = argval; } } P_StartScript(pawn->Level, pawn, NULL, snum, primaryLevel->MapName, arg, MIN(countof(arg), argn), ACS_NET | always); } void Net_SkipCommand (int type, uint8_t **stream) { uint8_t t; size_t skip; switch (type) { case DEM_SAY: skip = strlen ((char *)(*stream + 1)) + 2; break; case DEM_ADDBOT: skip = strlen ((char *)(*stream + 1)) + 6; break; case DEM_GIVECHEAT: case DEM_TAKECHEAT: skip = strlen ((char *)(*stream)) + 5; break; case DEM_SETINV: skip = strlen((char *)(*stream)) + 6; break; case DEM_NETEVENT: skip = strlen((char *)(*stream)) + 15; break; case DEM_SUMMON2: case DEM_SUMMONFRIEND2: case DEM_SUMMONFOE2: skip = strlen ((char *)(*stream)) + 26; break; case DEM_CHANGEMAP2: skip = strlen((char *)(*stream + 1)) + 2; break; case DEM_MUSICCHANGE: case DEM_PRINT: case DEM_CENTERPRINT: case DEM_UINFCHANGED: case DEM_CHANGEMAP: case DEM_SUMMON: case DEM_SUMMONFRIEND: case DEM_SUMMONFOE: case DEM_SUMMONMBF: case DEM_REMOVE: case DEM_SPRAY: case DEM_MORPHEX: case DEM_KILLCLASSCHEAT: case DEM_MDK: skip = strlen ((char *)(*stream)) + 1; break; case DEM_WARPCHEAT: skip = 6; break; case DEM_INVUSE: case DEM_FOV: case DEM_MYFOV: skip = 4; break; case DEM_INVDROP: skip = 8; break; case DEM_GENERICCHEAT: case DEM_DROPPLAYER: case DEM_ADDCONTROLLER: case DEM_DELCONTROLLER: 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: case DEM_RUNSCRIPT2: skip = 3 + *(*stream + 2) * 4; break; case DEM_RUNNAMEDSCRIPT: skip = strlen((char *)(*stream)) + 2; skip += ((*(*stream + skip - 1)) & 127) * 4; break; case DEM_RUNSPECIAL: skip = 3 + *(*stream + 2) * 4; break; case DEM_CONVREPLY: skip = 3; break; case DEM_SETSLOT: case DEM_SETSLOTPNUM: { skip = 2 + (type == DEM_SETSLOTPNUM); for(int numweapons = (*stream)[skip-1]; numweapons > 0; numweapons--) { skip += 1 + ((*stream)[skip] >> 7); } } break; case DEM_ADDSLOT: case DEM_ADDSLOTDEFAULT: skip = 2 + ((*stream)[1] >> 7); break; case DEM_SETPITCHLIMIT: skip = 2; break; default: return; } *stream += skip; } // This was taken out of shared_hud, because UI code shouldn't do low level calculations that may change if the backing implementation changes. int Net_GetLatency(int *ld, int *ad) { int i, localdelay = 0, arbitratordelay = 0; for (i = 0; i < BACKUPTICS; i++) localdelay += netdelay[0][i]; for (i = 0; i < BACKUPTICS; i++) arbitratordelay += netdelay[nodeforplayer[Net_Arbitrator]][i]; arbitratordelay = ((arbitratordelay / BACKUPTICS) * ticdup) * (1000 / TICRATE); localdelay = ((localdelay / BACKUPTICS) * ticdup) * (1000 / TICRATE); int severity = 0; if (MAX(localdelay, arbitratordelay) > 200) { severity = 1; } if (MAX(localdelay, arbitratordelay) > 400) { severity = 2; } if (MAX(localdelay, arbitratordelay) >= ((BACKUPTICS / 2 - 1) * ticdup) * (1000 / TICRATE)) { severity = 3; } *ld = localdelay; *ad = arbitratordelay; return severity; } // [RH] List "ping" times CCMD (pings) { int i; for (i = 0; i < MAXPLAYERS; i++) if (playeringame[i]) Printf ("% 4" PRId64 " %s\n", currrecvtime[i] - lastrecvtime[i], players[i].userinfo.GetName()); } //========================================================================== // // Network_Controller // // Implement players who have the ability to change settings in a network // game. // //========================================================================== static void Network_Controller (int playernum, bool add) { if (consoleplayer != Net_Arbitrator) { Printf ("This command is only accessible to the net arbitrator.\n"); return; } if (players[playernum].settings_controller && add) { Printf ("%s is already on the setting controller list.\n", players[playernum].userinfo.GetName()); return; } if (!players[playernum].settings_controller && !add) { Printf ("%s is not on the setting controller list.\n", players[playernum].userinfo.GetName()); return; } if (!playeringame[playernum]) { Printf ("Player (%d) not found!\n", playernum); return; } if (players[playernum].Bot != NULL) { Printf ("Bots cannot be added to the controller list.\n"); return; } if (playernum == Net_Arbitrator) { Printf ("The net arbitrator cannot have their status changed on this list.\n"); return; } if (add) Net_WriteByte (DEM_ADDCONTROLLER); else Net_WriteByte (DEM_DELCONTROLLER); Net_WriteByte (playernum); } //========================================================================== // // CCMD net_addcontroller // //========================================================================== CCMD (net_addcontroller) { if (!netgame) { Printf ("This command can only be used when playing a net game.\n"); return; } if (argv.argc () < 2) { Printf ("Usage: net_addcontroller \n"); return; } Network_Controller (atoi (argv[1]), true); } //========================================================================== // // CCMD net_removecontroller // //========================================================================== CCMD (net_removecontroller) { if (!netgame) { Printf ("This command can only be used when playing a net game.\n"); return; } if (argv.argc () < 2) { Printf ("Usage: net_removecontroller \n"); return; } Network_Controller (atoi (argv[1]), false); } //========================================================================== // // CCMD net_listcontrollers // //========================================================================== CCMD (net_listcontrollers) { if (!netgame) { Printf ("This command can only be used when playing a net game.\n"); return; } Printf ("The following players can change the game settings:\n"); for (int i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i]) continue; if (players[i].settings_controller) { Printf ("- %s\n", players[i].userinfo.GetName()); } } }