1#include "ghost.h"
2
3#include <base/log.h>
4#include <base/system.h>
5
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;
13
14static const LOG_COLOR LOG_COLOR_GHOST{.r: 165, .g: 153, .b: 153};
15
16int CGhostHeader::GetTicks() const
17{
18 return bytes_be_to_uint(bytes: m_aNumTicks);
19}
20
21int CGhostHeader::GetTime() const
22{
23 return bytes_be_to_uint(bytes: m_aTime);
24}
25
26CGhostInfo CGhostHeader::ToGhostInfo() const
27{
28 CGhostInfo Result;
29 str_copy(dst&: Result.m_aOwner, src: m_aOwner);
30 str_copy(dst&: Result.m_aMap, src: m_aMap);
31 Result.m_NumTicks = GetTicks();
32 Result.m_Time = GetTime();
33 return Result;
34}
35
36CGhostRecorder::CGhostRecorder()
37{
38 m_File = nullptr;
39 m_aFilename[0] = '\0';
40 ResetBuffer();
41}
42
43void CGhostRecorder::Init()
44{
45 m_pStorage = Kernel()->RequestInterface<IStorage>();
46}
47
48int CGhostRecorder::Start(const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, const char *pName)
49{
50 dbg_assert(!m_File, "File already open");
51
52 m_File = m_pStorage->OpenFile(pFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
53 if(!m_File)
54 {
55 log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Unable to open '%s' for recording", pFilename);
56 return -1;
57 }
58 str_copy(dst&: m_aFilename, src: pFilename);
59
60 // write header
61 CGhostHeader Header;
62 mem_zero(block: &Header, size: sizeof(Header));
63 mem_copy(dest: Header.m_aMarker, source: gs_aHeaderMarker, size: sizeof(Header.m_aMarker));
64 Header.m_Version = gs_CurVersion;
65 str_copy(dst&: Header.m_aOwner, src: pName);
66 str_copy(dst&: Header.m_aMap, src: pMap);
67 Header.m_MapSha256 = MapSha256;
68 io_write(io: m_File, buffer: &Header, size: sizeof(Header));
69
70 m_LastItem.Reset();
71 ResetBuffer();
72
73 log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Recording to '%s'", pFilename);
74 return 0;
75}
76
77void CGhostRecorder::ResetBuffer()
78{
79 m_pBufferPos = m_aBuffer;
80 m_pBufferEnd = m_aBuffer;
81 m_BufferNumItems = 0;
82}
83
84static void DiffItem(const uint32_t *pPast, const uint32_t *pCurrent, uint32_t *pOut, size_t Size)
85{
86 while(Size)
87 {
88 *pOut = *pCurrent - *pPast;
89 pOut++;
90 pPast++;
91 pCurrent++;
92 Size--;
93 }
94}
95
96void CGhostRecorder::WriteData(int Type, const void *pData, size_t Size)
97{
98 dbg_assert((bool)m_File, "File not open");
99 dbg_assert(Type >= 0 && Type <= (int)std::numeric_limits<unsigned char>::max(), "Type invalid");
100 dbg_assert(Size > 0 && Size <= MAX_ITEM_SIZE && Size % sizeof(uint32_t) == 0, "Size invalid");
101
102 if((size_t)(m_pBufferEnd - m_pBufferPos) < Size)
103 {
104 FlushChunk();
105 }
106
107 CGhostItem Data(Type);
108 mem_copy(dest: Data.m_aData, source: pData, size: Size);
109 if(m_LastItem.m_Type == Data.m_Type)
110 {
111 DiffItem(pPast: (const uint32_t *)m_LastItem.m_aData, pCurrent: (const uint32_t *)Data.m_aData, pOut: (uint32_t *)m_pBufferPos, Size: Size / sizeof(uint32_t));
112 }
113 else
114 {
115 FlushChunk();
116 mem_copy(dest: m_pBufferPos, source: Data.m_aData, size: Size);
117 }
118
119 m_LastItem = Data;
120 m_pBufferPos += Size;
121 m_BufferNumItems++;
122 if(m_BufferNumItems >= NUM_ITEMS_PER_CHUNK)
123 {
124 FlushChunk();
125 }
126}
127
128void CGhostRecorder::FlushChunk()
129{
130 dbg_assert((bool)m_File, "File not open");
131
132 int Size = m_pBufferPos - m_aBuffer;
133 if(Size == 0 || m_BufferNumItems == 0)
134 {
135 return;
136 }
137 dbg_assert(Size % sizeof(uint32_t) == 0, "Chunk size invalid");
138
139 Size = CVariableInt::Compress(pSrc: m_aBuffer, SrcSize: Size, pDst: m_aBufferTemp, DstSize: sizeof(m_aBufferTemp));
140 if(Size < 0)
141 {
142 log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Failed to write chunk to '%s': error during intpack compression", m_aFilename);
143 m_LastItem.Reset();
144 ResetBuffer();
145 return;
146 }
147
148 Size = CNetBase::Compress(pData: m_aBufferTemp, DataSize: Size, pOutput: m_aBuffer, OutputSize: sizeof(m_aBuffer));
149 if(Size < 0)
150 {
151 log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Failed to write chunk to '%s': error during network compression", m_aFilename);
152 m_LastItem.Reset();
153 ResetBuffer();
154 return;
155 }
156
157 unsigned char aChunkHeader[4];
158 aChunkHeader[0] = m_LastItem.m_Type & 0xff;
159 aChunkHeader[1] = m_BufferNumItems & 0xff;
160 aChunkHeader[2] = (Size >> 8) & 0xff;
161 aChunkHeader[3] = Size & 0xff;
162
163 io_write(io: m_File, buffer: aChunkHeader, size: sizeof(aChunkHeader));
164 io_write(io: m_File, buffer: m_aBuffer, size: Size);
165
166 m_LastItem.Reset();
167 ResetBuffer();
168}
169
170void CGhostRecorder::Stop(int Ticks, int Time)
171{
172 if(!m_File)
173 {
174 return;
175 }
176
177 const bool DiscardFile = Ticks <= 0 || Time <= 0;
178
179 if(!DiscardFile)
180 {
181 FlushChunk();
182
183 // write number of ticks and time
184 io_seek(io: m_File, offsetof(CGhostHeader, m_aNumTicks), origin: IOSEEK_START);
185
186 unsigned char aNumTicks[sizeof(int32_t)];
187 uint_to_bytes_be(bytes: aNumTicks, value: Ticks);
188 io_write(io: m_File, buffer: aNumTicks, size: sizeof(aNumTicks));
189
190 unsigned char aTime[sizeof(int32_t)];
191 uint_to_bytes_be(bytes: aTime, value: Time);
192 io_write(io: m_File, buffer: aTime, size: sizeof(aTime));
193 }
194
195 io_close(io: m_File);
196 m_File = nullptr;
197
198 if(DiscardFile)
199 {
200 m_pStorage->RemoveFile(pFilename: m_aFilename, Type: IStorage::TYPE_SAVE);
201 }
202
203 log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Stopped recording to '%s'", m_aFilename);
204 m_aFilename[0] = '\0';
205}
206
207CGhostLoader::CGhostLoader()
208{
209 m_File = nullptr;
210 m_aFilename[0] = '\0';
211 ResetBuffer();
212}
213
214void CGhostLoader::Init()
215{
216 m_pStorage = Kernel()->RequestInterface<IStorage>();
217}
218
219void CGhostLoader::ResetBuffer()
220{
221 m_pBufferPos = m_aBuffer;
222 m_pBufferEnd = m_aBuffer;
223 m_BufferNumItems = 0;
224 m_BufferCurItem = 0;
225 m_BufferPrevItem = -1;
226}
227
228IOHANDLE CGhostLoader::ReadHeader(CGhostHeader &Header, const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc, bool LogMapMismatch) const
229{
230 IOHANDLE File = m_pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: IStorage::TYPE_SAVE);
231 if(!File)
232 {
233 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to open ghost file '%s' for reading", pFilename);
234 return nullptr;
235 }
236
237 if(io_read(io: File, buffer: &Header, size: sizeof(Header)) != sizeof(Header))
238 {
239 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': failed to read header", pFilename);
240 io_close(io: File);
241 return nullptr;
242 }
243
244 if(!ValidateHeader(Header, pFilename) ||
245 !CheckHeaderMap(Header, pFilename, pMap, MapSha256, MapCrc, LogMapMismatch))
246 {
247 io_close(io: File);
248 return nullptr;
249 }
250
251 return File;
252}
253
254bool CGhostLoader::ValidateHeader(const CGhostHeader &Header, const char *pFilename) const
255{
256 if(mem_comp(a: Header.m_aMarker, b: gs_aHeaderMarker, size: sizeof(gs_aHeaderMarker)) != 0)
257 {
258 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': invalid header marker", pFilename);
259 return false;
260 }
261
262 if(Header.m_Version < 4 || Header.m_Version > gs_CurVersion)
263 {
264 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': ghost version '%d' is not supported", pFilename, Header.m_Version);
265 return false;
266 }
267
268 if(!mem_has_null(block: Header.m_aOwner, size: sizeof(Header.m_aOwner)) || !str_utf8_check(str: Header.m_aOwner))
269 {
270 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': owner name is invalid", pFilename);
271 return false;
272 }
273
274 if(!mem_has_null(block: Header.m_aMap, size: sizeof(Header.m_aMap)) || !str_utf8_check(str: Header.m_aMap))
275 {
276 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': map name is invalid", pFilename);
277 return false;
278 }
279
280 const int NumTicks = Header.GetTicks();
281 if(NumTicks <= 0)
282 {
283 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': number of ticks '%d' is invalid", pFilename, NumTicks);
284 return false;
285 }
286
287 const int Time = Header.GetTime();
288 if(Time <= 0)
289 {
290 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': time '%d' is invalid", pFilename, Time);
291 return false;
292 }
293
294 return true;
295}
296
297bool CGhostLoader::CheckHeaderMap(const CGhostHeader &Header, const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc, bool LogMapMismatch) const
298{
299 if(str_comp(a: Header.m_aMap, b: pMap) != 0)
300 {
301 if(LogMapMismatch)
302 {
303 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': ghost map name '%s' does not match current map '%s'", pFilename, Header.m_aMap, pMap);
304 }
305 return false;
306 }
307
308 if(Header.m_Version >= 6)
309 {
310 if(Header.m_MapSha256 != MapSha256 && g_Config.m_ClRaceGhostStrictMap)
311 {
312 if(LogMapMismatch)
313 {
314 char aGhostSha256[SHA256_MAXSTRSIZE];
315 sha256_str(digest: Header.m_MapSha256, str: aGhostSha256, max_len: sizeof(aGhostSha256));
316 char aMapSha256[SHA256_MAXSTRSIZE];
317 sha256_str(digest: MapSha256, str: aMapSha256, max_len: sizeof(aMapSha256));
318 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': ghost map SHA256 mismatch (wanted='%s', ghost='%s')", pFilename, aMapSha256, aGhostSha256);
319 }
320 return false;
321 }
322 }
323 else
324 {
325 const unsigned GhostMapCrc = bytes_be_to_uint(bytes: Header.m_aZeroes);
326 if(GhostMapCrc != MapCrc && g_Config.m_ClRaceGhostStrictMap)
327 {
328 if(LogMapMismatch)
329 {
330 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': ghost map CRC mismatch (wanted='%08x', ghost='%08x')", pFilename, MapCrc, GhostMapCrc);
331 }
332 return false;
333 }
334 }
335
336 return true;
337}
338
339bool CGhostLoader::Load(const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc)
340{
341 dbg_assert(!m_File, "File already open");
342
343 CGhostHeader Header;
344 IOHANDLE File = ReadHeader(Header, pFilename, pMap, MapSha256, MapCrc, LogMapMismatch: true);
345 if(!File)
346 {
347 return false;
348 }
349
350 if(Header.m_Version < 6)
351 {
352 io_skip(io: File, size: -(int)sizeof(SHA256_DIGEST));
353 }
354
355 m_File = File;
356 str_copy(dst&: m_aFilename, src: pFilename);
357 m_Header = Header;
358 m_Info = m_Header.ToGhostInfo();
359 m_LastItem.Reset();
360 ResetBuffer();
361 return true;
362}
363
364bool CGhostLoader::ReadChunk(int *pType)
365{
366 if(m_Header.m_Version != 4)
367 {
368 m_LastItem.Reset();
369 }
370 ResetBuffer();
371
372 unsigned char aChunkHeader[4];
373 if(io_read(io: m_File, buffer: aChunkHeader, size: sizeof(aChunkHeader)) != sizeof(aChunkHeader))
374 {
375 return false; // EOF
376 }
377
378 *pType = aChunkHeader[0];
379 int Size = (aChunkHeader[2] << 8) | aChunkHeader[3];
380 m_BufferNumItems = aChunkHeader[1];
381
382 if(Size <= 0 || Size > MAX_CHUNK_SIZE)
383 {
384 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': invalid chunk header size", m_aFilename);
385 return false;
386 }
387
388 if(io_read(io: m_File, buffer: m_aBuffer, size: Size) != (unsigned)Size)
389 {
390 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': error reading chunk data", m_aFilename);
391 return false;
392 }
393
394 Size = CNetBase::Decompress(pData: m_aBuffer, DataSize: Size, pOutput: m_aBufferTemp, OutputSize: sizeof(m_aBufferTemp));
395 if(Size < 0)
396 {
397 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': error during network decompression", m_aFilename);
398 return false;
399 }
400
401 Size = CVariableInt::Decompress(pSrc: m_aBufferTemp, SrcSize: Size, pDst: m_aBuffer, DstSize: sizeof(m_aBuffer));
402 if(Size < 0)
403 {
404 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': error during intpack decompression", m_aFilename);
405 return false;
406 }
407
408 m_pBufferEnd = m_aBuffer + Size;
409 return true;
410}
411
412bool CGhostLoader::ReadNextType(int *pType)
413{
414 dbg_assert((bool)m_File, "File not open");
415
416 if(m_BufferCurItem != m_BufferPrevItem && m_BufferCurItem < m_BufferNumItems)
417 {
418 *pType = m_LastItem.m_Type;
419 }
420 else if(!ReadChunk(pType))
421 {
422 return false; // error or EOF
423 }
424
425 m_BufferPrevItem = m_BufferCurItem;
426 return true;
427}
428
429static void UndiffItem(const uint32_t *pPast, const uint32_t *pDiff, uint32_t *pOut, size_t Size)
430{
431 while(Size)
432 {
433 *pOut = *pPast + *pDiff;
434 pOut++;
435 pPast++;
436 pDiff++;
437 Size--;
438 }
439}
440
441bool CGhostLoader::ReadData(int Type, void *pData, size_t Size)
442{
443 dbg_assert((bool)m_File, "File not open");
444 dbg_assert(Type >= 0 && Type <= (int)std::numeric_limits<unsigned char>::max(), "Type invalid");
445 dbg_assert(Size > 0 && Size <= MAX_ITEM_SIZE && Size % sizeof(uint32_t) == 0, "Size invalid");
446
447 if((size_t)(m_pBufferEnd - m_pBufferPos) < Size)
448 {
449 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': not enough data (type='%d', got='%" PRIzu "', wanted='%" PRIzu "')", m_aFilename, Type, (size_t)(m_pBufferEnd - m_pBufferPos), Size);
450 return false;
451 }
452
453 CGhostItem Data(Type);
454 if(m_LastItem.m_Type == Data.m_Type)
455 {
456 UndiffItem(pPast: (const uint32_t *)m_LastItem.m_aData, pDiff: (const uint32_t *)m_pBufferPos, pOut: (uint32_t *)Data.m_aData, Size: Size / sizeof(uint32_t));
457 }
458 else
459 {
460 mem_copy(dest: Data.m_aData, source: m_pBufferPos, size: Size);
461 }
462
463 mem_copy(dest: pData, source: Data.m_aData, size: Size);
464
465 m_LastItem = Data;
466 m_pBufferPos += Size;
467 m_BufferCurItem++;
468 return true;
469}
470
471void CGhostLoader::Close()
472{
473 if(!m_File)
474 {
475 return;
476 }
477
478 io_close(io: m_File);
479 m_File = nullptr;
480 m_aFilename[0] = '\0';
481}
482
483bool CGhostLoader::GetGhostInfo(const char *pFilename, CGhostInfo *pGhostInfo, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc)
484{
485 CGhostHeader Header;
486 IOHANDLE File = ReadHeader(Header, pFilename, pMap, MapSha256, MapCrc, LogMapMismatch: false);
487 if(!File)
488 {
489 return false;
490 }
491 io_close(io: File);
492 *pGhostInfo = Header.ToGhostInfo();
493 return true;
494}
495