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 | |
11 | static const unsigned char [8] = {'T', 'W', 'G', 'H', 'O', 'S', 'T', 0}; |
12 | static const unsigned char gs_CurVersion = 6; |
13 | static const int gs_NumTicksOffset = 93; |
14 | |
15 | static const ColorRGBA gs_GhostPrintColor{0.65f, 0.6f, 0.6f, 1.0f}; |
16 | |
17 | CGhostRecorder::CGhostRecorder() |
18 | { |
19 | m_File = 0; |
20 | ResetBuffer(); |
21 | } |
22 | |
23 | void CGhostRecorder::Init() |
24 | { |
25 | m_pConsole = Kernel()->RequestInterface<IConsole>(); |
26 | m_pStorage = Kernel()->RequestInterface<IStorage>(); |
27 | } |
28 | |
29 | // Record |
30 | int 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 ; |
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 | |
60 | void CGhostRecorder::ResetBuffer() |
61 | { |
62 | m_pBufferPos = m_aBuffer; |
63 | m_BufferNumItems = 0; |
64 | } |
65 | |
66 | static 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 | |
78 | void 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 | |
101 | void 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 | |
136 | int 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 | |
161 | CGhostLoader::CGhostLoader() |
162 | { |
163 | m_File = 0; |
164 | ResetBuffer(); |
165 | } |
166 | |
167 | void CGhostLoader::Init() |
168 | { |
169 | m_pConsole = Kernel()->RequestInterface<IConsole>(); |
170 | m_pStorage = Kernel()->RequestInterface<IStorage>(); |
171 | } |
172 | |
173 | void CGhostLoader::ResetBuffer() |
174 | { |
175 | m_pBufferPos = m_aBuffer; |
176 | m_BufferNumItems = 0; |
177 | m_BufferCurItem = 0; |
178 | m_BufferPrevItem = -1; |
179 | } |
180 | |
181 | int 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 | |
263 | int 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 | |
306 | bool 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 | |
326 | static 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 | |
338 | bool 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 | |
358 | void CGhostLoader::Close() |
359 | { |
360 | if(!m_File) |
361 | return; |
362 | io_close(io: m_File); |
363 | m_File = 0; |
364 | } |
365 | |
366 | bool CGhostLoader::GetGhostInfo(const char *pFilename, CGhostInfo *pGhostInfo, const char *pMap, SHA256_DIGEST MapSha256, unsigned MapCrc) |
367 | { |
368 | CGhostHeader ; |
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 | |