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:
heinrich5991 2017-09-12 14:58:44 +02:00
parent 03fa475b5e
commit 6ef9c8dbcd
16 changed files with 590 additions and 3 deletions

View file

@ -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"

View file

@ -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())

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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")

View file

@ -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)

View file

@ -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]; }

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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

View file

@ -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();

View file

@ -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);

View 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());
}

View 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;
};