1/* (c) Redix and Sushi */
2
3#include <cctype>
4
5#include <base/system.h>
6#include <engine/shared/config.h>
7#include <engine/storage.h>
8
9#include <game/client/race.h>
10#include <game/localization.h>
11
12#include "race_demo.h"
13
14#include <game/client/gameclient.h>
15
16#include <chrono>
17
18using namespace std::chrono_literals;
19
20const char *CRaceDemo::ms_pRaceDemoDir = "demos/auto/race";
21
22struct CDemoItem
23{
24 char m_aName[128];
25 int m_Time;
26};
27
28struct CDemoListParam
29{
30 const CRaceDemo *m_pThis;
31 std::vector<CDemoItem> *m_pvDemos;
32 const char *pMap;
33};
34
35CRaceDemo::CRaceDemo() :
36 m_RaceState(RACE_NONE), m_RaceStartTick(-1), m_RecordStopTick(-1), m_Time(0) {}
37
38void CRaceDemo::GetPath(char *pBuf, int Size, int Time) const
39{
40 const char *pMap = Client()->GetCurrentMap();
41
42 char aPlayerName[MAX_NAME_LENGTH];
43 str_copy(dst&: aPlayerName, src: Client()->PlayerName());
44 str_sanitize_filename(str: aPlayerName);
45
46 if(Time < 0)
47 str_format(buffer: pBuf, buffer_size: Size, format: "%s/%s_tmp_%d.demo", ms_pRaceDemoDir, pMap, pid());
48 else if(g_Config.m_ClDemoName)
49 str_format(buffer: pBuf, buffer_size: Size, format: "%s/%s_%d.%03d_%s.demo", ms_pRaceDemoDir, pMap, Time / 1000, Time % 1000, aPlayerName);
50 else
51 str_format(buffer: pBuf, buffer_size: Size, format: "%s/%s_%d.%03d.demo", ms_pRaceDemoDir, pMap, Time / 1000, Time % 1000);
52}
53
54void CRaceDemo::OnStateChange(int NewState, int OldState)
55{
56 if(OldState == IClient::STATE_ONLINE)
57 StopRecord();
58}
59
60void CRaceDemo::OnNewSnapshot()
61{
62 if(!GameClient()->m_GameInfo.m_Race || !g_Config.m_ClAutoRaceRecord || Client()->State() != IClient::STATE_ONLINE)
63 return;
64
65 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)
66 return;
67
68 static int s_LastRaceTick = -1;
69
70 bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME;
71 bool ServerControl = RaceFlag && g_Config.m_ClRaceRecordServerControl;
72 int RaceTick = -m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer;
73
74 // start the demo
75 bool ForceStart = ServerControl && s_LastRaceTick != RaceTick && Client()->GameTick(Conn: g_Config.m_ClDummy) - RaceTick < Client()->GameTickSpeed();
76 bool AllowRestart = (m_AllowRestart || ForceStart) && m_RaceStartTick + 10 * Client()->GameTickSpeed() < Client()->GameTick(Conn: g_Config.m_ClDummy);
77 if(m_RaceState == RACE_IDLE || m_RaceState == RACE_PREPARE || (m_RaceState == RACE_STARTED && AllowRestart))
78 {
79 vec2 PrevPos = vec2(m_pClient->m_Snap.m_pLocalPrevCharacter->m_X, m_pClient->m_Snap.m_pLocalPrevCharacter->m_Y);
80 vec2 Pos = vec2(m_pClient->m_Snap.m_pLocalCharacter->m_X, m_pClient->m_Snap.m_pLocalCharacter->m_Y);
81
82 if(ForceStart || (!ServerControl && CRaceHelper::IsStart(pClient: m_pClient, Prev: PrevPos, Pos)))
83 {
84 if(m_RaceState == RACE_STARTED)
85 Client()->RaceRecord_Stop();
86 if(m_RaceState != RACE_PREPARE) // start recording again
87 {
88 GetPath(pBuf: m_aTmpFilename, Size: sizeof(m_aTmpFilename));
89 Client()->RaceRecord_Start(pFilename: m_aTmpFilename);
90 }
91 m_RaceStartTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
92 m_RaceState = RACE_STARTED;
93 }
94 }
95
96 // start recording before the player passes the start line, so we can see some preparation steps
97 if(m_RaceState == RACE_NONE)
98 {
99 GetPath(pBuf: m_aTmpFilename, Size: sizeof(m_aTmpFilename));
100 Client()->RaceRecord_Start(pFilename: m_aTmpFilename);
101 m_RaceStartTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
102 m_RaceState = RACE_PREPARE;
103 }
104
105 // stop recording if the player did not pass the start line after 20 seconds
106 if(m_RaceState == RACE_PREPARE && Client()->GameTick(Conn: g_Config.m_ClDummy) - m_RaceStartTick >= Client()->GameTickSpeed() * 20)
107 {
108 StopRecord();
109 m_RaceState = RACE_IDLE;
110 }
111
112 // stop the demo
113 if(m_RaceState == RACE_FINISHED && m_RecordStopTick <= Client()->GameTick(Conn: g_Config.m_ClDummy))
114 StopRecord(Time: m_Time);
115
116 s_LastRaceTick = RaceFlag ? RaceTick : -1;
117}
118
119void CRaceDemo::OnReset()
120{
121 StopRecord();
122}
123
124void CRaceDemo::OnShutdown()
125{
126 StopRecord();
127}
128
129void CRaceDemo::OnMessage(int MsgType, void *pRawMsg)
130{
131 // check for messages from server
132 if(MsgType == NETMSGTYPE_SV_KILLMSG)
133 {
134 CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
135 if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalClientId && Client()->RaceRecord_IsRecording())
136 StopRecord(Time: m_Time);
137 }
138 else if(MsgType == NETMSGTYPE_SV_KILLMSGTEAM)
139 {
140 CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg;
141 for(int i = 0; i < MAX_CLIENTS; i++)
142 {
143 if(m_pClient->m_Teams.Team(ClientId: i) == pMsg->m_Team && i == m_pClient->m_Snap.m_LocalClientId && Client()->RaceRecord_IsRecording())
144 StopRecord(Time: m_Time);
145 }
146 }
147 else if(MsgType == NETMSGTYPE_SV_CHAT)
148 {
149 CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
150 if(pMsg->m_ClientId == -1 && m_RaceState == RACE_STARTED)
151 {
152 char aName[MAX_NAME_LENGTH];
153 int Time = CRaceHelper::TimeFromFinishMessage(pStr: pMsg->m_pMessage, pNameBuf: aName, NameBufSize: sizeof(aName));
154 if(Time > 0 && m_pClient->m_Snap.m_LocalClientId >= 0 && str_comp(a: aName, b: m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId].m_aName) == 0)
155 {
156 m_RaceState = RACE_FINISHED;
157 m_RecordStopTick = Client()->GameTick(Conn: g_Config.m_ClDummy) + Client()->GameTickSpeed();
158 m_Time = Time;
159 }
160 }
161 }
162}
163
164void CRaceDemo::OnMapLoad()
165{
166 m_AllowRestart = false;
167}
168
169void CRaceDemo::StopRecord(int Time)
170{
171 if(Client()->RaceRecord_IsRecording())
172 Client()->RaceRecord_Stop();
173
174 if(m_aTmpFilename[0] != '\0')
175 {
176 if(Time > 0 && CheckDemo(Time))
177 {
178 // save file
179 char aNewFilename[512];
180 GetPath(pBuf: aNewFilename, Size: sizeof(aNewFilename), Time: m_Time);
181
182 Storage()->RenameFile(pOldFilename: m_aTmpFilename, pNewFilename: aNewFilename, Type: IStorage::TYPE_SAVE);
183 }
184 else // no new record
185 Storage()->RemoveFile(pFilename: m_aTmpFilename, Type: IStorage::TYPE_SAVE);
186
187 m_aTmpFilename[0] = '\0';
188 }
189
190 m_Time = 0;
191 m_RaceState = RACE_NONE;
192 m_RaceStartTick = -1;
193 m_RecordStopTick = -1;
194}
195
196struct SRaceDemoFetchUser
197{
198 CRaceDemo *m_pThis;
199 CDemoListParam *m_pParam;
200};
201
202int CRaceDemo::RaceDemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
203{
204 auto *pRealUser = (SRaceDemoFetchUser *)pUser;
205 auto *pParam = pRealUser->m_pParam;
206 int MapLen = str_length(str: pParam->pMap);
207 if(IsDir || !str_endswith(str: pInfo->m_pName, suffix: ".demo") || !str_startswith(str: pInfo->m_pName, prefix: pParam->pMap) || pInfo->m_pName[MapLen] != '_')
208 return 0;
209
210 CDemoItem Item;
211 str_truncate(dst: Item.m_aName, dst_size: sizeof(Item.m_aName), src: pInfo->m_pName, truncation_len: str_length(str: pInfo->m_pName) - 5);
212
213 const char *pTime = Item.m_aName + MapLen + 1;
214 const char *pTEnd = pTime;
215 while(isdigit(*pTEnd) || *pTEnd == ' ' || *pTEnd == '.' || *pTEnd == ',')
216 pTEnd++;
217
218 if(g_Config.m_ClDemoName)
219 {
220 char aPlayerName[MAX_NAME_LENGTH];
221 str_copy(dst&: aPlayerName, src: pParam->m_pThis->Client()->PlayerName());
222 str_sanitize_filename(str: aPlayerName);
223
224 if(pTEnd[0] != '_' || str_comp(a: pTEnd + 1, b: aPlayerName) != 0)
225 return 0;
226 }
227 else if(pTEnd[0])
228 return 0;
229
230 Item.m_Time = CRaceHelper::TimeFromSecondsStr(pStr: pTime);
231 if(Item.m_Time > 0)
232 pParam->m_pvDemos->push_back(x: Item);
233
234 if(time_get_nanoseconds() - pRealUser->m_pThis->m_RaceDemosLoadStartTime > 500ms)
235 {
236 pRealUser->m_pThis->GameClient()->m_Menus.RenderLoading(pCaption: Localize(pStr: "Loading race demo files"), pContent: "", IncreaseCounter: 0, RenderLoadingBar: false);
237 }
238
239 return 0;
240}
241
242bool CRaceDemo::CheckDemo(int Time)
243{
244 std::vector<CDemoItem> vDemos;
245 CDemoListParam Param = {.m_pThis: this, .m_pvDemos: &vDemos, .pMap: Client()->GetCurrentMap()};
246 m_RaceDemosLoadStartTime = time_get_nanoseconds();
247 SRaceDemoFetchUser User;
248 User.m_pParam = &Param;
249 User.m_pThis = this;
250 Storage()->ListDirectoryInfo(Type: IStorage::TYPE_SAVE, pPath: ms_pRaceDemoDir, pfnCallback: RaceDemolistFetchCallback, pUser: &User);
251
252 // loop through demo files
253 for(auto &Demo : vDemos)
254 {
255 if(Time >= Demo.m_Time) // found a better demo
256 return false;
257
258 // delete old demo
259 char aFilename[IO_MAX_PATH_LENGTH];
260 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "%s/%s.demo", ms_pRaceDemoDir, Demo.m_aName);
261 Storage()->RemoveFile(pFilename: aFilename, Type: IStorage::TYPE_SAVE);
262 }
263
264 return true;
265}
266