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