mirror of
https://github.com/ddnet/ddnet.git
synced 2024-09-21 18:14:19 +00:00
616 lines
16 KiB
C++
616 lines
16 KiB
C++
|
/* (c) Rajh, Redix and Sushi. */
|
||
|
|
||
|
#include <cstdio>
|
||
|
|
||
|
#include <engine/storage.h>
|
||
|
#include <engine/graphics.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>
|
||
|
|
||
|
#include "skins.h"
|
||
|
#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 = 1;
|
||
|
|
||
|
CGhost::CGhost()
|
||
|
{
|
||
|
m_lGhosts.clear();
|
||
|
m_CurPath.clear();
|
||
|
m_CurPos = 0;
|
||
|
m_Recording = false;
|
||
|
m_Rendering = false;
|
||
|
m_RaceState = RACE_NONE;
|
||
|
m_NewRecord = false;
|
||
|
m_BestTime = -1;
|
||
|
m_StartRenderTick = -1;
|
||
|
m_StartRecordTick = -1;
|
||
|
}
|
||
|
|
||
|
void CGhost::AddInfos(CNetObj_Character Player)
|
||
|
{
|
||
|
if(!m_Recording)
|
||
|
return;
|
||
|
|
||
|
|
||
|
// Just to be sure it doesnt eat too much memory, the first test should be enough anyway
|
||
|
if((Client()->GameTick()-m_StartRecordTick) > Client()->GameTickSpeed()*60*13 || m_CurPath.size() > 50*15*60)
|
||
|
{
|
||
|
dbg_msg("ghost", "13 minutes elapsed. Stopping ghost record");
|
||
|
StopRecord();
|
||
|
m_CurPath.clear();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
m_CurPath.add(Player);
|
||
|
}
|
||
|
|
||
|
void CGhost::OnRender()
|
||
|
{
|
||
|
// only for race
|
||
|
if(!m_pClient->m_IsRace || !g_Config.m_ClRaceGhost)
|
||
|
return;
|
||
|
|
||
|
// Check if the race line is crossed then start the render of the ghost if one
|
||
|
if(m_RaceState != RACE_STARTED) {
|
||
|
bool start = false;
|
||
|
|
||
|
std::list < int > Indices = m_pClient->Collision()->GetMapIndices(m_pClient->m_PredictedPrevChar.m_Pos, m_pClient->m_LocalCharacterPos);
|
||
|
if(!Indices.empty()) {
|
||
|
for(std::list < int >::iterator i = Indices.begin(); i != Indices.end(); i++) {
|
||
|
if(m_pClient->Collision()->GetTileIndex(*i) == TILE_BEGIN) start = true;
|
||
|
}
|
||
|
} else {
|
||
|
int CurrentIndex = m_pClient->Collision()->GetPureMapIndex(m_pClient->m_LocalCharacterPos);
|
||
|
if(m_pClient->Collision()->GetTileIndex(CurrentIndex) == TILE_BEGIN) start = true;
|
||
|
}
|
||
|
|
||
|
if(start) {
|
||
|
dbg_msg("ghost", "race started");
|
||
|
m_RaceState = RACE_STARTED;
|
||
|
StartRender();
|
||
|
StartRecord();
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
if(m_RaceState == RACE_FINISHED)
|
||
|
{
|
||
|
int OwnIndex = -1;
|
||
|
for(int i = 0; i < m_lGhosts.size(); i++)
|
||
|
if(m_lGhosts[i].m_ID == -1)
|
||
|
{
|
||
|
OwnIndex = i;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if(m_NewRecord)
|
||
|
{
|
||
|
dbg_msg("ghost", "new path saved");
|
||
|
m_NewRecord = false;
|
||
|
CGhostList Ghost;
|
||
|
Ghost.m_ID = -1;
|
||
|
Ghost.m_GhostInfo = m_CurInfo;
|
||
|
Ghost.m_BestPath.clear();
|
||
|
Ghost.m_BestPath = m_CurPath;
|
||
|
if(OwnIndex < 0)
|
||
|
m_lGhosts.add(Ghost);
|
||
|
else
|
||
|
m_lGhosts[OwnIndex] = Ghost;
|
||
|
|
||
|
Save();
|
||
|
}
|
||
|
StopRecord();
|
||
|
StopRender();
|
||
|
m_RaceState = RACE_NONE;
|
||
|
}
|
||
|
|
||
|
CNetObj_Character Player = m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalCid].m_Cur;
|
||
|
m_pClient->m_PredictedChar.Write(&Player);
|
||
|
|
||
|
if(m_pClient->m_NewPredictedTick)
|
||
|
AddInfos(Player);
|
||
|
|
||
|
// Play the ghost
|
||
|
if(!m_Rendering || !g_Config.m_ClRaceShowGhost)
|
||
|
return;
|
||
|
|
||
|
m_CurPos = Client()->PredGameTick()-m_StartRenderTick;
|
||
|
|
||
|
if(m_lGhosts.size() == 0 || m_CurPos < 0)
|
||
|
{
|
||
|
//dbg_msg("ghost", "Ghost path done");
|
||
|
m_Rendering = false;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
for(int i = 0; i < m_lGhosts.size(); i++)
|
||
|
{
|
||
|
RenderGhostHook(&m_lGhosts[i]);
|
||
|
RenderGhost(&m_lGhosts[i]);
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
void CGhost::RenderGhost(CGhostList *pGhost)
|
||
|
{
|
||
|
if(m_CurPos >= pGhost->m_BestPath.size())
|
||
|
return;
|
||
|
|
||
|
CNetObj_Character Player = pGhost->m_BestPath[m_CurPos];
|
||
|
CNetObj_Character Prev = pGhost->m_BestPath[m_CurPos];
|
||
|
|
||
|
if(m_CurPos > 0)
|
||
|
Prev = pGhost->m_BestPath[m_CurPos-1];
|
||
|
|
||
|
char aSkinName[64];
|
||
|
IntsToStr(&pGhost->m_GhostInfo.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(pGhost->m_GhostInfo.m_ColorBody);
|
||
|
RenderInfo.m_ColorFeet = m_pClient->m_pSkins->GetColorV4(pGhost->m_GhostInfo.m_ColorFeet);
|
||
|
|
||
|
if(pGhost->m_GhostInfo.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 Angle = mix((float)Prev.m_Angle, (float)Player.m_Angle, Client()->IntraGameTick())/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), Client()->PredIntraGameTick());
|
||
|
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), Client()->PredIntraGameTick());
|
||
|
|
||
|
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);
|
||
|
|
||
|
float WalkTime = fmod(absolute(Position.x), 100.0f)/100.0f;
|
||
|
CAnimState State;
|
||
|
State.Set(&g_pData->m_aAnimations[ANIM_BASE], 0);
|
||
|
|
||
|
if(InAir)
|
||
|
State.Add(&g_pData->m_aAnimations[ANIM_INAIR], 0, 1.0f);
|
||
|
else if(Stationary)
|
||
|
State.Add(&g_pData->m_aAnimations[ANIM_IDLE], 0, 1.0f);
|
||
|
else if(!WantOtherDir)
|
||
|
State.Add(&g_pData->m_aAnimations[ANIM_WALK], WalkTime, 1.0f);
|
||
|
|
||
|
if (Player.m_Weapon == WEAPON_GRENADE)
|
||
|
{
|
||
|
Graphics()->TextureSet(g_pData->m_aImages[IMAGE_GAME].m_Id);
|
||
|
Graphics()->QuadsBegin();
|
||
|
Graphics()->QuadsSetRotation(State.GetAttach()->m_Angle*pi*2+Angle);
|
||
|
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||
|
|
||
|
// normal weapons
|
||
|
int iw = clamp(Player.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+Client()->PredIntraGameTick())/5.0f;
|
||
|
if(a < 1)
|
||
|
Recoil = sinf(a*pi);
|
||
|
|
||
|
vec2 p = Position + Dir * g_pData->m_Weapons.m_aId[iw].m_Offsetx - Direction*Recoil*10.0f;
|
||
|
p.y += g_pData->m_Weapons.m_aId[iw].m_Offsety;
|
||
|
RenderTools()->DrawSprite(p.x, p.y, g_pData->m_Weapons.m_aId[iw].m_VisualSize);
|
||
|
Graphics()->QuadsEnd();
|
||
|
}
|
||
|
|
||
|
// Render ghost
|
||
|
RenderTools()->RenderTee(&State, &RenderInfo, 0, Direction, Position, true);
|
||
|
}
|
||
|
|
||
|
void CGhost::RenderGhostHook(CGhostList *pGhost)
|
||
|
{
|
||
|
if(m_CurPos >= pGhost->m_BestPath.size())
|
||
|
return;
|
||
|
|
||
|
CNetObj_Character Player = pGhost->m_BestPath[m_CurPos];
|
||
|
CNetObj_Character Prev = pGhost->m_BestPath[m_CurPos];
|
||
|
|
||
|
if(m_CurPos > 0)
|
||
|
Prev = pGhost->m_BestPath[m_CurPos-1];
|
||
|
|
||
|
if (Prev.m_HookState<=0 || Player.m_HookState<=0)
|
||
|
return;
|
||
|
|
||
|
float Angle = mix((float)Prev.m_Angle, (float)Player.m_Angle, Client()->IntraGameTick())/256.0f;
|
||
|
vec2 Direction = GetDirection((int)(Angle*256.0f));
|
||
|
vec2 Pos = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), Client()->PredIntraGameTick());
|
||
|
|
||
|
vec2 HookPos = mix(vec2(Prev.m_HookX, Prev.m_HookY), vec2(Player.m_HookX, Player.m_HookY), Client()->PredIntraGameTick());
|
||
|
float d = distance(Pos, HookPos);
|
||
|
vec2 Dir = normalize(Pos-HookPos);
|
||
|
|
||
|
Graphics()->TextureSet(g_pData->m_aImages[IMAGE_GAME].m_Id);
|
||
|
Graphics()->QuadsBegin();
|
||
|
Graphics()->QuadsSetRotation(GetAngle(Dir)+pi);
|
||
|
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||
|
|
||
|
// render head
|
||
|
RenderTools()->SelectSprite(SPRITE_HOOK_HEAD);
|
||
|
IGraphics::CQuadItem QuadItem(HookPos.x, HookPos.y, 24, 16);
|
||
|
Graphics()->QuadsDraw(&QuadItem, 1);
|
||
|
|
||
|
// render chain
|
||
|
RenderTools()->SelectSprite(SPRITE_HOOK_CHAIN);
|
||
|
IGraphics::CQuadItem Array[1024];
|
||
|
int j = 0;
|
||
|
for(float f = 24; f < d && j < 1024; f += 24, j++)
|
||
|
{
|
||
|
vec2 p = HookPos + Dir*f;
|
||
|
Array[j] = IGraphics::CQuadItem(p.x, p.y, 24, 16);
|
||
|
}
|
||
|
|
||
|
Graphics()->QuadsDraw(Array, j);
|
||
|
Graphics()->QuadsSetRotation(0);
|
||
|
Graphics()->QuadsEnd();
|
||
|
}
|
||
|
|
||
|
void CGhost::StartRecord()
|
||
|
{
|
||
|
m_Recording = true;
|
||
|
m_CurPath.clear();
|
||
|
CNetObj_ClientInfo *pInfo = (CNetObj_ClientInfo *) Client()->SnapFindItem(IClient::SNAP_CURRENT, NETOBJTYPE_CLIENTINFO, m_pClient->m_Snap.m_LocalCid);
|
||
|
m_CurInfo = *pInfo;
|
||
|
m_StartRecordTick = Client()->GameTick();
|
||
|
}
|
||
|
|
||
|
void CGhost::StopRecord()
|
||
|
{
|
||
|
m_Recording = false;
|
||
|
}
|
||
|
|
||
|
void CGhost::StartRender()
|
||
|
{
|
||
|
m_CurPos = 0;
|
||
|
m_Rendering = true;
|
||
|
m_StartRenderTick = Client()->PredGameTick();
|
||
|
}
|
||
|
|
||
|
void CGhost::StopRender()
|
||
|
{
|
||
|
m_Rendering = false;
|
||
|
}
|
||
|
|
||
|
void CGhost::Save()
|
||
|
{
|
||
|
if(!g_Config.m_ClRaceSaveGhost)
|
||
|
return;
|
||
|
|
||
|
CGhostHeader Header;
|
||
|
|
||
|
// 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;
|
||
|
|
||
|
if(aName[i] == '\\' || aName[i] == '/' || aName[i] == '|' || aName[i] == ':' || aName[i] == '*' || aName[i] == '?' || aName[i] == '<' || aName[i] == '>' || aName[i] == '"')
|
||
|
aName[i] = '%';
|
||
|
}
|
||
|
|
||
|
char aFilename[256];
|
||
|
char aBuf[256];
|
||
|
str_format(aFilename, sizeof(aFilename), "%s_%s_%.3f_%08x.gho", Client()->GetCurrentMap(), aName, m_BestTime, Client()->GetCurrentMapCrc());
|
||
|
str_format(aBuf, sizeof(aBuf), "ghosts/%s", aFilename);
|
||
|
IOHANDLE File = Storage()->OpenFile(aBuf, IOFLAG_WRITE, IStorage::TYPE_SAVE);
|
||
|
if(!File)
|
||
|
return;
|
||
|
|
||
|
// 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_CurInfo.m_Name0, 6, 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;
|
||
|
io_write(File, &Header, sizeof(Header));
|
||
|
|
||
|
// write client info
|
||
|
io_write(File, &m_CurInfo, sizeof(m_CurInfo));
|
||
|
|
||
|
// write data
|
||
|
int ItemsPerPackage = 500; // 500 ticks per package
|
||
|
int Num = m_CurPath.size();
|
||
|
CNetObj_Character *Data = &m_CurPath[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(CNetObj_Character)*Items;
|
||
|
mem_copy(aBuffer2, Data, Size);
|
||
|
Data += Items;
|
||
|
|
||
|
Size = CVariableInt::Compress(aBuffer2, Size, aBuffer);
|
||
|
Size = CNetBase::Compress(aBuffer, Size, aBuffer2, sizeof(aBuffer2));
|
||
|
|
||
|
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);
|
||
|
|
||
|
// remove old ghost from list
|
||
|
for(int i = 0; i < m_pClient->m_pMenus->m_lGhosts.size(); i++)
|
||
|
{
|
||
|
CMenus::CGhostItem TmpItem = m_pClient->m_pMenus->m_lGhosts[i];
|
||
|
if(TmpItem.m_ID == -1)
|
||
|
{
|
||
|
char aFile[256];
|
||
|
str_format(aFile, sizeof(aFile), "ghosts/%s", TmpItem.m_aFilename);
|
||
|
Storage()->RemoveFile(aFile, IStorage::TYPE_SAVE);
|
||
|
m_pClient->m_pMenus->m_lGhosts.remove_index(i);
|
||
|
break; // TODO: remove other ghosts?
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// add new ghost to ghost list
|
||
|
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));
|
||
|
Item.m_Time = m_BestTime;
|
||
|
Item.m_Active = true;
|
||
|
Item.m_ID = -1;
|
||
|
m_pClient->m_pMenus->m_lGhosts.add(Item);
|
||
|
}
|
||
|
|
||
|
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)
|
||
|
return;
|
||
|
|
||
|
// read header
|
||
|
CGhostHeader Header;
|
||
|
if(!GetHeader(&File, &Header))
|
||
|
{
|
||
|
io_close(File);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(ID == -1)
|
||
|
m_BestTime = Header.m_Time;
|
||
|
|
||
|
// create ghost
|
||
|
CGhostList Ghost;
|
||
|
Ghost.m_ID = ID;
|
||
|
|
||
|
// read client info
|
||
|
io_read(File, &Ghost.m_GhostInfo, sizeof(Ghost.m_GhostInfo));
|
||
|
|
||
|
// read data
|
||
|
Ghost.m_BestPath.clear();
|
||
|
|
||
|
while(1)
|
||
|
{
|
||
|
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)
|
||
|
{
|
||
|
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error reading chunk");
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
Size = CNetBase::Decompress(aCompresseddata, Size, aDecompressed, sizeof(aDecompressed));
|
||
|
if(Size < 0)
|
||
|
{
|
||
|
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during network decompression");
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
Size = CVariableInt::Decompress(aDecompressed, Size, aData);
|
||
|
if(Size < 0)
|
||
|
{
|
||
|
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during intpack decompression");
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
CNetObj_Character *Tmp = (CNetObj_Character*)aData;
|
||
|
for(int i = 0; i < (signed)(Size/sizeof(CNetObj_Character)); i++)
|
||
|
{
|
||
|
Ghost.m_BestPath.add(*Tmp);
|
||
|
Tmp++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
io_close(File);
|
||
|
|
||
|
m_lGhosts.add(Ghost);
|
||
|
}
|
||
|
|
||
|
void CGhost::Unload(int ID)
|
||
|
{
|
||
|
for(int i = 0; i < m_lGhosts.size(); i++)
|
||
|
{
|
||
|
if(m_lGhosts[i].m_ID == ID)
|
||
|
{
|
||
|
m_lGhosts.remove_index_fast(i);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void CGhost::ConGPlay(IConsole::IResult *pResult, void *pUserData, int ClientId)
|
||
|
{
|
||
|
((CGhost *)pUserData)->StartRender();
|
||
|
}
|
||
|
|
||
|
void CGhost::OnConsoleInit()
|
||
|
{
|
||
|
Console()->Register("gplay","", CFGFLAG_CLIENT, ConGPlay, this, "", -1);
|
||
|
}
|
||
|
|
||
|
void CGhost::OnMessage(int MsgType, void *pRawMsg)
|
||
|
{
|
||
|
if(!g_Config.m_ClRaceGhost || m_pClient->m_Snap.m_Spectate)
|
||
|
return;
|
||
|
|
||
|
// only for race
|
||
|
if(!m_pClient->m_IsRace)
|
||
|
return;
|
||
|
|
||
|
// 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_LocalCid)
|
||
|
{
|
||
|
OnReset();
|
||
|
}
|
||
|
}
|
||
|
else if(MsgType == NETMSGTYPE_SV_CHAT)
|
||
|
{
|
||
|
CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
|
||
|
if(pMsg->m_Cid == -1 && m_RaceState == RACE_STARTED)
|
||
|
{
|
||
|
const char* pMessage = pMsg->m_pMessage;
|
||
|
|
||
|
int Num = 0;
|
||
|
while(str_comp_num(pMessage, " finished in: ", 14))
|
||
|
{
|
||
|
pMessage++;
|
||
|
Num++;
|
||
|
if(!pMessage[0])
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// store the name
|
||
|
char aName[64];
|
||
|
str_copy(aName, pMsg->m_pMessage, Num+1);
|
||
|
|
||
|
// prepare values and state for saving
|
||
|
int Minutes;
|
||
|
float Seconds;
|
||
|
if(!str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalCid].m_aName) && sscanf(pMessage, " finished in: %d minute(s) %f", &Minutes, &Seconds) == 2)
|
||
|
{
|
||
|
m_RaceState = RACE_FINISHED;
|
||
|
if(m_Recording)
|
||
|
{
|
||
|
float CurTime = Minutes*60 + Seconds;
|
||
|
if(CurTime < m_BestTime || m_BestTime == -1)
|
||
|
{
|
||
|
m_NewRecord = true;
|
||
|
m_BestTime = CurTime;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void CGhost::OnReset()
|
||
|
{
|
||
|
StopRecord();
|
||
|
StopRender();
|
||
|
m_RaceState = RACE_NONE;
|
||
|
m_NewRecord = false;
|
||
|
m_CurPath.clear();
|
||
|
m_StartRenderTick = -1;
|
||
|
}
|
||
|
|
||
|
void CGhost::OnMapLoad()
|
||
|
{
|
||
|
OnReset();
|
||
|
m_BestTime = -1;
|
||
|
m_lGhosts.clear();
|
||
|
m_pClient->m_pMenus->GhostlistPopulate();
|
||
|
}
|