diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ee975bb1..ce37bfd65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -776,6 +776,8 @@ set_glob(GAME_SERVER GLOB_RECURSE src/game/server score/sql_score.h teams.cpp teams.h + teehistorian.cpp + teehistorian.h ) set(GAME_GENERATED_SERVER "src/game/generated/server_data.cpp" diff --git a/scripts/cmd5.py b/scripts/cmd5.py index f21f7bb83..2d085bf61 100644 --- a/scripts/cmd5.py +++ b/scripts/cmd5.py @@ -33,4 +33,4 @@ hash = hashlib.md5(f).hexdigest().lower()[16:] if hash == "3dc531e4296de555": hash = "626fce9a778df4d4" 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()) diff --git a/src/engine/server.h b/src/engine/server.h index 8e0a1791e..a642b459b 100644 --- a/src/engine/server.h +++ b/src/engine/server.h @@ -132,6 +132,8 @@ public: 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 SetClientClan(int ClientID, char const *pClan) = 0; virtual void SetClientCountry(int ClientID, int Country) = 0; diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index eaf41221f..dd76f209c 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -870,6 +870,13 @@ void CServer::SendRconType(int ClientID, bool UsernameReq) 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) { CMsgPacker Msg(NETMSG_MAP_CHANGE); diff --git a/src/engine/server/server.h b/src/engine/server/server.h index 4ce829495..291d8e59c 100644 --- a/src/engine/server/server.h +++ b/src/engine/server/server.h @@ -231,6 +231,7 @@ public: void SetRconCID(int ClientID); int GetAuthedState(int ClientID); + void GetMapInfo(char *pMapName, int MapNameSize, int *pMapSize, int *pMapCrc); int GetClientInfo(int ClientID, CClientInfo *pInfo); void GetClientAddr(int ClientID, char *pAddrStr, int Size); const char *ClientName(int ClientID); diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index d37c553a8..e6556c80f 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -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(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(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(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") diff --git a/src/engine/shared/snapshot.cpp b/src/engine/shared/snapshot.cpp index ea39b3130..35c8af147 100644 --- a/src/engine/shared/snapshot.cpp +++ b/src/engine/shared/snapshot.cpp @@ -129,7 +129,7 @@ static int GetItemIndexHashed(int Key, const CItemList *pHashlist) 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; while(Size) diff --git a/src/engine/shared/snapshot.h b/src/engine/shared/snapshot.h index c46a491e8..5f08a62c5 100644 --- a/src/engine/shared/snapshot.h +++ b/src/engine/shared/snapshot.h @@ -74,6 +74,7 @@ private: void UndiffItem(int *pPast, int *pDiff, int *pOut, int Size); public: + static int DiffItem(int *pPast, int *pCurrent, int *pOut, int Size); CSnapshotDelta(); int GetDataRate(int Index) { return m_aSnapshotDataRate[Index]; } int GetDataUpdates(int Index) { return m_aSnapshotDataUpdates[Index]; } diff --git a/src/engine/shared/storage.cpp b/src/engine/shared/storage.cpp index 8cbc12f89..bbe5b79ff 100644 --- a/src/engine/shared/storage.cpp +++ b/src/engine/shared/storage.cpp @@ -70,6 +70,7 @@ public: fs_makedir(GetPath(TYPE_SAVE, "demos/auto", 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, "teehistorian", aPath, sizeof(aPath))); } return m_NumPaths ? 0 : 1; diff --git a/src/engine/shared/uuid_manager.cpp b/src/engine/shared/uuid_manager.cpp index 6f6518d52..45c1222c0 100644 --- a/src/engine/shared/uuid_manager.cpp +++ b/src/engine/shared/uuid_manager.cpp @@ -11,6 +11,19 @@ static const CUuid TEEWORLDS_NAMESPACE = {{ 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) { md5_state_t Md5; diff --git a/src/engine/shared/uuid_manager.h b/src/engine/shared/uuid_manager.h index d677654bc..c3b4ced7c 100644 --- a/src/engine/shared/uuid_manager.h +++ b/src/engine/shared/uuid_manager.h @@ -21,6 +21,7 @@ struct CUuid bool operator!=(const CUuid &Other); }; +CUuid RandomUuid(); CUuid CalculateUuid(const char *pName); void FormatUuid(CUuid Uuid, char *pBuffer, unsigned BufferLength); diff --git a/src/game/server/ddracechat.cpp b/src/game/server/ddracechat.cpp index a835b0d97..7159f7ed1 100644 --- a/src/game/server/ddracechat.cpp +++ b/src/game/server/ddracechat.cpp @@ -41,7 +41,7 @@ void CGameContext::ConInfo(IConsole::IResult *pResult, void *pUserData) CGameContext *pSelf = (CGameContext *) pUserData; pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "info", "DDraceNetwork Mod. Version: " GAME_VERSION); -#if defined( GIT_SHORTREV_HASH ) +#if defined(GIT_SHORTREV_HASH) pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "info", "Git revision hash: " GIT_SHORTREV_HASH); #endif diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index 44ca5ab33..1a17ff52e 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -56,6 +56,7 @@ void CGameContext::Construct(int Resetting) } m_ChatResponseTargetID = -1; m_aDeleteTempfile[0] = 0; + m_TeeHistorianActive = false; } 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) { if(ClientID < 0 || ClientID >= MAX_CLIENTS || !m_apPlayers[ClientID]) @@ -586,6 +593,17 @@ void CGameContext::OnTick() // check tuning CheckPureTuning(); + if(m_TeeHistorianActive) + { + if(!m_TeeHistorian.Starting()) + { + m_TeeHistorian.EndInputs(); + m_TeeHistorian.EndTick(); + } + m_TeeHistorian.BeginTick(Server()->Tick()); + m_TeeHistorian.BeginPlayers(); + } + // copy tuning m_World.m_Core.m_Tuning[0] = m_Tuning; m_World.Tick(); @@ -593,6 +611,25 @@ void CGameContext::OnTick() //if(world.paused) // make sure that the game object always updates 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++) { if(m_apPlayers[i]) @@ -806,6 +843,13 @@ void CGameContext::OnClientDirectInput(int ClientID, void *pInput) { if(!m_World.m_Paused) 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) @@ -997,6 +1041,10 @@ void CGameContext::OnClientConnected(int ClientID) void CGameContext::OnClientDrop(int ClientID, const char *pReason) { + if(m_TeeHistorianActive) + { + io_flush(m_TeeHistorianFile); + } AbortVoteKickOnDisconnect(ClientID); m_apPlayers[ClientID]->OnDisconnect(pReason); delete m_apPlayers[ClientID]; @@ -2421,6 +2469,8 @@ void CGameContext::OnInit(/*class IKernel *pKernel*/) m_World.SetGameServer(this); m_Events.SetGameServer(this); + m_GameUuid = RandomUuid(); + DeleteTempfile(); //if(!data) // only load once @@ -2488,6 +2538,49 @@ void CGameContext::OnInit(/*class IKernel *pKernel*/) 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()->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); ((CGameControllerDDRace*)m_pController)->m_Teams.Reset(); @@ -2783,6 +2876,12 @@ void CGameContext::OnShutdown(bool FullShutdown) if (FullShutdown) Score()->OnShutdown(); + if(m_TeeHistorianActive) + { + m_TeeHistorian.Finish(); + io_close(m_TeeHistorianFile); + } + DeleteTempfile(); Console()->ResetServerGameSettings(); Collision()->Dest(); diff --git a/src/game/server/gamecontext.h b/src/game/server/gamecontext.h index 24aa018ac..0e879592a 100644 --- a/src/game/server/gamecontext.h +++ b/src/game/server/gamecontext.h @@ -14,6 +14,7 @@ #include "gamecontroller.h" #include "gameworld.h" #include "player.h" +#include "teehistorian.h" #include "score.h" #ifdef _MSC_VER @@ -61,6 +62,13 @@ class CGameContext : public IGameServer CTuningParams m_Tuning; 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 ConTuneReset(IConsole::IResult *pResult, void *pUserData); static void ConTuneDump(IConsole::IResult *pResult, void *pUserData); diff --git a/src/game/server/teehistorian.cpp b/src/game/server/teehistorian.cpp new file mode 100644 index 000000000..d410faf13 --- /dev/null +++ b/src/game/server/teehistorian.cpp @@ -0,0 +1,361 @@ +#include "teehistorian.h" + +#include + +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()); +} diff --git a/src/game/server/teehistorian.h b/src/game/server/teehistorian.h new file mode 100644 index 000000000..582471d54 --- /dev/null +++ b/src/game/server/teehistorian.h @@ -0,0 +1,90 @@ +#include +#include +#include + +#include + +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; +};