ddnet/src/game/client/components/race_demo.cpp
heinrich5991 f31e081bd4 Remove all checking for the gametype in the game
OK, maybe not actually remove because it is kept for fallback when the
new method isn't available.

The whole gametype parsing business had the same downsides as user agent
parsing on the web, hence I removed it while keeping behavior the same.

This allows servers to explicitly opt in or out of certain bug
workarounds and other client behavior. This increases the complexity of
different configurations that are available in the client (which is a
bad thing).
2019-06-14 00:28:59 +02:00

229 lines
6.6 KiB
C++

/* (c) Redix and Sushi */
#include <ctype.h>
#include <base/system.h>
#include <engine/shared/config.h>
#include <engine/serverbrowser.h>
#include <engine/storage.h>
#include <game/client/race.h>
#include "race_demo.h"
const char *CRaceDemo::ms_pRaceDemoDir = "demos/auto/race";
struct CDemoItem
{
char m_aName[128];
int m_Time;
};
struct CDemoListParam
{
std::vector<CDemoItem> *m_plDemos;
const char *pMap;
};
CRaceDemo::CRaceDemo() : m_RaceState(RACE_NONE), m_RaceStartTick(-1), m_RecordStopTick(-1), m_Time(0) {}
void CRaceDemo::GetPath(char *pBuf, int Size, int Time) const
{
const char *pMap = Client()->GetCurrentMap();
char aPlayerName[MAX_NAME_LENGTH];
str_copy(aPlayerName, g_Config.m_PlayerName, sizeof(aPlayerName));
str_sanitize_filename(aPlayerName);
if(Time < 0)
str_format(pBuf, Size, "%s/%s_tmp_%d.demo", ms_pRaceDemoDir, pMap, pid());
else if(g_Config.m_ClDemoName)
str_format(pBuf, Size, "%s/%s_%d.%03d_%s.demo", ms_pRaceDemoDir, pMap, Time / 1000, Time % 1000, aPlayerName);
else
str_format(pBuf, Size, "%s/%s_%d.%03d.demo", ms_pRaceDemoDir, pMap, Time / 1000, Time % 1000);
}
void CRaceDemo::OnStateChange(int NewState, int OldState)
{
if(OldState == IClient::STATE_ONLINE)
StopRecord();
}
void CRaceDemo::OnNewSnapshot()
{
if(!GameClient()->m_GameInfo.m_Race || !g_Config.m_ClAutoRaceRecord || Client()->State() != IClient::STATE_ONLINE)
return;
if(!m_pClient->m_Snap.m_pGameInfoObj || m_pClient->m_Snap.m_SpecInfo.m_Active || !m_pClient->m_Snap.m_pLocalCharacter || !m_pClient->m_Snap.m_pLocalPrevCharacter)
return;
static int s_LastRaceTick = -1;
bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags&GAMESTATEFLAG_RACETIME;
bool ServerControl = RaceFlag && g_Config.m_ClRaceRecordServerControl;
int RaceTick = -m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer;
// start the demo
bool ForceStart = ServerControl && s_LastRaceTick != RaceTick && Client()->GameTick() - RaceTick < Client()->GameTickSpeed();
bool AllowRestart = (m_AllowRestart || ForceStart) && m_RaceStartTick + 10 * Client()->GameTickSpeed() < Client()->GameTick();
if(m_RaceState == RACE_IDLE || m_RaceState == RACE_PREPARE || (m_RaceState == RACE_STARTED && AllowRestart))
{
vec2 PrevPos = vec2(m_pClient->m_Snap.m_pLocalPrevCharacter->m_X, m_pClient->m_Snap.m_pLocalPrevCharacter->m_Y);
vec2 Pos = vec2(m_pClient->m_Snap.m_pLocalCharacter->m_X, m_pClient->m_Snap.m_pLocalCharacter->m_Y);
if(ForceStart || (!ServerControl && CRaceHelper::IsStart(m_pClient, PrevPos, Pos)))
{
if(m_RaceState == RACE_STARTED)
Client()->RaceRecord_Stop();
if(m_RaceState != RACE_PREPARE) // start recording again
{
GetPath(m_aTmpFilename, sizeof(m_aTmpFilename));
Client()->RaceRecord_Start(m_aTmpFilename);
}
m_RaceStartTick = Client()->GameTick();
m_RaceState = RACE_STARTED;
}
}
// start recording before the player passes the start line, so we can see some preparation steps
if(m_RaceState == RACE_NONE)
{
GetPath(m_aTmpFilename, sizeof(m_aTmpFilename));
Client()->RaceRecord_Start(m_aTmpFilename);
m_RaceStartTick = Client()->GameTick();
m_RaceState = RACE_PREPARE;
}
// stop recording if the player did not pass the start line after 20 seconds
if(m_RaceState == RACE_PREPARE && Client()->GameTick() - m_RaceStartTick >= Client()->GameTickSpeed() * 20)
{
StopRecord();
m_RaceState = RACE_IDLE;
}
// stop the demo
if(m_RaceState == RACE_FINISHED && m_RecordStopTick <= Client()->GameTick())
StopRecord(m_Time);
s_LastRaceTick = RaceFlag ? RaceTick : -1;
}
void CRaceDemo::OnReset()
{
StopRecord();
}
void CRaceDemo::OnMessage(int MsgType, void *pRawMsg)
{
// check for messages from server
if(MsgType == NETMSGTYPE_SV_KILLMSG)
{
CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalClientID && Client()->RaceRecord_IsRecording())
StopRecord(m_Time);
}
else if(MsgType == NETMSGTYPE_SV_CHAT)
{
CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
if(pMsg->m_ClientID == -1 && m_RaceState == RACE_STARTED)
{
char aName[MAX_NAME_LENGTH];
int Time = CRaceHelper::TimeFromFinishMessage(pMsg->m_pMessage, aName, sizeof(aName));
if(Time > 0 && str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID].m_aName) == 0)
{
m_RaceState = RACE_FINISHED;
m_RecordStopTick = Client()->GameTick() + Client()->GameTickSpeed();
m_Time = Time;
}
}
}
}
void CRaceDemo::OnMapLoad()
{
m_AllowRestart = false;
}
void CRaceDemo::StopRecord(int Time)
{
if(Client()->RaceRecord_IsRecording())
Client()->RaceRecord_Stop();
if(m_aTmpFilename[0] != '\0')
{
if(Time > 0 && CheckDemo(Time))
{
// save file
char aNewFilename[512];
GetPath(aNewFilename, sizeof(aNewFilename), m_Time);
Storage()->RenameFile(m_aTmpFilename, aNewFilename, IStorage::TYPE_SAVE);
}
else // no new record
Storage()->RemoveFile(m_aTmpFilename, IStorage::TYPE_SAVE);
m_aTmpFilename[0] = '\0';
}
m_Time = 0;
m_RaceState = RACE_NONE;
m_RaceStartTick = -1;
m_RecordStopTick = -1;
}
int CRaceDemo::RaceDemolistFetchCallback(const char *pName, time_t Date, int IsDir, int StorageType, void *pUser)
{
CDemoListParam *pParam = (CDemoListParam*) pUser;
int Length = str_length(pName);
int MapLen = str_length(pParam->pMap);
if(IsDir || !str_endswith(pName, ".demo") || !str_startswith(pName, pParam->pMap) || pName[MapLen] != '_')
return 0;
CDemoItem Item;
str_copy(Item.m_aName, pName, minimum(static_cast<int>(sizeof(Item.m_aName)), Length - 4));
const char *pTime = Item.m_aName + MapLen + 1;
const char *pTEnd = pTime;
while(isdigit(*pTEnd) || *pTEnd == ' ' || *pTEnd == '.' || *pTEnd == ',')
pTEnd++;
if(g_Config.m_ClDemoName)
{
char aPlayerName[MAX_NAME_LENGTH];
str_copy(aPlayerName, g_Config.m_PlayerName, sizeof(aPlayerName));
str_sanitize_filename(aPlayerName);
if(pTEnd[0] != '_' || str_comp(pTEnd + 1, aPlayerName) != 0)
return 0;
}
else if(pTEnd[0])
return 0;
Item.m_Time = CRaceHelper::TimeFromSecondsStr(pTime);
if(Item.m_Time > 0)
pParam->m_plDemos->push_back(Item);
return 0;
}
bool CRaceDemo::CheckDemo(int Time) const
{
std::vector<CDemoItem> lDemos;
CDemoListParam Param = { &lDemos, Client()->GetCurrentMap() };
Storage()->ListDirectoryInfo(IStorage::TYPE_SAVE, ms_pRaceDemoDir, RaceDemolistFetchCallback, &Param);
// loop through demo files
for(unsigned i = 0; i < lDemos.size(); i++)
{
if(Time >= lDemos[i].m_Time) // found a better demo
return false;
// delete old demo
char aFilename[512];
str_format(aFilename, sizeof(aFilename), "%s/%s.demo", ms_pRaceDemoDir, lDemos[i].m_aName);
Storage()->RemoveFile(aFilename, IStorage::TYPE_SAVE);
}
return true;
}