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 | |
18 | using namespace std::chrono_literals; |
19 | |
20 | const char *CRaceDemo::ms_pRaceDemoDir = "demos/auto/race" ; |
21 | |
22 | struct CDemoItem |
23 | { |
24 | char m_aName[128]; |
25 | int m_Time; |
26 | }; |
27 | |
28 | struct CDemoListParam |
29 | { |
30 | const CRaceDemo *m_pThis; |
31 | std::vector<CDemoItem> *m_pvDemos; |
32 | const char *pMap; |
33 | }; |
34 | |
35 | CRaceDemo::CRaceDemo() : |
36 | m_RaceState(RACE_NONE), m_RaceStartTick(-1), m_RecordStopTick(-1), m_Time(0) {} |
37 | |
38 | void 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 | |
54 | void CRaceDemo::OnStateChange(int NewState, int OldState) |
55 | { |
56 | if(OldState == IClient::STATE_ONLINE) |
57 | StopRecord(); |
58 | } |
59 | |
60 | void 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 | |
119 | void CRaceDemo::OnReset() |
120 | { |
121 | StopRecord(); |
122 | } |
123 | |
124 | void CRaceDemo::OnShutdown() |
125 | { |
126 | StopRecord(); |
127 | } |
128 | |
129 | void 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 | |
164 | void CRaceDemo::OnMapLoad() |
165 | { |
166 | m_AllowRestart = false; |
167 | } |
168 | |
169 | void 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 | |
196 | struct SRaceDemoFetchUser |
197 | { |
198 | CRaceDemo *m_pThis; |
199 | CDemoListParam *m_pParam; |
200 | }; |
201 | |
202 | int 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 | |
242 | bool 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 | |