| 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 | |
| 17 | const char *CGhost::ms_pGhostDir = "ghosts" ; |
| 18 | |
| 19 | static const LOG_COLOR LOG_COLOR_GHOST{.r: 165, .g: 153, .b: 153}; |
| 20 | |
| 21 | void 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 | |
| 29 | void 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 | |
| 50 | void 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 | |
| 68 | CGhost::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 | |
| 75 | CGhost::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 | |
| 85 | void 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 | |
| 94 | void 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 | |
| 109 | void CGhost::CGhostPath::Add(const CGhostCharacter &Char) |
| 110 | { |
| 111 | SetSize(m_NumItems + 1); |
| 112 | *Get(Index: m_NumItems - 1) = Char; |
| 113 | } |
| 114 | |
| 115 | CGhostCharacter *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 | |
| 125 | void 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 | |
| 145 | void 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 | |
| 168 | int 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 | |
| 176 | int 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 | |
| 185 | void 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 | |
| 211 | void 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 | |
| 260 | void 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 | |
| 272 | void 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 | |
| 291 | void 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 | |
| 305 | void 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 | |
| 370 | void 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 | |
| 387 | void 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 | |
| 399 | void 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 | |
| 440 | void 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 | |
| 448 | void CGhost::StopRender() |
| 449 | { |
| 450 | m_Rendering = false; |
| 451 | m_NewRenderTick = -1; |
| 452 | } |
| 453 | |
| 454 | int 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 | |
| 541 | void CGhost::Unload(int Slot) |
| 542 | { |
| 543 | m_aActiveGhosts[Slot].Reset(); |
| 544 | } |
| 545 | |
| 546 | void CGhost::UnloadAll() |
| 547 | { |
| 548 | for(int i = 0; i < MAX_ACTIVE_GHOSTS; i++) |
| 549 | Unload(Slot: i); |
| 550 | } |
| 551 | |
| 552 | void CGhost::(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 | |
| 572 | void 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 | |
| 578 | void 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 | |
| 586 | void 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 | |
| 630 | void CGhost::OnReset() |
| 631 | { |
| 632 | StopRecord(); |
| 633 | StopRender(); |
| 634 | m_LastDeathTick = -1; |
| 635 | } |
| 636 | |
| 637 | void CGhost::OnShutdown() |
| 638 | { |
| 639 | OnReset(); |
| 640 | } |
| 641 | |
| 642 | void CGhost::OnMapLoad() |
| 643 | { |
| 644 | OnReset(); |
| 645 | UnloadAll(); |
| 646 | GameClient()->m_Menus.GhostlistPopulate(); |
| 647 | m_AllowRestart = false; |
| 648 | } |
| 649 | |