mirror of
https://github.com/ddnet/ddnet.git
synced 2024-11-10 01:58:19 +00:00
First working version of teehistorian
teehistorian records all inputs from the players as well as the player positions in each tick. It stores this info in a highly compressible output format (I've achived 5x compression using xz or bz2).
This commit is contained in:
parent
03fa475b5e
commit
6ef9c8dbcd
|
@ -776,6 +776,8 @@ set_glob(GAME_SERVER GLOB_RECURSE src/game/server
|
||||||
score/sql_score.h
|
score/sql_score.h
|
||||||
teams.cpp
|
teams.cpp
|
||||||
teams.h
|
teams.h
|
||||||
|
teehistorian.cpp
|
||||||
|
teehistorian.h
|
||||||
)
|
)
|
||||||
set(GAME_GENERATED_SERVER
|
set(GAME_GENERATED_SERVER
|
||||||
"src/game/generated/server_data.cpp"
|
"src/game/generated/server_data.cpp"
|
||||||
|
|
|
@ -33,4 +33,4 @@ hash = hashlib.md5(f).hexdigest().lower()[16:]
|
||||||
if hash == "3dc531e4296de555":
|
if hash == "3dc531e4296de555":
|
||||||
hash = "626fce9a778df4d4"
|
hash = "626fce9a778df4d4"
|
||||||
print('#define GAME_NETVERSION_HASH "%s"' % hash)
|
print('#define GAME_NETVERSION_HASH "%s"' % hash)
|
||||||
print('#define GIT_SHORTREV_HASH "%s"' % os.popen('git rev-parse HEAD').readline(8))
|
print('#define GIT_SHORTREV_HASH "%s"' % os.popen('git rev-parse --short HEAD').readline().strip())
|
||||||
|
|
|
@ -132,6 +132,8 @@ public:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual void GetMapInfo(char *pMapName, int MapNameSize, int *pMapSize, int *pMapCrc) = 0;
|
||||||
|
|
||||||
virtual void SetClientName(int ClientID, char const *pName) = 0;
|
virtual void SetClientName(int ClientID, char const *pName) = 0;
|
||||||
virtual void SetClientClan(int ClientID, char const *pClan) = 0;
|
virtual void SetClientClan(int ClientID, char const *pClan) = 0;
|
||||||
virtual void SetClientCountry(int ClientID, int Country) = 0;
|
virtual void SetClientCountry(int ClientID, int Country) = 0;
|
||||||
|
|
|
@ -870,6 +870,13 @@ void CServer::SendRconType(int ClientID, bool UsernameReq)
|
||||||
SendMsgEx(&Msg, MSGFLAG_VITAL, ClientID, true);
|
SendMsgEx(&Msg, MSGFLAG_VITAL, ClientID, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CServer::GetMapInfo(char *pMapName, int MapNameSize, int *pMapSize, int *pMapCrc)
|
||||||
|
{
|
||||||
|
str_copy(pMapName, GetMapName(), MapNameSize);
|
||||||
|
*pMapSize = m_CurrentMapSize;
|
||||||
|
*pMapCrc = m_CurrentMapCrc;
|
||||||
|
}
|
||||||
|
|
||||||
void CServer::SendMap(int ClientID)
|
void CServer::SendMap(int ClientID)
|
||||||
{
|
{
|
||||||
CMsgPacker Msg(NETMSG_MAP_CHANGE);
|
CMsgPacker Msg(NETMSG_MAP_CHANGE);
|
||||||
|
|
|
@ -231,6 +231,7 @@ public:
|
||||||
|
|
||||||
void SetRconCID(int ClientID);
|
void SetRconCID(int ClientID);
|
||||||
int GetAuthedState(int ClientID);
|
int GetAuthedState(int ClientID);
|
||||||
|
void GetMapInfo(char *pMapName, int MapNameSize, int *pMapSize, int *pMapCrc);
|
||||||
int GetClientInfo(int ClientID, CClientInfo *pInfo);
|
int GetClientInfo(int ClientID, CClientInfo *pInfo);
|
||||||
void GetClientAddr(int ClientID, char *pAddrStr, int Size);
|
void GetClientAddr(int ClientID, char *pAddrStr, int Size);
|
||||||
const char *ClientName(int ClientID);
|
const char *ClientName(int ClientID);
|
||||||
|
|
|
@ -154,6 +154,7 @@ MACRO_CONFIG_INT(SvRconMaxTries, sv_rcon_max_tries, 30, 0, 100, CFGFLAG_SERVER,
|
||||||
MACRO_CONFIG_INT(SvRconBantime, sv_rcon_bantime, 5, 0, 1440, CFGFLAG_SERVER, "The time a client gets banned if remote console authentication fails. 0 makes it just use kick")
|
MACRO_CONFIG_INT(SvRconBantime, sv_rcon_bantime, 5, 0, 1440, CFGFLAG_SERVER, "The time a client gets banned if remote console authentication fails. 0 makes it just use kick")
|
||||||
MACRO_CONFIG_INT(SvAutoDemoRecord, sv_auto_demo_record, 0, 0, 1, CFGFLAG_SERVER, "Automatically record demos")
|
MACRO_CONFIG_INT(SvAutoDemoRecord, sv_auto_demo_record, 0, 0, 1, CFGFLAG_SERVER, "Automatically record demos")
|
||||||
MACRO_CONFIG_INT(SvAutoDemoMax, sv_auto_demo_max, 10, 0, 1000, CFGFLAG_SERVER, "Maximum number of automatically recorded demos (0 = no limit)")
|
MACRO_CONFIG_INT(SvAutoDemoMax, sv_auto_demo_max, 10, 0, 1000, CFGFLAG_SERVER, "Maximum number of automatically recorded demos (0 = no limit)")
|
||||||
|
MACRO_CONFIG_INT(SvTeeHistorian, sv_tee_historian, 0, 0, 1, CFGFLAG_SERVER, "Activate the tee historian that writes complete gameplay data to disk (WARNING: This will use a lot of disk space)")
|
||||||
MACRO_CONFIG_INT(SvVanillaAntiSpoof, sv_vanilla_antispoof, 1, 0, 1, CFGFLAG_SERVER, "Enable vanilla Antispoof")
|
MACRO_CONFIG_INT(SvVanillaAntiSpoof, sv_vanilla_antispoof, 1, 0, 1, CFGFLAG_SERVER, "Enable vanilla Antispoof")
|
||||||
MACRO_CONFIG_INT(SvDnsbl, sv_dnsbl, 0, 0, 1, CFGFLAG_SERVER, "Enable DNSBL (DNS-based Blackhole List)")
|
MACRO_CONFIG_INT(SvDnsbl, sv_dnsbl, 0, 0, 1, CFGFLAG_SERVER, "Enable DNSBL (DNS-based Blackhole List)")
|
||||||
MACRO_CONFIG_STR(SvDnsblHost, sv_dnsbl_host, 128, "", CFGFLAG_SERVER, "Hostname of DNSBL provider to use for IP Verification")
|
MACRO_CONFIG_STR(SvDnsblHost, sv_dnsbl_host, 128, "", CFGFLAG_SERVER, "Hostname of DNSBL provider to use for IP Verification")
|
||||||
|
|
|
@ -129,7 +129,7 @@ static int GetItemIndexHashed(int Key, const CItemList *pHashlist)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int DiffItem(int *pPast, int *pCurrent, int *pOut, int Size)
|
int CSnapshotDelta::DiffItem(int *pPast, int *pCurrent, int *pOut, int Size)
|
||||||
{
|
{
|
||||||
int Needed = 0;
|
int Needed = 0;
|
||||||
while(Size)
|
while(Size)
|
||||||
|
|
|
@ -74,6 +74,7 @@ private:
|
||||||
void UndiffItem(int *pPast, int *pDiff, int *pOut, int Size);
|
void UndiffItem(int *pPast, int *pDiff, int *pOut, int Size);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
static int DiffItem(int *pPast, int *pCurrent, int *pOut, int Size);
|
||||||
CSnapshotDelta();
|
CSnapshotDelta();
|
||||||
int GetDataRate(int Index) { return m_aSnapshotDataRate[Index]; }
|
int GetDataRate(int Index) { return m_aSnapshotDataRate[Index]; }
|
||||||
int GetDataUpdates(int Index) { return m_aSnapshotDataUpdates[Index]; }
|
int GetDataUpdates(int Index) { return m_aSnapshotDataUpdates[Index]; }
|
||||||
|
|
|
@ -70,6 +70,7 @@ public:
|
||||||
fs_makedir(GetPath(TYPE_SAVE, "demos/auto", aPath, sizeof(aPath)));
|
fs_makedir(GetPath(TYPE_SAVE, "demos/auto", aPath, sizeof(aPath)));
|
||||||
fs_makedir(GetPath(TYPE_SAVE, "editor", aPath, sizeof(aPath)));
|
fs_makedir(GetPath(TYPE_SAVE, "editor", aPath, sizeof(aPath)));
|
||||||
fs_makedir(GetPath(TYPE_SAVE, "ghosts", aPath, sizeof(aPath)));
|
fs_makedir(GetPath(TYPE_SAVE, "ghosts", aPath, sizeof(aPath)));
|
||||||
|
fs_makedir(GetPath(TYPE_SAVE, "teehistorian", aPath, sizeof(aPath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return m_NumPaths ? 0 : 1;
|
return m_NumPaths ? 0 : 1;
|
||||||
|
|
|
@ -11,6 +11,19 @@ static const CUuid TEEWORLDS_NAMESPACE = {{
|
||||||
0xb6, 0x42, 0x5d, 0x48, 0xe8, 0x0c, 0x00, 0x29
|
0xb6, 0x42, 0x5d, 0x48, 0xe8, 0x0c, 0x00, 0x29
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
CUuid RandomUuid()
|
||||||
|
{
|
||||||
|
CUuid Result;
|
||||||
|
secure_random_fill(&Result, sizeof(Result));
|
||||||
|
|
||||||
|
Result.m_aData[6] &= 0x0f;
|
||||||
|
Result.m_aData[6] |= 0x40;
|
||||||
|
Result.m_aData[8] &= 0x3f;
|
||||||
|
Result.m_aData[8] |= 0x80;
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
CUuid CalculateUuid(const char *pName)
|
CUuid CalculateUuid(const char *pName)
|
||||||
{
|
{
|
||||||
md5_state_t Md5;
|
md5_state_t Md5;
|
||||||
|
|
|
@ -21,6 +21,7 @@ struct CUuid
|
||||||
bool operator!=(const CUuid &Other);
|
bool operator!=(const CUuid &Other);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
CUuid RandomUuid();
|
||||||
CUuid CalculateUuid(const char *pName);
|
CUuid CalculateUuid(const char *pName);
|
||||||
void FormatUuid(CUuid Uuid, char *pBuffer, unsigned BufferLength);
|
void FormatUuid(CUuid Uuid, char *pBuffer, unsigned BufferLength);
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ void CGameContext::ConInfo(IConsole::IResult *pResult, void *pUserData)
|
||||||
CGameContext *pSelf = (CGameContext *) pUserData;
|
CGameContext *pSelf = (CGameContext *) pUserData;
|
||||||
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "info",
|
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "info",
|
||||||
"DDraceNetwork Mod. Version: " GAME_VERSION);
|
"DDraceNetwork Mod. Version: " GAME_VERSION);
|
||||||
#if defined( GIT_SHORTREV_HASH )
|
#if defined(GIT_SHORTREV_HASH)
|
||||||
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "info",
|
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "info",
|
||||||
"Git revision hash: " GIT_SHORTREV_HASH);
|
"Git revision hash: " GIT_SHORTREV_HASH);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -56,6 +56,7 @@ void CGameContext::Construct(int Resetting)
|
||||||
}
|
}
|
||||||
m_ChatResponseTargetID = -1;
|
m_ChatResponseTargetID = -1;
|
||||||
m_aDeleteTempfile[0] = 0;
|
m_aDeleteTempfile[0] = 0;
|
||||||
|
m_TeeHistorianActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CGameContext::CGameContext(int Resetting)
|
CGameContext::CGameContext(int Resetting)
|
||||||
|
@ -100,6 +101,12 @@ void CGameContext::Clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void CGameContext::TeeHistorianWrite(const void *pData, int DataSize, void *pUser)
|
||||||
|
{
|
||||||
|
CGameContext *pSelf = (CGameContext *)pUser;
|
||||||
|
io_write(pSelf->m_TeeHistorianFile, pData, DataSize);
|
||||||
|
}
|
||||||
|
|
||||||
class CCharacter *CGameContext::GetPlayerChar(int ClientID)
|
class CCharacter *CGameContext::GetPlayerChar(int ClientID)
|
||||||
{
|
{
|
||||||
if(ClientID < 0 || ClientID >= MAX_CLIENTS || !m_apPlayers[ClientID])
|
if(ClientID < 0 || ClientID >= MAX_CLIENTS || !m_apPlayers[ClientID])
|
||||||
|
@ -586,6 +593,17 @@ void CGameContext::OnTick()
|
||||||
// check tuning
|
// check tuning
|
||||||
CheckPureTuning();
|
CheckPureTuning();
|
||||||
|
|
||||||
|
if(m_TeeHistorianActive)
|
||||||
|
{
|
||||||
|
if(!m_TeeHistorian.Starting())
|
||||||
|
{
|
||||||
|
m_TeeHistorian.EndInputs();
|
||||||
|
m_TeeHistorian.EndTick();
|
||||||
|
}
|
||||||
|
m_TeeHistorian.BeginTick(Server()->Tick());
|
||||||
|
m_TeeHistorian.BeginPlayers();
|
||||||
|
}
|
||||||
|
|
||||||
// copy tuning
|
// copy tuning
|
||||||
m_World.m_Core.m_Tuning[0] = m_Tuning;
|
m_World.m_Core.m_Tuning[0] = m_Tuning;
|
||||||
m_World.Tick();
|
m_World.Tick();
|
||||||
|
@ -593,6 +611,25 @@ void CGameContext::OnTick()
|
||||||
//if(world.paused) // make sure that the game object always updates
|
//if(world.paused) // make sure that the game object always updates
|
||||||
m_pController->Tick();
|
m_pController->Tick();
|
||||||
|
|
||||||
|
if(m_TeeHistorianActive)
|
||||||
|
{
|
||||||
|
for(int i = 0; i < MAX_CLIENTS; i++)
|
||||||
|
{
|
||||||
|
if(m_apPlayers[i] && m_apPlayers[i]->GetCharacter())
|
||||||
|
{
|
||||||
|
CNetObj_CharacterCore Char;
|
||||||
|
m_apPlayers[i]->GetCharacter()->GetCore().Write(&Char);
|
||||||
|
m_TeeHistorian.RecordPlayer(i, &Char);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_TeeHistorian.RecordDeadPlayer(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_TeeHistorian.EndPlayers();
|
||||||
|
m_TeeHistorian.BeginInputs();
|
||||||
|
}
|
||||||
|
|
||||||
for(int i = 0; i < MAX_CLIENTS; i++)
|
for(int i = 0; i < MAX_CLIENTS; i++)
|
||||||
{
|
{
|
||||||
if(m_apPlayers[i])
|
if(m_apPlayers[i])
|
||||||
|
@ -806,6 +843,13 @@ void CGameContext::OnClientDirectInput(int ClientID, void *pInput)
|
||||||
{
|
{
|
||||||
if(!m_World.m_Paused)
|
if(!m_World.m_Paused)
|
||||||
m_apPlayers[ClientID]->OnDirectInput((CNetObj_PlayerInput *)pInput);
|
m_apPlayers[ClientID]->OnDirectInput((CNetObj_PlayerInput *)pInput);
|
||||||
|
|
||||||
|
if(m_TeeHistorianActive)
|
||||||
|
{
|
||||||
|
// TODO: Only record when not in spectators. Be careful not to
|
||||||
|
// miss the first tick though.
|
||||||
|
m_TeeHistorian.RecordPlayerInput(ClientID, (CNetObj_PlayerInput *)pInput);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CGameContext::OnClientPredictedInput(int ClientID, void *pInput)
|
void CGameContext::OnClientPredictedInput(int ClientID, void *pInput)
|
||||||
|
@ -997,6 +1041,10 @@ void CGameContext::OnClientConnected(int ClientID)
|
||||||
|
|
||||||
void CGameContext::OnClientDrop(int ClientID, const char *pReason)
|
void CGameContext::OnClientDrop(int ClientID, const char *pReason)
|
||||||
{
|
{
|
||||||
|
if(m_TeeHistorianActive)
|
||||||
|
{
|
||||||
|
io_flush(m_TeeHistorianFile);
|
||||||
|
}
|
||||||
AbortVoteKickOnDisconnect(ClientID);
|
AbortVoteKickOnDisconnect(ClientID);
|
||||||
m_apPlayers[ClientID]->OnDisconnect(pReason);
|
m_apPlayers[ClientID]->OnDisconnect(pReason);
|
||||||
delete m_apPlayers[ClientID];
|
delete m_apPlayers[ClientID];
|
||||||
|
@ -2421,6 +2469,8 @@ void CGameContext::OnInit(/*class IKernel *pKernel*/)
|
||||||
m_World.SetGameServer(this);
|
m_World.SetGameServer(this);
|
||||||
m_Events.SetGameServer(this);
|
m_Events.SetGameServer(this);
|
||||||
|
|
||||||
|
m_GameUuid = RandomUuid();
|
||||||
|
|
||||||
DeleteTempfile();
|
DeleteTempfile();
|
||||||
|
|
||||||
//if(!data) // only load once
|
//if(!data) // only load once
|
||||||
|
@ -2488,6 +2538,49 @@ void CGameContext::OnInit(/*class IKernel *pKernel*/)
|
||||||
|
|
||||||
LoadMapSettings();
|
LoadMapSettings();
|
||||||
|
|
||||||
|
m_TeeHistorianActive = g_Config.m_SvTeeHistorian;
|
||||||
|
if(m_TeeHistorianActive)
|
||||||
|
{
|
||||||
|
char aGameUuid[UUID_MAXSTRSIZE];
|
||||||
|
FormatUuid(m_GameUuid, aGameUuid, sizeof(aGameUuid));
|
||||||
|
|
||||||
|
char aFilename[64];
|
||||||
|
str_format(aFilename, sizeof(aFilename), "teehistorian/%s.teehistorian", aGameUuid);
|
||||||
|
|
||||||
|
m_TeeHistorianFile = Kernel()->RequestInterface<IStorage>()->OpenFile(aFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
|
||||||
|
if(!m_TeeHistorianFile)
|
||||||
|
{
|
||||||
|
dbg_msg("teehistorian", "failed to open '%s'", aFilename);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dbg_msg("teehistorian", "recording to '%s'", aFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
char aVersion[128];
|
||||||
|
#ifdef GIT_SHORTREV_HASH
|
||||||
|
str_format(aVersion, sizeof(aVersion), "%s (%s)", GAME_VERSION, GIT_SHORTREV_HASH);
|
||||||
|
#else
|
||||||
|
str_format(aVersion, sizeof(aVersion), "%s", GAME_VERSION);
|
||||||
|
#endif
|
||||||
|
CTeeHistorian::CGameInfo GameInfo;
|
||||||
|
GameInfo.m_GameUuid = m_GameUuid;
|
||||||
|
GameInfo.m_pServerVersion = aVersion;
|
||||||
|
GameInfo.m_StartTime = time(0);
|
||||||
|
|
||||||
|
GameInfo.m_pServerName = g_Config.m_SvName;
|
||||||
|
GameInfo.m_ServerPort = g_Config.m_SvPort;
|
||||||
|
GameInfo.m_pGameType = "DDraceNetwork";
|
||||||
|
|
||||||
|
char aMapName[128];
|
||||||
|
Server()->GetMapInfo(aMapName, sizeof(aMapName), &GameInfo.m_MapSize, &GameInfo.m_MapCrc);
|
||||||
|
GameInfo.m_pMapName = aMapName;
|
||||||
|
|
||||||
|
m_TeeHistorian.Reset(&GameInfo, TeeHistorianWrite, this);
|
||||||
|
io_flush(m_TeeHistorianFile);
|
||||||
|
}
|
||||||
|
|
||||||
m_pController = new CGameControllerDDRace(this);
|
m_pController = new CGameControllerDDRace(this);
|
||||||
((CGameControllerDDRace*)m_pController)->m_Teams.Reset();
|
((CGameControllerDDRace*)m_pController)->m_Teams.Reset();
|
||||||
|
|
||||||
|
@ -2783,6 +2876,12 @@ void CGameContext::OnShutdown(bool FullShutdown)
|
||||||
if (FullShutdown)
|
if (FullShutdown)
|
||||||
Score()->OnShutdown();
|
Score()->OnShutdown();
|
||||||
|
|
||||||
|
if(m_TeeHistorianActive)
|
||||||
|
{
|
||||||
|
m_TeeHistorian.Finish();
|
||||||
|
io_close(m_TeeHistorianFile);
|
||||||
|
}
|
||||||
|
|
||||||
DeleteTempfile();
|
DeleteTempfile();
|
||||||
Console()->ResetServerGameSettings();
|
Console()->ResetServerGameSettings();
|
||||||
Collision()->Dest();
|
Collision()->Dest();
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
#include "gamecontroller.h"
|
#include "gamecontroller.h"
|
||||||
#include "gameworld.h"
|
#include "gameworld.h"
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
|
#include "teehistorian.h"
|
||||||
|
|
||||||
#include "score.h"
|
#include "score.h"
|
||||||
#ifdef _MSC_VER
|
#ifdef _MSC_VER
|
||||||
|
@ -61,6 +62,13 @@ class CGameContext : public IGameServer
|
||||||
CTuningParams m_Tuning;
|
CTuningParams m_Tuning;
|
||||||
CTuningParams m_aTuningList[NUM_TUNEZONES];
|
CTuningParams m_aTuningList[NUM_TUNEZONES];
|
||||||
|
|
||||||
|
bool m_TeeHistorianActive;
|
||||||
|
CTeeHistorian m_TeeHistorian;
|
||||||
|
IOHANDLE m_TeeHistorianFile;
|
||||||
|
CUuid m_GameUuid;
|
||||||
|
|
||||||
|
static void TeeHistorianWrite(const void *pData, int DataSize, void *pUser);
|
||||||
|
|
||||||
static void ConTuneParam(IConsole::IResult *pResult, void *pUserData);
|
static void ConTuneParam(IConsole::IResult *pResult, void *pUserData);
|
||||||
static void ConTuneReset(IConsole::IResult *pResult, void *pUserData);
|
static void ConTuneReset(IConsole::IResult *pResult, void *pUserData);
|
||||||
static void ConTuneDump(IConsole::IResult *pResult, void *pUserData);
|
static void ConTuneDump(IConsole::IResult *pResult, void *pUserData);
|
||||||
|
|
361
src/game/server/teehistorian.cpp
Normal file
361
src/game/server/teehistorian.cpp
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
#include "teehistorian.h"
|
||||||
|
|
||||||
|
#include <engine/shared/snapshot.h>
|
||||||
|
|
||||||
|
static const CUuid TEEHISTORIAN_UUID = CalculateUuid("teehistorian@ddnet.tw");
|
||||||
|
static const char TEEHISTORIAN_VERSION[] = "1";
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
TEEHISTORIAN_NONE,
|
||||||
|
TEEHISTORIAN_FINISH,
|
||||||
|
TEEHISTORIAN_TICK,
|
||||||
|
TEEHISTORIAN_PLAYER_NEW,
|
||||||
|
TEEHISTORIAN_PLAYER_OLD,
|
||||||
|
TEEHISTORIAN_INPUT_DIFF,
|
||||||
|
TEEHISTORIAN_INPUT_NEW,
|
||||||
|
};
|
||||||
|
|
||||||
|
static char EscapeJsonChar(char c)
|
||||||
|
{
|
||||||
|
switch(c)
|
||||||
|
{
|
||||||
|
case '\"': return '\"';
|
||||||
|
case '\\': return '\\';
|
||||||
|
case '\b': return 'b';
|
||||||
|
case '\n': return 'n';
|
||||||
|
case '\r': return 'r';
|
||||||
|
case '\t': return 't';
|
||||||
|
// Don't escape '\f', who uses that. :)
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *EscapeJson(char *pBuffer, int BufferSize, const char *pString)
|
||||||
|
{
|
||||||
|
dbg_assert(BufferSize > 0, "can't null-terminate the string");
|
||||||
|
// Subtract the space for null termination early.
|
||||||
|
BufferSize--;
|
||||||
|
|
||||||
|
char *pResult = pBuffer;
|
||||||
|
while(BufferSize && *pString)
|
||||||
|
{
|
||||||
|
char c = *pString;
|
||||||
|
pString++;
|
||||||
|
char Escaped = EscapeJsonChar(c);
|
||||||
|
if(Escaped)
|
||||||
|
{
|
||||||
|
if(BufferSize < 2)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
*pBuffer++ = '\\';
|
||||||
|
*pBuffer++ = Escaped;
|
||||||
|
BufferSize -= 2;
|
||||||
|
}
|
||||||
|
// Assuming ASCII/UTF-8, "if control character".
|
||||||
|
else if(c < 0x20)
|
||||||
|
{
|
||||||
|
// \uXXXX
|
||||||
|
if(BufferSize < 6)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
str_format(pBuffer, BufferSize, "\\u%04x", c);
|
||||||
|
BufferSize -= 6;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
*pBuffer++ = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*pBuffer = 0;
|
||||||
|
return pResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
CTeeHistorian::CTeeHistorian()
|
||||||
|
{
|
||||||
|
m_State = STATE_START;
|
||||||
|
m_pfnWriteCallback = 0;
|
||||||
|
m_pWriteCallbackUserdata = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::Reset(const CGameInfo *pGameInfo, WRITE_CALLBACK pfnWriteCallback, void *pUser)
|
||||||
|
{
|
||||||
|
dbg_assert(m_State == STATE_START || m_State == STATE_BEFORE_TICK, "invalid teehistorian state");
|
||||||
|
|
||||||
|
m_Debug = false;
|
||||||
|
|
||||||
|
m_Tick = 0;
|
||||||
|
m_LastWrittenTick = 0;
|
||||||
|
for(int i = 0; i < MAX_CLIENTS; i++)
|
||||||
|
{
|
||||||
|
m_aPrevPlayers[i].m_Alive = false;
|
||||||
|
m_aPrevPlayers[i].m_InputExists = false;
|
||||||
|
}
|
||||||
|
m_pfnWriteCallback = pfnWriteCallback;
|
||||||
|
m_pWriteCallbackUserdata = pUser;
|
||||||
|
|
||||||
|
WriteHeader(pGameInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::WriteHeader(const CGameInfo *pGameInfo)
|
||||||
|
{
|
||||||
|
Write(&TEEHISTORIAN_UUID, sizeof(TEEHISTORIAN_UUID));
|
||||||
|
|
||||||
|
char aGameUuid[UUID_MAXSTRSIZE];
|
||||||
|
char aStartTime[128];
|
||||||
|
|
||||||
|
FormatUuid(pGameInfo->m_GameUuid, aGameUuid, sizeof(aGameUuid));
|
||||||
|
str_timestamp_ex(pGameInfo->m_StartTime, aStartTime, sizeof(aStartTime), "%Y-%m-%d %H:%M:%S %z");
|
||||||
|
|
||||||
|
char aServerVersionBuffer[128];
|
||||||
|
char aStartTimeBuffer[128];
|
||||||
|
char aServerNameBuffer[128];
|
||||||
|
char aGameTypeBuffer[128];
|
||||||
|
char aMapNameBuffer[128];
|
||||||
|
|
||||||
|
char aJson[1024];
|
||||||
|
|
||||||
|
#define E(buf, str) EscapeJson(buf, sizeof(buf), str)
|
||||||
|
|
||||||
|
str_format(aJson, sizeof(aJson), "{\"version\":\"%s\",\"game_uuid\":\"%s\",\"server_version\":\"%s\",\"start_time\":\"%s\",\"server_name\":\"%s\",\"server_port\":%d,\"game_type\":\"%s\",\"map_name\":\"%s\",\"map_size\":%d,\"map_crc\":\"%08x\"}",
|
||||||
|
TEEHISTORIAN_VERSION,
|
||||||
|
aGameUuid,
|
||||||
|
E(aServerVersionBuffer, pGameInfo->m_pServerVersion),
|
||||||
|
E(aStartTimeBuffer, aStartTime),
|
||||||
|
E(aServerNameBuffer, pGameInfo->m_pServerName),
|
||||||
|
pGameInfo->m_ServerPort,
|
||||||
|
E(aGameTypeBuffer, pGameInfo->m_pGameType),
|
||||||
|
E(aMapNameBuffer, pGameInfo->m_pMapName),
|
||||||
|
pGameInfo->m_MapSize,
|
||||||
|
pGameInfo->m_MapCrc);
|
||||||
|
|
||||||
|
// Include null-termination.
|
||||||
|
Write(aJson, str_length(aJson) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::BeginTick(int Tick)
|
||||||
|
{
|
||||||
|
dbg_assert(m_State == STATE_START || m_State == STATE_BEFORE_TICK, "invalid teehistorian state");
|
||||||
|
|
||||||
|
m_Tick = Tick;
|
||||||
|
m_TickWritten = false;
|
||||||
|
|
||||||
|
if(m_Debug)
|
||||||
|
{
|
||||||
|
dbg_msg("teehistorian", "tick %d", Tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_State = STATE_BEFORE_PLAYERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::BeginPlayers()
|
||||||
|
{
|
||||||
|
dbg_assert(m_State == STATE_BEFORE_PLAYERS, "invalid teehistorian state");
|
||||||
|
|
||||||
|
m_Buffer.Reset();
|
||||||
|
m_PrevMaxClientID = m_MaxClientID;
|
||||||
|
m_MaxClientID = -1;
|
||||||
|
m_MinClientID = MAX_CLIENTS;
|
||||||
|
|
||||||
|
m_State = STATE_PLAYERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::UpdateMinMaxClientID(int ClientID)
|
||||||
|
{
|
||||||
|
if(ClientID > m_MaxClientID)
|
||||||
|
{
|
||||||
|
m_MaxClientID = ClientID;
|
||||||
|
}
|
||||||
|
if(ClientID < m_MinClientID)
|
||||||
|
{
|
||||||
|
m_MinClientID = ClientID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::RecordPlayer(int ClientID, const CNetObj_CharacterCore *pChar)
|
||||||
|
{
|
||||||
|
dbg_assert(m_State == STATE_PLAYERS, "invalid teehistorian state");
|
||||||
|
|
||||||
|
CPlayer *pPrev = &m_aPrevPlayers[ClientID];
|
||||||
|
if(!pPrev->m_Alive || pPrev->m_X != pChar->m_X || pPrev->m_Y != pChar->m_Y)
|
||||||
|
{
|
||||||
|
if(pPrev->m_Alive)
|
||||||
|
{
|
||||||
|
int dx = pChar->m_X - pPrev->m_X;
|
||||||
|
int dy = pChar->m_Y - pPrev->m_Y;
|
||||||
|
m_Buffer.AddInt(ClientID);
|
||||||
|
m_Buffer.AddInt(dx);
|
||||||
|
m_Buffer.AddInt(dy);
|
||||||
|
if(m_Debug)
|
||||||
|
{
|
||||||
|
dbg_msg("teehistorian", "diff cid=%d dx=%d dy=%d", ClientID, dx, dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int x = pChar->m_X;
|
||||||
|
int y = pChar->m_Y;
|
||||||
|
m_Buffer.AddInt(-TEEHISTORIAN_PLAYER_NEW);
|
||||||
|
m_Buffer.AddInt(ClientID);
|
||||||
|
m_Buffer.AddInt(x);
|
||||||
|
m_Buffer.AddInt(y);
|
||||||
|
if(m_Debug)
|
||||||
|
{
|
||||||
|
dbg_msg("teehistorian", "new cid=%d x=%d y=%d", ClientID, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateMinMaxClientID(ClientID);
|
||||||
|
}
|
||||||
|
pPrev->m_X = pChar->m_X;
|
||||||
|
pPrev->m_Y = pChar->m_Y;
|
||||||
|
pPrev->m_Alive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::RecordDeadPlayer(int ClientID)
|
||||||
|
{
|
||||||
|
dbg_assert(m_State == STATE_PLAYERS, "invalid teehistorian state");
|
||||||
|
|
||||||
|
CPlayer *pPrev = &m_aPrevPlayers[ClientID];
|
||||||
|
if(pPrev->m_Alive)
|
||||||
|
{
|
||||||
|
m_Buffer.AddInt(-TEEHISTORIAN_PLAYER_OLD);
|
||||||
|
m_Buffer.AddInt(ClientID);
|
||||||
|
if(m_Debug)
|
||||||
|
{
|
||||||
|
dbg_msg("teehistorian", "old cid=%d", ClientID);
|
||||||
|
}
|
||||||
|
UpdateMinMaxClientID(ClientID);
|
||||||
|
}
|
||||||
|
pPrev->m_Alive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::Write(const void *pData, int DataSize)
|
||||||
|
{
|
||||||
|
m_pfnWriteCallback(pData, DataSize, m_pWriteCallbackUserdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::WriteTick()
|
||||||
|
{
|
||||||
|
CPacker TickPacker;
|
||||||
|
TickPacker.Reset();
|
||||||
|
|
||||||
|
int dt = m_Tick - m_LastWrittenTick - 1;
|
||||||
|
TickPacker.AddInt(-TEEHISTORIAN_TICK);
|
||||||
|
TickPacker.AddInt(dt);
|
||||||
|
if(m_Debug)
|
||||||
|
{
|
||||||
|
dbg_msg("teehistorian", "before: skip_ticks dt=%d", dt);
|
||||||
|
}
|
||||||
|
Write(TickPacker.Data(), TickPacker.Size());
|
||||||
|
|
||||||
|
m_TickWritten = true;
|
||||||
|
m_LastWrittenTick = m_Tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::EndPlayers()
|
||||||
|
{
|
||||||
|
dbg_assert(m_State == STATE_PLAYERS, "invalid teehistorian state");
|
||||||
|
|
||||||
|
if(m_Buffer.Size())
|
||||||
|
{
|
||||||
|
if(!m_TickWritten && (m_MinClientID < m_PrevMaxClientID || m_LastWrittenTick + 1 != m_Tick))
|
||||||
|
{
|
||||||
|
WriteTick();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Tick is implicit.
|
||||||
|
m_LastWrittenTick = m_Tick;
|
||||||
|
m_TickWritten = true;
|
||||||
|
}
|
||||||
|
Write(m_Buffer.Data(), m_Buffer.Size());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_State = STATE_BEFORE_INPUTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::BeginInputs()
|
||||||
|
{
|
||||||
|
dbg_assert(m_State == STATE_BEFORE_INPUTS, "invalid teehistorian state");
|
||||||
|
m_Buffer.Reset();
|
||||||
|
m_State = STATE_INPUTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::RecordPlayerInput(int ClientID, const CNetObj_PlayerInput *pInput)
|
||||||
|
{
|
||||||
|
CPlayer *pPrev = &m_aPrevPlayers[ClientID];
|
||||||
|
CNetObj_PlayerInput DiffInput;
|
||||||
|
if(pPrev->m_InputExists)
|
||||||
|
{
|
||||||
|
if(mem_comp(&pPrev->m_Input, pInput, sizeof(pPrev->m_Input)) == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_Buffer.AddInt(-TEEHISTORIAN_INPUT_DIFF);
|
||||||
|
CSnapshotDelta::DiffItem((int *)&pPrev->m_Input, (int *)pInput, (int *)&DiffInput, sizeof(DiffInput) / sizeof(int));
|
||||||
|
if(m_Debug)
|
||||||
|
{
|
||||||
|
const int *pData = (const int *)&DiffInput;
|
||||||
|
dbg_msg("teehistorian", "diff_input cid=%d %d %d %d %d %d %d %d %d %d %d", ClientID,
|
||||||
|
pData[0], pData[1], pData[2], pData[3], pData[4],
|
||||||
|
pData[5], pData[6], pData[7], pData[8], pData[9]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_Buffer.AddInt(-TEEHISTORIAN_INPUT_NEW);
|
||||||
|
DiffInput = *pInput;
|
||||||
|
if(m_Debug)
|
||||||
|
{
|
||||||
|
dbg_msg("teehistorian", "new_input cid=%d", ClientID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_Buffer.AddInt(ClientID);
|
||||||
|
for(int i = 0; i < (int)(sizeof(DiffInput) / sizeof(int)); i++)
|
||||||
|
{
|
||||||
|
m_Buffer.AddInt(((int *)&DiffInput)[i]);
|
||||||
|
}
|
||||||
|
pPrev->m_InputExists = true;
|
||||||
|
pPrev->m_Input = *pInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::EndInputs()
|
||||||
|
{
|
||||||
|
dbg_assert(m_State == STATE_INPUTS, "invalid teehistorian state");
|
||||||
|
|
||||||
|
if(m_Buffer.Size())
|
||||||
|
{
|
||||||
|
if(!m_TickWritten)
|
||||||
|
{
|
||||||
|
WriteTick();
|
||||||
|
}
|
||||||
|
Write(m_Buffer.Data(), m_Buffer.Size());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_State = STATE_BEFORE_ENDTICK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::EndTick()
|
||||||
|
{
|
||||||
|
dbg_assert(m_State == STATE_BEFORE_ENDTICK, "invalid teehistorian state");
|
||||||
|
m_State = STATE_BEFORE_TICK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CTeeHistorian::Finish()
|
||||||
|
{
|
||||||
|
dbg_assert(m_State == STATE_START || m_State == STATE_INPUTS || m_State == STATE_BEFORE_ENDTICK, "invalid teehistorian state");
|
||||||
|
|
||||||
|
if(m_State == STATE_INPUTS)
|
||||||
|
{
|
||||||
|
EndInputs();
|
||||||
|
}
|
||||||
|
if(m_State == STATE_BEFORE_ENDTICK)
|
||||||
|
{
|
||||||
|
EndTick();
|
||||||
|
}
|
||||||
|
m_Buffer.Reset();
|
||||||
|
m_Buffer.AddInt(-TEEHISTORIAN_FINISH);
|
||||||
|
Write(m_Buffer.Data(), m_Buffer.Size());
|
||||||
|
}
|
90
src/game/server/teehistorian.h
Normal file
90
src/game/server/teehistorian.h
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
#include <engine/shared/packer.h>
|
||||||
|
#include <engine/shared/protocol.h>
|
||||||
|
#include <game/generated/protocol.h>
|
||||||
|
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
class CTeeHistorian
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
typedef void (*WRITE_CALLBACK)(const void *pData, int DataSize, void *pUser);
|
||||||
|
|
||||||
|
struct CGameInfo
|
||||||
|
{
|
||||||
|
CUuid m_GameUuid;
|
||||||
|
const char *m_pServerVersion;
|
||||||
|
time_t m_StartTime;
|
||||||
|
|
||||||
|
const char *m_pServerName;
|
||||||
|
int m_ServerPort;
|
||||||
|
const char *m_pGameType;
|
||||||
|
|
||||||
|
const char *m_pMapName;
|
||||||
|
int m_MapSize;
|
||||||
|
int m_MapCrc;
|
||||||
|
};
|
||||||
|
|
||||||
|
CTeeHistorian();
|
||||||
|
|
||||||
|
void Reset(const CGameInfo *pGameInfo, WRITE_CALLBACK pfnWriteCallback, void *pUser);
|
||||||
|
void Finish();
|
||||||
|
|
||||||
|
bool Starting() const { return m_State == STATE_START; }
|
||||||
|
|
||||||
|
void BeginTick(int Tick);
|
||||||
|
|
||||||
|
void BeginPlayers();
|
||||||
|
void RecordPlayer(int ClientID, const CNetObj_CharacterCore *pChar);
|
||||||
|
void RecordDeadPlayer(int ClientID);
|
||||||
|
void EndPlayers();
|
||||||
|
|
||||||
|
void BeginInputs();
|
||||||
|
void RecordPlayerInput(int ClientID, const CNetObj_PlayerInput *pInput);
|
||||||
|
void EndInputs();
|
||||||
|
|
||||||
|
void EndTick();
|
||||||
|
|
||||||
|
bool m_Debug;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void WriteHeader(const CGameInfo *pGameInfo);
|
||||||
|
void UpdateMinMaxClientID(int ClientID);
|
||||||
|
void WriteTick();
|
||||||
|
void Write(const void *pData, int DataSize);
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
STATE_START,
|
||||||
|
STATE_BEFORE_TICK,
|
||||||
|
STATE_BEFORE_PLAYERS,
|
||||||
|
STATE_PLAYERS,
|
||||||
|
STATE_BEFORE_INPUTS,
|
||||||
|
STATE_INPUTS,
|
||||||
|
STATE_BEFORE_ENDTICK,
|
||||||
|
NUM_STATES,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CPlayer
|
||||||
|
{
|
||||||
|
bool m_Alive;
|
||||||
|
int m_X;
|
||||||
|
int m_Y;
|
||||||
|
|
||||||
|
CNetObj_PlayerInput m_Input;
|
||||||
|
bool m_InputExists;
|
||||||
|
};
|
||||||
|
|
||||||
|
WRITE_CALLBACK m_pfnWriteCallback;
|
||||||
|
void *m_pWriteCallbackUserdata;
|
||||||
|
|
||||||
|
int m_State;
|
||||||
|
|
||||||
|
int m_LastWrittenTick;
|
||||||
|
bool m_TickWritten;
|
||||||
|
int m_Tick;
|
||||||
|
int m_PrevMaxClientID;
|
||||||
|
int m_MaxClientID;
|
||||||
|
int m_MinClientID;
|
||||||
|
CPlayer m_aPrevPlayers[MAX_CLIENTS];
|
||||||
|
CPacker m_Buffer;
|
||||||
|
};
|
Loading…
Reference in a new issue