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