1 | /* (c) Rajh, Redix and Sushi. */ |
2 | |
3 | #include <engine/ghost.h> |
4 | #include <engine/shared/config.h> |
5 | #include <engine/storage.h> |
6 | |
7 | #include <game/client/race.h> |
8 | |
9 | #include "ghost.h" |
10 | #include "menus.h" |
11 | #include "players.h" |
12 | #include "skins.h" |
13 | |
14 | #include <game/client/gameclient.h> |
15 | |
16 | const char *CGhost::ms_pGhostDir = "ghosts" ; |
17 | |
18 | CGhost::CGhost() : |
19 | m_NewRenderTick(-1), m_StartRenderTick(-1), m_LastDeathTick(-1), m_LastRaceTick(-1), m_Recording(false), m_Rendering(false) {} |
20 | |
21 | void CGhost::GetGhostSkin(CGhostSkin *pSkin, const char *pSkinName, int UseCustomColor, int ColorBody, int ColorFeet) |
22 | { |
23 | StrToInts(pInts: &pSkin->m_Skin0, NumInts: 6, pStr: pSkinName); |
24 | pSkin->m_UseCustomColor = UseCustomColor; |
25 | pSkin->m_ColorBody = ColorBody; |
26 | pSkin->m_ColorFeet = ColorFeet; |
27 | } |
28 | |
29 | void CGhost::GetGhostCharacter(CGhostCharacter *pGhostChar, const CNetObj_Character *pChar, const CNetObj_DDNetCharacter *pDDnetChar) |
30 | { |
31 | pGhostChar->m_X = pChar->m_X; |
32 | pGhostChar->m_Y = pChar->m_Y; |
33 | pGhostChar->m_VelX = pChar->m_VelX; |
34 | pGhostChar->m_VelY = 0; |
35 | pGhostChar->m_Angle = pChar->m_Angle; |
36 | pGhostChar->m_Direction = pChar->m_Direction; |
37 | int Weapon = pChar->m_Weapon; |
38 | if(pDDnetChar != nullptr && pDDnetChar->m_FreezeEnd != 0) |
39 | { |
40 | Weapon = WEAPON_NINJA; |
41 | } |
42 | pGhostChar->m_Weapon = Weapon; |
43 | pGhostChar->m_HookState = pChar->m_HookState; |
44 | pGhostChar->m_HookX = pChar->m_HookX; |
45 | pGhostChar->m_HookY = pChar->m_HookY; |
46 | pGhostChar->m_AttackTick = pChar->m_AttackTick; |
47 | pGhostChar->m_Tick = pChar->m_Tick; |
48 | } |
49 | |
50 | void CGhost::GetNetObjCharacter(CNetObj_Character *pChar, const CGhostCharacter *pGhostChar) |
51 | { |
52 | mem_zero(block: pChar, size: sizeof(CNetObj_Character)); |
53 | pChar->m_X = pGhostChar->m_X; |
54 | pChar->m_Y = pGhostChar->m_Y; |
55 | pChar->m_VelX = pGhostChar->m_VelX; |
56 | pChar->m_VelY = 0; |
57 | pChar->m_Angle = pGhostChar->m_Angle; |
58 | pChar->m_Direction = pGhostChar->m_Direction; |
59 | pChar->m_Weapon = pGhostChar->m_Weapon; |
60 | pChar->m_HookState = pGhostChar->m_HookState; |
61 | pChar->m_HookX = pGhostChar->m_HookX; |
62 | pChar->m_HookY = pGhostChar->m_HookY; |
63 | pChar->m_AttackTick = pGhostChar->m_AttackTick; |
64 | pChar->m_HookedPlayer = -1; |
65 | pChar->m_Tick = pGhostChar->m_Tick; |
66 | } |
67 | |
68 | CGhost::CGhostPath::CGhostPath(CGhostPath &&Other) noexcept : |
69 | m_ChunkSize(Other.m_ChunkSize), m_NumItems(Other.m_NumItems), m_vpChunks(std::move(Other.m_vpChunks)) |
70 | { |
71 | Other.m_NumItems = 0; |
72 | Other.m_vpChunks.clear(); |
73 | } |
74 | |
75 | CGhost::CGhostPath &CGhost::CGhostPath::operator=(CGhostPath &&Other) noexcept |
76 | { |
77 | Reset(ChunkSize: Other.m_ChunkSize); |
78 | m_NumItems = Other.m_NumItems; |
79 | m_vpChunks = std::move(Other.m_vpChunks); |
80 | Other.m_NumItems = 0; |
81 | Other.m_vpChunks.clear(); |
82 | return *this; |
83 | } |
84 | |
85 | void CGhost::CGhostPath::Reset(int ChunkSize) |
86 | { |
87 | for(auto &pChunk : m_vpChunks) |
88 | free(ptr: pChunk); |
89 | m_vpChunks.clear(); |
90 | m_ChunkSize = ChunkSize; |
91 | m_NumItems = 0; |
92 | } |
93 | |
94 | void CGhost::CGhostPath::SetSize(int Items) |
95 | { |
96 | int Chunks = m_vpChunks.size(); |
97 | int NeededChunks = (Items + m_ChunkSize - 1) / m_ChunkSize; |
98 | |
99 | if(NeededChunks > Chunks) |
100 | { |
101 | m_vpChunks.resize(new_size: NeededChunks); |
102 | for(int i = Chunks; i < NeededChunks; i++) |
103 | m_vpChunks[i] = (CGhostCharacter *)calloc(nmemb: m_ChunkSize, size: sizeof(CGhostCharacter)); |
104 | } |
105 | |
106 | m_NumItems = Items; |
107 | } |
108 | |
109 | void CGhost::CGhostPath::Add(const CGhostCharacter &Char) |
110 | { |
111 | SetSize(m_NumItems + 1); |
112 | *Get(Index: m_NumItems - 1) = Char; |
113 | } |
114 | |
115 | CGhostCharacter *CGhost::CGhostPath::Get(int Index) |
116 | { |
117 | if(Index < 0 || Index >= m_NumItems) |
118 | return 0; |
119 | |
120 | int Chunk = Index / m_ChunkSize; |
121 | int Pos = Index % m_ChunkSize; |
122 | return &m_vpChunks[Chunk][Pos]; |
123 | } |
124 | |
125 | void CGhost::GetPath(char *pBuf, int Size, const char *pPlayerName, int Time) const |
126 | { |
127 | const char *pMap = Client()->GetCurrentMap(); |
128 | SHA256_DIGEST Sha256 = Client()->GetCurrentMapSha256(); |
129 | char aSha256[SHA256_MAXSTRSIZE]; |
130 | sha256_str(digest: Sha256, str: aSha256, max_len: sizeof(aSha256)); |
131 | |
132 | char aPlayerName[MAX_NAME_LENGTH]; |
133 | str_copy(dst&: aPlayerName, src: pPlayerName); |
134 | str_sanitize_filename(str: aPlayerName); |
135 | |
136 | char aTimestamp[32]; |
137 | str_timestamp_format(buffer: aTimestamp, buffer_size: sizeof(aTimestamp), FORMAT_NOSPACE); |
138 | |
139 | if(Time < 0) |
140 | str_format(buffer: pBuf, buffer_size: Size, format: "%s/%s_%s_%s_tmp_%d.gho" , ms_pGhostDir, pMap, aPlayerName, aSha256, pid()); |
141 | else |
142 | str_format(buffer: pBuf, buffer_size: Size, format: "%s/%s_%s_%d.%03d_%s_%s.gho" , ms_pGhostDir, pMap, aPlayerName, Time / 1000, Time % 1000, aTimestamp, aSha256); |
143 | } |
144 | |
145 | void CGhost::AddInfos(const CNetObj_Character *pChar, const CNetObj_DDNetCharacter *pDDnetChar) |
146 | { |
147 | int NumTicks = m_CurGhost.m_Path.Size(); |
148 | |
149 | // do not start writing to file as long as we still touch the start line |
150 | if(g_Config.m_ClRaceSaveGhost && !GhostRecorder()->IsRecording() && NumTicks > 0) |
151 | { |
152 | GetPath(pBuf: m_aTmpFilename, Size: sizeof(m_aTmpFilename), pPlayerName: m_CurGhost.m_aPlayer); |
153 | GhostRecorder()->Start(pFilename: m_aTmpFilename, pMap: Client()->GetCurrentMap(), MapSha256: Client()->GetCurrentMapSha256(), pName: m_CurGhost.m_aPlayer); |
154 | |
155 | GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_START_TICK, pData: &m_CurGhost.m_StartTick, Size: sizeof(int)); |
156 | GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_SKIN, pData: &m_CurGhost.m_Skin, Size: sizeof(CGhostSkin)); |
157 | for(int i = 0; i < NumTicks; i++) |
158 | GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_CHARACTER, pData: m_CurGhost.m_Path.Get(Index: i), Size: sizeof(CGhostCharacter)); |
159 | } |
160 | |
161 | CGhostCharacter GhostChar; |
162 | GetGhostCharacter(pGhostChar: &GhostChar, pChar, pDDnetChar); |
163 | m_CurGhost.m_Path.Add(Char: GhostChar); |
164 | if(GhostRecorder()->IsRecording()) |
165 | GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_CHARACTER, pData: &GhostChar, Size: sizeof(CGhostCharacter)); |
166 | } |
167 | |
168 | int CGhost::GetSlot() const |
169 | { |
170 | for(int i = 0; i < MAX_ACTIVE_GHOSTS; i++) |
171 | if(m_aActiveGhosts[i].Empty()) |
172 | return i; |
173 | return -1; |
174 | } |
175 | |
176 | int CGhost::FreeSlots() const |
177 | { |
178 | int Num = 0; |
179 | for(const auto &ActiveGhost : m_aActiveGhosts) |
180 | if(ActiveGhost.Empty()) |
181 | Num++; |
182 | return Num; |
183 | } |
184 | |
185 | void CGhost::CheckStart() |
186 | { |
187 | int RaceTick = -m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer; |
188 | int RenderTick = m_NewRenderTick; |
189 | |
190 | if(m_LastRaceTick != RaceTick && Client()->GameTick(Conn: g_Config.m_ClDummy) - RaceTick < Client()->GameTickSpeed()) |
191 | { |
192 | if(m_Rendering && m_RenderingStartedByServer) // race restarted: stop rendering |
193 | StopRender(); |
194 | if(m_Recording && m_LastRaceTick != -1) // race restarted: activate restarting for local start detection so we have a smooth transition |
195 | m_AllowRestart = true; |
196 | if(m_LastRaceTick == -1) // no restart: reset rendering preparations |
197 | m_NewRenderTick = -1; |
198 | if(GhostRecorder()->IsRecording()) // race restarted: stop recording |
199 | GhostRecorder()->Stop(Ticks: 0, Time: -1); |
200 | int StartTick = RaceTick; |
201 | |
202 | if(GameClient()->m_GameInfo.m_BugDDRaceGhost) // the client recognizes the start one tick earlier than ddrace servers |
203 | StartTick--; |
204 | StartRecord(Tick: StartTick); |
205 | RenderTick = StartTick; |
206 | } |
207 | |
208 | TryRenderStart(Tick: RenderTick, ServerControl: true); |
209 | } |
210 | |
211 | void CGhost::CheckStartLocal(bool Predicted) |
212 | { |
213 | if(Predicted) // rendering |
214 | { |
215 | int RenderTick = m_NewRenderTick; |
216 | |
217 | vec2 PrevPos = m_pClient->m_PredictedPrevChar.m_Pos; |
218 | vec2 Pos = m_pClient->m_PredictedChar.m_Pos; |
219 | if(((!m_Rendering && RenderTick == -1) || m_AllowRestart) && CRaceHelper::IsStart(pClient: m_pClient, Prev: PrevPos, Pos)) |
220 | { |
221 | if(m_Rendering && !m_RenderingStartedByServer) // race restarted: stop rendering |
222 | StopRender(); |
223 | RenderTick = Client()->PredGameTick(Conn: g_Config.m_ClDummy); |
224 | } |
225 | |
226 | TryRenderStart(Tick: RenderTick, ServerControl: false); |
227 | } |
228 | else // recording |
229 | { |
230 | int PrevTick = m_pClient->m_Snap.m_pLocalPrevCharacter->m_Tick; |
231 | int CurTick = m_pClient->m_Snap.m_pLocalCharacter->m_Tick; |
232 | vec2 PrevPos = vec2(m_pClient->m_Snap.m_pLocalPrevCharacter->m_X, m_pClient->m_Snap.m_pLocalPrevCharacter->m_Y); |
233 | vec2 Pos = vec2(m_pClient->m_Snap.m_pLocalCharacter->m_X, m_pClient->m_Snap.m_pLocalCharacter->m_Y); |
234 | |
235 | // detecting death, needed because race allows immediate respawning |
236 | if((!m_Recording || m_AllowRestart) && m_LastDeathTick < PrevTick) |
237 | { |
238 | // estimate the exact start tick |
239 | int RecordTick = -1; |
240 | int TickDiff = CurTick - PrevTick; |
241 | for(int i = 0; i < TickDiff; i++) |
242 | { |
243 | if(CRaceHelper::IsStart(pClient: m_pClient, Prev: mix(a: PrevPos, b: Pos, amount: (float)i / TickDiff), Pos: mix(a: PrevPos, b: Pos, amount: (float)(i + 1) / TickDiff))) |
244 | { |
245 | RecordTick = PrevTick + i + 1; |
246 | if(!m_AllowRestart) |
247 | break; |
248 | } |
249 | } |
250 | if(RecordTick != -1) |
251 | { |
252 | if(GhostRecorder()->IsRecording()) // race restarted: stop recording |
253 | GhostRecorder()->Stop(Ticks: 0, Time: -1); |
254 | StartRecord(Tick: RecordTick); |
255 | } |
256 | } |
257 | } |
258 | } |
259 | |
260 | void CGhost::TryRenderStart(int Tick, bool ServerControl) |
261 | { |
262 | // only restart rendering if it did not change since last tick to prevent stuttering |
263 | if(m_NewRenderTick != -1 && m_NewRenderTick == Tick) |
264 | { |
265 | StartRender(Tick); |
266 | Tick = -1; |
267 | m_RenderingStartedByServer = ServerControl; |
268 | } |
269 | m_NewRenderTick = Tick; |
270 | } |
271 | |
272 | void CGhost::OnNewSnapshot() |
273 | { |
274 | if(!GameClient()->m_GameInfo.m_Race || Client()->State() != IClient::STATE_ONLINE) |
275 | return; |
276 | 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) |
277 | return; |
278 | |
279 | bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME; |
280 | bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl; |
281 | |
282 | if(g_Config.m_ClRaceGhost) |
283 | { |
284 | if(!ServerControl) |
285 | CheckStartLocal(Predicted: false); |
286 | else |
287 | CheckStart(); |
288 | |
289 | if(m_Recording) |
290 | AddInfos(pChar: m_pClient->m_Snap.m_pLocalCharacter, pDDnetChar: (m_pClient->m_Snap.m_LocalClientId != -1 && m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientId].m_HasExtendedData) ? &m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientId].m_ExtendedData : nullptr); |
291 | } |
292 | |
293 | // Record m_LastRaceTick for g_Config.m_ClConfirmDisconnect/QuitTime anyway |
294 | int RaceTick = -m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer; |
295 | m_LastRaceTick = RaceFlag ? RaceTick : -1; |
296 | } |
297 | |
298 | void CGhost::OnNewPredictedSnapshot() |
299 | { |
300 | if(!GameClient()->m_GameInfo.m_Race || !g_Config.m_ClRaceGhost || Client()->State() != IClient::STATE_ONLINE) |
301 | return; |
302 | 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) |
303 | return; |
304 | |
305 | bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME; |
306 | bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl; |
307 | |
308 | if(!ServerControl) |
309 | CheckStartLocal(Predicted: true); |
310 | } |
311 | |
312 | void CGhost::OnRender() |
313 | { |
314 | if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) |
315 | return; |
316 | |
317 | // Play the ghost |
318 | if(!m_Rendering || !g_Config.m_ClRaceShowGhost) |
319 | return; |
320 | |
321 | int PlaybackTick = Client()->PredGameTick(Conn: g_Config.m_ClDummy) - m_StartRenderTick; |
322 | |
323 | for(auto &Ghost : m_aActiveGhosts) |
324 | { |
325 | if(Ghost.Empty()) |
326 | continue; |
327 | |
328 | int GhostTick = Ghost.m_StartTick + PlaybackTick; |
329 | while(Ghost.m_PlaybackPos >= 0 && Ghost.m_Path.Get(Index: Ghost.m_PlaybackPos)->m_Tick < GhostTick) |
330 | { |
331 | if(Ghost.m_PlaybackPos < Ghost.m_Path.Size() - 1) |
332 | Ghost.m_PlaybackPos++; |
333 | else |
334 | Ghost.m_PlaybackPos = -1; |
335 | } |
336 | |
337 | if(Ghost.m_PlaybackPos < 0) |
338 | continue; |
339 | |
340 | int CurPos = Ghost.m_PlaybackPos; |
341 | int PrevPos = maximum(a: 0, b: CurPos - 1); |
342 | if(Ghost.m_Path.Get(Index: PrevPos)->m_Tick > GhostTick) |
343 | continue; |
344 | |
345 | CNetObj_Character Player, Prev; |
346 | GetNetObjCharacter(pChar: &Player, pGhostChar: Ghost.m_Path.Get(Index: CurPos)); |
347 | GetNetObjCharacter(pChar: &Prev, pGhostChar: Ghost.m_Path.Get(Index: PrevPos)); |
348 | |
349 | int TickDiff = Player.m_Tick - Prev.m_Tick; |
350 | float IntraTick = 0.f; |
351 | if(TickDiff > 0) |
352 | IntraTick = (GhostTick - Prev.m_Tick - 1 + Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy)) / TickDiff; |
353 | |
354 | Player.m_AttackTick += Client()->GameTick(Conn: g_Config.m_ClDummy) - GhostTick; |
355 | |
356 | CTeeRenderInfo *pRenderInfo = &Ghost.m_RenderInfo; |
357 | CTeeRenderInfo GhostNinjaRenderInfo; |
358 | if(Player.m_Weapon == WEAPON_NINJA && g_Config.m_ClShowNinja) |
359 | { |
360 | // change the skin for the ghost to the ninja |
361 | const auto *pSkin = m_pClient->m_Skins.FindOrNullptr(pName: "x_ninja" ); |
362 | if(pSkin != nullptr) |
363 | { |
364 | bool IsTeamplay = false; |
365 | if(m_pClient->m_Snap.m_pGameInfoObj) |
366 | IsTeamplay = (m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS) != 0; |
367 | |
368 | GhostNinjaRenderInfo = Ghost.m_RenderInfo; |
369 | GhostNinjaRenderInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin; |
370 | GhostNinjaRenderInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin; |
371 | GhostNinjaRenderInfo.m_BloodColor = pSkin->m_BloodColor; |
372 | GhostNinjaRenderInfo.m_SkinMetrics = pSkin->m_Metrics; |
373 | GhostNinjaRenderInfo.m_CustomColoredSkin = IsTeamplay; |
374 | if(!IsTeamplay) |
375 | { |
376 | GhostNinjaRenderInfo.m_ColorBody = ColorRGBA(1, 1, 1); |
377 | GhostNinjaRenderInfo.m_ColorFeet = ColorRGBA(1, 1, 1); |
378 | } |
379 | pRenderInfo = &GhostNinjaRenderInfo; |
380 | } |
381 | } |
382 | |
383 | m_pClient->m_Players.RenderHook(pPrevChar: &Prev, pPlayerChar: &Player, pRenderInfo, ClientId: -2, Intra: IntraTick); |
384 | m_pClient->m_Players.RenderHookCollLine(pPrevChar: &Prev, pPlayerChar: &Player, ClientId: -2, Intra: IntraTick); |
385 | m_pClient->m_Players.RenderPlayer(pPrevChar: &Prev, pPlayerChar: &Player, pRenderInfo, ClientId: -2, Intra: IntraTick); |
386 | } |
387 | } |
388 | |
389 | void CGhost::InitRenderInfos(CGhostItem *pGhost) |
390 | { |
391 | char aSkinName[24]; |
392 | IntsToStr(pInts: &pGhost->m_Skin.m_Skin0, NumInts: 6, pStr: aSkinName, StrSize: std::size(aSkinName)); |
393 | CTeeRenderInfo *pRenderInfo = &pGhost->m_RenderInfo; |
394 | |
395 | const CSkin *pSkin = m_pClient->m_Skins.Find(pName: aSkinName); |
396 | pRenderInfo->m_OriginalRenderSkin = pSkin->m_OriginalSkin; |
397 | pRenderInfo->m_ColorableRenderSkin = pSkin->m_ColorableSkin; |
398 | pRenderInfo->m_BloodColor = pSkin->m_BloodColor; |
399 | pRenderInfo->m_SkinMetrics = pSkin->m_Metrics; |
400 | pRenderInfo->m_CustomColoredSkin = pGhost->m_Skin.m_UseCustomColor; |
401 | if(pGhost->m_Skin.m_UseCustomColor) |
402 | { |
403 | pRenderInfo->m_ColorBody = color_cast<ColorRGBA>(hsl: ColorHSLA(pGhost->m_Skin.m_ColorBody).UnclampLighting()); |
404 | pRenderInfo->m_ColorFeet = color_cast<ColorRGBA>(hsl: ColorHSLA(pGhost->m_Skin.m_ColorFeet).UnclampLighting()); |
405 | } |
406 | else |
407 | { |
408 | pRenderInfo->m_ColorBody = ColorRGBA(1, 1, 1); |
409 | pRenderInfo->m_ColorFeet = ColorRGBA(1, 1, 1); |
410 | } |
411 | |
412 | pRenderInfo->m_Size = 64; |
413 | } |
414 | |
415 | void CGhost::StartRecord(int Tick) |
416 | { |
417 | m_Recording = true; |
418 | m_CurGhost.Reset(); |
419 | m_CurGhost.m_StartTick = Tick; |
420 | |
421 | const CGameClient::CClientData *pData = &m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId]; |
422 | str_copy(dst&: m_CurGhost.m_aPlayer, src: Client()->PlayerName()); |
423 | GetGhostSkin(pSkin: &m_CurGhost.m_Skin, pSkinName: pData->m_aSkinName, UseCustomColor: pData->m_UseCustomColor, ColorBody: pData->m_ColorBody, ColorFeet: pData->m_ColorFeet); |
424 | InitRenderInfos(pGhost: &m_CurGhost); |
425 | } |
426 | |
427 | void CGhost::StopRecord(int Time) |
428 | { |
429 | m_Recording = false; |
430 | bool RecordingToFile = GhostRecorder()->IsRecording(); |
431 | |
432 | if(RecordingToFile) |
433 | GhostRecorder()->Stop(Ticks: m_CurGhost.m_Path.Size(), Time); |
434 | |
435 | CMenus::CGhostItem *pOwnGhost = m_pClient->m_Menus.GetOwnGhost(); |
436 | if(Time > 0 && (!pOwnGhost || Time < pOwnGhost->m_Time || !g_Config.m_ClRaceGhostSaveBest)) |
437 | { |
438 | // add to active ghosts |
439 | int Slot = GetSlot(); |
440 | if(Slot != -1 && (!pOwnGhost || Time < pOwnGhost->m_Time)) |
441 | m_aActiveGhosts[Slot] = std::move(m_CurGhost); |
442 | |
443 | if(pOwnGhost && pOwnGhost->Active() && Time < pOwnGhost->m_Time) |
444 | Unload(Slot: pOwnGhost->m_Slot); |
445 | |
446 | // create ghost item |
447 | CMenus::CGhostItem Item; |
448 | if(RecordingToFile) |
449 | GetPath(pBuf: Item.m_aFilename, Size: sizeof(Item.m_aFilename), pPlayerName: m_CurGhost.m_aPlayer, Time); |
450 | str_copy(dst&: Item.m_aPlayer, src: m_CurGhost.m_aPlayer); |
451 | Item.m_Time = Time; |
452 | Item.m_Slot = Slot; |
453 | |
454 | // save new ghost file |
455 | if(Item.HasFile()) |
456 | Storage()->RenameFile(pOldFilename: m_aTmpFilename, pNewFilename: Item.m_aFilename, Type: IStorage::TYPE_SAVE); |
457 | |
458 | // add item to menu list |
459 | m_pClient->m_Menus.UpdateOwnGhost(Item); |
460 | } |
461 | else if(RecordingToFile) // no new record |
462 | Storage()->RemoveFile(pFilename: m_aTmpFilename, Type: IStorage::TYPE_SAVE); |
463 | |
464 | m_aTmpFilename[0] = 0; |
465 | |
466 | m_CurGhost.Reset(); |
467 | } |
468 | |
469 | void CGhost::StartRender(int Tick) |
470 | { |
471 | m_Rendering = true; |
472 | m_StartRenderTick = Tick; |
473 | for(auto &Ghost : m_aActiveGhosts) |
474 | Ghost.m_PlaybackPos = 0; |
475 | } |
476 | |
477 | void CGhost::StopRender() |
478 | { |
479 | m_Rendering = false; |
480 | m_NewRenderTick = -1; |
481 | } |
482 | |
483 | int CGhost::Load(const char *pFilename) |
484 | { |
485 | int Slot = GetSlot(); |
486 | if(Slot == -1) |
487 | return -1; |
488 | |
489 | if(GhostLoader()->Load(pFilename, pMap: Client()->GetCurrentMap(), MapSha256: Client()->GetCurrentMapSha256(), MapCrc: Client()->GetCurrentMapCrc()) != 0) |
490 | return -1; |
491 | |
492 | const CGhostInfo *pInfo = GhostLoader()->GetInfo(); |
493 | |
494 | if(pInfo->m_NumTicks <= 0 || pInfo->m_Time <= 0) |
495 | { |
496 | Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost" , pStr: "invalid header info" ); |
497 | GhostLoader()->Close(); |
498 | return -1; |
499 | } |
500 | |
501 | // select ghost |
502 | CGhostItem *pGhost = &m_aActiveGhosts[Slot]; |
503 | pGhost->Reset(); |
504 | pGhost->m_Path.SetSize(pInfo->m_NumTicks); |
505 | |
506 | str_copy(dst&: pGhost->m_aPlayer, src: pInfo->m_aOwner); |
507 | |
508 | int Index = 0; |
509 | bool FoundSkin = false; |
510 | bool NoTick = false; |
511 | bool Error = false; |
512 | |
513 | int Type; |
514 | while(!Error && GhostLoader()->ReadNextType(pType: &Type)) |
515 | { |
516 | if(Index == pInfo->m_NumTicks && (Type == GHOSTDATA_TYPE_CHARACTER || Type == GHOSTDATA_TYPE_CHARACTER_NO_TICK)) |
517 | { |
518 | Error = true; |
519 | break; |
520 | } |
521 | |
522 | if(Type == GHOSTDATA_TYPE_SKIN && !FoundSkin) |
523 | { |
524 | FoundSkin = true; |
525 | if(!GhostLoader()->ReadData(Type, pData: &pGhost->m_Skin, Size: sizeof(CGhostSkin))) |
526 | Error = true; |
527 | } |
528 | else if(Type == GHOSTDATA_TYPE_CHARACTER_NO_TICK) |
529 | { |
530 | NoTick = true; |
531 | if(!GhostLoader()->ReadData(Type, pData: pGhost->m_Path.Get(Index: Index++), Size: sizeof(CGhostCharacter_NoTick))) |
532 | Error = true; |
533 | } |
534 | else if(Type == GHOSTDATA_TYPE_CHARACTER) |
535 | { |
536 | if(!GhostLoader()->ReadData(Type, pData: pGhost->m_Path.Get(Index: Index++), Size: sizeof(CGhostCharacter))) |
537 | Error = true; |
538 | } |
539 | else if(Type == GHOSTDATA_TYPE_START_TICK) |
540 | { |
541 | if(!GhostLoader()->ReadData(Type, pData: &pGhost->m_StartTick, Size: sizeof(int))) |
542 | Error = true; |
543 | } |
544 | } |
545 | |
546 | GhostLoader()->Close(); |
547 | |
548 | if(Error || Index != pInfo->m_NumTicks) |
549 | { |
550 | Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost" , pStr: "invalid ghost data" ); |
551 | pGhost->Reset(); |
552 | return -1; |
553 | } |
554 | |
555 | if(NoTick) |
556 | { |
557 | int StartTick = 0; |
558 | for(int i = 1; i < pInfo->m_NumTicks; i++) // estimate start tick |
559 | if(pGhost->m_Path.Get(Index: i)->m_AttackTick != pGhost->m_Path.Get(Index: i - 1)->m_AttackTick) |
560 | StartTick = pGhost->m_Path.Get(Index: i)->m_AttackTick - i; |
561 | for(int i = 0; i < pInfo->m_NumTicks; i++) |
562 | pGhost->m_Path.Get(Index: i)->m_Tick = StartTick + i; |
563 | } |
564 | |
565 | if(pGhost->m_StartTick == -1) |
566 | pGhost->m_StartTick = pGhost->m_Path.Get(Index: 0)->m_Tick; |
567 | |
568 | if(!FoundSkin) |
569 | GetGhostSkin(pSkin: &pGhost->m_Skin, pSkinName: "default" , UseCustomColor: 0, ColorBody: 0, ColorFeet: 0); |
570 | InitRenderInfos(pGhost); |
571 | |
572 | return Slot; |
573 | } |
574 | |
575 | void CGhost::Unload(int Slot) |
576 | { |
577 | m_aActiveGhosts[Slot].Reset(); |
578 | } |
579 | |
580 | void CGhost::UnloadAll() |
581 | { |
582 | for(int i = 0; i < MAX_ACTIVE_GHOSTS; i++) |
583 | Unload(Slot: i); |
584 | } |
585 | |
586 | void CGhost::(CMenus::CGhostItem *pItem) |
587 | { |
588 | int Slot = pItem->m_Slot; |
589 | if(!pItem->Active() || pItem->HasFile() || m_aActiveGhosts[Slot].Empty() || GhostRecorder()->IsRecording()) |
590 | return; |
591 | |
592 | CGhostItem *pGhost = &m_aActiveGhosts[Slot]; |
593 | |
594 | int NumTicks = pGhost->m_Path.Size(); |
595 | GetPath(pBuf: pItem->m_aFilename, Size: sizeof(pItem->m_aFilename), pPlayerName: pItem->m_aPlayer, Time: pItem->m_Time); |
596 | GhostRecorder()->Start(pFilename: pItem->m_aFilename, pMap: Client()->GetCurrentMap(), MapSha256: Client()->GetCurrentMapSha256(), pName: pItem->m_aPlayer); |
597 | |
598 | GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_START_TICK, pData: &pGhost->m_StartTick, Size: sizeof(int)); |
599 | GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_SKIN, pData: &pGhost->m_Skin, Size: sizeof(CGhostSkin)); |
600 | for(int i = 0; i < NumTicks; i++) |
601 | GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_CHARACTER, pData: pGhost->m_Path.Get(Index: i), Size: sizeof(CGhostCharacter)); |
602 | |
603 | GhostRecorder()->Stop(Ticks: NumTicks, Time: pItem->m_Time); |
604 | } |
605 | |
606 | void CGhost::ConGPlay(IConsole::IResult *pResult, void *pUserData) |
607 | { |
608 | CGhost *pGhost = (CGhost *)pUserData; |
609 | pGhost->StartRender(Tick: pGhost->Client()->PredGameTick(Conn: g_Config.m_ClDummy)); |
610 | } |
611 | |
612 | void CGhost::OnConsoleInit() |
613 | { |
614 | m_pGhostLoader = Kernel()->RequestInterface<IGhostLoader>(); |
615 | m_pGhostRecorder = Kernel()->RequestInterface<IGhostRecorder>(); |
616 | |
617 | Console()->Register(pName: "gplay" , pParams: "" , Flags: CFGFLAG_CLIENT, pfnFunc: ConGPlay, pUser: this, pHelp: "Start playback of ghosts" ); |
618 | } |
619 | |
620 | void CGhost::OnMessage(int MsgType, void *pRawMsg) |
621 | { |
622 | // check for messages from server |
623 | if(MsgType == NETMSGTYPE_SV_KILLMSG) |
624 | { |
625 | CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg; |
626 | if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalClientId) |
627 | { |
628 | if(m_Recording) |
629 | StopRecord(); |
630 | StopRender(); |
631 | m_LastDeathTick = Client()->GameTick(Conn: g_Config.m_ClDummy); |
632 | } |
633 | } |
634 | else if(MsgType == NETMSGTYPE_SV_KILLMSGTEAM) |
635 | { |
636 | CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg; |
637 | for(int i = 0; i < MAX_CLIENTS; i++) |
638 | { |
639 | if(m_pClient->m_Teams.Team(ClientId: i) == pMsg->m_Team && i == m_pClient->m_Snap.m_LocalClientId) |
640 | { |
641 | if(m_Recording) |
642 | StopRecord(); |
643 | StopRender(); |
644 | m_LastDeathTick = Client()->GameTick(Conn: g_Config.m_ClDummy); |
645 | } |
646 | } |
647 | } |
648 | else if(MsgType == NETMSGTYPE_SV_CHAT) |
649 | { |
650 | CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg; |
651 | if(pMsg->m_ClientId == -1 && m_Recording) |
652 | { |
653 | char aName[MAX_NAME_LENGTH]; |
654 | int Time = CRaceHelper::TimeFromFinishMessage(pStr: pMsg->m_pMessage, pNameBuf: aName, NameBufSize: sizeof(aName)); |
655 | 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) |
656 | { |
657 | StopRecord(Time); |
658 | StopRender(); |
659 | } |
660 | } |
661 | } |
662 | } |
663 | |
664 | void CGhost::OnReset() |
665 | { |
666 | StopRecord(); |
667 | StopRender(); |
668 | m_LastDeathTick = -1; |
669 | m_LastRaceTick = -1; |
670 | } |
671 | |
672 | void CGhost::OnShutdown() |
673 | { |
674 | OnReset(); |
675 | } |
676 | |
677 | void CGhost::OnMapLoad() |
678 | { |
679 | OnReset(); |
680 | UnloadAll(); |
681 | m_pClient->m_Menus.GhostlistPopulate(); |
682 | m_AllowRestart = false; |
683 | } |
684 | |
685 | int CGhost::GetLastRaceTick() const |
686 | { |
687 | return m_LastRaceTick; |
688 | } |
689 | |
690 | void CGhost::OnRefreshSkins() |
691 | { |
692 | const auto &&RefindSkin = [&](auto &Ghost) { |
693 | if(Ghost.Empty()) |
694 | return; |
695 | char aSkinName[24]; |
696 | IntsToStr(&Ghost.m_Skin.m_Skin0, 6, aSkinName, std::size(aSkinName)); |
697 | CTeeRenderInfo *pRenderInfo = &Ghost.m_RenderInfo; |
698 | if(aSkinName[0] != '\0') |
699 | { |
700 | const CSkin *pSkin = m_pClient->m_Skins.Find(pName: aSkinName); |
701 | pRenderInfo->m_OriginalRenderSkin = pSkin->m_OriginalSkin; |
702 | pRenderInfo->m_ColorableRenderSkin = pSkin->m_ColorableSkin; |
703 | pRenderInfo->m_BloodColor = pSkin->m_BloodColor; |
704 | pRenderInfo->m_SkinMetrics = pSkin->m_Metrics; |
705 | } |
706 | else |
707 | { |
708 | pRenderInfo->m_OriginalRenderSkin.Reset(); |
709 | pRenderInfo->m_ColorableRenderSkin.Reset(); |
710 | } |
711 | }; |
712 | |
713 | for(auto &Ghost : m_aActiveGhosts) |
714 | { |
715 | RefindSkin(Ghost); |
716 | } |
717 | RefindSkin(m_CurGhost); |
718 | } |
719 | |