//----------------------------------------------------------------------------- // // Copyright 1993-1996 id Software // Copyright 1999-2016 Randy Heit // Copyright 2002-2016 Christoph Oelckers // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see http://www.gnu.org/licenses/ // //----------------------------------------------------------------------------- // // DESCRIPTION: // DOOM Network game communication and protocol, // all OS independent parts. // //----------------------------------------------------------------------------- #include #define __STDC_FORMAT_MACROS #include #include "version.h" #include "menu.h" #include "i_video.h" #include "i_net.h" #include "g_game.h" #include "c_console.h" #include "d_netinf.h" #include "d_net.h" #include "cmdlib.h" #include "m_cheat.h" #include "p_local.h" #include "c_dispatch.h" #include "sbar.h" #include "gi.h" #include "m_misc.h" #include "gameconfigfile.h" #include "p_acs.h" #include "p_trace.h" #include "a_sharedglobal.h" #include "st_start.h" #include "teaminfo.h" #include "p_conversation.h" #include "d_eventbase.h" #include "p_enemy.h" #include "m_argv.h" #include "p_lnspec.h" #include "p_spec.h" #include "hardware.h" #include "r_utility.h" #include "a_keys.h" #include "intermission/intermission.h" #include "g_levellocals.h" #include "actorinlines.h" #include "events.h" #include "i_time.h" #include "i_system.h" #include "vm.h" #include "gstrings.h" #include "s_music.h" #include "screenjob.h" #include "d_main.h" #include "i_interface.h" #include "savegamemanager.h" void P_RunClientsideLogic(); EXTERN_CVAR (Int, disableautosave) EXTERN_CVAR (Int, autosavecount) EXTERN_CVAR (Bool, cl_capfps) EXTERN_CVAR (Bool, vid_vsync) EXTERN_CVAR (Int, vid_maxfps) extern uint8_t *demo_p; // [RH] Special "ticcmds" get recorded in demos extern FString savedescription; extern FString savegamefile; extern bool AppActive; void P_ClearLevelInterpolation(); enum ELevelStartStatus { LST_READY, LST_HOST, LST_WAITING, }; enum EReadyType { RT_VOTE, RT_ANYONE, RT_HOST_ONLY, }; // NETWORKING // // 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 world tick cannot be ran until CurrentSequence >= gametic for all clients. 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] = {}; // Try and stabilize uneven connections by checking for spikes in available // sequences. If they're found, try and average out a buffer to prioritize // making the experience smoother over very stop and go heavy. static int StabilityBuffer = 0; static int PrevAvailableDiff = 0; static size_t CurStabilityTic = 0u; static int StabilityTics[STABILITYTICS] = {}; // 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] = {}; 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 uint64_t MutedClients = 0u; // Ignore messages from these clients. static int CutsceneCountdown = 0; // If enough people are ready, count down the timer. This won't reset between unreadies, only on intermission entrance. static uint64_t CutsceneReady = 0u; // If in a cutscene, check if we're ready to move to move past it. 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 uint64_t LevelStartAck = 0u; // Used by the host to determine if everyone has loaded in. 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. 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 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 RunScript(TArrayView& stream, AActor *pawn, int snum, int argn, int always); 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, true, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, net_extratic, false, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, net_limitsaves, true, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, net_repeatableactioncooldown, true, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, net_limitconversations, false, CVAR_SERVERINFO | CVAR_NOSAVE) CUSTOM_CVAR(Int, net_disablepause, 0, CVAR_SERVERINFO | CVAR_NOSAVE) { if (self < 0) self = 0; else if (self > 2) self = 2; } CUSTOM_CVAR(Int, net_cutscenereadytype, RT_VOTE, CVAR_SERVERINFO | CVAR_NOSAVE) { if (self < RT_VOTE) self = RT_VOTE; else if (self > RT_HOST_ONLY) self = RT_HOST_ONLY; } CUSTOM_CVAR(Float, net_cutscenereadypercent, 0.5f, CVAR_SERVERINFO | CVAR_NOSAVE) { if (self < 0.0f) self = 0.0f; else if (self > 1.0f) self = 1.0f; } CVAR(Float, net_cutscenecountdown, 30.0f, 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 < CHAT_DISABLED) self = CHAT_DISABLED; else if (self > CHAT_GLOBAL) self = CHAT_GLOBAL; } CUSTOM_CVAR(Int, cl_debugprediction, 0, CVAR_CHEAT) { if (self < 0) self = 0; else if (self > BACKUPTICS - 1) self = BACKUPTICS - 1; } // Used to write out all network events that occured leading up to the next tick. static struct NetEventData { struct FStream { uint8_t* Stream; size_t Used = 0; FStream() { Grow(256); } ~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) stream.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 / TicDup; CurrentStream = Streams[CurrentClientTic % BACKUPTICS].Stream; CurrentSize = 0; } void NewClientTic() { const int tic = ClientTic / 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); UncheckedWriteInt8(it, &CurrentStream); } return *this; } NetEventData& operator<<(int16_t it) { if (CurrentStream != nullptr) { AddBytes(2); UncheckedWriteInt16(it, &CurrentStream); } return *this; } NetEventData& operator<<(int32_t it) { if (CurrentStream != nullptr) { AddBytes(4); UncheckedWriteInt32(it, &CurrentStream); } return *this; } NetEventData& operator<<(int64_t it) { if (CurrentStream != nullptr) { AddBytes(8); UncheckedWriteInt64(it, &CurrentStream); } return *this; } NetEventData& operator<<(float it) { if (CurrentStream != nullptr) { AddBytes(4); UncheckedWriteFloat(it, &CurrentStream); } return *this; } NetEventData& operator<<(double it) { if (CurrentStream != nullptr) { AddBytes(8); UncheckedWriteDouble(it, &CurrentStream); } return *this; } NetEventData& operator<<(const char *it) { if (CurrentStream != nullptr) { AddBytes(strlen(it) + 1); UncheckedWriteString(it, &CurrentStream); } return *this; } } NetEvents; void Net_ClearBuffers() { CloseNetwork(); for (uint i = 0; i < MAXPLAYERS; ++i) { playeringame[i] = false; players[i].waiting = players[i].inconsistant = false; 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; state.ResendID = state.StabilityBuffer = 0u; state.CurrentNetConsistency = state.LastVerifiedConsistency = state.ConsistencyAck = state.ResendConsistencyFrom = -1; state.CurrentSequence = state.SequenceAck = state.ResendSequenceFrom = -1; state.Flags = 0; for (int j = 0; j < BACKUPTICS; ++j) state.Tics[j].Data.SetData(nullptr, 0); } NetBufferLength = 0u; RemoteClient = -1; MaxClients = TicDup = 1u; consoleplayer = 0; LocalNetBufferSize = 0u; Net_Arbitrator = 0; MutedClients = 0u; CurrentLobbyID = 0u; NetworkClients.Clear(); NetMode = NET_PeerToPeer; netgame = multiplayer = false; LastSentConsistency = CurrentConsistency = 0; LastEnterTic = LastGameUpdate = EnterTic; gametic = ClientTic = 0; SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0; StabilityBuffer = PrevAvailableDiff = 0; CurStabilityTic = 0u; memset(StabilityTics, 0, sizeof(StabilityTics)); NetEvents.ResetStream(); CutsceneReady = 0u; CutsceneCountdown = 0; bCommandsReset = false; LevelStartAck = 0u; LevelStartDelay = LevelStartDebug = 0; LevelStartStatus = LST_READY; FullLatencyCycle = MAXSENDTICS * 3; LastLatencyUpdate = 0; playeringame[0] = true; NetworkClients += 0; } bool Net_IsPlayerReady(int player) { if (demoplayback || net_cutscenereadytype != RT_VOTE) return false; if (cutscene.runner) { int type = ST_VOTE; IFVM(ScreenJobRunner, GetSkipType) type = VMCallSingle(func, cutscene.runner); if (type == ST_UNSKIPPABLE) return false; } return players[player].Bot != nullptr || (CutsceneReady & ((uint64_t)1u << player)); } // Check if every client is ready to move on from the current cutscene. void Net_PlayerReadiedUp(int player) { if (!netgame || demoplayback) return; // Allow unreadying in case a player needs to leave momentarily. if (Net_IsPlayerReady(player)) CutsceneReady &= ~((uint64_t)1u << player); else CutsceneReady |= (uint64_t)1u << player; } void Net_StartCutscene() { CutsceneCountdown = netgame && !demoplayback && net_cutscenecountdown > 0.0f ? static_cast(ceil(net_cutscenecountdown * TICRATE)) : 0; } // Allow the game to automatically start after a set amount of time. bool Net_CheckCutsceneReady() { if (!cutscene.runner) return false; int type = ST_VOTE; IFVM(ScreenJobRunner, GetSkipType) type = VMCallSingle(func, cutscene.runner); if (type == ST_UNSKIPPABLE) return false; if (net_cutscenereadytype == RT_ANYONE) return CutsceneReady != 0; if (net_cutscenereadytype == RT_HOST_ONLY) return (CutsceneReady & ((uint64_t)1u << Net_Arbitrator)); uint64_t mask = 0u; int totalReady = 0; // Bots will be automatically assumed to be ready, so we don't include them. for (auto client : NetworkClients) { mask |= (uint64_t)1u << client; totalReady += Net_IsPlayerReady(client); } if ((CutsceneReady & mask) == mask) return true; if ((float)totalReady / NetworkClients.Size() < net_cutscenereadypercent) return false; if (CutsceneCountdown <= 0) return true; --CutsceneCountdown; return false; } void Net_AdvanceCutscene() { CutsceneReady = 0u; CutsceneCountdown = 0; if (consoleplayer == Net_Arbitrator) Net_WriteInt8(DEM_ENDSCREENJOB); } void Net_ResetCommands(bool midTic) { bCommandsReset = midTic; ++CurrentLobbyID; SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0; StabilityBuffer = PrevAvailableDiff = 0; CurStabilityTic = 0u; memset(StabilityTics, 0, sizeof(StabilityTics)); int tic = gametic / TicDup; if (midTic) { // 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) * TicDup; gametic = (tic * TicDup) + (TicDup - 1); } else { ClientTic = gametic = tic * TicDup; --tic; } for (auto client : NetworkClients) { auto& state = ClientStates[client]; state.Flags &= CF_QUIT; state.StabilityBuffer = 0u; 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]; const int running = (curTic.Command.buttons & BT_RUN); // This isn't delta'd so needs to be kept. memset(&curTic.Command, 0, sizeof(curTic.Command)); curTic.Command.buttons |= running; } 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 size_t GetNetBufferSize() { if (NetBuffer[0] & NCMD_EXIT) return 1 + (NetMode == NET_PacketServer && RemoteClient == Net_Arbitrator); // TODO: Need a skipper for this. if (NetBuffer[0] & NCMD_SETUP) return NetBufferLength; if (NetBuffer[0] & (NCMD_LATENCY | NCMD_LATENCYACK)) return 2; if (NetBuffer[0] & NCMD_LEVELREADY) { int bytes = 2; if (NetMode == NET_PacketServer && RemoteClient == Net_Arbitrator) bytes += 2; return bytes; } // Header info uint 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; // Stability buffer/commands ahead if (NetMode == NET_PacketServer) ++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 && RemoteClient == Net_Arbitrator) padding += 2; if (NetBufferLength < totalBytes + playerCount * padding) return totalBytes + playerCount * padding; TArrayView skipper = TArrayView(&NetBuffer[totalBytes], MAX_MSGLEN - totalBytes); for (int p = 0; p < playerCount; ++p) { AdvanceStream(skipper, 1); if (NetMode == NET_PacketServer && RemoteClient == Net_Arbitrator) AdvanceStream(skipper, 2); for (int i = 0; i < ranTics; ++i) AdvanceStream(skipper, 3); for (int i = 0; i < numTics; ++i) { AdvanceStream(skipper, 1); SkipUserCmdMessage(skipper); } } return int(skipper.Data() - NetBuffer); } // // HSendPacket // static void HSendPacket(int client, size_t size) { // This data already exists locally in the demo file, so don't write it out. if (demoplayback) return; RemoteClient = client; NetBufferLength = size; if (client == consoleplayer) { memcpy(LocalNetBuffer, NetBuffer, size); LocalNetBufferSize = size; return; } if (!netgame) I_Error("Tried to send a packet to a client while offline"); I_NetCmd(CMD_SEND); } // HGetPacket // Returns false if no packet is waiting static bool HGetPacket() { if (demoplayback) return false; if (LocalNetBufferSize) { memcpy(NetBuffer, LocalNetBuffer, LocalNetBufferSize); NetBufferLength = LocalNetBufferSize; RemoteClient = consoleplayer; LocalNetBufferSize = 0u; return true; } if (!netgame) return false; I_NetCmd(CMD_GET); if (RemoteClient == -1) return false; size_t sizeCheck = GetNetBufferSize(); if (NetBufferLength != sizeCheck) { Printf("Incorrect packet size %d (expected %d)\n", NetBufferLength, sizeCheck); return false; } return true; } static void ClientConnecting(int client) { if (consoleplayer != Net_Arbitrator) return; // TODO: Eventually... } static void DisconnectClient(int clientNum) { NetworkClients -= clientNum; const uint64_t mask = ~((uint64_t)1u << clientNum); MutedClients &= mask; CutsceneReady &= mask; LevelStartAck &= mask; I_ClearClient(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 (auto client : NetworkClients) ClientStates[client].AverageLatency = 0u; Net_ResetCommands(false); Net_SetWaiting(); } } static void ClientQuit(int clientNum, int newHost) { if (!NetworkClients.InGame(clientNum)) return; // 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 (consoleplayer != Net_Arbitrator) DPrintf(DMSG_WARNING, "Received disconnect packet from client %d erroneously\n", clientNum); else ClientStates[clientNum].Flags |= CF_QUIT; return; } DisconnectClient(clientNum); if (clientNum == Net_Arbitrator) SetArbitrator(newHost >= 0 ? newHost : NetworkClients[0]); if (demorecording) G_CheckDemoStatus(); } 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 = 0u; LevelStartStatus = NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator ? LST_HOST : LST_READY; LevelStartDelay = LevelStartDebug = delayTics; LastGameUpdate = EnterTic; return; } uint64_t mask = 0u; for (auto pNum : NetworkClients) { if (pNum != Net_Arbitrator) mask |= (uint64_t)1u << pNum; } LevelStartAck |= (uint64_t)1u << 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 // static void GetPackets() { TArray latencyAcks = {}; while (HGetPacket()) { const int clientNum = RemoteClient; auto& clientState = ClientStates[clientNum]; if (NetBuffer[0] & NCMD_EXIT) { ClientQuit(clientNum, NetMode == NET_PacketServer && clientNum == Net_Arbitrator ? NetBuffer[1] : -1); continue; } if (NetBuffer[0] & NCMD_SETUP) { HandleIncomingConnection(); continue; } if (NetBuffer[0] & NCMD_LATENCY) { size_t i = 0u; for (; i < latencyAcks.Size(); ++i) { 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) { if (validID) { if (clientNum == Net_Arbitrator) CommandsAhead = NetBuffer[curByte]; else if (consoleplayer == Net_Arbitrator) clientState.StabilityBuffer = NetBuffer[curByte]; } ++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 + int(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 = {}; // each contained TArrayView represents a packet. 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++]; TArrayView skipper = TArrayView(&NetBuffer[curByte], MAX_MSGLEN - curByte); SkipUserCmdMessage(skipper); TArrayView packet = TArrayView(&NetBuffer[curByte], skipper.Data() - &NetBuffer[curByte]); data.Insert(ofs, packet); curByte += skipper.Data() - &NetBuffer[curByte]; } // 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 + int(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; } // Update this so host switching doesn't have any hiccups in packet-server mode. if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator && pNum != Net_Arbitrator) pState.SequenceAck = seq; } } } for (const auto& ack : latencyAcks) { NetBuffer[0] = NCMD_LATENCYACK; NetBuffer[1] = ack.Seq; HSendPacket(ack.Client, 2); } } 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; auto& state = ClientStates[client]; if (LastLatencyUpdate >= MAXSENDTICS) { int delta = 0; const uint8_t startTic = state.CurrentLatency - MAXSENDTICS; for (int i = 0; i < MAXSENDTICS; ++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]; } state.AverageLatency = delta / MAXSENDTICS; } 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) { ++clientState.LastVerifiedConsistency; const int tic = clientState.LastVerifiedConsistency % BACKUPTICS; if (clientState.LocalConsistency[tic] != clientState.NetConsistency[tic]) { players[client].inconsistant = true; clientState.LastVerifiedConsistency = clientState.CurrentNetConsistency; break; } } } } } //========================================================================== // // 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(); } 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 % TicDup) || !IsMapLoaded()) return; const uint32_t rngSum = StaticSumSeeds(); for (auto client : NetworkClients) { auto& clientState = ClientStates[client]; clientState.LocalConsistency[CurrentConsistency % BACKUPTICS] = CalculateConsistency(client, rngSum); } ++CurrentConsistency; } static bool Net_UpdateStatus() { if (!netgame || demoplayback || NetworkClients.Size() <= 1) return true; if (LevelStartStatus == LST_WAITING || LevelStartDelay > 0) return false; // 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 * TicDup) { // Try again in the next MaxDelay tics. LastGameUpdate = EnterTic; if (NetMode != NET_PacketServer || consoleplayer == Net_Arbitrator) { // 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 / 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 { // 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; } } 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 && !(gametic % TicDup)) { if (NetMode != NET_PacketServer) { // Check if everyone has a buffer for us. If they do, we're too far ahead. bool allUpdated = true; int highestLatency = 0; for (auto client : NetworkClients) { if (client != consoleplayer) { if (ClientStates[client].Flags & CF_UPDATED) { updated = true; int diff = ClientStates[client].SequenceAck - ClientStates[client].CurrentSequence; if (diff < lowestDiff) lowestDiff = diff; if (ClientStates[client].AverageLatency > highestLatency) highestLatency = ClientStates[client].AverageLatency; } else { allUpdated = false; } } ClientStates[client].Flags &= ~CF_UPDATED; } if (allUpdated) { // If we're consistently ahead of the highest latency player we're connected to, slow down // as well since we should generally be in that ballpark. const int diff = (ClientTic - gametic) / TicDup; const int goal = static_cast(ceil((double)highestLatency / TICRATE)) / TicDup + 1; if (diff > goal) lowestDiff = diff - goal; } } else if (consoleplayer == Net_Arbitrator) { // If we're consistenty ahead of the highest sequence player, slow down. bool allUpdated = true; const int curTic = ClientTic / TicDup; for (auto client : NetworkClients) { if (client != Net_Arbitrator) { if (ClientStates[client].Flags & CF_UPDATED) { updated = true; int diff = curTic - ClientStates[client].CurrentSequence; if (diff < lowestDiff) lowestDiff = diff; } else { allUpdated = false; } } ClientStates[client].Flags &= ~CF_UPDATED; } if (allUpdated) { // If we're consistently ahead of the world, force a stop here as well. Likely some client // has fallen super far behind and needs to be reset. const int diff = curTic - gametic / TicDup; if (diff > 1) lowestDiff = diff; } } 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) { lowestDiff -= StabilityBuffer; if (lowestDiff > 0) { if (SkipCommandTimer++ > TICRATE / 2) { SkipCommandTimer = 0; if (SkipCommandAmount <= 0) SkipCommandAmount = lowestDiff * 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 { 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 / 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); } bool netGood = Net_UpdateStatus(); const int startTic = ClientTic; tics = min(tics, MAXSENDTICS * TicDup); if ((startTic + tics - gametic) / TicDup > BACKUPTICS / 2) { tics = (gametic + BACKUPTICS / 2 * TicDup) - startTic; if (tics <= 0) { tics = 1; netGood = false; } } 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 (TicDup == 1) { Net_NewClientTic(); } else { const int ticDiff = ClientTic % TicDup; if (ticDiff) { const int startTic = ClientTic - ticDiff; // Even if we're not sending out inputs, update the local commands so that the 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 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 - TicDup; for (int j = ClientTic - 1; j > lastTic; --j) LocalCmds[(j - 1) % LOCALCMDTICS].buttons |= LocalCmds[j % LOCALCMDTICS].buttons; int pitch = 0; int yaw = 0; int roll = 0; int forwardmove = 0; int sidemove = 0; int upmove = 0; for (int j = 0; j < TicDup; ++j) { 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; for (int j = 0; j < TicDup; ++j) { 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_NewClientTic(); } } } const int newestTic = ClientTic / TicDup; if (demoplayback) { // Don't touch net command data while playing a demo, as it'll already exist. for (auto client : NetworkClients) ClientStates[client].CurrentSequence = newestTic; return; } constexpr int MaxPlayersPerPacket = 16; int startSequence = startTic / TicDup; int endSequence = newestTic; int quitters = 0; int quitNums[MAXPLAYERS]; int players = 1u; int maxCommands = MAXSENDTICS; if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) { // 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 / TicDup; int lowestSeq = endSequence - 1; for (auto client : NetworkClients) { if (client == Net_Arbitrator) continue; // The host has special handling when disconnecting in a packet server game. if (ClientStates[client].Flags & CF_QUIT) { quitNums[quitters++] = client; } else { ++players; if (ClientStates[client].CurrentSequence < lowestSeq) lowestSeq = ClientStates[client].CurrentSequence; } } endSequence = lowestSeq + 1; // To avoid fragmenting, split up commands into groups of 16p with only 2 commands per packet. // If the average packet size with 16p is ~500b, this gives up to ~1000b per packet of data // with some leeway for network events and UDP header data. Most routers have an MTU of 1500b. // If player count is < 16, scale the number of commands by 1 per every 4 less players. // If player count is < 8, scale the number of commands by 1 per every 1 less player. // If player count is < 4, scale the number of commands by 4 per every 1 less player. constexpr size_t MaxTicsPerPacket = 2u; if (players > 1u) { maxCommands = MaxTicsPerPacket; if (players >= MaxPlayersPerPacket / 2 && players < MaxPlayersPerPacket) maxCommands = MaxTicsPerPacket + (MaxPlayersPerPacket - players) / 4; else if (players >= MaxPlayersPerPacket / 4 && players < MaxPlayersPerPacket / 2) maxCommands = MaxPlayersPerPacket / 4 + MaxPlayersPerPacket / 2 - players; else if (players < MaxPlayersPerPacket / 4) maxCommands = MaxPlayersPerPacket / 2 + (MaxPlayersPerPacket / 4 - players) * 4; } } const bool resendOnly = startSequence == endSequence && (ClientTic % TicDup); const int playerLoops = static_cast(ceil((double)players / MaxPlayersPerPacket)); for (auto client : NetworkClients) { // 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; 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. if ((curState.Flags & CF_QUIT) || (resendOnly && !(curState.Flags & (CF_RETRANSMIT | CF_MISSING)))) continue; const bool isSelf = client == consoleplayer; NetBuffer[0] = (curState.Flags & CF_MISSING) ? NCMD_RETRANSMIT : 0; curState.Flags &= ~CF_MISSING; NetBuffer[1] = (curState.Flags & CF_RETRANSMIT_SEQ) ? curState.ResendID : CurrentLobbyID; int lastSeq = curState.CurrentSequence; int lastCon = curState.CurrentNetConsistency; if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) { // If in packet-server mode, make sure to get the lowest sequence of all players // since the host themselves might have gotten updated but someone else in the packet // did not. That way the host knows to send over the correct tic. for (auto cl : NetworkClients) { if (ClientStates[cl].CurrentSequence < lastSeq) lastSeq = ClientStates[cl].CurrentSequence; if (ClientStates[cl].CurrentNetConsistency < lastCon) lastCon = ClientStates[cl].CurrentNetConsistency; } } // Last sequence we got from this client. NetBuffer[2] = (lastSeq >> 24); NetBuffer[3] = (lastSeq >> 16); NetBuffer[4] = (lastSeq >> 8); NetBuffer[5] = lastSeq; // Last consistency we got from this client. NetBuffer[6] = (lastCon >> 24); NetBuffer[7] = (lastCon >> 16); NetBuffer[8] = (lastCon >> 8); NetBuffer[9] = lastCon; if (curState.Flags & CF_RETRANSMIT_SEQ) { 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.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); int ticLoops = static_cast(ceil(max(numTics, ran) / maxCommands)); if (isSelf || !ticLoops) ticLoops = 1; const int maxPlayerLoops = isSelf ? 1 : playerLoops; int totalQuits = quitters; for (int tLoops = 0, curTicOfs = 0; tLoops < ticLoops; ++tLoops, curTicOfs += maxCommands) { for (int pLoops = 0, curPlayerOfs = 0; pLoops < maxPlayerLoops; ++pLoops, curPlayerOfs += MaxPlayersPerPacket) { size_t size = 10; if (totalQuits > 0) { NetBuffer[0] |= NCMD_QUITTERS; NetBuffer[size++] = totalQuits; for (int i = 0; i < totalQuits; ++i) NetBuffer[size++] = quitNums[i]; totalQuits = 0; } else { NetBuffer[0] &= ~NCMD_QUITTERS; } int playerNums[MAXPLAYERS]; int playerCount = isSelf ? players : min(players - curPlayerOfs, MaxPlayersPerPacket); NetBuffer[size++] = playerCount; if (players > 1) { int i = 0; for (auto cl : NetworkClients) { if (ClientStates[cl].Flags & CF_QUIT) continue; if (i >= curPlayerOfs) playerNums[i - curPlayerOfs] = cl; ++i; if (!isSelf && i >= curPlayerOfs + MaxPlayersPerPacket) break; } } else { playerNums[0] = consoleplayer; } int sendTics = isSelf ? numTics : clamp(numTics - curTicOfs, 0, maxCommands); if (curState.ResendSequenceFrom >= 0) { curState.ResendSequenceFrom += sendTics; if (curState.ResendSequenceFrom >= endSequence) curState.ResendSequenceFrom = -1; } NetBuffer[size++] = sendTics; if (sendTics > 0) { NetBuffer[size++] = (sequenceNum + curTicOfs) >> 24; NetBuffer[size++] = (sequenceNum + curTicOfs) >> 16; NetBuffer[size++] = (sequenceNum + curTicOfs) >> 8; NetBuffer[size++] = sequenceNum + curTicOfs; } int sendCon = isSelf ? ran : clamp(ran - curTicOfs, 0, maxCommands); if (curState.ResendConsistencyFrom >= 0) { curState.ResendConsistencyFrom += sendCon; if (curState.ResendConsistencyFrom >= CurrentConsistency) curState.ResendConsistencyFrom = -1; } NetBuffer[size++] = sendCon; if (sendCon > 0) { NetBuffer[size++] = (baseConsistency + curTicOfs) >> 24; NetBuffer[size++] = (baseConsistency + curTicOfs) >> 16; NetBuffer[size++] = (baseConsistency + curTicOfs) >> 8; NetBuffer[size++] = baseConsistency + curTicOfs; } if (NetMode == NET_PacketServer) { if (consoleplayer == Net_Arbitrator) NetBuffer[size++] = client == Net_Arbitrator ? 0 : max(curState.CurrentSequence + curState.StabilityBuffer - newestTic, 0); else NetBuffer[size++] = max(StabilityBuffer, 0); } // Client commands. TArrayView cmd = TArrayView(&NetBuffer[size], MAX_MSGLEN - size); for (int i = 0; i < playerCount; ++i) { WriteInt8(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) { WriteInt16(clientState.AverageLatency, cmd); } for (int r = 0; r < sendCon; ++r) { WriteInt8(r, cmd); const int tic = (baseConsistency + curTicOfs + r) % BACKUPTICS; WriteInt16(clientState.LocalConsistency[tic], cmd); } for (int t = 0; t < sendTics; ++t) { WriteInt8(t, cmd); int curTic = sequenceNum + curTicOfs + t, lastTic = curTic - 1; if (playerNums[i] == consoleplayer) { int realTic = (curTic * TicDup) % LOCALCMDTICS; int realLastTic = (lastTic * 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]; WriteBytes(TArrayView(stream.Stream, stream.Used), cmd); WriteUserCmdMessage(LocalCmds[realTic], realLastTic >= 0 ? &LocalCmds[realLastTic] : nullptr, cmd); } else { auto& netTic = clientState.Tics[curTic % BACKUPTICS]; auto data = netTic.Data.GetTArrayView(); WriteBytes(data, cmd); WriteUserCmdMessage(netTic.Command, lastTic >= 0 ? &clientState.Tics[lastTic % BACKUPTICS].Command : nullptr, cmd); } } } HSendPacket(client, int(cmd.Data() - NetBuffer)); if (net_extratic && !isSelf) HSendPacket(client, int(cmd.Data() - NetBuffer)); } } } // Update this now that all the packets have been sent out. if (!resendOnly) LastSentConsistency = CurrentConsistency; // 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(); } // These have to be here since they have game-specific data. Only the data // from the frontend should be put in these, all backend handling should be // done in the core files. size_t Net_SetEngineInfo(uint8_t*& stream) { stream[0] = VER_MAJOR % 256; stream[1] = VER_MINOR % 256; stream[2] = VER_REVISION % 256; return 3u; } bool Net_VerifyEngine(uint8_t*& stream) { return stream[0] == (VER_MAJOR % 256) && stream[1] == (VER_MINOR % 256) && stream[2] == (VER_REVISION % 256); } void Net_SetupUserInfo() { D_SetupUserInfo(); } const char* Net_GetClientName(int client, unsigned int charLimit = 0u) { return players[client].userinfo.GetName(charLimit); } void Net_SetUserInfo(int client, TArrayView& stream) { auto str = D_GetUserInfoStrings(client, true); WriteFString(str, stream); } void Net_ReadUserInfo(int client, TArrayView& stream) { D_ReadUserInfoStrings(client, stream, false); } void Net_SetGameInfo(TArrayView& stream) { WriteFString(startmap, stream); WriteInt32(rngseed, stream); C_WriteCVars(stream, CVAR_SERVERINFO, true); auto load = Args->CheckValue("-loadgame"); if (load != nullptr) { WriteInt8(1, stream); WriteString(load, stream); } else { WriteInt8(0, stream); } } void Net_ReadGameInfo(TArrayView& stream) { startmap = ReadStringConst(stream); rngseed = ReadInt32(stream); C_ReadCVars(stream); if (ReadInt8(stream)) { auto load = ReadString(stream); // Don't override the existing argument in case they need to use // a custom savefile name. if (!Args->CheckParm("-loadgame")) { Args->AppendArg("-loadgame"); Args->AppendArg(load); } } } // Connects players to each other if needed. bool D_CheckNetGame() { if (!I_InitNetwork()) return false; if (Args->CheckParm("-extratic")) net_extratic = true; players[Net_Arbitrator].settings_controller = true; for (auto client : NetworkClients) playeringame[client] = true; if (MaxClients > 1u) { if (consoleplayer == Net_Arbitrator) Printf("Selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server"); else Printf("Host selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server"); Printf("Player %d of %d\n", consoleplayer + 1, MaxClients); } return true; } // // D_QuitNetGame // Called before quitting to leave a net game // without hanging the other players // void D_QuitNetGame() { if (!netgame || !usergame || consoleplayer == -1 || demoplayback || NetworkClients.Size() == 1) return; // Send a bunch of packets for stability. NetBuffer[0] = NCMD_EXIT; if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) { // 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 (client != Net_Arbitrator) { nextHost = client; break; } } 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 { for (int i = 0; i < 4; ++i) { // 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); } } } ADD_STAT(network) { FString out = {}; if (!netgame || demoplayback) { out.AppendFormat("No network stats available."); return out; } out.AppendFormat("Max players: %d\tNet mode: %s\tTic dup: %d", MaxClients, NetMode == NET_PacketServer ? "Packet server" : "Peer to peer", TicDup); if (net_extratic) out.AppendFormat("\tExtra tic enabled"); out.AppendFormat("\nWorld tic: %06d (sequence %06d)", gametic, gametic / TicDup); if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) out.AppendFormat("\tStart tics delay: %d", LevelStartDebug); const int delay = max((ClientTic - gametic) / TicDup, 0); const int msDelay = min(delay * TicDup * 1000.0 / TICRATE, 999); const int buffer = max(StabilityBuffer, 0); const int msBuffer = min(buffer * 1000.0 / TICRATE, 999); out.AppendFormat("\nLocal\n\tIs arbitrator: %d\tDelay: %02d (%03dms)\tStability Buffer: %02d (%03dms)", consoleplayer == Net_Arbitrator, delay, msDelay, buffer, msBuffer); 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 / 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 / TicDup), 0); const int mscDelay = min(cDelay * 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 / TicDup), 0)); return out; } // Forces playsim processing time to be consistent across frames. // This improves interpolation for frames in between tics. // // With this cvar off the mods with a high playsim processing time will appear // less smooth as the measured time used for interpolation will vary. CVAR(Bool, r_ticstability, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) static uint64_t stabilityticduration = 0; static uint64_t stabilitystarttime = 0; static void TicStabilityWait() { using namespace std::chrono; using namespace std::this_thread; if (!r_ticstability) return; uint64_t start = duration_cast(steady_clock::now().time_since_epoch()).count(); while (true) { uint64_t cur = duration_cast(steady_clock::now().time_since_epoch()).count(); if (cur - start > stabilityticduration) break; } } static void TicStabilityBegin() { using namespace std::chrono; stabilitystarttime = duration_cast(steady_clock::now().time_since_epoch()).count(); } static void TicStabilityEnd() { using namespace std::chrono; uint64_t stabilityendtime = duration_cast(steady_clock::now().time_since_epoch()).count(); 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; } // If the connection has been unstable then let the game lag behind for a little bit // while we wait for it to stabilize, otherwise everything will appear to jitter around. static void CalculateNetStabilityBuffer(int diff) { if (!netgame || demoplayback) { StabilityBuffer = 0; return; } if (diff < 0) diff = 0; if (!(gametic % TicDup)) { StabilityTics[CurStabilityTic++ % STABILITYTICS] = diff > PrevAvailableDiff ? diff : 0; PrevAvailableDiff = diff; } // If we're not balancing latency, just give an extra tic for padding // and nothing else. if (!net_ticbalance) { StabilityBuffer = 1; return; } double total = 0.0; int unstableCount = 0; for (int t : StabilityTics) { if (t > 0) { ++unstableCount; total += t; } } StabilityBuffer = unstableCount > 0 ? static_cast(ceil(total / unstableCount)) : 0; } // // TryRunTics // void TryRunTics() { GC::CheckGC(); if (ToggleFullscreen) { ToggleFullscreen = false; AddCommandString("toggle vid_fullscreen"); } bool doWait = (cl_capfps || pauseext || (!netgame && r_NoInterpolate && !M_IsAnimated())); if (vid_dontdowait && (vid_maxfps > 0 || vid_vsync)) doWait = false; if (!netgame && !AppActive && vid_lowerinbackground) doWait = true; // Get the full number of tics the client can run. if (doWait) EnterTic = I_WaitForTic(LastEnterTic); else EnterTic = I_GetTime(); 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 one seconds worth of commands that can be // generated in advanced from the last time the game updated. NetUpdate(totalTics); LastEnterTic = EnterTic; // If the game is paused, everything we need to update has already done so. if (pauseext) return; // 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 (ClientStates[client].CurrentSequence < lowestSequence) lowestSequence = ClientStates[client].CurrentSequence; } // If the lowest confirmed tic matches the server gametic or greater, allow the client // to run some of them. const int availableTics = (lowestSequence - gametic / TicDup) + 1; // 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 (!singletics && totalTics > 0) { CalculateNetStabilityBuffer(availableTics - totalTics); if (totalTics < availableTics - StabilityBuffer) ++runTics; } // Test player prediction code in singleplayer // by running the gametic behind the ClientTic if (!netgame && !demoplayback && cl_debugprediction > 0) { int debugTarget = ClientTic - cl_debugprediction; int debugOffset = gametic - debugTarget; if (debugOffset > 0) { runTics = max(runTics - debugOffset, 0); } } // If there are no tics to run, check for possible stall conditions and new // commands to predict. if (runTics <= 0) { // 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. } // 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) P_RunClientsideLogic(); return; } for (auto client : NetworkClients) players[client].waiting = false; // Update the last time the game tic'd. LastGameUpdate = EnterTic; // Run the available tics. P_UnPredictPlayer(); while (runTics--) { const bool stabilize = ShouldStabilizeTick(); if (stabilize) TicStabilityBegin(); if (advancedemo) D_DoAdvanceDemo(); G_Ticker(); MakeConsistencies(); ++gametic; if (stabilize) TicStabilityEnd(); if (bCommandsReset) { bCommandsReset = false; break; } } P_PredictPlayer(&players[consoleplayer]); S_UpdateSounds(players[consoleplayer].camera); // Update sounds only after predicting the client's newest position. // These should use the actual tics since they're not actually tied to the gameplay logic. // Make sure it always comes after so the HUD has the correct game state when updating. for (int i = 0; i < totalTics; ++i) P_RunClientsideLogic(); } void Net_NewClientTic() { NetEvents.NewClientTic(); } void Net_Initialize() { NetEvents.InitializeEventData(); } void Net_WriteInt8(uint8_t it) { NetEvents << it; } void Net_WriteInt16(int16_t it) { NetEvents << it; } void Net_WriteInt32(int32_t it) { NetEvents << it; } void Net_WriteInt64(int64_t it) { NetEvents << it; } void Net_WriteFloat(float it) { NetEvents << it; } void Net_WriteDouble(double it) { NetEvents << it; } void Net_WriteString(const char *it) { NetEvents << it; } void Net_WriteBytes(const uint8_t *block, int len) { while (len--) NetEvents << *block++; } //========================================================================== // // Dynamic buffer interface // //========================================================================== FDynamicBuffer::FDynamicBuffer() { m_Data = nullptr; m_Len = m_BufferLen = 0; } FDynamicBuffer::~FDynamicBuffer() { if (m_Data != nullptr) { M_Free(m_Data); m_Data = nullptr; } m_Len = m_BufferLen = 0; } void FDynamicBuffer::SetData(const uint8_t *data, int len) { if (len > m_BufferLen) { m_BufferLen = (len + 255) & ~255; m_Data = (uint8_t *)M_Realloc(m_Data, m_BufferLen); } if (data != nullptr) { m_Len = len; memcpy(m_Data, data, len); } else { m_Len = 0; } } uint8_t *FDynamicBuffer::GetData(int *len) { if (len != nullptr) *len = m_Len; return m_Len ? m_Data : nullptr; } TArrayView FDynamicBuffer::GetTArrayView() { return TArrayView(m_Data, m_Len); } static int RemoveClass(FLevelLocals *Level, const PClass *cls) { AActor *actor; int removecount = 0; bool player = false; auto iterator = Level->GetThinkerIterator(cls->TypeName); while ((actor = iterator.Next())) { if (actor->IsA(cls)) { // [MC]Do not remove LIVE players. if (actor->player != 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 cmd, TArrayView& stream, int player) { uint8_t pos = 0; const char* s = nullptr; int i = 0; switch (cmd) { case DEM_SAY: { const char *name = players[player].userinfo.GetName(); uint8_t who = ReadInt8(stream); s = ReadStringConst(stream); // If chat is disabled, there's nothing else to do here since the stream has been advanced. if (cl_showchat == CHAT_DISABLED || (MutedClients & ((uint64_t)1u << 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 [%d]" TEXTCOLOR_BOLD "%s" TEXTCOLOR_BOLD "\n", name, player, s); else Printf(PRINT_CHAT, "%s [%d]" TEXTCOLOR_CHAT ": %s" TEXTCOLOR_CHAT "\n", name, player, s); if (!cl_nochatsound) S_Sound(CHAN_VOICE, CHANF_UI, gameinfo.chatSound, 1.0f, ATTN_NONE); } 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 [%d]" TEXTCOLOR_BOLD "%s" TEXTCOLOR_BOLD "\n", name, player, s); else Printf(PRINT_TEAMCHAT, "%s [%d]" TEXTCOLOR_TEAMCHAT ": %s" TEXTCOLOR_TEAMCHAT "\n", name, player, s); if (!cl_nochatsound) S_Sound(CHAN_VOICE, CHANF_UI, gameinfo.chatSound, 1.0f, ATTN_NONE); } } break; case DEM_MUSICCHANGE: S_ChangeMusic(ReadStringConst(stream)); break; case DEM_PRINT: Printf("%s", ReadStringConst(stream)); break; case DEM_CENTERPRINT: C_MidPrint(nullptr, ReadStringConst(stream)); break; case DEM_UINFCHANGED: D_ReadUserInfoStrings(player, stream, true); break; case DEM_SINFCHANGED: D_DoServerInfoChange(stream, false); break; case DEM_SINFCHANGEDXOR: D_DoServerInfoChange(stream, true); break; case DEM_GIVECHEAT: s = ReadStringConst(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)); break; case DEM_SETINV: s = ReadStringConst(stream); i = ReadInt32(stream); cht_SetInv(&players[player], s, i, !!ReadInt8(stream)); break; case DEM_WARPCHEAT: { 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)); break; case DEM_CHANGEMAP2: pos = ReadInt8(stream); [[fallthrough]]; case DEM_CHANGEMAP: // Change to another map without disconnecting other players s = ReadStringConst(stream); // Using LEVEL_NOINTERMISSION tends to throw the game out of sync. // That was a long time ago. Maybe it works now? primaryLevel->flags |= LEVEL_CHANGEMAPCHEAT; primaryLevel->ChangeLevel(s, pos, 0); break; case DEM_SUICIDE: cht_Suicide(&players[player]); break; case DEM_ADDBOT: primaryLevel->BotInfo.TryAddBot(primaryLevel, stream, player); break; case DEM_KILLBOTS: primaryLevel->BotInfo.RemoveAllBots(primaryLevel, true); Printf ("Removed all bots\n"); break; case DEM_CENTERVIEW: players[player].centering = true; break; case DEM_INVUSEALL: if (gamestate == GS_LEVEL && !paused && players[player].playerstate != PST_DEAD) { AActor *item = players[player].mo->Inventory; auto pitype = PClass::FindActor(NAME_PuzzleItem); while (item != nullptr) { AActor *next = item->Inventory; IFVIRTUALPTRNAME(item, NAME_Inventory, UseAll) { VMValue param[] = { item, players[player].mo }; VMCall(func, param, 2, nullptr, 0); } item = next; } } break; case DEM_INVUSE: case DEM_INVDROP: { uint32_t which = ReadInt32(stream); int amt = -1; 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 != nullptr && item->InventoryID != which) item = item->Inventory; if (item != nullptr) { if (cmd == DEM_INVUSE) players[player].mo->UseInventory(item); else players[player].mo->DropInventory(item, amt); } } } break; case DEM_SUMMON: case DEM_SUMMONFRIEND: case DEM_SUMMONFOE: case DEM_SUMMONMBF: case DEM_SUMMON2: case DEM_SUMMONFRIEND2: case DEM_SUMMONFOE2: { int angle = 0; int16_t tid = 0; uint8_t special = 0; int args[5]; s = ReadStringConst(stream); 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); } AActor* source = players[player].mo; if (source != NULL) { PClassActor* typeinfo = PClass::FindActor(s); if (typeinfo != NULL) { if (GetDefaultByType(typeinfo)->flags & MF_MISSILE) { P_SpawnPlayerMissile(source, 0, 0, 0, typeinfo, source->Angles.Yaw); } else { const AActor* def = GetDefaultByType(typeinfo); DVector3 spawnpos = source->Vec3Angle(def->radius * 2 + source->radius, source->Angles.Yaw, 8.); AActor* spawned = Spawn(primaryLevel, typeinfo, spawnpos, ALLOW_REPLACE); if (spawned != NULL) { spawned->SpawnFlags |= MTF_CONSOLETHING; if (cmd == DEM_SUMMONFRIEND || cmd == DEM_SUMMONFRIEND2 || cmd == DEM_SUMMONMBF) { if (spawned->CountsAsKill()) { primaryLevel->total_monsters--; } spawned->FriendPlayer = player + 1; spawned->flags |= MF_FRIENDLY; spawned->LastHeard = players[player].mo; spawned->health = spawned->SpawnHealth(); if (cmd == DEM_SUMMONMBF) spawned->flags3 |= MF3_NOBLOCKMONST; } else if (cmd == DEM_SUMMONFOE || cmd == DEM_SUMMONFOE2) { spawned->FriendPlayer = 0; spawned->flags &= ~MF_FRIENDLY; spawned->health = spawned->SpawnHealth(); } 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++) { spawned->args[i] = args[i]; } if (tid) spawned->SetTID(tid); } } } } else { // not an actor, must be a 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, false); if (vt) { vt->PT.Pos = spawnpos; vt->UpdateSector(); } } } } } break; case DEM_SPRAY: s = ReadStringConst(stream); SprayDecal(players[player].mo, s); break; case DEM_MDK: s = ReadStringConst(stream); cht_DoMDK(&players[player], s); break; case DEM_PAUSE: if (gamestate == GS_LEVEL) { if (paused) { paused = 0; S_ResumeSound(false); } else { paused = player + 1; S_PauseSound(false, false); } } break; case DEM_SAVEGAME: if (gamestate == GS_LEVEL) { savegamefile = ReadStringConst(stream); savedescription = ReadStringConst(stream); if (player != consoleplayer) { // Paths sent over the network will be valid for the system that sent // the save command. For other systems, the path needs to be changed. FString basename = ExtractFileBase(savegamefile.GetChars(), true); savegamefile = G_BuildSaveName(basename.GetChars()); } } gameaction = ga_savegame; break; case DEM_CHECKAUTOSAVE: // Do not autosave in multiplayer games or when dead. // For demo playback, DEM_DOAUTOSAVE already exists in the demo if the // autosave happened. And if it doesn't, we must not generate it. if (!netgame && !demoplayback && disableautosave < 2 && autosavecount && players[player].playerstate == PST_LIVE) { Net_WriteInt8(DEM_DOAUTOSAVE); } break; case DEM_DOAUTOSAVE: gameaction = ga_autosave; break; case DEM_FOV: { float newfov = ReadFloat(stream); if (newfov != players[player].DesiredFOV) { Printf("FOV%s set to %g\n", player == Net_Arbitrator ? " for everyone" : "", newfov); } for (auto client : NetworkClients) players[client].DesiredFOV = newfov; } break; case DEM_MYFOV: 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, (cmd == DEM_RUNSCRIPT2) ? ACS_ALWAYS : 0); } break; case DEM_RUNNAMEDSCRIPT: { s = ReadStringConst(stream); int argn = ReadInt8(stream); RunScript(stream, players[player].mo, -FName(s).GetIndex(), argn & 127, (argn & 128) ? ACS_ALWAYS : 0); } break; case DEM_RUNSPECIAL: { int snum = ReadInt16(stream); int argn = ReadInt8(stream); 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, 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 != nullptr && players[player].playerstate == PST_LIVE && !(players[player].oldbuttons & BT_JUMP) && !P_IsPlayerTotallyFrozen(&players[player])) { players[player].crouching = players[player].crouchdir < 0 ? 1 : -1; } break; case DEM_MORPHEX: { s = ReadStringConst(stream); FString msg = cht_Morph(players + player, PClass::FindActor(s), false); if (player == consoleplayer) Printf("%s\n", msg[0] != '\0' ? msg.GetChars() : "Morph failed."); } break; case DEM_ADDCONTROLLER: { uint8_t playernum = ReadInt8(stream); players[playernum].settings_controller = true; if (consoleplayer == playernum) Printf("You can now control game settings\n"); else if (consoleplayer == Net_Arbitrator) Printf("%s [%d] is now a settings controller\n", players[playernum].userinfo.GetName(), playernum); } break; case DEM_DELCONTROLLER: { uint8_t playernum = ReadInt8(stream); players[playernum].settings_controller = false; if (consoleplayer == playernum) Printf("You can no longer control game settings\n"); else if (consoleplayer == Net_Arbitrator) Printf("%s [%d] is no longer a settings controller\n", players[playernum].userinfo.GetName(), playernum); } break; case DEM_KILLCLASSCHEAT: { s = ReadStringConst(stream); int killcount = 0; PClassActor *cls = PClass::FindActor(s); 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); } else { 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 != nullptr && cls->IsDescendantOf(RUNTIME_CLASS(AActor))) { removecount = RemoveClass(primaryLevel, cls); const PClass *cls_rep = cls->GetReplacement(primaryLevel); if (cls != cls_rep) removecount += RemoveClass(primaryLevel, cls_rep); Printf("Removed %d actors of type %s.\n", removecount, s); } else { Printf("%s is not an actor class.\n", s); } } break; case DEM_CONVREPLY: case DEM_CONVCLOSE: case DEM_CONVNULL: P_ConversationCommand(cmd, player, stream); break; case DEM_SETSLOT: case DEM_SETSLOTPNUM: { int pnum = player; if (cmd == DEM_SETSLOTPNUM) pnum = ReadInt8(stream); 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) { PClassActor *wpn = Net_ReadWeapon(stream); players[pnum].weapons.AddSlot(slot, wpn, pnum == consoleplayer); } } break; case DEM_ADDSLOT: { int slot = ReadInt8(stream); PClassActor *wpn = Net_ReadWeapon(stream); players[player].weapons.AddSlot(slot, wpn, player == consoleplayer); } break; case DEM_ADDSLOTDEFAULT: { int slot = ReadInt8(stream); PClassActor *wpn = Net_ReadWeapon(stream); players[player].weapons.AddSlotDefault(slot, wpn, player == consoleplayer); } break; case DEM_SETPITCHLIMIT: players[player].MinPitch = DAngle::fromDeg(-ReadInt8(stream)); // up players[player].MaxPitch = DAngle::fromDeg(ReadInt8(stream)); // down break; case DEM_REVERTCAMERA: players[player].camera = players[player].mo; break; case DEM_FINISHGAME: // Simulate an end-of-game action primaryLevel->ChangeLevel(nullptr, 0, 0); break; case DEM_NETEVENT: { s = ReadStringConst(stream); int argn = ReadInt8(stream); int arg[3] = { 0, 0, 0 }; for (int i = 0; i < 3; i++) arg[i] = ReadInt32(stream); bool manual = !!ReadInt8(stream); primaryLevel->localEventManager->Console(player, s, arg[0], arg[1], arg[2], manual, false); } break; case DEM_ENDSCREENJOB: EndScreenJob(); break; case DEM_READIED: Net_PlayerReadiedUp(player); break; case DEM_ZSC_CMD: { FName cmd = ReadStringConst(stream); unsigned int size = ReadInt16(stream); TArray buffer; if (size) { buffer.Grow(size); for (unsigned int i = 0u; i < size; ++i) buffer.Push(ReadInt8(stream)); } FNetworkCommand netCmd = { player, cmd, buffer }; primaryLevel->localEventManager->NetCommand(netCmd); } 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 if (NetworkClients.InGame(pNum)) { Printf("%s [%d] has been kicked from the game\n", players[pNum].userinfo.GetName(), pNum); DisconnectClient(pNum); } } break; default: I_Error("Unknown net command: %d", cmd); break; } } // Used by DEM_RUNSCRIPT, DEM_RUNSCRIPT2, and DEM_RUNNAMEDSCRIPT static void RunScript(TArrayView& 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) return; 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, nullptr, snum, primaryLevel->MapName.GetChars(), arg, min(countof(arg), argn), ACS_NET | always); } // 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, TArrayView& stream) { size_t skip = 0; switch (cmd) { case DEM_SAY: skip = strlen((char *)(stream.Data() + 1)) + 2; break; case DEM_ADDBOT: skip = strlen((char *)(stream.Data() + 1)) + 6; break; case DEM_GIVECHEAT: case DEM_TAKECHEAT: skip = strlen((char *)(stream.Data())) + 5; break; case DEM_SETINV: skip = strlen((char *)(stream.Data())) + 6; break; case DEM_NETEVENT: skip = strlen((char *)(stream.Data())) + 15; break; case DEM_ZSC_CMD: skip = strlen((char*)(stream.Data())) + 1; skip += (stream[skip] << 8) | (stream[skip + 1]) + 2; break; case DEM_SUMMON2: case DEM_SUMMONFRIEND2: case DEM_SUMMONFOE2: skip = strlen((char *)(stream.Data())) + 26; break; case DEM_CHANGEMAP2: skip = strlen((char *)(stream.Data() + 1)) + 2; break; case DEM_MUSICCHANGE: case DEM_PRINT: case DEM_CENTERPRINT: case DEM_UINFCHANGED: case DEM_CHANGEMAP: case DEM_SUMMON: case DEM_SUMMONFRIEND: case DEM_SUMMONFOE: case DEM_SUMMONMBF: case DEM_REMOVE: case DEM_SPRAY: case DEM_MORPHEX: case DEM_KILLCLASSCHEAT: case DEM_MDK: skip = strlen((char *)(stream.Data())) + 1; break; case DEM_WARPCHEAT: skip = 6; break; case DEM_INVUSE: case DEM_FOV: case DEM_MYFOV: case DEM_CHANGESKILL: skip = 4; break; case DEM_INVDROP: skip = 8; break; case DEM_GENERICCHEAT: case DEM_DROPPLAYER: case DEM_ADDCONTROLLER: case DEM_DELCONTROLLER: case DEM_KICK: skip = 1; break; case DEM_SAVEGAME: skip = strlen((char *)(stream.Data())) + 1; skip += strlen((char *)(stream.Data()) + skip) + 1; break; case DEM_SINFCHANGEDXOR: case DEM_SINFCHANGED: { uint8_t t = stream[0]; skip = 1 + (t & 63); if (cmd == DEM_SINFCHANGED) { switch (t >> 6) { case CVAR_Bool: skip += 1; break; case CVAR_Int: case CVAR_Float: skip += 4; break; case CVAR_String: skip += strlen((char*)(stream.Data() + skip)) + 1; break; } } else { skip += 1; } } break; case DEM_RUNSCRIPT: case DEM_RUNSCRIPT2: skip = 3 + *(stream.Data() + 2) * 4; break; case DEM_RUNNAMEDSCRIPT: skip = strlen((char *)(stream.Data())) + 2; skip += ((*(stream.Data() + skip - 1)) & 127) * 4; break; case DEM_RUNSPECIAL: skip = 3 + *(stream.Data() + 2) * 4; break; case DEM_CONVREPLY: skip = 3; break; case DEM_SETSLOT: case DEM_SETSLOTPNUM: { skip = 2 + (cmd == DEM_SETSLOTPNUM); for (int numweapons = stream[skip-1]; numweapons > 0; --numweapons) skip += 1 + (stream[skip] >> 7); } break; case DEM_ADDSLOT: case DEM_ADDSLOTDEFAULT: skip = 2 + (stream[1] >> 7); break; case DEM_SETPITCHLIMIT: skip = 2; break; } AdvanceStream(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* localDelay, int* arbitratorDelay) { const int gameDelayMs = (ClientTic - gametic) * 1000 / TICRATE; int severity = 0; if (gameDelayMs >= 160) severity = 3; 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; } //========================================================================== // // // //========================================================================== // Intermission lobby info static int IsPlayerReady(int player) { return Net_IsPlayerReady(player); } DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, IsPlayerReady, IsPlayerReady) { PARAM_PROLOGUE; PARAM_INT(player); ACTION_RETURN_BOOL(IsPlayerReady(player)); } static void ReadyPlayer() { if (netgame && !demoplayback) Net_WriteInt8(DEM_READIED); } DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, ReadyPlayer, ReadyPlayer) { PARAM_PROLOGUE; ReadyPlayer(); return 0; } static void ResetReadyTimer() { Net_StartCutscene(); } DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, ResetReadyTimer, ResetReadyTimer) { PARAM_PROLOGUE; ResetReadyTimer(); return 0; } static int GetReadyTimer() { return CutsceneCountdown; } DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, GetReadyTimer, GetReadyTimer) { PARAM_PROLOGUE; ACTION_RETURN_INT(GetReadyTimer()); } // [RH] List "ping" times CCMD(pings) { if (!netgame) { Printf("This command can only be used when playing 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 [%d]\n", ClientStates[client].AverageLatency, players[client].userinfo.GetName(), client); } } CCMD(kick) { if (argv.argc() < 2) { Printf("Usage: kick \nRemove these clients from the game\n"); return; } if (!netgame) { Printf("This command can only be used when playing 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("This command is only accessible to the host\n"); return; } TArray cNums = {}; for (size_t i = 1u; i < argv.argc(); ++i) { int cNum = -1; if (!C_IsValidInt(argv[i], cNum) || cNum < 0 || cNum >= MAXPLAYERS) Printf("Bad client number %s\n", argv[i]); else if (cNum != consoleplayer && cNums.Find(cNum) >= cNums.Size()) cNums.Push(cNum); } for (auto cNum : cNums) { if (!NetworkClients.InGame(cNum)) { Printf("Client %d is not in game\n", cNum); } else { Net_WriteInt8(DEM_KICK); Net_WriteInt8(cNum); } } } CCMD(mute) { if (argv.argc() < 2) { Printf("Usage: mute \nDisable messages from these players\n"); return; } if (!multiplayer) { Printf("This command can only be used when playing in multiplayer\n"); return; } TArray pNums = {}; for (size_t i = 1u; i < argv.argc(); ++i) { int pNum = -1; if (!C_IsValidInt(argv[i], pNum) || pNum < 0 || pNum >= MAXPLAYERS) Printf("Bad player number %s\n", argv[i]); else if (pNum != consoleplayer && pNums.Find(pNum) >= pNums.Size()) pNums.Push(pNum); } for (auto pNum : pNums) { if (!playeringame[pNum]) { Printf("Player %d is not in game\n", pNum); } else { MutedClients |= (uint64_t)1u << pNum; Printf("Muted player %s [%d]\n", players[pNum].userinfo.GetName(), pNum); } } } CCMD(muteall) { if (!multiplayer) { Printf("This command can only be used when playing in multiplayer\n"); return; } for (int i = 0; i < MAXPLAYERS; ++i) { if (playeringame[i] && i != consoleplayer) MutedClients |= (uint64_t)1u << i; } } CCMD(listmuted) { if (!multiplayer) { Printf("This command can only be used when playing in multiplayer\n"); return; } bool found = false; for (uint i = 0; i < MAXPLAYERS; ++i) { if (MutedClients & ((uint64_t)1u << i)) { found = true; Printf("%d. %s\n", i, players[i].userinfo.GetName()); } } if (!found) Printf("No one currently muted\n"); } CCMD(unmute) { if (argv.argc() < 2) { Printf("Usage: unmute \nAllow messages from these players again\n"); return; } if (!multiplayer) { Printf("This command can only be used when playing in multiplayer\n"); return; } TArray pNums = {}; for (size_t i = 1u; i < argv.argc(); ++i) { int pNum = -1; if (!C_IsValidInt(argv[i], pNum) || pNum < 0 || pNum >= MAXPLAYERS) Printf("Bad player number %s\n", argv[i]); else if (pNum != consoleplayer && pNums.Find(pNum) >= pNums.Size()) pNums.Push(pNum); } for (auto pNum : pNums) { if (!playeringame[pNum]) { Printf("Player %d is not in game\n", pNum); } else { MutedClients &= ~((uint64_t)1u << pNum); Printf("Unmuted player %s [%d]\n", players[pNum].userinfo.GetName(), pNum); } } } CCMD(unmuteall) { if (!multiplayer) { Printf("This command can only be used when playing in multiplayer\n"); return; } MutedClients = 0u; } //========================================================================== // // Net_ChangeSettingsControllers // // Implement players who have the ability to change settings in a network // game. // //========================================================================== static void Net_ChangeSettingsControllers(const TArray& cNums, bool add) { if (!netgame) { Printf("This command can only be used when playing in a net game\n"); return; } if (consoleplayer != Net_Arbitrator) { Printf("This command is only accessible to the host\n"); return; } for (auto cNum : cNums) { if (cNum == Net_Arbitrator) { Printf("The host cannot change their own settings controller status\n"); } else if (!NetworkClients.InGame(cNum)) { Printf("Client %d is not in game\n", cNum); } else if (players[cNum].settings_controller && add) { Printf("Client %d is already a settings controller\n", cNum); } else if (!players[cNum].settings_controller && !add) { Printf("Client %d is already not a settings controller\n", cNum); } else { Net_WriteInt8(add ? DEM_ADDCONTROLLER : DEM_DELCONTROLLER); Net_WriteInt8(cNum); } } } //========================================================================== // // CCMD addsettingscontrollers // //========================================================================== CCMD(addsettingscontrollers) { if (argv.argc() < 2) { Printf("Usage: addsettingscontrollers \nAllow these clients to control game settings\n"); return; } TArray cNums = {}; for (size_t i = 1u; i < argv.argc(); ++i) { int cNum = -1; if (!C_IsValidInt(argv[i], cNum) || cNum < 0 || cNum >= MAXPLAYERS) Printf("Bad client number %s\n", argv[i]); else if (cNum != Net_Arbitrator && cNums.Find(cNum) >= cNums.Size()) cNums.Push(cNum); } Net_ChangeSettingsControllers(cNums, true); } //========================================================================== // // CCMD removesettingscontrollers // //========================================================================== CCMD(removesettingscontrollers) { if (argv.argc() < 2) { Printf("Usage: removesettingscontrollers \nRemove the ability for these clients to control game settings\n"); return; } TArray cNums = {}; for (size_t i = 1u; i < argv.argc(); ++i) { int cNum = -1; if (!C_IsValidInt(argv[i], cNum) || cNum < 0 || cNum >= MAXPLAYERS) Printf("Bad player number %s\n", argv[i]); else if (cNum != Net_Arbitrator && cNums.Find(cNum) >= cNums.Size()) cNums.Push(cNum); } Net_ChangeSettingsControllers(cNums, false); } //========================================================================== // // CCMD removeallsettingscontrollers // //========================================================================== CCMD(removeallsettingscontrollers) { TArray cNums = {}; for (auto client : NetworkClients) { if (client != Net_Arbitrator && players[client].settings_controller) cNums.Push(client); } Net_ChangeSettingsControllers(cNums, false); } //========================================================================== // // CCMD listsettingscontrollers // //========================================================================== CCMD(listsettingscontrollers) { if (!netgame) { Printf("This command can only be used when playing in a net game\n"); return; } TArray cNums = {}; for (auto client : NetworkClients) { if (client != Net_Arbitrator && players[client].settings_controller) cNums.Push(client); } if (!cNums.Size()) { Printf("No other settings controllers\n"); return; } Printf("The following players can change the game settings:\n"); for (auto cNum : cNums) { Printf("%d. %s", cNum, players[cNum].userinfo.GetName()); if (cNum == consoleplayer) Printf(" [*]"); Printf("\n"); } }