mirror of
https://github.com/ddnet/ddnet.git
synced 2024-11-19 14:38:18 +00:00
Cleaned up the ghost component and made it use the ghost recorder and loader
This commit is contained in:
parent
b1e7138847
commit
c189678e44
|
@ -182,6 +182,11 @@ public:
|
|||
virtual void RaceRecordStop() = 0;
|
||||
virtual bool RaceRecordIsRecording() = 0;
|
||||
|
||||
virtual void Ghost_GetPath(char *pBuf, int Size, int Time = -1) = 0;
|
||||
virtual void GhostRecorder_Start() = 0;
|
||||
virtual bool GhostLoader_Load(const char *pFilename) = 0;
|
||||
virtual bool GhostLoader_GetGhostInfo(const char *pFilename, struct CGhostHeader *pGhostHeader) = 0;
|
||||
|
||||
virtual void DemoSliceBegin() = 0;
|
||||
virtual void DemoSliceEnd() = 0;
|
||||
virtual void DemoSlice(const char *pDstPath, CLIENTFUNC_FILTER pfnFilter, void *pUser) = 0;
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
#include <engine/shared/datafile.h>
|
||||
#include <engine/shared/demo.h>
|
||||
#include <engine/shared/filecollection.h>
|
||||
#include <engine/shared/ghost.h>
|
||||
#include <engine/shared/network.h>
|
||||
#include <engine/shared/packer.h>
|
||||
#include <engine/shared/protocol.h>
|
||||
|
@ -2529,6 +2530,8 @@ void CClient::RegisterInterfaces()
|
|||
{
|
||||
Kernel()->RegisterInterface(static_cast<IDemoRecorder*>(&m_DemoRecorder[RECORDER_MANUAL]), false);
|
||||
Kernel()->RegisterInterface(static_cast<IDemoPlayer*>(&m_DemoPlayer), false);
|
||||
Kernel()->RegisterInterface(static_cast<IGhostRecorder*>(&m_GhostRecorder), false);
|
||||
Kernel()->RegisterInterface(static_cast<IGhostLoader*>(&m_GhostLoader), false);
|
||||
Kernel()->RegisterInterface(static_cast<IServerBrowser*>(&m_ServerBrowser), false);
|
||||
Kernel()->RegisterInterface(static_cast<IFetcher*>(&m_Fetcher), false);
|
||||
#if !defined(CONF_PLATFORM_MACOSX) && !defined(__ANDROID__)
|
||||
|
@ -3622,6 +3625,47 @@ bool CClient::RaceRecordIsRecording()
|
|||
return m_DemoRecorder[RECORDER_RACE].IsRecording();
|
||||
}
|
||||
|
||||
void ClearFilename(char *pStr)
|
||||
{
|
||||
while(*pStr)
|
||||
{
|
||||
if(*pStr == '\\' || *pStr == '/' || *pStr == '|' || *pStr == ':' || *pStr == '*' || *pStr == '?' || *pStr == '<' || *pStr == '>' || *pStr == '"')
|
||||
*pStr = '%';
|
||||
pStr++;
|
||||
}
|
||||
}
|
||||
|
||||
void CClient::Ghost_GetPath(char *pBuf, int Size, int Time)
|
||||
{
|
||||
// check the player name
|
||||
char aPlayerName[MAX_NAME_LENGTH];
|
||||
str_copy(aPlayerName, g_Config.m_PlayerName, sizeof(aPlayerName));
|
||||
ClearFilename(aPlayerName);
|
||||
|
||||
if(Time < 0)
|
||||
str_format(pBuf, Size, "ghosts/%s_%s_%08x_tmp_%d.gho", m_aCurrentMap, aPlayerName, m_pMap->Crc(), pid());
|
||||
else
|
||||
str_format(pBuf, Size, "ghosts/%s_%s_%d.%03d_%08x.gho", m_aCurrentMap, aPlayerName, Time / 1000, Time % 1000, m_pMap->Crc());
|
||||
}
|
||||
|
||||
void CClient::GhostRecorder_Start()
|
||||
{
|
||||
char aFilename[128];
|
||||
Ghost_GetPath(aFilename, sizeof(aFilename));
|
||||
m_GhostRecorder.Start(Storage(), m_pConsole, aFilename, m_aCurrentMap, m_pMap->Crc(), g_Config.m_PlayerName);
|
||||
}
|
||||
|
||||
bool CClient::GhostLoader_Load(const char *pFilename)
|
||||
{
|
||||
return m_GhostLoader.Load(Storage(), m_pConsole, pFilename, m_aCurrentMap, m_pMap->Crc()) == 0;
|
||||
}
|
||||
|
||||
bool CClient::GhostLoader_GetGhostInfo(const char *pFilename, struct CGhostHeader *pGhostHeader)
|
||||
{
|
||||
return m_GhostLoader.GetGhostInfo(Storage(), m_pConsole, pFilename, pGhostHeader, m_aCurrentMap, m_pMap->Crc());
|
||||
}
|
||||
|
||||
|
||||
void CClient::RequestDDNetInfo()
|
||||
{
|
||||
char aUrl[256];
|
||||
|
|
|
@ -75,6 +75,8 @@ class CClient : public IClient, public CDemoPlayer::IListener
|
|||
class CDemoPlayer m_DemoPlayer;
|
||||
class CDemoRecorder m_DemoRecorder[RECORDER_MAX];
|
||||
class CDemoEditor m_DemoEditor;
|
||||
class CGhostRecorder m_GhostRecorder;
|
||||
class CGhostLoader m_GhostLoader;
|
||||
class CServerBrowser m_ServerBrowser;
|
||||
class CFetcher m_Fetcher;
|
||||
class CUpdater m_Updater;
|
||||
|
@ -388,6 +390,11 @@ public:
|
|||
virtual void RaceRecordStop();
|
||||
virtual bool RaceRecordIsRecording();
|
||||
|
||||
void Ghost_GetPath(char *pBuf, int Size, int Time = -1);
|
||||
void GhostRecorder_Start();
|
||||
bool GhostLoader_Load(const char *pFilename);
|
||||
bool GhostLoader_GetGhostInfo(const char *pFilename, struct CGhostHeader *pGhostHeader);
|
||||
|
||||
virtual void DemoSliceBegin();
|
||||
virtual void DemoSliceEnd();
|
||||
virtual void DemoSlice(const char *pDstPath, CLIENTFUNC_FILTER pfnFilter, void *pUser);
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
/* (c) Rajh, Redix and Sushi. */
|
||||
|
||||
#include <engine/storage.h>
|
||||
#include <engine/ghost.h>
|
||||
#include <engine/graphics.h>
|
||||
#include <engine/storage.h>
|
||||
#include <engine/shared/config.h>
|
||||
#include <engine/shared/compression.h>
|
||||
#include <engine/shared/network.h>
|
||||
|
||||
#include <game/generated/client_data.h>
|
||||
#include <game/client/animstate.h>
|
||||
|
@ -14,47 +13,30 @@
|
|||
#include "menus.h"
|
||||
#include "ghost.h"
|
||||
|
||||
/*
|
||||
Note:
|
||||
Freezing fucks up the ghost
|
||||
the ghost isnt really sync
|
||||
don't really get the client tick system for prediction
|
||||
can used PrevChar and PlayerChar and it would be fluent and accurate but won't be predicted
|
||||
so it will be affected by lags
|
||||
*/
|
||||
|
||||
static const unsigned char gs_aHeaderMarker[8] = {'T', 'W', 'G', 'H', 'O', 'S', 'T', 0};
|
||||
static const unsigned char gs_ActVersion = 2;
|
||||
|
||||
CGhost::CGhost()
|
||||
{
|
||||
m_lGhosts.clear();
|
||||
m_CurGhost.m_Path.clear();
|
||||
m_CurGhost.m_ID = -1;
|
||||
m_CurPos = 0;
|
||||
m_Recording = false;
|
||||
m_Rendering = false;
|
||||
m_RaceState = RACE_NONE;
|
||||
m_NewRecord = false;
|
||||
m_BestTime = -1;
|
||||
m_StartRenderTick = -1;
|
||||
}
|
||||
|
||||
void CGhost::AddInfos(CGhostCharacter Player)
|
||||
void CGhost::AddInfos(const CNetObj_Character *pChar)
|
||||
{
|
||||
if(!m_Recording)
|
||||
return;
|
||||
|
||||
// Just to be sure it doesnt eat too much memory, the first test should be enough anyway
|
||||
if(m_CurGhost.m_Path.size() > Client()->GameTickSpeed()*60*20)
|
||||
if(m_CurGhost.m_lPath.size() > Client()->GameTickSpeed()*60*20)
|
||||
{
|
||||
dbg_msg("ghost", "20 minutes elapsed. stopping ghost record");
|
||||
StopRecord();
|
||||
m_CurGhost.m_Path.clear();
|
||||
m_CurGhost.Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
m_CurGhost.m_Path.add(Player);
|
||||
CGhostCharacter_NoTick GhostChar;
|
||||
CGhostTools::GetGhostCharacter(&GhostChar, pChar);
|
||||
m_CurGhost.m_lPath.add(GhostChar);
|
||||
}
|
||||
|
||||
void CGhost::OnRender()
|
||||
|
@ -78,36 +60,15 @@ void CGhost::OnRender()
|
|||
|
||||
if(Start)
|
||||
{
|
||||
OnReset();
|
||||
m_RaceState = RACE_STARTED;
|
||||
StartRender();
|
||||
StartRecord();
|
||||
}
|
||||
|
||||
if(m_RaceState == RACE_FINISHED)
|
||||
{
|
||||
if(m_NewRecord)
|
||||
{
|
||||
// search for own ghost
|
||||
array<CGhostItem>::range r = find_linear(m_lGhosts.all(), m_CurGhost);
|
||||
m_NewRecord = false;
|
||||
if(r.empty())
|
||||
m_lGhosts.add(m_CurGhost);
|
||||
else
|
||||
r.front() = m_CurGhost;
|
||||
|
||||
Save();
|
||||
}
|
||||
StopRecord();
|
||||
StopRender();
|
||||
m_RaceState = RACE_NONE;
|
||||
}
|
||||
|
||||
CNetObj_Character Char = m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientID].m_Cur;
|
||||
m_pClient->m_PredictedChar.Write(&Char);
|
||||
|
||||
if(m_pClient->m_NewPredictedTick)
|
||||
AddInfos(GetGhostCharacter(Char));
|
||||
if(m_pClient->m_NewPredictedTick && m_Recording)
|
||||
AddInfos(&Char);
|
||||
|
||||
// Play the ghost
|
||||
if(!m_Rendering || !g_Config.m_ClRaceShowGhost)
|
||||
|
@ -124,58 +85,30 @@ void CGhost::OnRender()
|
|||
for(int i = 0; i < m_lGhosts.size(); i++)
|
||||
{
|
||||
CGhostItem *pGhost = &m_lGhosts[i];
|
||||
if(m_CurPos >= pGhost->m_Path.size())
|
||||
if(m_CurPos >= pGhost->m_lPath.size())
|
||||
continue;
|
||||
|
||||
int PrevPos = (m_CurPos > 0) ? m_CurPos-1 : m_CurPos;
|
||||
CGhostCharacter Player = pGhost->m_Path[m_CurPos];
|
||||
CGhostCharacter Prev = pGhost->m_Path[PrevPos];
|
||||
CNetObj_ClientInfo Info = pGhost->m_Info;
|
||||
int PrevPos = max(0, m_CurPos-1);
|
||||
CGhostCharacter_NoTick Player = pGhost->m_lPath[m_CurPos];
|
||||
CGhostCharacter_NoTick Prev = pGhost->m_lPath[PrevPos];
|
||||
|
||||
RenderGhostHook(Player, Prev);
|
||||
RenderGhost(Player, Prev, Info);
|
||||
RenderGhostHook(&Player, &Prev);
|
||||
RenderGhost(&Player, &Prev, &pGhost->m_RenderInfo);
|
||||
}
|
||||
}
|
||||
|
||||
void CGhost::RenderGhost(CGhostCharacter Player, CGhostCharacter Prev, CNetObj_ClientInfo Info)
|
||||
void CGhost::RenderGhost(const CGhostCharacter_NoTick *pPlayer, const CGhostCharacter_NoTick *pPrev, CTeeRenderInfo *pInfo)
|
||||
{
|
||||
char aSkinName[64];
|
||||
IntsToStr(&Info.m_Skin0, 6, aSkinName);
|
||||
int SkinId = m_pClient->m_pSkins->Find(aSkinName);
|
||||
if(SkinId < 0)
|
||||
{
|
||||
SkinId = m_pClient->m_pSkins->Find("default");
|
||||
if(SkinId < 0)
|
||||
SkinId = 0;
|
||||
}
|
||||
|
||||
CTeeRenderInfo RenderInfo;
|
||||
RenderInfo.m_ColorBody = m_pClient->m_pSkins->GetColorV4(Info.m_ColorBody);
|
||||
RenderInfo.m_ColorFeet = m_pClient->m_pSkins->GetColorV4(Info.m_ColorFeet);
|
||||
|
||||
if(Info.m_UseCustomColor)
|
||||
RenderInfo.m_Texture = m_pClient->m_pSkins->Get(SkinId)->m_ColorTexture;
|
||||
else
|
||||
{
|
||||
RenderInfo.m_Texture = m_pClient->m_pSkins->Get(SkinId)->m_OrgTexture;
|
||||
RenderInfo.m_ColorBody = vec4(1,1,1,1);
|
||||
RenderInfo.m_ColorFeet = vec4(1,1,1,1);
|
||||
}
|
||||
|
||||
RenderInfo.m_ColorBody.a = 0.5f;
|
||||
RenderInfo.m_ColorFeet.a = 0.5f;
|
||||
RenderInfo.m_Size = 64;
|
||||
|
||||
float IntraTick = Client()->PredIntraGameTick();
|
||||
|
||||
float Angle = mix((float)Prev.m_Angle, (float)Player.m_Angle, IntraTick)/256.0f;
|
||||
float Angle = mix((float)pPrev->m_Angle, (float)pPlayer->m_Angle, IntraTick)/256.0f;
|
||||
vec2 Direction = GetDirection((int)(Angle*256.0f));
|
||||
vec2 Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), IntraTick);
|
||||
vec2 Vel = mix(vec2(Prev.m_VelX/256.0f, Prev.m_VelY/256.0f), vec2(Player.m_VelX/256.0f, Player.m_VelY/256.0f), IntraTick);
|
||||
vec2 Position = mix(vec2(pPrev->m_X, pPrev->m_Y), vec2(pPlayer->m_X, pPlayer->m_Y), IntraTick);
|
||||
vec2 Vel = mix(vec2(pPrev->m_VelX/256.0f, pPrev->m_VelY/256.0f), vec2(pPlayer->m_VelX/256.0f, pPlayer->m_VelY/256.0f), IntraTick);
|
||||
|
||||
bool Stationary = Player.m_VelX <= 1 && Player.m_VelX >= -1;
|
||||
bool InAir = !Collision()->CheckPoint(Player.m_X, Player.m_Y+16);
|
||||
bool WantOtherDir = (Player.m_Direction == -1 && Vel.x > 0) || (Player.m_Direction == 1 && Vel.x < 0);
|
||||
bool Stationary = pPlayer->m_VelX <= 1 && pPlayer->m_VelX >= -1;
|
||||
bool InAir = !Collision()->CheckPoint(pPlayer->m_X, pPlayer->m_Y+16);
|
||||
bool WantOtherDir = (pPlayer->m_Direction == -1 && Vel.x > 0) || (pPlayer->m_Direction == 1 && Vel.x < 0);
|
||||
|
||||
float WalkTime = fmod(absolute(Position.x), 100.0f)/100.0f;
|
||||
CAnimState State;
|
||||
|
@ -188,7 +121,7 @@ void CGhost::RenderGhost(CGhostCharacter Player, CGhostCharacter Prev, CNetObj_C
|
|||
else if(!WantOtherDir)
|
||||
State.Add(&g_pData->m_aAnimations[ANIM_WALK], WalkTime, 1.0f);
|
||||
|
||||
if (Player.m_Weapon == WEAPON_GRENADE)
|
||||
if (pPlayer->m_Weapon == WEAPON_GRENADE)
|
||||
{
|
||||
Graphics()->TextureSet(g_pData->m_aImages[IMAGE_GAME].m_Id);
|
||||
Graphics()->QuadsBegin();
|
||||
|
@ -196,13 +129,13 @@ void CGhost::RenderGhost(CGhostCharacter Player, CGhostCharacter Prev, CNetObj_C
|
|||
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
|
||||
// normal weapons
|
||||
int iw = clamp(Player.m_Weapon, 0, NUM_WEAPONS-1);
|
||||
int iw = clamp(pPlayer->m_Weapon, 0, NUM_WEAPONS-1);
|
||||
RenderTools()->SelectSprite(g_pData->m_Weapons.m_aId[iw].m_pSpriteBody, Direction.x < 0 ? SPRITE_FLAG_FLIP_Y : 0);
|
||||
|
||||
vec2 Dir = Direction;
|
||||
float Recoil = 0.0f;
|
||||
// TODO: is this correct?
|
||||
float a = (Client()->PredGameTick()-Player.m_AttackTick+IntraTick)/5.0f;
|
||||
float a = (Client()->PredGameTick()-pPlayer->m_AttackTick+IntraTick)/5.0f;
|
||||
if(a < 1)
|
||||
Recoil = sinf(a*pi);
|
||||
|
||||
|
@ -213,19 +146,19 @@ void CGhost::RenderGhost(CGhostCharacter Player, CGhostCharacter Prev, CNetObj_C
|
|||
}
|
||||
|
||||
// Render ghost
|
||||
RenderTools()->RenderTee(&State, &RenderInfo, 0, Direction, Position, true);
|
||||
RenderTools()->RenderTee(&State, pInfo, 0, Direction, Position, true);
|
||||
}
|
||||
|
||||
void CGhost::RenderGhostHook(CGhostCharacter Player, CGhostCharacter Prev)
|
||||
void CGhost::RenderGhostHook(const CGhostCharacter_NoTick *pPlayer, const CGhostCharacter_NoTick *pPrev)
|
||||
{
|
||||
if (Prev.m_HookState<=0 || Player.m_HookState<=0)
|
||||
if(pPrev->m_HookState<=0 || pPlayer->m_HookState<=0)
|
||||
return;
|
||||
|
||||
float IntraTick = Client()->PredIntraGameTick();
|
||||
|
||||
vec2 Pos = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), IntraTick);
|
||||
vec2 Pos = mix(vec2(pPrev->m_X, pPrev->m_Y), vec2(pPlayer->m_X, pPlayer->m_Y), IntraTick);
|
||||
|
||||
vec2 HookPos = mix(vec2(Prev.m_HookX, Prev.m_HookY), vec2(Player.m_HookX, Player.m_HookY), IntraTick);
|
||||
vec2 HookPos = mix(vec2(pPrev->m_HookX, pPrev->m_HookY), vec2(pPlayer->m_HookX, pPlayer->m_HookY), IntraTick);
|
||||
float d = distance(Pos, HookPos);
|
||||
vec2 Dir = normalize(Pos-HookPos);
|
||||
|
||||
|
@ -254,31 +187,40 @@ void CGhost::RenderGhostHook(CGhostCharacter Player, CGhostCharacter Prev)
|
|||
Graphics()->QuadsEnd();
|
||||
}
|
||||
|
||||
CGhost::CGhostCharacter CGhost::GetGhostCharacter(CNetObj_Character Char)
|
||||
void CGhost::InitRenderInfos(CTeeRenderInfo *pRenderInfo, const char *pSkinName, int UseCustomColor, int ColorBody, int ColorFeet)
|
||||
{
|
||||
CGhostCharacter Player;
|
||||
Player.m_X = Char.m_X;
|
||||
Player.m_Y = Char.m_Y;
|
||||
Player.m_VelX = Char.m_VelX;
|
||||
Player.m_VelY = Char.m_VelY;
|
||||
Player.m_Angle = Char.m_Angle;
|
||||
Player.m_Direction = Char.m_Direction;
|
||||
Player.m_Weapon = Char.m_Weapon;
|
||||
Player.m_HookState = Char.m_HookState;
|
||||
Player.m_HookX = Char.m_HookX;
|
||||
Player.m_HookY = Char.m_HookY;
|
||||
Player.m_AttackTick = Char.m_AttackTick;
|
||||
int SkinId = m_pClient->m_pSkins->Find(pSkinName);
|
||||
if(SkinId < 0)
|
||||
{
|
||||
SkinId = m_pClient->m_pSkins->Find("default");
|
||||
if(SkinId < 0)
|
||||
SkinId = 0;
|
||||
}
|
||||
|
||||
return Player;
|
||||
pRenderInfo->m_ColorBody = m_pClient->m_pSkins->GetColorV4(ColorBody);
|
||||
pRenderInfo->m_ColorFeet = m_pClient->m_pSkins->GetColorV4(ColorFeet);
|
||||
|
||||
if(UseCustomColor)
|
||||
pRenderInfo->m_Texture = m_pClient->m_pSkins->Get(SkinId)->m_ColorTexture;
|
||||
else
|
||||
{
|
||||
pRenderInfo->m_Texture = m_pClient->m_pSkins->Get(SkinId)->m_OrgTexture;
|
||||
pRenderInfo->m_ColorBody = vec4(1, 1, 1, 1);
|
||||
pRenderInfo->m_ColorFeet = vec4(1, 1, 1, 1);
|
||||
}
|
||||
|
||||
pRenderInfo->m_ColorBody.a = 0.5f;
|
||||
pRenderInfo->m_ColorFeet.a = 0.5f;
|
||||
pRenderInfo->m_Size = 64;
|
||||
}
|
||||
|
||||
void CGhost::StartRecord()
|
||||
{
|
||||
m_Recording = true;
|
||||
m_CurGhost.m_Path.clear();
|
||||
CNetObj_ClientInfo *pInfo = (CNetObj_ClientInfo *) Client()->SnapFindItem(IClient::SNAP_CURRENT, NETOBJTYPE_CLIENTINFO, m_pClient->m_Snap.m_LocalClientID);
|
||||
if (pInfo)
|
||||
m_CurGhost.m_Info = *pInfo;
|
||||
m_CurGhost.Reset();
|
||||
|
||||
CGameClient::CClientData ClientData = m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID];
|
||||
InitRenderInfos(&m_CurGhost.m_RenderInfo, ClientData.m_aSkinName, ClientData.m_UseCustomColor, ClientData.m_ColorBody, ClientData.m_ColorFeet);
|
||||
}
|
||||
|
||||
void CGhost::StopRecord()
|
||||
|
@ -298,85 +240,25 @@ void CGhost::StopRender()
|
|||
m_Rendering = false;
|
||||
}
|
||||
|
||||
void CGhost::Save()
|
||||
void CGhost::Save(int Time)
|
||||
{
|
||||
if(!g_Config.m_ClRaceSaveGhost)
|
||||
return;
|
||||
Client()->GhostRecorder_Start();
|
||||
|
||||
CGhostHeader Header;
|
||||
CGameClient::CClientData ClientData = m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID];
|
||||
|
||||
// check the player name
|
||||
char aName[MAX_NAME_LENGTH];
|
||||
str_copy(aName, g_Config.m_PlayerName, sizeof(aName));
|
||||
for(int i = 0; i < MAX_NAME_LENGTH; i++)
|
||||
{
|
||||
if(!aName[i])
|
||||
break;
|
||||
CGhostSkin Skin;
|
||||
StrToInts(&Skin.m_Skin0, 6, ClientData.m_aSkinName);
|
||||
Skin.m_UseCustomColor = ClientData.m_UseCustomColor;
|
||||
Skin.m_ColorBody = ClientData.m_ColorBody;
|
||||
Skin.m_ColorFeet = ClientData.m_ColorFeet;
|
||||
GhostRecorder()->WriteData(GHOSTDATA_TYPE_SKIN, (const char*)&Skin, sizeof(Skin));
|
||||
|
||||
if(aName[i] == '\\' || aName[i] == '/' || aName[i] == '|' || aName[i] == ':' || aName[i] == '*' || aName[i] == '?' || aName[i] == '<' || aName[i] == '>' || aName[i] == '"')
|
||||
aName[i] = '%';
|
||||
}
|
||||
int NumTicks = m_CurGhost.m_lPath.size();
|
||||
|
||||
char aFilename[256];
|
||||
str_format(aFilename, sizeof(aFilename), "ghosts/%s_%s_%d.%03d_%08x.gho", Client()->GetCurrentMap(), aName, m_BestTime / 1000, m_BestTime % 1000, Client()->GetCurrentMapCrc());
|
||||
IOHANDLE File = Storage()->OpenFile(aFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
|
||||
if(!File)
|
||||
return;
|
||||
for(int i = 0; i < m_CurGhost.m_lPath.size(); i++)
|
||||
GhostRecorder()->WriteData(GHOSTDATA_TYPE_CHARACTER_NO_TICK, (const char*)&m_CurGhost.m_lPath[i], sizeof(CGhostCharacter_NoTick));
|
||||
|
||||
// write header
|
||||
int Crc = Client()->GetCurrentMapCrc();
|
||||
mem_zero(&Header, sizeof(Header));
|
||||
mem_copy(Header.m_aMarker, gs_aHeaderMarker, sizeof(Header.m_aMarker));
|
||||
Header.m_Version = gs_ActVersion;
|
||||
IntsToStr(&m_CurGhost.m_Info.m_Name0, 4, Header.m_aOwner);
|
||||
str_copy(Header.m_aMap, Client()->GetCurrentMap(), sizeof(Header.m_aMap));
|
||||
Header.m_aCrc[0] = (Crc>>24)&0xff;
|
||||
Header.m_aCrc[1] = (Crc>>16)&0xff;
|
||||
Header.m_aCrc[2] = (Crc>>8)&0xff;
|
||||
Header.m_aCrc[3] = (Crc)&0xff;
|
||||
Header.m_Time = m_BestTime / 1000.f;
|
||||
Header.m_NumShots = m_CurGhost.m_Path.size();
|
||||
io_write(File, &Header, sizeof(Header));
|
||||
|
||||
// write client info
|
||||
io_write(File, &m_CurGhost.m_Info, sizeof(m_CurGhost.m_Info));
|
||||
|
||||
// write data
|
||||
int ItemsPerPackage = 500; // 500 ticks per package
|
||||
int Num = Header.m_NumShots;
|
||||
CGhostCharacter *Data = &m_CurGhost.m_Path[0];
|
||||
|
||||
while(Num)
|
||||
{
|
||||
int Items = min(Num, ItemsPerPackage);
|
||||
Num -= Items;
|
||||
|
||||
char aBuffer[100*500];
|
||||
char aBuffer2[100*500];
|
||||
unsigned char aSize[4];
|
||||
|
||||
int Size = sizeof(CGhostCharacter)*Items;
|
||||
mem_copy(aBuffer2, Data, Size);
|
||||
Data += Items;
|
||||
|
||||
Size = CVariableInt::Compress(aBuffer2, Size, aBuffer, sizeof(aBuffer));
|
||||
if(Size < 0)
|
||||
return;
|
||||
|
||||
Size = CNetBase::Compress(aBuffer, Size, aBuffer2, sizeof(aBuffer2));
|
||||
if(Size < 0)
|
||||
return;
|
||||
|
||||
aSize[0] = (Size>>24)&0xff;
|
||||
aSize[1] = (Size>>16)&0xff;
|
||||
aSize[2] = (Size>>8)&0xff;
|
||||
aSize[3] = (Size)&0xff;
|
||||
|
||||
io_write(File, aSize, sizeof(aSize));
|
||||
io_write(File, aBuffer2, Size);
|
||||
}
|
||||
|
||||
io_close(File);
|
||||
GhostRecorder()->Stop(NumTicks, Time);
|
||||
|
||||
// remove old ghost from list (TODO: remove other ghosts?)
|
||||
if(m_pClient->m_pMenus->m_OwnGhost)
|
||||
|
@ -388,9 +270,15 @@ void CGhost::Save()
|
|||
m_pClient->m_pMenus->m_lGhosts.remove(*m_pClient->m_pMenus->m_OwnGhost);
|
||||
}
|
||||
|
||||
char aTmpFilename[128];
|
||||
char aFilename[128];
|
||||
Client()->Ghost_GetPath(aTmpFilename, sizeof(aTmpFilename));
|
||||
Client()->Ghost_GetPath(aFilename, sizeof(aFilename), m_BestTime);
|
||||
Storage()->RenameFile(aTmpFilename, aFilename, IStorage::TYPE_SAVE);
|
||||
|
||||
CMenus::CGhostItem Item;
|
||||
str_copy(Item.m_aFilename, aFilename, sizeof(Item.m_aFilename));
|
||||
str_copy(Item.m_aPlayer, Header.m_aOwner, sizeof(Item.m_aPlayer));
|
||||
str_copy(Item.m_aPlayer, g_Config.m_PlayerName, sizeof(Item.m_aPlayer));
|
||||
Item.m_Time = m_BestTime;
|
||||
Item.m_Active = true;
|
||||
Item.m_ID = -1;
|
||||
|
@ -399,124 +287,65 @@ void CGhost::Save()
|
|||
m_pClient->m_pMenus->m_OwnGhost = &find_linear(m_pClient->m_pMenus->m_lGhosts.all(), Item).front();
|
||||
|
||||
dbg_msg("ghost", "saved better ghost");
|
||||
m_Saving = false;
|
||||
}
|
||||
|
||||
bool CGhost::GetHeader(IOHANDLE *pFile, CGhostHeader *pHeader)
|
||||
{
|
||||
if(!*pFile)
|
||||
return 0;
|
||||
|
||||
CGhostHeader Header;
|
||||
io_read(*pFile, &Header, sizeof(Header));
|
||||
|
||||
*pHeader = Header;
|
||||
|
||||
if(mem_comp(Header.m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) != 0)
|
||||
return 0;
|
||||
|
||||
if(Header.m_Version != gs_ActVersion)
|
||||
return 0;
|
||||
|
||||
int Crc = (Header.m_aCrc[0]<<24) | (Header.m_aCrc[1]<<16) | (Header.m_aCrc[2]<<8) | (Header.m_aCrc[3]);
|
||||
if(str_comp(Header.m_aMap, Client()->GetCurrentMap()) != 0 || Crc != Client()->GetCurrentMapCrc())
|
||||
return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool CGhost::GetInfo(const char* pFilename, CGhostHeader *pHeader)
|
||||
{
|
||||
char aFilename[256];
|
||||
str_format(aFilename, sizeof(aFilename), "ghosts/%s", pFilename);
|
||||
IOHANDLE File = Storage()->OpenFile(aFilename, IOFLAG_READ, IStorage::TYPE_SAVE);
|
||||
if(!File)
|
||||
return 0;
|
||||
|
||||
bool Check = GetHeader(&File, pHeader);
|
||||
io_close(File);
|
||||
|
||||
return Check;
|
||||
}
|
||||
|
||||
void CGhost::Load(const char* pFilename, int ID)
|
||||
{
|
||||
char aFilename[256];
|
||||
str_format(aFilename, sizeof(aFilename), "ghosts/%s", pFilename);
|
||||
IOHANDLE File = Storage()->OpenFile(aFilename, IOFLAG_READ, IStorage::TYPE_SAVE);
|
||||
if(!File)
|
||||
if(!Client()->GhostLoader_Load(pFilename))
|
||||
return;
|
||||
|
||||
// read header
|
||||
CGhostHeader Header;
|
||||
if(!GetHeader(&File, &Header))
|
||||
const CGhostHeader *pHeader = GhostLoader()->GetHeader();
|
||||
|
||||
int NumTicks = GhostLoader()->GetTicks(pHeader);
|
||||
int Time = GhostLoader()->GetTime(pHeader);
|
||||
if(NumTicks <= 0 || Time <= 0)
|
||||
{
|
||||
io_close(File);
|
||||
GhostLoader()->Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if(ID == -1)
|
||||
m_BestTime = Header.m_Time * 1000;
|
||||
|
||||
int NumShots = Header.m_NumShots;
|
||||
|
||||
// create ghost
|
||||
// select ghost
|
||||
CGhostItem Ghost;
|
||||
Ghost.m_ID = ID;
|
||||
Ghost.m_Path.clear();
|
||||
Ghost.m_Path.set_size(NumShots);
|
||||
Ghost.m_lPath.set_size(NumTicks);
|
||||
|
||||
// read client info
|
||||
io_read(File, &Ghost.m_Info, sizeof(Ghost.m_Info));
|
||||
|
||||
// read data
|
||||
int Index = 0;
|
||||
while(Index < NumShots)
|
||||
bool FoundSkin = false;
|
||||
|
||||
int Type;
|
||||
while(GhostLoader()->ReadNextType(&Type))
|
||||
{
|
||||
static char aCompresseddata[100*500];
|
||||
static char aDecompressed[100*500];
|
||||
static char aData[100*500];
|
||||
|
||||
unsigned char aSize[4];
|
||||
if(io_read(File, aSize, sizeof(aSize)) != sizeof(aSize))
|
||||
break;
|
||||
int Size = (aSize[0]<<24) | (aSize[1]<<16) | (aSize[2]<<8) | aSize[3];
|
||||
|
||||
if(io_read(File, aCompresseddata, Size) != (unsigned)Size)
|
||||
if(Index == NumTicks && Type == GHOSTDATA_TYPE_CHARACTER_NO_TICK)
|
||||
{
|
||||
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error reading chunk");
|
||||
Index = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
Size = CNetBase::Decompress(aCompresseddata, Size, aDecompressed, sizeof(aDecompressed));
|
||||
if(Size < 0)
|
||||
if(Type == GHOSTDATA_TYPE_SKIN)
|
||||
{
|
||||
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during network decompression");
|
||||
break;
|
||||
CGhostSkin Skin;
|
||||
if(GhostLoader()->ReadData(Type, (char*)&Skin, sizeof(Skin)) && !FoundSkin)
|
||||
{
|
||||
FoundSkin = true;
|
||||
char aSkinName[64];
|
||||
IntsToStr(&Skin.m_Skin0, 6, aSkinName);
|
||||
InitRenderInfos(&Ghost.m_RenderInfo, aSkinName, Skin.m_UseCustomColor, Skin.m_ColorBody, Skin.m_ColorFeet);
|
||||
}
|
||||
}
|
||||
|
||||
Size = CVariableInt::Decompress(aDecompressed, Size, aData, sizeof(aData));
|
||||
if(Size < 0)
|
||||
else if(Type == GHOSTDATA_TYPE_CHARACTER_NO_TICK)
|
||||
{
|
||||
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during intpack decompression");
|
||||
break;
|
||||
}
|
||||
|
||||
CGhostCharacter *Tmp = (CGhostCharacter*)aData;
|
||||
for(unsigned i = 0; i < Size/sizeof(CGhostCharacter); i++)
|
||||
{
|
||||
if(Index >= NumShots)
|
||||
break;
|
||||
|
||||
Ghost.m_Path[Index] = *Tmp;
|
||||
Index++;
|
||||
Tmp++;
|
||||
CGhostCharacter_NoTick Char;
|
||||
GhostLoader()->ReadData(Type, (char*)&Ghost.m_lPath[Index++], sizeof(Char));
|
||||
}
|
||||
}
|
||||
|
||||
io_close(File);
|
||||
GhostLoader()->Close();
|
||||
|
||||
m_lGhosts.add(Ghost);
|
||||
if(!FoundSkin)
|
||||
InitRenderInfos(&Ghost.m_RenderInfo, "default", 0, 0, 0);
|
||||
|
||||
if(Index == NumTicks)
|
||||
m_lGhosts.add(Ghost);
|
||||
}
|
||||
|
||||
void CGhost::Unload(int ID)
|
||||
|
@ -533,6 +362,9 @@ void CGhost::ConGPlay(IConsole::IResult *pResult, void *pUserData)
|
|||
|
||||
void CGhost::OnConsoleInit()
|
||||
{
|
||||
m_pGhostLoader = Kernel()->RequestInterface<IGhostLoader>();
|
||||
m_pGhostRecorder = Kernel()->RequestInterface<IGhostRecorder>();
|
||||
|
||||
Console()->Register("gplay", "", CFGFLAG_CLIENT, ConGPlay, this, "");
|
||||
}
|
||||
|
||||
|
@ -546,26 +378,32 @@ void CGhost::OnMessage(int MsgType, void *pRawMsg)
|
|||
{
|
||||
CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
|
||||
if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalClientID)
|
||||
{
|
||||
if(!m_Saving)
|
||||
OnReset();
|
||||
}
|
||||
OnReset();
|
||||
}
|
||||
else if(MsgType == NETMSGTYPE_SV_CHAT)
|
||||
{
|
||||
CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
|
||||
if(pMsg->m_ClientID == -1 && m_RaceState == RACE_STARTED)
|
||||
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)
|
||||
{
|
||||
m_RaceState = RACE_FINISHED;
|
||||
if(m_Recording && (Time < m_BestTime || m_BestTime == -1))
|
||||
StopRecord();
|
||||
StopRender();
|
||||
|
||||
if(Time < m_BestTime || m_BestTime == -1)
|
||||
{
|
||||
m_NewRecord = true;
|
||||
m_BestTime = Time;
|
||||
m_Saving = true;
|
||||
|
||||
array<CGhostItem>::range r = find_linear(m_lGhosts.all(), m_CurGhost);
|
||||
if(r.empty())
|
||||
m_lGhosts.add(m_CurGhost);
|
||||
else
|
||||
r.front() = m_CurGhost;
|
||||
|
||||
if(g_Config.m_ClRaceSaveGhost)
|
||||
Save(Time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -576,11 +414,7 @@ void CGhost::OnReset()
|
|||
{
|
||||
StopRecord();
|
||||
StopRender();
|
||||
m_RaceState = RACE_NONE;
|
||||
m_NewRecord = false;
|
||||
m_CurGhost.m_Path.clear();
|
||||
m_StartRenderTick = -1;
|
||||
m_Saving = false;
|
||||
m_CurGhost.Reset();
|
||||
}
|
||||
|
||||
void CGhost::OnMapLoad()
|
||||
|
|
|
@ -5,45 +5,83 @@
|
|||
|
||||
#include <game/client/component.h>
|
||||
|
||||
class CGhost : public CComponent
|
||||
enum
|
||||
{
|
||||
GHOSTDATA_TYPE_SKIN = 0,
|
||||
GHOSTDATA_TYPE_CHARACTER_NO_TICK,
|
||||
GHOSTDATA_TYPE_CHARACTER
|
||||
};
|
||||
|
||||
struct CGhostSkin
|
||||
{
|
||||
int m_Skin0;
|
||||
int m_Skin1;
|
||||
int m_Skin2;
|
||||
int m_Skin3;
|
||||
int m_Skin4;
|
||||
int m_Skin5;
|
||||
int m_UseCustomColor;
|
||||
int m_ColorBody;
|
||||
int m_ColorFeet;
|
||||
};
|
||||
|
||||
struct CGhostCharacter_NoTick
|
||||
{
|
||||
int m_X;
|
||||
int m_Y;
|
||||
int m_VelX;
|
||||
int m_VelY;
|
||||
int m_Angle;
|
||||
int m_Direction;
|
||||
int m_Weapon;
|
||||
int m_HookState;
|
||||
int m_HookX;
|
||||
int m_HookY;
|
||||
int m_AttackTick;
|
||||
};
|
||||
|
||||
struct CGhostCharacter : public CGhostCharacter_NoTick
|
||||
{
|
||||
int m_Tick;
|
||||
};
|
||||
|
||||
class CGhostTools
|
||||
{
|
||||
public:
|
||||
struct CGhostHeader
|
||||
static void GetGhostCharacter(CGhostCharacter_NoTick *pGhostChar, const CNetObj_Character *pChar)
|
||||
{
|
||||
unsigned char m_aMarker[8];
|
||||
unsigned char m_Version;
|
||||
char m_aOwner[MAX_NAME_LENGTH];
|
||||
char m_aMap[64];
|
||||
unsigned char m_aCrc[4];
|
||||
int m_NumShots;
|
||||
float m_Time;
|
||||
};
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
class CGhost : public CComponent
|
||||
{
|
||||
private:
|
||||
struct CGhostCharacter
|
||||
{
|
||||
int m_X;
|
||||
int m_Y;
|
||||
int m_VelX;
|
||||
int m_VelY;
|
||||
int m_Angle;
|
||||
int m_Direction;
|
||||
int m_Weapon;
|
||||
int m_HookState;
|
||||
int m_HookX;
|
||||
int m_HookY;
|
||||
int m_AttackTick;
|
||||
};
|
||||
|
||||
struct CGhostItem
|
||||
{
|
||||
int m_ID;
|
||||
CNetObj_ClientInfo m_Info;
|
||||
array<CGhostCharacter> m_Path;
|
||||
CTeeRenderInfo m_RenderInfo;
|
||||
array<CGhostCharacter_NoTick> m_lPath;
|
||||
|
||||
bool Empty() { return m_lPath.size() == 0; }
|
||||
void Reset() { m_lPath.clear(); }
|
||||
|
||||
bool operator==(const CGhostItem &Other) { return m_ID == Other.m_ID; }
|
||||
};
|
||||
|
||||
class IGhostLoader *m_pGhostLoader;
|
||||
class IGhostRecorder *m_pGhostRecorder;
|
||||
|
||||
array<CGhostItem> m_lGhosts;
|
||||
CGhostItem m_CurGhost;
|
||||
|
||||
|
@ -51,10 +89,7 @@ private:
|
|||
int m_CurPos;
|
||||
bool m_Recording;
|
||||
bool m_Rendering;
|
||||
int m_RaceState;
|
||||
int m_BestTime;
|
||||
bool m_NewRecord;
|
||||
bool m_Saving;
|
||||
|
||||
enum
|
||||
{
|
||||
|
@ -63,20 +98,19 @@ private:
|
|||
RACE_FINISHED,
|
||||
};
|
||||
|
||||
void AddInfos(CGhostCharacter Player);
|
||||
|
||||
CGhostCharacter GetGhostCharacter(CNetObj_Character Char);
|
||||
void AddInfos(const CNetObj_Character *pChar);
|
||||
|
||||
void StartRecord();
|
||||
void StopRecord();
|
||||
void StartRender();
|
||||
void StopRender();
|
||||
void RenderGhost(CGhostCharacter Player, CGhostCharacter Prev, CNetObj_ClientInfo Info);
|
||||
void RenderGhostHook(CGhostCharacter Player, CGhostCharacter Prev);
|
||||
|
||||
bool GetHeader(IOHANDLE *pFile, CGhostHeader *pHeader);
|
||||
void RenderGhost(const CGhostCharacter_NoTick *pPlayer, const CGhostCharacter_NoTick *pPrev, CTeeRenderInfo *pInfo);
|
||||
void RenderGhostHook(const CGhostCharacter_NoTick *pPlayer, const CGhostCharacter_NoTick *pPrev);
|
||||
|
||||
void Save();
|
||||
void Save(int Time);
|
||||
|
||||
void InitRenderInfos(CTeeRenderInfo *pRenderInfo, const char *pSkinName, int UseCustomColor, int ColorBody, int ColorFeet);
|
||||
|
||||
static void ConGPlay(IConsole::IResult *pResult, void *pUserData);
|
||||
|
||||
|
@ -92,7 +126,8 @@ public:
|
|||
void Load(const char* pFilename, int ID);
|
||||
void Unload(int ID);
|
||||
|
||||
bool GetInfo(const char* pFilename, CGhostHeader *pHeader);
|
||||
class IGhostLoader *GhostLoader() const { return m_pGhostLoader; }
|
||||
class IGhostRecorder *GhostRecorder() const { return m_pGhostRecorder; }
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include <engine/config.h>
|
||||
#include <engine/demo.h>
|
||||
#include <engine/friends.h>
|
||||
#include <engine/ghost.h>
|
||||
#include <engine/graphics.h>
|
||||
#include <engine/serverbrowser.h>
|
||||
#include <engine/textrender.h>
|
||||
|
@ -841,17 +842,20 @@ int CMenus::GhostlistFetchCallback(const char *pName, int IsDir, int StorageType
|
|||
(!IsDir && (Length < 4 || str_comp(pName+Length-4, ".gho") || str_comp_num(pName, pMap, str_length(pMap)))))
|
||||
return 0;
|
||||
|
||||
CGhost::CGhostHeader Header;
|
||||
if(!pSelf->m_pClient->m_pGhost->GetInfo(pName, &Header))
|
||||
char aFilename[256];
|
||||
str_format(aFilename, sizeof(aFilename), "ghosts/%s", pName);
|
||||
|
||||
CGhostHeader Header;
|
||||
if(!pSelf->Client()->GhostLoader_GetGhostInfo(aFilename, &Header))
|
||||
return 0;
|
||||
|
||||
CGhostItem Item;
|
||||
str_copy(Item.m_aFilename, pName, sizeof(Item.m_aFilename));
|
||||
str_copy(Item.m_aFilename, aFilename, sizeof(Item.m_aFilename));
|
||||
str_copy(Item.m_aPlayer, Header.m_aOwner, sizeof(Item.m_aPlayer));
|
||||
Item.m_Time = Header.m_Time * 1000;
|
||||
Item.m_Time = pSelf->m_pClient->m_pGhost->GhostLoader()->GetTime(&Header);
|
||||
Item.m_Active = false;
|
||||
Item.m_ID = pSelf->m_lGhosts.add(Item);
|
||||
|
||||
if(Item.m_Time > 0)
|
||||
Item.m_ID = pSelf->m_lGhosts.add(Item);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue