diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index 7d241b202..850d55995 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -2775,10 +2775,21 @@ int CServer::Run() GameServer()->OnPreTickTeehistorian(); for(int c = 0; c < MAX_CLIENTS; c++) - if(m_aClients[c].m_State == CClient::STATE_INGAME) - for(auto &Input : m_aClients[c].m_aInputs) - if(Input.m_GameTick == Tick() + 1) - GameServer()->OnClientPredictedEarlyInput(c, Input.m_aData); + { + if(m_aClients[c].m_State != CClient::STATE_INGAME) + continue; + bool ClientHadInput = false; + for(auto &Input : m_aClients[c].m_aInputs) + { + if(Input.m_GameTick == Tick() + 1) + { + GameServer()->OnClientPredictedEarlyInput(c, Input.m_aData); + ClientHadInput = true; + } + } + if(!ClientHadInput) + GameServer()->OnClientPredictedEarlyInput(c, nullptr); + } m_CurrentGameTick++; NewTicks++; @@ -2788,14 +2799,18 @@ int CServer::Run() { if(m_aClients[c].m_State != CClient::STATE_INGAME) continue; + bool ClientHadInput = false; for(auto &Input : m_aClients[c].m_aInputs) { if(Input.m_GameTick == Tick()) { GameServer()->OnClientPredictedInput(c, Input.m_aData); + ClientHadInput = true; break; } } + if(!ClientHadInput) + GameServer()->OnClientPredictedInput(c, nullptr); } GameServer()->OnTick(); diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index 0ab7be3a5..5d6e277f1 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -3,6 +3,7 @@ #include #include +#include "base/system.h" #include "gamecontext.h" #include "teeinfo.h" #include @@ -73,6 +74,9 @@ void CGameContext::Construct(int Resetting) for(auto &pPlayer : m_apPlayers) pPlayer = 0; + mem_zero(&m_aLastPlayerInput, sizeof(m_aLastPlayerInput)); + mem_zero(&m_aPlayerHasInput, sizeof(m_aPlayerHasInput)); + m_pController = 0; m_aVoteCommand[0] = 0; m_VoteType = VOTE_TYPE_UNKNOWN; @@ -1153,18 +1157,49 @@ void CGameContext::OnClientDirectInput(int ClientID, void *pInput) void CGameContext::OnClientPredictedInput(int ClientID, void *pInput) { + // early return if no input at all has been sent by a player + if(pInput == nullptr && !m_aPlayerHasInput[ClientID]) + return; + + // set to last sent input when no new input has been sent + CNetObj_PlayerInput *pApplyInput = (CNetObj_PlayerInput *)pInput; + if(pApplyInput == nullptr) + { + pApplyInput = &m_aLastPlayerInput[ClientID]; + } + if(!m_World.m_Paused) - m_apPlayers[ClientID]->OnPredictedInput((CNetObj_PlayerInput *)pInput); + m_apPlayers[ClientID]->OnPredictedInput(pApplyInput); } void CGameContext::OnClientPredictedEarlyInput(int ClientID, void *pInput) { + // early return if no input at all has been sent by a player + if(pInput == nullptr && !m_aPlayerHasInput[ClientID]) + return; + + // set to last sent input when no new input has been sent + CNetObj_PlayerInput *pApplyInput = (CNetObj_PlayerInput *)pInput; + if(pApplyInput == nullptr) + { + pApplyInput = &m_aLastPlayerInput[ClientID]; + } + else + { + // Store input in this function and not in `OnClientPredictedInput`, + // because this function is called on all inputs, while + // `OnClientPredictedInput` is only called on the first input of each + // tick. + mem_copy(&m_aLastPlayerInput[ClientID], pApplyInput, sizeof(m_aLastPlayerInput[ClientID])); + m_aPlayerHasInput[ClientID] = true; + } + if(!m_World.m_Paused) - m_apPlayers[ClientID]->OnPredictedEarlyInput((CNetObj_PlayerInput *)pInput); + m_apPlayers[ClientID]->OnPredictedEarlyInput(pApplyInput); if(m_TeeHistorianActive) { - m_TeeHistorian.RecordPlayerInput(ClientID, (CNetObj_PlayerInput *)pInput); + m_TeeHistorian.RecordPlayerInput(ClientID, m_apPlayers[ClientID]->GetUniqueCID(), pApplyInput); } } @@ -1348,6 +1383,8 @@ void CGameContext::OnClientEnter(int ClientID) Server()->ExpireServerInfo(); CPlayer *pNewPlayer = m_apPlayers[ClientID]; + mem_zero(&m_aLastPlayerInput[ClientID], sizeof(m_aLastPlayerInput[ClientID])); + m_aPlayerHasInput[ClientID] = false; // new info for others protocol7::CNetMsg_Sv_ClientInfo NewClientInfoMsg; @@ -1457,7 +1494,8 @@ void CGameContext::OnClientConnected(int ClientID, void *pData) if(m_apPlayers[ClientID]) delete m_apPlayers[ClientID]; - m_apPlayers[ClientID] = new(ClientID) CPlayer(this, ClientID, StartTeam); + m_apPlayers[ClientID] = new(ClientID) CPlayer(this, NextUniqueClientID, ClientID, StartTeam); + NextUniqueClientID += 1; #ifdef CONF_DEBUG if(g_Config.m_DbgDummies) diff --git a/src/game/server/gamecontext.h b/src/game/server/gamecontext.h index a41dab550..c311e3704 100644 --- a/src/game/server/gamecontext.h +++ b/src/game/server/gamecontext.h @@ -15,6 +15,7 @@ #include "eventhandler.h" //#include "gamecontroller.h" +#include "game/generated/protocol.h" #include "gameworld.h" #include "teehistorian.h" @@ -149,6 +150,9 @@ public: CEventHandler m_Events; CPlayer *m_apPlayers[MAX_CLIENTS]; + // keep last input to always apply when none is sent + CNetObj_PlayerInput m_aLastPlayerInput[MAX_CLIENTS]; + bool m_aPlayerHasInput[MAX_CLIENTS]; IGameController *m_pController; CGameWorld m_World; @@ -295,6 +299,8 @@ public: std::shared_ptr m_SqlRandomMapResult; private: + // starting 1 to make 0 the special value "no client id" + uint32_t NextUniqueClientID = 1; bool m_VoteWillPass; class CScore *m_pScore; diff --git a/src/game/server/player.cpp b/src/game/server/player.cpp index 11f895df7..250aa6416 100644 --- a/src/game/server/player.cpp +++ b/src/game/server/player.cpp @@ -3,6 +3,7 @@ #include "player.h" #include +#include "base/system.h" #include "entities/character.h" #include "gamecontext.h" #include @@ -13,7 +14,8 @@ MACRO_ALLOC_POOL_ID_IMPL(CPlayer, MAX_CLIENTS) IServer *CPlayer::Server() const { return m_pGameServer->Server(); } -CPlayer::CPlayer(CGameContext *pGameServer, int ClientID, int Team) +CPlayer::CPlayer(CGameContext *pGameServer, uint32_t UniqueClientID, int ClientID, int Team) : + m_UniqueClientID(UniqueClientID) { m_pGameServer = pGameServer; m_ClientID = ClientID; diff --git a/src/game/server/player.h b/src/game/server/player.h index ea96512d0..37197184e 100644 --- a/src/game/server/player.h +++ b/src/game/server/player.h @@ -22,7 +22,7 @@ class CPlayer MACRO_ALLOC_POOL_ID() public: - CPlayer(CGameContext *pGameServer, int ClientID, int Team); + CPlayer(CGameContext *pGameServer, uint32_t UniqueClientID, int ClientID, int Team); ~CPlayer(); void Reset(); @@ -33,6 +33,7 @@ public: void SetTeam(int Team, bool DoChatMsg = true); int GetTeam() const { return m_Team; } int GetCID() const { return m_ClientID; } + uint32_t GetUniqueCID() const { return m_UniqueClientID; } int GetClientVersion() const; bool SetTimerType(int TimerType); @@ -113,6 +114,7 @@ public: } m_Latency; private: + const uint32_t m_UniqueClientID; CCharacter *m_pCharacter; int m_NumInputs; CGameContext *m_pGameServer; diff --git a/src/game/server/teehistorian.cpp b/src/game/server/teehistorian.cpp index aa09868da..f9b53c958 100644 --- a/src/game/server/teehistorian.cpp +++ b/src/game/server/teehistorian.cpp @@ -53,7 +53,8 @@ void CTeeHistorian::Reset(const CGameInfo *pGameInfo, WRITE_CALLBACK pfnWriteCal for(auto &PrevPlayer : m_aPrevPlayers) { PrevPlayer.m_Alive = false; - PrevPlayer.m_InputExists = false; + // zero means no id + PrevPlayer.m_UniqueClientID = 0; PrevPlayer.m_Team = 0; } for(auto &PrevTeam : m_aPrevTeams) @@ -256,7 +257,7 @@ void CTeeHistorian::RecordPlayer(int ClientID, const CNetObj_CharacterCore *pCha { dbg_assert(m_State == STATE_PLAYERS, "invalid teehistorian state"); - CPlayer *pPrev = &m_aPrevPlayers[ClientID]; + CTeehistorianPlayer *pPrev = &m_aPrevPlayers[ClientID]; if(!pPrev->m_Alive || pPrev->m_X != pChar->m_X || pPrev->m_Y != pChar->m_Y) { EnsureTickWrittenPlayerData(ClientID); @@ -299,7 +300,7 @@ void CTeeHistorian::RecordDeadPlayer(int ClientID) { dbg_assert(m_State == STATE_PLAYERS, "invalid teehistorian state"); - CPlayer *pPrev = &m_aPrevPlayers[ClientID]; + CTeehistorianPlayer *pPrev = &m_aPrevPlayers[ClientID]; if(pPrev->m_Alive) { EnsureTickWrittenPlayerData(ClientID); @@ -406,13 +407,13 @@ void CTeeHistorian::BeginInputs() m_State = STATE_INPUTS; } -void CTeeHistorian::RecordPlayerInput(int ClientID, const CNetObj_PlayerInput *pInput) +void CTeeHistorian::RecordPlayerInput(int ClientID, uint32_t UniqueClientID, const CNetObj_PlayerInput *pInput) { CPacker Buffer; - CPlayer *pPrev = &m_aPrevPlayers[ClientID]; + CTeehistorianPlayer *pPrev = &m_aPrevPlayers[ClientID]; CNetObj_PlayerInput DiffInput; - if(pPrev->m_InputExists) + if(pPrev->m_UniqueClientID == UniqueClientID) { if(mem_comp(&pPrev->m_Input, pInput, sizeof(pPrev->m_Input)) == 0) { @@ -447,7 +448,7 @@ void CTeeHistorian::RecordPlayerInput(int ClientID, const CNetObj_PlayerInput *p { Buffer.AddInt(((int *)&DiffInput)[i]); } - pPrev->m_InputExists = true; + pPrev->m_UniqueClientID = UniqueClientID; pPrev->m_Input = *pInput; Write(Buffer.Data(), Buffer.Size()); diff --git a/src/game/server/teehistorian.h b/src/game/server/teehistorian.h index 1ccd0cd41..397641c74 100644 --- a/src/game/server/teehistorian.h +++ b/src/game/server/teehistorian.h @@ -12,6 +12,7 @@ class CConfig; class CTuningParams; class CUuidManager; +class CPlayer; class CTeeHistorian { @@ -62,7 +63,7 @@ public: void EndPlayers(); void BeginInputs(); - void RecordPlayerInput(int ClientID, const CNetObj_PlayerInput *pInput); + void RecordPlayerInput(int ClientID, uint32_t UniqueClientID, const CNetObj_PlayerInput *pInput); void RecordPlayerMessage(int ClientID, const void *pMsg, int MsgSize); void RecordPlayerJoin(int ClientID, int Protocol); void RecordPlayerReady(int ClientID); @@ -106,14 +107,14 @@ private: NUM_STATES, }; - struct CPlayer + struct CTeehistorianPlayer { bool m_Alive; int m_X; int m_Y; CNetObj_PlayerInput m_Input; - bool m_InputExists; + uint32_t m_UniqueClientID; // DDNet team int m_Team; @@ -134,7 +135,7 @@ private: int m_Tick; int m_PrevMaxClientID; int m_MaxClientID; - CPlayer m_aPrevPlayers[MAX_CLIENTS]; + CTeehistorianPlayer m_aPrevPlayers[MAX_CLIENTS]; CTeam m_aPrevTeams[MAX_CLIENTS]; }; diff --git a/src/test/teehistorian.cpp b/src/test/teehistorian.cpp index c9caac3f5..181af189c 100644 --- a/src/test/teehistorian.cpp +++ b/src/test/teehistorian.cpp @@ -463,6 +463,47 @@ TEST_F(TeeHistorian, JoinLeave) Expect(EXPECTED, sizeof(EXPECTED)); } +TEST_F(TeeHistorian, Input) +{ + CNetObj_PlayerInput Input = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + const unsigned char EXPECTED[] = { + // TICK_SKIP dt=0 + 0x41, 0x00, + // new player -> InputNew + 0x45, + 0x00, // ClientID 0 + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, + // same unique id, same input -> nothing + // same unique id, different input -> InputDiff + 0x44, + 0x00, // ClientID 0 + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // different unique id, same input -> InputNew + 0x45, + 0x00, // ClientID 0 + 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, + // FINISH + 0x40}; + + Tick(1); + + // new player -> InputNew + m_TH.RecordPlayerInput(0, 1, &Input); + // same unique id, same input -> nothing + m_TH.RecordPlayerInput(0, 1, &Input); + + Input.m_Direction = 0; + + // same unique id, different input -> InputDiff + m_TH.RecordPlayerInput(0, 1, &Input); + + // different unique id, same input -> InputNew + m_TH.RecordPlayerInput(0, 2, &Input); + + Finish(); + Expect(EXPECTED, sizeof(EXPECTED)); +} + TEST_F(TeeHistorian, SaveSuccess) { const unsigned char EXPECTED[] = {