1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2004, Sven2
6 * Copyright (c) 2017-2022, 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// game saving functionality
19
20#include <C4GameSave.h>
21
22#include <C4Components.h>
23#include <C4Game.h>
24#include "C4Version.h"
25#include "StdMarkup.h"
26#include <C4Console.h>
27#include <C4Log.h>
28#include <C4Player.h>
29#include <C4RTF.h>
30
31#include <format>
32#include <utility>
33
34// *** C4GameSave main class
35
36bool C4GameSave::SaveCreateGroup(const char *szFilename, C4Group &hUseGroup)
37{
38 // erase any previous item (2do: work in C4Groups?)
39 EraseItem(szItemName: szFilename);
40 // copy from previous group?
41 if (GetCopyScenario())
42 if (!ItemIdentical(szFilename1: Game.ScenarioFilename, szFilename2: szFilename))
43 if (!C4Group_CopyItem(szSource: Game.ScenarioFilename, szTarget: szFilename))
44 {
45 Log(id: C4ResStrTableKey::IDS_CNS_SAVEASERROR, args&: szFilename); return false;
46 }
47 // open it
48 if (!hUseGroup.Open(szGroupName: szFilename, fCreate: !GetCopyScenario()))
49 {
50 EraseItem(szItemName: szFilename);
51 Log(id: C4ResStrTableKey::IDS_CNS_SAVEASERROR, args&: szFilename);
52 return false;
53 }
54 // done, success
55 return true;
56}
57
58bool C4GameSave::SaveCore()
59{
60 // base on original, current core
61 rC4S = Game.C4S;
62 // Always mark current engine version
63 rC4S.Head.C4XVer[0] = C4XVER1; rC4S.Head.C4XVer[1] = C4XVER2;
64 rC4S.Head.C4XVer[2] = C4XVER3; rC4S.Head.C4XVer[3] = C4XVER4;
65 // Some flags are not to be set for initial settings:
66 // They depend on whether specific runtime data is present, which may simply not be stored into initial
67 // saves, because they rely on any data present and up-to-date within the scenario!
68 if (!fInitial)
69 {
70 // NoInitialize: Marks whether object data is contained and not to be created from core
71 rC4S.Head.NoInitialize = true;
72 // the SaveGame-value, despite it's name, marks whether exact runtime data is contained
73 // the flag must not be altered for pure
74 rC4S.Head.SaveGame = GetSaveRuntimeData() && IsExact();
75 }
76 // reset some network flags
77 rC4S.Head.NetworkGame = 0;
78 // Title in language game was started in (not: save scenarios and net references)
79 if (!GetKeepTitle()) SCopy(szSource: Game.Parameters.ScenarioTitle.getData(), sTarget: rC4S.Head.Title, iMaxL: C4MaxTitle);
80 // some adjustments for everything but saved scenarios
81 if (IsExact())
82 {
83 // Store used definitions
84 rC4S.Definitions.SetModules(modules: Game.DefinitionFilenames, relativeToPath: Config.General.ExePath, relativeToPath2: Config.General.DefinitionPath);
85 // Save game parameters
86 if (!Game.Parameters.Save(hGroup&: *pSaveGroup, pDefault: &Game.C4S)) return false;
87 }
88 // clear MissionAccess in save games and records (sulai)
89 *rC4S.Head.MissionAccess = 0;
90 // OldGfx is no longer supported
91 // checks for IsExact() || ExactLandscape wouldn't catch scenarios using more than 23 materials, so let's make it easy
92 rC4S.Head.ForcedGfxMode = C4SGFXMODE_NEWGFX;
93 // store origin
94 if (GetSaveOrigin())
95 {
96 // keep if assigned already (e.g., when doing a record of a savegame)
97 if (!rC4S.Head.Origin.getLength())
98 {
99 rC4S.Head.Origin.Copy(pnData: Game.ScenarioFilename);
100 Config.ForceRelativePath(sFilename: &rC4S.Head.Origin);
101 }
102 }
103 else if (GetClearOrigin())
104 rC4S.Head.Origin.Clear();
105 // adjust specific values (virtual call)
106 AdjustCore(rC4S);
107 // Save scenario core
108 return !!rC4S.Save(hGroup&: *pSaveGroup);
109}
110
111bool C4GameSave::SaveScenarioSections()
112{
113 // any scenario sections?
114 if (!Game.pScenarioSections) return true;
115 // prepare section filename
116 int iWildcardPos = SCharPos(cTarget: '*', C4CFN_ScenarioSections);
117 char fn[_MAX_FNAME + 1];
118 // save all modified sections
119 for (C4ScenarioSection *pSect = Game.pScenarioSections; pSect; pSect = pSect->pNext)
120 {
121 // compose section filename
122 SCopy(C4CFN_ScenarioSections, sTarget: fn);
123 SDelete(szString: fn, iLen: 1, iPosition: iWildcardPos); SInsert(szString: fn, szInsert: pSect->GetName(), iPosition: iWildcardPos);
124 // do not save self, because that is implied in CurrentScenarioSection and the main landscape/object data
125 if (pSect == Game.pCurrentScenarioSection)
126 pSaveGroup->DeleteEntry(szFilename: fn);
127 else if (pSect->fModified)
128 {
129 // modified section: delete current
130 pSaveGroup->DeleteEntry(szFilename: fn);
131 // replace by new
132 pSaveGroup->Add(szFile: pSect->GetTempFilename(), szAddAs: fn);
133 }
134 }
135 // done, success
136 return true;
137}
138
139bool C4GameSave::SaveLandscape()
140{
141 // exact?
142 if (Game.Landscape.Mode == C4LSC_Exact || GetForceExactLandscape())
143 {
144 C4DebugRecOff DBGRECOFF;
145 // Landscape
146 Game.Objects.RemoveSolidMasks();
147 bool fSuccess;
148 if (Game.Landscape.Mode == C4LSC_Exact)
149 fSuccess = !!Game.Landscape.Save(hGroup&: *pSaveGroup);
150 else
151 fSuccess = !!Game.Landscape.SaveDiff(hGroup&: *pSaveGroup, fSyncSave: !IsSynced());
152 Game.Objects.PutSolidMasks();
153 if (!fSuccess) return false;
154 DBGRECOFF.Clear();
155 // PXS
156 if (!Game.PXS.Save(hGroup&: *pSaveGroup)) return false;
157 // MassMover (create copy, may not modify running data)
158 C4MassMoverSet MassMoverSet;
159 MassMoverSet.Copy(rSet&: Game.MassMover);
160 if (!MassMoverSet.Save(hGroup&: *pSaveGroup)) return false;
161 // Material enumeration
162 if (!Game.Material.SaveEnumeration(hGroup&: *pSaveGroup)) return false;
163 }
164 // static / dynamic
165 if (Game.Landscape.Mode == C4LSC_Static)
166 {
167 // static map
168 // remove old-style landscape.bmp
169 pSaveGroup->DeleteEntry(C4CFN_Landscape);
170 // save materials if not already done
171 if (!GetForceExactLandscape())
172 {
173 // save map
174 if (!Game.Landscape.SaveMap(hGroup&: *pSaveGroup)) return false;
175 // save textures (if changed)
176 if (!Game.Landscape.SaveTextures(hGroup&: *pSaveGroup)) return false;
177 }
178 }
179 else if (Game.Landscape.Mode != C4LSC_Exact)
180 {
181 // dynamic map by landscape.txt or scenario core: nothing to save
182 // in fact, it doesn't even make much sense to save the Objects.txt
183 // but the user pressed save after all...
184 }
185 return true;
186}
187
188bool C4GameSave::SaveRuntimeData()
189{
190 // scenario sections (exact only)
191 if (IsExact()) if (!SaveScenarioSections())
192 {
193 Log(id: C4ResStrTableKey::IDS_ERR_SAVE_SCENSECTIONS); return false;
194 }
195 // landscape
196 if (!SaveLandscape()) { Log(id: C4ResStrTableKey::IDS_ERR_SAVE_LANDSCAPE); return false; }
197 // Strings
198 Game.ScriptEngine.Strings.EnumStrings();
199 if (!Game.ScriptEngine.Strings.Save(ParentGroup&: (*pSaveGroup)))
200 {
201 Log(id: C4ResStrTableKey::IDS_ERR_SAVE_SCRIPTSTRINGS); return false;
202 }
203 // Objects
204 if (!Game.Objects.Save(hGroup&: (*pSaveGroup), fSaveGame: IsExact(), fSaveInactive: true))
205 {
206 Log(id: C4ResStrTableKey::IDS_ERR_SAVE_OBJECTS); return false;
207 }
208 // Round results
209 if (GetSaveUserPlayers()) if (!Game.RoundResults.Save(hGroup&: *pSaveGroup))
210 {
211 Log(id: C4ResStrTableKey::IDS_ERR_ERRORSAVINGROUNDRESULTS); return false;
212 }
213 // Teams
214 if (!Game.Teams.Save(hGroup&: *pSaveGroup))
215 {
216 Log(id: C4ResStrTableKey::IDS_ERR_ERRORSAVINGTEAMS); return false;
217 }
218 // some scenario components possiby modified in console mode
219 // such modifications cannot possibly be done before game start
220 // so it's runtime data
221 // Script
222 if (!Game.Script.Save(hGroup&: (*pSaveGroup))) Log(id: C4ResStrTableKey::IDS_ERR_SAVE_SCRIPT); /* nofail */
223 // Title - unexact only, because in savegames, the title will be set in core
224 if (!IsExact()) if (!Game.Title.Save(hGroup&: (*pSaveGroup))) Log(id: C4ResStrTableKey::IDS_ERR_SAVE_TITLE); /* nofail */
225 // Info
226 if (!Game.Info.Save(hGroup&: (*pSaveGroup))) Log(id: C4ResStrTableKey::IDS_ERR_SAVE_INFO); /* nofail */
227 if (GetSaveUserPlayers() || GetSaveScriptPlayers())
228 {
229 // player infos
230 // the stored player info filenames will point into the scenario file, and no ressource information
231 // will be saved. PlayerInfo must be saved first, because those will generate the storage filenames to be used by
232 // C4PlayerList
233 C4PlayerInfoList RestoreInfos;
234 RestoreInfos.SetAsRestoreInfos(rFromPlayers&: Game.PlayerInfos, fSaveUserPlrs: GetSaveUserPlayers(), fSaveScriptPlrs: GetSaveScriptPlayers(), fSetUserPlrRefToLocalGroup: GetSaveUserPlayerFiles(), fSetScriptPlrRefToLocalGroup: GetSaveScriptPlayerFiles());
235 if (!RestoreInfos.Save(hGroup&: *pSaveGroup, C4CFN_SavePlayerInfos))
236 {
237 Log(id: C4ResStrTableKey::IDS_ERR_SAVE_RESTOREPLAYERINFOS); return false;
238 }
239 // Players
240 // this will save the player files to the savegame scenario group only
241 // synchronization to the original player files will be done in global game
242 // synchronization (via control queue)
243 if (GetSaveUserPlayerFiles() || GetSaveScriptPlayerFiles())
244 {
245 if (!Game.Players.Save(hGroup&: (*pSaveGroup), fStoreTiny: GetCreateSmallFile(), rStoreList: RestoreInfos))
246 {
247 Log(id: C4ResStrTableKey::IDS_ERR_SAVE_PLAYERS); return false;
248 }
249 }
250 }
251 else
252 {
253 // non-exact runtime data: remove any exact files
254 // No Game.txt
255 pSaveGroup->Delete(C4CFN_Game);
256 // No player files
257 pSaveGroup->Delete(C4CFN_PlayerInfos);
258 pSaveGroup->Delete(C4CFN_SavePlayerInfos);
259 }
260 // done, success
261 return true;
262}
263
264bool C4GameSave::SaveDesc(C4Group &hToGroup)
265{
266 // Scenario title
267 StdStrBuf title{Game.Parameters.ScenarioTitle};
268 CMarkup::StripMarkup(sText: &title);
269
270 // Unfortunately, there's no way to prealloc the buffer in an appropriate size
271 std::string desc{std::format(
272 fmt: "{{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1031{{\\fonttbl {{\\f0\\fnil\\fcharset{} Times New Roman;}}}}" LineFeed
273 "\\uc1\\pard\\ulnone\\b\\f0\\fs20 {}\\par" LineFeed "\\b0\\fs16\\par" LineFeed,
274 args: C4Config::GetCharsetCode(charset: Config.General.LanguageCharset),
275 args: RtfEscape(plainText: title.getData())
276 )};
277
278 // OK; each specializations has its own desc format
279 WriteDesc(desc);
280
281 // End of file
282 desc.append(LineFeed "}" LineFeed EndOfFile);
283
284 // Generate Filename
285 char szLang[3];
286 SCopyUntil(szSource: Config.General.Language, sTarget: szLang, cUntil: ',', iMaxL: 2);
287
288 // Save to file
289 StdStrBuf buf{desc.c_str(), desc.size()};
290 return !!hToGroup.Add(szName: std::vformat(C4CFN_ScenarioDesc, args: std::make_format_args(fmt_args&: szLang)).c_str(), pBuffer&: buf, fChild: false, fHoldBuffer: true);
291}
292
293void C4GameSave::WriteDescLineFeed(std::string &desc)
294{
295 // paragraph end + cosmetics
296 desc.append(s: "\\par" LineFeed);
297}
298
299void C4GameSave::WriteDescDate(std::string &desc, bool fRecord)
300{
301 // write local time/date
302 time_t tTime; time(timer: &tTime);
303 struct tm *pLocalTime;
304 pLocalTime = localtime(timer: &tTime);
305
306 std::string msg;
307 if (fRecord)
308 {
309 msg = LoadResStr(id: C4ResStrTableKey::IDS_DESC_DATEREC, args&: pLocalTime->tm_mday, args: pLocalTime->tm_mon + 1, args: pLocalTime->tm_year + 1900, args&: pLocalTime->tm_hour, args&: pLocalTime->tm_min);
310 }
311 else
312 {
313 msg = LoadResStrChoice(condition: Game.Network.isEnabled(), ifTrue: C4ResStrTableKey::IDS_DESC_DATENET, ifFalse: C4ResStrTableKey::IDS_DESC_DATE, args&: pLocalTime->tm_mday, args: pLocalTime->tm_mon + 1, args: pLocalTime->tm_year + 1900, args&: pLocalTime->tm_hour, args&: pLocalTime->tm_min);
314 }
315
316 desc.append(str: RtfEscape(plainText: msg));
317 WriteDescLineFeed(desc);
318}
319
320void C4GameSave::WriteDescGameTime(std::string &desc)
321{
322 // Write game duration
323 if (Game.Time)
324 {
325 desc.append(str: RtfEscape(plainText: LoadResStr(id: C4ResStrTableKey::IDS_DESC_DURATION,
326 args: Game.Time / 3600, args: (Game.Time % 3600) / 60, args: Game.Time % 60)));
327 WriteDescLineFeed(desc);
328 }
329}
330
331void C4GameSave::WriteDescEngine(std::string &desc)
332{
333 desc.append(str: RtfEscape(plainText: LoadResStr(id: C4ResStrTableKey::IDS_DESC_VERSION, args: std::format(fmt: "{:03}", C4XVERBUILD))));
334 WriteDescLineFeed(desc);
335}
336
337void C4GameSave::WriteDescLeague(std::string &desc, bool fLeague, const char *strLeagueName)
338{
339 if (fLeague)
340 {
341 desc.append(str: RtfEscape(plainText: LoadResStr(id: C4ResStrTableKey::IDS_PRC_LEAGUE, args&: strLeagueName)));
342 WriteDescLineFeed(desc);
343 }
344}
345
346void C4GameSave::WriteDescDefinitions(std::string &desc)
347{
348 // Definition specs
349 if (Game.DefinitionFilenames.size())
350 {
351 // Desc
352 desc.append(str: RtfEscape(plainText: LoadResStr(id: C4ResStrTableKey::IDS_DESC_DEFSPECS)));
353 // Get definition modules
354 int32_t i = 0;
355 for (const auto &def : Game.DefinitionFilenames)
356 {
357 // Get exe relative path
358 StdStrBuf sDefFilename;
359 sDefFilename.Copy(pnData: Config.AtExeRelativePath(szFilename: def.c_str()));
360 // Convert rtf backslashes
361 sDefFilename.Replace(szOld: "\\", szNew: "\\\\");
362 // Append comma
363 if (i++ > 0) desc.append(s: ", ");
364 // Apend to desc
365 desc.append(str: RtfEscape(plainText: sDefFilename.getData()));
366 }
367 // End of line
368 WriteDescLineFeed(desc);
369 }
370}
371
372void C4GameSave::WriteDescNetworkClients(std::string &desc)
373{
374 // Desc
375 desc.append(s: RtfEscape(plainText: LoadResStr(id: C4ResStrTableKey::IDS_DESC_CLIENTS)).c_str());
376 // Client names
377 bool comma{false};
378 for (C4Network2Client *pClient = Game.Network.Clients.GetNextClient(pClient: nullptr); pClient; pClient = Game.Network.Clients.GetNextClient(pClient))
379 {
380 if (comma)
381 {
382 desc.append(s: ", ");
383 }
384
385 desc.append(str: RtfEscape(plainText: pClient->getName()));
386 comma = true;
387 }
388 // End of line
389 WriteDescLineFeed(desc);
390}
391
392void C4GameSave::WriteDescPlayers(std::string &desc, bool fByTeam, int32_t idTeam)
393{
394 // write out all players; only if they match the given team if specified
395 C4PlayerInfo *pPlr; bool fAnyPlrWritten = false;
396 for (int i = 0; pPlr = Game.PlayerInfos.GetPlayerInfoByIndex(index: i); i++)
397 if (pPlr->HasJoined() && !pPlr->IsRemoved() && !pPlr->IsInvisible())
398 {
399 if (fByTeam)
400 if (idTeam)
401 {
402 // match team
403 if (pPlr->GetTeam() != idTeam) continue;
404 }
405 else
406 {
407 // must be in no known team
408 if (Game.Teams.GetTeamByID(iID: pPlr->GetTeam())) continue;
409 }
410 if (fAnyPlrWritten)
411 desc.append(s: ", ");
412 else if (fByTeam && idTeam)
413 {
414 C4Team *pTeam = Game.Teams.GetTeamByID(iID: idTeam);
415 if (pTeam) desc.append(str: RtfEscape(plainText: std::format(fmt: "{}: ", args: pTeam->GetName())));
416 }
417 desc.append(str: RtfEscape(plainText: pPlr->GetName()));
418 fAnyPlrWritten = true;
419 }
420 if (fAnyPlrWritten) WriteDescLineFeed(desc);
421}
422
423void C4GameSave::WriteDescPlayers(std::string &desc)
424{
425 // New style using Game.PlayerInfos
426 if (Game.PlayerInfos.GetPlayerCount())
427 {
428 desc.append(str: RtfEscape(plainText: LoadResStr(id: C4ResStrTableKey::IDS_DESC_PLRS)));
429 if (Game.Teams.IsMultiTeams() && !Game.Teams.IsAutoGenerateTeams())
430 {
431 // Teams defined: Print players sorted by teams
432 WriteDescLineFeed(desc);
433 C4Team *pTeam; int32_t i = 0;
434 while (pTeam = Game.Teams.GetTeamByIndex(iIndex: i++))
435 {
436 WriteDescPlayers(desc, fByTeam: true, idTeam: pTeam->GetID());
437 }
438 // Finally, print out players outside known teams (those can only be achieved by script using SetPlayerTeam)
439 WriteDescPlayers(desc, fByTeam: true, idTeam: 0);
440 }
441 else
442 {
443 // No teams defined: Print all players that have ever joined
444 WriteDescPlayers(desc, fByTeam: false, idTeam: 0);
445 }
446 }
447}
448
449bool C4GameSave::Save(const char *szFilename)
450{
451 // close any previous
452 Close();
453 // create group
454 C4Group *pLSaveGroup = new C4Group();
455 if (!SaveCreateGroup(szFilename, hUseGroup&: *pLSaveGroup))
456 {
457 Log(id: C4ResStrTableKey::IDS_ERR_SAVE_TARGETGRP, args: szFilename ? szFilename : "nullptr!");
458 delete pLSaveGroup;
459 return false;
460 }
461 // save to it
462 return Save(hToGroup&: *pLSaveGroup, fKeepGroup: true);
463}
464
465bool C4GameSave::Save(C4Group &hToGroup, bool fKeepGroup)
466{
467 // close any previous
468 Close();
469 // set group
470 pSaveGroup = &hToGroup; fOwnGroup = fKeepGroup;
471 // PreSave-actions (virtual call)
472 if (!OnSaving()) return false;
473 // always save core
474 if (!SaveCore()) { Log(id: C4ResStrTableKey::IDS_ERR_SAVE_CORE); return false; }
475 // cleanup group
476 pSaveGroup->Delete(C4CFN_PlayerFiles);
477 // remove: Title text, image and icon if specified
478 if (!GetKeepTitle())
479 {
480 pSaveGroup->Delete(C4CFN_ScenarioTitle);
481 pSaveGroup->Delete(C4CFN_ScenarioIcon);
482 pSaveGroup->Delete(szFiles: std::vformat(C4CFN_ScenarioDesc, args: std::make_format_args(fmt_args: "*")).c_str());
483 pSaveGroup->Delete(C4CFN_Titles);
484 pSaveGroup->Delete(C4CFN_Info);
485 }
486 // Always save Game.txt; even for saved scenarios, because global effects need to be saved
487 if (!Game.SaveData(hGroup&: *pSaveGroup, fSaveSection: false, fInitial, fSaveExact: IsExact()))
488 {
489 Log(id: C4ResStrTableKey::IDS_ERR_SAVE_RUNTIMEDATA); return false;
490 }
491 // save additional runtime data
492 if (GetSaveRuntimeData()) if (!SaveRuntimeData()) return false;
493 // Desc
494 if (GetSaveDesc())
495 if (!SaveDesc(hToGroup&: *pSaveGroup))
496 Log(id: C4ResStrTableKey::IDS_ERR_SAVE_DESC); /* nofail */
497 // save specialized components (virtual call)
498 if (!SaveComponents()) return false;
499 // done, success
500 return true;
501}
502
503bool C4GameSave::Close()
504{
505 bool fSuccess = true;
506 // any group open?
507 if (pSaveGroup)
508 {
509 // sort group
510 const char *szSortOrder = GetSortOrder();
511 if (szSortOrder) pSaveGroup->Sort(szSortList: szSortOrder);
512 // close if owned group
513 if (fOwnGroup)
514 {
515 fSuccess = !!pSaveGroup->Close();
516 delete pSaveGroup;
517 fOwnGroup = false;
518 }
519 pSaveGroup = nullptr;
520 }
521 return fSuccess;
522}
523
524// *** C4GameSaveSavegame
525
526bool C4GameSaveSavegame::OnSaving()
527{
528 if (!Game.IsRunning) return true;
529 // synchronization to sync player files on all clients
530 // this resets playing times and stores them in the players?
531 // but doing so would be too late when the queue is executed!
532 // TODO: remove it? (-> PeterW ;))
533 if (Game.Network.isEnabled())
534 Game.Input.Add(eType: CID_Synchronize, pCtrl: new C4ControlSynchronize(true));
535 else
536 Game.Players.SynchronizeLocalFiles();
537 // OK; save now
538 return true;
539}
540
541void C4GameSaveSavegame::AdjustCore(C4Scenario &rC4S)
542{
543 // Determine save game index from trailing number in group file name
544 int iSaveGameIndex = GetTrailingNumber(strString: GetFilenameOnly(strFilename: pSaveGroup->GetFullName().getData()));
545 // Looks like a decent index: set numbered icon
546 if (Inside(ival: iSaveGameIndex, lbound: 1, rbound: 10))
547 rC4S.Head.Icon = 2 + (iSaveGameIndex - 1);
548 // Else: set normal script icon
549 else
550 rC4S.Head.Icon = 29;
551}
552
553bool C4GameSaveSavegame::SaveComponents()
554{
555 // special for savegames: save a screenshot
556 if (!Game.SaveGameTitle(hGroup&: (*pSaveGroup)))
557 Log(id: C4ResStrTableKey::IDS_ERR_SAVE_GAMETITLE); /* nofail */
558 // done, success
559 return true;
560}
561
562bool C4GameSaveSavegame::WriteDesc(std::string &desc)
563{
564 // compose savegame desc
565 WriteDescDate(desc);
566 WriteDescGameTime(desc);
567 WriteDescDefinitions(desc);
568 if (Game.Network.isEnabled()) WriteDescNetworkClients(desc);
569 WriteDescPlayers(desc);
570 // done, success
571 return true;
572}
573
574// *** C4GameSaveRecord
575
576void C4GameSaveRecord::AdjustCore(C4Scenario &rC4S)
577{
578 // specific recording flags
579 rC4S.Head.Replay = true;
580 rC4S.Head.Icon = 29;
581 // default record title
582 std::array<char, C4MaxTitle + 1> buf;
583 FormatWithNull(buf, fmt: "{:03} {} [{}]", args&: iNum, args: Game.Parameters.ScenarioTitle.getData(), C4XVERBUILD);
584 SCopy(szSource: buf.data(), sTarget: rC4S.Head.Title, iMaxL: C4MaxTitle);
585}
586
587bool C4GameSaveRecord::SaveComponents()
588{
589 // special: records need player infos even if done initially
590 if (fInitial) Game.PlayerInfos.Save(hGroup&: (*pSaveGroup), C4CFN_PlayerInfos);
591 // for !fInitial, player infos will be saved as regular runtime data
592 // done, success
593 return true;
594}
595
596bool C4GameSaveRecord::WriteDesc(std::string &desc)
597{
598 // compose record desc
599 WriteDescDate(desc, fRecord: true);
600 WriteDescGameTime(desc);
601 WriteDescEngine(desc);
602 WriteDescDefinitions(desc);
603 WriteDescLeague(desc, fLeague, strLeagueName: Game.Parameters.League.getData());
604 if (Game.Network.isEnabled()) WriteDescNetworkClients(desc);
605 WriteDescPlayers(desc);
606 // done, success
607 return true;
608}
609
610// *** C4GameSaveNetwork
611
612void C4GameSaveNetwork::AdjustCore(C4Scenario &rC4S)
613{
614 // specific dynamic flags
615 rC4S.Head.NetworkGame = true;
616 rC4S.Head.NetworkRuntimeJoin = !fInitial;
617}
618