1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2017-2022, The LegacyClonk 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
17#include "C4Include.h"
18#include "C4GameParameters.h"
19
20#include "C4Log.h"
21#include "C4Components.h"
22#include "C4Game.h"
23#include "C4Gui.h"
24#include "C4Wrappers.h"
25
26#include <iterator>
27
28// C4GameRes
29
30C4GameRes::C4GameRes()
31 : eType(NRT_Null), pResCore(nullptr), pNetRes(nullptr) {}
32
33C4GameRes::C4GameRes(const C4GameRes &Res)
34 : eType(Res.getType()), File(Res.getFile()), pResCore(Res.getResCore()), pNetRes(Res.getNetRes())
35{
36 if (pResCore && !pNetRes)
37 pResCore = new C4Network2ResCore(*pResCore);
38}
39
40C4GameRes::~C4GameRes()
41{
42 Clear();
43}
44
45C4GameRes &C4GameRes::operator=(const C4GameRes &Res)
46{
47 Clear();
48 eType = Res.getType();
49 File = Res.getFile();
50 pResCore = Res.getResCore();
51 pNetRes = Res.getNetRes();
52 if (pResCore && !pNetRes)
53 pResCore = new C4Network2ResCore(*pResCore);
54 return *this;
55}
56
57void C4GameRes::Clear()
58{
59 eType = NRT_Null;
60 File.Clear();
61 if (!pNetRes) delete pResCore;
62 pResCore = nullptr;
63 pNetRes = nullptr;
64}
65
66void C4GameRes::SetFile(C4Network2ResType enType, const char *sznFile)
67{
68 assert(!pNetRes && !pResCore);
69 eType = enType;
70 File = sznFile;
71}
72
73void C4GameRes::SetNetRes(C4Network2Res::Ref pnNetRes)
74{
75 Clear();
76 pNetRes = pnNetRes;
77 eType = pNetRes->getType();
78 File = pNetRes->getFile();
79 pResCore = &pNetRes->getCore();
80}
81
82void C4GameRes::CompileFunc(StdCompiler *pComp)
83{
84 bool fCompiler = pComp->isCompiler();
85 // Clear previous data for compiling
86 if (fCompiler) Clear();
87 // Core is needed to decompile something meaningful
88 if (!fCompiler) assert(pResCore);
89 // De-/Compile core
90 pComp->Value(rStruct: mkPtrAdaptNoNull(rpObj&: const_cast<C4Network2ResCore *&>(pResCore)));
91 // Compile: Set type accordingly
92 if (fCompiler)
93 eType = pResCore->getType();
94}
95
96bool C4GameRes::Publish(C4Network2ResList *pNetResList)
97{
98 assert(isPresent());
99 // Already present?
100 if (pNetRes) return true;
101 // determine whether it's loadable
102 bool fAllowUnloadable = false;
103 if (eType == NRT_Definitions) fAllowUnloadable = true;
104 // Add to network resource list
105 C4Network2Res::Ref pNetRes = pNetResList->AddByFile(strFilePath: File.getData(), fTemp: false, eType, iResID: -1, szResName: nullptr, fAllowUnloadable);
106 if (!pNetRes) return false;
107 // Set resource
108 SetNetRes(pNetRes);
109 return true;
110}
111
112bool C4GameRes::Load(C4Network2ResList *pNetResList)
113{
114 assert(pResCore);
115 // Already present?
116 if (pNetRes) return true;
117 // Add to network resource list
118 C4Network2Res::Ref pNetRes = pNetResList->AddByCore(Core: *pResCore);
119 if (!pNetRes) return false;
120 // Set resource
121 SetNetRes(pNetRes);
122 return true;
123}
124
125bool C4GameRes::InitNetwork(C4Network2ResList *pNetResList)
126{
127 // Already initialized?
128 if (getNetRes())
129 return true;
130 // Present? [Host]
131 if (isPresent())
132 {
133 // Publish on network
134 if (!Publish(pNetResList))
135 {
136 LogFatal(id: C4ResStrTableKey::IDS_NET_NOFILEPUBLISH, args: getFile());
137 return false;
138 }
139 }
140 // Got a core? [Client]
141 else if (pResCore)
142 {
143 // Search/Load it
144 if (!Load(pNetResList))
145 {
146 // Give some hints to why this might happen.
147 const char *szFilename = pResCore->getFileName();
148 if (!pResCore->isLoadable())
149 if (pResCore->getType() == NRT_System)
150 LogFatal(id: C4ResStrTableKey::IDS_NET_NOSAMESYSTEM, args&: szFilename);
151 else
152 LogFatal(id: C4ResStrTableKey::IDS_NET_NOSAMEANDTOOLARGE, args&: szFilename);
153 // Should not happen
154 else
155 LogFatal(id: C4ResStrTableKey::IDS_NET_NOVALIDCORE, args&: szFilename);
156 return false;
157 }
158 }
159 // Okay
160 return true;
161}
162
163void C4GameRes::CalcHash()
164{
165 if (!pNetRes) return;
166 pNetRes->CalculateSHA();
167}
168
169// C4GameResList
170
171C4GameResList &C4GameResList::operator=(const C4GameResList &List)
172{
173 Clear();
174 resList.reserve(n: List.resList.size());
175 std::transform(first: List.resList.begin(), last: List.resList.end(), result: std::back_inserter(x&: resList), unary_op: [](const auto &res)
176 {
177 return std::make_unique<C4GameRes>(*res);
178 });
179 return *this;
180}
181
182C4GameResList::ResTypeIterator C4GameResList::iterRes(C4Network2ResType type)
183{
184 return {type, resList};
185}
186
187void C4GameResList::Clear()
188{
189 resList.clear();
190}
191
192bool C4GameResList::Load(const std::vector<std::string> &DefinitionFilenames)
193{
194 // clear any prev
195 Clear();
196 // no defs to be added? that's OK (LocalOnly)
197 if (DefinitionFilenames.size())
198 {
199 for (const auto &def : DefinitionFilenames)
200 {
201 C4Group Def;
202 if (!Def.Open(szGroupName: def.c_str()))
203 {
204 LogFatal(id: C4ResStrTableKey::IDS_PRC_DEFNOTFOUND, args: def);
205 Def.Close();
206 return false;
207 }
208 Def.Close();
209 CreateByFile(eType: NRT_Definitions, szFile: def.c_str());
210 }
211 }
212 // add System.c4g
213 CreateByFile(eType: NRT_System, C4CFN_System);
214 // add all instances of Material.c4g, except those inside the scenario file
215 C4Group *pMatParentGrp = nullptr;
216 while (pMatParentGrp = Game.GroupSet.FindGroup(C4GSCnt_Material, pAfter: pMatParentGrp))
217 if (pMatParentGrp != &Game.ScenarioFile)
218 {
219 CreateByFile(eType: NRT_Material, szFile: (pMatParentGrp->GetFullName() + DirSep C4CFN_Material).getData());
220 }
221 // add global Material.c4g
222 CreateByFile(eType: NRT_Material, C4CFN_Material);
223 // done; success
224 return true;
225}
226
227C4GameRes *C4GameResList::CreateByFile(C4Network2ResType eType, const char *szFile)
228{
229 // Create & set
230 C4GameRes *pRes = new C4GameRes;
231 pRes->SetFile(enType: eType, sznFile: szFile);
232 // Add to list
233 Add(pRes);
234 return pRes;
235}
236
237bool C4GameResList::InitNetwork(C4Network2ResList *pNetResList)
238{
239 // Check all resources without attached network resource object
240 for (const auto &it : resList)
241 {
242 if (!it->InitNetwork(pNetResList))
243 return false;
244 }
245 // Success
246 return true;
247}
248
249void C4GameResList::CalcHashes()
250{
251 for (const auto &it : resList)
252 it->CalcHash();
253}
254
255bool C4GameResList::RetrieveFiles()
256{
257 // wait for all resources
258 for (const auto &it : resList)
259 {
260 if (const C4Network2ResCore *const core{it->getResCore()}; core)
261 {
262 const std::string resName{std::format(fmt: "{}: {}", args: LoadResStr(id: C4ResStrTableKey::IDS_DLG_DEFINITION), args: GetFilename(path: core->getFileName()))};
263 if (!Game.Network.RetrieveRes(Core: *core, iTimeout: C4NetResRetrieveTimeout, szResName: resName.c_str()))
264 return false;
265 }
266 else
267 {
268 return false;
269 }
270 }
271 return true;
272}
273
274void C4GameResList::Add(C4GameRes *pRes)
275{
276 resList.emplace_back(args&: pRes);
277}
278
279void C4GameResList::CompileFunc(StdCompiler *pComp)
280{
281 bool fCompiler = pComp->isCompiler();
282 // Clear previous data
283 int resCount = resList.size();
284 // Compile resource count
285 pComp->Value(rStruct: mkNamingCountAdapt(iCount&: resCount, szName: "Resource"));
286 // Create list
287 if (fCompiler)
288 {
289 Clear();
290 resList.resize(new_size: resCount);
291 }
292 // Compile list
293 pComp->Value(
294 rStruct: mkNamingAdapt(
295 rValue: mkArrayAdaptS(array: resList.data(), size: resCount),
296 szName: "Resource"));
297}
298
299C4GameResList::ResTypeIterator::ResTypeIterator(C4Network2ResType type, const std::vector<std::unique_ptr<C4GameRes>> &resList) : resList{resList}, type{type}, it{resList.begin()}
300{
301 filter();
302}
303
304void C4GameResList::ResTypeIterator::filter()
305{
306 while (it != resList.end() && (*it)->getType() != type)
307 {
308 ++it;
309 }
310}
311
312C4GameResList::ResTypeIterator &C4GameResList::ResTypeIterator::operator++()
313{
314 ++it;
315 filter();
316 return *this;
317}
318
319C4GameRes &C4GameResList::ResTypeIterator::operator*() const
320{
321 return **it;
322}
323
324C4GameRes *C4GameResList::ResTypeIterator::operator->() const
325{
326 return it->get();
327}
328
329bool C4GameResList::ResTypeIterator::operator==(const ResTypeIterator &other) const
330{
331 return type == other.type && it == other.it;
332}
333
334C4GameResList::ResTypeIterator C4GameResList::ResTypeIterator::end() const
335{
336 auto ret = *this;
337 ret.it = resList.end();
338 return ret;
339}
340
341// *** C4GameParameters
342
343C4GameParameters::C4GameParameters() {}
344
345C4GameParameters::~C4GameParameters() {}
346
347void C4GameParameters::Clear()
348{
349 League.Clear();
350 LeagueAddress.Clear();
351 Rules.Clear();
352 Goals.Clear();
353 ScenarioTitle.Ref(pnData: "No title");
354 Scenario.Clear();
355 GameRes.Clear();
356 Clients.Clear();
357 PlayerInfos.Clear();
358 RestorePlayerInfos.Clear();
359 Teams.Clear();
360}
361
362bool C4GameParameters::Load(C4Group &hGroup, C4Scenario *pScenario, const char *szGameText, C4LangStringTable *pLang, const std::vector<std::string> &DefinitionFilenames)
363{
364 // Clear previous data
365 Clear();
366
367 // Scenario
368 Scenario.SetFile(enType: NRT_Scenario, sznFile: hGroup.GetFullName().getData());
369
370 // Additional game resources
371 if (!GameRes.Load(DefinitionFilenames))
372 return false;
373
374 // Player infos (replays only)
375 if (pScenario->Head.Replay)
376 if (hGroup.FindEntry(C4CFN_PlayerInfos))
377 PlayerInfos.Load(hGroup, C4CFN_PlayerInfos);
378
379 // Savegame restore infos: Used for savegames to rejoin joined players
380 if (hGroup.FindEntry(C4CFN_SavePlayerInfos))
381 {
382 // load to savegame info list
383 RestorePlayerInfos.Load(hGroup, C4CFN_SavePlayerInfos, pLang);
384 // transfer counter to allow for additional player joins in savegame resumes
385 PlayerInfos.SetIDCounter(RestorePlayerInfos.GetIDCounter());
386 // in network mode, savegame players may be reassigned in the lobby
387 // in any mode, the final player restoration will be done in InitPlayers()
388 // dropping any players that could not be restored
389 }
390 else if (pScenario->Head.SaveGame)
391 {
392 // maybe there should be a player info file? (old-style savegame)
393 if (szGameText)
394 {
395 // then recreate the player infos to be restored from game text
396 RestorePlayerInfos.LoadFromGameText(pSource: szGameText);
397 // transfer counter
398 PlayerInfos.SetIDCounter(RestorePlayerInfos.GetIDCounter());
399 }
400 }
401
402 // Load teams
403 if (!Teams.Load(hGroup, pInitDefault: pScenario, pLang))
404 {
405 LogFatal(id: C4ResStrTableKey::IDS_PRC_ERRORLOADINGTEAMS); return false;
406 }
407
408 // Compile data
409 StdStrBuf Buf;
410 if (hGroup.LoadEntryString(C4CFN_Parameters, Buf))
411 {
412 if (!CompileFromBuf_LogWarn<StdCompilerINIRead>(
413 TargetStruct: mkNamingAdapt(rValue: mkParAdapt(rObj&: *this, rPar: pScenario), szName: "Parameters"),
414 SrcBuf: Buf,
415 C4CFN_Parameters))
416 return false;
417 }
418 else
419 {
420 // Set default values
421 StdCompilerNull DefaultCompiler;
422 DefaultCompiler.Compile(rStruct: mkParAdapt(rObj&: *this, rPar: pScenario));
423
424 // Set random seed
425 RandomSeed = static_cast<int32_t>(time(timer: nullptr));
426
427 // Set control rate default
428 if (ControlRate < 0)
429 ControlRate = Config.Network.ControlRate;
430
431 // network game?
432 IsNetworkGame = Game.NetworkActive;
433
434 // FairCrew-flag by command line
435 if (!FairCrewForced)
436 UseFairCrew = !!Config.General.FairCrew;
437 if (!FairCrewStrength && UseFairCrew)
438 FairCrewStrength = Config.General.FairCrewStrength;
439
440 // Auto frame skip by options
441 AutoFrameSkip = ::Config.Graphics.AutoFrameSkip;
442 }
443
444 // enforce league settings
445 if (isLeague()) EnforceLeagueRules(pScenario);
446
447 // Done
448 return true;
449}
450
451void C4GameParameters::EnforceLeagueRules(C4Scenario *pScenario)
452{
453 Scenario.CalcHash();
454 GameRes.CalcHashes();
455 Teams.EnforceLeagueRules();
456 AllowDebug = false;
457 // Fair crew enabled in league, if not explicitely disabled by scenario
458 // Fair crew strengt to a moderately high value
459 if (!Game.Parameters.FairCrewForced)
460 {
461 Game.Parameters.UseFairCrew = true;
462 Game.Parameters.FairCrewForced = true;
463 Game.Parameters.FairCrewStrength = 20000;
464 }
465 if (pScenario) MaxPlayers = pScenario->Head.MaxPlayerLeague;
466}
467
468bool C4GameParameters::CheckLeagueRulesStart(bool fFixIt)
469{
470 // Additional checks for start parameters that are illegal in league games.
471
472 if (!isLeague()) return true;
473
474 bool fError = false;
475 std::string error;
476
477 // league games: enforce one team per client
478 C4ClientPlayerInfos *pClient; C4PlayerInfo *pInfo;
479 for (int iClient = 0; pClient = Game.PlayerInfos.GetIndexedInfo(iIndex: iClient); iClient++)
480 {
481 bool fHaveTeam = false; int32_t iClientTeam; const char *szFirstPlayer;
482 for (int iInfo = 0; pInfo = pClient->GetPlayerInfo(iIndex: iInfo); iInfo++)
483 {
484 // Actual human players only
485 if (pInfo->GetType() != C4PT_User) continue;
486
487 int32_t iTeam = pInfo->GetTeam();
488 if (!fHaveTeam)
489 {
490 iClientTeam = iTeam;
491 szFirstPlayer = pInfo->GetName();
492 fHaveTeam = true;
493 }
494 else if ((!Teams.IsCustom() && Game.C4S.Game.IsMelee()) || iTeam != iClientTeam)
495 {
496 error = LoadResStr(id: C4ResStrTableKey::IDS_MSG_NOSPLITSCREENINLEAGUE, args&: szFirstPlayer, args: pInfo->GetName());
497 if (!fFixIt)
498 {
499 fError = true;
500 }
501 else
502 {
503 C4Client *pClient2 = Game.Clients.getClientByID(iID: pClient->GetClientID());
504 if (!pClient2 || pClient2->isHost())
505 fError = true;
506 else
507 Game.Clients.CtrlRemove(pClient: pClient2, szReason: error.c_str());
508 }
509 }
510 }
511 }
512
513 // Error?
514 if (fError)
515 {
516 if (Game.pGUI)
517 Game.pGUI->ShowMessageModal(szMessage: error.c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE), dwButtons: C4GUI::MessageDialog::btnOK, icoIcon: C4GUI::Ico_MeleeLeague);
518 else
519 LogNTr(message: error);
520 return false;
521 }
522 // All okay
523 return true;
524}
525
526bool C4GameParameters::Save(C4Group &hGroup, C4Scenario *pScenario)
527{
528 // Write Parameters.txt
529 const std::string parData{DecompileToBuf<StdCompilerINIWrite>(
530 SrcStruct: mkNamingAdapt(rValue: mkParAdapt(rObj&: *this, rPar: pScenario), szName: "Parameters"))};
531 StdStrBuf buf{parData.c_str(), parData.size()};
532 if (!hGroup.Add(C4CFN_Parameters, pBuffer&: buf, fChild: false, fHoldBuffer: true))
533 return false;
534
535 // Done
536 return true;
537}
538
539bool C4GameParameters::InitNetwork(C4Network2ResList *pResList)
540{
541 // Scenario & material resource
542 if (!Scenario.InitNetwork(pNetResList: pResList))
543 return false;
544
545 // Other game resources
546 if (!GameRes.InitNetwork(pNetResList: pResList))
547 return false;
548
549 // Done
550 return true;
551}
552
553void C4GameParameters::CompileFunc(StdCompiler *pComp, C4Scenario *pScenario)
554{
555 pComp->Value(rStruct: mkNamingAdapt(rValue&: RandomSeed, szName: "RandomSeed", rDefault: !pScenario ? 0 : pScenario->Head.RandomSeed));
556 pComp->Value(rStruct: mkNamingAdapt(rValue&: StartupPlayerCount, szName: "StartupPlayerCount", rDefault: 0));
557 pComp->Value(rStruct: mkNamingAdapt(rValue&: MaxPlayers, szName: "MaxPlayers", rDefault: !pScenario ? 0 : pScenario->Head.MaxPlayer));
558 pComp->Value(rStruct: mkNamingAdapt(rValue&: UseFairCrew, szName: "UseFairCrew", rDefault: !pScenario ? false : (pScenario->Head.ForcedFairCrew == C4SFairCrew_FairCrew)));
559 pComp->Value(rStruct: mkNamingAdapt(rValue&: FairCrewForced, szName: "FairCrewForced", rDefault: !pScenario ? false : (pScenario->Head.ForcedFairCrew != C4SFairCrew_Free)));
560 pComp->Value(rStruct: mkNamingAdapt(rValue&: FairCrewStrength, szName: "FairCrewStrength", rDefault: !pScenario ? 0 : pScenario->Head.FairCrewStrength));
561 pComp->Value(rStruct: mkNamingAdapt(rValue&: AllowDebug, szName: "AllowDebug", rDefault: true));
562 pComp->Value(rStruct: mkNamingAdapt(rValue&: IsNetworkGame, szName: "IsNetworkGame", rDefault: false));
563 pComp->Value(rStruct: mkNamingAdapt(rValue&: ControlRate, szName: "ControlRate", rDefault: -1));
564 pComp->Value(rStruct: mkNamingAdapt(rValue&: AutoFrameSkip, szName: "AutoFrameSkip", rDefault: false));
565 pComp->Value(rStruct: mkNamingAdapt(rValue&: Rules, szName: "Rules", rDefault: !pScenario ? C4IDList() : pScenario->Game.Rules));
566 pComp->Value(rStruct: mkNamingAdapt(rValue&: Goals, szName: "Goals", rDefault: !pScenario ? C4IDList() : pScenario->Game.Goals));
567 pComp->Value(rStruct: mkNamingAdapt(rValue&: League, szName: "League", rDefault: StdStrBuf()));
568
569 // These values are either stored separately (see Load/Save) or
570 // don't make sense for savegames.
571 if (!pScenario)
572 {
573 pComp->Value(rStruct: mkNamingAdapt(rValue&: LeagueAddress, szName: "LeagueAddress", rDefault: ""));
574
575 pComp->Value(rStruct: mkNamingAdapt(rValue&: ScenarioTitle, szName: "Title", rDefault: "No title"));
576 pComp->Value(rStruct: mkNamingAdapt(rValue&: Scenario, szName: "Scenario"));
577 pComp->Value(rStruct&: GameRes);
578
579 pComp->Value(rStruct: mkNamingAdapt(rValue&: PlayerInfos, szName: "PlayerInfos"));
580 pComp->Value(rStruct: mkNamingAdapt(rValue&: RestorePlayerInfos, szName: "RestorePlayerInfos"));
581 pComp->Value(rStruct: mkNamingAdapt(rValue&: Teams, szName: "Teams"));
582 }
583
584 pComp->Value(rStruct&: Clients);
585}
586
587std::string C4GameParameters::GetGameGoalString()
588{
589 // getting game goals from the ID list
590 // unfortunately, names cannot be deduced before object definitions are loaded
591 std::string result;
592 C4ID idGoal;
593 for (int32_t i = 0; i < Goals.GetNumberOfIDs(); ++i)
594 if (idGoal = Goals.GetID(index: i)) if (idGoal != C4ID_None)
595 {
596 if (Game.IsRunning)
597 {
598 C4Def *pDef = C4Id2Def(id: idGoal);
599 if (pDef)
600 {
601 if (!result.empty()) result += ", ";
602 result += pDef->GetName();
603 }
604 }
605 else
606 {
607 if (!result.empty()) result += ", ";
608 result += C4IdText(id: idGoal);
609 }
610 }
611 // Max length safety
612 if (result.size() > C4MaxTitle) result.resize(n: C4MaxTitle);
613 // Compose desc string
614 if (!result.empty())
615 return std::format(fmt: "{}: {}", args: LoadResStr(id: C4ResStrTableKey::IDS_MENU_CPGOALS), args&: result);
616 else
617 return LoadResStr(id: C4ResStrTableKey::IDS_CTL_NOGOAL);
618}
619