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