1#include "ghost.h"
2
3#include <base/system.h>
4
5#include <engine/console.h>
6#include <engine/shared/compression.h>
7#include <engine/shared/config.h>
8#include <engine/shared/network.h>
9#include <engine/storage.h>
10
11static const unsigned char gs_aHeaderMarker[8] = {'T', 'W', 'G', 'H', 'O', 'S', 'T', 0};
12static const unsigned char gs_CurVersion = 6;
13static const int gs_NumTicksOffset = 93;
14
15static const ColorRGBA gs_GhostPrintColor{0.65f, 0.6f, 0.6f, 1.0f};
16
17CGhostRecorder::CGhostRecorder()
18{
19 m_File = 0;
20 ResetBuffer();
21}
22
23void CGhostRecorder::Init()
24{
25 m_pConsole = Kernel()->RequestInterface<IConsole>();
26 m_pStorage = Kernel()->RequestInterface<IStorage>();
27}
28
29// Record
30int CGhostRecorder::Start(const char *pFilename, const char *pMap, SHA256_DIGEST MapSha256, const char *pName)
31{
32 m_File = m_pStorage->OpenFile(pFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
33 if(!m_File)
34 {
35 char aBuf[256];
36 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Unable to open '%s' for ghost recording", pFilename);
37 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost_recorder", pStr: aBuf, PrintColor: gs_GhostPrintColor);
38 return -1;
39 }
40
41 // write header
42 CGhostHeader Header;
43 mem_zero(block: &Header, size: sizeof(Header));
44 mem_copy(dest: Header.m_aMarker, source: gs_aHeaderMarker, size: sizeof(Header.m_aMarker));
45 Header.m_Version = gs_CurVersion;
46 str_copy(dst&: Header.m_aOwner, src: pName);
47 str_copy(dst&: Header.m_aMap, src: pMap);
48 Header.m_MapSha256 = MapSha256;
49 io_write(io: m_File, buffer: &Header, size: sizeof(Header));
50
51 m_LastItem.Reset();
52 ResetBuffer();
53
54 char aBuf[256];
55 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "ghost recording to '%s'", pFilename);
56 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost_recorder", pStr: aBuf, PrintColor: gs_GhostPrintColor);
57 return 0;
58}
59
60void CGhostRecorder::ResetBuffer()
61{
62 m_pBufferPos = m_aBuffer;
63 m_BufferNumItems = 0;
64}
65
66static void DiffItem(int *pPast, int *pCurrent, int *pOut, int Size)
67{
68 while(Size)
69 {
70 *pOut = *pCurrent - *pPast;
71 pOut++;
72 pPast++;
73 pCurrent++;
74 Size--;
75 }
76}
77
78void CGhostRecorder::WriteData(int Type, const void *pData, int Size)
79{
80 if(!m_File || (unsigned)Size > MAX_ITEM_SIZE || Size <= 0 || Type == -1)
81 return;
82
83 CGhostItem Data(Type);
84 mem_copy(dest: Data.m_aData, source: pData, size: Size);
85
86 if(m_LastItem.m_Type == Data.m_Type)
87 DiffItem(pPast: (int *)m_LastItem.m_aData, pCurrent: (int *)Data.m_aData, pOut: (int *)m_pBufferPos, Size: Size / sizeof(int32_t));
88 else
89 {
90 FlushChunk();
91 mem_copy(dest: m_pBufferPos, source: Data.m_aData, size: Size);
92 }
93
94 m_LastItem = Data;
95 m_pBufferPos += Size;
96 m_BufferNumItems++;
97 if(m_BufferNumItems >= NUM_ITEMS_PER_CHUNK)
98 FlushChunk();
99}
100
101void CGhostRecorder::FlushChunk()
102{
103 static char s_aBuffer[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK];
104 static char s_aBuffer2[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK];
105 unsigned char aChunk[4];
106
107 int Size = m_pBufferPos - m_aBuffer;
108 int Type = m_LastItem.m_Type;
109
110 if(!m_File || Size == 0)
111 return;
112
113 while(Size & 3)
114 m_aBuffer[Size++] = 0;
115
116 Size = CVariableInt::Compress(pSrc: m_aBuffer, SrcSize: Size, pDst: s_aBuffer, DstSize: sizeof(s_aBuffer));
117 if(Size < 0)
118 return;
119
120 Size = CNetBase::Compress(pData: s_aBuffer, DataSize: Size, pOutput: s_aBuffer2, OutputSize: sizeof(s_aBuffer2));
121 if(Size < 0)
122 return;
123
124 aChunk[0] = Type & 0xff;
125 aChunk[1] = m_BufferNumItems & 0xff;
126 aChunk[2] = (Size >> 8) & 0xff;
127 aChunk[3] = (Size)&0xff;
128
129 io_write(io: m_File, buffer: aChunk, size: sizeof(aChunk));
130 io_write(io: m_File, buffer: s_aBuffer2, size: Size);
131
132 m_LastItem.Reset();
133 ResetBuffer();
134}
135
136int CGhostRecorder::Stop(int Ticks, int Time)
137{
138 if(!m_File)
139 return -1;
140
141 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost_recorder", pStr: "Stopped ghost recording", PrintColor: gs_GhostPrintColor);
142
143 FlushChunk();
144
145 // write down num shots and time
146 io_seek(io: m_File, offset: gs_NumTicksOffset, origin: IOSEEK_START);
147
148 unsigned char aNumTicks[sizeof(int32_t)];
149 uint_to_bytes_be(bytes: aNumTicks, value: Ticks);
150 io_write(io: m_File, buffer: aNumTicks, size: sizeof(aNumTicks));
151
152 unsigned char aTime[sizeof(int32_t)];
153 uint_to_bytes_be(bytes: aTime, value: Time);
154 io_write(io: m_File, buffer: aTime, size: sizeof(aTime));
155
156 io_close(io: m_File);
157 m_File = 0;
158 return 0;
159}
160
161CGhostLoader::CGhostLoader()
162{
163 m_File = 0;
164 ResetBuffer();
165}
166
167void CGhostLoader::Init()
168{
169 m_pConsole = Kernel()->RequestInterface<IConsole>();
170 m_pStorage = Kernel()->RequestInterface<IStorage>();
171}
172
173void CGhostLoader::ResetBuffer()
174{
175 m_pBufferPos = m_aBuffer;
176 m_BufferNumItems = 0;
177 m_BufferCurItem = 0;
178 m_BufferPrevItem = -1;
179}
180
181int CGhostLoader::Load(const char *pFilename, const char *pMap, SHA256_DIGEST MapSha256, unsigned MapCrc)
182{
183 m_File = m_pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: IStorage::TYPE_SAVE);
184 if(!m_File)
185 {
186 char aBuf[256];
187 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "could not open '%s'", pFilename);
188 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost_loader", pStr: aBuf);
189 return -1;
190 }
191
192 // read the header
193 mem_zero(block: &m_Header, size: sizeof(m_Header));
194 io_read(io: m_File, buffer: &m_Header, size: sizeof(CGhostHeader));
195 if(mem_comp(a: m_Header.m_aMarker, b: gs_aHeaderMarker, size: sizeof(gs_aHeaderMarker)) != 0)
196 {
197 char aBuf[256];
198 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' is not a ghost file", pFilename);
199 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost_loader", pStr: aBuf);
200 io_close(io: m_File);
201 m_File = 0;
202 return -1;
203 }
204
205 if(!(4 <= m_Header.m_Version && m_Header.m_Version <= gs_CurVersion))
206 {
207 char aBuf[256];
208 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "ghost version %d is not supported", m_Header.m_Version);
209 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost_loader", pStr: aBuf);
210 io_close(io: m_File);
211 m_File = 0;
212 return -1;
213 }
214
215 if(str_comp(a: m_Header.m_aMap, b: pMap) != 0)
216 {
217 char aBuf[256];
218 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "ghost map name '%s' does not match current map '%s'", m_Header.m_aMap, pMap);
219 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost_loader", pStr: aBuf);
220 io_close(io: m_File);
221 m_File = 0;
222 return -1;
223 }
224
225 if(m_Header.m_Version >= 6)
226 {
227 if(m_Header.m_MapSha256 != MapSha256 && g_Config.m_ClRaceGhostStrictMap)
228 {
229 char aGhostSha256[SHA256_MAXSTRSIZE];
230 sha256_str(digest: m_Header.m_MapSha256, str: aGhostSha256, max_len: sizeof(aGhostSha256));
231 char aMapSha256[SHA256_MAXSTRSIZE];
232 sha256_str(digest: MapSha256, str: aMapSha256, max_len: sizeof(aMapSha256));
233 char aBuf[256];
234 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "ghost map '%s' sha256 mismatch, wanted=%s ghost=%s", pMap, aMapSha256, aGhostSha256);
235 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost_loader", pStr: aBuf);
236 io_close(io: m_File);
237 m_File = 0;
238 return -1;
239 }
240 }
241 else
242 {
243 io_skip(io: m_File, size: -(int)sizeof(SHA256_DIGEST));
244 unsigned GhostMapCrc = bytes_be_to_uint(bytes: m_Header.m_aZeroes);
245 if(GhostMapCrc != MapCrc && g_Config.m_ClRaceGhostStrictMap)
246 {
247 char aBuf[256];
248 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "ghost map '%s' crc mismatch, wanted=%08x ghost=%08x", pMap, MapCrc, GhostMapCrc);
249 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost_loader", pStr: aBuf);
250 io_close(io: m_File);
251 m_File = 0;
252 return -1;
253 }
254 }
255
256 m_Info = m_Header.ToGhostInfo();
257 m_LastItem.Reset();
258 ResetBuffer();
259
260 return 0;
261}
262
263int CGhostLoader::ReadChunk(int *pType)
264{
265 static char s_aCompresseddata[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK];
266 static char s_aDecompressed[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK];
267 unsigned char aChunk[4];
268
269 if(m_Header.m_Version != 4)
270 m_LastItem.Reset();
271 ResetBuffer();
272
273 if(io_read(io: m_File, buffer: aChunk, size: sizeof(aChunk)) != sizeof(aChunk))
274 return -1;
275
276 *pType = aChunk[0];
277 int Size = (aChunk[2] << 8) | aChunk[3];
278 m_BufferNumItems = aChunk[1];
279
280 if(Size > MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK || Size <= 0)
281 return -1;
282
283 if(io_read(io: m_File, buffer: s_aCompresseddata, size: Size) != (unsigned)Size)
284 {
285 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost", pStr: "error reading chunk");
286 return -1;
287 }
288
289 Size = CNetBase::Decompress(pData: s_aCompresseddata, DataSize: Size, pOutput: s_aDecompressed, OutputSize: sizeof(s_aDecompressed));
290 if(Size < 0)
291 {
292 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost", pStr: "error during network decompression");
293 return -1;
294 }
295
296 Size = CVariableInt::Decompress(pSrc: s_aDecompressed, SrcSize: Size, pDst: m_aBuffer, DstSize: sizeof(m_aBuffer));
297 if(Size < 0)
298 {
299 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "ghost", pStr: "error during intpack decompression");
300 return -1;
301 }
302
303 return 0;
304}
305
306bool CGhostLoader::ReadNextType(int *pType)
307{
308 if(!m_File)
309 return false;
310
311 if(m_BufferCurItem != m_BufferPrevItem && m_BufferCurItem < m_BufferNumItems)
312 {
313 *pType = m_LastItem.m_Type;
314 }
315 else
316 {
317 if(ReadChunk(pType))
318 return false; // error or eof
319 }
320
321 m_BufferPrevItem = m_BufferCurItem;
322
323 return true;
324}
325
326static void UndiffItem(int *pPast, int *pDiff, int *pOut, int Size)
327{
328 while(Size)
329 {
330 *pOut = *pPast + *pDiff;
331 pOut++;
332 pPast++;
333 pDiff++;
334 Size--;
335 }
336}
337
338bool CGhostLoader::ReadData(int Type, void *pData, int Size)
339{
340 if(!m_File || Size > MAX_ITEM_SIZE || Size <= 0 || Type == -1)
341 return false;
342
343 CGhostItem Data(Type);
344
345 if(m_LastItem.m_Type == Data.m_Type)
346 UndiffItem(pPast: (int *)m_LastItem.m_aData, pDiff: (int *)m_pBufferPos, pOut: (int *)Data.m_aData, Size: Size / sizeof(int32_t));
347 else
348 mem_copy(dest: Data.m_aData, source: m_pBufferPos, size: Size);
349
350 mem_copy(dest: pData, source: Data.m_aData, size: Size);
351
352 m_LastItem = Data;
353 m_pBufferPos += Size;
354 m_BufferCurItem++;
355 return true;
356}
357
358void CGhostLoader::Close()
359{
360 if(!m_File)
361 return;
362 io_close(io: m_File);
363 m_File = 0;
364}
365
366bool CGhostLoader::GetGhostInfo(const char *pFilename, CGhostInfo *pGhostInfo, const char *pMap, SHA256_DIGEST MapSha256, unsigned MapCrc)
367{
368 CGhostHeader Header;
369 mem_zero(block: &Header, size: sizeof(Header));
370
371 IOHANDLE File = m_pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: IStorage::TYPE_SAVE);
372 if(!File)
373 return false;
374
375 io_read(io: File, buffer: &Header, size: sizeof(Header));
376 io_close(io: File);
377
378 if(mem_comp(a: Header.m_aMarker, b: gs_aHeaderMarker, size: sizeof(gs_aHeaderMarker)) || !(4 <= Header.m_Version && Header.m_Version <= gs_CurVersion))
379 return false;
380
381 if(str_comp(a: Header.m_aMap, b: pMap) != 0)
382 {
383 return false;
384 }
385
386 if(Header.m_Version >= 6 && g_Config.m_ClRaceGhostStrictMap)
387 {
388 if(Header.m_MapSha256 != MapSha256)
389 {
390 return false;
391 }
392 }
393 else if(g_Config.m_ClRaceGhostStrictMap)
394 {
395 unsigned GhostMapCrc = bytes_be_to_uint(bytes: Header.m_aZeroes);
396 if(GhostMapCrc != MapCrc)
397 {
398 return false;
399 }
400 }
401 *pGhostInfo = Header.ToGhostInfo();
402
403 return true;
404}
405