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 = std::nullopt;
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;
112 mem_copy(dest: Data.m_aData, source: pData, size: Size);
113 Data.m_Size = Size;
114 Data.m_Type = Type;
115 if(m_LastItem.has_value() && m_LastItem.value().m_Type == Data.m_Type)
116 {
117 dbg_assert(m_LastItem.value().m_Size == Data.m_Size, "Size mismatch between current and last item");
118 DiffItem(pPast: (const uint32_t *)m_LastItem.value().m_aData, pCurrent: (const uint32_t *)Data.m_aData, pOut: (uint32_t *)m_pBufferPos, Size: Data.m_Size / sizeof(uint32_t));
119 }
120 else
121 {
122 FlushChunk();
123 mem_copy(dest: m_pBufferPos, source: Data.m_aData, size: Data.m_Size);
124 }
125
126 m_LastItem = Data;
127 m_pBufferPos += Data.m_Size;
128 m_BufferNumItems++;
129 if(m_BufferNumItems >= NUM_ITEMS_PER_CHUNK)
130 {
131 FlushChunk();
132 }
133}
134
135void CGhostRecorder::FlushChunk()
136{
137 dbg_assert((bool)m_File, "File not open");
138
139 int Size = m_pBufferPos - m_aBuffer;
140 if(Size == 0 || m_BufferNumItems == 0)
141 {
142 return;
143 }
144 dbg_assert(Size % sizeof(uint32_t) == 0, "Chunk size invalid");
145 dbg_assert(m_LastItem.has_value(), "Tried to flush chunk without last item");
146
147 Size = CVariableInt::Compress(pSrc: m_aBuffer, SrcSize: Size, pDst: m_aBufferTemp, DstSize: sizeof(m_aBufferTemp));
148 if(Size < 0)
149 {
150 log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Failed to write chunk to '%s': error during intpack compression", m_aFilename);
151 m_LastItem = std::nullopt;
152 ResetBuffer();
153 return;
154 }
155
156 Size = CNetBase::Compress(pData: m_aBufferTemp, DataSize: Size, pOutput: m_aBuffer, OutputSize: sizeof(m_aBuffer));
157 if(Size < 0)
158 {
159 log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Failed to write chunk to '%s': error during network compression", m_aFilename);
160 m_LastItem = std::nullopt;
161 ResetBuffer();
162 return;
163 }
164
165 unsigned char aChunkHeader[4];
166 aChunkHeader[0] = m_LastItem.value().m_Type & 0xff;
167 aChunkHeader[1] = m_BufferNumItems & 0xff;
168 aChunkHeader[2] = (Size >> 8) & 0xff;
169 aChunkHeader[3] = Size & 0xff;
170
171 io_write(io: m_File, buffer: aChunkHeader, size: sizeof(aChunkHeader));
172 io_write(io: m_File, buffer: m_aBuffer, size: Size);
173
174 m_LastItem = std::nullopt;
175 ResetBuffer();
176}
177
178void CGhostRecorder::Stop(int Ticks, int Time)
179{
180 if(!m_File)
181 {
182 return;
183 }
184
185 const bool DiscardFile = Ticks <= 0 || Time <= 0;
186
187 if(!DiscardFile)
188 {
189 FlushChunk();
190
191 // write number of ticks and time
192 io_seek(io: m_File, offsetof(CGhostHeader, m_aNumTicks), origin: IOSEEK_START);
193
194 unsigned char aNumTicks[sizeof(int32_t)];
195 uint_to_bytes_be(bytes: aNumTicks, value: Ticks);
196 io_write(io: m_File, buffer: aNumTicks, size: sizeof(aNumTicks));
197
198 unsigned char aTime[sizeof(int32_t)];
199 uint_to_bytes_be(bytes: aTime, value: Time);
200 io_write(io: m_File, buffer: aTime, size: sizeof(aTime));
201 }
202
203 io_close(io: m_File);
204 m_File = nullptr;
205
206 if(DiscardFile)
207 {
208 m_pStorage->RemoveFile(pFilename: m_aFilename, Type: IStorage::TYPE_SAVE);
209 }
210
211 log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Stopped recording to '%s'", m_aFilename);
212 m_aFilename[0] = '\0';
213}
214
215CGhostLoader::CGhostLoader()
216{
217 m_File = nullptr;
218 m_aFilename[0] = '\0';
219 ResetBuffer();
220}
221
222void CGhostLoader::Init()
223{
224 m_pStorage = Kernel()->RequestInterface<IStorage>();
225}
226
227void CGhostLoader::ResetBuffer()
228{
229 m_pBufferPos = m_aBuffer;
230 m_pBufferEnd = m_aBuffer;
231 m_BufferNumItems = 0;
232 m_BufferCurItem = 0;
233 m_BufferPrevItem = -1;
234}
235
236IOHANDLE CGhostLoader::ReadHeader(CGhostHeader &Header, const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc, bool LogMapMismatch) const
237{
238 IOHANDLE File = m_pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: IStorage::TYPE_SAVE);
239 if(!File)
240 {
241 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to open ghost file '%s' for reading", pFilename);
242 return nullptr;
243 }
244
245 if(io_read(io: File, buffer: &Header, size: sizeof(Header) - sizeof(SHA256_DIGEST)) != sizeof(Header) - sizeof(SHA256_DIGEST))
246 {
247 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': failed to read header", pFilename);
248 io_close(io: File);
249 return nullptr;
250 }
251
252 if(!ValidateHeader(Header, pFilename))
253 {
254 io_close(io: File);
255 return nullptr;
256 }
257
258 if(Header.m_Version < 6)
259 {
260 mem_zero(block: &Header.m_MapSha256, size: sizeof(Header.m_MapSha256));
261 }
262 else if(io_read(io: File, buffer: &Header.m_MapSha256, size: sizeof(SHA256_DIGEST)) != sizeof(SHA256_DIGEST))
263 {
264 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': failed to read header map SHA256", pFilename);
265 io_close(io: File);
266 return nullptr;
267 }
268
269 if(!CheckHeaderMap(Header, pFilename, pMap, MapSha256, MapCrc, LogMapMismatch))
270 {
271 io_close(io: File);
272 return nullptr;
273 }
274
275 return File;
276}
277
278bool CGhostLoader::ValidateHeader(const CGhostHeader &Header, const char *pFilename) const
279{
280 if(mem_comp(a: Header.m_aMarker, b: gs_aHeaderMarker, size: sizeof(gs_aHeaderMarker)) != 0)
281 {
282 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': invalid header marker", pFilename);
283 return false;
284 }
285
286 if(Header.m_Version < 4 || Header.m_Version > gs_CurVersion)
287 {
288 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': ghost version '%d' is not supported", pFilename, Header.m_Version);
289 return false;
290 }
291
292 if(!mem_has_null(block: Header.m_aOwner, size: sizeof(Header.m_aOwner)) || !str_utf8_check(str: Header.m_aOwner))
293 {
294 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': owner name is invalid", pFilename);
295 return false;
296 }
297
298 if(!mem_has_null(block: Header.m_aMap, size: sizeof(Header.m_aMap)) || !str_utf8_check(str: Header.m_aMap))
299 {
300 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': map name is invalid", pFilename);
301 return false;
302 }
303
304 const int NumTicks = Header.GetTicks();
305 if(NumTicks <= 0)
306 {
307 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': number of ticks '%d' is invalid", pFilename, NumTicks);
308 return false;
309 }
310
311 const int Time = Header.GetTime();
312 if(Time <= 0)
313 {
314 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': time '%d' is invalid", pFilename, Time);
315 return false;
316 }
317
318 return true;
319}
320
321bool CGhostLoader::CheckHeaderMap(const CGhostHeader &Header, const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc, bool LogMapMismatch) const
322{
323 if(str_comp(a: Header.m_aMap, b: pMap) != 0)
324 {
325 if(LogMapMismatch)
326 {
327 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);
328 }
329 return false;
330 }
331
332 if(Header.m_Version >= 6)
333 {
334 if(Header.m_MapSha256 != MapSha256 && g_Config.m_ClRaceGhostStrictMap)
335 {
336 if(LogMapMismatch)
337 {
338 char aGhostSha256[SHA256_MAXSTRSIZE];
339 sha256_str(digest: Header.m_MapSha256, str: aGhostSha256, max_len: sizeof(aGhostSha256));
340 char aMapSha256[SHA256_MAXSTRSIZE];
341 sha256_str(digest: MapSha256, str: aMapSha256, max_len: sizeof(aMapSha256));
342 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);
343 }
344 return false;
345 }
346 }
347 else
348 {
349 const unsigned GhostMapCrc = bytes_be_to_uint(bytes: Header.m_aZeroes);
350 if(GhostMapCrc != MapCrc && g_Config.m_ClRaceGhostStrictMap)
351 {
352 if(LogMapMismatch)
353 {
354 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);
355 }
356 return false;
357 }
358 }
359
360 return true;
361}
362
363bool CGhostLoader::Load(const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc)
364{
365 dbg_assert(!m_File, "File already open");
366
367 CGhostHeader Header;
368 IOHANDLE File = ReadHeader(Header, pFilename, pMap, MapSha256, MapCrc, LogMapMismatch: true);
369 if(!File)
370 {
371 return false;
372 }
373
374 m_File = File;
375 str_copy(dst&: m_aFilename, src: pFilename);
376 m_Header = Header;
377 m_Info = m_Header.ToGhostInfo();
378 m_LastItem = std::nullopt;
379 ResetBuffer();
380 return true;
381}
382
383bool CGhostLoader::ReadChunk(int *pType)
384{
385 if(m_Header.m_Version != 4)
386 {
387 m_LastItem = std::nullopt;
388 }
389 ResetBuffer();
390
391 unsigned char aChunkHeader[4];
392 const unsigned ReadHeaderSize = io_read(io: m_File, buffer: aChunkHeader, size: sizeof(aChunkHeader));
393 if(ReadHeaderSize != sizeof(aChunkHeader))
394 {
395 if(ReadHeaderSize != 0)
396 {
397 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': chunk header truncated", m_aFilename);
398 }
399 return false; // EOF
400 }
401
402 *pType = aChunkHeader[0];
403 int Size = (aChunkHeader[2] << 8) | aChunkHeader[3];
404 m_BufferNumItems = aChunkHeader[1];
405
406 if(Size <= 0 || Size > MAX_CHUNK_SIZE)
407 {
408 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': invalid chunk header size", m_aFilename);
409 return false;
410 }
411
412 if(io_read(io: m_File, buffer: m_aBuffer, size: Size) != (unsigned)Size)
413 {
414 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': error reading chunk data", m_aFilename);
415 return false;
416 }
417
418 Size = CNetBase::Decompress(pData: m_aBuffer, DataSize: Size, pOutput: m_aBufferTemp, OutputSize: sizeof(m_aBufferTemp));
419 if(Size < 0)
420 {
421 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': error during network decompression", m_aFilename);
422 return false;
423 }
424
425 Size = CVariableInt::Decompress(pSrc: m_aBufferTemp, SrcSize: Size, pDst: m_aBuffer, DstSize: sizeof(m_aBuffer));
426 if(Size < 0)
427 {
428 log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': error during intpack decompression", m_aFilename);
429 return false;
430 }
431
432 m_pBufferEnd = m_aBuffer + Size;
433 return true;
434}
435
436bool CGhostLoader::ReadNextType(int *pType)
437{
438 dbg_assert((bool)m_File, "File not open");
439
440 if(m_BufferCurItem != m_BufferPrevItem && m_BufferCurItem < m_BufferNumItems)
441 {
442 dbg_assert(m_LastItem.has_value(), "Buffer contains items but last item is unset");
443 *pType = m_LastItem.value().m_Type;
444 }
445 else if(!ReadChunk(pType))
446 {
447 return false; // error or EOF
448 }
449
450 m_BufferPrevItem = m_BufferCurItem;
451 return true;
452}
453
454static void UndiffItem(const uint32_t *pPast, const uint32_t *pDiff, uint32_t *pOut, size_t Size)
455{
456 while(Size)
457 {
458 *pOut = *pPast + *pDiff;
459 pOut++;
460 pPast++;
461 pDiff++;
462 Size--;
463 }
464}
465
466bool CGhostLoader::ReadData(int Type, void *pData, size_t Size)
467{
468 dbg_assert((bool)m_File, "File not open");
469 dbg_assert(Type >= 0 && Type <= (int)std::numeric_limits<unsigned char>::max(), "Type invalid");
470 dbg_assert(Size > 0 && Size <= MAX_ITEM_SIZE && Size % sizeof(uint32_t) == 0, "Size invalid");
471
472 if((size_t)(m_pBufferEnd - m_pBufferPos) < Size)
473 {
474 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);
475 return false;
476 }
477
478 CGhostItem Data;
479 Data.m_Size = Size;
480 Data.m_Type = Type;
481 if(m_LastItem.has_value())
482 {
483 dbg_assert(m_LastItem.value().m_Type == Data.m_Type, "Type mismatch between current and last item");
484 dbg_assert(m_LastItem.value().m_Size == Data.m_Size, "Size mismatch between current and last item");
485 UndiffItem(pPast: (const uint32_t *)m_LastItem.value().m_aData, pDiff: (const uint32_t *)m_pBufferPos, pOut: (uint32_t *)Data.m_aData, Size: Data.m_Size / sizeof(uint32_t));
486 }
487 else
488 {
489 mem_copy(dest: Data.m_aData, source: m_pBufferPos, size: Data.m_Size);
490 }
491
492 mem_copy(dest: pData, source: Data.m_aData, size: Data.m_Size);
493
494 m_LastItem = Data;
495 m_pBufferPos += Data.m_Size;
496 m_BufferCurItem++;
497 return true;
498}
499
500void CGhostLoader::Close()
501{
502 if(!m_File)
503 {
504 return;
505 }
506
507 io_close(io: m_File);
508 m_File = nullptr;
509 m_aFilename[0] = '\0';
510}
511
512bool CGhostLoader::GetGhostInfo(const char *pFilename, CGhostInfo *pGhostInfo, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc)
513{
514 CGhostHeader Header;
515 IOHANDLE File = ReadHeader(Header, pFilename, pMap, MapSha256, MapCrc, LogMapMismatch: false);
516 if(!File)
517 {
518 return false;
519 }
520 io_close(io: File);
521 *pGhostInfo = Header.ToGhostInfo();
522 return true;
523}
524