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