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[] = {