1 /*
2 * OpenClonk, http://www.openclonk.org
3 *
4 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
5 * Copyright (c) 2009-2016, The OpenClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16 // scenario record functionality
17
18 #include "C4Include.h"
19 #include "control/C4Record.h"
20
21 #include "control/C4GameControl.h"
22 #include "control/C4GameSave.h"
23 #include "control/C4PlayerInfo.h"
24 #include "editor/C4Console.h"
25 #include "player/C4Player.h"
26
27 #define IMMEDIATEREC
28
29 CStdFile DbgRecFile;
30 int DoNoDebugRec=0; // debugrec disable counter
31
AddDbgRec(C4RecordChunkType eType,const void * pData,int iSize)32 void AddDbgRec(C4RecordChunkType eType, const void *pData, int iSize)
33 {
34 ::Control.DbgRec(eType, (const uint8_t *) pData, iSize);
35 }
36
C4DebugRecOff()37 C4DebugRecOff::C4DebugRecOff()
38 {
39 DEBUGREC_OFF;
40 }
41
C4DebugRecOff(bool fDoOff)42 C4DebugRecOff::C4DebugRecOff(bool fDoOff) : fDoOff(fDoOff)
43 {
44 if (fDoOff) { DEBUGREC_OFF; }
45 }
46
~C4DebugRecOff()47 C4DebugRecOff::~C4DebugRecOff()
48 {
49 if (fDoOff) { DEBUGREC_ON; }
50 }
51
Clear()52 void C4DebugRecOff::Clear()
53 {
54 DEBUGREC_ON;
55 fDoOff = false;
56 }
57
CompileFunc(StdCompiler * pComp)58 void C4PktDebugRec::CompileFunc(StdCompiler *pComp)
59 {
60 // type
61 pComp->Value(mkNamingAdapt(mkIntAdapt(eType), "Type"));
62 // Packet data
63 C4PktBuf::CompileFunc(pComp);
64 }
65
C4RecordChunk()66 C4RecordChunk::C4RecordChunk()
67 : pCtrl(nullptr)
68 {
69
70 }
71
Delete()72 void C4RecordChunk::Delete()
73 {
74 switch (Type)
75 {
76 case RCT_Ctrl: delete pCtrl; pCtrl = nullptr; break;
77 case RCT_CtrlPkt: delete pPkt; pPkt = nullptr; break;
78 case RCT_End: break;
79 case RCT_Frame: break;
80 case RCT_File: delete pFileData; break;
81 default: delete pDbg; pDbg = nullptr; break;
82 }
83 }
84
CompileFunc(StdCompiler * pComp)85 void C4RecordChunk::CompileFunc(StdCompiler *pComp)
86 {
87 pComp->Value(mkNamingAdapt(Frame, "Frame"));
88 pComp->Value(mkNamingAdapt(mkIntAdapt(Type), "Type"));
89 switch (Type)
90 {
91 case RCT_Ctrl: pComp->Value(mkPtrAdaptNoNull(pCtrl)); break;
92 case RCT_CtrlPkt: pComp->Value(mkPtrAdaptNoNull(pPkt)); break;
93 case RCT_End: break;
94 case RCT_Frame: break;
95 case RCT_File: pComp->Value(Filename); pComp->Value(mkPtrAdaptNoNull(pFileData)); break;
96 default: pComp->Value(mkPtrAdaptNoNull(pDbg)); break;
97 }
98 }
99
100 C4Record::C4Record() = default;
101
102 C4Record::~C4Record() = default;
103
Start(bool fInitial)104 bool C4Record::Start(bool fInitial)
105 {
106 // no double record
107 if (fRecording) return false;
108
109 // create demos folder
110 if (!Config.General.CreateSaveFolder(Config.AtUserDataPath(C4CFN_Records), LoadResStr("IDS_GAME_RECORDSTITLE")))
111 return false;
112
113 // various infos
114 StdStrBuf sDemoFolder(C4CFN_Records);
115 char sScenName[_MAX_FNAME+ 1]; SCopy(GetFilenameOnly(Game.Parameters.Scenario.getFile()), sScenName, _MAX_FNAME);
116
117 // remove trailing numbers from scenario name (e.g. from savegames) - could we perhaps use C4S.Head.Origin instead...?
118 char *pScenNameEnd = sScenName + SLen(sScenName);
119 while (Inside<char>(*--pScenNameEnd, '0', '9'))
120 if (pScenNameEnd == sScenName)
121 break;
122 pScenNameEnd[1] = 0;
123
124 // determine index (by total number of records)
125 Index = 1;
126 for (DirectoryIterator i(Config.AtUserDataPath(C4CFN_Records)); *i; ++i)
127 if (WildcardMatch(C4CFN_ScenarioFiles, *i))
128 Index++;
129
130 // compose record filename
131 sFilename.Format("%s" DirSep "%03i-%s.ocs", Config.AtUserDataPath(sDemoFolder.getData()), Index, sScenName);
132
133 // log
134 StdStrBuf sLog; sLog.Format(LoadResStr("IDS_PRC_RECORDINGTO"),sFilename.getData());
135 if (Game.FrameCounter) sLog.AppendFormat(" (Frame %d)", Game.FrameCounter);
136 Log(sLog.getData());
137
138 // save game - this also saves player info list
139 C4GameSaveRecord saveRec(fInitial, Index, Game.Parameters.isLeague());
140 if (!saveRec.Save(sFilename.getData())) return false;
141 saveRec.Close();
142
143 // unpack group, if neccessary
144 if ( !DirectoryExists(sFilename.getData()) &&
145 !C4Group_UnpackDirectory(sFilename.getData()) )
146 return false;
147
148 // open control record file
149 char szCtrlRecFilename[_MAX_PATH+1 + _MAX_FNAME];
150 sprintf(szCtrlRecFilename, "%s" DirSep C4CFN_CtrlRec, sFilename.getData());
151 if (!CtrlRec.Create(szCtrlRecFilename)) return false;
152
153 // open log file in record
154 char szLogRecFilename[_MAX_PATH+1 + _MAX_FNAME];
155 sprintf(szLogRecFilename, "%s" DirSep C4CFN_LogRec, sFilename.getData());
156 if (!LogRec.Create(szLogRecFilename)) return false;
157
158 // open record group
159 if (!RecordGrp.Open(sFilename.getData()))
160 return false;
161
162 // record go
163 fStreaming = false;
164 fRecording = true;
165 iLastFrame = 0;
166 return true;
167 }
168
Stop(StdStrBuf * pRecordName,BYTE * pRecordSHA1)169 bool C4Record::Stop(StdStrBuf *pRecordName, BYTE *pRecordSHA1)
170 {
171 // safety
172 if (!fRecording) return false;
173 if (!DirectoryExists(sFilename.getData())) return false;
174
175 // streaming finished
176 StopStreaming();
177
178 // save desc into record group
179 C4GameSaveRecord saveRec(false, Index, Game.Parameters.isLeague());
180 saveRec.SaveDesc(RecordGrp);
181
182 // save end player infos into record group
183 Game.PlayerInfos.Save(RecordGrp, C4CFN_RecPlayerInfos);
184 RecordGrp.Close();
185
186 // write last entry and close
187 C4RecordChunkHead Head;
188 Head.iFrm = 37;
189 Head.Type = RCT_End;
190 CtrlRec.Write(&Head, sizeof(Head));
191 CtrlRec.Close();
192
193 LogRec.Close();
194
195 // pack group
196 if (!Config.General.DebugRec)
197 if (!C4Group_PackDirectory(sFilename.getData())) return false;
198
199 // return record data
200 if (pRecordName)
201 pRecordName->Copy(sFilename);
202 if (pRecordSHA1)
203 if (!GetFileSHA1(sFilename.getData(), pRecordSHA1))
204 return false;
205
206 // ok
207 fRecording = false;
208 return true;
209 }
210
Rec(const C4Control & Ctrl,int iFrame)211 bool C4Record::Rec(const C4Control &Ctrl, int iFrame)
212 {
213 if (!fRecording) return false;
214 // don't record empty control
215 if (!Ctrl.firstPkt()) return true;
216 // create copy
217 C4Control Cpy; Cpy.Copy(Ctrl);
218 // prepare it for record
219 Cpy.PreRec(this);
220 // record it
221 return Rec(iFrame, DecompileToBuf<StdCompilerBinWrite>(Cpy), RCT_Ctrl);
222 }
223
Rec(C4PacketType eCtrlType,C4ControlPacket * pCtrl,int iFrame)224 bool C4Record::Rec(C4PacketType eCtrlType, C4ControlPacket *pCtrl, int iFrame)
225 {
226 if (!fRecording) return false;
227 // create copy
228 C4IDPacket Pkt = C4IDPacket(eCtrlType, pCtrl, false); if (!Pkt.getPkt()) return false;
229 C4ControlPacket *pCtrlCpy = static_cast<C4ControlPacket *>(Pkt.getPkt());
230 // prepare for recording
231 pCtrlCpy->PreRec(this);
232 // record it
233 return Rec(iFrame, DecompileToBuf<StdCompilerBinWrite>(Pkt), RCT_CtrlPkt);
234 }
235
Rec(int iFrame,const StdBuf & sBuf,C4RecordChunkType eType)236 bool C4Record::Rec(int iFrame, const StdBuf &sBuf, C4RecordChunkType eType)
237 {
238 // filler chunks (this should never be necessary, though)
239 while (iFrame > int(iLastFrame + 0xff))
240 Rec(iLastFrame + 0xff, StdBuf(), RCT_Frame);
241 // get frame difference
242 uint8_t iFrameDiff = std::max<uint8_t>(0, iFrame - iLastFrame);
243 iLastFrame += iFrameDiff;
244 // create head
245 C4RecordChunkHead Head = { iFrameDiff, uint8_t(eType) };
246 // pack
247 CtrlRec.Write(&Head, sizeof(Head));
248 CtrlRec.Write(sBuf.getData(), sBuf.getSize());
249 #ifdef IMMEDIATEREC
250 // immediate rec: always flush
251 CtrlRec.Flush();
252 #endif
253 // Stream
254 if (fStreaming)
255 Stream(Head, sBuf);
256 return true;
257 }
258
Stream(const C4RecordChunkHead & Head,const StdBuf & sBuf)259 void C4Record::Stream(const C4RecordChunkHead &Head, const StdBuf &sBuf)
260 {
261 if (!fStreaming) return;
262 StreamingData.Append(&Head, sizeof(Head));
263 StreamingData.Append(sBuf.getData(), sBuf.getSize());
264 }
265
AddFile(const char * szLocalFilename,const char * szAddAs,bool fDelete)266 bool C4Record::AddFile(const char *szLocalFilename, const char *szAddAs, bool fDelete)
267 {
268 if (!fRecording) return false;
269
270 // Streaming?
271 if (fStreaming)
272 {
273
274 // Special stripping for streaming
275 StdCopyStrBuf szFile(szLocalFilename);
276 if (SEqualNoCase(GetExtension(szAddAs), "ocp"))
277 {
278 // Create a copy
279 MakeTempFilename(&szFile);
280 if (!CopyItem(szLocalFilename, szFile.getData()))
281 return false;
282 // Strip it
283 if (!C4Player::Strip(szFile.getData(), true))
284 return false;
285 }
286
287 // Add to stream
288 if (!StreamFile(szFile.getData(), szAddAs))
289 return false;
290
291 // Remove temporary file
292 if (szFile != szLocalFilename)
293 EraseItem(szFile.getData());
294 }
295
296 // Add to record group
297 if (fDelete)
298 {
299 if (!RecordGrp.Move(szLocalFilename, szAddAs))
300 return false;
301 }
302 else
303 {
304 if (!RecordGrp.Add(szLocalFilename, szAddAs))
305 return false;
306 }
307
308 return true;
309 }
310
StartStreaming(bool fInitial)311 bool C4Record::StartStreaming(bool fInitial)
312 {
313 if (!fRecording) return false;
314 if (fStreaming) return false;
315
316 // Get temporary file name
317 StdCopyStrBuf sTempFilename(sFilename);
318 MakeTempFilename(&sTempFilename);
319
320 // Save start state (without copy of scenario!)
321 C4GameSaveRecord saveRec(fInitial, Index, Game.Parameters.isLeague(), false);
322 if (!saveRec.Save(sTempFilename.getData())) return false;
323 saveRec.Close();
324
325 // Add file into stream, delete file
326 fStreaming = true;
327 if (!StreamFile(sTempFilename.getData(), sFilename.getData()))
328 {
329 fStreaming = false;
330 return false;
331 }
332
333 // Okay
334 EraseFile(sTempFilename.getData());
335 iStreamingPos = 0;
336 return true;
337 }
338
ClearStreamingBuf(unsigned int iAmount)339 void C4Record::ClearStreamingBuf(unsigned int iAmount)
340 {
341 iStreamingPos += iAmount;
342 if (iAmount == StreamingData.getSize())
343 StreamingData.Clear();
344 else
345 {
346 StreamingData.Move(iAmount, StreamingData.getSize() - iAmount);
347 StreamingData.SetSize(StreamingData.getSize() - iAmount);
348 }
349 }
350
StopStreaming()351 void C4Record::StopStreaming()
352 {
353 fStreaming = false;
354 }
355
StreamFile(const char * szLocalFilename,const char * szAddAs)356 bool C4Record::StreamFile(const char *szLocalFilename, const char *szAddAs)
357 {
358
359 // Load file into memory
360 StdBuf FileData;
361 if (!FileData.LoadFromFile(szLocalFilename))
362 return false;
363
364 // Prepend name
365 StdBuf Packed = DecompileToBuf<StdCompilerBinWrite>(
366 mkInsertAdapt(StdStrBuf(szAddAs), FileData, false));
367
368 // Add to stream
369 C4RecordChunkHead Head = { 0, RCT_File };
370 Stream(Head, Packed);
371 return true;
372 }
373
374 // set defaults
375 C4Playback::C4Playback() = default;
376
~C4Playback()377 C4Playback::~C4Playback()
378 {
379 Clear();
380 }
381
Open(C4Group & rGrp)382 bool C4Playback::Open(C4Group &rGrp)
383 {
384 // clean up
385 Clear();
386 iLastSequentialFrame = 0;
387 bool fStrip = false;
388
389 // open group? Then do some sequential reading for large files
390 // Can't do this when a dump is forced, because the dump needs all data
391 // Also can't do this when stripping is desired
392 fLoadSequential = !rGrp.IsPacked() && !Game.RecordDumpFile.getLength() && !fStrip;
393
394 // get text record file
395 StdStrBuf TextBuf;
396 if (rGrp.LoadEntryString(C4CFN_CtrlRecText, &TextBuf))
397 {
398 if (!ReadText(TextBuf))
399 return false;
400 }
401 else
402 {
403 // get record file
404 if (fLoadSequential)
405 {
406 if (!rGrp.FindEntry(C4CFN_CtrlRec)) return false;
407 if (!playbackFile.Open(FormatString("%s%c%s", rGrp.GetFullName().getData(), (char) DirectorySeparator, (const char *) C4CFN_CtrlRec).getData())) return false;
408 // forcing first chunk to be read; will call ReadBinary
409 currChunk = chunks.end();
410 if (!NextSequentialChunk())
411 {
412 // empty replay??!
413 LogFatal("Record: Binary read error.");
414 return false;
415 }
416 }
417 else
418 {
419 // non-sequential reading: Just read as a whole
420 StdBuf BinaryBuf;
421 if (rGrp.LoadEntry(C4CFN_CtrlRec, &BinaryBuf))
422 {
423 if (!ReadBinary(BinaryBuf))
424 return false;
425 }
426 else
427 {
428 // file too large? Try sequential loading and parsing
429 /* size_t iSize;
430 if (rGrp.AccessEntry(C4CFN_CtrlRec, &iSize))
431 {
432 CStdFile fOut; fOut.Create(Game.RecordDumpFile.getData());
433 fLoadSequential = true;
434 const size_t iChunkSize = 1024*1024*16; // 16M
435 while (iSize)
436 {
437 size_t iLoadSize = std::min<size_t>(iChunkSize, iSize);
438 BinaryBuf.SetSize(iLoadSize);
439 if (!rGrp.Read(BinaryBuf.getMData(), iLoadSize))
440 {
441 LogFatal("Record: Binary load error!");
442 return false;
443 }
444 iSize -= iLoadSize;
445 if (!ReadBinary(BinaryBuf)) return false;
446 LogF("%d binary remaining", iSize);
447 currChunk = chunks.begin();
448 if (fStrip) Strip();
449 StdStrBuf s(ReWriteText());
450 fOut.WriteString(s.getData());
451 LogF("Wrote %d text bytes (%d binary remaining)", s.getLength(), iSize);
452 chunks.clear();
453 }
454 fOut.Close();
455 fLoadSequential = false;
456 }
457 else*/
458 {
459 // no control data?
460 LogFatal("Record: No control data found!");
461 return false;
462 }
463 }
464 }
465 }
466 // rewrite record
467 if (fStrip) Strip();
468 if (Game.RecordDumpFile.getLength())
469 {
470 if (SEqualNoCase(GetExtension(Game.RecordDumpFile.getData()), "txt"))
471 ReWriteText().SaveToFile(Game.RecordDumpFile.getData());
472 else
473 ReWriteBinary().SaveToFile(Game.RecordDumpFile.getData());
474 }
475 // reset status
476 currChunk = chunks.begin();
477 Finished = false;
478 // external debugrec file
479 if (Config.General.DebugRecExternalFile[0] && Config.General.DebugRec)
480 {
481 if (Config.General.DebugRecWrite)
482 {
483 if (!DbgRecFile.Create(Config.General.DebugRecExternalFile))
484 {
485 LogFatal(FormatString(R"(DbgRec: Creation of external file "%s" failed!)", Config.General.DebugRecExternalFile).getData());
486 return false;
487 }
488 else LogF(R"(DbgRec: Writing to "%s"...)", Config.General.DebugRecExternalFile);
489 }
490 else
491 {
492 if (!DbgRecFile.Open(Config.General.DebugRecExternalFile))
493 {
494 LogFatal(FormatString(R"(DbgRec: Opening of external file "%s" failed!)", Config.General.DebugRecExternalFile).getData());
495 return false;
496 }
497 else LogF(R"(DbgRec: Checking against "%s"...)", Config.General.DebugRecExternalFile);
498 }
499 }
500 // ok
501 return true;
502 }
503
ReadBinary(const StdBuf & Buf)504 bool C4Playback::ReadBinary(const StdBuf &Buf)
505 {
506 // sequential reading: Take over rest from last buffer
507 const StdBuf *pUseBuf; uint32_t iFrame = 0;
508 if (fLoadSequential)
509 {
510 sequentialBuffer.Append(Buf);
511 pUseBuf = &sequentialBuffer;
512 iFrame = iLastSequentialFrame;
513 }
514 else
515 pUseBuf = &Buf;
516 // get buffer data
517 size_t iPos = 0; bool fFinished = false;
518 do
519 {
520 // unpack header
521 if (pUseBuf->getSize() - iPos < sizeof(C4RecordChunkHead)) break;
522 const C4RecordChunkHead *pHead = getBufPtr<C4RecordChunkHead>(*pUseBuf, iPos);
523 // get chunk
524 iPos += sizeof(C4RecordChunkHead);
525 StdBuf Chunk = pUseBuf->getPart(iPos, pUseBuf->getSize() - iPos);
526 // Create entry
527 C4RecordChunk c;
528 c.Frame = (iFrame + pHead->iFrm);
529 c.Type = pHead->Type;
530 // Unpack data
531 try
532 {
533 // Initialize compiler
534 StdCompilerBinRead Compiler;
535 Compiler.setInput(Chunk.getRef());
536 Compiler.Begin();
537 // Read chunk
538 switch (pHead->Type)
539 {
540 case RCT_Ctrl:
541 Compiler.Value(mkPtrAdaptNoNull(c.pCtrl));
542 break;
543 case RCT_CtrlPkt:
544 Compiler.Value(mkPtrAdaptNoNull(c.pPkt));
545 break;
546 case RCT_End:
547 fFinished = true;
548 break;
549 case RCT_File:
550 Compiler.Value(c.Filename);
551 Compiler.Value(mkPtrAdaptNoNull(c.pFileData));
552 break;
553 default:
554 // debugrec
555 if (pHead->Type >= 0x80)
556 Compiler.Value(mkPtrAdaptNoNull(c.pDbg));
557 }
558 // Advance over data
559 Compiler.End();
560 iPos += Compiler.getPosition();
561 }
562 catch (StdCompiler::EOFException *pEx)
563 {
564 // This is to be expected for sequential reading
565 if (fLoadSequential)
566 {
567 iPos -= sizeof(C4RecordChunkHead);
568 delete pEx;
569 break;
570 }
571 LogF("Record: Binary unpack error: %s", pEx->Msg.getData());
572 c.Delete();
573 delete pEx;
574 return false;
575 }
576 catch (StdCompiler::Exception *pEx)
577 {
578 LogF("Record: Binary unpack error: %s", pEx->Msg.getData());
579 c.Delete();
580 delete pEx;
581 return false;
582 }
583 // Add to list
584 chunks.push_back(c); c.pPkt = nullptr;
585 iFrame = c.Frame;
586 }
587 while (!fFinished);
588 // erase everything but the trailing part from sequential buffer
589 if (fLoadSequential)
590 {
591 if (iPos >= sequentialBuffer.getSize())
592 sequentialBuffer.Clear();
593 else if (iPos)
594 {
595 sequentialBuffer.Move(iPos, sequentialBuffer.getSize() - iPos);
596 sequentialBuffer.Shrink(iPos);
597 }
598 // remember frame
599 iLastSequentialFrame = iFrame;
600 }
601 return true;
602 }
603
ReadText(const StdStrBuf & Buf)604 bool C4Playback::ReadText(const StdStrBuf &Buf)
605 {
606 return CompileFromBuf_LogWarn<StdCompilerINIRead>(mkNamingAdapt(mkSTLContainerAdapt(chunks), "Rec"), Buf, C4CFN_CtrlRecText);
607 }
608
NextChunk()609 void C4Playback::NextChunk()
610 {
611 assert(currChunk != chunks.end());
612 ++currChunk;
613 if (currChunk != chunks.end()) return;
614 // end of all chunks if not loading sequential here
615 if (!fLoadSequential) return;
616 // otherwise, get next few chunks
617 for (auto & chunk : chunks) chunk.Delete();
618 chunks.clear(); currChunk = chunks.end();
619 NextSequentialChunk();
620 }
621
NextSequentialChunk()622 bool C4Playback::NextSequentialChunk()
623 {
624 StdBuf BinaryBuf; size_t iRealSize;
625 BinaryBuf.New(4096);
626 // load data until a chunk could be filled
627 for (;;)
628 {
629 iRealSize = 0;
630 playbackFile.Read(BinaryBuf.getMData(), 4096, &iRealSize);
631 if (!iRealSize) return false;
632 BinaryBuf.SetSize(iRealSize);
633 if (!ReadBinary(BinaryBuf)) return false;
634 // okay, at least one chunk has been read!
635 if (chunks.size())
636 {
637 currChunk = chunks.begin();
638 return true;
639 }
640 }
641 // playback file reading failed - looks like we're done
642 return false;
643 }
644
ReWriteText()645 StdStrBuf C4Playback::ReWriteText()
646 {
647 // Would work, too, but is currently too slow due to bad buffering inside StdCompilerINIWrite:
648 // return DecompileToBuf<StdCompilerINIWrite>(mkNamingAdapt(mkSTLContainerAdapt(chunks), "Rec"));
649 StdStrBuf Output;
650 for (chunks_t::const_iterator i = chunks.begin(); i != chunks.end(); i++)
651 {
652 Output.Append(static_cast<const StdStrBuf&>(DecompileToBuf<StdCompilerINIWrite>(mkNamingAdapt(mkDecompileAdapt(*i), "Rec"))));
653 Output.Append("\n\n");
654 }
655 return Output;
656 }
657
ReWriteBinary()658 StdBuf C4Playback::ReWriteBinary()
659 {
660 const int OUTPUT_GROW = 16 * 1024;
661 StdBuf Output; int iPos = 0;
662 bool fFinished = false;
663 int32_t iFrame = 0;
664 for (chunks_t::const_iterator i = chunks.begin(); !fFinished && i != chunks.end(); i++)
665 {
666 // Check frame difference
667 if (i->Frame - iFrame < 0 || i->Frame - iFrame > 0xff)
668 LogF("ERROR: Invalid frame difference between chunks (0-255 allowed)! Data will be invalid!");
669 // Pack data
670 StdBuf Chunk;
671 try
672 {
673 switch (i->Type)
674 {
675 case RCT_Ctrl:
676 Chunk = DecompileToBuf<StdCompilerBinWrite>(*i->pCtrl);
677 break;
678 case RCT_CtrlPkt:
679 Chunk = DecompileToBuf<StdCompilerBinWrite>(*i->pPkt);
680 break;
681 case RCT_End:
682 fFinished = true;
683 break;
684 default: // debugrec
685 if (i->pDbg)
686 Chunk = DecompileToBuf<StdCompilerBinWrite>(*i->pDbg);
687 break;
688 }
689 }
690 catch (StdCompiler::Exception *pEx)
691 {
692 LogF("Record: Binary unpack error: %s", pEx->Msg.getData());
693 delete pEx;
694 return StdBuf();
695 }
696 // Grow output
697 while (Output.getSize() - iPos < sizeof(C4RecordChunkHead) + Chunk.getSize())
698 Output.Grow(OUTPUT_GROW);
699 // Write header
700 C4RecordChunkHead *pHead = getMBufPtr<C4RecordChunkHead>(Output, iPos);
701 pHead->Type = i->Type;
702 pHead->iFrm = i->Frame - iFrame;
703 iPos += sizeof(C4RecordChunkHead);
704 iFrame = i->Frame;
705 // Write chunk
706 Output.Write(Chunk, iPos);
707 iPos += Chunk.getSize();
708 }
709 Output.SetSize(iPos);
710 return Output;
711 }
712
Strip()713 void C4Playback::Strip()
714 {
715 // Strip what?
716 const bool fStripPlayers = false;
717 const bool fStripSyncChecks = false;
718 const bool fStripDebugRec = true;
719 const bool fCheckCheat = false;
720 const bool fStripMessages = true;
721 const int32_t iEndFrame = -1;
722 // Iterate over chunk list
723 for (chunks_t::iterator i = chunks.begin(); i != chunks.end(); )
724 {
725 // Strip rest of record?
726 if (iEndFrame >= 0 && i->Frame > iEndFrame)
727 {
728 // Remove this and all remaining chunks
729 while (i != chunks.end())
730 {
731 i->Delete();
732 i = chunks.erase(i);
733 }
734 // Push new End-Chunk
735 C4RecordChunk EndChunk;
736 EndChunk.Frame = iEndFrame;
737 EndChunk.Type = RCT_End;
738 chunks.push_back(EndChunk);
739 // Done
740 break;
741 }
742 switch (i->Type)
743 {
744 case RCT_Ctrl:
745 {
746 // Iterate over controls
747 C4Control *pCtrl = i->pCtrl;
748 for (C4IDPacket *pPkt = pCtrl->firstPkt(), *pNext; pPkt; pPkt = pNext)
749 {
750 pNext = pCtrl->nextPkt(pPkt);
751 switch (pPkt->getPktType())
752 {
753 // Player join: Strip player file (if possible)
754 case CID_JoinPlr:
755 if (fStripPlayers)
756 {
757 C4ControlJoinPlayer *pJoinPlr = static_cast<C4ControlJoinPlayer *>(pPkt->getPkt());
758 pJoinPlr->Strip();
759 }
760 break;
761 // EM commands: May be cheats, so log them
762 case CID_Script:
763 case CID_EMMoveObj:
764 case CID_EMDrawTool:
765 case CID_ReInitScenario:
766 case CID_EditGraph:
767 if (fCheckCheat) Log(DecompileToBuf<StdCompilerINIWrite>(mkNamingAdapt(*pPkt, FormatString("Frame %d", i->Frame).getData())).getData());
768 break;
769 // Strip sync check
770 case CID_SyncCheck:
771 if (fStripSyncChecks)
772 {
773 i->pCtrl->Remove(pPkt);
774 }
775 break;
776 default:
777 // TODO
778 break;
779 }
780 }
781 // Strip empty control lists (always)
782 if (!pCtrl->firstPkt())
783 {
784 i->Delete();
785 i = chunks.erase(i);
786 }
787 else
788 i++;
789 }
790 break;
791 case RCT_CtrlPkt:
792 {
793 bool fStripThis=false;
794 switch (i->pPkt->getPktType())
795 {
796 // EM commands: May be cheats, so log them
797 case CID_Script:
798 case CID_EMMoveObj:
799 case CID_EMDrawTool:
800 case CID_ReInitScenario:
801 case CID_EditGraph:
802 if (fCheckCheat) Log(DecompileToBuf<StdCompilerINIWrite>(mkNamingAdapt(*i->pPkt, FormatString("Frame %d", i->Frame).getData())).getData());
803 break;
804 // Strip some stuff
805 case CID_SyncCheck:
806 if (fStripSyncChecks) fStripThis = true;
807 break;
808 case CID_Message:
809 if (fStripMessages) fStripThis=true;
810 break;
811 default:
812 // TODO
813 break;
814 }
815 if (fStripThis)
816 {
817 i->Delete();
818 i = chunks.erase(i);
819 }
820 else i++;
821 }
822 break;
823 case RCT_End:
824 i++;
825 break;
826 default:
827 // Strip debugrec
828 if (fStripDebugRec)
829 {
830 i->Delete();
831 i = chunks.erase(i);
832 }
833 else
834 i++;
835 }
836 }
837 }
838
839
ExecuteControl(C4Control * pCtrl,int iFrame)840 bool C4Playback::ExecuteControl(C4Control *pCtrl, int iFrame)
841 {
842 // still playbacking?
843 if (currChunk == chunks.end()) return false;
844 if (Finished) { Finish(); return false; }
845 if (Config.General.DebugRec)
846 {
847 if (DebugRec.firstPkt())
848 DebugRecError("Debug rec overflow!");
849 DebugRec.Clear();
850 }
851 // return all control until this frame
852 while (currChunk != chunks.end() && currChunk->Frame <= iFrame)
853 {
854 switch (currChunk->Type)
855 {
856 case RCT_Ctrl:
857 pCtrl->Append(*currChunk->pCtrl);
858 break;
859
860 case RCT_CtrlPkt:
861 {
862 C4IDPacket Packet(*currChunk->pPkt);
863 pCtrl->Add(Packet.getPktType(), static_cast<C4ControlPacket *>(Packet.getPkt()));
864 Packet.Default();
865 break;
866 }
867
868 case RCT_End:
869 // end of playback; stop it!
870 Finished=true;
871 break;
872
873 default: // expect it to be debug rec
874 if (Config.General.DebugRec)
875 {
876 // append to debug rec buffer
877 if (currChunk->pDbg)
878 {
879 DebugRec.Add(CID_DebugRec, currChunk->pDbg);
880 // the debugrec buffer is now responsible for deleting the packet
881 currChunk->pDbg = nullptr;
882 }
883 break;
884 }
885 }
886 // next chunk
887 NextChunk();
888 }
889 return true;
890 }
891
Finish()892 void C4Playback::Finish()
893 {
894 Clear();
895 // finished playback: end game
896 if (Console.Active)
897 {
898 ++Game.HaltCount;
899 Console.UpdateHaltCtrls(!!Game.HaltCount);
900 }
901 else
902 {
903 Game.DoGameOver();
904 }
905 // finish playback: enable controls
906 ::Control.ChangeToLocal();
907 }
908
Clear()909 void C4Playback::Clear()
910 {
911 // free stuff
912 for (auto & chunk : chunks) chunk.Delete();
913 chunks.clear(); currChunk = chunks.end();
914 playbackFile.Close();
915 sequentialBuffer.Clear();
916 fLoadSequential = false;
917 if (Config.General.DebugRec)
918 {
919 C4IDPacket *pkt;
920 while ((pkt = DebugRec.firstPkt())) DebugRec.Delete(pkt);
921 if (Config.General.DebugRecExternalFile[0])
922 DbgRecFile.Close();
923 }
924 // done
925 Finished = true;
926 }
927
GetRecordChunkTypeName(C4RecordChunkType eType)928 const char * GetRecordChunkTypeName(C4RecordChunkType eType)
929 {
930 switch (eType)
931 {
932 case RCT_Ctrl: return "Ctrl"; // control
933 case RCT_CtrlPkt: return "CtrlPkt"; // control packet
934 case RCT_Frame: return "Frame"; // beginning frame
935 case RCT_End: return "End"; // --- the end ---
936 case RCT_Log: return "Log"; // log message
937 case RCT_File: return "File"; // file data
938 // DEBUGREC
939 case RCT_Block: return "Block"; // point in Game::Execute
940 case RCT_SetPix: return "SetPix"; // set landscape pixel
941 case RCT_ExecObj: return "ExecObj"; // exec object
942 case RCT_Random: return "Random"; // Random()-call
943 case RCT_Rn3: return "Rn3"; // Rn3()-call
944 case RCT_MMC: return "MMC"; // create MassMover
945 case RCT_MMD: return "MMD"; // destroy MassMover
946 case RCT_CrObj: return "CrObj"; // create object
947 case RCT_DsObj: return "DsObj"; // remove object
948 case RCT_GetPix: return "GetPix"; // get landscape pixel; let the Gigas flow!
949 case RCT_RotVtx1: return "RotVtx1"; // before shape is rotated
950 case RCT_RotVtx2: return "RotVtx2"; // after shape is rotated
951 case RCT_ExecPXS: return "ExecPXS"; // execute pxs system
952 case RCT_Sin: return "Sin"; // sin by Shape-Rotation
953 case RCT_Cos: return "Cos"; // cos by Shape-Rotation
954 case RCT_Map: return "Map"; // map dump
955 case RCT_Ls: return "Ls"; // complete landscape dump!
956 case RCT_MCT1: return "MCT1"; // MapCreatorS2: before transformation
957 case RCT_MCT2: return "MCT2"; // MapCreatorS2: after transformation
958 case RCT_AulFunc: return "AulFunc"; // script function call
959 case RCT_ObjCom: return "ObjCom"; // object com
960 case RCT_PlrCom: return "PlrCom"; // player com
961 case RCT_PlrInCom: return "PlrInCom"; // player InCom
962 case RCT_MatScan: return "MatScan"; // landscape scan execute
963 case RCT_MatScanDo: return "MatScanDo"; // landscape scan mat change
964 case RCT_Area: return "Area"; // object area change
965 case RCT_MenuAdd: return "MenuAdd"; // add menu item
966 case RCT_MenuAddC: return "MenuAddC"; // add menu item: Following commands
967 case RCT_OCF: return "OCF"; // OCF setting of updating
968 case RCT_DirectExec: return "DirectExec"; // a DirectExec-script
969 case RCT_Definition: return "Definition"; // Definition callback
970
971 case RCT_Custom: return "Custom"; // varies
972
973 case RCT_Undefined: default: return "Undefined";
974 };
975 }
976
GetDbgRecPktData(C4RecordChunkType eType,const StdBuf & RawData)977 StdStrBuf GetDbgRecPktData(C4RecordChunkType eType, const StdBuf & RawData)
978 {
979 StdStrBuf r;
980 switch (eType)
981 {
982 case RCT_AulFunc: r.Ref(reinterpret_cast<const char*>(RawData.getData()), RawData.getSize()-1);
983 break;
984 default:
985 for (unsigned int i=0; i<RawData.getSize(); ++i)
986 r.AppendFormat("%02x ", (uint32_t) *getBufPtr<uint8_t>(RawData, i));
987 break;
988 }
989 return r;
990 }
991
Check(C4RecordChunkType eType,const uint8_t * pData,int iSize)992 void C4Playback::Check(C4RecordChunkType eType, const uint8_t *pData, int iSize)
993 {
994 // only if enabled
995 if (DoNoDebugRec>0) return;
996 if (Game.FrameCounter < DEBUGREC_START_FRAME) return;
997
998 C4PktDebugRec PktInReplay;
999 bool fHasPacketFromHead = false;
1000 if (Config.General.DebugRecExternalFile[0])
1001 {
1002 if (Config.General.DebugRecWrite)
1003 {
1004 // writing of external debugrec file
1005 DbgRecFile.Write(&eType, sizeof eType);
1006 int32_t iSize32 = iSize;
1007 DbgRecFile.Write(&iSize32, sizeof iSize32);
1008 DbgRecFile.Write(pData, iSize);
1009 return;
1010 }
1011 else
1012 {
1013 int32_t iSize32 = 0;
1014 C4RecordChunkType eTypeRec = RCT_Undefined;
1015 DbgRecFile.Read(&eTypeRec, sizeof eTypeRec);
1016 DbgRecFile.Read(&iSize32, sizeof iSize32);
1017 if (iSize32)
1018 {
1019 StdBuf buf;
1020 buf.SetSize(iSize32);
1021 DbgRecFile.Read(buf.getMData(), iSize32);
1022 PktInReplay = C4PktDebugRec(eTypeRec, buf);
1023 }
1024 }
1025 }
1026 else
1027 {
1028 // check debug rec in list
1029 C4IDPacket *pkt;
1030 if ((pkt = DebugRec.firstPkt()))
1031 {
1032 // copy from list
1033 PktInReplay = *static_cast<C4PktDebugRec *>(pkt->getPkt());
1034 DebugRec.Delete(pkt);
1035 }
1036 else
1037 {
1038 // special sync check skip...
1039 while (currChunk != chunks.end() && currChunk->Type == RCT_CtrlPkt)
1040 {
1041 C4IDPacket Packet(*currChunk->pPkt);
1042 C4ControlPacket *pCtrlPck = static_cast<C4ControlPacket *>(Packet.getPkt());
1043 assert(!pCtrlPck->Sync());
1044 ::Control.ExecControlPacket(Packet.getPktType(), pCtrlPck);
1045 NextChunk();
1046 }
1047 // record end?
1048 if (currChunk == chunks.end() || currChunk->Type == RCT_End || Finished)
1049 {
1050 Log("DebugRec end: All in sync!");
1051 ++DoNoDebugRec;
1052 return;
1053 }
1054 // unpack directly from head
1055 if (currChunk->Type != eType)
1056 {
1057 DebugRecError(FormatString("Playback type %x, this type %x", currChunk->Type, eType).getData());
1058 return;
1059 }
1060 if (currChunk->pDbg)
1061 PktInReplay = *currChunk->pDbg;
1062
1063 fHasPacketFromHead = true;
1064 }
1065 }
1066 // record end?
1067 if (PktInReplay.getType() == RCT_End)
1068 {
1069 Log("DebugRec end: All in sync (2)!");
1070 ++DoNoDebugRec;
1071 return;
1072 }
1073 // replay packet is unpacked to PktInReplay now; check it
1074 if (PktInReplay.getType() != eType)
1075 {
1076 DebugRecError(FormatString("Type %s != %s", GetRecordChunkTypeName(PktInReplay.getType()), GetRecordChunkTypeName(eType)).getData());
1077 return;
1078 }
1079 if (PktInReplay.getSize() != unsigned(iSize))
1080 {
1081 DebugRecError(FormatString("Size %d != %d", (int) PktInReplay.getSize(), (int) iSize).getData());
1082 }
1083 // check packet data
1084 if (memcmp(PktInReplay.getData(), pData, iSize))
1085 {
1086 StdStrBuf sErr;
1087 sErr.Format("DbgRecPkt Type %s, size %d", GetRecordChunkTypeName(eType), iSize);
1088 sErr.Append(" Replay: ");
1089 StdBuf replay(PktInReplay.getData(), PktInReplay.getSize());
1090 sErr.Append(GetDbgRecPktData(eType, replay));
1091 sErr.Append(" Here: ");
1092 StdBuf here(pData, iSize);
1093 sErr.Append(GetDbgRecPktData(eType, here));
1094 DebugRecError(sErr.getData());
1095 }
1096 // packet is fine, jump over it
1097 if (fHasPacketFromHead)
1098 NextChunk();
1099 }
1100
DebugRecError(const char * szError)1101 void C4Playback::DebugRecError(const char *szError)
1102 {
1103 LogF("Playback error: %s", szError);
1104 BREAKPOINT_HERE;
1105 }
1106
StreamToRecord(const char * szStream,StdStrBuf * pRecordFile)1107 bool C4Playback::StreamToRecord(const char *szStream, StdStrBuf *pRecordFile)
1108 {
1109
1110 // Load data
1111 StdBuf CompressedData;
1112 Log("Reading stream...");
1113 if (!CompressedData.LoadFromFile(szStream))
1114 return false;
1115
1116 // Decompress
1117 unsigned long iStreamSize = CompressedData.getSize() * 5;
1118 StdBuf StreamData; StreamData.New(iStreamSize);
1119 while (true)
1120 {
1121
1122 // Initialize stream
1123 z_stream strm;
1124 ZeroMem(&strm, sizeof strm);
1125 strm.next_in = getMBufPtr<BYTE>(CompressedData);
1126 strm.avail_in = CompressedData.getSize();
1127 strm.next_out = getMBufPtr<BYTE>(StreamData);
1128 strm.avail_out = StreamData.getSize();
1129
1130 // Decompress
1131 if (inflateInit(&strm) != Z_OK)
1132 return false;
1133 int ret = inflate(&strm, Z_FINISH);
1134 if (ret == Z_OK)
1135 {
1136 inflateEnd(&strm);
1137 break;
1138 }
1139 if (ret != Z_BUF_ERROR)
1140 return false;
1141
1142 // All input consumed?
1143 iStreamSize = strm.total_out;
1144 if (strm.avail_in == 0)
1145 {
1146 Log("Stream data incomplete, using as much data as possible");
1147 break;
1148 }
1149
1150 // Larger buffer needed
1151 StreamData.Grow(CompressedData.getSize());
1152 iStreamSize = StreamData.getSize();
1153 }
1154 StreamData.SetSize(iStreamSize);
1155
1156 // Parse
1157 C4Playback Playback;
1158 Playback.ReadBinary(StreamData);
1159 LogF("Got %lu chunks from stream", static_cast<unsigned long>(Playback.chunks.size()));
1160
1161 // Get first chunk, which must contain the initial
1162 chunks_t::iterator chunkIter = Playback.chunks.begin();
1163 if (chunkIter == Playback.chunks.end() || chunkIter->Type != RCT_File)
1164 return false;
1165
1166 // Get initial chunk, go over file name
1167 StdBuf InitialData = *chunkIter->pFileData;
1168
1169 // Put to temporary file and unpack
1170 char szInitial[_MAX_PATH+1] = "~initial.tmp";
1171 MakeTempFilename(szInitial);
1172 if (!InitialData.SaveToFile(szInitial) ||
1173 !C4Group_UnpackDirectory(szInitial))
1174 return false;
1175
1176 // Load Scenario.txt from Initial
1177 C4Group Grp; C4Scenario Initial;
1178 if (!Grp.Open(szInitial) ||
1179 !Initial.Load(Grp) ||
1180 !Grp.Close())
1181 return false;
1182
1183 // Copy original scenario
1184 const char *szOrigin = Initial.Head.Origin.getData();
1185 char szRecord[_MAX_PATH + 1];
1186 SCopy(szStream, szRecord, _MAX_PATH);
1187 if (GetExtension(szRecord))
1188 *(GetExtension(szRecord) - 1) = 0;
1189 SAppend(".ocs", szRecord, _MAX_PATH);
1190 LogF("Original scenario is %s, creating %s.", szOrigin, szRecord);
1191 if (!C4Group_CopyItem(szOrigin, szRecord, false, false))
1192 return false;
1193
1194 // Merge initial
1195 if (!Grp.Open(szRecord) ||
1196 !Grp.Merge(szInitial))
1197 return false;
1198
1199 // Process other files in stream
1200 chunkIter->Delete();
1201 chunkIter = Playback.chunks.erase(chunkIter);
1202 while (chunkIter != Playback.chunks.end())
1203 if (chunkIter->Type == RCT_File)
1204 {
1205 LogF("Inserting %s...", chunkIter->Filename.getData());
1206 StdStrBuf Temp; Temp.Copy(chunkIter->Filename);
1207 MakeTempFilename(&Temp);
1208 if (!chunkIter->pFileData->SaveToFile(Temp.getData()))
1209 return false;
1210 if (!Grp.Move(Temp.getData(), chunkIter->Filename.getData()))
1211 return false;
1212 chunkIter = Playback.chunks.erase(chunkIter);
1213 }
1214 else
1215 chunkIter++;
1216
1217 // Write record data
1218 StdBuf RecordData = Playback.ReWriteBinary();
1219 if (!Grp.Add(C4CFN_CtrlRec, RecordData, false, true))
1220 return false;
1221
1222 // Done
1223 Log("Writing record file...");
1224 Grp.Close();
1225 pRecordFile->Copy(szRecord);
1226 return true;
1227 }
1228