1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (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/* Control packets contain all player input in the message queue */
18
19#include <C4Include.h>
20#include <C4Control.h>
21
22#include <C4Object.h>
23#include <C4GameSave.h>
24#include <C4GameLobby.h>
25#include <C4Random.h>
26#include <C4Console.h>
27#include <C4Log.h>
28#include <C4Wrappers.h>
29#include <C4Player.h>
30
31#include <cassert>
32#include <cinttypes>
33#include <format>
34
35#include <fmt/printf.h>
36
37// *** C4ControlPacket
38C4ControlPacket::C4ControlPacket()
39 : iByClient(Game.Control.ClientID()) {}
40
41C4ControlPacket::~C4ControlPacket() {}
42
43bool C4ControlPacket::LocalControl() const
44{
45 return iByClient == Game.Control.ClientID();
46}
47
48void C4ControlPacket::SetByClient(int32_t inByClient)
49{
50 iByClient = inByClient;
51}
52
53void C4ControlPacket::CompileFunc(StdCompiler *pComp)
54{
55 // Section must be set by caller
56 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iByClient), szName: "ByClient", rDefault: -1));
57}
58
59// *** C4Control
60
61C4Control::C4Control() {}
62
63C4Control::~C4Control()
64{
65 Clear();
66}
67
68void C4Control::Clear()
69{
70 Pkts.Clear();
71}
72
73bool C4Control::PreExecute(const std::shared_ptr<spdlog::logger> &logger) const
74{
75 bool fReady = true;
76 for (C4IDPacket *pPkt = firstPkt(); pPkt; pPkt = nextPkt(pPkt))
77 {
78 // recheck packet type: Must be control
79 if (pPkt->getPktType() & CID_First)
80 {
81 C4ControlPacket *pCtrlPkt = static_cast<C4ControlPacket *>(pPkt->getPkt());
82 if (pCtrlPkt)
83 fReady &= pCtrlPkt->PreExecute(logger);
84 }
85 else
86 {
87 logger->warn(fmt: "C4Control::PreExecute: WARNING: Ignoring packet type {:2x} (not control.)", args: std::to_underlying(value: pPkt->getPktType()));
88 }
89 }
90 return fReady;
91}
92
93void C4Control::Execute(const std::shared_ptr<spdlog::logger> &logger) const
94{
95 for (C4IDPacket *pPkt = firstPkt(); pPkt; pPkt = nextPkt(pPkt))
96 {
97 // recheck packet type: Must be control
98 if (pPkt->getPktType() & CID_First)
99 {
100 C4ControlPacket *pCtrlPkt = static_cast<C4ControlPacket *>(pPkt->getPkt());
101 if (pCtrlPkt)
102 pCtrlPkt->Execute(logger);
103 }
104 else
105 {
106 logger->warn(fmt: "C4Control::Execute: Ignoring packet type {:2x} (not control.)", args: std::to_underlying(value: pPkt->getPktType()));
107 }
108 }
109}
110
111void C4Control::PreRec(C4Record *pRecord) const
112{
113 for (C4IDPacket *pPkt = firstPkt(); pPkt; pPkt = nextPkt(pPkt))
114 {
115 C4ControlPacket *pCtrlPkt = static_cast<C4ControlPacket *>(pPkt->getPkt());
116 if (pCtrlPkt)
117 pCtrlPkt->PreRec(pRecord);
118 }
119}
120
121void C4Control::CompileFunc(StdCompiler *pComp)
122{
123 pComp->Value(rStruct&: Pkts);
124}
125
126// *** C4ControlSet
127
128void C4ControlSet::Execute(const std::shared_ptr<spdlog::logger> &) const
129{
130 switch (eValType)
131 {
132 case C4CVT_ControlRate: // adjust control rate
133 // host only
134 if (!HostControl()) break;
135 // adjust control rate
136 Game.Control.ControlRate += iData;
137 Game.Control.ControlRate = BoundBy<int32_t>(bval: Game.Control.ControlRate, lbound: 1, rbound: C4MaxControlRate);
138 Game.Parameters.ControlRate = Game.Control.ControlRate;
139 // write back adjusted control rate to network settings
140 if (Game.Control.isCtrlHost() && !Game.Control.isReplay() && Game.Control.isNetwork())
141 Config.Network.ControlRate = Game.Control.ControlRate;
142 // always show msg
143 Game.GraphicsSystem.FlashMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_NET_CONTROLRATE, args&: Game.Control.ControlRate).c_str());
144 break;
145
146 case C4CVT_DisableDebug: // force debug mode disabled
147 {
148 if (Game.DebugMode)
149 {
150 Game.DebugMode = false;
151 Game.GraphicsSystem.DeactivateDebugOutput();
152 Game.MessageInput.RemoveCommand(command: C4MessageInput::SPEED);
153 }
154 // save flag, log
155 Game.Parameters.AllowDebug = false;
156 const auto *const client = ::Game.Clients.getClientByID(iID: iByClient);
157 LogNTr(fmt: "Debug mode forced disabled by {}", args: client ? client->getName() : "<unknown client>");
158 break;
159 }
160 break;
161
162 case C4CVT_MaxPlayer:
163 // host only
164 if (!HostControl()) break;
165 // not in league
166 if (Game.Parameters.isLeague())
167 {
168 LogNTr(message: "/set maxplayer disabled in league!");
169 C4GUI::GUISound(szSound: "Error");
170 break;
171 }
172 // set it
173 Game.Parameters.MaxPlayers = iData;
174 LogNTr(fmt: "MaxPlayer = {}", args&: Game.Parameters.MaxPlayers);
175
176 if (auto *const lobby = Game.Network.GetLobby(); lobby)
177 {
178 lobby->UpdatePlayerCountDisplay();
179 }
180 break;
181
182 case C4CVT_TeamDistribution:
183 // host only
184 if (!HostControl()) break;
185 // set new value
186 Game.Teams.SetTeamDistribution(static_cast<C4TeamList::TeamDist>(iData));
187 break;
188
189 case C4CVT_TeamColors:
190 // host only
191 if (!HostControl()) break;
192 // set new value
193 Game.Teams.SetTeamColors(!!iData);
194 break;
195
196 case C4CVT_FairCrew:
197 // host only
198 if (!HostControl()) break;
199 // deny setting if it's fixed by scenario
200 if (Game.Parameters.FairCrewForced)
201 {
202 if (Game.Control.isCtrlHost()) Log(id: C4ResStrTableKey::IDS_MSG_NOMODIFYFAIRCREW);
203 break;
204 }
205 // set new value
206 if (iData < 0)
207 {
208 Game.Parameters.UseFairCrew = false;
209 Game.Parameters.FairCrewStrength = 0;
210 }
211 else
212 {
213 Game.Parameters.UseFairCrew = true;
214 Game.Parameters.FairCrewStrength = iData;
215 }
216 // runtime updates for runtime fairness adjustments
217 if (Game.IsRunning)
218 {
219 // invalidate fair crew physicals
220 for (std::size_t i{0}; const auto def = Game.Defs.GetDef(index: i); ++i)
221 def->ClearFairCrewPhysicals();
222 // show msg
223 if (Game.Parameters.UseFairCrew)
224 {
225 int iRank = Game.Rank.RankByExperience(iExp: Game.Parameters.FairCrewStrength);
226 Game.GraphicsSystem.FlashMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_FAIRCREW_ACTIVATED, args: Game.Rank.GetRankName(iRank, fReturnLastIfOver: true).getData()).c_str());
227 }
228 else
229 Game.GraphicsSystem.FlashMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_FAIRCREW_DEACTIVATED));
230 }
231 // lobby updates
232#ifndef USE_CONSOLE
233 if (Game.Network.isLobbyActive())
234 {
235 Game.Network.GetLobby()->UpdateFairCrew();
236 }
237#endif
238 // this setting is part of the reference
239 if (Game.Network.isEnabled() && Game.Network.isHost())
240 Game.Network.InvalidateReference();
241 break;
242
243 case C4CVT_None:
244 assert(!"C4ControlSet of type C4CVT_None");
245 break;
246 }
247}
248
249void C4ControlSet::CompileFunc(StdCompiler *pComp)
250{
251 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntAdapt(rValue&: eValType), szName: "Type", rDefault: C4CVT_None));
252 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iData), szName: "Data", rDefault: 0));
253 C4ControlPacket::CompileFunc(pComp);
254}
255
256// *** C4ControlScript
257
258void C4ControlScript::Execute(const std::shared_ptr<spdlog::logger> &) const
259{
260 if (Game.Parameters.isLeague())
261 {
262 return;
263 }
264
265 if (const auto byHost = iByClient == C4ClientIDHost; !byHost)
266 {
267 if (Game.Control.isReplay())
268 {
269 if (!Config.General.AllowScriptingInReplays)
270 {
271 return;
272 }
273 }
274 else if (!Console.Active)
275 {
276 return;
277 }
278 }
279
280 const char *szScript = Script.getData();
281 // execute
282 C4Object *pObj = nullptr;
283 C4AulScript *pScript;
284 if (iTargetObj == SCOPE_Console)
285 pScript = &Game.Script;
286 else if (iTargetObj == SCOPE_Global)
287 pScript = &Game.ScriptEngine;
288 else if (pObj = Game.Objects.SafeObjectPointer(iNumber: iTargetObj))
289 pScript = &(pObj->Def->Script);
290 else
291 // default: Fallback to global context
292 pScript = &Game.ScriptEngine;
293 C4Value rVal(pScript->DirectExec(pObj, szScript, szContext: "console script", fPassErrors: false, Strict));
294 // show messages
295 // print script
296 if (pObj)
297 LogNTr(fmt: "-> {}::{}", args: pObj->Def->GetName(), args&: szScript);
298 else
299 LogNTr(fmt: "-> {}", args&: szScript);
300 // print result
301 if (!LocalControl())
302 {
303 C4Network2Client *pClient = nullptr;
304 if (Game.Network.isEnabled())
305 pClient = Game.Network.Clients.GetClientByID(iID: iByClient);
306 if (pClient)
307 LogNTr(fmt: " = {} (by {})", args: rVal.GetDataString(), args: pClient->getName());
308 else
309 LogNTr(fmt: " = {} (by client {})", args: rVal.GetDataString(), args: iByClient);
310 }
311 else
312 LogNTr(fmt: " = {}", args: rVal.GetDataString());
313}
314
315void C4ControlScript::CompileFunc(StdCompiler *pComp)
316{
317 pComp->Value(rStruct: mkNamingAdapt(rValue&: iTargetObj, szName: "TargetObj", rDefault: -1));
318 pComp->Value(rStruct: mkNamingAdapt(rValue&: Strict, szName: "Strict", rDefault: C4AulScriptStrict::MAXSTRICT));
319
320 if (pComp->isCompiler())
321 {
322 CheckStrictness(strict: Strict, comp&: *pComp);
323 }
324
325 pComp->Value(rStruct: mkNamingAdapt(rValue&: Script, szName: "Script", rDefault: ""));
326 C4ControlPacket::CompileFunc(pComp);
327}
328
329// *** C4ControlPlayerSelect
330
331C4ControlPlayerSelect::C4ControlPlayerSelect(int32_t iPlr, const C4ObjectList &Objs)
332 : iPlr(iPlr), iObjCnt(Objs.ObjectCount())
333{
334 pObjNrs = new int32_t[iObjCnt];
335 int32_t i = 0;
336 for (C4ObjectLink *pLnk = Objs.First; pLnk; pLnk = pLnk->Next)
337 pObjNrs[i++] = pLnk->Obj->Number;
338 assert(i == iObjCnt);
339}
340
341void C4ControlPlayerSelect::Execute(const std::shared_ptr<spdlog::logger> &) const
342{
343 // get player
344 C4Player *pPlr = Game.Players.Get(iPlayer: iPlr);
345 if (!pPlr) return;
346
347 // Check object list
348 C4Object *pObj;
349 C4ObjectList SelectObjs;
350 int32_t iControlChecksum = 0;
351 for (int32_t i = 0; i < iObjCnt; i++)
352 if (pObj = Game.Objects.SafeObjectPointer(iNumber: pObjNrs[i]))
353 {
354 iControlChecksum += pObj->Number * (iControlChecksum + 4787821);
355 // user defined object selection: callback to object
356 if (pObj->Category & C4D_MouseSelect)
357 pObj->Call(PSF_MouseSelection, pPars: {C4VInt(iVal: iPlr)});
358 // player crew selection (recheck status of pObj)
359 if (pObj->Status && pPlr->ObjectInCrew(tobj: pObj))
360 SelectObjs.Add(nObj: pObj, eSort: C4ObjectList::stNone);
361 }
362 // count
363 pPlr->CountControl(eType: C4Player::PCID_Command, iID: iControlChecksum);
364
365 // any crew to be selected (or complete crew deselection)?
366 if (!SelectObjs.IsClear() || !iObjCnt)
367 pPlr->SelectCrew(rList&: SelectObjs);
368}
369
370void C4ControlPlayerSelect::CompileFunc(StdCompiler *pComp)
371{
372 pComp->Value(rStruct: mkNamingAdapt(rValue&: iPlr, szName: "Player", rDefault: -1));
373 pComp->Value(rStruct: mkNamingAdapt(rValue&: iObjCnt, szName: "ObjCnt", rDefault: 0));
374 // Compile array
375 if (pComp->isCompiler())
376 {
377 delete[] pObjNrs; pObjNrs = new int32_t[iObjCnt];
378 }
379 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdaptS(array: pObjNrs, size: iObjCnt), szName: "Objs", rDefault: 0));
380
381 C4ControlPacket::CompileFunc(pComp);
382}
383
384// *** C4ControlPlayerControl
385
386void C4ControlPlayerControl::Execute(const std::shared_ptr<spdlog::logger> &) const
387{
388 C4Player *pPlr = Game.Players.Get(iPlayer: iPlr);
389 if (pPlr)
390 {
391 if (!Inside<int>(ival: iCom, lbound: COM_ReleaseFirst, rbound: COM_ReleaseLast))
392 pPlr->CountControl(eType: C4Player::PCID_DirectCom, iID: iCom * 10000 + iData);
393 pPlr->InCom(byCom: iCom, iData);
394 }
395}
396
397void C4ControlPlayerControl::CompileFunc(StdCompiler *pComp)
398{
399 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iPlr), szName: "Player", rDefault: -1));
400 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iCom), szName: "Com", rDefault: 0));
401 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iData), szName: "Data", rDefault: 0));
402 C4ControlPacket::CompileFunc(pComp);
403}
404
405// *** C4ControlPlayerCommand
406
407C4ControlPlayerCommand::C4ControlPlayerCommand(int32_t iPlr, int32_t iCmd, int32_t iX, int32_t iY,
408 C4Object *pTarget, C4Object *pTarget2, int32_t iData, int32_t iAddMode)
409 : iPlr(iPlr), iCmd(iCmd), iX(iX), iY(iY),
410 iTarget(Game.Objects.ObjectNumber(pObj: pTarget)), iTarget2(Game.Objects.ObjectNumber(pObj: pTarget2)),
411 iData(iData), iAddMode(iAddMode) {}
412
413void C4ControlPlayerCommand::Execute(const std::shared_ptr<spdlog::logger> &) const
414{
415 C4Player *pPlr = Game.Players.Get(iPlayer: iPlr);
416 if (pPlr)
417 {
418 pPlr->CountControl(eType: C4Player::PCID_Command, iID: iCmd + iX + iY + iTarget + iTarget2);
419 pPlr->ObjectCommand(iCommand: iCmd,
420 pTarget: Game.Objects.ObjectPointer(iNumber: iTarget),
421 iTx: iX, iTy: iY,
422 pTarget2: Game.Objects.ObjectPointer(iNumber: iTarget2),
423 iData,
424 iAddMode);
425 }
426}
427
428void C4ControlPlayerCommand::CompileFunc(StdCompiler *pComp)
429{
430 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iPlr), szName: "Player", rDefault: -1));
431 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iCmd), szName: "Cmd", rDefault: 0));
432 pComp->Value(rStruct: mkNamingAdapt(rValue&: iX, szName: "X", rDefault: 0));
433 pComp->Value(rStruct: mkNamingAdapt(rValue&: iY, szName: "Y", rDefault: 0));
434 pComp->Value(rStruct: mkNamingAdapt(rValue&: iTarget, szName: "Target", rDefault: 0));
435 pComp->Value(rStruct: mkNamingAdapt(rValue&: iTarget2, szName: "Target2", rDefault: 0));
436 pComp->Value(rStruct: mkNamingAdapt(rValue&: iData, szName: "Data", rDefault: 0));
437 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iAddMode), szName: "AddMode", rDefault: 0));
438 C4ControlPacket::CompileFunc(pComp);
439}
440
441// *** C4ControlSyncCheck
442
443C4ControlSyncCheck::C4ControlSyncCheck() {}
444
445void C4ControlSyncCheck::Set()
446{
447 extern int32_t FRndPtr3;
448 Frame = Game.FrameCounter;
449 ControlTick = Game.Control.ControlTick;
450 Random3 = FRndPtr3;
451 RandomCount = ::RandomCount;
452 AllCrewPosX = GetAllCrewPosX();
453 PXSCount = Game.PXS.Count;
454 MassMoverIndex = Game.MassMover.CreatePtr;
455 ObjectCount = Game.Objects.ObjectCount();
456 ObjectEnumerationIndex = Game.ObjectEnumerationIndex;
457 SectShapeSum = Game.Objects.Sectors.getShapeSum();
458}
459
460int32_t C4ControlSyncCheck::GetAllCrewPosX()
461{
462 int32_t cpx = 0;
463 for (C4Player *pPlr = Game.Players.First; pPlr; pPlr = pPlr->Next)
464 for (C4ObjectLink *clnk = pPlr->Crew.First; clnk; clnk = clnk->Next)
465 cpx += fixtoi(x: clnk->Obj->fix_x, prec: 100);
466 return cpx;
467}
468
469void C4ControlSyncCheck::Execute(const std::shared_ptr<spdlog::logger> &) const
470{
471 // control host?
472 if (Game.Control.isCtrlHost()) return;
473
474 // get the saved sync check data
475 C4ControlSyncCheck *pSyncCheck = Game.Control.GetSyncCheck(iTick: Frame), &SyncCheck = *pSyncCheck;
476 if (!pSyncCheck)
477 {
478 Game.Control.SyncChecks.Add(eType: CID_SyncCheck, pCtrl: new C4ControlSyncCheck(*this));
479 return;
480 }
481
482 // Not equal
483 if (Frame != pSyncCheck->Frame
484 || (ControlTick != pSyncCheck->ControlTick && !Game.Control.isReplay())
485 || Random3 != pSyncCheck->Random3
486 || RandomCount != pSyncCheck->RandomCount
487 || AllCrewPosX != pSyncCheck->AllCrewPosX
488 || PXSCount != pSyncCheck->PXSCount
489 || MassMoverIndex != pSyncCheck->MassMoverIndex
490 || ObjectCount != pSyncCheck->ObjectCount
491 || ObjectEnumerationIndex != pSyncCheck->ObjectEnumerationIndex
492 || SectShapeSum != pSyncCheck->SectShapeSum)
493 {
494 const char *szThis = "Client", *szOther = Game.Control.isReplay() ? "Rec " : "Host";
495 if (iByClient != Game.Control.ClientID())
496 {
497 const char *szTemp = szThis; szThis = szOther; szOther = szTemp;
498 }
499 // Message
500 LogFatalNTr(message: "Network: Synchronization loss!");
501 LogFatalNTr(fmt: "Network: {} Frm {} Ctrl {} Rnc {} Rn3 {} Cpx {} PXS {} MMi {} Obc {} Oei {} Sct {}", args&: szThis, args: Frame, args: ControlTick, args: RandomCount, args: Random3, args: AllCrewPosX, args: PXSCount, args: MassMoverIndex, args: ObjectCount, args: ObjectEnumerationIndex, args: SectShapeSum);
502 LogFatalNTr(fmt: "Network: {} Frm {} Ctrl {} Rnc {} Rn3 {} Cpx {} PXS {} MMi {} Obc {} Oei {} Sct {}", args&: szOther, args&: SyncCheck.Frame, args&: SyncCheck.ControlTick, args&: SyncCheck.RandomCount, args&: SyncCheck.Random3, args&: SyncCheck.AllCrewPosX, args&: SyncCheck.PXSCount, args&: SyncCheck.MassMoverIndex, args&: SyncCheck.ObjectCount, args&: SyncCheck.ObjectEnumerationIndex, args&: SyncCheck.SectShapeSum);
503 StartSoundEffect(name: "SyncError");
504#ifndef NDEBUG
505 // Debug safe
506 C4GameSaveNetwork SaveGame(false);
507 SaveGame.Save(Config.AtExePath("Desync.c4s"));
508#endif
509 // league: Notify regular client disconnect within the game
510 Game.Network.LeagueNotifyDisconnect(iClientID: C4ClientIDHost, eReason: C4LDR_Desync);
511 // Deactivate / end
512 if (Game.Control.isReplay())
513 Game.DoGameOver();
514 else if (Game.Control.isNetwork())
515 {
516 Game.RoundResults.EvaluateNetwork(eResult: C4RoundResults::NR_NetError, szResultsString: "Network: Synchronization loss!");
517 Game.Network.Clear();
518 }
519 }
520}
521
522void C4ControlSyncCheck::CompileFunc(StdCompiler *pComp)
523{
524 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: Frame), szName: "Frame", rDefault: -1));
525 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: ControlTick), szName: "ControlTick", rDefault: 0));
526 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: Random3), szName: "Random3", rDefault: 0));
527 pComp->Value(rStruct: mkNamingAdapt(rValue&: RandomCount, szName: "RandomCount", rDefault: 0));
528 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: AllCrewPosX), szName: "AllCrewPosX", rDefault: 0));
529 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: PXSCount), szName: "PXSCount", rDefault: 0));
530 pComp->Value(rStruct: mkNamingAdapt(rValue&: MassMoverIndex, szName: "MassMoverIndex", rDefault: 0));
531 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: ObjectCount), szName: "ObjectCount", rDefault: 0));
532 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: ObjectEnumerationIndex), szName: "ObjectEnumerationIndex", rDefault: 0));
533 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: SectShapeSum), szName: "SectShapeSum", rDefault: 0));
534 C4ControlPacket::CompileFunc(pComp);
535}
536
537// *** C4ControlSynchronize
538
539void C4ControlSynchronize::Execute(const std::shared_ptr<spdlog::logger> &) const
540{
541 Game.Synchronize(fSavePlayerFiles: fSavePlrFiles);
542 if (fSyncClearance) Game.SyncClearance();
543}
544
545void C4ControlSynchronize::CompileFunc(StdCompiler *pComp)
546{
547 pComp->Value(rStruct: mkNamingAdapt(rValue&: fSavePlrFiles, szName: "SavePlrs", rDefault: false));
548 pComp->Value(rStruct: mkNamingAdapt(rValue&: fSyncClearance, szName: "SyncClear", rDefault: false));
549 C4ControlPacket::CompileFunc(pComp);
550}
551
552// *** C4ControlClientJoin
553
554void C4ControlClientJoin::Execute(const std::shared_ptr<spdlog::logger> &) const
555{
556 // host only
557 if (iByClient != C4ClientIDHost) return;
558 // add client
559 C4Client *pClient = Game.Clients.Add(Core);
560 if (!pClient) return;
561 // log
562 Log(id: C4ResStrTableKey::IDS_NET_CLIENT_JOIN, args: Core.getName());
563 // lobby callback
564 C4GameLobby::MainDlg *pLobby = Game.Network.GetLobby();
565 if (pLobby) pLobby->OnClientJoin(pNewClient: pClient);
566 // console callback
567 if (Console.Active) Console.UpdateMenus();
568}
569
570void C4ControlClientJoin::CompileFunc(StdCompiler *pComp)
571{
572 pComp->Value(rStruct: mkNamingAdapt(rValue&: Core, szName: "ClientCore"));
573 C4ControlPacket::CompileFunc(pComp);
574}
575
576// *** C4Control
577
578void C4ControlClientUpdate::Execute(const std::shared_ptr<spdlog::logger> &) const
579{
580 // host only
581 if (iByClient != C4ClientIDHost) return;
582 // find client
583 C4Client *pClient = Game.Clients.getClientByID(iID);
584 if (!pClient) return;
585 StdStrBuf strClient(LoadResStrChoice(condition: pClient->isLocal(), ifTrue: C4ResStrTableKey::IDS_NET_LOCAL_CLIENT, ifFalse: C4ResStrTableKey::IDS_NET_CLIENT));
586 // do whatever specified
587 switch (eType)
588 {
589 case CUT_Activate:
590 // nothing to do?
591 if (pClient->isActivated() == !!iData) break;
592 // log
593 if (iData)
594 {
595 Log(id: C4ResStrTableKey::IDS_NET_CLIENT_ACTIVATED, args: strClient.getData(), args: pClient->getName());
596 }
597 else
598 {
599 Log(id: C4ResStrTableKey::IDS_NET_CLIENT_DEACTIVATED, args: strClient.getData(), args: pClient->getName());
600 }
601 // activate/deactivate
602 pClient->SetActivated(!!iData);
603 // local?
604 if (pClient->isLocal())
605 Game.Control.SetActivated(!!iData);
606 break;
607 case CUT_SetObserver:
608 // nothing to do?
609 if (pClient->isObserver()) break;
610 // log
611 Log(id: C4ResStrTableKey::IDS_NET_CLIENT_OBSERVE, args: strClient.getData(), args: pClient->getName());
612 // set observer (will deactivate)
613 pClient->SetObserver();
614 // local?
615 if (pClient->isLocal())
616 Game.Control.SetActivated(false);
617 // remove all players ("soft kick")
618 Game.Players.RemoveAtClient(iClient: iID, fDisconnect: true);
619 break;
620 case CUT_None:
621 assert(!"C4ControlClientUpdate of type CUT_None");
622 break;
623 }
624}
625
626void C4ControlClientUpdate::CompileFunc(StdCompiler *pComp)
627{
628 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntAdaptT<uint8_t>(rValue&: eType), szName: "Type", rDefault: CUT_None));
629 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iID), szName: "ClientID", rDefault: C4ClientIDUnknown));
630 if (eType == CUT_Activate)
631 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iData), szName: "Data", rDefault: 0));
632 C4ControlPacket::CompileFunc(pComp);
633}
634
635// *** C4ControlClientRemove
636
637void C4ControlClientRemove::Execute(const std::shared_ptr<spdlog::logger> &) const
638{
639 // host only
640 if (iByClient != C4ClientIDHost) return;
641 if (iID == C4ClientIDHost) return;
642 // find client
643 C4Client *pClient = Game.Clients.getClientByID(iID);
644 if (!pClient)
645 {
646 // TODO: in replays, client list is not yet synchronized
647 // remove players anyway
648 if (Game.Control.isReplay()) Game.Players.RemoveAtClient(iClient: iID, fDisconnect: true);
649 return;
650 }
651 StdStrBuf strClient(LoadResStrChoice(condition: pClient->isLocal(), ifTrue: C4ResStrTableKey::IDS_NET_LOCAL_CLIENT, ifFalse: C4ResStrTableKey::IDS_NET_CLIENT));
652 // local?
653 if (pClient->isLocal())
654 {
655 const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENT_REMOVED, args: strClient.getData(), args: pClient->getName(), args: strReason.getData())};
656 LogNTr(message);
657 Game.RoundResults.EvaluateNetwork(eResult: C4RoundResults::NR_NetError, szResultsString: message.c_str());
658 Game.Control.ChangeToLocal();
659 return;
660 }
661 // remove client
662 if (!Game.Clients.Remove(pClient)) return;
663 // log
664 Log(id: C4ResStrTableKey::IDS_NET_CLIENT_REMOVED, args: strClient.getData(), args: pClient->getName(), args: strReason.getData());
665 // remove all players
666 Game.Players.RemoveAtClient(iClient: iID, fDisconnect: true);
667 // remove all resources
668 if (Game.Network.isEnabled())
669 Game.Network.ResList.RemoveAtClient(iClientID: iID);
670 // lobby callback
671 C4GameLobby::MainDlg *pLobby = Game.Network.GetLobby();
672 if (pLobby && Game.pGUI) pLobby->OnClientPart(pPartClient: pClient);
673 // player list callback
674 Game.Network.Players.OnClientPart(pPartClient: pClient);
675 // console callback
676 if (Console.Active) Console.UpdateMenus();
677
678 // delete
679 delete pClient;
680}
681
682void C4ControlClientRemove::CompileFunc(StdCompiler *pComp)
683{
684 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iID), szName: "ClientID", rDefault: CUT_None));
685 pComp->Value(rStruct: mkNamingAdapt(rValue&: strReason, szName: "Reason", rDefault: ""));
686 C4ControlPacket::CompileFunc(pComp);
687}
688
689// *** C4ControlJoinPlayer
690
691C4ControlJoinPlayer::C4ControlJoinPlayer(const char *szFilename, int32_t iAtClient, int32_t iIDInfo, const C4Network2ResCore &ResCore)
692 : Filename(szFilename, true), iAtClient(iAtClient),
693 idInfo(iIDInfo), fByRes(true), ResCore(ResCore) {}
694
695C4ControlJoinPlayer::C4ControlJoinPlayer(const char *szFilename, int32_t iAtClient, int32_t iIDInfo)
696 : Filename(szFilename, true), iAtClient(iAtClient),
697 idInfo(iIDInfo), fByRes(false)
698{
699 // load from file if filename is given - which may not be the case for script players
700 if (szFilename)
701 {
702 if (!PlrData.LoadFromFile(szFile: szFilename))
703 {
704 spdlog::error(fmt: "Failed loading player data from file {}", args&: szFilename);
705 assert(!"PlrData.LoadFromFile failed");
706 }
707 }
708}
709
710void C4ControlJoinPlayer::Execute(const std::shared_ptr<spdlog::logger> &logger) const
711{
712 const char *szFilename = Filename.getData();
713
714 // get client
715 C4Client *pClient = Game.Clients.getClientByID(iID: iAtClient);
716 if (!pClient) return;
717
718 // get info
719 C4PlayerInfo *pInfo = Game.PlayerInfos.GetPlayerInfoByID(id: idInfo);
720 if (!pInfo)
721 {
722 logger->error(fmt: "Ghost player join: No info for {}", args: idInfo);
723 assert(false);
724 return;
725 }
726 else if (LocalControl())
727 {
728 // Local player: Just join from local file
729 Game.JoinPlayer(szFilename, iAtClient, szAtClientName: pClient->getName(), pInfo);
730 }
731 else if (!fByRes)
732 {
733 if (PlrData.getSize())
734 {
735 // create temp file
736 std::string playerFilename{std::format(fmt: "{}-{}", args: pClient->getName(), args: GetFilename(path: szFilename))};
737 playerFilename = Config.AtTempPath(szFilename: playerFilename.c_str());
738 // copy to it
739 if (PlrData.SaveToFile(szFile: playerFilename.c_str()))
740 {
741 Game.JoinPlayer(szFilename: playerFilename.c_str(), iAtClient, szAtClientName: pClient->getName(), pInfo);
742 EraseFile(szFileName: playerFilename.c_str());
743 }
744 }
745 else if (pInfo->GetType() == C4PT_Script)
746 {
747 // script players may join without data
748 Game.JoinPlayer(szFilename: nullptr, iAtClient, szAtClientName: pClient->getName(), pInfo);
749 }
750 else
751 {
752 // no player data for user player present: Must not happen
753 logger->error(fmt: "Ghost player join: No player data for {}", args: pInfo->GetName());
754 assert(false);
755 return;
756 }
757 }
758 else if (Game.Control.isNetwork())
759 {
760 // Find ressource
761 C4Network2Res::Ref pRes = Game.Network.ResList.getRefRes(iResID: ResCore.getID());
762 if (pRes && pRes->isComplete())
763 Game.JoinPlayer(szFilename: pRes->getFile(), iAtClient, szAtClientName: pClient->getName(), pInfo);
764 }
765 else if (Game.Control.isReplay())
766 {
767 // Expect player in scenario file
768 Game.JoinPlayer(szFilename: std::format(fmt: "{}" DirSep "{}-{}", args: +Game.ScenarioFilename, args: ResCore.getID(), args: GetFilename(path: ResCore.getFileName())).c_str(), iAtClient, szAtClientName: pClient ? pClient->getName() : "Unknown", pInfo);
769 }
770 else
771 // Shouldn't happen
772 assert(false);
773}
774
775void C4ControlJoinPlayer::Strip()
776{
777 // By resource? Can't touch player file, then.
778 if (fByRes) return;
779 // create temp file
780 StdStrBuf PlayerFilename; PlayerFilename.Ref(pnData: GetFilename(path: Filename.getData()));
781 PlayerFilename.Ref(pnData: Config.AtTempPath(szFilename: PlayerFilename.getData()));
782 // Copy to it
783 if (PlrData.SaveToFile(szFile: PlayerFilename.getData()))
784 {
785 // open as group
786 C4Group Grp;
787 if (!Grp.Open(szGroupName: PlayerFilename.getData()))
788 {
789 EraseFile(szFileName: PlayerFilename.getData()); return;
790 }
791 // remove portrais
792 Grp.Delete(C4CFN_Portraits, fRecursive: true);
793 // remove bigicon, if the file size is too large
794 size_t iBigIconSize = 0;
795 if (Grp.FindEntry(C4CFN_BigIcon, sFileName: nullptr, iSize: &iBigIconSize))
796 if (iBigIconSize > C4NetResMaxBigicon * 1024)
797 Grp.Delete(C4CFN_BigIcon);
798 Grp.Close();
799 // Set new data
800 StdBuf NewPlrData;
801 if (!NewPlrData.LoadFromFile(szFile: PlayerFilename.getData()))
802 {
803 EraseFile(szFileName: PlayerFilename.getData()); return;
804 }
805 PlrData.Take(Buf2&: NewPlrData);
806 // Done
807 EraseFile(szFileName: PlayerFilename.getData());
808 }
809}
810
811bool C4ControlJoinPlayer::PreExecute(const std::shared_ptr<spdlog::logger> &) const
812{
813 // all data included in control packet?
814 if (!fByRes) return true;
815 // client lost?
816 if (!Game.Clients.getClientByID(iID: iAtClient)) return true;
817 // network only
818 if (!Game.Control.isNetwork()) return true;
819 // search ressource
820 C4Network2Res::Ref pRes = Game.Network.ResList.getRefRes(iResID: ResCore.getID());
821 // doesn't exist? start loading
822 if (!pRes) { pRes = Game.Network.ResList.AddByCore(Core: ResCore, fLoad: true); }
823 if (!pRes) return true;
824 // is loading or removed?
825 return !pRes->isLoading();
826}
827
828void C4ControlJoinPlayer::PreRec(C4Record *pRecord)
829{
830 if (!pRecord) return;
831 if (fByRes)
832 {
833 // get local file by id
834 C4Network2Res::Ref pRes = Game.Network.ResList.getRefRes(iResID: ResCore.getID());
835 if (!pRes || pRes->isRemoved()) return;
836 // create a copy of the resource
837 StdStrBuf szTemp; szTemp.Copy(pnData: pRes->getFile());
838 MakeTempFilename(sFileName: &szTemp);
839 if (C4Group_CopyItem(szSource: pRes->getFile(), szTarget: szTemp.getData()))
840 {
841 // add to record
842 const std::string target{std::format(fmt: "{}-{}", args: ResCore.getID(), args: GetFilename(path: ResCore.getFileName()))};
843 pRecord->AddFile(szLocalFilename: szTemp.getData(), szAddAs: target.c_str(), fDelete: true);
844 }
845 }
846 else
847 {
848 // player data raw within control: Will be used directly in record
849 }
850}
851
852void C4ControlJoinPlayer::CompileFunc(StdCompiler *pComp)
853{
854 pComp->Value(rStruct: mkNamingAdapt(rValue: mkNetFilenameAdapt(FileName&: Filename), szName: "Filename", rDefault: ""));
855 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iAtClient), szName: "AtClient", rDefault: -1));
856 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: idInfo), szName: "InfoID", rDefault: -1));
857 pComp->Value(rStruct: mkNamingAdapt(rValue&: fByRes, szName: "ByRes", rDefault: false));
858 if (fByRes)
859 pComp->Value(rStruct: mkNamingAdapt(rValue&: ResCore, szName: "ResCore"));
860 else
861 pComp->Value(rStruct: mkNamingAdapt(rValue&: PlrData, szName: "PlrData"));
862 C4ControlPacket::CompileFunc(pComp);
863}
864
865// *** C4ControlEMMoveObject
866
867C4ControlEMMoveObject::C4ControlEMMoveObject(C4ControlEMObjectAction eAction, int32_t tx, int32_t ty, C4Object *pTargetObj,
868 int32_t iObjectNum, int32_t *pObjects, const char *szScript, const C4AulScriptStrict strict)
869 : eAction(eAction), tx(tx), ty(ty), iTargetObj(Game.Objects.ObjectNumber(pObj: pTargetObj)),
870 iObjectNum(iObjectNum), Strict{strict}, pObjects(pObjects), Script(szScript, true) {}
871
872C4ControlEMMoveObject::~C4ControlEMMoveObject()
873{
874 delete[] pObjects; pObjects = nullptr;
875}
876
877void C4ControlEMMoveObject::Execute(const std::shared_ptr<spdlog::logger> &logger) const
878{
879 // Ignore in league mode
880 if (Game.Parameters.isLeague())
881 return;
882
883 bool fLocalCall = LocalControl();
884 switch (eAction)
885 {
886 case EMMO_Move:
887 {
888 if (!pObjects) break;
889 // move all given objects
890 C4Object *pObj;
891 for (int i = 0; i < iObjectNum; ++i)
892 if (pObj = Game.Objects.SafeObjectPointer(iNumber: pObjects[i])) if (pObj->Status)
893 {
894 pObj->ForcePosition(tx: pObj->x + tx, ty: pObj->y + ty);
895 pObj->xdir = pObj->ydir = 0;
896 pObj->Mobile = false;
897 }
898 }
899 break;
900 case EMMO_Enter:
901 {
902 if (!pObjects) break;
903 // enter all given objects into target
904 C4Object *pObj, *pTarget = Game.Objects.SafeObjectPointer(iNumber: iTargetObj);
905 if (pTarget)
906 for (int i = 0; i < iObjectNum; ++i)
907 if (pObj = Game.Objects.SafeObjectPointer(iNumber: pObjects[i]))
908 pObj->Enter(pTarget);
909 }
910 break;
911 case EMMO_Duplicate:
912 {
913 if (!pObjects) break;
914 // local call? adjust selection then
915 if (fLocalCall) Console.EditCursor.GetSelection().Clear();
916 // perform duplication
917 C4Object *pObj;
918 for (int i = 0; i < iObjectNum; ++i)
919 if (pObj = Game.Objects.SafeObjectPointer(iNumber: pObjects[i]))
920 {
921 pObj = Game.CreateObject(type: pObj->id, pCreator: pObj, owner: pObj->Owner, x: pObj->x, y: pObj->y);
922 if (pObj && fLocalCall) Console.EditCursor.GetSelection().Add(nObj: pObj, eSort: C4ObjectList::stNone);
923 }
924 // update status
925 if (fLocalCall)
926 {
927 Console.EditCursor.SetHold(true);
928 Console.PropertyDlg.Update(rSelection&: Console.EditCursor.GetSelection());
929 }
930 }
931 break;
932 case EMMO_Script:
933 {
934 if (!pObjects) return;
935 // execute script ...
936 C4ControlScript ScriptCtrl(Script.getData(), C4ControlScript::SCOPE_Global, Strict);
937 ScriptCtrl.SetByClient(iByClient);
938 // ... for each object in selection
939 for (int i = 0; i < iObjectNum; ++i)
940 {
941 ScriptCtrl.SetTargetObj(pObjects[i]);
942 ScriptCtrl.Execute(logger);
943 }
944 break;
945 }
946 case EMMO_Remove:
947 {
948 if (!pObjects) return;
949 // remove all objects
950 C4Object *pObj;
951 for (int i = 0; i < iObjectNum; ++i)
952 if (pObj = Game.Objects.SafeObjectPointer(iNumber: pObjects[i]))
953 pObj->AssignRemoval();
954 }
955 break; // Here was fallthrough. Seemed wrong. ck.
956 case EMMO_Exit:
957 {
958 if (!pObjects) return;
959 // exit all objects
960 C4Object *pObj;
961 for (int i = 0; i < iObjectNum; ++i)
962 if (pObj = Game.Objects.SafeObjectPointer(iNumber: pObjects[i]))
963 pObj->Exit(iX: pObj->x, iY: pObj->y, iR: pObj->r);
964 }
965 break; // Same. ck.
966 }
967 // update property dlg & status bar
968 if (fLocalCall)
969 Console.EditCursor.OnSelectionChanged();
970}
971
972void C4ControlEMMoveObject::CompileFunc(StdCompiler *pComp)
973{
974 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntAdaptT<uint8_t>(rValue&: eAction), szName: "Action"));
975 pComp->Value(rStruct: mkNamingAdapt(rValue&: tx, szName: "tx", rDefault: 0));
976 pComp->Value(rStruct: mkNamingAdapt(rValue&: ty, szName: "ty", rDefault: 0));
977 pComp->Value(rStruct: mkNamingAdapt(rValue&: iTargetObj, szName: "TargetObj", rDefault: -1));
978 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iObjectNum), szName: "ObjectNum", rDefault: 0));
979 pComp->Value(rStruct: mkNamingAdapt(rValue&: Strict, szName: "Strict", rDefault: C4AulScriptStrict::MAXSTRICT));
980
981 if (pComp->isCompiler())
982 {
983 C4ControlScript::CheckStrictness(strict: Strict, comp&: *pComp);
984
985 delete[] pObjects; pObjects = new int32_t[iObjectNum];
986 }
987
988 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdaptS(array: pObjects, size: iObjectNum), szName: "Objs", rDefault: -1));
989 if (eAction == EMMO_Script)
990 pComp->Value(rStruct: mkNamingAdapt(rValue&: Script, szName: "Script", rDefault: ""));
991 C4ControlPacket::CompileFunc(pComp);
992}
993
994// *** C4ControlEMDrawTool
995
996C4ControlEMDrawTool::C4ControlEMDrawTool(C4ControlEMDrawAction eAction, int32_t iMode,
997 int32_t iX, int32_t iY, int32_t iX2, int32_t iY2, int32_t iGrade,
998 bool fIFT, const char *szMaterial, const char *szTexture)
999 : eAction(eAction), iMode(iMode), iX(iX), iY(iY), iX2(iX2), iY2(iY2), iGrade(iGrade),
1000 fIFT(fIFT), Material(szMaterial, true), Texture(szTexture, true) {}
1001
1002void C4ControlEMDrawTool::Execute(const std::shared_ptr<spdlog::logger> &) const
1003{
1004 // Ignore in league mode
1005 if (Game.Parameters.isLeague())
1006 return;
1007 // set new mode
1008 if (eAction == EMDT_SetMode)
1009 {
1010 Console.ToolsDlg.SetLandscapeMode(iMode, fThroughControl: true);
1011 return;
1012 }
1013 // check current mode
1014 assert(Game.Landscape.Mode == iMode);
1015 if (Game.Landscape.Mode != iMode) return;
1016 // assert validity of parameters
1017 if (!Material.getSize()) return;
1018 const char *szMaterial = Material.getData(),
1019 *szTexture = Texture.getData();
1020 // perform action
1021 switch (eAction)
1022 {
1023 case EMDT_Brush: // brush tool
1024 if (!Texture.getSize()) break;
1025 Game.Landscape.DrawBrush(iX, iY, iGrade, szMaterial, szTexture, fIFT);
1026 break;
1027 case EMDT_Line: // line tool
1028 if (!Texture.getSize()) break;
1029 Game.Landscape.DrawLine(iX1: iX, iY1: iY, iX2, iY2, iGrade, szMaterial, szTexture, fIFT);
1030 break;
1031 case EMDT_Rect: // rect tool
1032 if (!Texture.getSize()) break;
1033 Game.Landscape.DrawBox(iX1: iX, iY1: iY, iX2, iY2, iGrade, szMaterial, szTexture, fIFT);
1034 break;
1035 case EMDT_Fill: // fill tool
1036 {
1037 int iMat = Game.Material.Get(szMaterial);
1038 if (!MatValid(mat: iMat)) return;
1039 for (int cnt = 0; cnt < iGrade; cnt++)
1040 {
1041 // force argument evaluation order
1042 const auto r2 = iY + Random(iRange: iGrade) - iGrade / 2;
1043 const auto r1 = iX + Random(iRange: iGrade) - iGrade / 2;
1044 Game.Landscape.InsertMaterial(mat: iMat, tx: r1, ty: r2);
1045 }
1046 }
1047 break;
1048
1049 case EMDT_SetMode:
1050 // should be handled above already
1051 assert(!"Unhandled switch case");
1052 break;
1053 }
1054}
1055
1056void C4ControlEMDrawTool::CompileFunc(StdCompiler *pComp)
1057{
1058 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntAdaptT<uint8_t>(rValue&: eAction), szName: "Action"));
1059 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iMode), szName: "Mode", rDefault: 0));
1060 pComp->Value(rStruct: mkNamingAdapt(rValue&: iX, szName: "X", rDefault: 0));
1061 pComp->Value(rStruct: mkNamingAdapt(rValue&: iY, szName: "Y", rDefault: 0));
1062 pComp->Value(rStruct: mkNamingAdapt(rValue&: iX2, szName: "X2", rDefault: 0));
1063 pComp->Value(rStruct: mkNamingAdapt(rValue&: iY2, szName: "Y2", rDefault: 0));
1064 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iGrade), szName: "Grade", rDefault: 0));
1065 pComp->Value(rStruct: mkNamingAdapt(rValue&: fIFT, szName: "IFT", rDefault: false));
1066 pComp->Value(rStruct: mkNamingAdapt(rValue&: Material, szName: "Material", rDefault: ""));
1067 pComp->Value(rStruct: mkNamingAdapt(rValue&: Texture, szName: "Texture", rDefault: ""));
1068 C4ControlPacket::CompileFunc(pComp);
1069}
1070
1071// *** C4ControlMessage
1072
1073void C4ControlMessage::Execute(const std::shared_ptr<spdlog::logger> &) const
1074{
1075 const char *szMessage = Message.getData();
1076 // get player
1077 C4Player *pPlr = (iPlayer < 0 ? nullptr : Game.Players.Get(iPlayer));
1078 // security
1079 if (pPlr && pPlr->AtClient != iByClient) return;
1080
1081 // should we flash the window?
1082 bool alert = false;
1083
1084 auto checkAlert = [&alert, this]
1085 {
1086 if (auto *local = Game.Clients.getLocal(); local && local->getID() != iByClient)
1087 {
1088 if (!alert)
1089 {
1090 if (const char *pos = SSearchNoCase(szString: Message.getData(), szIndex: local->getNick()); pos && !IsIdentifier(cChar: *pos))
1091 {
1092 if (const char *begin = pos - strlen(s: local->getNick()); begin - Message.getData() > 0)
1093 {
1094 alert = !IsIdentifier(cChar: begin[-1]);
1095 }
1096 else
1097 {
1098 alert = true;
1099 }
1100 }
1101 }
1102 }
1103 };
1104
1105 // get lobby to forward to
1106 C4GameLobby::MainDlg *pLobby = Game.Network.GetLobby();
1107 switch (eType)
1108 {
1109 case C4CMT_Normal:
1110 case C4CMT_Me:
1111 {
1112 std::string log;
1113 // log it
1114 if (pPlr)
1115 {
1116 if (pPlr->AtClient != iByClient) break;
1117 const char *const plrName{pPlr->GetName()};
1118 log = std::vformat(fmt: (eType == C4CMT_Normal ? (Config.General.UseWhiteIngameChat ? "<c {:x}><{}></c> {}" : "<c {:x}><{}> {}") : (Config.General.UseWhiteIngameChat ? "<c {:x}> * {}</c> {}" : "<c {:x}> * {} {}")),
1119 args: std::make_format_args(fmt_args&: pPlr->ColorDw, fmt_args: plrName, fmt_args&: szMessage));
1120 }
1121 else
1122 {
1123 const auto white = pLobby && Config.General.UseWhiteLobbyChat;
1124 C4Client *pClient = Game.Clients.getClientByID(iID: iByClient);
1125 const char *const nick{pClient ? pClient->getNick() : "???"};
1126 log = std::vformat(fmt: (eType == C4CMT_Normal ? (white ? "<{}> <c ffffff>{}" : "<{}> {}") : (white ? " * {} <c ffffff>{}" : " * {} {}")),
1127 args: std::make_format_args(fmt_args: nick, fmt_args&: szMessage));
1128 }
1129 // 2 lobby
1130 if (pLobby)
1131 pLobby->OnMessage(pOfClient: Game.Clients.getClientByID(iID: iByClient), szMessage: log.c_str());
1132 // or 2 log
1133 else
1134 LogNTr(message: log);
1135
1136 checkAlert();
1137 break;
1138 }
1139 case C4CMT_Say:
1140 {
1141 // show as game message above player view object
1142 if (!pPlr) break;
1143 C4Object *pViewObject = pPlr->ViewTarget;
1144 if (!pViewObject) pViewObject = pPlr->Cursor;
1145 if (!pViewObject) break;
1146 if (Game.C4S.Head.Film == C4SFilm_Cinematic)
1147 {
1148 const std::string message{std::format(fmt: "<{}> {}", args: pPlr->Cursor ? pPlr->Cursor->GetName() : pPlr->GetName(), args&: szMessage)};
1149 uint32_t dwClr = pPlr->Cursor ? pPlr->Cursor->Color : pPlr->ColorDw;
1150 if (!dwClr) dwClr = 0xff;
1151 GameMsgObjectDw(szText: message.c_str(), pTarget: pViewObject, dwClr: dwClr | 0xff000000);
1152 }
1153 else
1154 GameMsgObjectDw(szText: szMessage, pTarget: pViewObject, dwClr: pPlr->ColorDw | 0xff000000);
1155 }
1156 break;
1157
1158 case C4CMT_Team:
1159 {
1160 // show only if sending player is allied with a local one
1161 if (pPlr)
1162 {
1163 // for running game mode, check actual hostility
1164 C4Player *pLocalPlr;
1165 for (int cnt = 0; pLocalPlr = Game.Players.GetLocalByIndex(iIndex: cnt); cnt++)
1166 if (!Hostile(plr1: pLocalPlr->Number, plr2: iPlayer))
1167 break;
1168 if (pLocalPlr)
1169 {
1170 if (Config.General.UseWhiteIngameChat)
1171 {
1172 LogNTr(fmt: "<c {:x}>{{{}}}</c> {}", args&: pPlr->ColorDw, args: pPlr->GetName(), args&: szMessage);
1173 }
1174 else
1175 {
1176 LogNTr(fmt: "<c {:x}>{{{}}} {}", args&: pPlr->ColorDw, args: pPlr->GetName(), args&: szMessage);
1177 }
1178 }
1179 }
1180 else if (pLobby)
1181 {
1182 // in lobby mode, no player has joined yet - check teams of unjoined players
1183 if (!Game.PlayerInfos.HasSameTeamPlayers(iClient1: iByClient, iClient2: Game.Clients.getLocalID())) break;
1184 // OK - permit message
1185 C4Client *pClient = Game.Clients.getClientByID(iID: iByClient);
1186 const char *const nick{pClient ? pClient->getNick() : "???"};
1187 pLobby->OnMessage(pOfClient: Game.Clients.getClientByID(iID: iByClient),
1188 szMessage: std::vformat(fmt: Config.General.UseWhiteLobbyChat ? "{{{}}} <c ffffff>{}" : "{{{}}} {}", args: std::make_format_args(fmt_args: nick, fmt_args&: szMessage)).c_str());
1189 }
1190
1191 checkAlert();
1192 }
1193 break;
1194
1195 case C4CMT_Private:
1196 {
1197 if (!pPlr) break;
1198 // show only if the target player is local
1199 C4Player *pLocalPlr;
1200 for (int cnt = 0; pLocalPlr = Game.Players.GetLocalByIndex(iIndex: cnt); cnt++)
1201 if (pLocalPlr->Number == iToPlayer)
1202 break;
1203 if (pLocalPlr)
1204 {
1205 if (Config.General.UseWhiteIngameChat)
1206 {
1207 LogNTr(fmt: "<c {:x}>[{}]</c> {}", args&: pPlr->ColorDw, args: pPlr->GetName(), args&: szMessage);
1208 }
1209 else
1210 {
1211 LogNTr(fmt: "<c {:x}>[{}] {}", args&: pPlr->ColorDw, args: pPlr->GetName(), args&: szMessage);
1212 }
1213 }
1214
1215 checkAlert();
1216 }
1217 break;
1218
1219 case C4CMT_Sound:
1220 // tehehe, sound!
1221 if (auto *client = Game.Clients.getClientByID(iID: iByClient); client && client->TryAllowSound())
1222 {
1223 if (client->isMuted()
1224 || StartSoundEffect(name: szMessage, loop: false, volume: 100, obj: nullptr)
1225 || StartSoundEffect(name: (szMessage + std::string{".ogg"}).c_str(), loop: false, volume: 100, obj: nullptr)
1226 || StartSoundEffect(name: (szMessage + std::string{".mp3"}).c_str(), loop: false, volume: 100, obj: nullptr))
1227 {
1228 if (pLobby) pLobby->OnClientSound(pOfClient: Game.Clients.getClientByID(iID: iByClient));
1229 }
1230 }
1231 break;
1232
1233 case C4CMT_Alert:
1234 // notify inactive users
1235 alert = true;
1236 break;
1237
1238 case C4CMT_System:
1239 // sender must be host
1240 if (!HostControl()) break;
1241 // show
1242 LogNTr(fmt: "Network: {}", args&: szMessage);
1243 break;
1244 }
1245
1246 if (alert)
1247 {
1248 Application.NotifyUserIfInactive();
1249 }
1250}
1251
1252void C4ControlMessage::CompileFunc(StdCompiler *pComp)
1253{
1254 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntAdaptT<uint8_t>(rValue&: eType), szName: "Type", rDefault: C4CMT_Normal));
1255 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iPlayer), szName: "Player", rDefault: -1));
1256 if (eType == C4CMT_Private)
1257 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iToPlayer), szName: "ToPlayer", rDefault: -1));
1258 pComp->Value(rStruct: mkNamingAdapt(rValue&: Message, szName: "Message", rDefault: ""));
1259 C4ControlPacket::CompileFunc(pComp);
1260}
1261
1262// *** C4ControlPlayerInfo
1263
1264void C4ControlPlayerInfo::Execute(const std::shared_ptr<spdlog::logger> &) const
1265{
1266 // join to player info list
1267 // replay and local control: direct join
1268 if (Game.Control.isReplay() || !Game.Control.isNetwork())
1269 {
1270 // add info directly
1271 Game.PlayerInfos.AddInfo(pNewClientInfo: new C4ClientPlayerInfos(PlrInfo));
1272 // make sure team list reflects teams set in player infos
1273 Game.Teams.RecheckPlayers();
1274 // replay: actual player join packet will follow
1275 // offline game: Issue the join
1276 if (Game.Control.isLocal())
1277 Game.PlayerInfos.LocalJoinUnjoinedPlayersInQueue();
1278 }
1279 else
1280 // network:
1281 Game.Network.Players.HandlePlayerInfo(rInfoPacket: PlrInfo, localOrigin: iByClient == Game.Clients.getLocalID());
1282}
1283
1284void C4ControlPlayerInfo::CompileFunc(StdCompiler *pComp)
1285{
1286 pComp->Value(rStruct&: PlrInfo);
1287 C4ControlPacket::CompileFunc(pComp);
1288}
1289
1290// *** C4ControlRemovePlr
1291
1292void C4ControlRemovePlr::Execute(const std::shared_ptr<spdlog::logger> &) const
1293{
1294 // host only
1295 if (iByClient != C4ClientIDHost) return;
1296 // remove
1297 Game.Players.Remove(iPlayer: iPlr, fDisonnected: fDisconnected, fNoCalls: false);
1298}
1299
1300void C4ControlRemovePlr::CompileFunc(StdCompiler *pComp)
1301{
1302 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iPlr), szName: "Plr", rDefault: -1));
1303 pComp->Value(rStruct: mkNamingAdapt(rValue&: fDisconnected, szName: "Disconnected", rDefault: false));
1304 C4ControlPacket::CompileFunc(pComp);
1305}
1306
1307// *** C4ControlDebugRec
1308
1309void C4ControlDebugRec::Execute(const std::shared_ptr<spdlog::logger> &) const {}
1310
1311void C4ControlDebugRec::CompileFunc(StdCompiler *pComp)
1312{
1313 pComp->Value(rStruct&: Data);
1314}
1315
1316// *** C4ControlVote
1317
1318StdStrBuf C4ControlVote::getDesc() const
1319{
1320 // Describe action
1321 StdStrBuf Action;
1322 switch (eType)
1323 {
1324 case VT_Cancel:
1325 Action.Ref(pnData: LoadResStr(id: C4ResStrTableKey::IDS_VOTE_CANCELTHEROUND)); break;
1326 case VT_Kick:
1327 if (iData == iByClient)
1328 Action.Ref(pnData: LoadResStr(id: C4ResStrTableKey::IDS_VOTE_LEAVETHEGAME));
1329 else
1330 {
1331 C4Client *pTargetClient = Game.Clients.getClientByID(iID: iData);
1332 Action.Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_VOTE_KICKCLIENT, args: pTargetClient ? pTargetClient->getName() : "???").c_str());
1333 }
1334 break;
1335 case VT_Pause:
1336 if (iData)
1337 Action.Ref(pnData: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_PAUSETHEGAME));
1338 else
1339 Action.Ref(pnData: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_UNPAUSETHEGAME));
1340 break;
1341 case VT_None:
1342 ; // fallthrough
1343 }
1344 if (!Action)
1345 {
1346 Action = "perform some mysterious action";
1347 }
1348 return Action;
1349}
1350
1351StdStrBuf C4ControlVote::getDescWarning() const
1352{
1353 StdStrBuf Warning;
1354 switch (eType)
1355 {
1356 case VT_Cancel:
1357 Warning.Ref(pnData: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_WARNINGIFTHEGAMEISCANCELL)); break;
1358 case VT_Kick:
1359 Warning.Ref(pnData: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_WARNINGNOLEAGUEPOINTSWILL)); break;
1360 default:
1361 Warning = ""; break;
1362 }
1363 return Warning;
1364}
1365
1366void C4ControlVote::Execute(const std::shared_ptr<spdlog::logger> &) const
1367{
1368 // Log
1369 C4Client *pClient = Game.Clients.getClientByID(iID: iByClient);
1370 if (fApprove)
1371 Log(id: C4ResStrTableKey::IDS_VOTE_WANTSTO, args: pClient->getName(), args: getDesc().getData());
1372 else
1373 Log(id: C4ResStrTableKey::IDS_VOTE_DOESNOTWANTTO, args: pClient->getName(), args: getDesc().getData());
1374 // Save vote back
1375 if (Game.Network.isEnabled())
1376 Game.Network.AddVote(Vote: *this);
1377 // Vote done?
1378 if (Game.Control.isCtrlHost())
1379 {
1380 // Count votes
1381 int32_t iPositive = 0, iNegative = 0, iVotes = 0;
1382 // If there are no teams, count as if all were in the same team
1383 // (which happens to be equivalent to "everyone is in his own team" here)
1384 for (int32_t i = 0; i < std::max<int32_t>(a: Game.Teams.GetTeamCount(), b: 1); i++)
1385 {
1386 C4Team *pTeam = Game.Teams.GetTeamByIndex(iIndex: i);
1387 // Votes for this team
1388 int32_t iPositiveTeam = 0, iNegativeTeam = 0, iVotesTeam = 0;
1389 // Check each player
1390 for (int32_t j = 0; j < (pTeam ? pTeam->GetPlayerCount() : Game.PlayerInfos.GetPlayerCount()); j++)
1391 {
1392 int32_t iClientID = C4ClientIDUnknown;
1393 C4PlayerInfo *pNfo;
1394 if (!pTeam)
1395 {
1396 pNfo = Game.PlayerInfos.GetPlayerInfoByIndex(index: j);
1397 if (!pNfo) continue; // shouldn't happen
1398 iClientID = Game.PlayerInfos.GetClientInfoByPlayerID(id: pNfo->GetID())->GetClientID();
1399 }
1400 else
1401 {
1402 pNfo = Game.PlayerInfos.GetPlayerInfoByID(id: pTeam->GetIndexedPlayer(iIndex: j), pidClient: &iClientID);
1403 if (!pNfo) continue; // shouldn't happen
1404 }
1405 if (iClientID < 0) continue;
1406 // Client disconnected?
1407 if (!Game.Clients.getClientByID(iID: iClientID)) continue;
1408 // Player eliminated or never joined?
1409 if (!pNfo->IsJoined()) continue;
1410 // Okay, this player can vote
1411 iVotesTeam++;
1412 // Search vote of this client on the subject
1413 C4IDPacket *pPkt; C4ControlVote *pVote;
1414 if (pPkt = Game.Network.GetVote(iClientID, eType, iData))
1415 if (pVote = static_cast<C4ControlVote *>(pPkt->getPkt()))
1416 if (pVote->isApprove())
1417 iPositiveTeam++;
1418 else
1419 iNegativeTeam++;
1420 }
1421 // Any votes available?
1422 if (iVotesTeam)
1423 {
1424 iVotes++;
1425 // Approval by team? More then 50% needed
1426 if (iPositiveTeam * 2 > iVotesTeam)
1427 iPositive++;
1428 // Disapproval by team? More then 50% needed
1429 else if (iNegativeTeam * 2 >= iVotesTeam)
1430 iNegative++;
1431 }
1432 }
1433 // Approval? More then 50% needed
1434 if (iPositive * 2 > iVotes)
1435 Game.Control.DoInput(eCtrlType: CID_VoteEnd,
1436 pPkt: new C4ControlVoteEnd(eType, true, iData),
1437 eDelivery: CDT_Sync);
1438 // Disapproval?
1439 else if (iNegative * 2 >= iVotes)
1440 Game.Control.DoInput(eCtrlType: CID_VoteEnd,
1441 pPkt: new C4ControlVoteEnd(eType, false, iData),
1442 eDelivery: CDT_Sync);
1443 }
1444}
1445
1446void C4ControlVote::CompileFunc(StdCompiler *pComp)
1447{
1448 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntAdaptT<uint8_t>(rValue&: eType), szName: "Type", rDefault: VT_None));
1449 pComp->Value(rStruct: mkNamingAdapt(rValue&: fApprove, szName: "Approve", rDefault: true));
1450 pComp->Value(rStruct: mkNamingAdapt(rValue&: iData, szName: "Data", rDefault: 0));
1451 C4ControlPacket::CompileFunc(pComp);
1452}
1453
1454// *** C4ControlVoteEnd
1455
1456void C4ControlVoteEnd::Execute(const std::shared_ptr<spdlog::logger> &) const
1457{
1458 // End the voting process
1459 if (!HostControl()) return;
1460 if (Game.Network.isEnabled())
1461 Game.Network.EndVote(eType: getType(), fApprove: isApprove(), iData: getData());
1462 // Log
1463 const std::string msg{LoadResStrChoice(condition: isApprove(), ifTrue: C4ResStrTableKey::IDS_TEXT_ITWASDECIDEDTO, ifFalse: C4ResStrTableKey::IDS_TEXT_ITWASDECIDEDNOTTO, args: getDesc().getData())};
1464 LogNTr(message: msg);
1465 // Approved?
1466 if (!isApprove()) return;
1467 // Do it
1468 C4ClientPlayerInfos *pInfos; C4PlayerInfo *pInfo;
1469 int iClient, iInfo;
1470 switch (getType())
1471 {
1472 case VT_Cancel:
1473 // Flag players
1474 if (!Game.GameOver)
1475 for (iClient = 0; pInfos = Game.PlayerInfos.GetIndexedInfo(iIndex: iClient); iClient++)
1476 for (iInfo = 0; pInfo = pInfos->GetPlayerInfo(iIndex: iInfo); iInfo++)
1477 if (!pInfo->IsRemoved())
1478 pInfo->SetVotedOut();
1479 // Abort the game
1480 Game.Abort(fApproved: true);
1481 break;
1482 case VT_Kick:
1483 // Flag players
1484 pInfos = Game.PlayerInfos.GetInfoByClientID(iClientID: getData());
1485 if (!Game.GameOver)
1486 if (pInfos)
1487 for (iInfo = 0; pInfo = pInfos->GetPlayerInfo(iIndex: iInfo); iInfo++)
1488 if (!pInfo->IsRemoved())
1489 pInfo->SetVotedOut();
1490 // Remove the client
1491 if (Game.Control.isCtrlHost())
1492 {
1493 C4Client *pClient = Game.Clients.getClientByID(iID: getData());
1494 if (pClient)
1495 Game.Clients.CtrlRemove(pClient, szReason: LoadResStr(id: C4ResStrTableKey::IDS_VOTE_VOTEDOUT));
1496 }
1497 // It is ourselves that have been voted out?
1498 if (getData() == Game.Clients.getLocalID())
1499 {
1500 // otherwise, we have been kicked by the host.
1501 // Do a regular disconnect and display reason in game over dialog, so the client knows what has happened!
1502 Game.RoundResults.EvaluateNetwork(eResult: C4RoundResults::NR_NetError, szResultsString: LoadResStr(id: C4ResStrTableKey::IDS_ERR_YOUHAVEBEENREMOVEDBYVOTIN, args: msg).c_str());
1503 Game.Network.Clear();
1504 // Game over immediately, so poor player won't continue game alone
1505 Game.DoGameOver();
1506 }
1507 break;
1508 case VT_None:
1509 assert(!"C4ControlVoteEnd of type VT_None");
1510 break;
1511 case VT_Pause:
1512 // handled elsewhere
1513 break;
1514 }
1515}
1516
1517void C4ControlVoteEnd::CompileFunc(StdCompiler *pComp)
1518{
1519 C4ControlVote::CompileFunc(pComp);
1520}
1521
1522// *** internal script replacements
1523
1524void C4ControlEMDropDef::CompileFunc(StdCompiler *pComp)
1525{
1526 pComp->Value(rStruct: mkNamingAdapt(rValue: mkC4IDAdapt(rValue&: id), szName: "ID", rDefault: 0));
1527 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: x), szName: "X", rDefault: 0));
1528 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: y), szName: "Y", rDefault: 0));
1529 C4ControlInternalScriptBase::CompileFunc(pComp);
1530}
1531
1532bool C4ControlEMDropDef::Allowed() const
1533{
1534 return !Game.Parameters.isLeague() && C4Id2Def(id);
1535}
1536
1537std::string C4ControlEMDropDef::FormatScript() const
1538{
1539 const auto def = C4Id2Def(id);
1540 if (def->Category & C4D_Structure)
1541 return std::format(fmt: "CreateConstruction({},{},{},-1,{},true)", args: C4IdText(id), args: x, args: y, args: FullCon);
1542 else
1543 return std::format(fmt: "CreateObject({},{},{},-1)", args: C4IdText(id), args: x, args: y);
1544}
1545
1546void C4ControlInternalScriptBase::Execute(const std::shared_ptr<spdlog::logger> &) const
1547{
1548 if (!Allowed()) return;
1549
1550 const auto scope = Scope();
1551 // execute
1552 C4Object *pObj = nullptr;
1553 C4AulScript *pScript;
1554 if (scope == C4ControlScript::SCOPE_Console)
1555 pScript = &Game.Script;
1556 else if (scope == C4ControlScript::SCOPE_Global)
1557 pScript = &Game.ScriptEngine;
1558 else if (pObj = Game.Objects.SafeObjectPointer(iNumber: scope))
1559 pScript = &pObj->Def->Script;
1560 else
1561 // default: Fallback to global context
1562 pScript = &Game.ScriptEngine;
1563 pScript->DirectExec(pObj, szScript: FormatScript().c_str(), szContext: "internal script");
1564}
1565
1566void C4ControlInternalPlayerScriptBase::CompileFunc(StdCompiler *pComp)
1567{
1568 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: plr), szName: "Plr", rDefault: NO_OWNER));
1569 C4ControlInternalScriptBase::CompileFunc(pComp);
1570}
1571
1572bool C4ControlInternalPlayerScriptBase::Allowed() const
1573{
1574 if (plr == NO_OWNER) return true;
1575
1576 const auto player = Game.Players.Get(iPlayer: plr);
1577 return player && player->AtClient == iByClient;
1578}
1579
1580void C4ControlMessageBoardAnswer::CompileFunc(StdCompiler *pComp)
1581{
1582 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: obj), szName: "Object", rDefault: 0));
1583 pComp->Value(rStruct: mkNamingAdapt(rValue&: answer, szName: "Answer", rDefault: ""));
1584 C4ControlInternalPlayerScriptBase::CompileFunc(pComp);
1585}
1586
1587std::string C4ControlMessageBoardAnswer::FormatScript() const
1588{
1589 if (answer.empty()) return std::format(fmt: "OnMessageBoardAnswer(Object({}),{},)", args: obj, args: plr);
1590
1591 StdStrBuf escapedAnswer(answer.c_str(), false);
1592 escapedAnswer.EscapeString();
1593 return std::format(fmt: "OnMessageBoardAnswer(Object({}),{},\"{}\")", args: obj, args: plr, args: escapedAnswer.getData());
1594}
1595
1596void C4ControlCustomCommand::CompileFunc(StdCompiler *pComp)
1597{
1598 pComp->Value(rStruct: mkNamingAdapt(rValue&: command, szName: "Command", rDefault: ""));
1599 pComp->Value(rStruct: mkNamingAdapt(rValue&: argument, szName: "Argument", rDefault: ""));
1600 C4ControlInternalPlayerScriptBase::CompileFunc(pComp);
1601}
1602
1603bool C4ControlCustomCommand::Allowed() const
1604{
1605 if (!C4ControlInternalPlayerScriptBase::Allowed()) return false;
1606
1607 return Game.IsRunning && Game.MessageInput.GetCommand(strName: command.c_str());
1608}
1609
1610std::string C4ControlCustomCommand::FormatScript() const
1611{
1612 // the existence of cmd is checked already in Allowed()
1613 const auto cmd = Game.MessageInput.GetCommand(strName: command.c_str());
1614 std::string script;
1615 std::string cmdScript;
1616 // replace %player% by calling player number
1617 if (cmd->script.contains(x: "%player%"))
1618 {
1619 cmdScript = ReplaceInString(subject: static_cast<std::string_view>(cmd->script), needle: static_cast<std::string_view>("%player%"), value: static_cast<std::string_view>(std::to_string(val: plr)));
1620 }
1621 else
1622 {
1623 cmdScript = cmd->script;
1624 }
1625 // insert parameters
1626 if (cmdScript.contains(x: "%d"))
1627 {
1628 // make sure it's a number by converting
1629 std::string_view arg{argument};
1630 if (arg.starts_with(x: '+'))
1631 {
1632 arg.remove_prefix(n: 1);
1633 }
1634
1635 int result{};
1636 std::from_chars(first: arg.data(), last: arg.data() + arg.size(), value&: result);
1637 script = fmt::sprintf(fmt: cmdScript, args: result);
1638 }
1639 else if (cmdScript.contains(x: "%s"))
1640 {
1641 // Unrestricted parameters?
1642 // That's kind of a security risk as it will allow anyone to execute code
1643 switch (cmd->restriction)
1644 {
1645 case C4MessageBoardCommand::C4MSGCMDR_Escaped:
1646 {
1647 // escape strings
1648 StdStrBuf Par;
1649 Par.Copy(pnData: argument.c_str());
1650 Par.EscapeString();
1651 // compose script
1652 script = fmt::sprintf(fmt: cmdScript, args: Par.getData());
1653 }
1654 break;
1655
1656 case C4MessageBoardCommand::C4MSGCMDR_Plain:
1657 // unescaped
1658 script = fmt::sprintf(fmt: cmdScript, args: argument);
1659 break;
1660
1661 case C4MessageBoardCommand::C4MSGCMDR_Identifier:
1662 {
1663 // only allow identifier-characters
1664 std::string par;
1665 for (const auto c : argument)
1666 {
1667 if (!(IsIdentifier(cChar: c) || isspace(static_cast<unsigned char>(c)))) break;
1668 par.push_back(c: c);
1669 }
1670 // compose script
1671 script = fmt::sprintf(fmt: cmdScript, args: par);
1672 }
1673 break;
1674 }
1675 }
1676 else
1677 {
1678 return cmdScript;
1679 }
1680
1681 return script;
1682}
1683
1684void C4ControlInitScenarioPlayer::CompileFunc(StdCompiler *pComp)
1685{
1686 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: team), szName: "Team", rDefault: 0));
1687 C4ControlInternalPlayerScriptBase::CompileFunc(pComp);
1688}
1689
1690void C4ControlToggleHostility::CompileFunc(StdCompiler *pComp)
1691{
1692 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: opponent), szName: "Opponent", rDefault: NO_OWNER));
1693 C4ControlInternalPlayerScriptBase::CompileFunc(pComp);
1694}
1695
1696void C4ControlActivateGameGoalRule::CompileFunc(StdCompiler *pComp)
1697{
1698 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: obj), szName: "Object", rDefault: 0));
1699 C4ControlInternalPlayerScriptBase::CompileFunc(pComp);
1700}
1701
1702void C4ControlSetPlayerTeam::CompileFunc(StdCompiler *pComp)
1703{
1704 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: team), szName: "Team", rDefault: 0));
1705 C4ControlInternalPlayerScriptBase::CompileFunc(pComp);
1706}
1707
1708void C4ControlScript::CheckStrictness(const C4AulScriptStrict strict, StdCompiler &comp)
1709{
1710 if (!Inside(ival: std::to_underlying(value: strict), lbound: std::to_underlying(value: C4AulScriptStrict::NONSTRICT), rbound: std::to_underlying(value: C4AulScriptStrict::MAXSTRICT)))
1711 {
1712 comp.excCorrupt(message: "Invalid strictness: {}", args: std::to_underlying(value: strict));
1713 }
1714}
1715