From b1e71388472b1edf75778a1ad5193fc7c981de80 Mon Sep 17 00:00:00 2001 From: Redix Date: Sat, 9 Sep 2017 02:41:11 +0200 Subject: [PATCH] Added ghost file handling classes --- CMakeLists.txt | 2 + src/engine/ghost.h | 46 ++++ src/engine/shared/ghost.cpp | 521 ++++++++++++++++++++++++++++++++++++ src/engine/shared/ghost.h | 142 ++++++++++ 4 files changed, 711 insertions(+) create mode 100644 src/engine/ghost.h create mode 100644 src/engine/shared/ghost.cpp create mode 100644 src/engine/shared/ghost.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f3b2719a0..61dd8ec91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -420,6 +420,8 @@ set_glob(ENGINE_SHARED GLOB src/engine/shared fifo.h filecollection.cpp filecollection.h + ghost.cpp + ghost.h global_uuid_manager.cpp huffman.cpp huffman.h diff --git a/src/engine/ghost.h b/src/engine/ghost.h new file mode 100644 index 000000000..5195ae5fa --- /dev/null +++ b/src/engine/ghost.h @@ -0,0 +1,46 @@ +#ifndef ENGINE_GHOST_H +#define ENGINE_GHOST_H + +#include + +#include "kernel.h" + +struct CGhostHeader +{ + unsigned char m_aMarker[8]; + unsigned char m_Version; + char m_aOwner[MAX_NAME_LENGTH]; + char m_aMap[64]; + unsigned char m_aCrc[4]; + unsigned char m_aNumTicks[4]; + unsigned char m_aTime[4]; +}; + +class IGhostRecorder : public IInterface +{ + MACRO_INTERFACE("ghostrecorder", 0) +public: + virtual ~IGhostRecorder() {} + virtual int Stop(int Ticks, int Time) = 0; + + virtual void WriteData(int Type, const char *pData, int Size) = 0; + virtual bool IsRecording() const = 0; +}; + +class IGhostLoader : public IInterface +{ + MACRO_INTERFACE("ghostloader", 0) +public: + virtual ~IGhostLoader() {} + virtual void Close() = 0; + + virtual const CGhostHeader *GetHeader() const = 0; + + virtual bool ReadNextType(int *pType) = 0; + virtual bool ReadData(int Type, char *pData, int Size) = 0; + + virtual int GetTime(const CGhostHeader *pHeader) const = 0; + virtual int GetTicks(const CGhostHeader *pHeader) const = 0; +}; + +#endif diff --git a/src/engine/shared/ghost.cpp b/src/engine/shared/ghost.cpp new file mode 100644 index 000000000..0afbf1107 --- /dev/null +++ b/src/engine/shared/ghost.cpp @@ -0,0 +1,521 @@ + + +#include + +#include +#include + +#include "ghost.h" +#include "compression.h" +#include "network.h" + +static const unsigned char gs_aHeaderMarker[8] = {'T', 'W', 'G', 'H', 'O', 'S', 'T', 0}; +static const unsigned char gs_ActVersion = 5; +static const int gs_NumTicksOffset = 93; + +CGhostRecorder::CGhostRecorder() +{ + m_File = 0; + ResetBuffer(); +} + +// Record +int CGhostRecorder::Start(IStorage *pStorage, IConsole *pConsole, const char *pFilename, const char *pMap, unsigned Crc, const char* pName) +{ + m_pConsole = pConsole; + + m_File = pStorage->OpenFile(pFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE); + if(!m_File) + { + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "Unable to open '%s' for ghost recording", pFilename); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_recorder", aBuf); + return -1; + } + + // write header + CGhostHeader Header; + mem_zero(&Header, sizeof(Header)); + mem_copy(Header.m_aMarker, gs_aHeaderMarker, sizeof(Header.m_aMarker)); + Header.m_Version = gs_ActVersion; + str_copy(Header.m_aOwner, pName, sizeof(Header.m_aOwner)); + str_copy(Header.m_aMap, pMap, 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; + io_write(m_File, &Header, sizeof(Header)); + + m_LastItem.Reset(); + ResetBuffer(); + + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "Ghost recording to '%s'", pFilename); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_recorder", aBuf); + return 0; +} + +void CGhostRecorder::ResetBuffer() +{ + m_pBufferPos = m_aBuffer; + m_BufferNumItems = 0; +} + +static void DiffItem(int *pPast, int *pCurrent, int *pOut, int Size) +{ + while(Size) + { + *pOut = *pCurrent - *pPast; + pOut++; + pPast++; + pCurrent++; + Size--; + } +} + +void CGhostRecorder::WriteData(int Type, const char *pData, int Size) +{ + if(!m_File || (unsigned)Size > MAX_ITEM_SIZE || Size <= 0 || Type == -1) + return; + + CGhostItem Data(Type); + mem_copy(Data.m_aData, pData, Size); + + if(m_LastItem.m_Type == Data.m_Type) + DiffItem((int*)m_LastItem.m_aData, (int*)Data.m_aData, (int*)m_pBufferPos, Size/4); + else + { + FlushChunk(); + mem_copy(m_pBufferPos, Data.m_aData, Size); + } + + m_LastItem = Data; + m_pBufferPos += Size; + m_BufferNumItems++; + if(m_BufferNumItems >= NUM_ITEMS_PER_CHUNK) + FlushChunk(); +} + +void CGhostRecorder::FlushChunk() +{ + static char s_aBuffer[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK]; + static char s_aBuffer2[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK]; + unsigned char aChunk[4]; + + int Size = m_pBufferPos - m_aBuffer; + int Type = m_LastItem.m_Type; + + if(!m_File || Size == 0) + return; + + while(Size&3) + m_aBuffer[Size++] = 0; + + Size = CVariableInt::Compress(m_aBuffer, Size, s_aBuffer, sizeof(s_aBuffer)); + if(Size < 0) + return; + + Size = CNetBase::Compress(s_aBuffer, Size, s_aBuffer2, sizeof(s_aBuffer2)); + if(Size < 0) + return; + + aChunk[0] = Type&0xff; + aChunk[1] = m_BufferNumItems&0xff; + aChunk[2] = (Size>>8)&0xff; + aChunk[3] = (Size)&0xff; + + io_write(m_File, aChunk, sizeof(aChunk)); + io_write(m_File, s_aBuffer2, Size); + + m_LastItem.Reset(); + ResetBuffer(); +} + +int CGhostRecorder::Stop(int Ticks, int Time) +{ + if(!m_File) + return -1; + + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_recorder", "Stopped ghost recording"); + + FlushChunk(); + + unsigned char aNumTicks[4]; + unsigned char aTime[4]; + + aNumTicks[0] = (Ticks>>24)&0xff; + aNumTicks[1] = (Ticks>>16)&0xff; + aNumTicks[2] = (Ticks>>8)&0xff; + aNumTicks[3] = (Ticks)&0xff; + + aTime[0] = (Time>>24)&0xff; + aTime[1] = (Time>>16)&0xff; + aTime[2] = (Time>>8)&0xff; + aTime[3] = (Time)&0xff; + + // write down num shots and time + io_seek(m_File, gs_NumTicksOffset, IOSEEK_START); + io_write(m_File, &aNumTicks, sizeof(aNumTicks)); + io_write(m_File, &aTime, sizeof(aTime)); + + io_close(m_File); + m_File = 0; + return 0; +} + +CGhostLoader::CGhostLoader() +{ + m_File = 0; + ResetBuffer(); +} + +void CGhostLoader::ResetBuffer() +{ + m_pBufferPos = m_aBuffer; + m_BufferNumItems = 0; + m_BufferCurItem = 0; + m_BufferPrevItem = -1; +} + +int CGhostLoader::Load(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, const char *pMap, unsigned Crc) +{ + m_pConsole = pConsole; + m_File = pStorage->OpenFile(pFilename, IOFLAG_READ, IStorage::TYPE_SAVE); + if(!m_File) + { + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "could not open '%s'", pFilename); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_loader", aBuf); + return -1; + } + + // read the header + mem_zero(&m_Header, sizeof(m_Header)); + io_read(m_File, &m_Header, sizeof(CGhostHeader)); + if(mem_comp(m_Header.m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) != 0) + { + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "'%s' is not a ghost file", pFilename); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_loader", aBuf); + io_close(m_File); + m_File = 0; + return -1; + } + + if(m_Header.m_Version != gs_ActVersion && m_Header.m_Version != 4) + { + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "ghost version %d is not supported", m_Header.m_Version); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_loader", aBuf); + io_close(m_File); + m_File = 0; + return -1; + } + + unsigned GhostMapCrc = (m_Header.m_aCrc[0] << 24) | (m_Header.m_aCrc[1] << 16) | (m_Header.m_aCrc[2] << 8) | (m_Header.m_aCrc[3]); + if(str_comp(m_Header.m_aMap, pMap) != 0 || GhostMapCrc != Crc) + { + io_close(m_File); + m_File = 0; + return -1; + } + + m_LastItem.Reset(); + ResetBuffer(); + + return 0; +} + +int CGhostLoader::ReadChunk(int *pType) +{ + static char s_aCompresseddata[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK]; + static char s_aDecompressed[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK]; + unsigned char aChunk[4]; + + if(m_Header.m_Version != 4) + m_LastItem.Reset(); + ResetBuffer(); + + if(io_read(m_File, aChunk, sizeof(aChunk)) != sizeof(aChunk)) + return -1; + + *pType = aChunk[0]; + int Size = (aChunk[2] << 8) | aChunk[3]; + m_BufferNumItems = aChunk[1]; + + if(Size > MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK || Size <= 0) + return -1; + + if(io_read(m_File, s_aCompresseddata, Size) != (unsigned)Size) + { + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error reading chunk"); + return -1; + } + + Size = CNetBase::Decompress(s_aCompresseddata, Size, s_aDecompressed, sizeof(s_aDecompressed)); + if(Size < 0) + { + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during network decompression"); + return -1; + } + + Size = CVariableInt::Decompress(s_aDecompressed, Size, m_aBuffer, sizeof(m_aBuffer)); + if(Size < 0) + { + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during intpack decompression"); + return -1; + } + + return 0; +} + +bool CGhostLoader::ReadNextType(int *pType) +{ + if(!m_File) + return false; + + if(m_BufferCurItem != m_BufferPrevItem && m_BufferCurItem < m_BufferNumItems) + { + *pType = m_LastItem.m_Type; + } + else + { + if(ReadChunk(pType)) + return false; // error or eof + } + + m_BufferPrevItem = m_BufferCurItem; + + return true; +} + +static void UndiffItem(int *pPast, int *pDiff, int *pOut, int Size) +{ + while(Size) + { + *pOut = *pPast + *pDiff; + pOut++; + pPast++; + pDiff++; + Size--; + } +} + +bool CGhostLoader::ReadData(int Type, char *pData, int Size) +{ + if(!m_File || Size > MAX_ITEM_SIZE || Size <= 0 || Type == -1) + return false; + + CGhostItem Data(Type); + + if(m_LastItem.m_Type == Data.m_Type) + UndiffItem((int*)m_LastItem.m_aData, (int*)m_pBufferPos, (int*)Data.m_aData, Size/4); + else + mem_copy(Data.m_aData, m_pBufferPos, Size); + + mem_copy(pData, Data.m_aData, Size); + + m_LastItem = Data; + m_pBufferPos += Size; + m_BufferCurItem++; + return true; +} + +void CGhostLoader::Close() +{ + if(!m_File) + return; + io_close(m_File); + m_File = 0; +} + +bool CGhostLoader::GetGhostInfo(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, CGhostHeader *pGhostHeader, const char *pMap, unsigned Crc) const +{ + if(!pGhostHeader) + return false; + + mem_zero(pGhostHeader, sizeof(CGhostHeader)); + + IOHANDLE File = pStorage->OpenFile(pFilename, IOFLAG_READ, IStorage::TYPE_SAVE); + if(!File) + return false; + + io_read(File, pGhostHeader, sizeof(CGhostHeader)); + + if(mem_comp(pGhostHeader->m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) == 0 && (pGhostHeader->m_Version == 2 || pGhostHeader->m_Version == 3)) + { + io_close(File); + // old version... try to update + if(CGhostUpdater::Update(pStorage, pConsole, pFilename)) + { + // try again + File = pStorage->OpenFile(pFilename, IOFLAG_READ, IStorage::TYPE_SAVE); + io_read(File, pGhostHeader, sizeof(CGhostHeader)); + } + else + return false; + } + + if(mem_comp(pGhostHeader->m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) || (pGhostHeader->m_Version != gs_ActVersion && pGhostHeader->m_Version != 4)) + { + io_close(File); + return false; + } + + unsigned GhostMapCrc = (pGhostHeader->m_aCrc[0] << 24) | (pGhostHeader->m_aCrc[1] << 16) | (pGhostHeader->m_aCrc[2] << 8) | (pGhostHeader->m_aCrc[3]); + if(str_comp(pGhostHeader->m_aMap, pMap) != 0 || GhostMapCrc != Crc) + { + io_close(File); + return false; + } + + io_close(File); + return true; +} + +int CGhostLoader::GetTime(const CGhostHeader *pHeader) const +{ + return (pHeader->m_aTime[0] << 24) | (pHeader->m_aTime[1] << 16) | (pHeader->m_aTime[2] << 8) | (pHeader->m_aTime[3]); +} + +int CGhostLoader::GetTicks(const CGhostHeader *pHeader) const +{ + return (pHeader->m_aNumTicks[0] << 24) | (pHeader->m_aNumTicks[1] << 16) | (pHeader->m_aNumTicks[2] << 8) | (pHeader->m_aNumTicks[3]); +} + +inline void StrToInts(int *pInts, int Num, const char *pStr) +{ + int Index = 0; + while (Num) + { + char aBuf[4] = { 0,0,0,0 }; + for (int c = 0; c < 4 && pStr[Index]; c++, Index++) + aBuf[c] = pStr[Index]; + *pInts = ((aBuf[0] + 128) << 24) | ((aBuf[1] + 128) << 16) | ((aBuf[2] + 128) << 8) | (aBuf[3] + 128); + pInts++; + Num--; + } + + // null terminate + pInts[-1] &= 0xffffff00; +} + +CGhostRecorder CGhostUpdater::ms_Recorder; + +bool CGhostUpdater::Update(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename) +{ + pStorage->CreateFolder("ghosts/backup", IStorage::TYPE_SAVE); + + const char *pExtractedName = pFilename; + for(const char *pSrc = pFilename; *pSrc; pSrc++) + if(*pSrc == '/' || *pSrc == '\\') + pExtractedName = pSrc + 1; + + char aBackupFilename[512]; + str_format(aBackupFilename, sizeof(aBackupFilename), "ghosts/backup/%s", pExtractedName); + if(!pStorage->RenameFile(pFilename, aBackupFilename, IStorage::TYPE_SAVE)) + return false; + + IOHANDLE File = pStorage->OpenFile(aBackupFilename, IOFLAG_READ, IStorage::TYPE_SAVE); + if(!File) + return false; + + // read header + CGhostHeaderMain Header; + io_read(File, &Header, sizeof(Header)); + if(mem_comp(Header.m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) != 0 || (Header.m_Version != 2 && Header.m_Version != 3)) + { + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost/updater", "error: no valid ghost file"); + io_close(File); + return false; + } + + io_seek(File, 0, IOSEEK_START); + + int Ticks, Time; + if(Header.m_Version == 2) + { + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost/updater", "updating v2 ghost file"); + CGhostHeaderV2 ExtHeader; + char aSkinData[ms_SkinSizeV2]; + io_read(File, &ExtHeader, sizeof(ExtHeader)); + io_read(File, aSkinData, sizeof(aSkinData)); + + Ticks = ExtHeader.m_NumShots; + Time = ExtHeader.m_Time * 1000; + + unsigned Crc = (ExtHeader.m_aCrc[0] << 24) | (ExtHeader.m_aCrc[1] << 16) | (ExtHeader.m_aCrc[2] << 8) | (ExtHeader.m_aCrc[3]); + ms_Recorder.Start(pStorage, pConsole, pFilename, ExtHeader.m_aMap, Crc, ExtHeader.m_aOwner); + + CGhostSkin Skin; + mem_copy(&Skin, aSkinData + ms_SkinOffsetV2, sizeof(Skin)); + ms_Recorder.WriteData(0 /* GHOSTDATA_TYPE_SKIN */, (const char*)&Skin, sizeof(Skin)); + } + else + { + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost/updater", "updating v3 ghost file"); + CGhostHeaderV3 ExtHeader; + io_read(File, &ExtHeader, sizeof(ExtHeader)); + + Ticks = ExtHeader.m_NumShots; + Time = ExtHeader.m_Time * 1000; + + unsigned Crc = (ExtHeader.m_aCrc[0] << 24) | (ExtHeader.m_aCrc[1] << 16) | (ExtHeader.m_aCrc[2] << 8) | (ExtHeader.m_aCrc[3]); + ms_Recorder.Start(pStorage, pConsole, pFilename, ExtHeader.m_aMap, Crc, ExtHeader.m_aOwner); + + CGhostSkin Skin; + StrToInts(&Skin.m_Skin0, 6, ExtHeader.m_aSkinName); + Skin.m_UseCustomColor = ExtHeader.m_UseCustomColor; + Skin.m_ColorBody = ExtHeader.m_ColorBody; + Skin.m_ColorFeet = ExtHeader.m_ColorFeet; + ms_Recorder.WriteData(0 /* GHOSTDATA_TYPE_SKIN */, (const char*)&Skin, sizeof(Skin)); + } + + // read data + int Index = 0; + while(Index < Ticks) + { + static char s_aCompresseddata[100 * 500]; + static char s_aDecompressed[100 * 500]; + static char s_aData[100 * 500]; + + unsigned char aSize[4]; + if(io_read(File, aSize, sizeof(aSize)) != sizeof(aSize)) + break; + unsigned Size = (aSize[0] << 24) | (aSize[1] << 16) | (aSize[2] << 8) | aSize[3]; + + if(io_read(File, s_aCompresseddata, Size) != Size) + { + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost/updater", "error reading chunk"); + break; + } + + int DataSize = CNetBase::Decompress(s_aCompresseddata, Size, s_aDecompressed, sizeof(s_aDecompressed)); + if(DataSize < 0) + { + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost/updater", "error during network decompression"); + break; + } + + DataSize = CVariableInt::Decompress(s_aDecompressed, DataSize, s_aData, sizeof(s_aData)); + if(DataSize < 0) + { + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost/updater", "error during intpack decompression"); + break; + } + + char *pTmp = s_aData; + for(int i = 0; i < DataSize / ms_GhostCharacterSize; i++) + { + ms_Recorder.WriteData(1 /* GHOSTDATA_TYPE_CHARACTER_NO_TICK */, pTmp, ms_GhostCharacterSize); + pTmp += ms_GhostCharacterSize; + Index++; + } + } + + io_close(File); + + bool Error = Ticks != Index; + ms_Recorder.Stop(Index, Error ? 0 : Time); + return !Error; +} diff --git a/src/engine/shared/ghost.h b/src/engine/shared/ghost.h new file mode 100644 index 000000000..87c0e38b4 --- /dev/null +++ b/src/engine/shared/ghost.h @@ -0,0 +1,142 @@ +#ifndef ENGINE_SHARED_GHOST_H +#define ENGINE_SHARED_GHOST_H + +#include + +enum +{ + MAX_ITEM_SIZE = 128, + NUM_ITEMS_PER_CHUNK = 50, +}; + +class CGhostItem +{ +public: + char m_aData[MAX_ITEM_SIZE]; + int m_Type; + + CGhostItem() : m_Type(-1) {} + CGhostItem(int Type) : m_Type(Type) {} + void Reset() { m_Type = -1; } +}; + +class CGhostRecorder : public IGhostRecorder +{ + IOHANDLE m_File; + class IConsole *m_pConsole; + + CGhostItem m_LastItem; + + char m_aBuffer[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK]; + char *m_pBufferPos; + int m_BufferNumItems; + + void ResetBuffer(); + void FlushChunk(); + +public: + CGhostRecorder(); + + int Start(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, const char *pMap, unsigned MapCrc, const char *pName); + int Stop(int Ticks, int Time); + + void WriteData(int Type, const char *pData, int Size); + bool IsRecording() const { return m_File != 0; } +}; + +class CGhostLoader : public IGhostLoader +{ + IOHANDLE m_File; + class IConsole *m_pConsole; + + CGhostHeader m_Header; + + CGhostItem m_LastItem; + + char m_aBuffer[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK]; + char *m_pBufferPos; + int m_BufferNumItems; + int m_BufferCurItem; + int m_BufferPrevItem; + + void ResetBuffer(); + int ReadChunk(int *pType); + +public: + CGhostLoader(); + + int Load(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, const char *pMap, unsigned Crc); + void Close(); + const CGhostHeader *GetHeader() const { return &m_Header; } + + bool ReadNextType(int *pType); + bool ReadData(int Type, char *pData, int Size); + + bool GetGhostInfo(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, CGhostHeader *pGhostHeader, const char *pMap, unsigned Crc) const; + int GetTime(const CGhostHeader *pHeader) const; + int GetTicks(const CGhostHeader *pHeader) const; +}; + +class CGhostUpdater +{ + // all + struct CGhostHeaderMain + { + unsigned char m_aMarker[8]; + unsigned char m_Version; + }; + + // version 2 + struct CGhostHeaderV2 + { + 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; + }; + + static const int ms_SkinSizeV2 = 17 * sizeof(int); + static const int ms_SkinOffsetV2 = 8 * sizeof(int); + + // version 3 + struct CGhostHeaderV3 + { + unsigned char m_aMarker[8]; + unsigned char m_Version; + char m_aOwner[MAX_NAME_LENGTH]; + char m_aSkinName[64]; + int m_UseCustomColor; + int m_ColorBody; + int m_ColorFeet; + char m_aMap[64]; + unsigned char m_aCrc[4]; + int m_NumShots; + float m_Time; + }; + + // actual version + 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; + }; + + static const int ms_GhostCharacterSize = 11 * sizeof(int); + + static CGhostRecorder ms_Recorder; + +public: + static bool Update(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename); +}; + +#endif