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