1/* (c) Rajh, Redix and Sushi. */
2
3#include <engine/ghost.h>
4#include <engine/shared/config.h>
5#include <engine/storage.h>
6
7#include <game/client/race.h>
8
9#include "ghost.h"
10#include "menus.h"
11#include "players.h"
12#include "skins.h"
13
14#include <game/client/gameclient.h>
15
16const char *CGhost::ms_pGhostDir = "ghosts";
17
18CGhost::CGhost() :
19 m_NewRenderTick(-1), m_StartRenderTick(-1), m_LastDeathTick(-1), m_LastRaceTick(-1), m_Recording(false), m_Rendering(false) {}
20
21void CGhost::GetGhostSkin(CGhostSkin *pSkin, const char *pSkinName, int UseCustomColor, int ColorBody, int ColorFeet)
22{
23 StrToInts(pInts: &pSkin->m_Skin0, NumInts: 6, pStr: pSkinName);
24 pSkin->m_UseCustomColor = UseCustomColor;
25 pSkin->m_ColorBody = ColorBody;
26 pSkin->m_ColorFeet = ColorFeet;
27}
28
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 0;
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 = -m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer;
188 int RenderTick = m_NewRenderTick;
189
190 if(m_LastRaceTick != RaceTick && Client()->GameTick(Conn: g_Config.m_ClDummy) - RaceTick < Client()->GameTickSpeed())
191 {
192 if(m_Rendering && m_RenderingStartedByServer) // race restarted: stop rendering
193 StopRender();
194 if(m_Recording && m_LastRaceTick != -1) // race restarted: activate restarting for local start detection so we have a smooth transition
195 m_AllowRestart = true;
196 if(m_LastRaceTick == -1) // no restart: reset rendering preparations
197 m_NewRenderTick = -1;
198 if(GhostRecorder()->IsRecording()) // race restarted: stop recording
199 GhostRecorder()->Stop(Ticks: 0, Time: -1);
200 int StartTick = RaceTick;
201
202 if(GameClient()->m_GameInfo.m_BugDDRaceGhost) // the client recognizes the start one tick earlier than ddrace servers
203 StartTick--;
204 StartRecord(Tick: StartTick);
205 RenderTick = StartTick;
206 }
207
208 TryRenderStart(Tick: RenderTick, ServerControl: true);
209}
210
211void CGhost::CheckStartLocal(bool Predicted)
212{
213 if(Predicted) // rendering
214 {
215 int RenderTick = m_NewRenderTick;
216
217 vec2 PrevPos = m_pClient->m_PredictedPrevChar.m_Pos;
218 vec2 Pos = m_pClient->m_PredictedChar.m_Pos;
219 if(((!m_Rendering && RenderTick == -1) || m_AllowRestart) && CRaceHelper::IsStart(pClient: m_pClient, Prev: PrevPos, Pos))
220 {
221 if(m_Rendering && !m_RenderingStartedByServer) // race restarted: stop rendering
222 StopRender();
223 RenderTick = Client()->PredGameTick(Conn: g_Config.m_ClDummy);
224 }
225
226 TryRenderStart(Tick: RenderTick, ServerControl: false);
227 }
228 else // recording
229 {
230 int PrevTick = m_pClient->m_Snap.m_pLocalPrevCharacter->m_Tick;
231 int CurTick = m_pClient->m_Snap.m_pLocalCharacter->m_Tick;
232 vec2 PrevPos = vec2(m_pClient->m_Snap.m_pLocalPrevCharacter->m_X, m_pClient->m_Snap.m_pLocalPrevCharacter->m_Y);
233 vec2 Pos = vec2(m_pClient->m_Snap.m_pLocalCharacter->m_X, m_pClient->m_Snap.m_pLocalCharacter->m_Y);
234
235 // detecting death, needed because race allows immediate respawning
236 if((!m_Recording || m_AllowRestart) && m_LastDeathTick < PrevTick)
237 {
238 // estimate the exact start tick
239 int RecordTick = -1;
240 int TickDiff = CurTick - PrevTick;
241 for(int i = 0; i < TickDiff; i++)
242 {
243 if(CRaceHelper::IsStart(pClient: m_pClient, Prev: mix(a: PrevPos, b: Pos, amount: (float)i / TickDiff), Pos: mix(a: PrevPos, b: Pos, amount: (float)(i + 1) / TickDiff)))
244 {
245 RecordTick = PrevTick + i + 1;
246 if(!m_AllowRestart)
247 break;
248 }
249 }
250 if(RecordTick != -1)
251 {
252 if(GhostRecorder()->IsRecording()) // race restarted: stop recording
253 GhostRecorder()->Stop(Ticks: 0, Time: -1);
254 StartRecord(Tick: RecordTick);
255 }
256 }
257 }
258}
259
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 || Client()->State() != IClient::STATE_ONLINE)
275 return;
276 if(!m_pClient->m_Snap.m_pGameInfoObj || m_pClient->m_Snap.m_SpecInfo.m_Active || !m_pClient->m_Snap.m_pLocalCharacter || !m_pClient->m_Snap.m_pLocalPrevCharacter)
277 return;
278
279 bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME;
280 bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl;
281
282 if(g_Config.m_ClRaceGhost)
283 {
284 if(!ServerControl)
285 CheckStartLocal(Predicted: false);
286 else
287 CheckStart();
288
289 if(m_Recording)
290 AddInfos(pChar: m_pClient->m_Snap.m_pLocalCharacter, pDDnetChar: (m_pClient->m_Snap.m_LocalClientId != -1 && m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientId].m_HasExtendedData) ? &m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientId].m_ExtendedData : nullptr);
291 }
292
293 // Record m_LastRaceTick for g_Config.m_ClConfirmDisconnect/QuitTime anyway
294 int RaceTick = -m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer;
295 m_LastRaceTick = RaceFlag ? RaceTick : -1;
296}
297
298void CGhost::OnNewPredictedSnapshot()
299{
300 if(!GameClient()->m_GameInfo.m_Race || !g_Config.m_ClRaceGhost || Client()->State() != IClient::STATE_ONLINE)
301 return;
302 if(!m_pClient->m_Snap.m_pGameInfoObj || m_pClient->m_Snap.m_SpecInfo.m_Active || !m_pClient->m_Snap.m_pLocalCharacter || !m_pClient->m_Snap.m_pLocalPrevCharacter)
303 return;
304
305 bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME;
306 bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl;
307
308 if(!ServerControl)
309 CheckStartLocal(Predicted: true);
310}
311
312void CGhost::OnRender()
313{
314 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
315 return;
316
317 // Play the ghost
318 if(!m_Rendering || !g_Config.m_ClRaceShowGhost)
319 return;
320
321 int PlaybackTick = Client()->PredGameTick(Conn: g_Config.m_ClDummy) - m_StartRenderTick;
322
323 for(auto &Ghost : m_aActiveGhosts)
324 {
325 if(Ghost.Empty())
326 continue;
327
328 int GhostTick = Ghost.m_StartTick + PlaybackTick;
329 while(Ghost.m_PlaybackPos >= 0 && Ghost.m_Path.Get(Index: Ghost.m_PlaybackPos)->m_Tick < GhostTick)
330 {
331 if(Ghost.m_PlaybackPos < Ghost.m_Path.Size() - 1)
332 Ghost.m_PlaybackPos++;
333 else
334 Ghost.m_PlaybackPos = -1;
335 }
336
337 if(Ghost.m_PlaybackPos < 0)
338 continue;
339
340 int CurPos = Ghost.m_PlaybackPos;
341 int PrevPos = maximum(a: 0, b: CurPos - 1);
342 if(Ghost.m_Path.Get(Index: PrevPos)->m_Tick > GhostTick)
343 continue;
344
345 CNetObj_Character Player, Prev;
346 GetNetObjCharacter(pChar: &Player, pGhostChar: Ghost.m_Path.Get(Index: CurPos));
347 GetNetObjCharacter(pChar: &Prev, pGhostChar: Ghost.m_Path.Get(Index: PrevPos));
348
349 int TickDiff = Player.m_Tick - Prev.m_Tick;
350 float IntraTick = 0.f;
351 if(TickDiff > 0)
352 IntraTick = (GhostTick - Prev.m_Tick - 1 + Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy)) / TickDiff;
353
354 Player.m_AttackTick += Client()->GameTick(Conn: g_Config.m_ClDummy) - GhostTick;
355
356 CTeeRenderInfo *pRenderInfo = &Ghost.m_RenderInfo;
357 CTeeRenderInfo GhostNinjaRenderInfo;
358 if(Player.m_Weapon == WEAPON_NINJA && g_Config.m_ClShowNinja)
359 {
360 // change the skin for the ghost to the ninja
361 const auto *pSkin = m_pClient->m_Skins.FindOrNullptr(pName: "x_ninja");
362 if(pSkin != nullptr)
363 {
364 bool IsTeamplay = false;
365 if(m_pClient->m_Snap.m_pGameInfoObj)
366 IsTeamplay = (m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS) != 0;
367
368 GhostNinjaRenderInfo = Ghost.m_RenderInfo;
369 GhostNinjaRenderInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin;
370 GhostNinjaRenderInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin;
371 GhostNinjaRenderInfo.m_BloodColor = pSkin->m_BloodColor;
372 GhostNinjaRenderInfo.m_SkinMetrics = pSkin->m_Metrics;
373 GhostNinjaRenderInfo.m_CustomColoredSkin = IsTeamplay;
374 if(!IsTeamplay)
375 {
376 GhostNinjaRenderInfo.m_ColorBody = ColorRGBA(1, 1, 1);
377 GhostNinjaRenderInfo.m_ColorFeet = ColorRGBA(1, 1, 1);
378 }
379 pRenderInfo = &GhostNinjaRenderInfo;
380 }
381 }
382
383 m_pClient->m_Players.RenderHook(pPrevChar: &Prev, pPlayerChar: &Player, pRenderInfo, ClientId: -2, Intra: IntraTick);
384 m_pClient->m_Players.RenderHookCollLine(pPrevChar: &Prev, pPlayerChar: &Player, ClientId: -2, Intra: IntraTick);
385 m_pClient->m_Players.RenderPlayer(pPrevChar: &Prev, pPlayerChar: &Player, pRenderInfo, ClientId: -2, Intra: IntraTick);
386 }
387}
388
389void CGhost::InitRenderInfos(CGhostItem *pGhost)
390{
391 char aSkinName[24];
392 IntsToStr(pInts: &pGhost->m_Skin.m_Skin0, NumInts: 6, pStr: aSkinName, StrSize: std::size(aSkinName));
393 CTeeRenderInfo *pRenderInfo = &pGhost->m_RenderInfo;
394
395 const CSkin *pSkin = m_pClient->m_Skins.Find(pName: aSkinName);
396 pRenderInfo->m_OriginalRenderSkin = pSkin->m_OriginalSkin;
397 pRenderInfo->m_ColorableRenderSkin = pSkin->m_ColorableSkin;
398 pRenderInfo->m_BloodColor = pSkin->m_BloodColor;
399 pRenderInfo->m_SkinMetrics = pSkin->m_Metrics;
400 pRenderInfo->m_CustomColoredSkin = pGhost->m_Skin.m_UseCustomColor;
401 if(pGhost->m_Skin.m_UseCustomColor)
402 {
403 pRenderInfo->m_ColorBody = color_cast<ColorRGBA>(hsl: ColorHSLA(pGhost->m_Skin.m_ColorBody).UnclampLighting());
404 pRenderInfo->m_ColorFeet = color_cast<ColorRGBA>(hsl: ColorHSLA(pGhost->m_Skin.m_ColorFeet).UnclampLighting());
405 }
406 else
407 {
408 pRenderInfo->m_ColorBody = ColorRGBA(1, 1, 1);
409 pRenderInfo->m_ColorFeet = ColorRGBA(1, 1, 1);
410 }
411
412 pRenderInfo->m_Size = 64;
413}
414
415void CGhost::StartRecord(int Tick)
416{
417 m_Recording = true;
418 m_CurGhost.Reset();
419 m_CurGhost.m_StartTick = Tick;
420
421 const CGameClient::CClientData *pData = &m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId];
422 str_copy(dst&: m_CurGhost.m_aPlayer, src: Client()->PlayerName());
423 GetGhostSkin(pSkin: &m_CurGhost.m_Skin, pSkinName: pData->m_aSkinName, UseCustomColor: pData->m_UseCustomColor, ColorBody: pData->m_ColorBody, ColorFeet: pData->m_ColorFeet);
424 InitRenderInfos(pGhost: &m_CurGhost);
425}
426
427void CGhost::StopRecord(int Time)
428{
429 m_Recording = false;
430 bool RecordingToFile = GhostRecorder()->IsRecording();
431
432 if(RecordingToFile)
433 GhostRecorder()->Stop(Ticks: m_CurGhost.m_Path.Size(), Time);
434
435 CMenus::CGhostItem *pOwnGhost = m_pClient->m_Menus.GetOwnGhost();
436 if(Time > 0 && (!pOwnGhost || Time < pOwnGhost->m_Time || !g_Config.m_ClRaceGhostSaveBest))
437 {
438 // add to active ghosts
439 int Slot = GetSlot();
440 if(Slot != -1 && (!pOwnGhost || Time < pOwnGhost->m_Time))
441 m_aActiveGhosts[Slot] = std::move(m_CurGhost);
442
443 if(pOwnGhost && pOwnGhost->Active() && Time < pOwnGhost->m_Time)
444 Unload(Slot: pOwnGhost->m_Slot);
445
446 // create ghost item
447 CMenus::CGhostItem Item;
448 if(RecordingToFile)
449 GetPath(pBuf: Item.m_aFilename, Size: sizeof(Item.m_aFilename), pPlayerName: m_CurGhost.m_aPlayer, Time);
450 str_copy(dst&: Item.m_aPlayer, src: m_CurGhost.m_aPlayer);
451 Item.m_Time = Time;
452 Item.m_Slot = Slot;
453
454 // save new ghost file
455 if(Item.HasFile())
456 Storage()->RenameFile(pOldFilename: m_aTmpFilename, pNewFilename: Item.m_aFilename, Type: IStorage::TYPE_SAVE);
457
458 // add item to menu list
459 m_pClient->m_Menus.UpdateOwnGhost(Item);
460 }
461 else if(RecordingToFile) // no new record
462 Storage()->RemoveFile(pFilename: m_aTmpFilename, Type: IStorage::TYPE_SAVE);
463
464 m_aTmpFilename[0] = 0;
465
466 m_CurGhost.Reset();
467}
468
469void CGhost::StartRender(int Tick)
470{
471 m_Rendering = true;
472 m_StartRenderTick = Tick;
473 for(auto &Ghost : m_aActiveGhosts)
474 Ghost.m_PlaybackPos = 0;
475}
476
477void CGhost::StopRender()
478{
479 m_Rendering = false;
480 m_NewRenderTick = -1;
481}
482
483int CGhost::Load(const char *pFilename)
484{
485 int Slot = GetSlot();
486 if(Slot == -1)
487 return -1;
488
489 if(GhostLoader()->Load(pFilename, pMap: Client()->GetCurrentMap(), MapSha256: Client()->GetCurrentMapSha256(), MapCrc: Client()->GetCurrentMapCrc()) != 0)
490 return -1;
491
492 const CGhostInfo *pInfo = GhostLoader()->GetInfo();
493
494 if(pInfo->m_NumTicks <= 0 || pInfo->m_Time <= 0)
495 {
496 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost", pStr: "invalid header info");
497 GhostLoader()->Close();
498 return -1;
499 }
500
501 // select ghost
502 CGhostItem *pGhost = &m_aActiveGhosts[Slot];
503 pGhost->Reset();
504 pGhost->m_Path.SetSize(pInfo->m_NumTicks);
505
506 str_copy(dst&: pGhost->m_aPlayer, src: pInfo->m_aOwner);
507
508 int Index = 0;
509 bool FoundSkin = false;
510 bool NoTick = false;
511 bool Error = false;
512
513 int Type;
514 while(!Error && GhostLoader()->ReadNextType(pType: &Type))
515 {
516 if(Index == pInfo->m_NumTicks && (Type == GHOSTDATA_TYPE_CHARACTER || Type == GHOSTDATA_TYPE_CHARACTER_NO_TICK))
517 {
518 Error = true;
519 break;
520 }
521
522 if(Type == GHOSTDATA_TYPE_SKIN && !FoundSkin)
523 {
524 FoundSkin = true;
525 if(!GhostLoader()->ReadData(Type, pData: &pGhost->m_Skin, Size: sizeof(CGhostSkin)))
526 Error = true;
527 }
528 else if(Type == GHOSTDATA_TYPE_CHARACTER_NO_TICK)
529 {
530 NoTick = true;
531 if(!GhostLoader()->ReadData(Type, pData: pGhost->m_Path.Get(Index: Index++), Size: sizeof(CGhostCharacter_NoTick)))
532 Error = true;
533 }
534 else if(Type == GHOSTDATA_TYPE_CHARACTER)
535 {
536 if(!GhostLoader()->ReadData(Type, pData: pGhost->m_Path.Get(Index: Index++), Size: sizeof(CGhostCharacter)))
537 Error = true;
538 }
539 else if(Type == GHOSTDATA_TYPE_START_TICK)
540 {
541 if(!GhostLoader()->ReadData(Type, pData: &pGhost->m_StartTick, Size: sizeof(int)))
542 Error = true;
543 }
544 }
545
546 GhostLoader()->Close();
547
548 if(Error || Index != pInfo->m_NumTicks)
549 {
550 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost", pStr: "invalid ghost data");
551 pGhost->Reset();
552 return -1;
553 }
554
555 if(NoTick)
556 {
557 int StartTick = 0;
558 for(int i = 1; i < pInfo->m_NumTicks; i++) // estimate start tick
559 if(pGhost->m_Path.Get(Index: i)->m_AttackTick != pGhost->m_Path.Get(Index: i - 1)->m_AttackTick)
560 StartTick = pGhost->m_Path.Get(Index: i)->m_AttackTick - i;
561 for(int i = 0; i < pInfo->m_NumTicks; i++)
562 pGhost->m_Path.Get(Index: i)->m_Tick = StartTick + i;
563 }
564
565 if(pGhost->m_StartTick == -1)
566 pGhost->m_StartTick = pGhost->m_Path.Get(Index: 0)->m_Tick;
567
568 if(!FoundSkin)
569 GetGhostSkin(pSkin: &pGhost->m_Skin, pSkinName: "default", UseCustomColor: 0, ColorBody: 0, ColorFeet: 0);
570 InitRenderInfos(pGhost);
571
572 return Slot;
573}
574
575void CGhost::Unload(int Slot)
576{
577 m_aActiveGhosts[Slot].Reset();
578}
579
580void CGhost::UnloadAll()
581{
582 for(int i = 0; i < MAX_ACTIVE_GHOSTS; i++)
583 Unload(Slot: i);
584}
585
586void CGhost::SaveGhost(CMenus::CGhostItem *pItem)
587{
588 int Slot = pItem->m_Slot;
589 if(!pItem->Active() || pItem->HasFile() || m_aActiveGhosts[Slot].Empty() || GhostRecorder()->IsRecording())
590 return;
591
592 CGhostItem *pGhost = &m_aActiveGhosts[Slot];
593
594 int NumTicks = pGhost->m_Path.Size();
595 GetPath(pBuf: pItem->m_aFilename, Size: sizeof(pItem->m_aFilename), pPlayerName: pItem->m_aPlayer, Time: pItem->m_Time);
596 GhostRecorder()->Start(pFilename: pItem->m_aFilename, pMap: Client()->GetCurrentMap(), MapSha256: Client()->GetCurrentMapSha256(), pName: pItem->m_aPlayer);
597
598 GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_START_TICK, pData: &pGhost->m_StartTick, Size: sizeof(int));
599 GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_SKIN, pData: &pGhost->m_Skin, Size: sizeof(CGhostSkin));
600 for(int i = 0; i < NumTicks; i++)
601 GhostRecorder()->WriteData(Type: GHOSTDATA_TYPE_CHARACTER, pData: pGhost->m_Path.Get(Index: i), Size: sizeof(CGhostCharacter));
602
603 GhostRecorder()->Stop(Ticks: NumTicks, Time: pItem->m_Time);
604}
605
606void CGhost::ConGPlay(IConsole::IResult *pResult, void *pUserData)
607{
608 CGhost *pGhost = (CGhost *)pUserData;
609 pGhost->StartRender(Tick: pGhost->Client()->PredGameTick(Conn: g_Config.m_ClDummy));
610}
611
612void CGhost::OnConsoleInit()
613{
614 m_pGhostLoader = Kernel()->RequestInterface<IGhostLoader>();
615 m_pGhostRecorder = Kernel()->RequestInterface<IGhostRecorder>();
616
617 Console()->Register(pName: "gplay", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConGPlay, pUser: this, pHelp: "Start playback of ghosts");
618}
619
620void CGhost::OnMessage(int MsgType, void *pRawMsg)
621{
622 // check for messages from server
623 if(MsgType == NETMSGTYPE_SV_KILLMSG)
624 {
625 CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
626 if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalClientId)
627 {
628 if(m_Recording)
629 StopRecord();
630 StopRender();
631 m_LastDeathTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
632 }
633 }
634 else if(MsgType == NETMSGTYPE_SV_KILLMSGTEAM)
635 {
636 CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg;
637 for(int i = 0; i < MAX_CLIENTS; i++)
638 {
639 if(m_pClient->m_Teams.Team(ClientId: i) == pMsg->m_Team && i == m_pClient->m_Snap.m_LocalClientId)
640 {
641 if(m_Recording)
642 StopRecord();
643 StopRender();
644 m_LastDeathTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
645 }
646 }
647 }
648 else if(MsgType == NETMSGTYPE_SV_CHAT)
649 {
650 CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
651 if(pMsg->m_ClientId == -1 && m_Recording)
652 {
653 char aName[MAX_NAME_LENGTH];
654 int Time = CRaceHelper::TimeFromFinishMessage(pStr: pMsg->m_pMessage, pNameBuf: aName, NameBufSize: sizeof(aName));
655 if(Time > 0 && m_pClient->m_Snap.m_LocalClientId >= 0 && str_comp(a: aName, b: m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId].m_aName) == 0)
656 {
657 StopRecord(Time);
658 StopRender();
659 }
660 }
661 }
662}
663
664void CGhost::OnReset()
665{
666 StopRecord();
667 StopRender();
668 m_LastDeathTick = -1;
669 m_LastRaceTick = -1;
670}
671
672void CGhost::OnShutdown()
673{
674 OnReset();
675}
676
677void CGhost::OnMapLoad()
678{
679 OnReset();
680 UnloadAll();
681 m_pClient->m_Menus.GhostlistPopulate();
682 m_AllowRestart = false;
683}
684
685int CGhost::GetLastRaceTick() const
686{
687 return m_LastRaceTick;
688}
689
690void CGhost::OnRefreshSkins()
691{
692 const auto &&RefindSkin = [&](auto &Ghost) {
693 if(Ghost.Empty())
694 return;
695 char aSkinName[24];
696 IntsToStr(&Ghost.m_Skin.m_Skin0, 6, aSkinName, std::size(aSkinName));
697 CTeeRenderInfo *pRenderInfo = &Ghost.m_RenderInfo;
698 if(aSkinName[0] != '\0')
699 {
700 const CSkin *pSkin = m_pClient->m_Skins.Find(pName: aSkinName);
701 pRenderInfo->m_OriginalRenderSkin = pSkin->m_OriginalSkin;
702 pRenderInfo->m_ColorableRenderSkin = pSkin->m_ColorableSkin;
703 pRenderInfo->m_BloodColor = pSkin->m_BloodColor;
704 pRenderInfo->m_SkinMetrics = pSkin->m_Metrics;
705 }
706 else
707 {
708 pRenderInfo->m_OriginalRenderSkin.Reset();
709 pRenderInfo->m_ColorableRenderSkin.Reset();
710 }
711 };
712
713 for(auto &Ghost : m_aActiveGhosts)
714 {
715 RefindSkin(Ghost);
716 }
717 RefindSkin(m_CurGhost);
718}
719