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