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