mirror of
https://github.com/ddnet/ddnet.git
synced 2024-11-13 03:28:19 +00:00
f31e081bd4
OK, maybe not actually remove because it is kept for fallback when the new method isn't available. The whole gametype parsing business had the same downsides as user agent parsing on the web, hence I removed it while keeping behavior the same. This allows servers to explicitly opt in or out of certain bug workarounds and other client behavior. This increases the complexity of different configurations that are available in the client (which is a bad thing).
626 lines
18 KiB
C++
626 lines
18 KiB
C++
/* (c) Rajh, Redix and Sushi. */
|
|
|
|
#include <engine/ghost.h>
|
|
#include <engine/serverbrowser.h>
|
|
#include <engine/storage.h>
|
|
#include <engine/shared/config.h>
|
|
|
|
#include <game/client/race.h>
|
|
|
|
#include "players.h"
|
|
#include "skins.h"
|
|
#include "menus.h"
|
|
#include "ghost.h"
|
|
|
|
const char *CGhost::ms_pGhostDir = "ghosts";
|
|
|
|
CGhost::CGhost() : m_NewRenderTick(-1), m_StartRenderTick(-1), m_LastDeathTick(-1), m_LastRaceTick(-1), m_Recording(false), m_Rendering(false) {}
|
|
|
|
void CGhost::GetGhostSkin(CGhostSkin *pSkin, const char *pSkinName, int UseCustomColor, int ColorBody, int ColorFeet)
|
|
{
|
|
StrToInts(&pSkin->m_Skin0, 6, pSkinName);
|
|
pSkin->m_UseCustomColor = UseCustomColor;
|
|
pSkin->m_ColorBody = ColorBody;
|
|
pSkin->m_ColorFeet = ColorFeet;
|
|
}
|
|
|
|
void CGhost::GetGhostCharacter(CGhostCharacter *pGhostChar, const CNetObj_Character *pChar)
|
|
{
|
|
pGhostChar->m_X = pChar->m_X;
|
|
pGhostChar->m_Y = pChar->m_Y;
|
|
pGhostChar->m_VelX = pChar->m_VelX;
|
|
pGhostChar->m_VelY = 0;
|
|
pGhostChar->m_Angle = pChar->m_Angle;
|
|
pGhostChar->m_Direction = pChar->m_Direction;
|
|
pGhostChar->m_Weapon = pChar->m_Weapon;
|
|
pGhostChar->m_HookState = pChar->m_HookState;
|
|
pGhostChar->m_HookX = pChar->m_HookX;
|
|
pGhostChar->m_HookY = pChar->m_HookY;
|
|
pGhostChar->m_AttackTick = pChar->m_AttackTick;
|
|
pGhostChar->m_Tick = pChar->m_Tick;
|
|
}
|
|
|
|
void CGhost::GetNetObjCharacter(CNetObj_Character *pChar, const CGhostCharacter *pGhostChar)
|
|
{
|
|
mem_zero(pChar, sizeof(CNetObj_Character));
|
|
pChar->m_X = pGhostChar->m_X;
|
|
pChar->m_Y = pGhostChar->m_Y;
|
|
pChar->m_VelX = pGhostChar->m_VelX;
|
|
pChar->m_VelY = 0;
|
|
pChar->m_Angle = pGhostChar->m_Angle;
|
|
pChar->m_Direction = pGhostChar->m_Direction;
|
|
pChar->m_Weapon = pGhostChar->m_Weapon == WEAPON_GRENADE ? WEAPON_GRENADE : WEAPON_GUN;
|
|
pChar->m_HookState = pGhostChar->m_HookState;
|
|
pChar->m_HookX = pGhostChar->m_HookX;
|
|
pChar->m_HookY = pGhostChar->m_HookY;
|
|
pChar->m_AttackTick = pGhostChar->m_AttackTick;
|
|
pChar->m_HookedPlayer = -1;
|
|
pChar->m_Tick = pGhostChar->m_Tick;
|
|
}
|
|
|
|
CGhost::CGhostPath::CGhostPath(CGhostPath &&Other)
|
|
: m_ChunkSize(Other.m_ChunkSize), m_NumItems(Other.m_NumItems), m_lChunks(std::move(Other.m_lChunks))
|
|
{
|
|
Other.m_NumItems = 0;
|
|
Other.m_lChunks.clear();
|
|
}
|
|
|
|
CGhost::CGhostPath &CGhost::CGhostPath::operator = (CGhostPath &&Other)
|
|
{
|
|
Reset(Other.m_ChunkSize);
|
|
m_NumItems = Other.m_NumItems;
|
|
m_lChunks = std::move(Other.m_lChunks);
|
|
Other.m_NumItems = 0;
|
|
Other.m_lChunks.clear();
|
|
return *this;
|
|
}
|
|
|
|
void CGhost::CGhostPath::Reset(int ChunkSize)
|
|
{
|
|
for(unsigned i = 0; i < m_lChunks.size(); i++)
|
|
free(m_lChunks[i]);
|
|
m_lChunks.clear();
|
|
m_ChunkSize = ChunkSize;
|
|
m_NumItems = 0;
|
|
}
|
|
|
|
void CGhost::CGhostPath::SetSize(int Items)
|
|
{
|
|
int Chunks = m_lChunks.size();
|
|
int NeededChunks = (Items + m_ChunkSize - 1) / m_ChunkSize;
|
|
|
|
if(NeededChunks > Chunks)
|
|
{
|
|
m_lChunks.resize(NeededChunks);
|
|
for(int i = Chunks; i < NeededChunks; i++)
|
|
m_lChunks[i] = (CGhostCharacter *)calloc(m_ChunkSize, sizeof(CGhostCharacter));
|
|
}
|
|
|
|
m_NumItems = Items;
|
|
}
|
|
|
|
void CGhost::CGhostPath::Add(CGhostCharacter Char)
|
|
{
|
|
SetSize(m_NumItems + 1);
|
|
*Get(m_NumItems - 1) = Char;
|
|
}
|
|
|
|
CGhostCharacter *CGhost::CGhostPath::Get(int Index)
|
|
{
|
|
if(Index < 0 || Index >= m_NumItems)
|
|
return 0;
|
|
|
|
int Chunk = Index / m_ChunkSize;
|
|
int Pos = Index % m_ChunkSize;
|
|
return &m_lChunks[Chunk][Pos];
|
|
}
|
|
|
|
void CGhost::GetPath(char *pBuf, int Size, const char *pPlayerName, int Time) const
|
|
{
|
|
const char *pMap = Client()->GetCurrentMap();
|
|
unsigned Crc = Client()->GetMapCrc();
|
|
|
|
char aPlayerName[MAX_NAME_LENGTH];
|
|
str_copy(aPlayerName, pPlayerName, sizeof(aPlayerName));
|
|
str_sanitize_filename(aPlayerName);
|
|
|
|
if(Time < 0)
|
|
str_format(pBuf, Size, "%s/%s_%s_%08x_tmp_%d.gho", ms_pGhostDir, pMap, aPlayerName, Crc, pid());
|
|
else
|
|
str_format(pBuf, Size, "%s/%s_%s_%d.%03d_%08x.gho", ms_pGhostDir, pMap, aPlayerName, Time / 1000, Time % 1000, Crc);
|
|
}
|
|
|
|
void CGhost::AddInfos(const CNetObj_Character *pChar)
|
|
{
|
|
int NumTicks = m_CurGhost.m_Path.Size();
|
|
|
|
// do not start writing to file as long as we still touch the start line
|
|
if(g_Config.m_ClRaceSaveGhost && !GhostRecorder()->IsRecording() && NumTicks > 0)
|
|
{
|
|
GetPath(m_aTmpFilename, sizeof(m_aTmpFilename), m_CurGhost.m_aPlayer);
|
|
GhostRecorder()->Start(m_aTmpFilename, Client()->GetCurrentMap(), Client()->GetMapCrc(), m_CurGhost.m_aPlayer);
|
|
|
|
GhostRecorder()->WriteData(GHOSTDATA_TYPE_START_TICK, &m_CurGhost.m_StartTick, sizeof(int));
|
|
GhostRecorder()->WriteData(GHOSTDATA_TYPE_SKIN, &m_CurGhost.m_Skin, sizeof(CGhostSkin));
|
|
for(int i = 0; i < NumTicks; i++)
|
|
GhostRecorder()->WriteData(GHOSTDATA_TYPE_CHARACTER, m_CurGhost.m_Path.Get(i), sizeof(CGhostCharacter));
|
|
}
|
|
|
|
CGhostCharacter GhostChar;
|
|
GetGhostCharacter(&GhostChar, pChar);
|
|
m_CurGhost.m_Path.Add(GhostChar);
|
|
if(GhostRecorder()->IsRecording())
|
|
GhostRecorder()->WriteData(GHOSTDATA_TYPE_CHARACTER, &GhostChar, sizeof(CGhostCharacter));
|
|
}
|
|
|
|
int CGhost::GetSlot() const
|
|
{
|
|
for(int i = 0; i < MAX_ACTIVE_GHOSTS; i++)
|
|
if(m_aActiveGhosts[i].Empty())
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
int CGhost::FreeSlots() const
|
|
{
|
|
int Num = 0;
|
|
for(int i = 0; i < MAX_ACTIVE_GHOSTS; i++)
|
|
if(m_aActiveGhosts[i].Empty())
|
|
Num++;
|
|
return Num;
|
|
}
|
|
|
|
void CGhost::CheckStart()
|
|
{
|
|
int RaceTick = -m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer;
|
|
int RenderTick = m_NewRenderTick;
|
|
|
|
if(m_LastRaceTick != RaceTick && Client()->GameTick() - RaceTick < Client()->GameTickSpeed())
|
|
{
|
|
if(m_Rendering && m_RenderingStartedByServer) // race restarted: stop rendering
|
|
StopRender();
|
|
if(m_Recording && m_LastRaceTick != -1) // race restarted: activate restarting for local start detection so we have a smooth transition
|
|
m_AllowRestart = true;
|
|
if(m_LastRaceTick == -1) // no restart: reset rendering preparations
|
|
m_NewRenderTick = -1;
|
|
if(GhostRecorder()->IsRecording()) // race restarted: stop recording
|
|
GhostRecorder()->Stop(0, -1);
|
|
int StartTick = RaceTick;
|
|
|
|
if(GameClient()->m_GameInfo.m_BugDDRaceGhost) // the client recognizes the start one tick earlier than ddrace servers
|
|
StartTick--;
|
|
StartRecord(StartTick);
|
|
RenderTick = StartTick;
|
|
}
|
|
|
|
TryRenderStart(RenderTick, true);
|
|
}
|
|
|
|
void CGhost::CheckStartLocal(bool Predicted)
|
|
{
|
|
if(Predicted) // rendering
|
|
{
|
|
int RenderTick = m_NewRenderTick;
|
|
|
|
vec2 PrevPos = m_pClient->m_PredictedPrevChar.m_Pos;
|
|
vec2 Pos = m_pClient->m_PredictedChar.m_Pos;
|
|
if(((!m_Rendering && RenderTick == -1) || m_AllowRestart) && CRaceHelper::IsStart(m_pClient, PrevPos, Pos))
|
|
{
|
|
if(m_Rendering && !m_RenderingStartedByServer) // race restarted: stop rendering
|
|
StopRender();
|
|
RenderTick = Client()->PredGameTick();
|
|
}
|
|
|
|
TryRenderStart(RenderTick, false);
|
|
}
|
|
else // recording
|
|
{
|
|
int PrevTick = m_pClient->m_Snap.m_pLocalPrevCharacter->m_Tick;
|
|
int CurTick = m_pClient->m_Snap.m_pLocalCharacter->m_Tick;
|
|
vec2 PrevPos = vec2(m_pClient->m_Snap.m_pLocalPrevCharacter->m_X, m_pClient->m_Snap.m_pLocalPrevCharacter->m_Y);
|
|
vec2 Pos = vec2(m_pClient->m_Snap.m_pLocalCharacter->m_X, m_pClient->m_Snap.m_pLocalCharacter->m_Y);
|
|
|
|
// detecting death, needed because race allows immediate respawning
|
|
if((!m_Recording || m_AllowRestart) && m_LastDeathTick < PrevTick)
|
|
{
|
|
// estimate the exact start tick
|
|
int RecordTick = -1;
|
|
int TickDiff = CurTick - PrevTick;
|
|
for(int i = 0; i < TickDiff; i++)
|
|
{
|
|
if(CRaceHelper::IsStart(m_pClient, mix(PrevPos, Pos, (float)i / TickDiff), mix(PrevPos, Pos, (float)(i + 1) / TickDiff)))
|
|
{
|
|
RecordTick = PrevTick + i + 1;
|
|
if(!m_AllowRestart)
|
|
break;
|
|
}
|
|
}
|
|
if(RecordTick != -1)
|
|
{
|
|
if(GhostRecorder()->IsRecording()) // race restarted: stop recording
|
|
GhostRecorder()->Stop(0, -1);
|
|
StartRecord(RecordTick);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CGhost::TryRenderStart(int Tick, bool ServerControl)
|
|
{
|
|
// only restart rendering if it did not change since last tick to prevent stuttering
|
|
if(m_NewRenderTick != -1 && m_NewRenderTick == Tick)
|
|
{
|
|
StartRender(Tick);
|
|
Tick = -1;
|
|
m_RenderingStartedByServer = ServerControl;
|
|
}
|
|
m_NewRenderTick = Tick;
|
|
}
|
|
|
|
void CGhost::OnNewSnapshot()
|
|
{
|
|
if(!GameClient()->m_GameInfo.m_Race || Client()->State() != IClient::STATE_ONLINE)
|
|
return;
|
|
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)
|
|
return;
|
|
|
|
bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags&GAMESTATEFLAG_RACETIME;
|
|
bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl;
|
|
|
|
if(g_Config.m_ClRaceGhost)
|
|
{
|
|
if(!ServerControl)
|
|
CheckStartLocal(false);
|
|
else
|
|
CheckStart();
|
|
|
|
if(m_Recording)
|
|
AddInfos(m_pClient->m_Snap.m_pLocalCharacter);
|
|
}
|
|
|
|
// Record m_LastRaceTick for g_Config.m_ClConfirmDisconnect/QuitTime anyway
|
|
int RaceTick = -m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer;
|
|
m_LastRaceTick = RaceFlag ? RaceTick : -1;
|
|
}
|
|
|
|
void CGhost::OnNewPredictedSnapshot()
|
|
{
|
|
if(!GameClient()->m_GameInfo.m_Race || !g_Config.m_ClRaceGhost || Client()->State() != IClient::STATE_ONLINE)
|
|
return;
|
|
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)
|
|
return;
|
|
|
|
bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags&GAMESTATEFLAG_RACETIME;
|
|
bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl;
|
|
|
|
if(!ServerControl)
|
|
CheckStartLocal(true);
|
|
}
|
|
|
|
void CGhost::OnRender()
|
|
{
|
|
// Play the ghost
|
|
if(!m_Rendering || !g_Config.m_ClRaceShowGhost)
|
|
return;
|
|
|
|
int PlaybackTick = Client()->PredGameTick() - m_StartRenderTick;
|
|
|
|
for(int i = 0; i < MAX_ACTIVE_GHOSTS; i++)
|
|
{
|
|
CGhostItem *pGhost = &m_aActiveGhosts[i];
|
|
if(pGhost->Empty())
|
|
continue;
|
|
|
|
int GhostTick = pGhost->m_StartTick + PlaybackTick;
|
|
while(pGhost->m_PlaybackPos >= 0 && pGhost->m_Path.Get(pGhost->m_PlaybackPos)->m_Tick < GhostTick)
|
|
{
|
|
if(pGhost->m_PlaybackPos < pGhost->m_Path.Size() - 1)
|
|
pGhost->m_PlaybackPos++;
|
|
else
|
|
pGhost->m_PlaybackPos = -1;
|
|
}
|
|
|
|
if(pGhost->m_PlaybackPos < 0)
|
|
continue;
|
|
|
|
int CurPos = pGhost->m_PlaybackPos;
|
|
int PrevPos = maximum(0, CurPos - 1);
|
|
if(pGhost->m_Path.Get(PrevPos)->m_Tick > GhostTick)
|
|
continue;
|
|
|
|
CNetObj_Character Player, Prev;
|
|
GetNetObjCharacter(&Player, pGhost->m_Path.Get(CurPos));
|
|
GetNetObjCharacter(&Prev, pGhost->m_Path.Get(PrevPos));
|
|
|
|
int TickDiff = Player.m_Tick - Prev.m_Tick;
|
|
float IntraTick = 0.f;
|
|
if(TickDiff > 0)
|
|
IntraTick = (GhostTick - Prev.m_Tick - 1 + Client()->PredIntraGameTick()) / TickDiff;
|
|
|
|
Player.m_AttackTick += Client()->GameTick() - GhostTick;
|
|
|
|
m_pClient->m_pPlayers->RenderHook(&Prev, &Player, &pGhost->m_RenderInfo , -2, IntraTick);
|
|
m_pClient->m_pPlayers->RenderPlayer(&Prev, &Player, &pGhost->m_RenderInfo, -2, IntraTick);
|
|
}
|
|
}
|
|
|
|
void CGhost::InitRenderInfos(CGhostItem *pGhost)
|
|
{
|
|
char aSkinName[64];
|
|
IntsToStr(&pGhost->m_Skin.m_Skin0, 6, aSkinName);
|
|
CTeeRenderInfo *pRenderInfo = &pGhost->m_RenderInfo;
|
|
|
|
int SkinId = m_pClient->m_pSkins->Find(aSkinName);
|
|
|
|
if(pGhost->m_Skin.m_UseCustomColor)
|
|
{
|
|
pRenderInfo->m_Texture = m_pClient->m_pSkins->Get(SkinId)->m_ColorTexture;
|
|
pRenderInfo->m_ColorBody = color_cast<ColorRGBA>(ColorHSLA(pGhost->m_Skin.m_ColorBody));
|
|
pRenderInfo->m_ColorFeet = color_cast<ColorRGBA>(ColorHSLA(pGhost->m_Skin.m_ColorFeet));
|
|
}
|
|
else
|
|
{
|
|
pRenderInfo->m_Texture = m_pClient->m_pSkins->Get(SkinId)->m_OrgTexture;
|
|
pRenderInfo->m_ColorBody = ColorRGBA(1, 1, 1);
|
|
pRenderInfo->m_ColorFeet = ColorRGBA(1, 1, 1);
|
|
}
|
|
|
|
pRenderInfo->m_Size = 64;
|
|
}
|
|
|
|
void CGhost::StartRecord(int Tick)
|
|
{
|
|
m_Recording = true;
|
|
m_CurGhost.Reset();
|
|
m_CurGhost.m_StartTick = Tick;
|
|
|
|
const CGameClient::CClientData *pData = &m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID];
|
|
str_copy(m_CurGhost.m_aPlayer, g_Config.m_PlayerName, sizeof(m_CurGhost.m_aPlayer));
|
|
GetGhostSkin(&m_CurGhost.m_Skin, pData->m_aSkinName, pData->m_UseCustomColor, pData->m_ColorBody, pData->m_ColorFeet);
|
|
InitRenderInfos(&m_CurGhost);
|
|
}
|
|
|
|
void CGhost::StopRecord(int Time)
|
|
{
|
|
m_Recording = false;
|
|
bool RecordingToFile = GhostRecorder()->IsRecording();
|
|
|
|
if(RecordingToFile)
|
|
GhostRecorder()->Stop(m_CurGhost.m_Path.Size(), Time);
|
|
|
|
CMenus::CGhostItem *pOwnGhost = m_pClient->m_pMenus->GetOwnGhost();
|
|
if(Time > 0 && (!pOwnGhost || Time < pOwnGhost->m_Time))
|
|
{
|
|
if(pOwnGhost && pOwnGhost->Active())
|
|
Unload(pOwnGhost->m_Slot);
|
|
|
|
// add to active ghosts
|
|
int Slot = GetSlot();
|
|
if(Slot != -1)
|
|
m_aActiveGhosts[Slot] = std::move(m_CurGhost);
|
|
|
|
// create ghost item
|
|
CMenus::CGhostItem Item;
|
|
if(RecordingToFile)
|
|
GetPath(Item.m_aFilename, sizeof(Item.m_aFilename), m_CurGhost.m_aPlayer, Time);
|
|
str_copy(Item.m_aPlayer, m_CurGhost.m_aPlayer, sizeof(Item.m_aPlayer));
|
|
Item.m_Time = Time;
|
|
Item.m_Slot = Slot;
|
|
|
|
// save new ghost file
|
|
if(Item.HasFile())
|
|
Storage()->RenameFile(m_aTmpFilename, Item.m_aFilename, IStorage::TYPE_SAVE);
|
|
|
|
// add item to menu list
|
|
m_pClient->m_pMenus->UpdateOwnGhost(Item);
|
|
}
|
|
else if(RecordingToFile) // no new record
|
|
Storage()->RemoveFile(m_aTmpFilename, IStorage::TYPE_SAVE);
|
|
|
|
m_aTmpFilename[0] = 0;
|
|
|
|
m_CurGhost.Reset();
|
|
}
|
|
|
|
void CGhost::StartRender(int Tick)
|
|
{
|
|
m_Rendering = true;
|
|
m_StartRenderTick = Tick;
|
|
for(int i = 0; i < MAX_ACTIVE_GHOSTS; i++)
|
|
m_aActiveGhosts[i].m_PlaybackPos = 0;
|
|
}
|
|
|
|
void CGhost::StopRender()
|
|
{
|
|
m_Rendering = false;
|
|
m_NewRenderTick = -1;
|
|
}
|
|
|
|
int CGhost::Load(const char *pFilename)
|
|
{
|
|
int Slot = GetSlot();
|
|
if(Slot == -1)
|
|
return -1;
|
|
|
|
if(GhostLoader()->Load(pFilename, Client()->GetCurrentMap(), Client()->GetMapCrc()) != 0)
|
|
return -1;
|
|
|
|
const CGhostHeader *pHeader = GhostLoader()->GetHeader();
|
|
|
|
int NumTicks = pHeader->GetTicks();
|
|
int Time = pHeader->GetTime();
|
|
if(NumTicks <= 0 || Time <= 0)
|
|
{
|
|
GhostLoader()->Close();
|
|
return -1;
|
|
}
|
|
|
|
// select ghost
|
|
CGhostItem *pGhost = &m_aActiveGhosts[Slot];
|
|
pGhost->Reset();
|
|
pGhost->m_Path.SetSize(NumTicks);
|
|
|
|
str_copy(pGhost->m_aPlayer, pHeader->m_aOwner, sizeof(pGhost->m_aPlayer));
|
|
|
|
int Index = 0;
|
|
bool FoundSkin = false;
|
|
bool NoTick = false;
|
|
bool Error = false;
|
|
|
|
int Type;
|
|
while(!Error && GhostLoader()->ReadNextType(&Type))
|
|
{
|
|
if(Index == NumTicks && (Type == GHOSTDATA_TYPE_CHARACTER || Type == GHOSTDATA_TYPE_CHARACTER_NO_TICK))
|
|
{
|
|
Error = true;
|
|
break;
|
|
}
|
|
|
|
if(Type == GHOSTDATA_TYPE_SKIN && !FoundSkin)
|
|
{
|
|
FoundSkin = true;
|
|
if(!GhostLoader()->ReadData(Type, &pGhost->m_Skin, sizeof(CGhostSkin)))
|
|
Error = true;
|
|
}
|
|
else if(Type == GHOSTDATA_TYPE_CHARACTER_NO_TICK)
|
|
{
|
|
NoTick = true;
|
|
if(!GhostLoader()->ReadData(Type, pGhost->m_Path.Get(Index++), sizeof(CGhostCharacter_NoTick)))
|
|
Error = true;
|
|
}
|
|
else if(Type == GHOSTDATA_TYPE_CHARACTER)
|
|
{
|
|
if(!GhostLoader()->ReadData(Type, pGhost->m_Path.Get(Index++), sizeof(CGhostCharacter)))
|
|
Error = true;
|
|
}
|
|
else if(Type == GHOSTDATA_TYPE_START_TICK)
|
|
{
|
|
if(!GhostLoader()->ReadData(Type, &pGhost->m_StartTick, sizeof(int)))
|
|
Error = true;
|
|
}
|
|
}
|
|
|
|
GhostLoader()->Close();
|
|
|
|
if(Error || Index != NumTicks)
|
|
{
|
|
pGhost->Reset();
|
|
return -1;
|
|
}
|
|
|
|
if(NoTick)
|
|
{
|
|
int StartTick = 0;
|
|
for(int i = 1; i < NumTicks; i++) // estimate start tick
|
|
if(pGhost->m_Path.Get(i)->m_AttackTick != pGhost->m_Path.Get(i - 1)->m_AttackTick)
|
|
StartTick = pGhost->m_Path.Get(i)->m_AttackTick - i;
|
|
for(int i = 0; i < NumTicks; i++)
|
|
pGhost->m_Path.Get(i)->m_Tick = StartTick + i;
|
|
}
|
|
|
|
if(pGhost->m_StartTick == -1)
|
|
pGhost->m_StartTick = pGhost->m_Path.Get(0)->m_Tick;
|
|
|
|
if(!FoundSkin)
|
|
GetGhostSkin(&pGhost->m_Skin, "default", 0, 0, 0);
|
|
InitRenderInfos(pGhost);
|
|
|
|
return Slot;
|
|
}
|
|
|
|
void CGhost::Unload(int Slot)
|
|
{
|
|
m_aActiveGhosts[Slot].Reset();
|
|
}
|
|
|
|
void CGhost::UnloadAll()
|
|
{
|
|
for(int i = 0; i < MAX_ACTIVE_GHOSTS; i++)
|
|
Unload(i);
|
|
}
|
|
|
|
void CGhost::SaveGhost(CMenus::CGhostItem *pItem)
|
|
{
|
|
int Slot = pItem->m_Slot;
|
|
if(!pItem->Active() || pItem->HasFile() || m_aActiveGhosts[Slot].Empty() || GhostRecorder()->IsRecording())
|
|
return;
|
|
|
|
CGhostItem *pGhost = &m_aActiveGhosts[Slot];
|
|
|
|
int NumTicks = pGhost->m_Path.Size();
|
|
GetPath(pItem->m_aFilename, sizeof(pItem->m_aFilename), pItem->m_aPlayer, pItem->m_Time);
|
|
GhostRecorder()->Start(pItem->m_aFilename, Client()->GetCurrentMap(), Client()->GetMapCrc(), pItem->m_aPlayer);
|
|
|
|
GhostRecorder()->WriteData(GHOSTDATA_TYPE_START_TICK, &pGhost->m_StartTick, sizeof(int));
|
|
GhostRecorder()->WriteData(GHOSTDATA_TYPE_SKIN, &pGhost->m_Skin, sizeof(CGhostSkin));
|
|
for(int i = 0; i < NumTicks; i++)
|
|
GhostRecorder()->WriteData(GHOSTDATA_TYPE_CHARACTER, pGhost->m_Path.Get(i), sizeof(CGhostCharacter));
|
|
|
|
GhostRecorder()->Stop(NumTicks, pItem->m_Time);
|
|
}
|
|
|
|
void CGhost::ConGPlay(IConsole::IResult *pResult, void *pUserData)
|
|
{
|
|
CGhost *pGhost = (CGhost *)pUserData;
|
|
pGhost->StartRender(pGhost->Client()->PredGameTick());
|
|
}
|
|
|
|
void CGhost::OnConsoleInit()
|
|
{
|
|
m_pGhostLoader = Kernel()->RequestInterface<IGhostLoader>();
|
|
m_pGhostRecorder = Kernel()->RequestInterface<IGhostRecorder>();
|
|
|
|
Console()->Register("gplay", "", CFGFLAG_CLIENT, ConGPlay, this, "");
|
|
}
|
|
|
|
void CGhost::OnMessage(int MsgType, void *pRawMsg)
|
|
{
|
|
// check for messages from server
|
|
if(MsgType == NETMSGTYPE_SV_KILLMSG)
|
|
{
|
|
CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
|
|
if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalClientID)
|
|
{
|
|
if(m_Recording)
|
|
StopRecord();
|
|
StopRender();
|
|
m_LastDeathTick = Client()->GameTick();
|
|
}
|
|
}
|
|
else if(MsgType == NETMSGTYPE_SV_CHAT)
|
|
{
|
|
CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
|
|
if(pMsg->m_ClientID == -1 && m_Recording)
|
|
{
|
|
char aName[MAX_NAME_LENGTH];
|
|
int Time = CRaceHelper::TimeFromFinishMessage(pMsg->m_pMessage, aName, sizeof(aName));
|
|
if(Time > 0 && str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID].m_aName) == 0)
|
|
{
|
|
StopRecord(Time);
|
|
StopRender();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CGhost::OnReset()
|
|
{
|
|
StopRecord();
|
|
StopRender();
|
|
m_LastDeathTick = -1;
|
|
m_LastRaceTick = -1;
|
|
}
|
|
|
|
void CGhost::OnMapLoad()
|
|
{
|
|
OnReset();
|
|
UnloadAll();
|
|
m_pClient->m_pMenus->GhostlistPopulate();
|
|
m_AllowRestart = false;
|
|
}
|
|
|
|
int CGhost::GetLastRaceTick()
|
|
{
|
|
return m_LastRaceTick;
|
|
}
|