diff --git a/src/common/console/c_dispatch.cpp b/src/common/console/c_dispatch.cpp index 8b6d738a0..c4a364a61 100644 --- a/src/common/console/c_dispatch.cpp +++ b/src/common/console/c_dispatch.cpp @@ -190,6 +190,22 @@ static const char *KeyConfCommands[] = // CODE -------------------------------------------------------------------- +bool C_IsValidInt(const char* arg, int& value, int base) +{ + char* end_read; + value = std::strtol(arg, &end_read, base); + ptrdiff_t chars_read = end_read - arg; + return chars_read == strlen(arg); +} + +bool C_IsValidFloat(const char* arg, double& value) +{ + char* end_read; + value = std::strtod(arg, &end_read); + ptrdiff_t chars_read = end_read - arg; + return chars_read == strlen(arg); +} + void C_DoCommand (const char *cmd, int keynum) { FConsoleCommand *com; diff --git a/src/common/console/c_dispatch.h b/src/common/console/c_dispatch.h index e8b2bfdf0..13b2503e5 100644 --- a/src/common/console/c_dispatch.h +++ b/src/common/console/c_dispatch.h @@ -74,6 +74,8 @@ void C_ClearDelayedCommands(); // Process a single console command. Does not handle wait. void C_DoCommand (const char *cmd, int keynum=0); +bool C_IsValidInt(const char* arg, int& value, int base = 10); +bool C_IsValidFloat(const char* arg, double& value); FExecList *C_ParseExecFile(const char *file, FExecList *source); void C_SearchForPullins(FExecList *exec, const char *file, class FCommandLine &args); diff --git a/src/common/engine/i_net.cpp b/src/common/engine/i_net.cpp index 0a67062a0..e85e38789 100644 --- a/src/common/engine/i_net.cpp +++ b/src/common/engine/i_net.cpp @@ -75,6 +75,7 @@ #include "cmdlib.h" #include "printf.h" #include "i_interface.h" +#include "c_cvars.h" #include "i_net.h" @@ -104,18 +105,31 @@ typedef int SOCKET; typedef int socklen_t; #endif +constexpr int MaxPlayers = 16; // TODO: This needs to be put in some kind of unified header later +constexpr size_t MaxPasswordSize = 256u; + bool netgame, multiplayer; int consoleplayer; // i.e. myconnectindex in Build. doomcom_t doomcom; +CUSTOM_CVAR(String, net_password, "", CVAR_IGNORE) +{ + if (strlen(self) + 1 > MaxPasswordSize) + { + self = ""; + Printf(TEXTCOLOR_RED "Password cannot be greater than 255 characters\n"); + } +} + +FClientStack NetworkClients; + // // NETWORKING // static u_short DOOMPORT = (IPPORT_USERRESERVED + 29); static SOCKET mysocket = INVALID_SOCKET; -static sockaddr_in sendaddress[MAXNETNODES]; -static uint8_t sendplayer[MAXNETNODES]; +static sockaddr_in sendaddress[MaxPlayers]; #ifdef __WIN32__ const char *neterror (void); @@ -134,12 +148,20 @@ enum PRE_ALLHEREACK, // Sent from guest to host to acknowledge PRE_ALLHEREACK receipt PRE_GO, // Sent from host to guest to continue game startup PRE_IN_PROGRESS, // Sent from host to guest if the game has already started + PRE_WRONG_PASSWORD, // Sent from host to guest if their provided password was wrong }; // Set PreGamePacket.fake to this so that the game rejects any pregame packets // after it starts. This translates to NCMD_SETUP|NCMD_MULTI. #define PRE_FAKE 0x30 +struct PreGameConnectPacket +{ + uint8_t Fake; + uint8_t Message; + char Password[MaxPasswordSize]; +}; + struct PreGamePacket { uint8_t Fake; @@ -154,17 +176,16 @@ struct PreGamePacket { uint32_t address; uint16_t port; - uint8_t player; - uint8_t pad; - } machines[MAXNETNODES]; + uint16_t pad; + } machines[MaxPlayers]; }; uint8_t TransmitBuffer[TRANSMIT_SIZE]; FString GetPlayerName(int num) { - if (sysCallbacks.GetPlayerName) return sysCallbacks.GetPlayerName(sendplayer[num]); - else return FStringf("Player %d", sendplayer[num] + 1); + if (sysCallbacks.GetPlayerName) return sysCallbacks.GetPlayerName(num); + else return FStringf("Player %d", num + 1); } // @@ -200,22 +221,34 @@ void BindToLocalPort (SOCKET s, u_short port) I_FatalError ("BindToPort: %s", neterror ()); } +void I_ClearNode(int node) +{ + memset(&sendaddress[node], 0, sizeof(sendaddress[node])); +} + +static bool I_ShouldStartNetGame() +{ + if (doomcom.consoleplayer != 0) + return false; + + return StartWindow->ShouldStartNet(); +} + int FindNode (const sockaddr_in *address) { - int i; + int i = 0; // find remote node number - for (i = 0; isin_addr.s_addr == sendaddress[i].sin_addr.s_addr && address->sin_port == sendaddress[i].sin_port) + { break; - - if (i == doomcom.numnodes) - { - // packet is not from one of the players (new game broadcast?) - i = -1; + } } - return i; + + return (i == doomcom.numplayers) ? -1 : i; } // @@ -249,8 +282,8 @@ void PacketSend (void) { // Printf("send %lu/%d\n", size, doomcom.datalength); c = sendto(mysocket, (char *)TransmitBuffer, size, - 0, (sockaddr *)&sendaddress[doomcom.remotenode], - sizeof(sendaddress[doomcom.remotenode])); + 0, (sockaddr *)&sendaddress[doomcom.remoteplayer], + sizeof(sendaddress[doomcom.remoteplayer])); } else { @@ -262,8 +295,8 @@ void PacketSend (void) { // Printf("send %d\n", doomcom.datalength); c = sendto(mysocket, (char *)doomcom.data, doomcom.datalength, - 0, (sockaddr *)&sendaddress[doomcom.remotenode], - sizeof(sendaddress[doomcom.remotenode])); + 0, (sockaddr *)&sendaddress[doomcom.remoteplayer], + sizeof(sendaddress[doomcom.remoteplayer])); } } // if (c == -1) @@ -315,7 +348,7 @@ void PacketGet (void) } else { - doomcom.remotenode = -1; // no packet + doomcom.remoteplayer = -1; // no packet return; } } @@ -331,7 +364,7 @@ void PacketGet (void) { Printf("Net decompression failed (zlib error %s)\n", M_ZLibError(err).GetChars()); // Pretend no packet - doomcom.remotenode = -1; + doomcom.remoteplayer = -1; return; } c = msgsize + 1; @@ -350,11 +383,11 @@ void PacketGet (void) uint8_t msg[] = { PRE_FAKE, PRE_IN_PROGRESS }; PreSend(msg, 2, &fromaddress); } - doomcom.remotenode = -1; + doomcom.remoteplayer = -1; return; } - doomcom.remotenode = node; + doomcom.remoteplayer = node; doomcom.datalength = (short)c; } @@ -373,22 +406,6 @@ sockaddr_in *PreGet (void *buffer, int bufferlen, bool noabort) int err = WSAGetLastError(); if (err == WSAEWOULDBLOCK || (noabort && err == WSAECONNRESET)) return NULL; // no packet - - if (doomcom.consoleplayer == 0) - { - int node = FindNode(&fromaddress); - I_NetMessage("Got unexpected disconnect."); - doomcom.numnodes--; - for (; node < doomcom.numnodes; ++node) - sendaddress[node] = sendaddress[node + 1]; - - // Let remaining guests know that somebody left. - SendConAck(doomcom.numnodes, doomcom.numplayers); - } - else - { - I_NetError("The host disbanded the game unexpectedly"); - } } return &fromaddress; } @@ -439,7 +456,7 @@ void BuildAddress (sockaddr_in *address, const char *name) if (!isnamed) { address->sin_addr.s_addr = inet_addr (target.GetChars()); - Printf ("Node number %d, address %s\n", doomcom.numnodes, target.GetChars()); + Printf ("Node number %d, address %s\n", doomcom.numplayers, target.GetChars()); } else { @@ -448,7 +465,7 @@ void BuildAddress (sockaddr_in *address, const char *name) I_FatalError ("gethostbyname: couldn't find %s\n%s", target.GetChars(), neterror()); address->sin_addr.s_addr = *(int *)hostentry->h_addr_list[0]; Printf ("Node number %d, hostname %s\n", - doomcom.numnodes, hostentry->h_name); + doomcom.numplayers, hostentry->h_name); } } @@ -489,17 +506,17 @@ void StartNetwork (bool autoPort) #endif } -void SendAbort (void) +void SendAbort (int connected) { uint8_t dis[2] = { PRE_FAKE, PRE_DISCONNECT }; int i, j; - if (doomcom.numnodes > 1) + if (connected > 1) { - if (consoleplayer == 0) + if (doomcom.consoleplayer == 0) { // The host needs to let everyone know - for (i = 1; i < doomcom.numnodes; ++i) + for (i = 1; i < connected; ++i) { for (j = 4; j > 0; --j) { @@ -512,7 +529,7 @@ void SendAbort (void) // Guests only need to let the host know. for (i = 4; i > 0; --i) { - PreSend (dis, 2, &sendaddress[1]); + PreSend (dis, 2, &sendaddress[0]); } } } @@ -526,17 +543,17 @@ void SendConAck (int num_connected, int num_needed) packet.Message = PRE_CONACK; packet.NumNodes = num_needed; packet.NumPresent = num_connected; - for (int node = 1; node < doomcom.numnodes; ++node) + for (int node = 1; node < num_connected; ++node) { PreSend (&packet, 4, &sendaddress[node]); } - I_NetProgress (doomcom.numnodes); + I_NetProgress (num_connected); } bool Host_CheckForConnects (void *userdata) { - PreGamePacket packet; - int numplayers = (int)(intptr_t)userdata; + PreGameConnectPacket packet; + int* connectedPlayers = (int*)userdata; sockaddr_in *from; int node; @@ -550,7 +567,7 @@ bool Host_CheckForConnects (void *userdata) { case PRE_CONNECT: node = FindNode (from); - if (doomcom.numnodes == numplayers) + if (doomcom.numplayers == *connectedPlayers) { if (node == -1) { @@ -566,13 +583,21 @@ bool Host_CheckForConnects (void *userdata) { if (node == -1) { - node = doomcom.numnodes++; + if (strlen(net_password) > 0 && strcmp(net_password, packet.Password)) + { + packet.Message = PRE_WRONG_PASSWORD; + PreSend(&packet, 2, from); + break; + } + + node = *connectedPlayers; + ++*connectedPlayers; sendaddress[node] = *from; I_NetMessage ("Got connect from node %d.", node); } // Let the new guest (and everyone else) know we got their message. - SendConAck (doomcom.numnodes, numplayers); + SendConAck (*connectedPlayers, doomcom.numplayers); } break; @@ -580,27 +605,25 @@ bool Host_CheckForConnects (void *userdata) node = FindNode (from); if (node >= 0) { + I_ClearNode(node); I_NetMessage ("Got disconnect from node %d.", node); - doomcom.numnodes--; - while (node < doomcom.numnodes) + --*connectedPlayers; + while (node < *connectedPlayers) { sendaddress[node] = sendaddress[node+1]; - node++; + ++node; } // Let remaining guests know that somebody left. - SendConAck (doomcom.numnodes, numplayers); + SendConAck (*connectedPlayers, doomcom.numplayers); } break; - - case PRE_KEEPALIVE: - break; } } - if (doomcom.numnodes < numplayers) + if (*connectedPlayers < doomcom.numplayers && !I_ShouldStartNetGame()) { // Send message to everyone as a keepalive - SendConAck(doomcom.numnodes, numplayers); + SendConAck(*connectedPlayers, doomcom.numplayers); return false; } @@ -614,53 +637,53 @@ bool Host_CheckForConnects (void *userdata) node = FindNode (from); if (node >= 0) { - doomcom.numnodes--; - while (node < doomcom.numnodes) + I_ClearNode(node); + --*connectedPlayers; + while (node < *connectedPlayers) { sendaddress[node] = sendaddress[node+1]; - node++; + ++node; } // Let remaining guests know that somebody left. - SendConAck (doomcom.numnodes, numplayers); + SendConAck (*connectedPlayers, doomcom.numplayers); } break; } } - return doomcom.numnodes >= numplayers; + + // TODO: This will need a much better solution later. + if (I_ShouldStartNetGame()) + doomcom.numplayers = *connectedPlayers; + + return *connectedPlayers >= doomcom.numplayers; } bool Host_SendAllHere (void *userdata) { - int *gotack = (int *)userdata; // ackcount is at gotack[MAXNETNODES] + int* gotack = (int*)userdata; + const int mask = (1 << doomcom.numplayers) - 1; PreGamePacket packet; int node; sockaddr_in *from; - // Send out address information to all guests. Guests that have already - // acknowledged receipt effectively get just a heartbeat packet. + // Send out address information to all guests. This is will be the final order + // of the send addresses so each guest will need to rearrange their own in order + // to keep node -> player numbers synchronized. packet.Fake = PRE_FAKE; packet.Message = PRE_ALLHERE; - for (node = 1; node < doomcom.numnodes; node++) + for (node = 1; node < doomcom.numplayers; node++) { int machine, spot = 0; packet.ConsoleNum = node; - if (!gotack[node]) + if (!((*gotack) & (1 << node))) { - for (spot = 0, machine = 1; machine < doomcom.numnodes; machine++) + for (spot = 0; spot < doomcom.numplayers; spot++) { - if (node != machine) - { - packet.machines[spot].address = sendaddress[machine].sin_addr.s_addr; - packet.machines[spot].port = sendaddress[machine].sin_port; - packet.machines[spot].player = node; - - spot++; // fixes problem of new address replacing existing address in - // array; it's supposed to increment the index before getting - // and storing in the packet the next address. - } + packet.machines[spot].address = sendaddress[spot].sin_addr.s_addr; + packet.machines[spot].port = sendaddress[spot].sin_port; } - packet.NumNodes = doomcom.numnodes - 2; + packet.NumNodes = doomcom.numplayers; } else { @@ -672,23 +695,36 @@ bool Host_SendAllHere (void *userdata) // Check for replies. while ( (from = PreGet (&packet, sizeof(packet), false)) ) { - if (packet.Fake == PRE_FAKE && packet.Message == PRE_ALLHEREACK) + if (packet.Fake != PRE_FAKE) + continue; + + if (packet.Message == PRE_ALLHEREACK) { node = FindNode (from); if (node >= 0) + *gotack |= 1 << node; + } + else if (packet.Message == PRE_CONNECT) + { + // If someone is still trying to connect, let them know it's either too + // late or that they need to move on to the next stage. + node = FindNode(from); + if (node == -1) { - if (!gotack[node]) - { - gotack[node] = true; - gotack[MAXNETNODES]++; - } + packet.Message = PRE_ALLFULL; + PreSend(&packet, 2, from); + } + else + { + packet.Message = PRE_CONACK; + packet.NumNodes = packet.NumPresent = doomcom.numplayers; + PreSend(&packet, 4, from); } - PreSend (&packet, 2, from); } } // If everybody has replied, then this loop can end. - return gotack[MAXNETNODES] == doomcom.numnodes - 1; + return ((*gotack) & mask) == mask; } bool HostGame (int i) @@ -696,24 +732,24 @@ bool HostGame (int i) PreGamePacket packet; int numplayers; int node; - int gotack[MAXNETNODES+1]; if ((i == Args->NumArgs() - 1) || !(numplayers = atoi (Args->GetArg(i+1)))) { // No player count specified, assume 2 numplayers = 2; } - if (numplayers > MAXNETNODES) + if (numplayers > MaxPlayers) { - I_FatalError("You cannot host a game with %d players. The limit is currently %d.", numplayers, MAXNETNODES); + I_FatalError("You cannot host a game with %d players. The limit is currently %d.", numplayers, MaxPlayers); } if (numplayers == 1) { // Special case: Only 1 player, so don't bother starting the network + NetworkClients += 0; netgame = false; multiplayer = true; doomcom.id = DOOMCOM_ID; - doomcom.numplayers = doomcom.numnodes = 1; + doomcom.numplayers = 1; doomcom.consoleplayer = 0; return true; } @@ -723,27 +759,41 @@ bool HostGame (int i) // [JC] - this computer is starting the game, therefore it should // be the Net Arbitrator. doomcom.consoleplayer = 0; - Printf ("Console player number: %d\n", doomcom.consoleplayer); - - doomcom.numnodes = 1; + doomcom.numplayers = numplayers; I_NetInit ("Hosting game", numplayers); - // Wait for numplayers-1 different connections - if (!I_NetLoop (Host_CheckForConnects, (void *)(intptr_t)numplayers)) + // Wait for the lobby to be full. + int connectedPlayers = 1; + if (!I_NetLoop (Host_CheckForConnects, (void *)&connectedPlayers)) { - SendAbort(); + SendAbort(connectedPlayers); + throw CExitEvent(0); return false; } + // If the player force started with only themselves in the lobby, start the game + // immediately. + if (doomcom.numplayers <= 1) + { + NetworkClients += 0; + netgame = false; + multiplayer = true; + doomcom.id = DOOMCOM_ID; + doomcom.numplayers = 1; + I_NetDone(); + return true; + } + // Now inform everyone of all machines involved in the game - memset (gotack, 0, sizeof(gotack)); + int gotack = 1; I_NetMessage ("Sending all here."); I_NetInit ("Done waiting", 1); - if (!I_NetLoop (Host_SendAllHere, (void *)gotack)) + if (!I_NetLoop (Host_SendAllHere, (void *)&gotack)) { - SendAbort(); + SendAbort(doomcom.numplayers); + throw CExitEvent(0); return false; } @@ -751,7 +801,7 @@ bool HostGame (int i) I_NetMessage ("Go"); packet.Fake = PRE_FAKE; packet.Message = PRE_GO; - for (node = 1; node < doomcom.numnodes; node++) + for (node = 1; node < doomcom.numplayers; node++) { // If we send the packets eight times to each guest, // hopefully at least one of them will get through. @@ -761,16 +811,10 @@ bool HostGame (int i) } } - I_NetMessage ("Total players: %d", doomcom.numnodes); + I_NetMessage ("Total players: %d", doomcom.numplayers); doomcom.id = DOOMCOM_ID; - doomcom.numplayers = doomcom.numnodes; - // On the host, each player's number is the same as its node number - for (i = 0; i < doomcom.numnodes; ++i) - { - sendplayer[i] = i; - } return true; } @@ -782,16 +826,18 @@ bool Guest_ContactHost (void *userdata) { sockaddr_in *from; PreGamePacket packet; + PreGameConnectPacket sendPacket; // Let the host know we are here. - packet.Fake = PRE_FAKE; - packet.Message = PRE_CONNECT; - PreSend (&packet, 2, &sendaddress[1]); + sendPacket.Fake = PRE_FAKE; + sendPacket.Message = PRE_CONNECT; + memcpy(sendPacket.Password, net_password, strlen(net_password) + 1); + PreSend (&sendPacket, sizeof(sendPacket), &sendaddress[0]); // Listen for a reply. while ( (from = PreGet (&packet, sizeof(packet), true)) ) { - if (packet.Fake == PRE_FAKE && FindNode(from) == 1) + if (packet.Fake == PRE_FAKE && !FindNode(from)) { if (packet.Message == PRE_CONACK) { @@ -812,6 +858,10 @@ bool Guest_ContactHost (void *userdata) { I_NetError("The game was already started."); } + else if (packet.Message == PRE_WRONG_PASSWORD) + { + I_NetError("Invalid password."); + } } } @@ -828,7 +878,7 @@ bool Guest_WaitForOthers (void *userdata) while ( (from = PreGet (&packet, sizeof(packet), false)) ) { - if (packet.Fake != PRE_FAKE || FindNode(from) != 1) + if (packet.Fake != PRE_FAKE || FindNode(from)) { continue; } @@ -839,31 +889,35 @@ bool Guest_WaitForOthers (void *userdata) break; case PRE_ALLHERE: - if (doomcom.numnodes == 2) + if (doomcom.consoleplayer == -1) { - int node; - - doomcom.numnodes = packet.NumNodes + 2; - sendplayer[0] = packet.ConsoleNum; // My player number + doomcom.numplayers = packet.NumNodes; doomcom.consoleplayer = packet.ConsoleNum; + // Don't use the address sent from the host for our own machine. + sendaddress[doomcom.consoleplayer] = sendaddress[1]; + I_NetMessage ("Console player number: %d", doomcom.consoleplayer); - for (node = 0; node < packet.NumNodes; node++) + + for (int node = 1; node < doomcom.numplayers; ++node) { - sendaddress[node+2].sin_addr.s_addr = packet.machines[node].address; - sendaddress[node+2].sin_port = packet.machines[node].port; - sendplayer[node+2] = packet.machines[node].player; + if (node == doomcom.consoleplayer) + continue; + + sendaddress[node].sin_addr.s_addr = packet.machines[node].address; + sendaddress[node].sin_port = packet.machines[node].port; // [JC] - fixes problem of games not starting due to // no address family being assigned to nodes stored in // sendaddress[] from the All Here packet. - sendaddress[node+2].sin_family = AF_INET; + sendaddress[node].sin_family = AF_INET; } + + I_NetMessage("Received All Here, sending ACK."); } - I_NetMessage ("Received All Here, sending ACK."); packet.Fake = PRE_FAKE; packet.Message = PRE_ALLHEREACK; - PreSend (&packet, 2, &sendaddress[1]); + PreSend (&packet, 2, &sendaddress[0]); break; case PRE_GO: @@ -876,10 +930,6 @@ bool Guest_WaitForOthers (void *userdata) } } - packet.Fake = PRE_FAKE; - packet.Message = PRE_KEEPALIVE; - PreSend(&packet, 2, &sendaddress[1]); - return false; } @@ -892,33 +942,32 @@ bool JoinGame (int i) StartNetwork (true); - // Host is always node 1 - BuildAddress (&sendaddress[1], Args->GetArg(i+1)); - sendplayer[1] = 0; - doomcom.numnodes = 2; + // Host is always node 0 + BuildAddress (&sendaddress[0], Args->GetArg(i+1)); + doomcom.numplayers = 2; doomcom.consoleplayer = -1; - // Let host know we are here I_NetInit ("Contacting host", 0); - if (!I_NetLoop (Guest_ContactHost, NULL)) + if (!I_NetLoop (Guest_ContactHost, nullptr)) { - SendAbort(); + SendAbort(2); + throw CExitEvent(0); return false; } // Wait for everyone else to connect - if (!I_NetLoop (Guest_WaitForOthers, 0)) + if (!I_NetLoop (Guest_WaitForOthers, nullptr)) { - SendAbort(); + SendAbort(2); + throw CExitEvent(0); return false; } - I_NetMessage ("Total players: %d", doomcom.numnodes); + I_NetMessage ("Total players: %d", doomcom.numplayers); doomcom.id = DOOMCOM_ID; - doomcom.numplayers = doomcom.numnodes; return true; } @@ -954,19 +1003,20 @@ static int PrivateNetOf(in_addr in) static bool NodesOnSameNetwork() { - int net1; + if (doomcom.consoleplayer != 0) + return false; - net1 = PrivateNetOf(sendaddress[1].sin_addr); + const int firstClient = PrivateNetOf(sendaddress[1].sin_addr); // Printf("net1 = %08x\n", net1); - if (net1 == 0) + if (firstClient == 0) { return false; } - for (int i = 2; i < doomcom.numnodes; ++i) + for (int i = 2; i < doomcom.numplayers; ++i) { - int net = PrivateNetOf(sendaddress[i].sin_addr); + const int net = PrivateNetOf(sendaddress[i].sin_addr); // Printf("Net[%d] = %08x\n", i, net); - if (net != net1) + if (net != firstClient) { return false; } @@ -1004,6 +1054,8 @@ int I_InitNetwork (void) Printf ("using alternate port %i\n", DOOMPORT); } + net_password = Args->CheckValue("-password"); + // parse network game options, // player 1: -host // player x: -join @@ -1018,19 +1070,22 @@ int I_InitNetwork (void) else { // single player game + NetworkClients += 0; netgame = false; multiplayer = false; doomcom.id = DOOMCOM_ID; - doomcom.numplayers = doomcom.numnodes = 1; + doomcom.ticdup = 1; + doomcom.numplayers = 1; doomcom.consoleplayer = 0; return false; } - if (doomcom.numnodes < 3) + + if (doomcom.numplayers < 3) { // Packet server mode with only two players is effectively the same as // peer-to-peer but with some slightly larger packets. return false; } - return doomcom.numnodes > 3 || !NodesOnSameNetwork(); + return !NodesOnSameNetwork(); } @@ -1070,7 +1125,7 @@ void I_NetMessage(const char* text, ...) void I_NetError(const char* error) { - doomcom.numnodes = 0; + doomcom.numplayers = 0; StartWindow->NetClose(); I_FatalError("%s", error); } diff --git a/src/common/engine/i_net.h b/src/common/engine/i_net.h index 15720374d..6fbf6c51e 100644 --- a/src/common/engine/i_net.h +++ b/src/common/engine/i_net.h @@ -5,6 +5,7 @@ // Called by D_DoomMain. int I_InitNetwork (void); +void I_ClearNode(int node); void I_NetCmd (void); void I_NetMessage(const char*, ...); void I_NetError(const char* error); @@ -15,74 +16,70 @@ void I_NetDone(); enum ENetConstants { - MAXNETNODES = 8, // max computers in a game DOOMCOM_ID = 0x12345678, - BACKUPTICS = 36, // number of tics to remember - MAXTICDUP = 5, - LOCALCMDTICS =(BACKUPTICS*MAXTICDUP), + BACKUPTICS = 35 * 5, // Remember up to 5 seconds of data. + MAXTICDUP = 3, + MAXSENDTICS = 35 * 1, // Only send up to 1 second of data at a time. + LOCALCMDTICS = (BACKUPTICS*MAXTICDUP), MAX_MSGLEN = 14000, CMD_SEND = 1, CMD_GET = 2, }; -// [RH] -// New generic packet structure: -// -// Header: -// One byte with following 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(). - enum ENCMD { - NCMD_EXIT = 0x80, - NCMD_RETRANSMIT = 0x40, - NCMD_SETUP = 0x20, - NCMD_MULTI = 0x10, // multiple players in this packet - NCMD_QUITTERS = 0x08, // one or more players just quit (packet server only) - NCMD_COMPRESSED = 0x04, // remainder of packet is compressed + NCMD_EXIT = 0x80, // Client has left the game + NCMD_RETRANSMIT = 0x40, // + NCMD_SETUP = 0x20, // Guest is letting the host know who it is + NCMD_LEVELREADY = 0x10, // After loading a level, guests send this over to the host who then sends it back after all are received + NCMD_QUITTERS = 0x08, // Client is getting info about one or more players quitting (packet server only) + NCMD_COMPRESSED = 0x04, // Remainder of packet is compressed + NCMD_LATENCYACK = 0x02, // A latency packet was just read, so let the sender know. + NCMD_LATENCY = 0x01, // Latency packet, used for measuring RTT. - NCMD_XTICS = 0x03, // packet contains >2 tics - NCMD_2TICS = 0x02, // packet contains 2 tics - NCMD_1TICS = 0x01, // packet contains 1 tic - NCMD_0TICS = 0x00, // packet contains 0 tics + NCMD_USERINFO = NCMD_SETUP + 1, // Guest is getting another client's user info + NCMD_GAMEINFO = NCMD_SETUP + 2, // Guest is getting the state of the game from the host + NCMD_GAMEREADY = NCMD_SETUP + 3, // Host has verified the game is ready to be started }; +struct FClientStack : public TArray +{ + inline bool InGame(int i) const { return Find(i) < Size(); } + + void operator+=(const int i) + { + if (!InGame(i)) + SortedInsert(i); + } + + void operator-=(const int i) + { + Delete(Find(i)); + } +}; + +extern FClientStack NetworkClients; + // // Network packet data. // struct doomcom_t { - uint32_t id; // should be DOOMCOM_ID - int16_t intnum; // DOOM executes an int to execute commands - -// communication between DOOM and the driver - int16_t command; // CMD_SEND or CMD_GET - int16_t remotenode; // dest for send, set by get (-1 = no packet). - int16_t datalength; // bytes in data to be sent - -// info common to all nodes - int16_t numnodes; // console is always node 0. - int16_t ticdup; // 1 = no duplication, 2-5 = dup for slow nets - -// info specific to this node - int16_t consoleplayer; + // info common to all nodes + uint32_t id; // should be DOOMCOM_ID + int16_t ticdup; // 1 = no duplication, 2-3 = dup for slow nets int16_t numplayers; -// packet data to be sent - uint8_t data[MAX_MSGLEN]; + // info specific to this node + int16_t consoleplayer; + // communication between DOOM and the driver + int16_t command; // CMD_SEND or CMD_GET + int16_t remoteplayer; // dest for send, set by get (-1 = no packet). + // packet data to be sent + int16_t datalength; // bytes in data to be sent + uint8_t data[MAX_MSGLEN]; }; extern doomcom_t doomcom; diff --git a/src/common/engine/st_start.h b/src/common/engine/st_start.h index b09991d04..07bc61469 100644 --- a/src/common/engine/st_start.h +++ b/src/common/engine/st_start.h @@ -56,6 +56,7 @@ public: virtual void NetProgress(int count) {} virtual void NetDone() {} virtual void NetClose() {} + virtual bool ShouldStartNet() { return false; } virtual bool NetLoop(bool (*timer_callback)(void *), void *userdata) { return false; } virtual void AppendStatusLine(const char* status) {} virtual void LoadingStatus(const char* message, int colors) {} @@ -76,6 +77,7 @@ public: void NetMessage(const char* format, ...); // cover for printf void NetDone(); void NetClose(); + bool ShouldStartNet() override; bool NetLoop(bool (*timer_callback)(void*), void* userdata); protected: int NetMaxPos, NetCurPos; diff --git a/src/common/platform/posix/cocoa/st_console.h b/src/common/platform/posix/cocoa/st_console.h index 91f77d124..8b35989f8 100644 --- a/src/common/platform/posix/cocoa/st_console.h +++ b/src/common/platform/posix/cocoa/st_console.h @@ -67,6 +67,7 @@ public: void NetProgress(int count); void NetDone(); void NetClose(); + bool ShouldStartNet(); private: NSWindow* m_window; diff --git a/src/common/platform/posix/cocoa/st_console.mm b/src/common/platform/posix/cocoa/st_console.mm index 95d0ee11e..ce352ca72 100644 --- a/src/common/platform/posix/cocoa/st_console.mm +++ b/src/common/platform/posix/cocoa/st_console.mm @@ -536,3 +536,8 @@ void FConsoleWindow::NetClose() { // TODO: Implement this } + +bool FConsoleWindow::ShouldStartNet() +{ + return false; +} diff --git a/src/common/platform/posix/cocoa/st_start.mm b/src/common/platform/posix/cocoa/st_start.mm index 2cd73104d..948e9f207 100644 --- a/src/common/platform/posix/cocoa/st_start.mm +++ b/src/common/platform/posix/cocoa/st_start.mm @@ -115,6 +115,11 @@ void FBasicStartupScreen::NetClose() FConsoleWindow::GetInstance().NetClose(); } +bool FBasicStartupScreen::ShouldStartNet() +{ + return FConsoleWindow::GetInstance().ShouldStartNet(); +} + bool FBasicStartupScreen::NetLoop(bool (*timerCallback)(void*), void* const userData) { while (true) diff --git a/src/common/platform/posix/i_system.h b/src/common/platform/posix/i_system.h index 5573e26d3..da99cf9e0 100644 --- a/src/common/platform/posix/i_system.h +++ b/src/common/platform/posix/i_system.h @@ -13,7 +13,6 @@ #include "tarray.h" #include "zstring.h" -struct ticcmd_t; struct WadStuff; #ifndef SHARE_DIR diff --git a/src/common/platform/posix/sdl/st_start.cpp b/src/common/platform/posix/sdl/st_start.cpp index 5bd88684b..5efd4200f 100644 --- a/src/common/platform/posix/sdl/st_start.cpp +++ b/src/common/platform/posix/sdl/st_start.cpp @@ -58,6 +58,7 @@ class FTTYStartupScreen : public FStartupScreen void NetProgress(int count); void NetDone(); void NetClose(); + bool ShouldStartNet(); bool NetLoop(bool (*timer_callback)(void *), void *userdata); protected: bool DidNetInit; @@ -243,6 +244,11 @@ void FTTYStartupScreen::NetClose() // TODO: Implement this } +bool FTTYStartupScreen::ShouldStartNet() +{ + return false; +} + //=========================================================================== // // FTTYStartupScreen :: NetLoop diff --git a/src/common/platform/win32/i_mainwindow.cpp b/src/common/platform/win32/i_mainwindow.cpp index 5bdd3569c..be7d0f796 100644 --- a/src/common/platform/win32/i_mainwindow.cpp +++ b/src/common/platform/win32/i_mainwindow.cpp @@ -133,6 +133,11 @@ void MainWindow::CloseNetStartPane() NetStartWindow::NetClose(); } +bool MainWindow::ShouldStartNetGame() +{ + return NetStartWindow::ShouldStartNetGame(); +} + void MainWindow::SetNetStartProgress(int pos) { NetStartWindow::SetNetStartProgress(pos); diff --git a/src/common/platform/win32/i_mainwindow.h b/src/common/platform/win32/i_mainwindow.h index 83567135c..e2056c674 100644 --- a/src/common/platform/win32/i_mainwindow.h +++ b/src/common/platform/win32/i_mainwindow.h @@ -30,6 +30,7 @@ public: bool RunMessageLoop(bool (*timer_callback)(void*), void* userdata); void HideNetStartPane(); void CloseNetStartPane(); + bool ShouldStartNetGame(); void SetWindowTitle(const char* caption); diff --git a/src/common/platform/win32/i_system.h b/src/common/platform/win32/i_system.h index b70efb78b..f83b3f8c1 100644 --- a/src/common/platform/win32/i_system.h +++ b/src/common/platform/win32/i_system.h @@ -8,7 +8,6 @@ #include "zstring.h" #include "utf8.h" -struct ticcmd_t; struct WadStuff; // [RH] Detects the OS the game is running under. diff --git a/src/common/platform/win32/st_start.cpp b/src/common/platform/win32/st_start.cpp index fceeb3ced..6fb5a5f95 100644 --- a/src/common/platform/win32/st_start.cpp +++ b/src/common/platform/win32/st_start.cpp @@ -206,3 +206,8 @@ void FBasicStartupScreen::NetClose() { mainwindow.CloseNetStartPane(); } + +bool FBasicStartupScreen::ShouldStartNet() +{ + return mainwindow.ShouldStartNetGame(); +} diff --git a/src/common/widgets/netstartwindow.cpp b/src/common/widgets/netstartwindow.cpp index 887f9cc31..1a8845961 100644 --- a/src/common/widgets/netstartwindow.cpp +++ b/src/common/widgets/netstartwindow.cpp @@ -54,13 +54,6 @@ bool NetStartWindow::RunMessageLoop(bool (*newtimer_callback)(void*), void* newu if (Instance->CallbackException) std::rethrow_exception(Instance->CallbackException); - // Even though the comment in FBasicStartupScreen::NetLoop says we should return false, the old code actually throws an exception! - // This effectively also means the function always returns true... - if (!Instance->exitreason) - { - throw CExitEvent(0); - } - return Instance->exitreason; } @@ -70,6 +63,14 @@ void NetStartWindow::NetClose() Instance->OnClose(); } +bool NetStartWindow::ShouldStartNetGame() +{ + if (Instance != nullptr) + return Instance->shouldstart; + + return false; +} + NetStartWindow::NetStartWindow() : Widget(nullptr, WidgetType::Window) { SetWindowBackground(Colorf::fromRgba8(51, 51, 51)); @@ -81,13 +82,16 @@ NetStartWindow::NetStartWindow() : Widget(nullptr, WidgetType::Window) MessageLabel = new TextLabel(this); ProgressLabel = new TextLabel(this); AbortButton = new PushButton(this); + ForceStartButton = new PushButton(this); MessageLabel->SetTextAlignment(TextLabelAlignment::Center); ProgressLabel->SetTextAlignment(TextLabelAlignment::Center); AbortButton->OnClick = [=]() { OnClose(); }; + ForceStartButton->OnClick = [=]() { ForceStart(); }; - AbortButton->SetText("Abort Network Game"); + AbortButton->SetText("Abort"); + ForceStartButton->SetText("Start Game"); CallbackTimer = new Timer(this); CallbackTimer->FuncExpired = [=]() { OnCallbackTimerExpired(); }; @@ -117,6 +121,11 @@ void NetStartWindow::OnClose() DisplayWindow::ExitLoop(); } +void NetStartWindow::ForceStart() +{ + shouldstart = true; +} + void NetStartWindow::OnGeometryChanged() { double w = GetWidth(); @@ -132,7 +141,8 @@ void NetStartWindow::OnGeometryChanged() y += labelheight; y = GetHeight() - 15.0 - AbortButton->GetPreferredHeight(); - AbortButton->SetFrameGeometry((w - 200.0) * 0.5, y, 200.0, AbortButton->GetPreferredHeight()); + AbortButton->SetFrameGeometry((w + 10.0) * 0.5, y, 100.0, AbortButton->GetPreferredHeight()); + ForceStartButton->SetFrameGeometry((w - 210.0) * 0.5, y, 100.0, ForceStartButton->GetPreferredHeight()); } void NetStartWindow::OnCallbackTimerExpired() diff --git a/src/common/widgets/netstartwindow.h b/src/common/widgets/netstartwindow.h index dcc9d688e..b4189af8a 100644 --- a/src/common/widgets/netstartwindow.h +++ b/src/common/widgets/netstartwindow.h @@ -15,6 +15,7 @@ public: static void SetNetStartProgress(int pos); static bool RunMessageLoop(bool (*timer_callback)(void*), void* userdata); static void NetClose(); + static bool ShouldStartNetGame(); private: NetStartWindow(); @@ -25,6 +26,7 @@ private: protected: void OnClose() override; void OnGeometryChanged() override; + virtual void ForceStart(); private: void OnCallbackTimerExpired(); @@ -32,6 +34,7 @@ private: TextLabel* MessageLabel = nullptr; TextLabel* ProgressLabel = nullptr; PushButton* AbortButton = nullptr; + PushButton* ForceStartButton = nullptr; Timer* CallbackTimer = nullptr; @@ -41,6 +44,7 @@ private: void* userdata = nullptr; bool exitreason = false; + bool shouldstart = false; std::exception_ptr CallbackException; diff --git a/src/ct_chat.cpp b/src/ct_chat.cpp index b6d96aa4e..20636e859 100644 --- a/src/ct_chat.cpp +++ b/src/ct_chat.cpp @@ -55,6 +55,7 @@ enum EXTERN_CVAR (Bool, sb_cooperative_enable) EXTERN_CVAR (Bool, sb_deathmatch_enable) EXTERN_CVAR (Bool, sb_teamdeathmatch_enable) +EXTERN_CVAR (Int, cl_showchat) int active_con_scaletext(); @@ -73,8 +74,16 @@ static void CT_BackSpace (); static void ShoveChatStr (const char *str, uint8_t who); static bool DoSubstitution (FString &out, const char *in); -static TArray ChatQueue; +constexpr int MessageLimit = 2; // Clamp the amount of messages you can send in a brief period +constexpr uint64_t MessageThrottleTime = 1000u; // Time in ms that spam messages will be tracked. +constexpr uint64_t SpamCoolDown = 3000u; +static TArray ChatQueue; +static uint64_t ChatThrottle = 0u; +static int ChatSpamCount = 0; +static uint64_t ChatCoolDown = 0u; // Spam limiter + +CVAR (Int, net_chatslowmode, 0, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR (String, chatmacro1, "I'm ready to kick butt!", CVAR_ARCHIVE) CVAR (String, chatmacro2, "I'm OK.", CVAR_ARCHIVE) CVAR (String, chatmacro3, "I'm not looking too good!", CVAR_ARCHIVE) @@ -265,7 +274,7 @@ void CT_Drawer (void) } } - FStringf prompt("%s ", GStrings.GetString("TXT_SAY")); + FStringf prompt("%s ", chatmodeon == 2 && deathmatch && teamplay ? GStrings.GetString("TXT_SAYTEAM") : GStrings.GetString("TXT_SAY")); int x, scalex, y, promptwidth; y = (viewactive || gamestate != GS_LEVEL) ? -displayfont->GetHeight()-2 : -displayfont->GetHeight() - 22; @@ -365,6 +374,24 @@ static void ShoveChatStr (const char *str, uint8_t who) if (str == NULL || str[0] == '\0') return; + if (netgame) + { + const uint64_t time = I_msTime(); + if (time >= ChatThrottle) + { + ChatSpamCount = 0; + } + else if (++ChatSpamCount >= MessageLimit) + { + ChatSpamCount = 0; + ChatCoolDown = time + SpamCoolDown; + } + + ChatThrottle = time + MessageThrottleTime; + if (net_chatslowmode > 0) + ChatCoolDown = max(time + net_chatslowmode * 1000, ChatCoolDown); + } + FString substBuff; if (str[0] == '/' && @@ -508,13 +535,40 @@ static bool DoSubstitution (FString &out, const char *in) CCMD (messagemode) { - if (menuactive == MENU_Off) + if (menuactive != MENU_Off) + return; + + const uint64_t time = I_msTime(); + if (ChatCoolDown > time) { - buttonMap.ResetButtonStates(); - chatmodeon = 1; - C_HideConsole (); - CT_ClearChatMessage (); + Printf("You must wait %d more seconds before being able to chat again\n", int((ChatCoolDown - time) * 0.001)); + return; } + + if (multiplayer && deathmatch) + { + if (cl_showchat < CHAT_GLOBAL) + { + Printf("Global chat is currently disabled\n"); + return; + } + + chatmodeon = 1; + } + else + { + if (cl_showchat < CHAT_TEAM_ONLY) + { + Printf("Team chat is currently disabled\n"); + return; + } + + chatmodeon = 2; + } + + buttonMap.ResetButtonStates(); + C_HideConsole (); + CT_ClearChatMessage (); } CCMD (say) @@ -522,22 +576,75 @@ CCMD (say) if (argv.argc() == 1) { Printf ("Usage: say \n"); + return; + } + + const uint64_t time = I_msTime(); + if (ChatCoolDown > time) + { + Printf("You must wait %d more seconds before being able to chat again\n", int((ChatCoolDown - time) * 0.001)); + return; + } + + // If not in a DM lobby, route it to team chat instead (helps improve chat + // filtering). + if (multiplayer && deathmatch) + { + if (cl_showchat < CHAT_GLOBAL) + { + Printf("Global chat is currently disabled\n"); + } + else + { + ShoveChatStr(argv[1], 0); + } + } + else if (cl_showchat < CHAT_TEAM_ONLY) + { + Printf("Team chat is currently disabled\n"); } else { - ShoveChatStr (argv[1], 0); + ShoveChatStr(argv[1], 1); } } CCMD (messagemode2) { - if (menuactive == MENU_Off) + if (menuactive != MENU_Off) + return; + + const uint64_t time = I_msTime(); + if (ChatCoolDown > time) { - buttonMap.ResetButtonStates(); - chatmodeon = 2; - C_HideConsole (); - CT_ClearChatMessage (); + Printf("You must wait %d more seconds before being able to chat again\n", int((ChatCoolDown - time) * 0.001)); + return; } + + if (multiplayer && deathmatch && !teamplay) + { + if (cl_showchat < CHAT_GLOBAL) + { + Printf("Global chat is currently disabled\n"); + return; + } + + chatmodeon = 1; + } + else + { + if (cl_showchat < CHAT_TEAM_ONLY) + { + Printf("Team chat is currently disabled\n"); + return; + } + + chatmodeon = 2; + } + + buttonMap.ResetButtonStates(); + C_HideConsole(); + CT_ClearChatMessage(); } CCMD (say_team) @@ -545,6 +652,32 @@ CCMD (say_team) if (argv.argc() == 1) { Printf ("Usage: say_team \n"); + return; + } + + const uint64_t time = I_msTime(); + if (ChatCoolDown > time) + { + Printf("You must wait %d more seconds before being able to chat again\n", int((ChatCoolDown - time) * 0.001)); + return; + } + + // If in a DM lobby, route it to global chat instead (helps + // improve chat filtering). + if (multiplayer && deathmatch && !teamplay) + { + if (cl_showchat < CHAT_GLOBAL) + { + Printf("Global chat is currently disabled\n"); + } + else + { + ShoveChatStr(argv[1], 0); + } + } + else if (cl_showchat < CHAT_TEAM_ONLY) + { + Printf("Team chat is currently disabled\n"); } else { diff --git a/src/d_event.h b/src/d_event.h index 409dcb62b..56ae9b07e 100644 --- a/src/d_event.h +++ b/src/d_event.h @@ -99,6 +99,7 @@ enum gameaction_t : int ga_intro, ga_intermission, ga_titleloop, + ga_mapwarp, }; extern gameaction_t gameaction; diff --git a/src/d_main.cpp b/src/d_main.cpp index 875f892bf..9b1f9c530 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -167,7 +167,7 @@ void CloseWidgetResources(); bool D_CheckNetGame (); void D_ProcessEvents (); -void G_BuildTiccmd (ticcmd_t* cmd); +void G_BuildTiccmd (usercmd_t* cmd); void D_DoAdvanceDemo (); void D_LoadWadSettings (); void ParseGLDefs(); @@ -874,7 +874,6 @@ static void DrawRateStuff() static void DrawOverlays() { - NetUpdate (); C_DrawConsole (); M_Drawer (); DrawRateStuff(); @@ -904,6 +903,8 @@ void D_Display () int wipe_type; sector_t *viewsec; + GC::CheckGC(); + if (nodrawers || screen == NULL) return; // for comparative timing / profiling @@ -960,7 +961,7 @@ void D_Display () } // [RH] Allow temporarily disabling wipes - if (NoWipe || !CanWipe()) + if (netgame || NoWipe || !CanWipe()) { if (NoWipe > 0) NoWipe--; wipestart = nullptr; @@ -1153,7 +1154,6 @@ void D_Display () } else { - NetUpdate(); // send out any new accumulation PerformWipe(wipestart, screen->WipeEndScreen(), wipe_type, false, DrawOverlays); } cycles.Unclock(); @@ -1177,7 +1177,6 @@ void D_ErrorCleanup () Net_ClearBuffers (); G_NewInit (); M_ClearMenus (); - singletics = false; playeringame[0] = 1; players[0].playerstate = PST_LIVE; gameaction = ga_fullconsole; @@ -1224,28 +1223,7 @@ void D_DoomLoop () } I_SetFrameTime(); - // process one or more tics - if (singletics) - { - I_StartTic (); - D_ProcessEvents (); - G_BuildTiccmd (&netcmds[consoleplayer][maketic%BACKUPTICS]); - if (advancedemo) - D_DoAdvanceDemo (); - C_Ticker (); - M_Ticker (); - G_Ticker (); - // [RH] Use the consoleplayer's camera to update sounds - S_UpdateSounds (players[consoleplayer].camera); // move positional sounds - gametic++; - maketic++; - GC::CheckGC (); - Net_NewMakeTic (); - } - else - { - TryRunTics (); // will run at least one tic - } + TryRunTics (); // will run at least one tic // Update display, next frame, with current state. I_StartTic (); D_ProcessEvents(); @@ -3466,7 +3444,7 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw // [RH] Run any saved commands from the command line or autoexec.cfg now. gamestate = GS_FULLCONSOLE; - Net_NewMakeTic (); + Net_Initialize(); C_RunDelayedCommands(); gamestate = GS_STARTUP; @@ -3558,6 +3536,7 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw AddCommandString(StoredWarp.GetChars()); StoredWarp = ""; } + gameaction = ga_mapwarp; } else { diff --git a/src/d_net.cpp b/src/d_net.cpp index e1f665eec..1c9ca9202 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -79,969 +79,1208 @@ EXTERN_CVAR (Bool, cl_capfps) EXTERN_CVAR (Bool, vid_vsync) EXTERN_CVAR (Int, vid_maxfps) -//#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]; - extern bool AppActive; -#define netbuffer (doomcom.data) +void P_ClearLevelInterpolation(); -enum { NET_PeerToPeer, NET_PacketServer }; -uint8_t NetMode = NET_PeerToPeer; +struct FNetGameInfo +{ + uint32_t DetectedPlayers[MAXPLAYERS]; + uint8_t GotSetup[MAXPLAYERS]; +}; +enum ELevelStartStatus +{ + LST_READY, + LST_HOST, + LST_WAITING, +}; - -// // 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 +// gametic is the tic about to (or currently being) run. +// ClientTic is the tick the client is currently on and building a command for. // -// a gametic cannot be run until nettics[] > gametic for all players -// -#define RESENDCOUNT 10 -#define PL_DRONE 0x80 // bit flag in doomdata->player +// A world tick cannot be ran until CurrentSequence >= gametic for all clients. -ticcmd_t localcmds[LOCALCMDTICS]; +#define NetBuffer (doomcom.data) +ENetMode NetMode = NET_PeerToPeer; +int ClientTic = 0; +usercmd_t LocalCmds[LOCALCMDTICS] = {}; +int LastSentConsistency = 0; // Last consistency we sent out. If < CurrentConsistency, send them out. +int CurrentConsistency = 0; // Last consistency we generated. +FClientNetState ClientStates[MAXPLAYERS] = {}; -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]; +// If we're sending a packet to ourselves, store it here instead. This is the simplest way to execute +// playback as it means in the world running code itself all player commands are built the exact same way +// instead of having to rely on pulling from the correct local buffers. It also ensures all commands are +// executed over the net at the exact same tick. +static size_t LocalNetBufferSize = 0; +static uint8_t LocalNetBuffer[MAX_MSGLEN] = {}; -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; +static uint8_t CurrentLobbyID = 0u; // Ignore commands not from this lobby (useful when transitioning levels). +static int LastGameUpdate = 0; // Track the last time the game actually ran the world. +static int MutedClients = 0; // Ignore messages from these clients. -int nodeforplayer[MAXPLAYERS]; -int playerfornode[MAXNETNODES]; +static int LevelStartDebug = 0; +static int LevelStartDelay = 0; // While this is > 0, don't start generating packets yet. +static ELevelStartStatus LevelStartStatus = LST_READY; // Listen for when to actually start making tics. +static int LevelStartAck = 0; // Used by the host to determine if everyone has loaded in. -int maketic; -int skiptics; -int ticdup = 1; +static int FullLatencyCycle = MAXSENDTICS * 3; // Give ~3 seconds to gather latency info about clients on boot up. +static int LastLatencyUpdate = 0; // Update average latency every ~1 second. -void D_ProcessEvents (void); -void G_BuildTiccmd (ticcmd_t *cmd); -void D_DoAdvanceDemo (void); +static int EnterTic = 0; +static int LastEnterTic = 0; +static bool bCommandsReset = false; // If true, commands were recently cleared. Don't generate any more tics. -static void SendSetup (uint32_t playersdetected[MAXNETNODES], uint8_t gotsetup[MAXNETNODES], int len); +static int CommandsAhead = 0; // In packet server mode, the host will let us know if we're outpacing them. +static int SkipCommandTimer = 0; // Tracker for when to check for skipping commands. ~0.5 seconds in a row of being ahead will start skipping. +static int SkipCommandAmount = 0; // Amount of commands to skip. Try and batch skip them all at once since we won't be able to get an update until the full RTT. + +void D_ProcessEvents(void); +void G_BuildTiccmd(usercmd_t *cmd); +void D_DoAdvanceDemo(void); + +static void SendSetup(const FNetGameInfo& info, 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; CVAR(Bool, vid_dontdowait, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR(Bool, vid_lowerinbackground, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) -CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) -CUSTOM_CVAR(Int, net_extratic, 0, CVAR_SERVERINFO | CVAR_NOSAVE) +CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) // Currently deprecated, but may be brought back later. +CVAR(Bool, net_extratic, false, CVAR_SERVERINFO | CVAR_NOSAVE) +CVAR(Bool, net_disablepause, false, CVAR_SERVERINFO | CVAR_NOSAVE) + +CVAR(Bool, cl_noboldchat, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) +CVAR(Bool, cl_nochatsound, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) +CUSTOM_CVAR(Int, cl_showchat, CHAT_GLOBAL, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) { - if (self < 0) - { - self = 0; - } - else if (self > 2) - { - self = 2; - } + if (self < CHAT_DISABLED) + self = CHAT_DISABLED; + else if (self > CHAT_GLOBAL) + self = CHAT_GLOBAL; } -#ifdef _DEBUG -CVAR(Int, net_fakelatency, 0, 0); - -struct PacketStore +// Used to write out all network events that occured leading up to the next tick. +static struct NetEventData { - int timer; - doomcom_t message; -}; + struct FStream { + uint8_t* Stream; + size_t Used = 0; -static TArray InBuffer; -static TArray OutBuffer; -#endif + FStream() + { + Grow(256); + } -// [RH] Special "ticcmds" get stored in here -static struct TicSpecial + ~FStream() + { + if (Stream != nullptr) + M_Free(Stream); + } + + void Grow(size_t size) + { + Stream = (uint8_t*)M_Realloc(Stream, size); + } + } Streams[BACKUPTICS]; + +private: + size_t CurrentSize = 0; + size_t MaxSize = 256; + int CurrentClientTic = 0; + + // Make more room for special Command. + void GetMoreBytes(size_t newSize) + { + MaxSize = max(MaxSize * 2, newSize + 30); + + DPrintf(DMSG_NOTIFY, "Expanding special size to %zu\n", MaxSize); + + for (auto& stream : Streams) + Streams->Grow(MaxSize); + + CurrentStream = Streams[CurrentClientTic % BACKUPTICS].Stream + CurrentSize; + } + + void AddBytes(size_t bytes) + { + if (CurrentSize + bytes >= MaxSize) + GetMoreBytes(CurrentSize + bytes); + + CurrentSize += bytes; + } + +public: + uint8_t* CurrentStream = nullptr; + + // Boot up does some faux network events so we need to wait until after + // everything is initialized to actually set up the network stream. + void InitializeEventData() + { + CurrentStream = Streams[0].Stream; + CurrentSize = 0; + } + + void ResetStream() + { + CurrentClientTic = ClientTic / doomcom.ticdup; + CurrentStream = Streams[CurrentClientTic % BACKUPTICS].Stream; + CurrentSize = 0; + } + + void NewClientTic() + { + const int tic = ClientTic / doomcom.ticdup; + if (CurrentClientTic == tic) + return; + + Streams[CurrentClientTic % BACKUPTICS].Used = CurrentSize; + + CurrentClientTic = tic; + CurrentStream = Streams[tic % BACKUPTICS].Stream; + CurrentSize = 0; + } + + NetEventData& operator<<(uint8_t it) + { + if (CurrentStream != nullptr) + { + AddBytes(1); + WriteInt8(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(int16_t it) + { + if (CurrentStream != nullptr) + { + AddBytes(2); + WriteInt16(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(int32_t it) + { + if (CurrentStream != nullptr) + { + AddBytes(4); + WriteInt32(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(int64_t it) + { + if (CurrentStream != nullptr) + { + AddBytes(8); + WriteInt64(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(float it) + { + if (CurrentStream != nullptr) + { + AddBytes(4); + WriteFloat(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(double it) + { + if (CurrentStream != nullptr) + { + AddBytes(8); + WriteDouble(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(const char *it) + { + if (CurrentStream != nullptr) + { + AddBytes(strlen(it) + 1); + WriteString(it, &CurrentStream); + } + return *this; + } +} NetEvents; + +void Net_ClearBuffers() { - uint8_t *streams[BACKUPTICS]; - size_t used[BACKUPTICS]; - uint8_t *streamptr; - size_t streamoffs; - size_t specialsize; - int lastmaketic; - bool okay; - - TicSpecial () + for (int i = 0; i < MAXPLAYERS; ++i) { - int i; + playeringame[i] = false; + players[i].waiting = players[i].inconsistant = false; - lastmaketic = -1; - specialsize = 256; + auto& state = ClientStates[i]; + state.AverageLatency = state.CurrentLatency = 0u; + memset(state.SentTime, 0, sizeof(state.SentTime)); + memset(state.RecvTime, 0, sizeof(state.RecvTime)); + state.bNewLatency = true; - for (i = 0; i < BACKUPTICS; i++) - streams[i] = NULL; + state.ResendID = 0u; + state.CurrentNetConsistency = state.LastVerifiedConsistency = state.ConsistencyAck = state.ResendConsistencyFrom = -1; + state.CurrentSequence = state.SequenceAck = state.ResendSequenceFrom = -1; + state.Flags = 0; - for (i = 0; i < BACKUPTICS; i++) - { - streams[i] = (uint8_t *)M_Malloc (256); - used[i] = 0; - } - okay = true; + for (int j = 0; j < BACKUPTICS; ++j) + state.Tics[j].Data.SetData(nullptr, 0); } - ~TicSpecial () - { - int i; + doomcom.command = doomcom.datalength = 0; + doomcom.remoteplayer = -1; + doomcom.numplayers = doomcom.ticdup = 1; + consoleplayer = doomcom.consoleplayer = 0; + LocalNetBufferSize = 0; + Net_Arbitrator = 0; - for (i = 0; i < BACKUPTICS; i++) - { - if (streams[i]) - { - M_Free (streams[i]); - streams[i] = NULL; - used[i] = 0; - } - } - okay = false; - } + MutedClients = 0; + CurrentLobbyID = 0u; + NetworkClients.Clear(); + NetMode = NET_PeerToPeer; + netgame = multiplayer = false; + LastSentConsistency = CurrentConsistency = 0; + LastEnterTic = LastGameUpdate = EnterTic; + gametic = ClientTic = 0; + SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0; + NetEvents.ResetStream(); - // Make more room for special commands. - void GetMoreSpace (size_t needed) - { - int i; + LevelStartAck = 0; + LevelStartDelay = LevelStartDebug = 0; + LevelStartStatus = LST_READY; - specialsize = max(specialsize * 2, needed + 30); + FullLatencyCycle = MAXSENDTICS * 3; + LastLatencyUpdate = 0; - 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); - WriteInt8 (it, &streamptr); - } - return *this; - } - - TicSpecial &operator << (int16_t it) - { - if (streamptr) - { - CheckSpace (2); - WriteInt16 (it, &streamptr); - } - return *this; - } - - TicSpecial &operator << (int32_t it) - { - if (streamptr) - { - CheckSpace (4); - WriteInt32 (it, &streamptr); - } - return *this; - } - - TicSpecial& operator << (int64_t it) - { - if (streamptr) - { - CheckSpace(8); - WriteInt64(it, &streamptr); - } - return *this; - } - - TicSpecial &operator << (float it) - { - if (streamptr) - { - CheckSpace (4); - WriteFloat (it, &streamptr); - } - return *this; - } - - TicSpecial& operator << (double it) - { - if (streamptr) - { - CheckSpace(8); - WriteDouble(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; + playeringame[0] = true; + NetworkClients += 0; } -// -// [RH] Rewritten to properly calculate the packet size -// with our variable length commands. -// -int NetbufferSize () +void Net_ResetCommands(bool midTic) { - if (netbuffer[0] & (NCMD_EXIT | NCMD_SETUP)) + bCommandsReset = midTic; + ++CurrentLobbyID; + SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0; + + int tic = gametic / doomcom.ticdup; + if (midTic) { - 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; + // If we're mid ticdup cycle, make sure we immediately enter the next one after + // the current tic we're in finishes. + ClientTic = (tic + 1) * doomcom.ticdup; + gametic = (tic * doomcom.ticdup) + (doomcom.ticdup - 1); } else { - count = 1; + ClientTic = gametic = tic * doomcom.ticdup; + --tic; + } + + for (auto client : NetworkClients) + { + auto& state = ClientStates[client]; + state.Flags &= CF_QUIT; + state.CurrentSequence = min(state.CurrentSequence, tic); + state.SequenceAck = min(state.SequenceAck, tic); + if (state.ResendSequenceFrom >= tic) + state.ResendSequenceFrom = -1; + + // Make sure not to run its current command either. + auto& curTic = state.Tics[tic % BACKUPTICS]; + memset(&curTic.Command, 0, sizeof(curTic.Command)); + curTic.Data.SetData(nullptr, 0); } - // Need at least 3 bytes per tic per player - if (doomcom.datalength < k + 3 * count * numtics) + NetEvents.ResetStream(); +} + +void Net_SetWaiting() +{ + if (netgame && !demoplayback && NetworkClients.Size() > 1) + LevelStartStatus = LST_WAITING; +} + +// [RH] Rewritten to properly calculate the packet size +// with our variable length Command. +static int GetNetBufferSize() +{ + if (NetBuffer[0] & NCMD_EXIT) + return 1 + (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator); + // TODO: Need a skipper for this. + if (NetBuffer[0] & NCMD_SETUP) + return doomcom.datalength; + if (NetBuffer[0] & (NCMD_LATENCY | NCMD_LATENCYACK)) + return 2; + + if (NetBuffer[0] & NCMD_LEVELREADY) { - return k + 3 * count * numtics; + int bytes = 2; + if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + bytes += 2; + + return bytes; } - uint8_t *skipper = &netbuffer[k]; - if ((netbuffer[0] & NCMD_EXIT) == 0) + // Header info + int totalBytes = 10; + if (NetBuffer[0] & NCMD_QUITTERS) + totalBytes += NetBuffer[totalBytes] + 1; + + const int playerCount = NetBuffer[totalBytes++]; + const int numTics = NetBuffer[totalBytes++]; + if (numTics > 0) + totalBytes += 4; + const int ranTics = NetBuffer[totalBytes++]; + if (ranTics > 0) + totalBytes += 4; + if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + ++totalBytes; + + // Minimum additional packet size per player: + // 1 byte for player number + // If in packet server mode and from the host, 2 bytes for the latency to the host + int padding = 1; + if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + padding += 2; + if (doomcom.datalength < totalBytes + playerCount * padding) + return totalBytes + playerCount * padding; + + uint8_t* skipper = &NetBuffer[totalBytes]; + for (int p = 0; p < playerCount; ++p) { - while (count-- > 0) + ++skipper; + if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + skipper += 2; + + for (int i = 0; i < ranTics; ++i) + skipper += 3; + + for (int i = 0; i < numTics; ++i) { - SkipTicCmd (&skipper, numtics); + ++skipper; + SkipUserCmdMessage(skipper); } } - return int(skipper - netbuffer); + + 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) +static void HSendPacket(int client, size_t size) { - 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; - } - + // This data already exists locally in the demo file, so don't write it out. if (demoplayback) return; - if (!netgame) - I_Error ("Tried to transmit to another node"); - -#if SIMULATEERRORS - if (rand() < SIMULATEERRORS) + doomcom.remoteplayer = client; + doomcom.datalength = size; + if (client == consoleplayer) { - if (debugfile) - fprintf (debugfile, "Drop!\n"); + memcpy(LocalNetBuffer, NetBuffer, size); + LocalNetBufferSize = size; return; } -#endif + + if (!netgame) + I_Error("Tried to send a packet to a client while offline"); 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) +static bool HGetPacket() { - if (reboundpacket) + if (demoplayback) + return false; + + if (LocalNetBufferSize) { - memcpy (netbuffer, reboundstore, reboundpacket); - doomcom.remotenode = 0; - reboundpacket = 0; + memcpy(NetBuffer, LocalNetBuffer, LocalNetBufferSize); + doomcom.datalength = LocalNetBufferSize; + doomcom.remoteplayer = consoleplayer; + LocalNetBufferSize = 0; return true; } if (!netgame) return false; - if (demoplayback) - return false; - doomcom.command = CMD_GET; - I_NetCmd (); + 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) - { + if (doomcom.remoteplayer == -1) return false; - } -#endif - - if (debugfile) + + // When the host randomly drops this can cause issues in packet server mode, so at + // least attempt to recover gracefully. + if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator + && (NetBuffer[0] & NCMD_EXIT) && doomcom.datalength == 1) { - 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"); - } + NetBuffer[1] = NetworkClients[1]; + doomcom.datalength = 2; } - if (doomcom.datalength != NetbufferSize ()) + int sizeCheck = GetNetBufferSize(); + if (doomcom.datalength != sizeCheck) { - 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()); + Printf("Incorrect packet size %d (expected %d)\n", doomcom.datalength, sizeCheck); return false; } - return true; + return true; } -void PlayerIsGone (int netnode, int netconsole) +static void ClientConnecting(int client) { - int i; + if (consoleplayer != Net_Arbitrator) + return; - if (nodeingame[netnode]) + // TODO: Eventually... +} + +static void DisconnectClient(int clientNum) +{ + NetworkClients -= clientNum; + MutedClients &= ~(1 << clientNum); + I_ClearNode(clientNum); + // Capture the pawn leaving in the next world tick. + players[clientNum].playerstate = PST_GONE; +} + +static void SetArbitrator(int clientNum) +{ + Net_Arbitrator = clientNum; + players[Net_Arbitrator].settings_controller = true; + Printf("%s is the new host\n", players[Net_Arbitrator].userinfo.GetName()); + if (NetMode == NET_PacketServer) { - for (i = netnode + 1; i < doomcom.numnodes; ++i) - { - if (nodeingame[i]) - break; - } - if (i == doomcom.numnodes) - { - doomcom.numnodes = netnode; - } + for (auto client : NetworkClients) + ClientStates[client].AverageLatency = 0u; - if (playeringame[netconsole]) - { - players[netconsole].playerstate = PST_GONE; - } - nodeingame[netnode] = false; - nodejustleft[netnode] = false; + Net_ResetCommands(false); + Net_SetWaiting(); } - 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; - } - } - } +static void ClientQuit(int clientNum, int newHost) +{ + if (!NetworkClients.InGame(clientNum)) + return; - if (debugfile && NetMode == NET_PacketServer) + // This will get caught in the main loop and send it out to everyone as one big packet. The only + // exception is the host who will leave instantly and send out any needed data. + if (NetMode == NET_PacketServer && clientNum != Net_Arbitrator) { - if (Net_Arbitrator == consoleplayer) - { - fprintf(debugfile, "I am the new master!\n"); - } + if (consoleplayer != Net_Arbitrator) + DPrintf(DMSG_WARNING, "Received disconnect packet from client %d erroneously\n", clientNum); else - { - fprintf(debugfile, "Node %d is the new master!\n", nodeforplayer[Net_Arbitrator]); - } + ClientStates[clientNum].Flags |= CF_QUIT; + + return; } + DisconnectClient(clientNum); + if (clientNum == Net_Arbitrator) + SetArbitrator(newHost >= 0 ? newHost : NetworkClients[0]); + if (demorecording) - { - G_CheckDemoStatus (); + G_CheckDemoStatus(); +} - //WriteByte (DEM_DROPPLAYER, &demo_p); - //WriteByte ((uint8_t)netconsole, &demo_p); +static bool IsMapLoaded() +{ + return gamestate == GS_LEVEL; +} + +static void CheckLevelStart(int client, int delayTics) +{ + if (LevelStartStatus != LST_WAITING) + { + if (consoleplayer == Net_Arbitrator && client != consoleplayer) + { + // Someone might've missed the previous packet, so resend it just in case. + NetBuffer[0] = NCMD_LEVELREADY; + NetBuffer[1] = CurrentLobbyID; + if (NetMode == NET_PacketServer) + { + NetBuffer[2] = 0; + NetBuffer[3] = 0; + } + + HSendPacket(client, NetMode == NET_PacketServer ? 4 : 2); + } + + return; + } + + if (client == Net_Arbitrator) + { + LevelStartAck = 0; + LevelStartStatus = NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator ? LST_HOST : LST_READY; + LevelStartDelay = LevelStartDebug = delayTics; + LastGameUpdate = EnterTic; + return; + } + + int mask = 0; + for (auto pNum : NetworkClients) + { + if (pNum != Net_Arbitrator) + mask |= 1 << pNum; + } + + LevelStartAck |= 1 << client; + if ((LevelStartAck & mask) == mask && IsMapLoaded()) + { + // Beyond this point a player is likely lagging out anyway. + constexpr uint16_t LatencyCap = 350u; + + NetBuffer[0] = NCMD_LEVELREADY; + NetBuffer[1] = CurrentLobbyID; + uint16_t highestAvg = 0u; + if (NetMode == NET_PacketServer) + { + // Wait for enough latency info to be accepted so a better average + // can be calculated for everyone. + if (FullLatencyCycle > 0) + return; + + for (auto client : NetworkClients) + { + if (client == Net_Arbitrator) + continue; + + const uint16_t latency = min(ClientStates[client].AverageLatency, LatencyCap); + if (latency > highestAvg) + highestAvg = latency; + } + } + + constexpr double MS2Sec = 1.0 / 1000.0; + for (auto client : NetworkClients) + { + if (NetMode == NET_PacketServer) + { + int delay = 0; + if (client != Net_Arbitrator) + delay = int(floor((highestAvg - min(ClientStates[client].AverageLatency, LatencyCap)) * MS2Sec * TICRATE)); + + NetBuffer[2] = (delay << 8); + NetBuffer[3] = delay; + } + + HSendPacket(client, NetMode == NET_PacketServer ? 4 : 2); + } } } +struct FLatencyAck +{ + int Client; + uint8_t Seq; + + FLatencyAck(int client, uint8_t seq) : Client(client), Seq(seq) {} +}; + // // GetPackets // - -void GetPackets (void) +static void GetPackets() { - int netconsole; - int netnode; - int realend; - int realstart; - int numtics; - int retransmitfrom; - int k; - uint8_t playerbytes[MAXNETNODES]; - int numplayers; - - while ( HGetPacket() ) + TArray latencyAcks = {}; + TArray stuckInLobby = {}; + while (HGetPacket()) { - if (netbuffer[0] & NCMD_SETUP) + const int clientNum = doomcom.remoteplayer; + auto& clientState = ClientStates[clientNum]; + + if (NetBuffer[0] & NCMD_EXIT) { - 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 + ClientQuit(clientNum, NetMode == NET_PacketServer && clientNum == Net_Arbitrator ? NetBuffer[1] : -1); + continue; } - - 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 (NetBuffer[0] & NCMD_SETUP) { - if (!nodeingame[netnode]) - continue; - - if (NetMode != NET_PacketServer || netconsole == Net_Arbitrator) + if (NetworkClients.InGame(clientNum)) { - PlayerIsGone (netnode, netconsole); - if (NetMode == NET_PacketServer) - { - uint8_t *foo = &netbuffer[2]; - for (int i = 0; i < MAXPLAYERS; ++i) - { - if (playeringame[i]) - { - int resend = ReadInt32 (&foo); - if (i != consoleplayer) - { - resendto[nodeforplayer[i]] = resend; - } - } - } - } + if (consoleplayer == Net_Arbitrator && stuckInLobby.Find(clientNum) >= stuckInLobby.Size()) + stuckInLobby.Push(clientNum); } else { - nodeingame[netnode] = false; - nodejustleft[netnode] = true; + ClientConnecting(clientNum); } + continue; } - k = 2; - - if (NetMode == NET_PacketServer && - netconsole == Net_Arbitrator && - netconsole != consoleplayer) + if (NetBuffer[0] & NCMD_LATENCY) { - 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) + size_t i = 0u; + for (; i < latencyAcks.Size(); ++i) { - PlayerIsGone (nodeforplayer[netbuffer[k]], netbuffer[k]); - k++; + if (latencyAcks[i].Client == clientNum) + break; + } + + if (i >= latencyAcks.Size()) + latencyAcks.Push({ clientNum, NetBuffer[1] }); + + continue; + } + + if (NetBuffer[0] & NCMD_LATENCYACK) + { + if (NetBuffer[1] == clientState.CurrentLatency) + { + clientState.RecvTime[clientState.CurrentLatency++ % MAXSENDTICS] = I_msTime(); + clientState.bNewLatency = true; + } + + continue; + } + + if (NetBuffer[0] & NCMD_LEVELREADY) + { + if (NetBuffer[1] == CurrentLobbyID) + { + int delay = 0; + if (NetMode == NET_PacketServer && clientNum == Net_Arbitrator) + delay = (NetBuffer[2] << 8) | NetBuffer[3]; + + CheckLevelStart(clientNum, delay); + } + + continue; + } + + if (NetBuffer[0] & NCMD_RETRANSMIT) + { + clientState.ResendID = NetBuffer[1]; + clientState.Flags |= CF_RETRANSMIT; + } + + const bool validID = NetBuffer[1] == CurrentLobbyID; + if (validID) + { + clientState.Flags |= CF_UPDATED; + clientState.SequenceAck = (NetBuffer[2] << 24) | (NetBuffer[3] << 16) | (NetBuffer[4] << 8) | NetBuffer[5]; + } + + const int consistencyAck = (NetBuffer[6] << 24) | (NetBuffer[7] << 16) | (NetBuffer[8] << 8) | NetBuffer[9]; + + int curByte = 10; + if (NetBuffer[0] & NCMD_QUITTERS) + { + int numPlayers = NetBuffer[curByte++]; + for (int i = 0; i < numPlayers; ++i) + DisconnectClient(NetBuffer[curByte++]); + } + + const int playerCount = NetBuffer[curByte++]; + + int baseSequence = -1; + const int totalTics = NetBuffer[curByte++]; + if (totalTics > 0) + baseSequence = (NetBuffer[curByte++] << 24) | (NetBuffer[curByte++] << 16) | (NetBuffer[curByte++] << 8) | NetBuffer[curByte++]; + + int baseConsistency = -1; + const int ranTics = NetBuffer[curByte++]; + if (ranTics > 0) + baseConsistency = (NetBuffer[curByte++] << 24) | (NetBuffer[curByte++] << 16) | (NetBuffer[curByte++] << 8) | NetBuffer[curByte++]; + + if (NetMode == NET_PacketServer && clientNum == Net_Arbitrator) + { + if (validID) + CommandsAhead = NetBuffer[curByte++]; + else + ++curByte; + } + + for (int p = 0; p < playerCount; ++p) + { + const int pNum = NetBuffer[curByte++]; + auto& pState = ClientStates[pNum]; + + // This gets sent over per-player so latencies are correct in packet server mode. + if (NetMode == NET_PacketServer && clientNum == Net_Arbitrator) + { + if (consoleplayer != Net_Arbitrator) + pState.AverageLatency = (NetBuffer[curByte++] << 8) | NetBuffer[curByte++]; + else + curByte += 2; + } + + // Make sure the host doesn't update a player's last consistency ack with their own data. + if (NetMode != NET_PacketServer || consoleplayer != Net_Arbitrator + || pNum == Net_Arbitrator || clientNum != Net_Arbitrator) + { + pState.ConsistencyAck = consistencyAck; + } + + TArray consistencies = {}; + for (int r = 0; r < ranTics; ++r) + { + int ofs = NetBuffer[curByte++]; + consistencies.Insert(ofs, (NetBuffer[curByte++] << 8) | NetBuffer[curByte++]); + } + + for (size_t i = 0u; i < consistencies.Size(); ++i) + { + const int cTic = baseConsistency + i; + if (cTic <= pState.CurrentNetConsistency) + continue; + + if (cTic > pState.CurrentNetConsistency + 1 || !consistencies[i]) + { + clientState.Flags |= CF_MISSING_CON; + break; + } + + pState.NetConsistency[cTic % BACKUPTICS] = consistencies[i]; + pState.CurrentNetConsistency = cTic; + } + + // Each tic within a given packet is given a sequence number to ensure that things were put + // back together correctly. Normally this wouldn't matter as much but since we need to keep + // clients in lock step a misordered packet will instantly cause a desync. + TArray data = {}; + for (int t = 0; t < totalTics; ++t) + { + // Try and reorder the tics if they're all there but end up out of order. + const int ofs = NetBuffer[curByte++]; + data.Insert(ofs, &NetBuffer[curByte]); + uint8_t* skipper = &NetBuffer[curByte]; + curByte += SkipUserCmdMessage(skipper); + } + + // If it's from a previous waiting period, the commands are no longer relevant. + if (!validID) + continue; + + for (size_t i = 0u; i < data.Size(); ++i) + { + const int seq = baseSequence + i; + // Duplicate command, ignore it. + if (seq <= pState.CurrentSequence) + continue; + + // Skipped a command. Packet likely got corrupted while being put back together, so have + // the client send over the properly ordered commands. + if (seq > pState.CurrentSequence + 1 || data[i] == nullptr) + { + clientState.Flags |= CF_MISSING_SEQ; + break; + } + + ReadUserCmdMessage(data[i], pNum, seq); + // The host and clients are a bit desynched here. We don't want to update the host's latest ack with their own + // info since they get those from the actual clients, but clients have to get them from the host since they + // don't commincate with each other except in P2P mode. + if (NetMode != NET_PacketServer || consoleplayer != Net_Arbitrator + || pNum == Net_Arbitrator || clientNum != Net_Arbitrator) + { + pState.CurrentSequence = seq; + } } } + } - // Pull current network delay from node - netdelay[netnode][(nettics[netnode]+1) % BACKUPTICS] = netbuffer[k++]; + for (const auto& ack : latencyAcks) + { + NetBuffer[0] = NCMD_LATENCYACK; + NetBuffer[1] = ack.Seq; + HSendPacket(ack.Client, 2); + } - playerbytes[0] = netconsole; - if (netbuffer[0] & NCMD_MULTI) - { - numplayers = netbuffer[k++]; - memcpy (playerbytes+1, &netbuffer[k], numplayers - 1); - k += numplayers - 1; - } - else - { - numplayers = 1; - } + for (auto client : stuckInLobby) + { + NetBuffer[0] = NCMD_GAMEREADY; + HSendPacket(client, 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]) +static void SendHeartbeat() +{ + // TODO: This could probably also be used to determine if there's packets + // missing and a retransmission is needed. + const uint64_t time = I_msTime(); + for (auto client : NetworkClients) + { + if (client == consoleplayer) 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 + auto& state = ClientStates[client]; + if (LastLatencyUpdate >= MAXSENDTICS) { - uint8_t *start; - int i, tics; - remoteresend[netnode] = false; - - start = &netbuffer[k]; - - for (i = 0; i < numplayers; ++i) + int delta = 0; + const uint8_t startTic = state.CurrentLatency - MAXSENDTICS; + for (int i = 0; i < MAXSENDTICS; ++i) { - int node = nodeforplayer[playerbytes[i]]; + const int tic = (startTic + i) % MAXSENDTICS; + const uint64_t high = state.RecvTime[tic] < state.SentTime[tic] ? time : state.RecvTime[tic]; + delta += high - state.SentTime[tic]; + } - SkipTicCmd (&start, nettics[node] - realstart); - for (tics = nettics[node]; tics < realend; tics++) - ReadTicCmd (&start, playerbytes[i], tics); + state.AverageLatency = delta / MAXSENDTICS; + } - nettics[nodeforplayer[playerbytes[i]]] = realend; + if (state.bNewLatency) + { + // Use the most up-to-date time here for better accuracy. + state.SentTime[state.CurrentLatency % MAXSENDTICS] = I_msTime(); + state.bNewLatency = false; + } + + NetBuffer[0] = NCMD_LATENCY; + NetBuffer[1] = state.CurrentLatency; + HSendPacket(client, 2); + } +} + +static void CheckConsistencies() +{ + // Check consistencies retroactively to see if there was a desync at some point. We still + // check the local client here because in packet server mode these could realistically desync + // if the client's current position doesn't agree with the host. + for (auto client : NetworkClients) + { + auto& clientState = ClientStates[client]; + // If previously inconsistent, always mark it as such going forward. We don't want this to + // accidentally go away at some point since the game state is already completely broken. + if (players[client].inconsistant) + { + clientState.LastVerifiedConsistency = clientState.CurrentNetConsistency; + } + else + { + // Make sure we don't check past tics we haven't even ran yet. + const int limit = min(CurrentConsistency - 1, clientState.CurrentNetConsistency); + while (clientState.LastVerifiedConsistency < limit) + { + const int tic = clientState.LastVerifiedConsistency % BACKUPTICS; + if (clientState.LocalConsistency[tic] != clientState.NetConsistency[tic]) + { + players[client].inconsistant = true; + clientState.LastVerifiedConsistency = clientState.CurrentNetConsistency; + break; + } + + ++clientState.LastVerifiedConsistency; } } } } +//========================================================================== // -// NetUpdate -// Builds ticcmds for console player, -// sends out a packet +// FRandom :: StaticSumSeeds // -int gametime; +// This function produces a uint32_t that can be used to check the consistancy +// of network games between different machines. Only a select few RNGs are +// used for the sum, because not all RNGs are important to network sync. +// +//========================================================================== -void NetUpdate (void) +extern FRandom pr_spawnmobj; +extern FRandom pr_acs; +extern FRandom pr_chase; +extern FRandom pr_damagemobj; + +static uint32_t StaticSumSeeds() { - int lowtic; - int nowtime; - int newtics; - int i,j; - int realstart; - uint8_t *cmddata; - bool resendOnly; + return + pr_spawnmobj.Seed() + + pr_acs.Seed() + + pr_chase.Seed() + + pr_damagemobj.Seed(); +} - GC::CheckGC(); - - if (ticdup == 0) +static int16_t CalculateConsistency(int client, uint32_t seed) +{ + if (players[client].mo != nullptr) { + seed += int((players[client].mo->X() + players[client].mo->Y() + players[client].mo->Z()) * 257) + players[client].mo->Angles.Yaw.BAMs() + players[client].mo->Angles.Pitch.BAMs(); + seed ^= players[client].health; + } + + // Zero value consistencies are seen as invalid, so always have a valid value. + return (seed & 0xFFFF) ? seed : 1; +} + +// Ran a tick, so prep the next consistencies to send out. +// [RH] Include some random seeds and player stuff in the consistancy +// check, not just the player's x position like BOOM. +static void MakeConsistencies() +{ + if (!netgame || demoplayback || (gametic % doomcom.ticdup) || !IsMapLoaded()) return; + + const uint32_t rngSum = StaticSumSeeds(); + for (auto client : NetworkClients) + { + auto& clientState = ClientStates[client]; + clientState.LocalConsistency[CurrentConsistency % BACKUPTICS] = CalculateConsistency(client, rngSum); } - // check time - nowtime = I_GetTime (); - newtics = nowtime - gametime; - gametime = nowtime; + ++CurrentConsistency; +} - if (newtics <= 0) // nothing new to update - { - GetPackets (); - return; - } +static bool Net_UpdateStatus() +{ + if (!netgame || demoplayback || NetworkClients.Size() <= 1) + return true; - if (skiptics <= newtics) - { - newtics -= skiptics; - skiptics = 0; - } - else - { - skiptics -= newtics; - newtics = 0; - } + if (LevelStartStatus == LST_WAITING || LevelStartDelay > 0) + return false; - // build new ticcmds for console player - for (i = 0; i < newtics; i++) + // Check against the previous tick in case we're recovering from a huge + // system hiccup. If the game has taken too long to update, it's likely + // another client is hanging up the game. + if (LastEnterTic - LastGameUpdate >= MAXSENDTICS * doomcom.ticdup) { - 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++; + // Try again in the next MaxDelay tics. + LastGameUpdate = EnterTic; - if (ticdup == 1 || maketic == 0) + if (NetMode != NET_PacketServer || consoleplayer == Net_Arbitrator) { - Net_NewMakeTic (); + // Use a missing packet here to tell the other players to retransmit instead of simply retransmitting our + // own data over instantly. This avoids flooding the network at a time where it's not opportune to do so. + const int curTic = gametic / doomcom.ticdup; + for (auto client : NetworkClients) + { + if (client == consoleplayer) + continue; + + if (ClientStates[client].CurrentSequence < curTic) + { + ClientStates[client].Flags |= CF_MISSING; + players[client].waiting = true; + } + else + { + players[client].waiting = false; + } + } } 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; + // In packet server mode, the client is waiting for data from the host and hasn't recieved it yet. Send + // our data back over in case the host is waiting for us. + ClientStates[Net_Arbitrator].Flags |= CF_MISSING; + players[Net_Arbitrator].waiting = true; + } + } - // 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) + if (LevelStartStatus == LST_HOST) + return false; + + for (auto client : NetworkClients) + { + if (players[client].waiting) + return false; + } + + // Wait for the game to stabilize a bit after launch before skipping commands. + bool updated = false; + int lowestDiff = INT_MAX; + if (gametic > TICRATE * 2) + { + if (NetMode != NET_PacketServer) + { + // Check if everyone has a buffer for us. If they do, we're too far ahead. + for (auto client : NetworkClients) + { + if (client != consoleplayer && (ClientStates[client].Flags & CF_UPDATED)) { - localcmds[j % LOCALCMDTICS].ucmd.buttons |= - localcmds[(j + 1) % LOCALCMDTICS].ucmd.buttons; + updated = true; + int diff = ClientStates[client].SequenceAck - ClientStates[client].CurrentSequence; + if (diff < lowestDiff) + lowestDiff = diff; } + + ClientStates[client].Flags &= ~CF_UPDATED; + } + } + else if (consoleplayer == Net_Arbitrator) + { + // If we're consistenty ahead of the highest sequence player, slow down. + const int curTic = ClientTic / doomcom.ticdup; + for (auto client : NetworkClients) + { + if (client != Net_Arbitrator && (ClientStates[client].Flags & CF_UPDATED)) + { + updated = true; + int diff = curTic - ClientStates[client].CurrentSequence; + if (diff < lowestDiff) + lowestDiff = diff; + } + + ClientStates[client].Flags &= ~CF_UPDATED; + } + } + else if (ClientStates[Net_Arbitrator].Flags & CF_UPDATED) + { + // Check if the host is reporting that we're too far ahead of them. + updated = true; + lowestDiff = CommandsAhead; + ClientStates[Net_Arbitrator].Flags &= ~CF_UPDATED; + } + } + + if (updated) + { + if (lowestDiff > 0) + { + if (SkipCommandTimer++ > TICRATE / 2) + { + SkipCommandTimer = 0; + if (SkipCommandAmount <= 0) + SkipCommandAmount = lowestDiff * doomcom.ticdup; + } + } + else + { + SkipCommandTimer = 0; + } + } + + return true; +} + +void NetUpdate(int tics) +{ + GetPackets(); + if (tics <= 0) + return; + + if (netgame && !demoplayback) + { + // If a tic has passed, always send out a heartbeat packet (also doubles as + // a latency measurement tool). + if (NetMode != NET_PacketServer || consoleplayer == Net_Arbitrator) + { + LastLatencyUpdate += tics; + if (FullLatencyCycle > 0) + FullLatencyCycle = max(FullLatencyCycle - tics, 0); + + SendHeartbeat(); + + if (LastLatencyUpdate >= MAXSENDTICS) + LastLatencyUpdate = 0; + } + + CheckConsistencies(); + } + + // Sit idle after the level has loaded until everyone is ready to go. This keeps players better + // in sync with each other than relying on tic balancing to speed up/slow down the game and mirrors + // how players would wait for a true server to load. + if (LevelStartStatus != LST_READY) + { + if (LevelStartStatus == LST_WAITING) + { + if (NetworkClients.Size() == 1) + { + // If we got stuck in limbo waiting, force start the map. + CheckLevelStart(Net_Arbitrator, 0); } 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; + if (consoleplayer != Net_Arbitrator && IsMapLoaded()) + { + NetBuffer[0] = NCMD_LEVELREADY; + NetBuffer[1] = CurrentLobbyID; + HSendPacket(Net_Arbitrator, 2); + } + } + } + else if (LevelStartStatus == LST_HOST) + { + // If we're the host, idly wait until all packets have arrived. There's no point in predicting since we + // know for a fact the game won't be started until everyone is accounted for. (Packet server only) + const int curTic = gametic / doomcom.ticdup; + int lowestSeq = curTic; + for (auto client : NetworkClients) + { + if (client != Net_Arbitrator && ClientStates[client].CurrentSequence < lowestSeq) + lowestSeq = ClientStates[client].CurrentSequence; + } + + if (lowestSeq >= curTic) + LevelStartStatus = LST_READY; + } + } + else if (LevelStartDelay > 0) + { + if (LevelStartDelay < tics) + tics -= LevelStartDelay; + + LevelStartDelay = max(LevelStartDelay - tics, 0); + } + + const bool netGood = Net_UpdateStatus(); + const int startTic = ClientTic; + tics = min(tics, MAXSENDTICS * doomcom.ticdup); + for (int i = 0; i < tics; ++i) + { + I_StartTic(); + D_ProcessEvents(); + if (pauseext || !netGood) + break; + + if (SkipCommandAmount > 0) + { + --SkipCommandAmount; + continue; + } + + G_BuildTiccmd(&LocalCmds[ClientTic++ % LOCALCMDTICS]); + if (doomcom.ticdup == 1) + { + Net_NewClientTic(); + } + else + { + const int ticDiff = ClientTic % doomcom.ticdup; + if (ticDiff) + { + const int startTic = ClientTic - ticDiff; + + // Even if we're not sending out inputs, update the local commands so that the doomcom.ticdup + // is correctly played back while predicting as best as possible. This will help prevent + // minor hitches when playing online. + for (int j = ClientTic - 1; j > startTic; --j) + LocalCmds[(j - 1) % LOCALCMDTICS].buttons |= LocalCmds[j % LOCALCMDTICS].buttons; + } + else + { + // Gather up the Command across the last doomcom.ticdup number of tics + // and average them out. These are propagated back to the local + // command so that they'll be predicted correctly. + const int lastTic = ClientTic - doomcom.ticdup; + for (int j = ClientTic - 1; j > lastTic; --j) + LocalCmds[(j - 1) % LOCALCMDTICS].buttons |= LocalCmds[j % LOCALCMDTICS].buttons; int pitch = 0; int yaw = 0; @@ -1050,784 +1289,710 @@ void NetUpdate (void) int sidemove = 0; int upmove = 0; - for (j = 0; j < ticdup; ++j) + for (int j = 0; j < doomcom.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; + const int mod = (lastTic + j) % LOCALCMDTICS; + pitch += LocalCmds[mod].pitch; + yaw += LocalCmds[mod].yaw; + roll += LocalCmds[mod].roll; + forwardmove += LocalCmds[mod].forwardmove; + sidemove += LocalCmds[mod].sidemove; + upmove += LocalCmds[mod].upmove; } - pitch /= ticdup; - yaw /= ticdup; - roll /= ticdup; - forwardmove /= ticdup; - sidemove /= ticdup; - upmove /= ticdup; + pitch /= doomcom.ticdup; + yaw /= doomcom.ticdup; + roll /= doomcom.ticdup; + forwardmove /= doomcom.ticdup; + sidemove /= doomcom.ticdup; + upmove /= doomcom.ticdup; - for (j = 0; j < ticdup; ++j) + for (int j = 0; j < doomcom.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; + const int mod = (lastTic + j) % LOCALCMDTICS; + LocalCmds[mod].pitch = pitch; + LocalCmds[mod].yaw = yaw; + LocalCmds[mod].roll = roll; + LocalCmds[mod].forwardmove = forwardmove; + LocalCmds[mod].sidemove = sidemove; + LocalCmds[mod].upmove = upmove; } - Net_NewMakeTic (); + Net_NewClientTic(); } } } - if (singletics) - return; // singletic update is synchronous - + const int newestTic = ClientTic / doomcom.ticdup; if (demoplayback) { - resendto[0] = nettics[0] = (maketic / ticdup); - return; // Don't touch netcmd data while playing a demo, as it'll already exist. + // Don't touch net command data while playing a demo, as it'll already exist. + for (auto client : NetworkClients) + ClientStates[client].CurrentSequence = newestTic; + + return; } - // 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) + int startSequence = startTic / doomcom.ticdup; + int endSequence = newestTic; + int quitters = 0; + int quitNums[MAXPLAYERS]; + if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) { - if (NetMode == NET_PacketServer) + // In packet server mode special handling is used to ensure the host only + // sends out available tics when ready instead of constantly shotgunning + // them out as they're made locally. + startSequence = gametic / doomcom.ticdup; + int lowestSeq = endSequence - 1; + for (auto client : NetworkClients) { - for (j = 0; j < MAXPLAYERS; j++) - { - if (playeringame[j] && players[j].Bot == NULL) - { - count++; - } - } + if (client == Net_Arbitrator) + continue; - // 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; - } + // The host has special handling when disconnecting in a packet server game. + if (ClientStates[client].Flags & CF_QUIT) + quitNums[quitters++] = client; + else if (ClientStates[client].CurrentSequence < lowestSeq) + lowestSeq = ClientStates[client].CurrentSequence; } + + endSequence = lowestSeq + 1; } - for (i = 0; i < doomcom.numnodes; i++) + const bool resendOnly = startSequence == endSequence && (ClientTic % doomcom.ticdup); + for (auto client : NetworkClients) { - uint8_t playerbytes[MAXPLAYERS]; - - if (!nodeingame[i]) - { + // If in packet server mode, we don't want to send information to anyone but the host. On the other + // hand, if we're the host we send out everyone's info to everyone else. + if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator && client != Net_Arbitrator) continue; - } - if (NetMode == NET_PacketServer && - consoleplayer != Net_Arbitrator && - i != nodeforplayer[Net_Arbitrator] && - i != 0) - { + + auto& curState = ClientStates[client]; + // If we can only resend, don't send clients any information that they already have. If + // we couldn't generate any commands because we're at the cap, instead send out a heartbeat + // containing the latest command. + if (resendOnly && !(curState.Flags & (CF_RETRANSMIT | CF_MISSING))) continue; - } - if (resendOnly && resendcount[i] <= 0 && !remoteresend[i] && nettics[i]) + + NetBuffer[0] = (curState.Flags & CF_MISSING) ? NCMD_RETRANSMIT : 0; + curState.Flags &= ~CF_MISSING; + + NetBuffer[1] = (curState.Flags & CF_RETRANSMIT_SEQ) ? curState.ResendID : CurrentLobbyID; + // Last sequence we got from this client. + NetBuffer[2] = (curState.CurrentSequence >> 24); + NetBuffer[3] = (curState.CurrentSequence >> 16); + NetBuffer[4] = (curState.CurrentSequence >> 8); + NetBuffer[5] = curState.CurrentSequence; + // Last consistency we got from this client. + NetBuffer[6] = (curState.CurrentNetConsistency >> 24); + NetBuffer[7] = (curState.CurrentNetConsistency >> 16); + NetBuffer[8] = (curState.CurrentNetConsistency >> 8); + NetBuffer[9] = curState.CurrentNetConsistency; + + size_t size = 10; + if (quitters > 0) { - continue; + NetBuffer[0] |= NCMD_QUITTERS; + NetBuffer[size++] = quitters; + for (int i = 0; i < quitters; ++i) + NetBuffer[size++] = quitNums[i]; } - 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) + int playerNums[MAXPLAYERS]; + int playerCount = NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator ? NetworkClients.Size() : 1; + NetBuffer[size++] = playerCount; + if (playerCount > 1) { - 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; + int i = 0; + for (auto cl : NetworkClients) + playerNums[i++] = cl; } else { - netbuffer[0] |= NCMD_XTICS; - netbuffer[k++] = numtics - 3; + playerNums[0] = consoleplayer; } - if (quitcount > 0) + // Only send over our newest commands. If a client missed one, they'll let us know. + if (curState.Flags & CF_RETRANSMIT_SEQ) { - netbuffer[0] |= NCMD_QUITTERS; - netbuffer[k++] = quitcount; - for (int l = 0; l < doomcom.numnodes; ++l) + curState.Flags &= ~CF_RETRANSMIT_SEQ; + if (curState.ResendSequenceFrom < 0) + curState.ResendSequenceFrom = curState.SequenceAck + 1; + } + + const int sequenceNum = curState.ResendSequenceFrom >= 0 ? curState.ResendSequenceFrom : startSequence; + const int numTics = clamp(endSequence - sequenceNum, 0, MAXSENDTICS); + if (curState.ResendSequenceFrom >= 0) + { + curState.ResendSequenceFrom += numTics; + if (curState.ResendSequenceFrom >= endSequence) + curState.ResendSequenceFrom = -1; + } + + NetBuffer[size++] = numTics; + if (numTics > 0) + { + NetBuffer[size++] = (sequenceNum >> 24); + NetBuffer[size++] = (sequenceNum >> 16); + NetBuffer[size++] = (sequenceNum >> 8); + NetBuffer[size++] = sequenceNum; + } + + if (curState.Flags & CF_RETRANSMIT_CON) + { + curState.Flags &= ~CF_RETRANSMIT_CON; + if (curState.ResendConsistencyFrom < 0) + curState.ResendConsistencyFrom = curState.ConsistencyAck + 1; + } + + const int baseConsistency = curState.ResendConsistencyFrom >= 0 ? curState.ResendConsistencyFrom : LastSentConsistency; + // Don't bother sending over consistencies in packet server unless you're the host. + int ran = 0; + if (NetMode != NET_PacketServer || consoleplayer == Net_Arbitrator) + { + ran = clamp(CurrentConsistency - baseConsistency, 0, MAXSENDTICS); + if (curState.ResendConsistencyFrom >= 0) { - if (nodejustleft[l]) + curState.ResendConsistencyFrom += ran; + if (curState.ResendConsistencyFrom >= CurrentConsistency) + curState.ResendConsistencyFrom = -1; + } + } + + NetBuffer[size++] = ran; + if (ran > 0) + { + NetBuffer[size++] = (baseConsistency >> 24); + NetBuffer[size++] = (baseConsistency >> 16); + NetBuffer[size++] = (baseConsistency >> 8); + NetBuffer[size++] = baseConsistency; + } + + if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) + NetBuffer[size++] = client == Net_Arbitrator ? 0 : max(curState.CurrentSequence - newestTic, 0); + + // Client commands. + + uint8_t* cmd = &NetBuffer[size]; + for (int i = 0; i < playerCount; ++i) + { + cmd[0] = playerNums[i]; + ++cmd; + + auto& clientState = ClientStates[playerNums[i]]; + // Time used to track latency since in packet server mode we want each + // client's latency to the server itself. + if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) + { + cmd[0] = (clientState.AverageLatency >> 8); + ++cmd; + cmd[0] = clientState.AverageLatency; + ++cmd; + } + + for (int r = 0; r < ran; ++r) + { + cmd[0] = r; + ++cmd; + const int tic = (baseConsistency + r) % BACKUPTICS; + cmd[0] = (clientState.LocalConsistency[tic] >> 8); + ++cmd; + cmd[0] = clientState.LocalConsistency[tic]; + ++cmd; + } + + for (int t = 0; t < numTics; ++t) + { + cmd[0] = t; + ++cmd; + + int curTic = sequenceNum + t, lastTic = curTic - 1; + if (playerNums[i] == consoleplayer) { - netbuffer[k++] = playerfornode[l]; + int realTic = (curTic * doomcom.ticdup) % LOCALCMDTICS; + int realLastTic = (lastTic * doomcom.ticdup) % LOCALCMDTICS; + // Write out the net events before the user commands so inputs can + // be used as a marker for when the given command ends. + auto& stream = NetEvents.Streams[curTic % BACKUPTICS]; + if (stream.Used) + { + memcpy(cmd, stream.Stream, stream.Used); + cmd += stream.Used; + } + + WriteUserCmdMessage(LocalCmds[realTic], + realLastTic >= 0 ? &LocalCmds[realLastTic] : nullptr, cmd); + } + else + { + auto& netTic = clientState.Tics[curTic % BACKUPTICS]; + + int len; + uint8_t* data = netTic.Data.GetData(&len); + if (data != nullptr) + { + memcpy(cmd, data, len); + cmd += len; + } + + WriteUserCmdMessage(netTic.Command, + lastTic >= 0 ? &clientState.Tics[lastTic % BACKUPTICS].Command : nullptr, cmd); } } } - // 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) - { - WriteInt16 (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; - - WriteInt16 (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); - } + HSendPacket(client, int(cmd - NetBuffer)); + if (client != consoleplayer && net_extratic) + HSendPacket(client, int(cmd - NetBuffer)); } - // listen for other packets - GetPackets (); - + // Update this now that all the packets have been sent out. if (!resendOnly) - { - // ideally nettics[0] should be 1 - 3 tics above lowtic - // if we are consistantly slower, speed up time + LastSentConsistency = CurrentConsistency; - // [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]; - } - } + // Listen for other packets. This has to also come after sending so the player that sent + // data to themselves gets it immediately (important for singleplayer, otherwise there + // would always be a one-tic delay). + GetPackets(); } - -// -// 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 +// One byte set to NCMD_SETUP or NCMD_USERINFO +// One byte for the relevant player number. +// Three bytes for the sender's game version (major, minor, revision). +// Four bytes for the bit mask indicating which players the sender knows about. +// If NCMD_SETUP, one byte indicating the guest got the game setup info. +// Player's user information. // // The guests always send NCMD_SETUP packets, and the host always -// sends NCMD_SETUP+1 packets. +// sends NCMD_USERINFO 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 +// One byte set to NCMD_GAMEINFO. +// One byte for doomcom.ticdup setting. +// One byte for NetMode setting. +// String with starting map's name. +// The game's RNG seed. +// Remaining game information. // -// Finished packet looks like this: +// Ready packets look like this: // -// 0 One byte set to NCMD_SETUP+3 +// One byte set to NCMD_GAMEREADY. // // 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 +bool ExchangeNetGameInfo(void *userdata) { - uint32_t playersdetected[MAXNETNODES]; - uint8_t gotsetup[MAXNETNODES]; -}; - -bool DoArbitrate (void *userdata) -{ - ArbitrateData *data = (ArbitrateData *)userdata; - uint8_t *stream; - int version; - int node; - int i, j; - - while (HGetPacket ()) + FNetGameInfo *data = reinterpret_cast(userdata); + uint8_t *stream = nullptr; + while (HGetPacket()) { - if (netbuffer[0] == NCMD_EXIT) + // For now throw an error until something more complex can be done to handle this case. + if (NetBuffer[0] == NCMD_EXIT) + I_Error("Game unexpectedly ended."); + + if (NetBuffer[0] == NCMD_SETUP || NetBuffer[0] == NCMD_USERINFO) { - I_FatalError ("The game was aborted."); - } + int clientNum = NetBuffer[1]; + data->DetectedPlayers[clientNum] = + (NetBuffer[5] << 24) | (NetBuffer[6] << 16) | (NetBuffer[7] << 8) | NetBuffer[8]; - 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]; + if (NetBuffer[0] == NCMD_SETUP) + { + // Sent from guest. + data->GotSetup[clientNum] = NetBuffer[9]; + 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"); - } + // Sent from host. + stream = &NetBuffer[9]; + } - playeringame[netbuffer[1]] = true; - nodeingame[node] = true; + D_ReadUserInfoStrings(clientNum, &stream, false); + if (!NetworkClients.InGame(clientNum)) + { + if (NetBuffer[2] != VER_MAJOR % 255 || NetBuffer[3] != VER_MINOR % 255 || NetBuffer[4] != VER_REVISION % 255) + I_Error("Different " GAMENAME " versions cannot play a net game"); - data->playersdetected[0] |= 1 << netbuffer[1]; + NetworkClients += clientNum; + data->DetectedPlayers[consoleplayer] |= 1 << clientNum; - I_NetMessage ("Found %s (node %d, player %d)", - players[netbuffer[1]].userinfo.GetName(), - node, netbuffer[1]+1); + I_NetMessage("Found %s (%d)", players[clientNum].userinfo.GetName(), clientNum); } } - else if (netbuffer[0] == NCMD_SETUP+2) // got game info + else if (NetBuffer[0] == NCMD_GAMEINFO) { - data->gotsetup[0] = 0x80; + data->GotSetup[consoleplayer] = true; - ticdup = doomcom.ticdup = clamp(netbuffer[1], 1, MAXTICDUP); - NetMode = netbuffer[2]; + doomcom.ticdup = clamp(NetBuffer[1], 1, MAXTICDUP); + NetMode = static_cast(NetBuffer[2]); - stream = &netbuffer[3]; + stream = &NetBuffer[3]; startmap = ReadStringConst(&stream); - rngseed = ReadInt32 (&stream); - C_ReadCVars (&stream); + rngseed = ReadInt32(&stream); + C_ReadCVars(&stream); } - else if (netbuffer[0] == NCMD_SETUP+3) + else if (NetBuffer[0] == NCMD_GAMEREADY) { return true; } } - // If everybody already knows everything, it's time to go + // If everybody already knows everything, it's time to start. 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; + bool stillWaiting = false; - if (i == doomcom.numnodes) + // Update this dynamically. + uint32_t allPlayers = 0u; + for (int i = 0; i < doomcom.numplayers; ++i) + allPlayers |= 1 << i; + + for (int i = 0; i < doomcom.numplayers; ++i) + { + if (!data->GotSetup[i] || (data->DetectedPlayers[i] & allPlayers) != allPlayers) + { + stillWaiting = true; + break; + } + } + + if (!stillWaiting) 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]; + NetBuffer[2] = VER_MAJOR % 255; + NetBuffer[3] = VER_MINOR % 255; + NetBuffer[4] = VER_REVISION % 255; + NetBuffer[5] = data->DetectedPlayers[consoleplayer] >> 24; + NetBuffer[6] = data->DetectedPlayers[consoleplayer] >> 16; + NetBuffer[7] = data->DetectedPlayers[consoleplayer] >> 8; + NetBuffer[8] = data->DetectedPlayers[consoleplayer]; 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]; - auto str = D_GetUserInfoStrings (consoleplayer, true); - memcpy(stream, str.GetChars(), str.Len() + 1); - stream += str.Len(); - SendSetup (data->playersdetected, data->gotsetup, int(stream - netbuffer)); + { + // If we're a guest, send our info over to the host. + NetBuffer[0] = NCMD_SETUP; + NetBuffer[1] = consoleplayer; + NetBuffer[9] = data->GotSetup[consoleplayer]; + stream = &NetBuffer[10]; + // If the host already knows we're here, just send over a heartbeat. + if (!(data->DetectedPlayers[Net_Arbitrator] & (1 << consoleplayer))) + { + auto str = D_GetUserInfoStrings(consoleplayer, true); + const size_t userSize = str.Len() + 1; + memcpy(stream, str.GetChars(), userSize); + stream += userSize; + } + else + { + *stream = 0; + ++stream; + } } else - { // Send user info for all nodes - netbuffer[0] = NCMD_SETUP+1; - for (i = 1; i < doomcom.numnodes; ++i) + { + // If we're the host, send over known player data to guests. This is done instantly + // since the game info will also get sent out after this. + NetBuffer[0] = NCMD_USERINFO; + for (auto client : NetworkClients) { - for (j = 0; j < doomcom.numnodes; ++j) + if (client == Net_Arbitrator) + continue; + + for (auto cl : NetworkClients) { - // Send info about player j to player i? - if ((data->playersdetected[0] & (1<playersdetected[i] & (1<DetectedPlayers[Net_Arbitrator] & clBit) && !(data->DetectedPlayers[client] & clBit)) { - netbuffer[1] = j; - stream = &netbuffer[9]; - auto str = D_GetUserInfoStrings(j, true); - memcpy(stream, str.GetChars(), str.Len() + 1); - stream += str.Len(); - HSendPacket (i, int(stream - netbuffer)); + NetBuffer[1] = cl; + stream = &NetBuffer[9]; + auto str = D_GetUserInfoStrings(cl, true); + const size_t userSize = str.Len() + 1; + memcpy(stream, str.GetChars(), userSize); + stream += userSize; + HSendPacket(client, int(stream - NetBuffer)); } } } + + // If we're the host, send the game info too. + NetBuffer[0] = NCMD_GAMEINFO; + NetBuffer[1] = (uint8_t)doomcom.ticdup; + NetBuffer[2] = NetMode; + stream = &NetBuffer[3]; + WriteString(startmap.GetChars(), &stream); + WriteInt32(rngseed, &stream); + C_WriteCVars(&stream, CVAR_SERVERINFO, true); } - // If we're the host, send the game info, too - if (consoleplayer == Net_Arbitrator) - { - netbuffer[0] = NCMD_SETUP+2; - netbuffer[1] = (uint8_t)doomcom.ticdup; - netbuffer[2] = NetMode; - stream = &netbuffer[3]; - WriteString (startmap.GetChars(), &stream); - WriteInt32 (rngseed, &stream); - C_WriteCVars (&stream, CVAR_SERVERINFO, true); - - SendSetup (data->playersdetected, data->gotsetup, int(stream - netbuffer)); - } + SendSetup(*data, int(stream - NetBuffer)); return false; } -bool D_ArbitrateNetStart (void) +static bool D_ExchangeNetInfo() { - ArbitrateData data; - int i; - - // Return right away if we're just playing with ourselves. - if (doomcom.numnodes == 1) + // Return right away if it's just a solo net game. + if (doomcom.numplayers == 1) return true; 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). + FNetGameInfo info = {}; + info.DetectedPlayers[consoleplayer] = 1 << consoleplayer; + info.GotSetup[consoleplayer] = consoleplayer == Net_Arbitrator; + NetworkClients += consoleplayer; + 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; - } - } + I_NetInit("Sending game information", 1); 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; - } - } - } + I_NetInit("Waiting for host information", 1); - if (consoleplayer == Net_Arbitrator) - { - data.gotsetup[0] = 0x80; - } - - I_NetInit ("Exchanging game information", 1); - if (!I_NetLoop (DoArbitrate, &data)) - { + if (!I_NetLoop(ExchangeNetGameInfo, &info)) return false; - } + // Let everyone else know the game is ready to start. if (consoleplayer == Net_Arbitrator) { - netbuffer[0] = NCMD_SETUP+3; - SendSetup (data.playersdetected, data.gotsetup, 1); - } - - if (debugfile) - { - for (i = 0; i < doomcom.numnodes; ++i) + NetBuffer[0] = NCMD_GAMEREADY; + for (auto client : NetworkClients) { - fprintf (debugfile, "player %d is on node %d\n", i, nodeforplayer[i]); + if (client != Net_Arbitrator) + HSendPacket(client, 1); } } + I_NetDone(); return true; } -static void SendSetup (uint32_t playersdetected[MAXNETNODES], uint8_t gotsetup[MAXNETNODES], int len) +static void SendSetup(const FNetGameInfo& info, int len) { if (consoleplayer != Net_Arbitrator) { - if (playersdetected[1] & (1 << consoleplayer)) - { - HSendPacket (1, 10); - } - else - { - HSendPacket (1, len); - } + HSendPacket(Net_Arbitrator, len); } else { - for (int i = 1; i < doomcom.numnodes; ++i) + for (auto client : NetworkClients) { - if (!gotsetup[i] || netbuffer[0] == NCMD_SETUP+3) - { - HSendPacket (i, len); - } + // Only send game info over to clients still needing it. + if (client != Net_Arbitrator && !info.GotSetup[client]) + HSendPacket(client, len); } } } -// -// D_CheckNetGame -// Works out player numbers among the net participants -// - -bool D_CheckNetGame (void) +// Connects players to each other if needed. +bool D_CheckNetGame() { - 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"); - } - - int result = I_InitNetwork (); - // I_InitNetwork sets doomcom and netgame + const char* v = Args->CheckValue("-netmode"); + int result = I_InitNetwork(); // I_InitNetwork sets doomcom and netgame if (result == -1) - { return false; - } - else if (result > 0) - { - // 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; + else if (result > 0 && v == nullptr) // Don't override manual netmode setting. + NetMode = NET_PacketServer; + if (doomcom.id != DOOMCOM_ID) + I_FatalError("Invalid doomcom id set for network buffer"); + + players[Net_Arbitrator].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) + if (v != nullptr) + NetMode = atoi(v) ? NET_PacketServer : NET_PeerToPeer; + + if (doomcom.numplayers > 1) { Printf("Selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode. (%s)\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server", - v != NULL ? "forced" : "auto"); + v != nullptr ? "forced" : "auto"); } if (Args->CheckParm("-extratic")) - { - net_extratic = 1; - } + net_extratic = true; } // [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"); - } + D_SetupUserInfo(); if (netgame) { - GameConfig->ReadNetVars (); // [RH] Read network ServerInfo cvars - if (!D_ArbitrateNetStart ()) return false; + GameConfig->ReadNetVars(); // [RH] Read network ServerInfo cvars + if (!D_ExchangeNetInfo()) + return false; } - // read values out of doomcom - ticdup = doomcom.ticdup; + for (auto client : NetworkClients) + playeringame[client] = true; - 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.numplayers > 1) + Printf("Host selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode.\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server"); - 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); + Printf("player %d of %d\n", consoleplayer + 1, doomcom.numplayers); return true; } - // // D_QuitNetGame // Called before quitting to leave a net game // without hanging the other players // -void D_QuitNetGame (void) +void D_QuitNetGame() { - int i, j, k; - - if (!netgame || !usergame || consoleplayer == -1 || demoplayback) + if (!netgame || !usergame || consoleplayer == -1 || demoplayback || NetworkClients.Size() == 1) return; - // send a bunch of packets for security - netbuffer[0] = NCMD_EXIT; - netbuffer[1] = 0; - - k = 2; + // Send a bunch of packets for stability. + NetBuffer[0] = NCMD_EXIT; 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) + // This currently isn't much different from the regular P2P code, but it's being split off into its + // own branch should proper host migration be added in the future (i.e. sending over stored event + // data rather than just dropping it entirely). + int nextHost = 0; + for (auto client : NetworkClients) { - if (playeringame[i] && i != consoleplayer) - WriteInt32 (resendto[nodeforplayer[i]], &foo); + if (client != Net_Arbitrator) + { + nextHost = client; + break; + } } - k = int(foo - netbuffer); - } - for (i = 0; i < 4; i++) + NetBuffer[1] = nextHost; + for (int i = 0; i < 4; ++i) + { + for (auto client : NetworkClients) + { + if (client != Net_Arbitrator) + HSendPacket(client, 2); + } + + I_WaitVBL(1); + } + } + else { - if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) + for (int i = 0; i < 4; ++i) { - HSendPacket (nodeforplayer[Net_Arbitrator], 2); + // If in packet server mode, only the host should know about this + // information. + if (NetMode == NET_PacketServer) + { + HSendPacket(Net_Arbitrator, 1); + } + else + { + for (auto client : NetworkClients) + { + if (client != consoleplayer) + HSendPacket(client, 1); + } + } + + I_WaitVBL(1); } - else - { - for (j = 1; j < doomcom.numnodes; j++) - if (nodeingame[j]) - HSendPacket (j, k); - } - I_WaitVBL (1); + } +} + +ADD_STAT(network) +{ + FString out = {}; + if (!netgame || demoplayback) + { + out.AppendFormat("No network stats available."); + return out; } - if (debugfile) - fclose (debugfile); + out.AppendFormat("Max players: %d\tNet mode: %s\tTic dup: %d", + doomcom.numplayers, + NetMode == NET_PacketServer ? "Packet server" : "Peer to peer", + doomcom.ticdup); + + if (net_extratic) + out.AppendFormat("\tExtra tic enabled"); + + out.AppendFormat("\nWorld tic: %06d (sequence %06d)", gametic, gametic / doomcom.ticdup); + if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) + out.AppendFormat("\tStart tics delay: %d", LevelStartDebug); + + const int delay = max((ClientTic - gametic) / doomcom.ticdup, 0); + const int msDelay = min(delay * doomcom.ticdup * 1000.0 / TICRATE, 999); + out.AppendFormat("\nLocal\n\tIs arbitrator: %d\tDelay: %02d (%03dms)", + consoleplayer == Net_Arbitrator, + delay, msDelay); + + if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) + out.AppendFormat("\tAvg latency: %03ums", min(ClientStates[consoleplayer].AverageLatency, 999u)); + + if (LevelStartStatus != LST_READY) + { + if (LevelStartStatus == LST_HOST) + out.AppendFormat("\tWaiting for packets"); + else if (consoleplayer == Net_Arbitrator) + out.AppendFormat("\tWaiting for acks"); + else + out.AppendFormat("\tWaiting for arbitrator"); + } + + int lowestSeq = ClientTic / doomcom.ticdup; + for (auto client : NetworkClients) + { + if (client == consoleplayer) + continue; + + auto& state = ClientStates[client]; + if (state.CurrentSequence < lowestSeq) + lowestSeq = state.CurrentSequence; + + out.AppendFormat("\n%s", players[client].userinfo.GetName(12)); + if (client == Net_Arbitrator) + out.AppendFormat("\t(Host)"); + + if ((state.Flags & CF_RETRANSMIT) == CF_RETRANSMIT) + out.AppendFormat("\t(RT)"); + else if (state.Flags & CF_RETRANSMIT_SEQ) + out.AppendFormat("\t(RT SEQ)"); + else if (state.Flags & CF_RETRANSMIT_CON) + out.AppendFormat("\t(RT CON)"); + + if ((state.Flags & CF_MISSING) == CF_MISSING) + out.AppendFormat("\t(MISS)"); + else if (state.Flags & CF_MISSING_SEQ) + out.AppendFormat("\t(MISS SEQ)"); + else if (state.Flags & CF_MISSING_CON) + out.AppendFormat("\t(MISS CON)"); + + out.AppendFormat("\n"); + + if (NetMode != NET_PacketServer) + { + const int cDelay = max(state.CurrentSequence - (gametic / doomcom.ticdup), 0); + const int mscDelay = min(cDelay * doomcom.ticdup * 1000.0 / TICRATE, 999); + out.AppendFormat("\tDelay: %02d (%03dms)", cDelay, mscDelay); + } + + out.AppendFormat("\tAck: %06d\tConsistency: %06d", state.SequenceAck, state.ConsistencyAck); + if (NetMode != NET_PacketServer || client != Net_Arbitrator) + out.AppendFormat("\tAvg latency: %03ums", min(state.AverageLatency, 999u)); + } + + if (NetMode != NET_PacketServer || consoleplayer == Net_Arbitrator) + out.AppendFormat("\nAvailable tics: %03d", max(lowestSeq - (gametic / doomcom.ticdup), 0)); + return out; } // Forces playsim processing time to be consistent across frames. @@ -1871,254 +2036,184 @@ static void TicStabilityEnd() stabilityticduration = min(stabilityendtime - stabilitystarttime, (uint64_t)1'000'000); } +// Don't stabilize tics that are going to have incredibly long pauses in them. +static bool ShouldStabilizeTick() +{ + return gameaction != ga_recordgame && gameaction != ga_newgame && gameaction != ga_newgame2 + && gameaction != ga_loadgame && gameaction != ga_loadgamehidecon && gameaction != ga_autoloadgame && gameaction != ga_loadgameplaydemo + && gameaction != ga_savegame && gameaction != ga_autosave + && gameaction != ga_worlddone && gameaction != ga_completed && gameaction != ga_screenshot && gameaction != ga_fullconsole; +} + // // TryRunTics // -void TryRunTics (void) +void TryRunTics() { - int i; - int lowtic; - int realtics; - int availabletics; - int counts; - int numplaying; - - bool doWait = (cl_capfps || pauseext || (r_NoInterpolate && !M_IsAnimated())); - - if (vid_dontdowait && ((vid_maxfps > 0) || (vid_vsync == true))) + GC::CheckGC(); + + bool doWait = (cl_capfps || pauseext || (!netgame && r_NoInterpolate && !M_IsAnimated())); + if (vid_dontdowait && (vid_maxfps > 0 || vid_vsync)) doWait = false; - - if (!AppActive && vid_lowerinbackground) + if (!netgame && !AppActive && vid_lowerinbackground) doWait = true; - // get real tics + // Get the full number of tics the client can run. if (doWait) - { - entertic = I_WaitForTic (oldentertics); - } + EnterTic = I_WaitForTic(LastEnterTic); else - { - entertic = I_GetTime (); - } - realtics = entertic - oldentertics; - oldentertics = entertic; + EnterTic = I_GetTime(); - // get available tics - NetUpdate (); + const int startCommand = ClientTic; + int totalTics = EnterTic - LastEnterTic; + if (totalTics > 1 && singletics) + totalTics = 1; + // Listen for other clients and send out data as needed. This is also + // needed for singleplayer! But is instead handled entirely through local + // buffers. This has a limit of 17 tics that can be generated. + NetUpdate(totalTics); + + LastEnterTic = EnterTic; + + // If the game is paused, everything we need to update has already done so. if (pauseext) return; - lowtic = INT_MAX; - numplaying = 0; - for (i = 0; i < doomcom.numnodes; i++) + // Get the amount of tics the client can actually run. This accounts for waiting for other + // players over the network. + int lowestSequence = INT_MAX; + for (auto client : NetworkClients) { - if (nodeingame[i]) - { - numplaying++; - if (nettics[i] < lowtic) - lowtic = nettics[i]; - } + if (ClientStates[client].CurrentSequence < lowestSequence) + lowestSequence = ClientStates[client].CurrentSequence; } - if (ticdup <= 1) - { - availabletics = lowtic - gametic; - } - else - { - availabletics = lowtic - gametic / ticdup; - } + // If the lowest confirmed tic matches the server gametic or greater, allow the client + // to run some of them. + const int availableTics = (lowestSequence - gametic / doomcom.ticdup) + 1; - // decide how many tics to run - if (realtics < availabletics-1) - counts = realtics+1; - else if (realtics < availabletics) - counts = realtics; - else - counts = availabletics; + // If the amount of tics to run is falling behind the amount of available tics, + // speed the playsim up a bit to help catch up. + int runTics = min(totalTics, availableTics); + if (totalTics > 0 && totalTics < availableTics - 1 && !singletics) + ++runTics; - // Uncapped framerate needs seprate checks - if (counts == 0 && !doWait) + // If there are no tics to run, check for possible stall conditions and new + // commands to predict. + if (runTics <= 0) { - TicStabilityWait(); - - // Check possible stall conditions - Net_CheckLastReceived(counts); - if (realtics >= 1) + // If we actually did have some tics available, make sure the UI + // still has a chance to run. + for (int i = 0; i < totalTics; ++i) { C_Ticker(); M_Ticker(); - // Repredict the player for new buffered movement + } + + // If we're in between a tic, try and balance things out. + if (totalTics <= 0) + TicStabilityWait(); + else + P_ClearLevelInterpolation(); + + // If we actually advanced a command, update the player's position (even if a + // tic passes this isn't guaranteed to happen since it's capped to 35 in advance). + if (ClientTic > startCommand) + { P_UnPredictPlayer(); P_PredictPlayer(&players[consoleplayer]); + S_UpdateSounds(players[consoleplayer].camera); // Update sounds only after predicting the client's newest position. } + return; } - if (counts < 1) - counts = 1; + for (auto client : NetworkClients) + players[client].waiting = false; - if (debugfile) - fprintf (debugfile, - "=======real: %i avail: %i game: %i\n", - realtics, availabletics, counts); + // Update the last time the game tic'd. + LastGameUpdate = EnterTic; - // wait for new tics if needed - while (lowtic < gametic + counts) + // Run the available tics. + P_UnPredictPlayer(); + while (runTics--) { - 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--) - { + const bool stabilize = ShouldStabilizeTick(); + if (stabilize) TicStabilityBegin(); - 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 + if (advancedemo) + D_DoAdvanceDemo(); + + C_Ticker(); + M_Ticker(); + G_Ticker(); + MakeConsistencies(); + ++gametic; + + if (stabilize) TicStabilityEnd(); - } - P_PredictPlayer(&players[consoleplayer]); - S_UpdateSounds (players[consoleplayer].camera); // move positional sounds - } - else - { - TicStabilityWait(); - } -} -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) + if (bCommandsReset) { - //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; + bCommandsReset = false; + break; } } + P_PredictPlayer(&players[consoleplayer]); + S_UpdateSounds(players[consoleplayer].camera); // Update sounds only after predicting the client's newest position. } -void Net_NewMakeTic (void) +void Net_NewClientTic() { - specials.NewMakeTic (); + NetEvents.NewClientTic(); } -void Net_WriteInt8 (uint8_t it) +void Net_Initialize() { - specials << it; + NetEvents.InitializeEventData(); } -void Net_WriteInt16 (int16_t it) +void Net_WriteInt8(uint8_t it) { - specials << it; + NetEvents << it; } -void Net_WriteInt32 (int32_t it) +void Net_WriteInt16(int16_t it) { - specials << it; + NetEvents << it; +} + +void Net_WriteInt32(int32_t it) +{ + NetEvents << it; } void Net_WriteInt64(int64_t it) { - specials << it; + NetEvents << it; } -void Net_WriteFloat (float it) +void Net_WriteFloat(float it) { - specials << it; + NetEvents << it; } void Net_WriteDouble(double it) { - specials << it; + NetEvents << it; } -void Net_WriteString (const char *it) +void Net_WriteString(const char *it) { - specials << it; + NetEvents << it; } -void Net_WriteBytes (const uint8_t *block, int len) +void Net_WriteBytes(const uint8_t *block, int len) { while (len--) - specials << *block++; + NetEvents << *block++; } //========================================================================== @@ -2127,33 +2222,34 @@ void Net_WriteBytes (const uint8_t *block, int len) // //========================================================================== -FDynamicBuffer::FDynamicBuffer () +FDynamicBuffer::FDynamicBuffer() { - m_Data = NULL; + m_Data = nullptr; m_Len = m_BufferLen = 0; } -FDynamicBuffer::~FDynamicBuffer () +FDynamicBuffer::~FDynamicBuffer() { - if (m_Data) + if (m_Data != nullptr) { - M_Free (m_Data); - m_Data = NULL; + M_Free(m_Data); + m_Data = nullptr; } m_Len = m_BufferLen = 0; } -void FDynamicBuffer::SetData (const uint8_t *data, int len) +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); + m_Data = (uint8_t *)M_Realloc(m_Data, m_BufferLen); } - if (data != NULL) + + if (data != nullptr) { m_Len = len; - memcpy (m_Data, data, len); + memcpy(m_Data, data, len); } else { @@ -2161,14 +2257,13 @@ void FDynamicBuffer::SetData (const uint8_t *data, int len) } } -uint8_t *FDynamicBuffer::GetData (int *len) +uint8_t *FDynamicBuffer::GetData(int *len) { - if (len) + if (len != nullptr) *len = m_Len; - return m_Len ? m_Data : NULL; + return m_Len ? m_Data : nullptr; } - static int RemoveClass(FLevelLocals *Level, const PClass *cls) { AActor *actor; @@ -2180,66 +2275,81 @@ static int RemoveClass(FLevelLocals *Level, const PClass *cls) if (actor->IsA(cls)) { // [MC]Do not remove LIVE players. - if (actor->player != NULL) + if (actor->player != nullptr) { 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) +void Net_DoCommand(int cmd, uint8_t **stream, int player) { uint8_t pos = 0; const char* s = nullptr; - int i; + int i = 0; - switch (type) + switch (cmd) { case DEM_SAY: { const char *name = players[player].userinfo.GetName(); - uint8_t who = ReadInt8 (stream); + uint8_t who = ReadInt8(stream); s = ReadStringConst(stream); - 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); - } + // If chat is disabled, there's nothing else to do here since the stream has been advanced. + if (cl_showchat == CHAT_DISABLED || (MutedClients & (1 << player))) + break; + + constexpr int MSG_TEAM = 1; + constexpr int MSG_BOLD = 2; + if (!(who & MSG_TEAM)) + { + if (cl_showchat < CHAT_GLOBAL) + break; + + // Said to everyone + if (deathmatch && teamplay) + Printf(PRINT_CHAT, "(All) "); + if ((who & MSG_BOLD) && !cl_noboldchat) + 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, CHANF_UI, gameinfo.chatSound, 1, ATTN_NONE); + Printf(PRINT_CHAT, "%s" TEXTCOLOR_CHAT ": %s" TEXTCOLOR_CHAT "\n", name, s); + + if (!cl_nochatsound) + S_Sound(CHAN_VOICE, CHANF_UI, gameinfo.chatSound, 1.0f, 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 if (!deathmatch || players[player].userinfo.GetTeam() == players[consoleplayer].userinfo.GetTeam()) + { + if (cl_showchat < CHAT_TEAM_ONLY) + break; + + // Said only to members of the player's team + if (deathmatch && teamplay) + Printf(PRINT_TEAMCHAT, "(Team) "); + if ((who & MSG_BOLD) && !cl_noboldchat) + 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, CHANF_UI, gameinfo.chatSound, 1, ATTN_NONE); + Printf(PRINT_TEAMCHAT, "%s" TEXTCOLOR_TEAMCHAT ": %s" TEXTCOLOR_TEAMCHAT "\n", name, s); + + if (!cl_nochatsound) + S_Sound(CHAN_VOICE, CHANF_UI, gameinfo.chatSound, 1.0f, ATTN_NONE); } } break; @@ -2257,32 +2367,31 @@ void Net_DoCommand (int type, uint8_t **stream, int player) break; case DEM_UINFCHANGED: - D_ReadUserInfoStrings (player, stream, true); + D_ReadUserInfoStrings(player, stream, true); break; case DEM_SINFCHANGED: - D_DoServerInfoChange (stream, false); + D_DoServerInfoChange(stream, false); break; case DEM_SINFCHANGEDXOR: - D_DoServerInfoChange (stream, true); + D_DoServerInfoChange(stream, true); break; case DEM_GIVECHEAT: s = ReadStringConst(stream); - cht_Give (&players[player], s, ReadInt32 (stream)); + cht_Give(&players[player], s, ReadInt32(stream)); if (player != consoleplayer) { FString message = GStrings.GetString("TXT_X_CHEATS"); message.Substitute("%s", players[player].userinfo.GetName()); Printf("%s: give %s\n", message.GetChars(), s); } - break; case DEM_TAKECHEAT: s = ReadStringConst(stream); - cht_Take (&players[player], s, ReadInt32 (stream)); + cht_Take(&players[player], s, ReadInt32(stream)); break; case DEM_SETINV: @@ -2293,21 +2402,20 @@ void Net_DoCommand (int type, uint8_t **stream, int player) case DEM_WARPCHEAT: { - int x, y, z; - x = ReadInt16 (stream); - y = ReadInt16 (stream); - z = ReadInt16 (stream); - P_TeleportMove (players[player].mo, DVector3(x, y, z), true); + int x = ReadInt16(stream); + int y = ReadInt16(stream); + int z = ReadInt16(stream); + P_TeleportMove(players[player].mo, DVector3(x, y, z), true); } break; case DEM_GENERICCHEAT: - cht_DoCheat (&players[player], ReadInt8 (stream)); + cht_DoCheat(&players[player], ReadInt8(stream)); break; case DEM_CHANGEMAP2: - pos = ReadInt8 (stream); - /* intentional fall-through */ + pos = ReadInt8(stream); + [[fallthrough]]; case DEM_CHANGEMAP: // Change to another map without disconnecting other players s = ReadStringConst(stream); @@ -2318,15 +2426,15 @@ void Net_DoCommand (int type, uint8_t **stream, int player) break; case DEM_SUICIDE: - cht_Suicide (&players[player]); + cht_Suicide(&players[player]); break; case DEM_ADDBOT: - primaryLevel->BotInfo.TryAddBot (primaryLevel, stream, player); + primaryLevel->BotInfo.TryAddBot(primaryLevel, stream, player); break; case DEM_KILLBOTS: - primaryLevel->BotInfo.RemoveAllBots (primaryLevel, true); + primaryLevel->BotInfo.RemoveAllBots(primaryLevel, true); Printf ("Removed all bots\n"); break; @@ -2335,7 +2443,8 @@ void Net_DoCommand (int type, uint8_t **stream, int player) break; case DEM_INVUSEALL: - if (gamestate == GS_LEVEL && !paused) + if (gamestate == GS_LEVEL && !paused + && players[player].playerstate != PST_DEAD) { AActor *item = players[player].mo->Inventory; auto pitype = PClass::FindActor(NAME_PuzzleItem); @@ -2355,29 +2464,24 @@ void Net_DoCommand (int type, uint8_t **stream, int player) case DEM_INVUSE: case DEM_INVDROP: { - uint32_t which = ReadInt32 (stream); + uint32_t which = ReadInt32(stream); int amt = -1; - - if (type == DEM_INVDROP) amt = ReadInt32(stream); + if (cmd == DEM_INVDROP) + amt = ReadInt32(stream); if (gamestate == GS_LEVEL && !paused && players[player].playerstate != PST_DEAD) { auto item = players[player].mo->Inventory; - while (item != NULL && item->InventoryID != which) - { + while (item != nullptr && item->InventoryID != which) item = item->Inventory; - } - if (item != NULL) + + if (item != nullptr) { - if (type == DEM_INVUSE) - { - players[player].mo->UseInventory (item); - } + if (cmd == DEM_INVUSE) + players[player].mo->UseInventory(item); else - { - players[player].mo->DropInventory (item, amt); - } + players[player].mo->DropInventory(item, amt); } } } @@ -2397,36 +2501,36 @@ void Net_DoCommand (int type, uint8_t **stream, int player) int args[5]; s = ReadStringConst(stream); - if (type >= DEM_SUMMON2 && type <= DEM_SUMMONFOE2) + if (cmd >= DEM_SUMMON2 && cmd <= DEM_SUMMONFOE2) { angle = ReadInt16(stream); tid = ReadInt16(stream); special = ReadInt8(stream); - for(i = 0; i < 5; i++) args[i] = ReadInt32(stream); + for (i = 0; i < 5; i++) args[i] = ReadInt32(stream); } - AActor *source = players[player].mo; - if(source != NULL) + AActor* source = players[player].mo; + if (source != NULL) { - PClassActor * typeinfo = PClass::FindActor(s); + PClassActor* typeinfo = PClass::FindActor(s); if (typeinfo != NULL) { - if (GetDefaultByType (typeinfo)->flags & MF_MISSILE) + if (GetDefaultByType(typeinfo)->flags & MF_MISSILE) { - P_SpawnPlayerMissile (source, 0, 0, 0, typeinfo, source->Angles.Yaw); + P_SpawnPlayerMissile(source, 0, 0, 0, typeinfo, source->Angles.Yaw); } else { - const AActor *def = GetDefaultByType (typeinfo); + 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); + AActor* spawned = Spawn(primaryLevel, typeinfo, spawnpos, ALLOW_REPLACE); if (spawned != NULL) { spawned->SpawnFlags |= MTF_CONSOLETHING; - if (type == DEM_SUMMONFRIEND || type == DEM_SUMMONFRIEND2 || type == DEM_SUMMONMBF) + if (cmd == DEM_SUMMONFRIEND || cmd == DEM_SUMMONFRIEND2 || cmd == DEM_SUMMONMBF) { - if (spawned->CountsAsKill()) + if (spawned->CountsAsKill()) { primaryLevel->total_monsters--; } @@ -2434,36 +2538,36 @@ void Net_DoCommand (int type, uint8_t **stream, int player) spawned->flags |= MF_FRIENDLY; spawned->LastHeard = players[player].mo; spawned->health = spawned->SpawnHealth(); - if (type == DEM_SUMMONMBF) + if (cmd == DEM_SUMMONMBF) spawned->flags3 |= MF3_NOBLOCKMONST; } - else if (type == DEM_SUMMONFOE || type == DEM_SUMMONFOE2) + else if (cmd == DEM_SUMMONFOE || cmd == DEM_SUMMONFOE2) { spawned->FriendPlayer = 0; spawned->flags &= ~MF_FRIENDLY; spawned->health = spawned->SpawnHealth(); } - if (type >= DEM_SUMMON2 && type <= DEM_SUMMONFOE2) + if (cmd >= DEM_SUMMON2 && cmd <= DEM_SUMMONFOE2) { spawned->Angles.Yaw = source->Angles.Yaw - DAngle::fromDeg(angle); spawned->special = special; - for(i = 0; i < 5; i++) { + for (i = 0; i < 5; i++) { spawned->args[i] = args[i]; } - if(tid) spawned->SetTID(tid); + if (tid) spawned->SetTID(tid); } } } } else { // not an actor, must be a visualthinker - PClass * typeinfo = PClass::FindClass(s); - if(typeinfo && typeinfo->IsDescendantOf("VisualThinker")) + PClass* typeinfo = PClass::FindClass(s); + if (typeinfo && typeinfo->IsDescendantOf("VisualThinker")) { DVector3 spawnpos = source->Vec3Angle(source->radius * 4, source->Angles.Yaw, 8.); auto vt = DVisualThinker::NewVisualThinker(source->Level, typeinfo); - if(vt) + if (vt) { vt->PT.Pos = spawnpos; vt->UpdateSector(); @@ -2490,12 +2594,12 @@ void Net_DoCommand (int type, uint8_t **stream, int player) if (paused) { paused = 0; - S_ResumeSound (false); + S_ResumeSound(false); } else { paused = player + 1; - S_PauseSound (false, false); + S_PauseSound(false, false); } } break; @@ -2510,7 +2614,7 @@ void Net_DoCommand (int type, uint8_t **stream, int player) // 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. FString basename = ExtractFileBase(savegamefile.GetChars(), true); - savegamefile = G_BuildSaveName (basename.GetChars()); + savegamefile = G_BuildSaveName(basename.GetChars()); } } gameaction = ga_savegame; @@ -2520,15 +2624,11 @@ void Net_DoCommand (int type, uint8_t **stream, int player) // 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) + if (!netgame && !demoplayback && disableautosave < 2 && autosavecount + && players[player].playerstate == PST_LIVE) { - break; + Net_WriteInt8(DEM_DOAUTOSAVE); } - Net_WriteInt8 (DEM_DOAUTOSAVE); break; case DEM_DOAUTOSAVE: @@ -2537,36 +2637,29 @@ void Net_DoCommand (int type, uint8_t **stream, int player) case DEM_FOV: { - float newfov = ReadFloat (stream); - - if (newfov != players[consoleplayer].DesiredFOV) + float newfov = ReadFloat(stream); + if (newfov != players[player].DesiredFOV) { - Printf ("FOV%s set to %g\n", - consoleplayer == Net_Arbitrator ? " for everyone" : "", + Printf("FOV%s set to %g\n", + player == Net_Arbitrator ? " for everyone" : "", newfov); } - for (i = 0; i < MAXPLAYERS; ++i) - { - if (playeringame[i]) - { - players[i].DesiredFOV = newfov; - } - } + for (auto client : NetworkClients) + players[client].DesiredFOV = newfov; } break; case DEM_MYFOV: - players[player].DesiredFOV = ReadFloat (stream); + players[player].DesiredFOV = ReadFloat(stream); break; case DEM_RUNSCRIPT: case DEM_RUNSCRIPT2: { - int snum = ReadInt16 (stream); - int argn = ReadInt8 (stream); - - RunScript(stream, players[player].mo, snum, argn, (type == DEM_RUNSCRIPT2) ? ACS_ALWAYS : 0); + int snum = ReadInt16(stream); + int argn = ReadInt8(stream); + RunScript(stream, players[player].mo, snum, argn, (cmd == DEM_RUNSCRIPT2) ? ACS_ALWAYS : 0); } break; @@ -2574,7 +2667,6 @@ void Net_DoCommand (int type, uint8_t **stream, int player) { s = ReadStringConst(stream); int argn = ReadInt8(stream); - RunScript(stream, players[player].mo, -FName(s).GetIndex(), argn & 127, (argn & 128) ? ACS_ALWAYS : 0); } break; @@ -2583,27 +2675,24 @@ void Net_DoCommand (int type, uint8_t **stream, int player) { int snum = ReadInt16(stream); int argn = ReadInt8(stream); - int arg[5] = { 0, 0, 0, 0, 0 }; + int arg[5] = {}; for (i = 0; i < argn; ++i) { int argval = ReadInt32(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]); - } + P_ExecuteSpecial(primaryLevel, snum, nullptr, 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])) + if (gamestate == GS_LEVEL && players[player].mo != nullptr + && players[player].playerstate == PST_LIVE && !(players[player].oldbuttons & BT_JUMP) + && !P_IsPlayerTotallyFrozen(&players[player])) { players[player].crouching = players[player].crouchdir < 0 ? 1 : -1; } @@ -2612,31 +2701,27 @@ void Net_DoCommand (int type, uint8_t **stream, int player) case DEM_MORPHEX: { s = ReadStringConst(stream); - FString msg = cht_Morph (players + player, PClass::FindActor(s), false); + FString msg = cht_Morph(players + player, PClass::FindActor(s), false); if (player == consoleplayer) - { - Printf ("%s\n", msg[0] != '\0' ? msg.GetChars() : "Morph failed."); - } + Printf("%s\n", msg[0] != '\0' ? msg.GetChars() : "Morph failed."); } break; case DEM_ADDCONTROLLER: { - uint8_t playernum = ReadInt8 (stream); + uint8_t playernum = ReadInt8(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()); + Printf("%s has been added to the controller list.\n", players[playernum].userinfo.GetName()); } break; case DEM_DELCONTROLLER: { - uint8_t playernum = ReadInt8 (stream); + uint8_t playernum = ReadInt8(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()); + Printf("%s has been removed from the controller list.\n", players[playernum].userinfo.GetName()); } break; @@ -2646,70 +2731,62 @@ void Net_DoCommand (int type, uint8_t **stream, int player) int killcount = 0; PClassActor *cls = PClass::FindActor(s); - if (cls != NULL) + if (cls != nullptr) { 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); + + Printf("Killed %d monsters of type %s.\n", killcount, s); } else { - Printf ("%s is not an actor class.\n", s); + Printf("%s is not an actor class.\n", s); } - } break; + case DEM_REMOVE: - { - s = ReadStringConst(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) + s = ReadStringConst(stream); + int removecount = 0; + PClassActor *cls = PClass::FindActor(s); + if (cls != nullptr && cls->IsDescendantOf(RUNTIME_CLASS(AActor))) { - removecount += RemoveClass(primaryLevel, cls_rep); + 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); } - 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); + P_ConversationCommand(cmd, player, stream); break; case DEM_SETSLOT: case DEM_SETSLOTPNUM: { - int pnum; - if (type == DEM_SETSLOTPNUM) - { + int pnum = player; + if (cmd == DEM_SETSLOTPNUM) pnum = ReadInt8(stream); - } - else - { - pnum = player; - } + unsigned int slot = ReadInt8(stream); int count = ReadInt8(stream); if (slot < NUM_WEAPON_SLOTS) - { players[pnum].weapons.ClearSlot(slot); - } - for(i = 0; i < count; ++i) + + for (i = 0; i < count; ++i) { PClassActor *wpn = Net_ReadWeapon(stream); players[pnum].weapons.AddSlot(slot, wpn, pnum == consoleplayer); @@ -2744,7 +2821,7 @@ void Net_DoCommand (int type, uint8_t **stream, int player) case DEM_FINISHGAME: // Simulate an end-of-game action - primaryLevel->ChangeLevel(NULL, 0, 0); + primaryLevel->ChangeLevel(nullptr, 0, 0); break; case DEM_NETEVENT: @@ -2768,7 +2845,7 @@ void Net_DoCommand (int type, uint8_t **stream, int player) FName cmd = ReadStringConst(stream); unsigned int size = ReadInt16(stream); - TArray buffer = {}; + TArray buffer; if (size) { buffer.Grow(size); @@ -2779,14 +2856,30 @@ void Net_DoCommand (int type, uint8_t **stream, int player) FNetworkCommand netCmd = { player, cmd, buffer }; primaryLevel->localEventManager->NetCommand(netCmd); } - break; + break; case DEM_CHANGESKILL: NextSkill = ReadInt32(stream); break; + + case DEM_KICK: + { + const int pNum = ReadInt8(stream); + if (pNum == consoleplayer) + { + I_Error("You have been kicked from the game"); + } + else + { + Printf("%s has been kicked from the game\n", players[pNum].userinfo.GetName()); + if (NetworkClients.InGame(pNum)) + DisconnectClient(pNum); + } + } + break; default: - I_Error ("Unknown net command: %d", type); + I_Error("Unknown net command: %d", cmd); break; } } @@ -2794,44 +2887,41 @@ void Net_DoCommand (int type, uint8_t **stream, int player) // Used by DEM_RUNSCRIPT, DEM_RUNSCRIPT2, and DEM_RUNNAMEDSCRIPT static void RunScript(uint8_t **stream, AActor *pawn, int snum, int argn, int always) { + // Scripts can be invoked without a level loaded, e.g. via puke(name) CCMD in fullscreen console if (pawn == nullptr) - { - // Scripts can be invoked without a level loaded, e.g. via puke(name) CCMD in fullscreen console return; - } - int arg[4] = { 0, 0, 0, 0 }; - int i; - - for (i = 0; i < argn; ++i) + int arg[4] = {}; + for (int i = 0; i < argn; ++i) { int argval = ReadInt32(stream); if ((unsigned)i < countof(arg)) - { arg[i] = argval; - } } - P_StartScript(pawn->Level, pawn, NULL, snum, primaryLevel->MapName.GetChars(), arg, min(countof(arg), argn), ACS_NET | always); + + P_StartScript(pawn->Level, pawn, nullptr, snum, primaryLevel->MapName.GetChars(), arg, min(countof(arg), argn), ACS_NET | always); } -void Net_SkipCommand (int type, uint8_t **stream) +// TODO: This really needs to be replaced with some kind of packet system that can simply read through packets and opt +// not to execute them. Right now this is making setting up net commands a nightmare. +// Reads through the network stream but doesn't actually execute any command. Used for getting the size of a stream. +// The skip amount is the number of bytes the command possesses. This should mirror the bytes in Net_DoCommand(). +void Net_SkipCommand(int cmd, uint8_t **stream) { - uint8_t t; - size_t skip; - - switch (type) + size_t skip = 0; + switch (cmd) { case DEM_SAY: - skip = strlen ((char *)(*stream + 1)) + 2; + skip = strlen((char *)(*stream + 1)) + 2; break; case DEM_ADDBOT: - skip = strlen ((char *)(*stream + 1)) + 6; + skip = strlen((char *)(*stream + 1)) + 6; break; case DEM_GIVECHEAT: case DEM_TAKECHEAT: - skip = strlen ((char *)(*stream)) + 5; + skip = strlen((char *)(*stream)) + 5; break; case DEM_SETINV: @@ -2850,7 +2940,7 @@ void Net_SkipCommand (int type, uint8_t **stream) case DEM_SUMMON2: case DEM_SUMMONFRIEND2: case DEM_SUMMONFOE2: - skip = strlen ((char *)(*stream)) + 26; + skip = strlen((char *)(*stream)) + 26; break; case DEM_CHANGEMAP2: skip = strlen((char *)(*stream + 1)) + 2; @@ -2869,7 +2959,7 @@ void Net_SkipCommand (int type, uint8_t **stream) case DEM_MORPHEX: case DEM_KILLCLASSCHEAT: case DEM_MDK: - skip = strlen ((char *)(*stream)) + 1; + skip = strlen((char *)(*stream)) + 1; break; case DEM_WARPCHEAT: @@ -2891,36 +2981,40 @@ void Net_SkipCommand (int type, uint8_t **stream) case DEM_DROPPLAYER: case DEM_ADDCONTROLLER: case DEM_DELCONTROLLER: + case DEM_KICK: skip = 1; break; case DEM_SAVEGAME: - skip = strlen ((char *)(*stream)) + 1; - skip += strlen ((char *)(*stream) + skip) + 1; + 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) + uint8_t t = **stream; + skip = 1 + (t & 63); + if (cmd == DEM_SINFCHANGED) { - case CVAR_Bool: + 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 CVAR_Int: case CVAR_Float: - skip += 4; - break; - case CVAR_String: - skip += strlen ((char *)(*stream + skip)) + 1; - break; } - } - else - { - skip += 1; } break; @@ -2945,11 +3039,9 @@ void Net_SkipCommand (int type, uint8_t **stream) case DEM_SETSLOT: case DEM_SETSLOTPNUM: { - skip = 2 + (type == DEM_SETSLOTPNUM); - for(int numweapons = (*stream)[skip-1]; numweapons > 0; numweapons--) - { + skip = 2 + (cmd == DEM_SETSLOTPNUM); + for (int numweapons = (*stream)[skip-1]; numweapons > 0; --numweapons) skip += 1 + ((*stream)[skip] >> 7); - } } break; @@ -2961,39 +3053,25 @@ void Net_SkipCommand (int type, uint8_t **stream) 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 Net_GetLatency(int* localDelay, int* arbitratorDelay) { - 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); + const int gameDelayMs = (ClientTic - gametic) * 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)) - { + if (gameDelayMs >= 160) severity = 3; - } - *ld = localdelay; - *ad = arbitratordelay; + else if (gameDelayMs >= 120) + severity = 2; + else if (gameDelayMs >= 80) + severity = 1; + + *localDelay = gameDelayMs; + *arbitratorDelay = NetMode == NET_PacketServer ? ClientStates[consoleplayer].AverageLatency : ClientStates[Net_Arbitrator].AverageLatency; return severity; } @@ -3004,13 +3082,198 @@ int Net_GetLatency(int *ld, int *ad) //========================================================================== // [RH] List "ping" times -CCMD (pings) +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()); + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + if (NetworkClients.Size() <= 1) + return; + + // In Packet Server mode, this displays the latency each individual client has to the host + for (auto client : NetworkClients) + { + if ((NetMode == NET_PeerToPeer && client != consoleplayer) || (NetMode == NET_PacketServer && client != Net_Arbitrator)) + Printf("%ums %s\n", ClientStates[client].AverageLatency, players[client].userinfo.GetName()); + } +} + +CCMD(listplayers) +{ + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + for (auto client : NetworkClients) + { + if (client == consoleplayer) + Printf("* "); + Printf("%s - %d\n", players[client].userinfo.GetName(), client); + } +} + +CCMD(kick) +{ + if (argv.argc() == 1) + { + Printf("Usage: kick \n"); + return; + } + + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + // Dont give settings controllers access to this. That should be reserved as a separate power + // the host can grant. + if (consoleplayer != Net_Arbitrator) + { + Printf("Only the host is allowed to kick other players\n"); + return; + } + + int pNum = -1; + if (!C_IsValidInt(argv[1], pNum)) + { + Printf("A player number must be provided. Use listplayers for more information\n"); + return; + } + + if (pNum == consoleplayer || pNum < 0 || pNum >= MAXPLAYERS) + { + Printf("Invalid player number provided\n"); + return; + } + + if (!NetworkClients.InGame(pNum)) + { + Printf("Player is not currently in the game\n"); + return; + } + + Net_WriteInt8(DEM_KICK); + Net_WriteInt8(pNum); +} + +CCMD(mute) +{ + if (argv.argc() == 1) + { + Printf("Usage: mute - Don't receive messages from this player\n"); + return; + } + + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + int pNum = -1; + if (!C_IsValidInt(argv[1], pNum)) + { + Printf("A player number must be provided. Use listplayers for more information\n"); + return; + } + + if (pNum == consoleplayer || pNum < 0 || pNum >= MAXPLAYERS) + { + Printf("Invalid player number provided\n"); + return; + } + + if (!NetworkClients.InGame(pNum)) + { + Printf("Player is not currently in the game\n"); + return; + } + + MutedClients |= 1 << pNum; +} + +CCMD(muteall) +{ + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + for (auto client : NetworkClients) + { + if (client != consoleplayer) + MutedClients |= 1 << client; + } +} + +CCMD(listmuted) +{ + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + bool found = false; + for (auto client : NetworkClients) + { + if (MutedClients & (1 << client)) + { + found = true; + Printf("%s - %d\n", players[client].userinfo.GetName(), client); + } + } + + if (!found) + Printf("No one currently muted\n"); +} + +CCMD(unmute) +{ + if (argv.argc() == 1) + { + Printf("Usage: unmute - Allow messages from this player again\n"); + return; + } + + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + int pNum = -1; + if (!C_IsValidInt(argv[1], pNum)) + { + Printf("A player number must be provided. Use listplayers for more information\n"); + return; + } + + if (pNum == consoleplayer || pNum < 0 || pNum >= MAXPLAYERS) + { + Printf("Invalid player number provided\n"); + return; + } + + MutedClients &= ~(1 << pNum); +} + +CCMD(unmuteall) +{ + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + MutedClients = 0; } //========================================================================== @@ -3022,50 +3285,46 @@ CCMD (pings) // //========================================================================== -static void Network_Controller (int playernum, bool add) +static void Network_Controller(int pNum, bool add) { + if (!netgame) + { + Printf("This command can only be used when playing a net game.\n"); + return; + } + if (consoleplayer != Net_Arbitrator) { - Printf ("This command is only accessible to the net arbitrator.\n"); + Printf("This command is only accessible to the host.\n"); return; } - if (players[playernum].settings_controller && add) + if (pNum == Net_Arbitrator) { - Printf ("%s is already on the setting controller list.\n", players[playernum].userinfo.GetName()); + Printf("The host cannot change their own settings controller status.\n"); return; } - if (!players[playernum].settings_controller && !add) + if (!NetworkClients.InGame(pNum)) { - Printf ("%s is not on the setting controller list.\n", players[playernum].userinfo.GetName()); + Printf("Player %d is not a valid client\n", pNum); return; } - if (!playeringame[playernum]) + if (players[pNum].settings_controller && add) { - Printf ("Player (%d) not found!\n", playernum); + Printf("%s is already on the setting controller list.\n", players[pNum].userinfo.GetName()); return; } - if (players[playernum].Bot != NULL) + if (!players[pNum].settings_controller && !add) { - Printf ("Bots cannot be added to the controller list.\n"); + Printf("%s is not on the setting controller list.\n", players[pNum].userinfo.GetName()); return; } - if (playernum == Net_Arbitrator) - { - Printf ("The net arbitrator cannot have their status changed on this list.\n"); - return; - } - - if (add) - Net_WriteInt8 (DEM_ADDCONTROLLER); - else - Net_WriteInt8 (DEM_DELCONTROLLER); - - Net_WriteInt8 (playernum); + Net_WriteInt8(add ? DEM_ADDCONTROLLER : DEM_DELCONTROLLER); + Net_WriteInt8(pNum); } //========================================================================== @@ -3074,21 +3333,15 @@ static void Network_Controller (int playernum, bool add) // //========================================================================== -CCMD (net_addcontroller) +CCMD(net_addcontroller) { - if (!netgame) + if (argv.argc() < 2) { - Printf ("This command can only be used when playing a net game.\n"); + Printf("Usage: net_addcontroller \n"); return; } - if (argv.argc () < 2) - { - Printf ("Usage: net_addcontroller \n"); - return; - } - - Network_Controller (atoi (argv[1]), true); + Network_Controller(atoi (argv[1]), true); } //========================================================================== @@ -3097,21 +3350,15 @@ CCMD (net_addcontroller) // //========================================================================== -CCMD (net_removecontroller) +CCMD(net_removecontroller) { - if (!netgame) + if (argv.argc() < 2) { - Printf ("This command can only be used when playing a net game.\n"); + Printf("Usage: net_removecontroller \n"); return; } - if (argv.argc () < 2) - { - Printf ("Usage: net_removecontroller \n"); - return; - } - - Network_Controller (atoi (argv[1]), false); + Network_Controller(atoi(argv[1]), false); } //========================================================================== @@ -3120,7 +3367,7 @@ CCMD (net_removecontroller) // //========================================================================== -CCMD (net_listcontrollers) +CCMD(net_listcontrollers) { if (!netgame) { @@ -3128,16 +3375,11 @@ CCMD (net_listcontrollers) return; } - Printf ("The following players can change the game settings:\n"); + Printf("The following players can change the game settings:\n"); - for (int i = 0; i < MAXPLAYERS; i++) + for (auto client : NetworkClients) { - if (!playeringame[i]) - continue; - - if (players[i].settings_controller) - { - Printf ("- %s\n", players[i].userinfo.GetName()); - } + if (players[client].settings_controller) + Printf("- %s\n", players[client].userinfo.GetName()); } } diff --git a/src/d_net.h b/src/d_net.h index e6a6a32b9..c04ec3c34 100644 --- a/src/d_net.h +++ b/src/d_net.h @@ -32,25 +32,109 @@ #include "doomdef.h" #include "d_protocol.h" #include "i_net.h" +#include + +uint64_t I_msTime(); class FDynamicBuffer { public: - FDynamicBuffer (); - ~FDynamicBuffer (); + FDynamicBuffer(); + ~FDynamicBuffer(); - void SetData (const uint8_t *data, int len); - uint8_t *GetData (int *len = NULL); + void SetData(const uint8_t *data, int len); + uint8_t* GetData(int *len = nullptr); private: - uint8_t *m_Data; + uint8_t* m_Data; int m_Len, m_BufferLen; }; -extern FDynamicBuffer NetSpecs[MAXPLAYERS][BACKUPTICS]; +enum EClientFlags +{ + CF_NONE = 0, + CF_QUIT = 1, // If in packet server mode, this client sent an exit command and needs to be disconnected. + CF_MISSING_SEQ = 1 << 1, // If a sequence was missed/out of order, ask this client to send back over their info. + CF_RETRANSMIT_SEQ = 1 << 2, // If set, this client needs command data resent to them. + CF_MISSING_CON = 1 << 3, // If a consistency was missed/out of order, ask this client to send back over their info. + CF_RETRANSMIT_CON = 1 << 4, // If set, this client needs consistency data resent to them. + CF_UPDATED = 1 << 5, // Got an updated packet from this client. + + CF_RETRANSMIT = CF_RETRANSMIT_CON | CF_RETRANSMIT_SEQ, + CF_MISSING = CF_MISSING_CON | CF_MISSING_SEQ, +}; + +struct FClientNetState +{ + // Networked client data. + struct FNetTic { + FDynamicBuffer Data; + usercmd_t Command; + } Tics[BACKUPTICS] = {}; + + // Local information about client. + uint8_t CurrentLatency = 0u; // Current latency id the client is on. If the one the client sends back is > this, update RecvTime and mark a new SentTime. + bool bNewLatency = true; // If the sequence was bumped, the next latency packet sent out should record the send time. + uint16_t AverageLatency = 0u; // Calculate the average latency every second or so, that way it doesn't give huge variance in the scoreboard. + uint64_t SentTime[MAXSENDTICS] = {}; // Timestamp for when we sent out the packet to this client. + uint64_t RecvTime[MAXSENDTICS] = {}; // Timestamp for when the client acknowledged our last packet. If in packet server mode, this is the server's delta. + + int Flags = 0; // State of this client. + + uint8_t ResendID = 0u; // Make sure that if the retransmit happened on a wait barrier, it can be properly resent back over. + int ResendSequenceFrom = -1; // If >= 0, send from this sequence up to the most recent one, capped to MAXSENDTICS. + int SequenceAck = -1; // The last sequence the client reported from us. + int CurrentSequence = -1; // The last sequence we've gotten from this client. + + // Every packet includes consistencies for tics that client ran. When + // a world tic is ran, the local client will store all the consistencies + // of the clients in their LocalConsistency. Then the consistencies will + // be checked against retroactively as they come in. + int ResendConsistencyFrom = -1; // If >= 0, send from this consistency up to the most recent one, capped to MAXSENDTICS. + int ConsistencyAck = -1; // Last consistency the client reported from us. + int LastVerifiedConsistency = -1; // Last consistency we checked from this client. If < CurrentNetConsistency, run through them. + int CurrentNetConsistency = -1; // Last consistency we got from this client. + int16_t NetConsistency[BACKUPTICS] = {}; // Consistencies we got from this client. + int16_t LocalConsistency[BACKUPTICS] = {}; // Local consistency of the client to check against. +}; + +enum ENetMode : uint8_t +{ + NET_PeerToPeer, + NET_PacketServer +}; + +enum EChatType +{ + CHAT_DISABLED, + CHAT_TEAM_ONLY, + CHAT_GLOBAL, +}; + +// New packet structure: +// +// One byte for the net command flags. +// Four bytes for the last sequence we got from that client. +// Four bytes for the last consistency we got from that client. +// If NCMD_QUITTERS set, one byte for the number of players followed by one byte for each player's consolenum. Packet server mode only. +// One byte for the number of players. +// One byte for the number of tics. +// If > 0, four bytes for the base sequence being worked from. +// One byte for the number of world tics ran. +// If > 0, four bytes for the base consistency being worked from. +// If in packet server mode and from the host, one byte for how far ahead of the host we are. +// For each player: +// One byte for the player number. +// If in packet server mode and from the host, two bytes for the latency to the host. +// For each consistency: +// One byte for the delta from the base consistency. +// Two bytes for each consistency. +// For each tic: +// One byte for the delta from the base sequence. +// The remaining command and event data for that player. // Create any new ticcmds and broadcast to other players. -void NetUpdate (void); +void NetUpdate(int tics); // Broadcasts special packets to other players // to notify of game exit @@ -59,74 +143,36 @@ void D_QuitNetGame (void); //? how many ticks to run? void TryRunTics (void); -//Use for checking to see if the netgame has stalled -void Net_CheckLastReceived(int); - // [RH] Functions for making and using special "ticcmds" -void Net_NewMakeTic (); -void Net_WriteInt8 (uint8_t); -void Net_WriteInt16 (int16_t); -void Net_WriteInt32 (int32_t); +void Net_NewClientTic(); +void Net_Initialize(); +void Net_WriteInt8(uint8_t); +void Net_WriteInt16(int16_t); +void Net_WriteInt32(int32_t); void Net_WriteInt64(int64_t); -void Net_WriteFloat (float); +void Net_WriteFloat(float); void Net_WriteDouble(double); -void Net_WriteString (const char *); -void Net_WriteBytes (const uint8_t *, int len); +void Net_WriteString(const char *); +void Net_WriteBytes(const uint8_t *, int len); -void Net_DoCommand (int type, uint8_t **stream, int player); -void Net_SkipCommand (int type, uint8_t **stream); - -void Net_ClearBuffers (); +void Net_DoCommand(int cmd, uint8_t **stream, int player); +void Net_SkipCommand(int cmd, uint8_t **stream); +void Net_ResetCommands(bool midTic); +void Net_SetWaiting(); +void Net_ClearBuffers(); // Netgame stuff (buffers and pointers, i.e. indices). // This is the interface to the packet driver, a separate program // in DOS, but just an abstraction here. -extern doomcom_t doomcom; - -extern struct ticcmd_t localcmds[LOCALCMDTICS]; - -extern int maketic; -extern int nettics[MAXNETNODES]; -extern int netdelay[MAXNETNODES][BACKUPTICS]; -extern int nodeforplayer[MAXPLAYERS]; - -extern ticcmd_t netcmds[MAXPLAYERS][BACKUPTICS]; -extern int ticdup; +extern doomcom_t doomcom; +extern usercmd_t LocalCmds[LOCALCMDTICS]; +extern int ClientTic; +extern FClientNetState ClientStates[MAXPLAYERS]; +extern ENetMode NetMode; class player_t; class DObject; - -// [RH] -// New generic packet structure: -// -// Header: -// One byte with following 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(). - -#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_COMPRESSED 0x04 // remainder of packet is compressed - -#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 - #endif diff --git a/src/d_protocol.cpp b/src/d_protocol.cpp index ec1750a12..044593c96 100644 --- a/src/d_protocol.cpp +++ b/src/d_protocol.cpp @@ -32,44 +32,42 @@ ** */ - #include "d_protocol.h" #include "d_net.h" #include "doomstat.h" #include "cmdlib.h" #include "serializer.h" - -char *ReadString (uint8_t **stream) +char* ReadString(uint8_t **stream) { char *string = *((char **)stream); - *stream += strlen (string) + 1; - return copystring (string); + *stream += strlen(string) + 1; + return copystring(string); } -const char *ReadStringConst(uint8_t **stream) +const char* ReadStringConst(uint8_t **stream) { const char *string = *((const char **)stream); - *stream += strlen (string) + 1; + *stream += strlen(string) + 1; return string; } -uint8_t ReadInt8 (uint8_t **stream) +uint8_t ReadInt8(uint8_t **stream) { uint8_t v = **stream; *stream += 1; return v; } -int16_t ReadInt16 (uint8_t **stream) +int16_t ReadInt16(uint8_t **stream) { int16_t v = (((*stream)[0]) << 8) | (((*stream)[1])); *stream += 2; return v; } -int32_t ReadInt32 (uint8_t **stream) +int32_t ReadInt32(uint8_t **stream) { int32_t v = (((*stream)[0]) << 24) | (((*stream)[1]) << 16) | (((*stream)[2]) << 8) | (((*stream)[3])); *stream += 4; @@ -84,7 +82,7 @@ int64_t ReadInt64(uint8_t** stream) return v; } -float ReadFloat (uint8_t **stream) +float ReadFloat(uint8_t **stream) { union { @@ -106,7 +104,7 @@ double ReadDouble(uint8_t** stream) return fakeint.f; } -void WriteString (const char *string, uint8_t **stream) +void WriteString(const char *string, uint8_t **stream) { char *p = *((char **)stream); @@ -118,21 +116,20 @@ void WriteString (const char *string, uint8_t **stream) *stream = (uint8_t *)p; } - -void WriteInt8 (uint8_t v, uint8_t **stream) +void WriteInt8(uint8_t v, uint8_t **stream) { **stream = v; *stream += 1; } -void WriteInt16 (int16_t v, uint8_t **stream) +void WriteInt16(int16_t v, uint8_t **stream) { (*stream)[0] = v >> 8; (*stream)[1] = v & 255; *stream += 2; } -void WriteInt32 (int32_t v, uint8_t **stream) +void WriteInt32(int32_t v, uint8_t **stream) { (*stream)[0] = v >> 24; (*stream)[1] = (v >> 16) & 255; @@ -154,7 +151,7 @@ void WriteInt64(int64_t v, uint8_t** stream) *stream += 8; } -void WriteFloat (float v, uint8_t **stream) +void WriteFloat(float v, uint8_t **stream) { union { @@ -176,171 +173,7 @@ void WriteDouble(double v, uint8_t** stream) WriteInt64(fakeint.i, stream); } -// Returns the number of bytes read -int UnpackUserCmd (usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream) -{ - uint8_t *start = *stream; - uint8_t flags; - - if (basis != NULL) - { - if (basis != ucmd) - { - memcpy (ucmd, basis, sizeof(usercmd_t)); - } - } - else - { - memset (ucmd, 0, sizeof(usercmd_t)); - } - - flags = ReadInt8 (stream); - - if (flags) - { - // We can support up to 29 buttons, using from 0 to 4 bytes to store them. - if (flags & UCMDF_BUTTONS) - { - uint32_t buttons = ucmd->buttons; - uint8_t in = ReadInt8(stream); - - buttons = (buttons & ~0x7F) | (in & 0x7F); - if (in & 0x80) - { - in = ReadInt8(stream); - buttons = (buttons & ~(0x7F << 7)) | ((in & 0x7F) << 7); - if (in & 0x80) - { - in = ReadInt8(stream); - buttons = (buttons & ~(0x7F << 14)) | ((in & 0x7F) << 14); - if (in & 0x80) - { - in = ReadInt8(stream); - buttons = (buttons & ~(0xFF << 21)) | (in << 21); - } - } - } - ucmd->buttons = buttons; - } - if (flags & UCMDF_PITCH) - ucmd->pitch = ReadInt16 (stream); - if (flags & UCMDF_YAW) - ucmd->yaw = ReadInt16 (stream); - if (flags & UCMDF_FORWARDMOVE) - ucmd->forwardmove = ReadInt16 (stream); - if (flags & UCMDF_SIDEMOVE) - ucmd->sidemove = ReadInt16 (stream); - if (flags & UCMDF_UPMOVE) - ucmd->upmove = ReadInt16 (stream); - if (flags & UCMDF_ROLL) - ucmd->roll = ReadInt16 (stream); - } - - return int(*stream - start); -} - -// Returns the number of bytes written -int PackUserCmd (const usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream) -{ - uint8_t flags = 0; - uint8_t *temp = *stream; - uint8_t *start = *stream; - usercmd_t blank; - uint32_t buttons_changed; - - if (basis == NULL) - { - memset (&blank, 0, sizeof(blank)); - basis = ␣ - } - - WriteInt8 (0, stream); // Make room for the packing bits - - buttons_changed = ucmd->buttons ^ basis->buttons; - if (buttons_changed != 0) - { - uint8_t bytes[4] = { uint8_t(ucmd->buttons & 0x7F), - uint8_t((ucmd->buttons >> 7) & 0x7F), - uint8_t((ucmd->buttons >> 14) & 0x7F), - uint8_t((ucmd->buttons >> 21) & 0xFF) }; - - flags |= UCMDF_BUTTONS; - - if (buttons_changed & 0xFFFFFF80) - { - bytes[0] |= 0x80; - if (buttons_changed & 0xFFFFC000) - { - bytes[1] |= 0x80; - if (buttons_changed & 0xFFE00000) - { - bytes[2] |= 0x80; - } - } - } - WriteInt8 (bytes[0], stream); - if (bytes[0] & 0x80) - { - WriteInt8 (bytes[1], stream); - if (bytes[1] & 0x80) - { - WriteInt8 (bytes[2], stream); - if (bytes[2] & 0x80) - { - WriteInt8 (bytes[3], stream); - } - } - } - } - if (ucmd->pitch != basis->pitch) - { - flags |= UCMDF_PITCH; - WriteInt16 (ucmd->pitch, stream); - } - if (ucmd->yaw != basis->yaw) - { - flags |= UCMDF_YAW; - WriteInt16 (ucmd->yaw, stream); - } - if (ucmd->forwardmove != basis->forwardmove) - { - flags |= UCMDF_FORWARDMOVE; - WriteInt16 (ucmd->forwardmove, stream); - } - if (ucmd->sidemove != basis->sidemove) - { - flags |= UCMDF_SIDEMOVE; - WriteInt16 (ucmd->sidemove, stream); - } - if (ucmd->upmove != basis->upmove) - { - flags |= UCMDF_UPMOVE; - WriteInt16 (ucmd->upmove, stream); - } - if (ucmd->roll != basis->roll) - { - flags |= UCMDF_ROLL; - WriteInt16 (ucmd->roll, stream); - } - - // Write the packing bits - WriteInt8 (flags, &temp); - - return int(*stream - start); -} - -FSerializer &Serialize(FSerializer &arc, const char *key, ticcmd_t &cmd, ticcmd_t *def) -{ - if (arc.BeginObject(key)) - { - arc("consistency", cmd.consistancy) - ("ucmd", cmd.ucmd) - .EndObject(); - } - return arc; -} - -FSerializer &Serialize(FSerializer &arc, const char *key, usercmd_t &cmd, usercmd_t *def) +FSerializer& Serialize(FSerializer& arc, const char* key, usercmd_t& cmd, usercmd_t* def) { // This used packed data with the old serializer but that's totally counterproductive when // having a text format that is human-readable. So this compression has been undone here. @@ -360,192 +193,308 @@ FSerializer &Serialize(FSerializer &arc, const char *key, usercmd_t &cmd, usercm return arc; } -int WriteUserCmdMessage (usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream) +// Returns the number of bytes read +int UnpackUserCmd(usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) { - if (basis == NULL) + const uint8_t* start = stream; + + if (basis != nullptr) { - if (ucmd->buttons != 0 || - ucmd->pitch != 0 || - ucmd->yaw != 0 || - ucmd->forwardmove != 0 || - ucmd->sidemove != 0 || - ucmd->upmove != 0 || - ucmd->roll != 0) - { - WriteInt8 (DEM_USERCMD, stream); - return PackUserCmd (ucmd, basis, stream) + 1; - } + if (basis != &cmd) + memcpy(&cmd, basis, sizeof(usercmd_t)); } else - if (ucmd->buttons != basis->buttons || - ucmd->pitch != basis->pitch || - ucmd->yaw != basis->yaw || - ucmd->forwardmove != basis->forwardmove || - ucmd->sidemove != basis->sidemove || - ucmd->upmove != basis->upmove || - ucmd->roll != basis->roll) { - WriteInt8 (DEM_USERCMD, stream); - return PackUserCmd (ucmd, basis, stream) + 1; + memset(&cmd, 0, sizeof(usercmd_t)); } - WriteInt8 (DEM_EMPTYUSERCMD, stream); + uint8_t flags = ReadInt8(&stream); + if (flags) + { + // We can support up to 29 buttons using 1 to 4 bytes to store them. The most + // significant bit of each button byte is a flag to indicate whether or not more buttons + // should be read in excluding the last one which supports all 8 bits. + if (flags & UCMDF_BUTTONS) + { + uint8_t in = ReadInt8(&stream); + uint32_t buttons = (cmd.buttons & ~0x7F) | (in & 0x7F); + if (in & MoreButtons) + { + in = ReadInt8(&stream); + buttons = (buttons & ~(0x7F << 7)) | ((in & 0x7F) << 7); + if (in & MoreButtons) + { + in = ReadInt8(&stream); + buttons = (buttons & ~(0x7F << 14)) | ((in & 0x7F) << 14); + if (in & MoreButtons) + { + in = ReadInt8(&stream); + buttons = (buttons & ~(0xFF << 21)) | (in << 21); + } + } + } + cmd.buttons = buttons; + } + if (flags & UCMDF_PITCH) + cmd.pitch = ReadInt16(&stream); + if (flags & UCMDF_YAW) + cmd.yaw = ReadInt16(&stream); + if (flags & UCMDF_FORWARDMOVE) + cmd.forwardmove = ReadInt16(&stream); + if (flags & UCMDF_SIDEMOVE) + cmd.sidemove = ReadInt16(&stream); + if (flags & UCMDF_UPMOVE) + cmd.upmove = ReadInt16(&stream); + if (flags & UCMDF_ROLL) + cmd.roll = ReadInt16(&stream); + } + + return int(stream - start); +} + +int PackUserCmd(const usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) +{ + uint8_t flags = 0; + uint8_t* start = stream; + + usercmd_t blank; + if (basis == nullptr) + { + memset(&blank, 0, sizeof(blank)); + basis = ␣ + } + + ++stream; // Make room for the flags. + uint32_t buttons_changed = cmd.buttons ^ basis->buttons; + if (buttons_changed != 0) + { + uint8_t bytes[4] = { uint8_t(cmd.buttons & 0x7F), + uint8_t((cmd.buttons >> 7) & 0x7F), + uint8_t((cmd.buttons >> 14) & 0x7F), + uint8_t((cmd.buttons >> 21) & 0xFF) }; + + flags |= UCMDF_BUTTONS; + if (buttons_changed & 0xFFFFFF80) + { + bytes[0] |= MoreButtons; + if (buttons_changed & 0xFFFFC000) + { + bytes[1] |= MoreButtons; + if (buttons_changed & 0xFFE00000) + bytes[2] |= MoreButtons; + } + } + WriteInt8(bytes[0], &stream); + if (bytes[0] & MoreButtons) + { + WriteInt8(bytes[1], &stream); + if (bytes[1] & MoreButtons) + { + WriteInt8(bytes[2], &stream); + if (bytes[2] & MoreButtons) + WriteInt8(bytes[3], &stream); + } + } + } + if (cmd.pitch != basis->pitch) + { + flags |= UCMDF_PITCH; + WriteInt16(cmd.pitch, &stream); + } + if (cmd.yaw != basis->yaw) + { + flags |= UCMDF_YAW; + WriteInt16 (cmd.yaw, &stream); + } + if (cmd.forwardmove != basis->forwardmove) + { + flags |= UCMDF_FORWARDMOVE; + WriteInt16 (cmd.forwardmove, &stream); + } + if (cmd.sidemove != basis->sidemove) + { + flags |= UCMDF_SIDEMOVE; + WriteInt16(cmd.sidemove, &stream); + } + if (cmd.upmove != basis->upmove) + { + flags |= UCMDF_UPMOVE; + WriteInt16(cmd.upmove, &stream); + } + if (cmd.roll != basis->roll) + { + flags |= UCMDF_ROLL; + WriteInt16(cmd.roll, &stream); + } + + // Write the packing bits + WriteInt8(flags, &start); + + return int(stream - start); +} + +int WriteUserCmdMessage(const usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) +{ + if (basis == nullptr) + { + if (cmd.buttons + || cmd.pitch || cmd.yaw || cmd.roll + || cmd.forwardmove || cmd.sidemove || cmd.upmove) + { + WriteInt8(DEM_USERCMD, &stream); + return PackUserCmd(cmd, basis, stream) + 1; + } + } + else if (cmd.buttons != basis->buttons + || cmd.yaw != basis->yaw || cmd.pitch != basis->pitch || cmd.roll != basis->roll + || cmd.forwardmove != basis->forwardmove || cmd.sidemove != basis->sidemove || cmd.upmove != basis->upmove) + { + WriteInt8(DEM_USERCMD, &stream); + return PackUserCmd(cmd, basis, stream) + 1; + } + + WriteInt8(DEM_EMPTYUSERCMD, &stream); return 1; } - -int SkipTicCmd (uint8_t **stream, int count) +// Reads through the user command without actually setting any of its info. Used to get the size +// of the command when getting the length of the stream. +int SkipUserCmdMessage(uint8_t*& stream) { - int i, skip; - uint8_t *flow = *stream; + const uint8_t* start = stream; - for (i = count; i > 0; i--) + while (true) { - bool moreticdata = true; - - flow += 2; // Skip consistancy marker - while (moreticdata) + const uint8_t type = *stream++; + if (type == DEM_USERCMD) { - uint8_t type = *flow++; - - if (type == DEM_USERCMD) + int skip = 1; + if (*stream & UCMDF_PITCH) + skip += 2; + if (*stream & UCMDF_YAW) + skip += 2; + if (*stream & UCMDF_FORWARDMOVE) + skip += 2; + if (*stream & UCMDF_SIDEMOVE) + skip += 2; + if (*stream & UCMDF_UPMOVE) + skip += 2; + if (*stream & UCMDF_ROLL) + skip += 2; + if (*stream & UCMDF_BUTTONS) { - moreticdata = false; - skip = 1; - if (*flow & UCMDF_PITCH) skip += 2; - if (*flow & UCMDF_YAW) skip += 2; - if (*flow & UCMDF_FORWARDMOVE) skip += 2; - if (*flow & UCMDF_SIDEMOVE) skip += 2; - if (*flow & UCMDF_UPMOVE) skip += 2; - if (*flow & UCMDF_ROLL) skip += 2; - if (*flow & UCMDF_BUTTONS) + if (*++stream & MoreButtons) { - if (*++flow & 0x80) + if (*++stream & MoreButtons) { - if (*++flow & 0x80) - { - if (*++flow & 0x80) - { - ++flow; - } - } + if (*++stream & MoreButtons) + ++stream; } } - flow += skip; - } - else if (type == DEM_EMPTYUSERCMD) - { - moreticdata = false; - } - else - { - Net_SkipCommand (type, &flow); } + stream += skip; + break; } - } - - skip = int(flow - *stream); - *stream = flow; - - return skip; -} - -extern short consistancy[MAXPLAYERS][BACKUPTICS]; -void ReadTicCmd (uint8_t **stream, int player, int tic) -{ - int type; - uint8_t *start; - ticcmd_t *tcmd; - - int ticmod = tic % BACKUPTICS; - - tcmd = &netcmds[player][ticmod]; - tcmd->consistancy = ReadInt16 (stream); - - start = *stream; - - while ((type = ReadInt8 (stream)) != DEM_USERCMD && type != DEM_EMPTYUSERCMD) - Net_SkipCommand (type, stream); - - NetSpecs[player][ticmod].SetData (start, int(*stream - start - 1)); - - if (type == DEM_USERCMD) - { - UnpackUserCmd (&tcmd->ucmd, - tic ? &netcmds[player][(tic-1)%BACKUPTICS].ucmd : NULL, stream); - } - else - { - if (tic) + else if (type == DEM_EMPTYUSERCMD) { - memcpy (&tcmd->ucmd, &netcmds[player][(tic-1)%BACKUPTICS].ucmd, sizeof(tcmd->ucmd)); + break; } else { - memset (&tcmd->ucmd, 0, sizeof(tcmd->ucmd)); + Net_SkipCommand(type, &stream); } } - if (player==consoleplayer&&tic>BACKUPTICS) - assert(consistancy[player][ticmod] == tcmd->consistancy); + return int(stream - start); } -void RunNetSpecs (int player, int buf) +int ReadUserCmdMessage(uint8_t*& stream, int player, int tic) { - uint8_t *stream; - int len; + const int ticMod = tic % BACKUPTICS; - if (gametic % ticdup == 0) + auto& curTic = ClientStates[player].Tics[ticMod]; + usercmd_t& ticCmd = curTic.Command; + + const uint8_t* start = stream; + + // Skip until we reach the player command. Event data will get read off once the + // tick is actually executed. + int type; + while ((type = ReadInt8(&stream)) != DEM_USERCMD && type != DEM_EMPTYUSERCMD) + Net_SkipCommand(type, &stream); + + // Subtract a byte to account for the fact the stream head is now sitting on the + // user command. + curTic.Data.SetData(start, int(stream - start - 1)); + + if (type == DEM_USERCMD) { - stream = NetSpecs[player][buf].GetData (&len); - if (stream) - { - uint8_t *end = stream + len; - while (stream < end) - { - int type = ReadInt8 (&stream); - Net_DoCommand (type, &stream, player); - } - if (!demorecording) - NetSpecs[player][buf].SetData (NULL, 0); - } + UnpackUserCmd(ticCmd, + tic > 0 ? &ClientStates[player].Tics[(tic - 1) % BACKUPTICS].Command : nullptr, stream); + } + else + { + if (tic > 0) + memcpy(&ticCmd, &ClientStates[player].Tics[(tic - 1) % BACKUPTICS].Command, sizeof(ticCmd)); + else + memset(&ticCmd, 0, sizeof(ticCmd)); + } + + return int(stream - start); +} + +void RunPlayerCommands(int player, int tic) +{ + // We don't have the full command yet, so don't run it. + if (gametic % doomcom.ticdup) + return; + + int len; + auto& data = ClientStates[player].Tics[tic % BACKUPTICS].Data; + uint8_t* stream = data.GetData(&len); + if (stream != nullptr) + { + uint8_t* end = stream + len; + while (stream < end) + Net_DoCommand(ReadInt8(&stream), &stream, player); + + if (!demorecording) + data.SetData(nullptr, 0); } } -uint8_t *lenspot; +// Demo related functionality + +uint8_t* streamPos = nullptr; // Write the header of an IFF chunk and leave space // for the length field. -void StartChunk (int id, uint8_t **stream) +void StartChunk(int id, uint8_t **stream) { - WriteInt32 (id, stream); - lenspot = *stream; + WriteInt32(id, stream); + streamPos = *stream; *stream += 4; } // Write the length field for the chunk and insert // pad byte if the chunk is odd-sized. -void FinishChunk (uint8_t **stream) +void FinishChunk(uint8_t **stream) { - int len; - - if (!lenspot) + if (streamPos == nullptr) return; - len = int(*stream - lenspot - 4); - WriteInt32 (len, &lenspot); + int len = int(*stream - streamPos - 4); + WriteInt32(len, &streamPos); if (len & 1) - WriteInt8 (0, stream); + WriteInt8(0, stream); - lenspot = NULL; + streamPos = nullptr; } // Skip past an unknown chunk. *stream should be // pointing to the chunk's length field. -void SkipChunk (uint8_t **stream) +void SkipChunk(uint8_t **stream) { - int len; - - len = ReadInt32 (stream); + int len = ReadInt32(stream); *stream += len + (len & 1); } diff --git a/src/d_protocol.h b/src/d_protocol.h index f726a6ef2..10b38f801 100644 --- a/src/d_protocol.h +++ b/src/d_protocol.h @@ -50,6 +50,7 @@ #define NETD_ID BIGE_ID('N','E','T','D') #define WEAP_ID BIGE_ID('W','E','A','P') +constexpr int MoreButtons = 0x80; struct zdemoheader_s { uint8_t demovermajor; @@ -165,6 +166,7 @@ enum EDemoCommand DEM_ENDSCREENJOB, DEM_ZSC_CMD, // 74 String: Command, Word: Byte size of command DEM_CHANGESKILL, // 75 Int: Skill + DEM_KICK, // 76 Byte: Player number }; // The following are implemented by cht_DoCheat in m_cheat.cpp @@ -230,23 +232,14 @@ void StartChunk (int id, uint8_t **stream); void FinishChunk (uint8_t **stream); void SkipChunk (uint8_t **stream); -int UnpackUserCmd (usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream); -int PackUserCmd (const usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream); -int WriteUserCmdMessage (usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream); +// Returns the number of bytes written to the stream +int UnpackUserCmd(usercmd_t& ucmd, const usercmd_t* basis, uint8_t*& stream); +int PackUserCmd(const usercmd_t& ucmd, const usercmd_t* basis, uint8_t*& stream); +int WriteUserCmdMessage(const usercmd_t& ucmd, const usercmd_t *basis, uint8_t*& stream); -// The data sampled per tick (single player) -// and transmitted to other peers (multiplayer). -// Mainly movements/button commands per game tick, -// plus a checksum for internal state consistency. -struct ticcmd_t -{ - usercmd_t ucmd; - int16_t consistancy; // checks for net game -}; - -int SkipTicCmd (uint8_t **stream, int count); -void ReadTicCmd (uint8_t **stream, int player, int tic); -void RunNetSpecs (int player, int buf); +int SkipUserCmdMessage(uint8_t*& stream); +int ReadUserCmdMessage(uint8_t*& stream, int player, int tic); +void RunPlayerCommands(int player, int tic); uint8_t ReadInt8 (uint8_t **stream); int16_t ReadInt16 (uint8_t **stream); diff --git a/src/doomdef.h b/src/doomdef.h index f1f4f352e..ce9b82316 100644 --- a/src/doomdef.h +++ b/src/doomdef.h @@ -64,7 +64,7 @@ constexpr int TICRATE = 35; enum { // The maximum number of players, multiplayer/networking. - MAXPLAYERS = 8, + MAXPLAYERS = 16, // Amount of damage done by a telefrag. TELEFRAG_DAMAGE = 1000000 diff --git a/src/g_game.cpp b/src/g_game.cpp index 786bf0629..c8b990150 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -100,8 +100,8 @@ extern int startpos, laststartpos; bool WriteZip(const char* filename, const FileSys::FCompressedBuffer* content, size_t contentcount); bool G_CheckDemoStatus (void); -void G_ReadDemoTiccmd (ticcmd_t *cmd, int player); -void G_WriteDemoTiccmd (ticcmd_t *cmd, int player, int buf); +void G_ReadDemoTiccmd (usercmd_t *cmd, int player); +void G_WriteDemoTiccmd (usercmd_t *cmd, int player, int buf); void G_PlayerReborn (int player); void G_DoNewGame (void); @@ -110,6 +110,7 @@ void G_DoPlayDemo (void); void G_DoCompleted (void); void G_DoVictory (void); void G_DoWorldDone (void); +void G_DoMapWarp(); void G_DoSaveGame (bool okForQuicksave, bool forceQuicksave, FString filename, const char *description); void G_DoAutoSave (); void G_DoQuickSave (); @@ -126,6 +127,7 @@ CVAR (Bool, cl_waitforsave, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); CVAR (Bool, enablescriptscreenshot, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); CVAR (Bool, cl_restartondeath, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); EXTERN_CVAR (Float, con_midtime); +EXTERN_CVAR(Bool, net_disablepause) //========================================================================== // @@ -185,8 +187,6 @@ uint8_t* zdembodyend; // end of ZDEM BODY chunk bool singledemo; // quit after playing a demo from cmdline bool precache = true; // if true, load all graphics at start - -short consistancy[MAXPLAYERS][BACKUPTICS]; #define MAXPLMOVE (forwardmove[1]) @@ -353,6 +353,12 @@ CCMD (land) CCMD (pause) { + if (netgame && !players[consoleplayer].settings_controller && net_disablepause) + { + Printf("Only settings controllers can currently (un)pause the game\n"); + return; + } + sendpause = true; } @@ -580,9 +586,9 @@ FBaseCVar* G_GetUserCVar(int playernum, const char* cvarname) return cvar; } -static ticcmd_t emptycmd; +static usercmd_t emptycmd; -ticcmd_t* G_BaseTiccmd() +usercmd_t* G_BaseTiccmd() { return &emptycmd; } @@ -594,7 +600,7 @@ ticcmd_t* G_BaseTiccmd() // or reads it from the demo buffer. // If recording a demo, write it out // -void G_BuildTiccmd (ticcmd_t *cmd) +void G_BuildTiccmd (usercmd_t *cmd) { int strafe; int speed; @@ -602,13 +608,11 @@ void G_BuildTiccmd (ticcmd_t *cmd) int side; int fly; - ticcmd_t *base; + usercmd_t *base; base = G_BaseTiccmd (); *cmd = *base; - cmd->consistancy = consistancy[consoleplayer][(maketic/ticdup)%BACKUPTICS]; - strafe = buttonMap.ButtonDown(Button_Strafe); speed = buttonMap.ButtonDown(Button_Speed) ^ (int)cl_run; @@ -618,7 +622,7 @@ void G_BuildTiccmd (ticcmd_t *cmd) // and not the joystick, since we treat the joystick as // the analog device it is. if (buttonMap.ButtonDown(Button_Left) || buttonMap.ButtonDown(Button_Right)) - turnheld += ticdup; + turnheld += doomcom.ticdup; else turnheld = 0; @@ -682,33 +686,33 @@ void G_BuildTiccmd (ticcmd_t *cmd) side -= sidemove[speed]; // buttons - if (buttonMap.ButtonDown(Button_Attack)) cmd->ucmd.buttons |= BT_ATTACK; - if (buttonMap.ButtonDown(Button_AltAttack)) cmd->ucmd.buttons |= BT_ALTATTACK; - if (buttonMap.ButtonDown(Button_Use)) cmd->ucmd.buttons |= BT_USE; - if (buttonMap.ButtonDown(Button_Jump)) cmd->ucmd.buttons |= BT_JUMP; - if (buttonMap.ButtonDown(Button_Crouch)) cmd->ucmd.buttons |= BT_CROUCH; - if (buttonMap.ButtonDown(Button_Zoom)) cmd->ucmd.buttons |= BT_ZOOM; - if (buttonMap.ButtonDown(Button_Reload)) cmd->ucmd.buttons |= BT_RELOAD; + if (buttonMap.ButtonDown(Button_Attack)) cmd->buttons |= BT_ATTACK; + if (buttonMap.ButtonDown(Button_AltAttack)) cmd->buttons |= BT_ALTATTACK; + if (buttonMap.ButtonDown(Button_Use)) cmd->buttons |= BT_USE; + if (buttonMap.ButtonDown(Button_Jump)) cmd->buttons |= BT_JUMP; + if (buttonMap.ButtonDown(Button_Crouch)) cmd->buttons |= BT_CROUCH; + if (buttonMap.ButtonDown(Button_Zoom)) cmd->buttons |= BT_ZOOM; + if (buttonMap.ButtonDown(Button_Reload)) cmd->buttons |= BT_RELOAD; - if (buttonMap.ButtonDown(Button_User1)) cmd->ucmd.buttons |= BT_USER1; - if (buttonMap.ButtonDown(Button_User2)) cmd->ucmd.buttons |= BT_USER2; - if (buttonMap.ButtonDown(Button_User3)) cmd->ucmd.buttons |= BT_USER3; - if (buttonMap.ButtonDown(Button_User4)) cmd->ucmd.buttons |= BT_USER4; + if (buttonMap.ButtonDown(Button_User1)) cmd->buttons |= BT_USER1; + if (buttonMap.ButtonDown(Button_User2)) cmd->buttons |= BT_USER2; + if (buttonMap.ButtonDown(Button_User3)) cmd->buttons |= BT_USER3; + if (buttonMap.ButtonDown(Button_User4)) cmd->buttons |= BT_USER4; - if (buttonMap.ButtonDown(Button_Speed)) cmd->ucmd.buttons |= BT_SPEED; - if (buttonMap.ButtonDown(Button_Strafe)) cmd->ucmd.buttons |= BT_STRAFE; - if (buttonMap.ButtonDown(Button_MoveRight)) cmd->ucmd.buttons |= BT_MOVERIGHT; - if (buttonMap.ButtonDown(Button_MoveLeft)) cmd->ucmd.buttons |= BT_MOVELEFT; - if (buttonMap.ButtonDown(Button_LookDown)) cmd->ucmd.buttons |= BT_LOOKDOWN; - if (buttonMap.ButtonDown(Button_LookUp)) cmd->ucmd.buttons |= BT_LOOKUP; - if (buttonMap.ButtonDown(Button_Back)) cmd->ucmd.buttons |= BT_BACK; - if (buttonMap.ButtonDown(Button_Forward)) cmd->ucmd.buttons |= BT_FORWARD; - if (buttonMap.ButtonDown(Button_Right)) cmd->ucmd.buttons |= BT_RIGHT; - if (buttonMap.ButtonDown(Button_Left)) cmd->ucmd.buttons |= BT_LEFT; - if (buttonMap.ButtonDown(Button_MoveDown)) cmd->ucmd.buttons |= BT_MOVEDOWN; - if (buttonMap.ButtonDown(Button_MoveUp)) cmd->ucmd.buttons |= BT_MOVEUP; - if (buttonMap.ButtonDown(Button_ShowScores)) cmd->ucmd.buttons |= BT_SHOWSCORES; - if (speed) cmd->ucmd.buttons |= BT_RUN; + if (buttonMap.ButtonDown(Button_Speed)) cmd->buttons |= BT_SPEED; + if (buttonMap.ButtonDown(Button_Strafe)) cmd->buttons |= BT_STRAFE; + if (buttonMap.ButtonDown(Button_MoveRight)) cmd->buttons |= BT_MOVERIGHT; + if (buttonMap.ButtonDown(Button_MoveLeft)) cmd->buttons |= BT_MOVELEFT; + if (buttonMap.ButtonDown(Button_LookDown)) cmd->buttons |= BT_LOOKDOWN; + if (buttonMap.ButtonDown(Button_LookUp)) cmd->buttons |= BT_LOOKUP; + if (buttonMap.ButtonDown(Button_Back)) cmd->buttons |= BT_BACK; + if (buttonMap.ButtonDown(Button_Forward)) cmd->buttons |= BT_FORWARD; + if (buttonMap.ButtonDown(Button_Right)) cmd->buttons |= BT_RIGHT; + if (buttonMap.ButtonDown(Button_Left)) cmd->buttons |= BT_LEFT; + if (buttonMap.ButtonDown(Button_MoveDown)) cmd->buttons |= BT_MOVEDOWN; + if (buttonMap.ButtonDown(Button_MoveUp)) cmd->buttons |= BT_MOVEUP; + if (buttonMap.ButtonDown(Button_ShowScores)) cmd->buttons |= BT_SHOWSCORES; + if (speed) cmd->buttons |= BT_RUN; // Handle joysticks/game controllers. float joyaxes[NUM_JOYAXIS]; @@ -746,7 +750,7 @@ void G_BuildTiccmd (ticcmd_t *cmd) forward += xs_CRoundToInt(mousey * m_forward); } - cmd->ucmd.pitch = LocalViewPitch >> 16; + cmd->pitch = LocalViewPitch >> 16; if (SendLand) { @@ -769,10 +773,10 @@ void G_BuildTiccmd (ticcmd_t *cmd) else if (side < -MAXPLMOVE) side = -MAXPLMOVE; - cmd->ucmd.forwardmove += forward; - cmd->ucmd.sidemove += side; - cmd->ucmd.yaw = LocalViewAngle >> 16; - cmd->ucmd.upmove = fly; + cmd->forwardmove += forward; + cmd->sidemove += side; + cmd->yaw = LocalViewAngle >> 16; + cmd->upmove = fly; LocalViewAngle = 0; LocalViewPitch = 0; @@ -780,7 +784,7 @@ void G_BuildTiccmd (ticcmd_t *cmd) if (sendturn180) { sendturn180 = false; - cmd->ucmd.buttons |= BT_TURN180; + cmd->buttons |= BT_TURN180; } if (sendpause) { @@ -814,8 +818,8 @@ void G_BuildTiccmd (ticcmd_t *cmd) SendItemDrop = NULL; } - cmd->ucmd.forwardmove <<= 8; - cmd->ucmd.sidemove <<= 8; + cmd->forwardmove <<= 8; + cmd->sidemove <<= 8; } static int LookAdjust(int look) @@ -1111,53 +1115,25 @@ static void G_FullConsole() } -//========================================================================== -// -// FRandom :: StaticSumSeeds -// -// This function produces a uint32_t that can be used to check the consistancy -// of network games between different machines. Only a select few RNGs are -// used for the sum, because not all RNGs are important to network sync. -// -//========================================================================== - -extern FRandom pr_spawnmobj; -extern FRandom pr_acs; -extern FRandom pr_chase; -extern FRandom pr_damagemobj; - -static uint32_t StaticSumSeeds() -{ - return - pr_spawnmobj.Seed() + - pr_acs.Seed() + - pr_chase.Seed() + - pr_damagemobj.Seed(); -} - // // G_Ticker // Make ticcmd_ts for the players. // void G_Ticker () { - int i; gamestate_t oldgamestate; // do player reborns if needed - for (i = 0; i < MAXPLAYERS; i++) + // TODO: These should really be moved to queues. + for (int i = 0; i < MAXPLAYERS; ++i) { - if (playeringame[i]) - { - if (players[i].playerstate == PST_GONE) - { - G_DoPlayerPop(i); - } - if (players[i].playerstate == PST_REBORN || players[i].playerstate == PST_ENTER) - { - primaryLevel->DoReborn(i); - } - } + if (!playeringame[i]) + continue; + + if (players[i].playerstate == PST_GONE) + G_DoPlayerPop(i); + else if (players[i].playerstate == PST_REBORN || players[i].playerstate == PST_ENTER) + primaryLevel->DoReborn(i, false); } if (ToggleFullscreen) @@ -1207,6 +1183,9 @@ void G_Ticker () case ga_playdemo: G_DoPlayDemo (); break; + case ga_mapwarp: + G_DoMapWarp(); + break; case ga_completed: G_DoCompleted (); break; @@ -1253,68 +1232,34 @@ void G_Ticker () } // get commands, check consistancy, and build new consistancy check - int buf = (gametic/ticdup)%BACKUPTICS; - - // [RH] Include some random seeds and player stuff in the consistancy - // check, not just the player's x position like BOOM. - uint32_t rngsum = StaticSumSeeds (); + const int curTic = gametic / doomcom.ticdup; //Added by MC: For some of that bot stuff. The main bot function. primaryLevel->BotInfo.Main (primaryLevel); - for (i = 0; i < MAXPLAYERS; i++) + for (auto client : NetworkClients) { - if (playeringame[i]) + usercmd_t *cmd = &players[client].cmd; + usercmd_t* nextCmd = &ClientStates[client].Tics[curTic % BACKUPTICS].Command; + + RunPlayerCommands(client, curTic); + if (demorecording) + G_WriteDemoTiccmd(nextCmd, client, curTic); + + players[client].oldbuttons = cmd->buttons; + // If the user alt-tabbed away, paused gets set to -1. In this case, + // we do not want to read more demo commands until paused is no + // longer negative. + if (demoplayback) + G_ReadDemoTiccmd(cmd, client); + else + memcpy(cmd, nextCmd, sizeof(usercmd_t)); + + // check for turbo cheats + if (multiplayer && turbo > 100.f && cmd->forwardmove > TURBOTHRESHOLD && + !(gametic & 31) && ((gametic >> 5) & (MAXPLAYERS-1)) == client) { - ticcmd_t *cmd = &players[i].cmd; - ticcmd_t *newcmd = &netcmds[i][buf]; - - if ((gametic % ticdup) == 0) - { - RunNetSpecs (i, buf); - } - if (demorecording) - { - G_WriteDemoTiccmd (newcmd, i, buf); - } - players[i].oldbuttons = cmd->ucmd.buttons; - // If the user alt-tabbed away, paused gets set to -1. In this case, - // we do not want to read more demo commands until paused is no - // longer negative. - if (demoplayback) - { - G_ReadDemoTiccmd (cmd, i); - } - else - { - memcpy(cmd, newcmd, sizeof(ticcmd_t)); - } - - // check for turbo cheats - if (multiplayer && turbo > 100.f && cmd->ucmd.forwardmove > TURBOTHRESHOLD && - !(gametic&31) && ((gametic>>5)&(MAXPLAYERS-1)) == i ) - { - Printf ("%s is turbo!\n", players[i].userinfo.GetName()); - } - - if (netgame && players[i].Bot == NULL && !demoplayback && (gametic%ticdup) == 0) - { - //players[i].inconsistant = 0; - if (gametic > BACKUPTICS*ticdup && consistancy[i][buf] != cmd->consistancy) - { - players[i].inconsistant = gametic - BACKUPTICS*ticdup; - } - if (players[i].mo) - { - uint32_t sum = rngsum + int((players[i].mo->X() + players[i].mo->Y() + players[i].mo->Z())*257) + players[i].mo->Angles.Yaw.BAMs() + players[i].mo->Angles.Pitch.BAMs(); - sum ^= players[i].health; - consistancy[i][buf] = sum; - } - else - { - consistancy[i][buf] = rngsum; - } - } + Printf("%s is turbo!\n", players[client].userinfo.GetName()); } } @@ -2026,6 +1971,7 @@ void FinishLoadingCVars(); void G_DoLoadGame () { + Net_ResetCommands(true); SetupLoadingCVars(); bool hidecon; @@ -2182,6 +2128,7 @@ void G_DoLoadGame () NextSkill = -1; arc("nextskill", NextSkill); + Net_SetWaiting(); if (level.info != nullptr) level.info->Snapshot.Clean(); @@ -2526,7 +2473,7 @@ void G_DoSaveGame (bool okForQuicksave, bool forceQuicksave, FString filename, c // DEMO RECORDING // -void G_ReadDemoTiccmd (ticcmd_t *cmd, int player) +void G_ReadDemoTiccmd (usercmd_t *cmd, int player) { int id = DEM_BAD; @@ -2549,7 +2496,7 @@ void G_ReadDemoTiccmd (ticcmd_t *cmd, int player) break; case DEM_USERCMD: - UnpackUserCmd (&cmd->ucmd, &cmd->ucmd, &demo_p); + UnpackUserCmd (*cmd, cmd, demo_p); break; case DEM_EMPTYUSERCMD: @@ -2580,9 +2527,9 @@ CCMD (stop) stoprecording = true; } -extern uint8_t *lenspot; +extern uint8_t *streamPos; -void G_WriteDemoTiccmd (ticcmd_t *cmd, int player, int buf) +void G_WriteDemoTiccmd (usercmd_t *cmd, int player, int buf) { uint8_t *specdata; int speclen; @@ -2598,28 +2545,29 @@ void G_WriteDemoTiccmd (ticcmd_t *cmd, int player, int buf) } // [RH] Write any special "ticcmds" for this player to the demo - if ((specdata = NetSpecs[player][buf].GetData (&speclen)) && gametic % ticdup == 0) + if ((specdata = ClientStates[player].Tics[buf % BACKUPTICS].Data.GetData (&speclen)) && !(gametic % doomcom.ticdup)) { memcpy (demo_p, specdata, speclen); demo_p += speclen; - NetSpecs[player][buf].SetData (NULL, 0); + + ClientStates[player].Tics[buf % BACKUPTICS].Data.SetData(nullptr, 0); } // [RH] Now write out a "normal" ticcmd. - WriteUserCmdMessage (&cmd->ucmd, &players[player].cmd.ucmd, &demo_p); + WriteUserCmdMessage (*cmd, &players[player].cmd, demo_p); // [RH] Bigger safety margin if (demo_p > demobuffer + maxdemosize - 64) { ptrdiff_t pos = demo_p - demobuffer; - ptrdiff_t spot = lenspot - demobuffer; + ptrdiff_t spot = streamPos - demobuffer; ptrdiff_t comp = democompspot - demobuffer; ptrdiff_t body = demobodyspot - demobuffer; // [RH] Allocate more space for the demo maxdemosize += 0x20000; demobuffer = (uint8_t *)M_Realloc (demobuffer, maxdemosize); demo_p = demobuffer + pos; - lenspot = demobuffer + spot; + streamPos = demobuffer + spot; democompspot = demobuffer + comp; demobodyspot = demobuffer + body; } @@ -3001,7 +2949,6 @@ void G_TimeDemo (const char* name) nodrawers = !!Args->CheckParm ("-nodraw"); noblit = !!Args->CheckParm ("-noblit"); timingdemo = true; - singletics = true; defdemoname = name; gameaction = (gameaction == ga_loadgame) ? ga_loadgameplaydemo : ga_playdemo; @@ -3041,7 +2988,6 @@ bool G_CheckDemoStatus (void) demoplayback = false; netgame = false; multiplayer = false; - singletics = false; for (int i = 1; i < MAXPLAYERS; i++) playeringame[i] = 0; consoleplayer = 0; diff --git a/src/g_level.cpp b/src/g_level.cpp index 9fe3c9699..f39588cae 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -1560,13 +1560,28 @@ DEFINE_ACTION_FUNCTION(FLevelLocals, WorldDone) return 0; } +//========================================================================== +// +// Started the game loaded into a map from the console. Run at least one tic +// so it can properly render out in net games. +// +//========================================================================== + +void G_DoMapWarp() +{ + gameaction = ga_nothing; + Net_ResetCommands(true); + Net_SetWaiting(); +} + //========================================================================== // // //========================================================================== void G_DoWorldDone (void) -{ +{ + Net_ResetCommands(true); gamestate = GS_LEVEL; if (nextlevel.IsEmpty()) { @@ -1578,6 +1593,7 @@ void G_DoWorldDone (void) G_DoLoadLevel (nextlevel, startpos, true, false); gameaction = ga_nothing; viewactive = true; + Net_SetWaiting(); } //========================================================================== diff --git a/src/g_statusbar/sbar.h b/src/g_statusbar/sbar.h index bffa1a823..5d3f9794d 100644 --- a/src/g_statusbar/sbar.h +++ b/src/g_statusbar/sbar.h @@ -413,6 +413,10 @@ public: return SBarTop; } void DoDrawAutomapHUD(int crdefault, int highlight); + void ClearInterpolation() + { + PrevCrosshairSize = CrosshairSize; + } //protected: void DrawPowerups (); diff --git a/src/g_statusbar/shared_sbar.cpp b/src/g_statusbar/shared_sbar.cpp index 5890fd58f..d336808b1 100644 --- a/src/g_statusbar/shared_sbar.cpp +++ b/src/g_statusbar/shared_sbar.cpp @@ -1234,19 +1234,28 @@ void DBaseStatusBar::DrawTopStuff (EHudState state) } -void DBaseStatusBar::DrawConsistancy () const +void DBaseStatusBar::DrawConsistancy() const { if (!netgame) return; bool desync = false; FString text = "Out of sync with:"; - for (int i = 0; i < MAXPLAYERS; i++) + for (auto client : NetworkClients) { - if (playeringame[i] && players[i].inconsistant) + if (players[client].inconsistant) { desync = true; - text.AppendFormat(" %s (%d)", players[i].userinfo.GetName(10u), i + 1); + // Fell out of sync with the host in packet server mode. Which specific user it is doesn't really matter. + if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) + { + text.AppendFormat(" %s (%d)", players[Net_Arbitrator].userinfo.GetName(10u), Net_Arbitrator + 1); + break; + } + else + { + text.AppendFormat(" %s (%d)", players[client].userinfo.GetName(10u), client + 1); + } } } @@ -1265,19 +1274,19 @@ void DBaseStatusBar::DrawConsistancy () const } } -void DBaseStatusBar::DrawWaiting () const +void DBaseStatusBar::DrawWaiting() const { if (!netgame) return; FString text = "Waiting for:"; bool isWaiting = false; - for (int i = 0; i < MAXPLAYERS; i++) + for (auto client : NetworkClients) { - if (playeringame[i] && players[i].waiting) + if (players[client].waiting) { isWaiting = true; - text.AppendFormat(" %s (%d)", players[i].userinfo.GetName(10u), i + 1); + text.AppendFormat(" %s (%d)", players[client].userinfo.GetName(10u), client + 1); } } diff --git a/src/hu_scores.cpp b/src/hu_scores.cpp index d7cde85fc..bb7999dda 100644 --- a/src/hu_scores.cpp +++ b/src/hu_scores.cpp @@ -146,10 +146,17 @@ static int FontScale; // //========================================================================== -void HU_DrawScores (player_t *player) +void HU_DrawScores(player_t* player) { displayFont = NewSmallFont; - FontScale = max(screen->GetHeight() / 400, 1); + FontScale = max(screen->GetHeight() / 400, 1); + + int numPlayers = 0; + for (int i = 0; i < MAXPLAYERS; ++i) + numPlayers += playeringame[i]; + + if (numPlayers > 8) + FontScale = static_cast(ceil(FontScale * 0.75)); if (deathmatch) { @@ -158,39 +165,34 @@ void HU_DrawScores (player_t *player) if (!sb_teamdeathmatch_enable) return; } - else + else if (!sb_deathmatch_enable) { - if (!sb_deathmatch_enable) - return; + return; } } - else + else if (!multiplayer || !sb_cooperative_enable) { - if (!sb_cooperative_enable || !multiplayer) - return; + return; } - int i, j; - player_t *sortedplayers[MAXPLAYERS]; - + player_t* sortedPlayers[MAXPLAYERS]; if (player->camera && player->camera->player) player = player->camera->player; - sortedplayers[MAXPLAYERS-1] = player; - for (i = 0, j = 0; j < MAXPLAYERS - 1; i++, j++) + sortedPlayers[MAXPLAYERS - 1] = player; + for (int i = 0, j = 0; j < MAXPLAYERS - 1; ++i, ++j) { if (&players[i] == player) - i++; - sortedplayers[j] = &players[i]; + ++i; + sortedPlayers[j] = &players[i]; } if (teamplay && deathmatch) - qsort (sortedplayers, MAXPLAYERS, sizeof(player_t *), compareteams); + qsort(sortedPlayers, MAXPLAYERS, sizeof(player_t*), compareteams); else - qsort (sortedplayers, MAXPLAYERS, sizeof(player_t *), comparepoints); - - HU_DoDrawScores (player, sortedplayers); + qsort(sortedPlayers, MAXPLAYERS, sizeof(player_t*), comparepoints); + HU_DoDrawScores(player, sortedPlayers); } //========================================================================== @@ -201,39 +203,37 @@ void HU_DrawScores (player_t *player) // //========================================================================== -void HU_GetPlayerWidths(int &maxnamewidth, int &maxscorewidth, int &maxiconheight) +void HU_GetPlayerWidths(int& maxNameWidth, int& maxScoreWidth, int& maxIconHeight) { - displayFont = NewSmallFont; - maxnamewidth = displayFont->StringWidth("Name"); - maxscorewidth = 0; - maxiconheight = 0; + constexpr char NameLabel[] = "Name"; - for (int i = 0; i < MAXPLAYERS; i++) + displayFont = NewSmallFont; + maxNameWidth = displayFont->StringWidth(NameLabel); + maxScoreWidth = 0; + maxIconHeight = 0; + + for (int i = 0; i < MAXPLAYERS; ++i) { - if (playeringame[i]) + if (!playeringame[i]) + continue; + + int width = displayFont->StringWidth(players[i].userinfo.GetName(16)); + if (width > maxNameWidth) + maxNameWidth = width; + + auto icon = FSetTextureID(players[i].mo->IntVar(NAME_ScoreIcon)); + if (icon.isValid()) { - int width = displayFont->StringWidth(players[i].userinfo.GetName()); - if (width > maxnamewidth) - { - maxnamewidth = width; - } - auto icon = FSetTextureID(players[i].mo->IntVar(NAME_ScoreIcon)); - if (icon.isValid()) - { - auto pic = TexMan.GetGameTexture(icon); - width = int(pic->GetDisplayWidth() - pic->GetDisplayLeftOffset() + 2.5); - if (width > maxscorewidth) - { - maxscorewidth = width; - } - // The icon's top offset does not count toward its height, because - // zdoom.pk3's standard Hexen class icons are designed that way. - int height = int(pic->GetDisplayHeight() - pic->GetDisplayTopOffset() + 0.5); - if (height > maxiconheight) - { - maxiconheight = height; - } - } + auto pic = TexMan.GetGameTexture(icon); + width = int(pic->GetDisplayWidth() - pic->GetDisplayLeftOffset() + 2.5); + if (width > maxScoreWidth) + maxScoreWidth = width; + + // The icon's top offset does not count toward its height, because + // zdoom.pk3's standard Hexen class icons are designed that way. + int height = int(pic->GetDisplayHeight() - pic->GetDisplayTopOffset() + 0.5); + if (height > maxIconHeight) + maxIconHeight = height; } } } @@ -249,16 +249,9 @@ static void HU_DrawFontScaled(double x, double y, int color, const char *text) DrawText(twod, displayFont, color, x / FontScale, y / FontScale, text, DTA_VirtualWidth, twod->GetWidth() / FontScale, DTA_VirtualHeight, twod->GetHeight() / FontScale, TAG_END); } -static void HU_DoDrawScores (player_t *player, player_t *sortedplayers[MAXPLAYERS]) +static void HU_DoDrawScores(player_t* player, player_t* sortedPlayers[MAXPLAYERS]) { - int color; - int height, lineheight; - unsigned int i; - int maxnamewidth, maxscorewidth, maxiconheight; - int numTeams = 0; - int x, y, ypadding, bottom; - int col2, col3, col4, col5; - + int color = sb_cooperative_headingcolor; if (deathmatch) { if (teamplay) @@ -266,86 +259,77 @@ static void HU_DoDrawScores (player_t *player, player_t *sortedplayers[MAXPLAYER else color = sb_deathmatch_headingcolor; } - else - { - color = sb_cooperative_headingcolor; - } - HU_GetPlayerWidths(maxnamewidth, maxscorewidth, maxiconheight); - height = displayFont->GetHeight() * FontScale; - lineheight = max(height, maxiconheight * CleanYfac); - ypadding = (lineheight - height + 1) / 2; + int maxNameWidth, maxScoreWidth, maxIconHeight; + HU_GetPlayerWidths(maxNameWidth, maxScoreWidth, maxIconHeight); + int height = displayFont->GetHeight() * FontScale; + int lineHeight = max(height, maxIconHeight * CleanYfac); + int yPadding = (lineHeight - height + 1) / 2; - bottom = StatusBar->GetTopOfStatusbar(); - y = max(48*CleanYfac, (bottom - MAXPLAYERS * (height + CleanYfac + 1)) / 2); + int bottom = StatusBar->GetTopOfStatusbar(); + int y = max(48 * CleanYfac, (bottom - MAXPLAYERS * (height + CleanYfac + 1)) / 2); - HU_DrawTimeRemaining (bottom - height); + HU_DrawTimeRemaining(bottom - height); if (teamplay && deathmatch) { y -= (BigFont->GetHeight() + 8) * CleanYfac; - for (i = 0; i < Teams.Size (); i++) + for (size_t i = 0u; i < Teams.Size(); ++i) { Teams[i].m_iPlayerCount = 0; Teams[i].m_iScore = 0; } - for (i = 0; i < MAXPLAYERS; ++i) + int numTeams = 0; + for (int i = 0; i < MAXPLAYERS; ++i) { - if (playeringame[sortedplayers[i]-players] && FTeam::IsValid (sortedplayers[i]->userinfo.GetTeam())) + if (playeringame[sortedPlayers[i]-players] && FTeam::IsValid(sortedPlayers[i]->userinfo.GetTeam())) { - if (Teams[sortedplayers[i]->userinfo.GetTeam()].m_iPlayerCount++ == 0) - { - numTeams++; - } + if (Teams[sortedPlayers[i]->userinfo.GetTeam()].m_iPlayerCount++ == 0) + ++numTeams; - Teams[sortedplayers[i]->userinfo.GetTeam()].m_iScore += sortedplayers[i]->fragcount; + Teams[sortedPlayers[i]->userinfo.GetTeam()].m_iScore += sortedPlayers[i]->fragcount; } } - int scorexwidth = twod->GetWidth() / max(8, numTeams); - int numscores = 0; - int scorex; - - for (i = 0; i < Teams.Size(); ++i) + int scoreXWidth = twod->GetWidth() / max(8, numTeams); + int numScores = 0; + for (size_t i = 0u; i < Teams.Size(); ++i) { if (Teams[i].m_iPlayerCount) - { - numscores++; - } + ++numScores; } - scorex = (twod->GetWidth() - scorexwidth * (numscores - 1)) / 2; - - for (i = 0; i < Teams.Size(); ++i) + int scoreX = (twod->GetWidth() - scoreXWidth * (numScores - 1)) / 2; + for (size_t i = 0u; i < Teams.Size(); ++i) { - if (Teams[i].m_iPlayerCount) - { - char score[80]; - mysnprintf (score, countof(score), "%d", Teams[i].m_iScore); + if (!Teams[i].m_iPlayerCount) + continue; - DrawText(twod, BigFont, Teams[i].GetTextColor(), - scorex - BigFont->StringWidth(score)*CleanXfac/2, y, score, - DTA_CleanNoMove, true, TAG_DONE); + char score[80]; + mysnprintf(score, countof(score), "%d", Teams[i].m_iScore); - scorex += scorexwidth; - } + DrawText(twod, BigFont, Teams[i].GetTextColor(), + scoreX - BigFont->StringWidth(score)*CleanXfac/2, y, score, + DTA_CleanNoMove, true, TAG_DONE); + + scoreX += scoreXWidth; } y += (BigFont->GetHeight() + 8) * CleanYfac; } - const char *text_color = GStrings.GetString("SCORE_COLOR"), + const char* text_color = GStrings.GetString("SCORE_COLOR"), *text_frags = GStrings.GetString(deathmatch ? "SCORE_FRAGS" : "SCORE_KILLS"), *text_name = GStrings.GetString("SCORE_NAME"), *text_delay = GStrings.GetString("SCORE_DELAY"); - col2 = (displayFont->StringWidth(text_color) + 16) * FontScale; - col3 = col2 + (displayFont->StringWidth(text_frags) + 16) * FontScale; - col4 = col3 + maxscorewidth * FontScale; - col5 = col4 + (maxnamewidth + 16) * FontScale; - x = (twod->GetWidth() >> 1) - (((displayFont->StringWidth(text_delay) * FontScale) + col5) >> 1); + int col2 = (displayFont->StringWidth(text_color) + 16) * FontScale; + int col3 = col2 + (displayFont->StringWidth(text_frags) + 16) * FontScale; + int col4 = col3 + maxScoreWidth * FontScale; + int col5 = col4 + (maxNameWidth + 16) * FontScale; + int x = (twod->GetWidth() >> 1) - (((displayFont->StringWidth(text_delay) * FontScale) + col5) >> 1); //HU_DrawFontScaled(x, y, color, text_color); HU_DrawFontScaled(x + col2, y, color, text_frags); @@ -355,13 +339,13 @@ static void HU_DoDrawScores (player_t *player, player_t *sortedplayers[MAXPLAYER y += height + 6 * CleanYfac; bottom -= height; - for (i = 0; i < MAXPLAYERS && y <= bottom; i++) + for (int i = 0; i < MAXPLAYERS && y <= bottom; ++i) { - if (playeringame[sortedplayers[i] - players]) - { - HU_DrawPlayer(sortedplayers[i], player == sortedplayers[i], x, col2, col3, col4, col5, maxnamewidth, y, ypadding, lineheight); - y += lineheight + CleanYfac; - } + if (!playeringame[sortedPlayers[i] - players]) + continue; + + HU_DrawPlayer(sortedPlayers[i], player == sortedPlayers[i], x, col2, col3, col4, col5, maxNameWidth, y, yPadding, lineHeight); + y += lineHeight + CleanYfac; } } @@ -437,14 +421,7 @@ static void HU_DrawPlayer (player_t *player, bool highlight, int col1, int col2, HU_DrawFontScaled(col4, y + ypadding, color, player->userinfo.GetName()); - int avgdelay = 0; - for (int i = 0; i < BACKUPTICS; i++) - { - avgdelay += netdelay[nodeforplayer[(int)(player - players)]][i]; - } - avgdelay /= BACKUPTICS; - - mysnprintf(str, countof(str), "%d", (avgdelay * ticdup) * (1000 / TICRATE)); + mysnprintf(str, countof(str), "%u", ClientStates[player - players].AverageLatency); HU_DrawFontScaled(col5, y + ypadding, color, str); diff --git a/src/hu_stuff.h b/src/hu_stuff.h index 5c6fe4b02..01cf429cc 100644 --- a/src/hu_stuff.h +++ b/src/hu_stuff.h @@ -42,8 +42,8 @@ void CT_Drawer (void); // [RH] Draw deathmatch scores -void HU_DrawScores (player_t *me); -void HU_GetPlayerWidths(int &maxnamewidth, int &maxscorewidth, int &maxiconheight); +void HU_DrawScores(player_t* me); +void HU_GetPlayerWidths(int& maxNameWidth, int& maxScoreWidth, int& maxIconHeight); void HU_DrawColorBar(int x, int y, int height, int playernum); int HU_GetRowColor(player_t *player, bool hightlight); diff --git a/src/p_tick.cpp b/src/p_tick.cpp index 59d66a285..efe35f412 100644 --- a/src/p_tick.cpp +++ b/src/p_tick.cpp @@ -72,6 +72,41 @@ bool P_CheckTickerPaused () return false; } +void P_ClearLevelInterpolation() +{ + for (auto Level : AllLevels()) + { + Level->interpolator.UpdateInterpolations(); + + auto it = Level->GetThinkerIterator(); + AActor* ac; + + while ((ac = it.Next())) + { + ac->ClearInterpolation(); + ac->ClearFOVInterpolation(); + } + } + + for (int i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i]) + { + DPSprite* pspr = players[i].psprites; + while (pspr) + { + pspr->ResetInterpolation(); + + pspr = pspr->Next; + } + } + } + + R_ClearInterpolationPath(); + if (StatusBar != nullptr) + StatusBar->ClearInterpolation(); +} + // // P_Ticker // diff --git a/src/playsim/bots/b_bot.h b/src/playsim/bots/b_bot.h index 2f214bbd9..9e9cadeb8 100644 --- a/src/playsim/bots/b_bot.h +++ b/src/playsim/bots/b_bot.h @@ -130,12 +130,12 @@ public: void FinishTravel (); bool IsLeader (player_t *player); void SetBodyAt (FLevelLocals *Level, const DVector3 &pos, int hostnum); - double FakeFire (AActor *source, AActor *dest, ticcmd_t *cmd); + double FakeFire (AActor *source, AActor *dest, usercmd_t *cmd); bool SafeCheckPosition (AActor *actor, double x, double y, FCheckPosition &tm); void BotTick(AActor *mo); //(b_move.cpp) - bool CleanAhead (AActor *thing, double x, double y, ticcmd_t *cmd); + bool CleanAhead (AActor *thing, double x, double y, usercmd_t *cmd); bool IsDangerous (sector_t *sec); TArray getspawned; //Array of bots (their names) which should be spawned when starting a game. @@ -216,21 +216,21 @@ public: private: //(b_think.cpp) void Think (); - void ThinkForMove (ticcmd_t *cmd); + void ThinkForMove (usercmd_t *cmd); void Set_enemy (); //(b_func.cpp) bool Reachable (AActor *target); - void Dofire (ticcmd_t *cmd); + void Dofire (usercmd_t *cmd); AActor *Choose_Mate (); AActor *Find_enemy (); - DAngle FireRox (AActor *enemy, ticcmd_t *cmd); + DAngle FireRox (AActor *enemy, usercmd_t *cmd); //(b_move.cpp) - void Roam (ticcmd_t *cmd); - bool Move (ticcmd_t *cmd); - bool TryWalk (ticcmd_t *cmd); - void NewChaseDir (ticcmd_t *cmd); + void Roam (usercmd_t *cmd); + bool Move (usercmd_t *cmd); + bool TryWalk (usercmd_t *cmd); + void NewChaseDir (usercmd_t *cmd); void TurnToAng (); void Pitch (AActor *target); }; diff --git a/src/playsim/bots/b_func.cpp b/src/playsim/bots/b_func.cpp index e537bf9f7..711553fdb 100644 --- a/src/playsim/bots/b_func.cpp +++ b/src/playsim/bots/b_func.cpp @@ -164,7 +164,7 @@ bool DBot::Check_LOS (AActor *to, DAngle vangle) //------------------------------------- //The bot will check if it's time to fire //and do so if that is the case. -void DBot::Dofire (ticcmd_t *cmd) +void DBot::Dofire (usercmd_t *cmd) { bool no_fire; //used to prevent bot from pumping rockets into nearby walls. int aiming_penalty=0; //For shooting at shading target, if screen is red, MAKEME: When screen red. @@ -276,7 +276,7 @@ void DBot::Dofire (ticcmd_t *cmd) } if (!no_fire) //If going to fire weapon { - cmd->ucmd.buttons |= BT_ATTACK; + cmd->buttons |= BT_ATTACK; } //Prevents bot from jerking, when firing automatic things with low skill. } @@ -503,7 +503,7 @@ void FCajunMaster::SetBodyAt (FLevelLocals *Level, const DVector3 &pos, int host //Emulates missile travel. Returns distance travelled. -double FCajunMaster::FakeFire (AActor *source, AActor *dest, ticcmd_t *cmd) +double FCajunMaster::FakeFire (AActor *source, AActor *dest, usercmd_t *cmd) { AActor *th = Spawn (source->Level, "CajunTrace", source->PosPlusZ(4*8.), NO_REPLACE); @@ -525,7 +525,7 @@ double FCajunMaster::FakeFire (AActor *source, AActor *dest, ticcmd_t *cmd) return dist; } -DAngle DBot::FireRox (AActor *enemy, ticcmd_t *cmd) +DAngle DBot::FireRox (AActor *enemy, usercmd_t *cmd) { double dist; AActor *actor; diff --git a/src/playsim/bots/b_move.cpp b/src/playsim/bots/b_move.cpp index 4d91839d8..87c21a5ae 100644 --- a/src/playsim/bots/b_move.cpp +++ b/src/playsim/bots/b_move.cpp @@ -63,7 +63,7 @@ extern dirtype_t diags[4]; //Called while the bot moves after its dest mobj //which can be a weapon/enemy/item whatever. -void DBot::Roam (ticcmd_t *cmd) +void DBot::Roam (usercmd_t *cmd) { if (Reachable(dest)) @@ -89,7 +89,7 @@ void DBot::Roam (ticcmd_t *cmd) } } -bool DBot::Move (ticcmd_t *cmd) +bool DBot::Move (usercmd_t *cmd) { double tryx, tryy; bool try_ok; @@ -136,20 +136,20 @@ bool DBot::Move (ticcmd_t *cmd) } if (good && ((pr_botopendoor() >= 203) ^ (good & 1))) { - cmd->ucmd.buttons |= BT_USE; - cmd->ucmd.forwardmove = FORWARDRUN; + cmd->buttons |= BT_USE; + cmd->forwardmove = FORWARDRUN; return true; } else return false; } else //Move forward. - cmd->ucmd.forwardmove = FORWARDRUN; + cmd->forwardmove = FORWARDRUN; return true; } -bool DBot::TryWalk (ticcmd_t *cmd) +bool DBot::TryWalk (usercmd_t *cmd) { if (!Move (cmd)) return false; @@ -158,7 +158,7 @@ bool DBot::TryWalk (ticcmd_t *cmd) return true; } -void DBot::NewChaseDir (ticcmd_t *cmd) +void DBot::NewChaseDir (usercmd_t *cmd) { dirtype_t d[3]; @@ -290,7 +290,7 @@ void DBot::NewChaseDir (ticcmd_t *cmd) // This is also a traverse function for // bots pre-rocket fire (preventing suicide) // -bool FCajunMaster::CleanAhead (AActor *thing, double x, double y, ticcmd_t *cmd) +bool FCajunMaster::CleanAhead (AActor *thing, double x, double y, usercmd_t *cmd) { FCheckPosition tm; @@ -310,7 +310,7 @@ bool FCajunMaster::CleanAhead (AActor *thing, double x, double y, ticcmd_t *cmd) //Jumpable if(tm.floorz > (thing->Sector->floorplane.ZatPoint(x, y)+thing->MaxStepHeight)) - cmd->ucmd.buttons |= BT_JUMP; + cmd->buttons |= BT_JUMP; if ( !(thing->flags & MF_TELEPORT) && diff --git a/src/playsim/bots/b_think.cpp b/src/playsim/bots/b_think.cpp index 9fba192d5..405d2719b 100644 --- a/src/playsim/bots/b_think.cpp +++ b/src/playsim/bots/b_think.cpp @@ -58,7 +58,7 @@ static FRandom pr_botmove ("BotMove"); //so this is what the bot does. void DBot::Think () { - ticcmd_t *cmd = &netcmds[player - players][((gametic + 1)/ticdup)%BACKUPTICS]; + usercmd_t *cmd = &player->cmd; memset (cmd, 0, sizeof(*cmd)); @@ -78,13 +78,12 @@ void DBot::Think () ThinkForMove (cmd); TurnToAng (); - cmd->ucmd.yaw = (short)((actor->Angles.Yaw - oldyaw).Degrees() * (65536 / 360.f)) / ticdup; - cmd->ucmd.pitch = (short)((oldpitch - actor->Angles.Pitch).Degrees() * (65536 / 360.f)); - if (cmd->ucmd.pitch == -32768) - cmd->ucmd.pitch = -32767; - cmd->ucmd.pitch /= ticdup; - actor->Angles.Yaw = oldyaw + DAngle::fromDeg(cmd->ucmd.yaw * ticdup * (360 / 65536.f)); - actor->Angles.Pitch = oldpitch - DAngle::fromDeg(cmd->ucmd.pitch * ticdup * (360 / 65536.f)); + cmd->yaw = (short)((actor->Angles.Yaw - oldyaw).Degrees() * (65536 / 360.f)); + cmd->pitch = (short)((oldpitch - actor->Angles.Pitch).Degrees() * (65536 / 360.f)); + if (cmd->pitch == -32768) + cmd->pitch = -32767; + actor->Angles.Yaw = oldyaw + DAngle::fromDeg(cmd->yaw * (360 / 65536.f)); + actor->Angles.Pitch = oldpitch - DAngle::fromDeg(cmd->pitch * (360 / 65536.f)); } if (t_active) t_active--; @@ -101,14 +100,14 @@ void DBot::Think () } else if (player->mo->health <= 0) { // Time to respawn - cmd->ucmd.buttons |= BT_USE; + cmd->buttons |= BT_USE; } } #define THINKDISTSQ (50000.*50000./(65536.*65536.)) //how the bot moves. //MAIN movement function. -void DBot::ThinkForMove (ticcmd_t *cmd) +void DBot::ThinkForMove (usercmd_t *cmd) { double dist; bool stuck; @@ -136,8 +135,8 @@ void DBot::ThinkForMove (ticcmd_t *cmd) { Pitch (missile); Angle = player->mo->AngleTo(missile); - cmd->ucmd.sidemove = sleft ? -SIDERUN : SIDERUN; - cmd->ucmd.forwardmove = -FORWARDRUN; //Back IS best. + cmd->sidemove = sleft ? -SIDERUN : SIDERUN; + cmd->forwardmove = -FORWARDRUN; //Back IS best. if ((player->mo->Pos() - old).LengthSquared() < THINKDISTSQ && t_strafe<=0) @@ -208,22 +207,22 @@ void DBot::ThinkForMove (ticcmd_t *cmd) GetBotInfo(player->ReadyWeapon).MoveCombatDist) { // If a monster, use lower speed (just for cooler apperance while strafing down doomed monster) - cmd->ucmd.forwardmove = (enemy->flags3 & MF3_ISMONSTER) ? FORWARDWALK : FORWARDRUN; + cmd->forwardmove = (enemy->flags3 & MF3_ISMONSTER) ? FORWARDWALK : FORWARDRUN; } else if (!stuck) //Too close, so move away. { // If a monster, use lower speed (just for cooler apperance while strafing down doomed monster) - cmd->ucmd.forwardmove = (enemy->flags3 & MF3_ISMONSTER) ? -FORWARDWALK : -FORWARDRUN; + cmd->forwardmove = (enemy->flags3 & MF3_ISMONSTER) ? -FORWARDWALK : -FORWARDRUN; } //Strafing. if (enemy->flags3 & MF3_ISMONSTER) //It's just a monster so take it down cool. { - cmd->ucmd.sidemove = sleft ? -SIDEWALK : SIDEWALK; + cmd->sidemove = sleft ? -SIDEWALK : SIDEWALK; } else { - cmd->ucmd.sidemove = sleft ? -SIDERUN : SIDERUN; + cmd->sidemove = sleft ? -SIDERUN : SIDERUN; } Dofire (cmd); //Order bot to fire current weapon } @@ -246,11 +245,11 @@ void DBot::ThinkForMove (ticcmd_t *cmd) matedist = player->mo->Distance2D(mate); if (matedist > (FRIEND_DIST*2)) - cmd->ucmd.forwardmove = FORWARDRUN; + cmd->forwardmove = FORWARDRUN; else if (matedist > FRIEND_DIST) - cmd->ucmd.forwardmove = FORWARDWALK; //Walk, when starting to get close. + cmd->forwardmove = FORWARDWALK; //Walk, when starting to get close. else if (matedist < FRIEND_DIST-(FRIEND_DIST/3)) //Got too close, so move away. - cmd->ucmd.forwardmove = -FORWARDWALK; + cmd->forwardmove = -FORWARDWALK; } else //Roam after something. { diff --git a/src/playsim/d_player.h b/src/playsim/d_player.h index 862890f07..025355d95 100644 --- a/src/playsim/d_player.h +++ b/src/playsim/d_player.h @@ -321,7 +321,7 @@ public: AActor *mo = nullptr; uint8_t playerstate = 0; - ticcmd_t cmd = {}; + usercmd_t cmd = {}; usercmd_t original_cmd = {}; uint32_t original_oldbuttons = 0; diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 6eb98c0ba..216d13db7 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -2572,7 +2572,7 @@ static double P_XYMovement (AActor *mo, DVector2 scroll) { // slide against wall if (BlockingLine != NULL && mo->player && mo->waterlevel && mo->waterlevel < 3 && - (mo->player->cmd.ucmd.forwardmove | mo->player->cmd.ucmd.sidemove) && + (mo->player->cmd.forwardmove | mo->player->cmd.sidemove) && mo->BlockingLine->sidedef[1] != NULL) { double spd = mo->FloatVar(NAME_WaterClimbSpeed); @@ -2811,7 +2811,7 @@ static double P_XYMovement (AActor *mo, DVector2 scroll) // moving corresponding player: if (fabs(mo->Vel.X) < STOPSPEED && fabs(mo->Vel.Y) < STOPSPEED && (!player || (player->mo != mo) - || !(player->cmd.ucmd.forwardmove | player->cmd.ucmd.sidemove))) + || !(player->cmd.forwardmove | player->cmd.sidemove))) { // if in a walking frame, stop moving // killough 10/98: @@ -3273,7 +3273,7 @@ void AActor::FallAndSink(double grav, double oldfloorz) double startvelz = Vel.Z; if (waterlevel == 0 || (player && - !(player->cmd.ucmd.forwardmove | player->cmd.ucmd.sidemove))) + !(player->cmd.forwardmove | player->cmd.sidemove))) { // [RH] Double gravity only if running off a ledge. Coming down from // an upward thrust (e.g. a jump) should not double it. @@ -6101,7 +6101,7 @@ AActor *FLevelLocals::SpawnPlayer (FPlayerStart *mthing, int playernum, int flag p->cheats = CF_CHASECAM; // setup gun psprite - if (!(flags & SPF_TEMPPLAYER)) + if (!(flags & SPF_TEMPPLAYER) || oldactor == nullptr) { // This can also start a script so don't do it for the dummy player. P_SetupPsprites (p, !!(flags & SPF_WEAPONFULLYUP)); } @@ -6156,7 +6156,7 @@ AActor *FLevelLocals::SpawnPlayer (FPlayerStart *mthing, int playernum, int flag } // [BC] Do script stuff - if (!(flags & SPF_TEMPPLAYER)) + if (!(flags & SPF_TEMPPLAYER) || oldactor == nullptr) { if (state == PST_ENTER || (state == PST_LIVE && !savegamerestore)) { diff --git a/src/playsim/p_pspr.cpp b/src/playsim/p_pspr.cpp index 923923420..df09b58c3 100644 --- a/src/playsim/p_pspr.cpp +++ b/src/playsim/p_pspr.cpp @@ -715,7 +715,7 @@ static void P_CheckWeaponButtons (player_t *player) for (size_t i = 0; i < countof(ButtonChecks); ++i) { if ((player->WeaponState & ButtonChecks[i].StateFlag) && - (player->cmd.ucmd.buttons & ButtonChecks[i].ButtonFlag)) + (player->cmd.buttons & ButtonChecks[i].ButtonFlag)) { FState *state = weapon->FindState(ButtonChecks[i].StateName); // [XA] don't change state if still null, so if the modder diff --git a/src/playsim/p_things.cpp b/src/playsim/p_things.cpp index 3391030aa..462c758c2 100644 --- a/src/playsim/p_things.cpp +++ b/src/playsim/p_things.cpp @@ -561,13 +561,13 @@ int P_Thing_CheckInputNum(player_t *p, int inputnum) case INPUT_UPMOVE: renum = p->original_cmd.upmove; break; case MODINPUT_OLDBUTTONS: renum = p->oldbuttons; break; - case MODINPUT_BUTTONS: renum = p->cmd.ucmd.buttons; break; - case MODINPUT_PITCH: renum = p->cmd.ucmd.pitch; break; - case MODINPUT_YAW: renum = p->cmd.ucmd.yaw; break; - case MODINPUT_ROLL: renum = p->cmd.ucmd.roll; break; - case MODINPUT_FORWARDMOVE: renum = p->cmd.ucmd.forwardmove; break; - case MODINPUT_SIDEMOVE: renum = p->cmd.ucmd.sidemove; break; - case MODINPUT_UPMOVE: renum = p->cmd.ucmd.upmove; break; + case MODINPUT_BUTTONS: renum = p->cmd.buttons; break; + case MODINPUT_PITCH: renum = p->cmd.pitch; break; + case MODINPUT_YAW: renum = p->cmd.yaw; break; + case MODINPUT_ROLL: renum = p->cmd.roll; break; + case MODINPUT_FORWARDMOVE: renum = p->cmd.forwardmove; break; + case MODINPUT_SIDEMOVE: renum = p->cmd.sidemove; break; + case MODINPUT_UPMOVE: renum = p->cmd.upmove; break; default: renum = 0; break; } diff --git a/src/playsim/p_user.cpp b/src/playsim/p_user.cpp index 65f892903..61fd17cee 100644 --- a/src/playsim/p_user.cpp +++ b/src/playsim/p_user.cpp @@ -95,6 +95,8 @@ #include "s_music.h" #include "d_main.h" +extern int paused; + static FRandom pr_skullpop ("SkullPop"); // [SP] Allows respawn in single player @@ -1236,7 +1238,7 @@ DEFINE_ACTION_FUNCTION(APlayerPawn, CheckEnvironment) void P_CheckUse(player_t *player) { // check for use - if (player->cmd.ucmd.buttons & BT_USE) + if (player->cmd.buttons & BT_USE) { if (!player->usedown) { @@ -1269,7 +1271,7 @@ DEFINE_ACTION_FUNCTION(APlayerPawn, CheckUse) void P_PlayerThink (player_t *player) { - ticcmd_t *cmd = &player->cmd; + usercmd_t *cmd = &player->cmd; if (player->mo == NULL) { @@ -1309,14 +1311,14 @@ void P_PlayerThink (player_t *player) { fprintf (debugfile, "tic %d for pl %d: (%f, %f, %f, %f) b:%02x p:%d y:%d f:%d s:%d u:%d\n", gametic, (int)(player-players), player->mo->X(), player->mo->Y(), player->mo->Z(), - player->mo->Angles.Yaw.Degrees(), player->cmd.ucmd.buttons, - player->cmd.ucmd.pitch, player->cmd.ucmd.yaw, player->cmd.ucmd.forwardmove, - player->cmd.ucmd.sidemove, player->cmd.ucmd.upmove); + player->mo->Angles.Yaw.Degrees(), player->cmd.buttons, + player->cmd.pitch, player->cmd.yaw, player->cmd.forwardmove, + player->cmd.sidemove, player->cmd.upmove); } // Make unmodified copies for ACS's GetPlayerInput. player->original_oldbuttons = player->original_cmd.buttons; - player->original_cmd = cmd->ucmd; + player->original_cmd = *cmd; // Don't interpolate the view for more than one tic player->cheats &= ~CF_INTERPVIEW; player->cheats &= ~CF_INTERPVIEWANGLES; @@ -1453,8 +1455,7 @@ void P_PredictPlayer (player_t *player) { int maxtic; - if (singletics || - demoplayback || + if (demoplayback || player->mo == NULL || player != player->mo->Level->GetConsolePlayer() || player->playerstate != PST_LIVE || @@ -1465,7 +1466,7 @@ void P_PredictPlayer (player_t *player) return; } - maxtic = maketic; + maxtic = ClientTic; if (gametic == maxtic) { @@ -1557,8 +1558,11 @@ void P_PredictPlayer (player_t *player) } } - player->oldbuttons = player->cmd.ucmd.buttons; - player->cmd = localcmds[i % LOCALCMDTICS]; + player->oldbuttons = player->cmd.buttons; + player->cmd = LocalCmds[i % LOCALCMDTICS]; + if (paused) + continue; + player->mo->ClearInterpolation(); player->mo->ClearFOVInterpolation(); P_PlayerThink(player); @@ -1591,6 +1595,10 @@ void P_PredictPlayer (player_t *player) player->viewz = snapPos.Z + zOfs; } } + else if (paused) + { + r_NoInterpolate = true; + } // This is intentionally done after rubberbanding starts since it'll automatically smooth itself towards // the right spot until it reaches it. @@ -1900,11 +1908,11 @@ DEFINE_FIELD_X(PlayerInfo, player_t, ConversationNPC) DEFINE_FIELD_X(PlayerInfo, player_t, ConversationPC) DEFINE_FIELD_X(PlayerInfo, player_t, ConversationNPCAngle) DEFINE_FIELD_X(PlayerInfo, player_t, ConversationFaceTalker) -DEFINE_FIELD_NAMED_X(PlayerInfo, player_t, cmd.ucmd, cmd) +DEFINE_FIELD_NAMED_X(PlayerInfo, player_t, cmd, cmd) DEFINE_FIELD_X(PlayerInfo, player_t, original_cmd) DEFINE_FIELD_X(PlayerInfo, player_t, userinfo) DEFINE_FIELD_X(PlayerInfo, player_t, weapons) -DEFINE_FIELD_NAMED_X(PlayerInfo, player_t, cmd.ucmd.buttons, buttons) +DEFINE_FIELD_NAMED_X(PlayerInfo, player_t, cmd.buttons, buttons) DEFINE_FIELD_X(PlayerInfo, player_t, SoundClass) DEFINE_FIELD_X(UserCmd, usercmd_t, buttons) diff --git a/src/serializer_doom.h b/src/serializer_doom.h index 1ce88c0e0..bc87df74a 100644 --- a/src/serializer_doom.h +++ b/src/serializer_doom.h @@ -8,7 +8,6 @@ struct sector_t; struct line_t; struct side_t; struct vertex_t; -struct ticcmd_t; struct usercmd_t; class PClassActor; struct FStrifeDialogueNode; @@ -40,7 +39,6 @@ FSerializer &SerializeArgs(FSerializer &arc, const char *key, int *args, int *de FSerializer &SerializeTerrain(FSerializer &arc, const char *key, int &terrain, int *def = nullptr); FSerializer& Serialize(FSerializer& arc, const char* key, char& value, char* defval); -FSerializer &Serialize(FSerializer &arc, const char *key, ticcmd_t &sid, ticcmd_t *def); FSerializer &Serialize(FSerializer &arc, const char *key, usercmd_t &cmd, usercmd_t *def); FSerializer &Serialize(FSerializer &arc, const char *key, FInterpolator &rs, FInterpolator *def); FSerializer& Serialize(FSerializer& arc, const char* key, struct FStandaloneAnimation& value, struct FStandaloneAnimation* defval); diff --git a/src/version.h b/src/version.h index 7a04c37dc..91f3319b5 100644 --- a/src/version.h +++ b/src/version.h @@ -57,11 +57,6 @@ const char *GetVersionString(); #define ENG_MINOR 15 #define ENG_REVISION 1 -// Version identifier for network games. -// Bump it every time you do a release unless you're certain you -// didn't change anything that will affect sync. -#define NETGAMEVERSION 235 - // Version stored in the ini's [LastRun] section. // Bump it if you made some configuration change that you want to // be able to migrate in FGameConfigFile::DoGlobalSetup(). diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 759cc900d..05a83d98d 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -654,6 +654,8 @@ OptionMenu "OtherControlsMenu" protected StaticText "" Control "$CNTRLMNU_CHASECAM" , "chase" Control "$CNTRLMNU_COOPSPY" , "spynext" + Control "$CNTRLMNU_COOPSPYPREV" , "spyprev" + Control "$CNTRLMNU_COOPSPYCANCEL" , "spycancel" StaticText "" Control "$CNTRLMNU_SCREENSHOT" , "screenshot" @@ -2378,21 +2380,25 @@ OptionMenu NetworkOptions protected { Title "$NETMNU_TITLE" StaticText "$NETMNU_LOCALOPTIONS", 1 + Option "$NETMNU_CHATTYPE", "cl_showchat", "ChatTypes" + Option "$NETMNU_NOBOLDCHAT", "cl_noboldchat", "OnOff" + Option "$NETMNU_NOCHATSND", "cl_nochatsound", "OnOff" Option "$NETMNU_LINESPECIALPREDICTION", "cl_predict_specials", "OnOff" Slider "$NETMNU_PREDICTIONLERPSCALE", "cl_rubberband_scale", 0.0, 1.0, 0.05, 2 Slider "$NETMNU_LERPTHRESHOLD", "cl_rubberband_threshold", 0.0, 256.0, 8.0 StaticText " " StaticText "$NETMNU_HOSTOPTIONS", 1 - Option "$NETMNU_EXTRATICS", "net_extratic", "ExtraTicMode" - Option "$NETMNU_TICBALANCE", "net_ticbalance", "OnOff" + Option "$NETMNU_EXTRATICS", "net_extratic", "OnOff" + Option "$NETMNU_DISABLEPAUSE", "net_disablepause", "OnOff" + NumberField "$NETMNU_CHATSLOMO", "net_chatslowmode", 0, 300, 5 } -OptionValue ExtraTicMode +OptionValue "ChatTypes" { - 0, "$OPTVAL_NONE" - 1, "1" - 2, "$OPTVAL_ALLUNACKNOWLEDGED" + 0, "$OPTVAL_DISABLECHAT" + 1, "$OPTVAL_TEAMONLYCHAT" + 2, "$OPTVAL_GLOBALCHAT" } OptionValue "LookupOrder" diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs index 22454dc65..c91410b0b 100644 --- a/wadsrc/static/zscript/actors/player/player.zs +++ b/wadsrc/static/zscript/actors/player/player.zs @@ -18,6 +18,7 @@ class PlayerPawn : Actor // 16 pixels of bob const MAXBOB = 16.; + int BobTimer; // Use a local timer for this so it can be predicted correctly. int crouchsprite; int MaxHealth; int BonusHealth; @@ -633,7 +634,7 @@ class PlayerPawn : Actor { if (player.health > 0) { - angle = Level.maptime / (120 * TICRATE / 35.) * 360.; + angle = BobTimer / (120 * TICRATE / 35.) * 360.; bob = player.GetStillBob() * sin(angle); } else @@ -643,7 +644,7 @@ class PlayerPawn : Actor } else { - angle = Level.maptime / (ViewBobSpeed * TICRATE / 35.) * 360.; + angle = BobTimer / (ViewBobSpeed * TICRATE / 35.) * 360.; bob = player.bob * sin(angle) * (waterlevel > 1 ? 0.25f : 0.5f); } @@ -1667,6 +1668,7 @@ class PlayerPawn : Actor PlayerFlags |= PF_VOODOO_ZOMBIE; } + ++BobTimer; CheckFOV(); if (player.inventorytics) @@ -2545,7 +2547,7 @@ class PlayerPawn : Actor for (int i = 0; i < 2; i++) { // Bob the weapon based on movement speed. ([SP] And user's bob speed setting) - double angle = (BobSpeed * player.GetWBobSpeed() * 35 / TICRATE*(Level.maptime - 1 + i)) * (360. / 8192.); + double angle = (BobSpeed * player.GetWBobSpeed() * 35 / TICRATE*(BobTimer - 1 + i)) * (360. / 8192.); // [RH] Smooth transitions between bobbing and not-bobbing frames. // This also fixes the bug where you can "stick" a weapon off-center by diff --git a/wadsrc/static/zscript/constants.zs b/wadsrc/static/zscript/constants.zs index cd3eceb15..c73ed76ab 100644 --- a/wadsrc/static/zscript/constants.zs +++ b/wadsrc/static/zscript/constants.zs @@ -1,6 +1,6 @@ // for flag changer functions. const FLAG_NO_CHANGE = -1; -const MAXPLAYERS = 8; +const MAXPLAYERS = 16; enum EStateUseFlags {