1/* (c) Rajh, Redix and Sushi. */
2
3#include "ghost.h"
4
5#include <base/log.h>
6
7#include <engine/ghost.h>
8#include <engine/shared/config.h>
9#include <engine/storage.h>
10
11#include <game/client/components/menus.h>
12#include <game/client/components/players.h>
13#include <game/client/components/skins.h>
14#include <game/client/gameclient.h>
15#include <game/client/race.h>
16
17const char *CGhost::ms_pGhostDir = "ghosts";
18
19static const LOG_COLOR LOG_COLOR_GHOST{.r: 165, .g: 153, .b: 153};
20
21void CGhost::SetGhostSkinData(CGhostSkin *pSkin, const char *pSkinName, int UseCustomColor, int ColorBody, int ColorFeet)
22{
23 StrToInts(pInts: pSkin->m_aSkin, NumInts: std::size(pSkin->m_aSkin), pStr: pSkinName);
24 pSkin->m_UseCustomColor = UseCustomColor;
25 pSkin->m_ColorBody = ColorBody;
26 pSkin->m_ColorFeet = ColorFeet;
27}
28
29void 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
50void 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
68CGhost::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
75CGhost::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
85void 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
94void 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
109void CGhost::CGhostPath::Add(const CGhostCharacter &Char)
110{
111 SetSize(m_NumItems + 1);
112 *Get(Index: m_NumItems - 1) = Char;
113}
114
115CGhostCharacter *CGhost::CGhostPath::Get(int Index)
116{
117 if(Index < 0 || Index >= m_NumItems)
118 return nullptr;
119
120 int Chunk = Index / m_ChunkSize;
121 int Pos = Index % m_ChunkSize;
122 return &m_vpChunks[Chunk][Pos];
123}
124
125void 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
145void 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
168int 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
176int 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
185void CGhost::CheckStart()
186{
187 int RaceTick = -GameClient()->m_Snap.m_pGameInfoObj->m_WarmupTimer;
188 int RenderTick = m_NewRenderTick;
189
190 if(GameClient()->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 && GameClient()->LastRaceTick() != -1) // race restarted: activate restarting for local start detection so we have a smooth transition
195 m_AllowRestart = true;
196 if(GameClient()->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
211void CGhost::CheckStartLocal(bool Predicted)
212{
213 if(Predicted) // rendering
214 {
215 int RenderTick = m_NewRenderTick;
216
217 vec2 PrevPos = GameClient()->m_PredictedPrevChar.m_Pos;
218 vec2 Pos = GameClient()->m_PredictedChar.m_Pos;
219 if(((!m_Rendering && RenderTick == -1) || m_AllowRestart) && GameClient()->RaceHelper()->IsStart(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 = GameClient()->m_Snap.m_pLocalPrevCharacter->m_Tick;
231 int CurTick = GameClient()->m_Snap.m_pLocalCharacter->m_Tick;
232 vec2 PrevPos = vec2(GameClient()->m_Snap.m_pLocalPrevCharacter->m_X, GameClient()->m_Snap.m_pLocalPrevCharacter->m_Y);
233 vec2 Pos = vec2(GameClient()->m_Snap.m_pLocalCharacter->m_X, GameClient()->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(GameClient()->RaceHelper()->IsStart(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
260void 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
272void CGhost::OnNewSnapshot()
273{
274 if(!GameClient()->m_GameInfo.m_Race || !g_Config.m_ClRaceGhost || Client()->State() != IClient::STATE_ONLINE)
275 return;
276 if(!GameClient()->m_Snap.m_pGameInfoObj || GameClient()->m_Snap.m_SpecInfo.m_Active || !GameClient()->m_Snap.m_pLocalCharacter || !GameClient()->m_Snap.m_pLocalPrevCharacter)
277 return;
278
279 const bool RaceFlag = GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME;
280 const bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl;
281
282 if(!ServerControl)
283 CheckStartLocal(Predicted: false);
284 else
285 CheckStart();
286
287 if(m_Recording)
288 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);
289}
290
291void CGhost::OnNewPredictedSnapshot()
292{
293 if(!GameClient()->m_GameInfo.m_Race || !g_Config.m_ClRaceGhost || Client()->State() != IClient::STATE_ONLINE)
294 return;
295 if(!GameClient()->m_Snap.m_pGameInfoObj || GameClient()->m_Snap.m_SpecInfo.m_Active || !GameClient()->m_Snap.m_pLocalCharacter || !GameClient()->m_Snap.m_pLocalPrevCharacter)
296 return;
297
298 const bool RaceFlag = GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME;
299 const bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl;
300
301 if(!ServerControl)
302 CheckStartLocal(Predicted: true);
303}
304
305void CGhost::OnRender()
306{
307 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
308 return;
309
310 // Play the ghost
311 if(!m_Rendering || !g_Config.m_ClRaceShowGhost)
312 return;
313
314 int PlaybackTick = Client()->PredGameTick(Conn: g_Config.m_ClDummy) - m_StartRenderTick;
315
316 for(auto &Ghost : m_aActiveGhosts)
317 {
318 if(Ghost.Empty())
319 continue;
320
321 int GhostTick = Ghost.m_StartTick + PlaybackTick;
322 while(Ghost.m_PlaybackPos >= 0 && Ghost.m_Path.Get(Index: Ghost.m_PlaybackPos)->m_Tick < GhostTick)
323 {
324 if(Ghost.m_PlaybackPos < Ghost.m_Path.Size() - 1)
325 Ghost.m_PlaybackPos++;
326 else
327 Ghost.m_PlaybackPos = -1;
328 }
329
330 if(Ghost.m_PlaybackPos < 0)
331 continue;
332
333 int CurPos = Ghost.m_PlaybackPos;
334 int PrevPos = maximum(a: 0, b: CurPos - 1);
335 if(Ghost.m_Path.Get(Index: PrevPos)->m_Tick > GhostTick)
336 continue;
337
338 CNetObj_Character Player, Prev;
339 GetNetObjCharacter(pChar: &Player, pGhostChar: Ghost.m_Path.Get(Index: CurPos));
340 GetNetObjCharacter(pChar: &Prev, pGhostChar: Ghost.m_Path.Get(Index: PrevPos));
341
342 int TickDiff = Player.m_Tick - Prev.m_Tick;
343 float IntraTick = 0.f;
344 if(TickDiff > 0)
345 IntraTick = (GhostTick - Prev.m_Tick - 1 + Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy)) / TickDiff;
346
347 Player.m_AttackTick += Client()->GameTick(Conn: g_Config.m_ClDummy) - GhostTick;
348
349 const CTeeRenderInfo *pRenderInfo = &Ghost.m_pManagedTeeRenderInfo->TeeRenderInfo();
350 CTeeRenderInfo GhostNinjaRenderInfo;
351 if(Player.m_Weapon == WEAPON_NINJA && g_Config.m_ClShowNinja)
352 {
353 // change the skin for the ghost to the ninja
354 GhostNinjaRenderInfo = Ghost.m_pManagedTeeRenderInfo->TeeRenderInfo();
355 GhostNinjaRenderInfo.ApplySkin(TeeRenderInfo: GameClient()->m_Players.NinjaTeeRenderInfo()->TeeRenderInfo());
356 GhostNinjaRenderInfo.m_CustomColoredSkin = GameClient()->IsTeamPlay();
357 if(!GhostNinjaRenderInfo.m_CustomColoredSkin)
358 {
359 GhostNinjaRenderInfo.m_ColorBody = ColorRGBA(1, 1, 1);
360 GhostNinjaRenderInfo.m_ColorFeet = ColorRGBA(1, 1, 1);
361 }
362 pRenderInfo = &GhostNinjaRenderInfo;
363 }
364
365 GameClient()->m_Players.RenderHook(pPrevChar: &Prev, pPlayerChar: &Player, pRenderInfo, ClientId: -2, Intra: IntraTick);
366 GameClient()->m_Players.RenderPlayer(pPrevChar: &Prev, pPlayerChar: &Player, pRenderInfo, ClientId: -2, Intra: IntraTick);
367 }
368}
369
370void CGhost::UpdateTeeRenderInfo(CGhostItem &Ghost)
371{
372 CSkinDescriptor SkinDescriptor;
373 SkinDescriptor.m_Flags = CSkinDescriptor::FLAG_SIX;
374 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));
375 if(!CSkin::IsValidName(pName: SkinDescriptor.m_aSkinName))
376 {
377 str_copy(dst&: SkinDescriptor.m_aSkinName, src: "default");
378 }
379
380 CTeeRenderInfo TeeRenderInfo;
381 TeeRenderInfo.ApplyColors(CustomColoredSkin: Ghost.m_Skin.m_UseCustomColor, ColorBody: Ghost.m_Skin.m_ColorBody, ColorFeet: Ghost.m_Skin.m_ColorFeet);
382 TeeRenderInfo.m_Size = 64.0f;
383
384 Ghost.m_pManagedTeeRenderInfo = GameClient()->CreateManagedTeeRenderInfo(TeeRenderInfo, SkinDescriptor);
385}
386
387void CGhost::StartRecord(int Tick)
388{
389 m_Recording = true;
390 m_CurGhost.Reset();
391 m_CurGhost.m_StartTick = Tick;
392
393 const CGameClient::CClientData *pData = &GameClient()->m_aClients[GameClient()->m_Snap.m_LocalClientId];
394 str_copy(dst&: m_CurGhost.m_aPlayer, src: Client()->PlayerName());
395 SetGhostSkinData(pSkin: &m_CurGhost.m_Skin, pSkinName: pData->m_aSkinName, UseCustomColor: pData->m_UseCustomColor, ColorBody: pData->m_ColorBody, ColorFeet: pData->m_ColorFeet);
396 UpdateTeeRenderInfo(Ghost&: m_CurGhost);
397}
398
399void CGhost::StopRecord(int Time)
400{
401 m_Recording = false;
402 bool RecordingToFile = GhostRecorder()->IsRecording();
403
404 CMenus::CGhostItem *pOwnGhost = GameClient()->m_Menus.GetOwnGhost();
405 const bool StoreGhost = Time > 0 && (!pOwnGhost || Time < pOwnGhost->m_Time || !g_Config.m_ClRaceGhostSaveBest);
406
407 if(RecordingToFile)
408 GhostRecorder()->Stop(Ticks: m_CurGhost.m_Path.Size(), Time: StoreGhost ? Time : -1);
409
410 if(StoreGhost)
411 {
412 // add to active ghosts
413 int Slot = GetSlot();
414 if(Slot != -1 && (!pOwnGhost || Time < pOwnGhost->m_Time))
415 m_aActiveGhosts[Slot] = std::move(m_CurGhost);
416
417 if(pOwnGhost && pOwnGhost->Active() && Time < pOwnGhost->m_Time)
418 Unload(Slot: pOwnGhost->m_Slot);
419
420 // create ghost item
421 CMenus::CGhostItem Item;
422 if(RecordingToFile)
423 GetPath(pBuf: Item.m_aFilename, Size: sizeof(Item.m_aFilename), pPlayerName: m_CurGhost.m_aPlayer, Time);
424 str_copy(dst&: Item.m_aPlayer, src: m_CurGhost.m_aPlayer);
425 Item.m_Time = Time;
426 Item.m_Slot = Slot;
427
428 // save new ghost file
429 if(Item.HasFile())
430 Storage()->RenameFile(pOldFilename: m_aTmpFilename, pNewFilename: Item.m_aFilename, Type: IStorage::TYPE_SAVE);
431
432 // add item to menu list
433 GameClient()->m_Menus.UpdateOwnGhost(Item);
434 }
435
436 m_aTmpFilename[0] = '\0';
437 m_CurGhost.Reset();
438}
439
440void CGhost::StartRender(int Tick)
441{
442 m_Rendering = true;
443 m_StartRenderTick = Tick;
444 for(auto &Ghost : m_aActiveGhosts)
445 Ghost.m_PlaybackPos = 0;
446}
447
448void CGhost::StopRender()
449{
450 m_Rendering = false;
451 m_NewRenderTick = -1;
452}
453
454int CGhost::Load(const char *pFilename)
455{
456 int Slot = GetSlot();
457 if(Slot == -1)
458 return -1;
459
460 if(!GhostLoader()->Load(pFilename, pMap: Client()->GetCurrentMap(), MapSha256: Client()->GetCurrentMapSha256(), MapCrc: Client()->GetCurrentMapCrc()))
461 return -1;
462
463 const CGhostInfo *pInfo = GhostLoader()->GetInfo();
464
465 // select ghost
466 CGhostItem *pGhost = &m_aActiveGhosts[Slot];
467 pGhost->Reset();
468 pGhost->m_Path.SetSize(pInfo->m_NumTicks);
469
470 str_copy(dst&: pGhost->m_aPlayer, src: pInfo->m_aOwner);
471
472 int Index = 0;
473 bool FoundSkin = false;
474 bool NoTick = false;
475 bool Error = false;
476
477 int Type;
478 while(!Error && GhostLoader()->ReadNextType(pType: &Type))
479 {
480 if(Index == pInfo->m_NumTicks && (Type == GHOSTDATA_TYPE_CHARACTER || Type == GHOSTDATA_TYPE_CHARACTER_NO_TICK))
481 {
482 Error = true;
483 break;
484 }
485
486 if(Type == GHOSTDATA_TYPE_SKIN && !FoundSkin)
487 {
488 FoundSkin = true;
489 if(!GhostLoader()->ReadData(Type, pData: &pGhost->m_Skin, Size: sizeof(CGhostSkin)))
490 Error = true;
491 }
492 else if(Type == GHOSTDATA_TYPE_CHARACTER_NO_TICK)
493 {
494 NoTick = true;
495 if(!GhostLoader()->ReadData(Type, pData: pGhost->m_Path.Get(Index: Index++), Size: sizeof(CGhostCharacter_NoTick)))
496 Error = true;
497 }
498 else if(Type == GHOSTDATA_TYPE_CHARACTER)
499 {
500 if(!GhostLoader()->ReadData(Type, pData: pGhost->m_Path.Get(Index: Index++), Size: sizeof(CGhostCharacter)))
501 Error = true;
502 }
503 else if(Type == GHOSTDATA_TYPE_START_TICK)
504 {
505 if(!GhostLoader()->ReadData(Type, pData: &pGhost->m_StartTick, Size: sizeof(int)))
506 Error = true;
507 }
508 }
509
510 GhostLoader()->Close();
511
512 if(Error || Index != pInfo->m_NumTicks)
513 {
514 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);
515 pGhost->Reset();
516 return -1;
517 }
518
519 if(NoTick)
520 {
521 int StartTick = 0;
522 for(int i = 1; i < pInfo->m_NumTicks; i++) // estimate start tick
523 if(pGhost->m_Path.Get(Index: i)->m_AttackTick != pGhost->m_Path.Get(Index: i - 1)->m_AttackTick)
524 StartTick = pGhost->m_Path.Get(Index: i)->m_AttackTick - i;
525 for(int i = 0; i < pInfo->m_NumTicks; i++)
526 pGhost->m_Path.Get(Index: i)->m_Tick = StartTick + i;
527 }
528
529 if(pGhost->m_StartTick == -1)
530 pGhost->m_StartTick = pGhost->m_Path.Get(Index: 0)->m_Tick;
531
532 if(!FoundSkin)
533 {
534 SetGhostSkinData(pSkin: &pGhost->m_Skin, pSkinName: "default", UseCustomColor: 0, ColorBody: 0, ColorFeet: 0);
535 }
536 UpdateTeeRenderInfo(Ghost&: *pGhost);
537
538 return Slot;
539}
540
541void CGhost::Unload(int Slot)
542{
543 m_aActiveGhosts[Slot].Reset();
544}
545
546void CGhost::UnloadAll()
547{
548 for(int i = 0; i < MAX_ACTIVE_GHOSTS; i++)
549 Unload(Slot: i);
550}
551
552void CGhost::SaveGhost(CMenus::CGhostItem *pItem)
553{
554 int Slot = pItem->m_Slot;
555 if(!pItem->Active() || pItem->HasFile() || m_aActiveGhosts[Slot].Empty() || GhostRecorder()->IsRecording())
556 return;
557
558 CGhostItem *pGhost = &m_aActiveGhosts[Slot];
559
560 int NumTicks = pGhost->m_Path.Size();
561 GetPath(pBuf: pItem->m_aFilename, Size: sizeof(pItem->m_aFilename), pPlayerName: pItem->m_aPlayer, Time: pItem->m_Time);
562 GhostRecorder()->Start(pFilename: pItem->m_aFilename, pMap: Client()->GetCurrentMap(), MapSha256: Client()->GetCurrentMapSha256(), pName: pItem->m_aPlayer);
563
564 GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_START_TICK, pData: &pGhost->m_StartTick, Size: sizeof(int));
565 GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_SKIN, pData: &pGhost->m_Skin, Size: sizeof(CGhostSkin));
566 for(int i = 0; i < NumTicks; i++)
567 GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_CHARACTER, pData: pGhost->m_Path.Get(Index: i), Size: sizeof(CGhostCharacter));
568
569 GhostRecorder()->Stop(Ticks: NumTicks, Time: pItem->m_Time);
570}
571
572void CGhost::ConGPlay(IConsole::IResult *pResult, void *pUserData)
573{
574 CGhost *pGhost = (CGhost *)pUserData;
575 pGhost->StartRender(Tick: pGhost->Client()->PredGameTick(Conn: g_Config.m_ClDummy));
576}
577
578void CGhost::OnConsoleInit()
579{
580 m_pGhostLoader = Kernel()->RequestInterface<IGhostLoader>();
581 m_pGhostRecorder = Kernel()->RequestInterface<IGhostRecorder>();
582
583 Console()->Register(pName: "gplay", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConGPlay, pUser: this, pHelp: "Start playback of ghosts");
584}
585
586void CGhost::OnMessage(int MsgType, void *pRawMsg)
587{
588 // check for messages from server
589 if(MsgType == NETMSGTYPE_SV_KILLMSG)
590 {
591 CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
592 if(pMsg->m_Victim == GameClient()->m_Snap.m_LocalClientId)
593 {
594 if(m_Recording)
595 StopRecord();
596 StopRender();
597 m_LastDeathTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
598 }
599 }
600 else if(MsgType == NETMSGTYPE_SV_KILLMSGTEAM)
601 {
602 CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg;
603 for(int i = 0; i < MAX_CLIENTS; i++)
604 {
605 if(GameClient()->m_Teams.Team(ClientId: i) == pMsg->m_Team && i == GameClient()->m_Snap.m_LocalClientId)
606 {
607 if(m_Recording)
608 StopRecord();
609 StopRender();
610 m_LastDeathTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
611 }
612 }
613 }
614 else if(MsgType == NETMSGTYPE_SV_CHAT)
615 {
616 CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
617 if(pMsg->m_ClientId == -1 && m_Recording)
618 {
619 char aName[MAX_NAME_LENGTH];
620 int Time = CRaceHelper::TimeFromFinishMessage(pStr: pMsg->m_pMessage, pNameBuf: aName, NameBufSize: sizeof(aName));
621 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)
622 {
623 StopRecord(Time);
624 StopRender();
625 }
626 }
627 }
628}
629
630void CGhost::OnReset()
631{
632 StopRecord();
633 StopRender();
634 m_LastDeathTick = -1;
635}
636
637void CGhost::OnShutdown()
638{
639 OnReset();
640}
641
642void CGhost::OnMapLoad()
643{
644 OnReset();
645 UnloadAll();
646 GameClient()->m_Menus.GhostlistPopulate();
647 m_AllowRestart = false;
648}
649