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/* Player data at runtime */
18
19#include <C4Include.h>
20#include <C4Player.h>
21
22#include <C4Application.h>
23#include <C4Object.h>
24#include <C4ObjectInfo.h>
25#include <C4Command.h>
26#include <C4Network2Stats.h>
27#include <C4MessageInput.h>
28#include <C4GamePadCon.h>
29#include <C4Wrappers.h>
30#include <C4Random.h>
31#include <C4Log.h>
32#include <C4FullScreen.h>
33#include <C4GameOverDlg.h>
34#include <C4ObjectMenu.h>
35
36static constexpr std::int32_t C4FOW_Def_View_RangeX{500};
37
38C4Player::C4Player() : C4PlayerInfoCore()
39{
40 Default();
41}
42
43C4Player::~C4Player()
44{
45 Clear();
46}
47
48bool C4Player::ObjectInCrew(C4Object *tobj)
49{
50 C4Object *cobj; C4ObjectLink *clnk;
51 if (!tobj) return false;
52 for (clnk = Crew.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next)
53 if (cobj == tobj) return true;
54 return false;
55}
56
57void C4Player::ClearPointers(C4Object *pObj, bool fDeath)
58{
59 // Game
60 if (Captain == pObj) Captain = nullptr;
61 // Crew
62 while (Crew.Remove(pObj));
63 // Cursor
64 if (Cursor == pObj)
65 {
66 // object is to be deleted; do NOT do script calls (like in Cursor->UnSelect(true))
67 Cursor = nullptr; AdjustCursorCommand(); // also selects and eventually does a script call!
68 }
69 // View-Cursor
70 if (ViewCursor == pObj) ViewCursor = nullptr;
71 // View
72 if (ViewTarget == pObj) ViewTarget = nullptr;
73 // FoW
74 // (do not clear locals!)
75 // no clear when death to do normal decay
76 if (!fDeath)
77 while (FoWViewObjs.Remove(pObj));
78 // Menu
79 Menu.ClearPointers(pObj);
80 // messageboard-queries
81 RemoveMessageBoardQuery(pForObj: pObj);
82}
83
84void C4Player::UpdateValue()
85{
86 int32_t lval = ValueGain, lobj = ObjectsOwned;
87 Value = 0; ObjectsOwned = 0;
88
89 // Points
90 Value += Points;
91
92 // Wealth
93 Value += Wealth;
94
95 // Asset all owned objects
96 C4Object *cobj; C4ObjectLink *clnk;
97 for (clnk = Game.Objects.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next)
98 if (cobj->Owner == Number && cobj->Status)
99 {
100 ObjectsOwned++;
101 Value += cobj->GetValue(pInBase: nullptr, iForPlayer: Number);
102 }
103
104 // Value gain (always positive)
105 ValueGain = Value - InitialValue;
106
107 // Update
108 if ((ValueGain != lval) || (ObjectsOwned != lobj)) ViewValue = C4ViewDelay;
109}
110
111bool C4Player::ScenarioAndTeamInit(int32_t idTeam)
112{
113 C4PlayerInfo *pInfo = GetInfo();
114 if (!pInfo) return false;
115 C4Team *pTeam;
116 if (idTeam == TEAMID_New)
117 {
118 // creation of a new team only if allowed by scenario
119 if (!Game.Teams.IsAutoGenerateTeams())
120 pTeam = nullptr;
121 else
122 {
123 if (pTeam = Game.Teams.GetGenerateTeamByID(iID: idTeam)) idTeam = pTeam->GetID();
124 }
125 }
126 else
127 {
128 // uage of an existing team
129 pTeam = Game.Teams.GetTeamByID(iID: idTeam);
130 }
131 C4Team *pPrevTeam = Game.Teams.GetTeamByID(iID: Team);
132 // check if join to team is possible; e.g. not too many players
133 if (pPrevTeam != pTeam && idTeam)
134 {
135 if (!Game.Teams.IsJoin2TeamAllowed(idTeam))
136 {
137 pTeam = nullptr;
138 }
139 }
140 if (!pTeam && idTeam)
141 {
142 OnTeamSelectionFailed();
143 return false;
144 }
145 // team selection OK; execute it!
146 if (pPrevTeam) pPrevTeam->RemovePlayerByID(iID: pInfo->GetID());
147 if (pTeam) pTeam->AddPlayer(rInfo&: *pInfo, fAdjustPlayer: true);
148 if (!ScenarioInit()) return false;
149 if (Game.Rules & C4RULE_TeamHombase) SyncHomebaseMaterialFromTeam();
150 if (!FinalInit(fInitialValue: false)) return false;
151 return true;
152}
153
154void C4Player::Execute()
155{
156 if (!Status) return;
157
158 // Open/refresh team menu if desired
159 if (Status == PS_TeamSelection)
160 {
161 int32_t idSelectedTeam;
162 if (idSelectedTeam = Game.Teams.GetForcedTeamSelection(idForPlayer: ID))
163 {
164 // There's only one team left to join? Join there immediately.
165 if (Menu.IsActive() && Menu.GetIdentification() == C4MN_TeamSelection) Menu.TryClose(fOK: false, fControl: false);
166 if (LocalControl && !Game.Control.isReplay())
167 {
168 // team selection done through queue because TeamSelection-status may not be in sync (may be TeamSelectionPending!)
169 DoTeamSelection(idTeam: idSelectedTeam);
170 }
171 }
172 else if (!Menu.IsActive()) ActivateMenuTeamSelection(fFromMain: false);
173 else
174 {
175 // during team selection: Update view to selected team, if it has a position assigned
176 C4MenuItem *pSelectedTeamItem;
177 if (pSelectedTeamItem = Menu.GetSelectedItem())
178 {
179 int32_t idSelectedTeam = int32_t(pSelectedTeamItem->GetC4ID());
180 if (idSelectedTeam)
181 {
182 C4Team *pSelectedTeam;
183 if (pSelectedTeam = Game.Teams.GetTeamByID(iID: idSelectedTeam))
184 {
185 int32_t iPlrStartIndex = pSelectedTeam->GetPlrStartIndex();
186 if (iPlrStartIndex && Inside<int32_t>(ival: iPlrStartIndex, lbound: 1, rbound: C4S_MaxPlayer))
187 {
188 if (Game.C4S.PlrStart[iPlrStartIndex - 1].Position[0] > -1)
189 {
190 // player has selected a team that has a valid start position assigned
191 // set view to this position!
192 ViewX = Game.C4S.PlrStart[iPlrStartIndex - 1].Position[0] * Game.Landscape.MapZoom;
193 ViewY = Game.C4S.PlrStart[iPlrStartIndex - 1].Position[1] * Game.Landscape.MapZoom;
194 }
195 }
196 }
197 }
198 }
199 }
200 }
201 else if (Menu.IsActive() && Menu.GetIdentification() == C4MN_TeamSelection)
202 {
203 Menu.TryClose(fOK: false, fControl: false);
204 }
205
206 // Tick1
207 UpdateCounts();
208 UpdateView();
209 ExecuteControl();
210 Menu.Execute();
211 if (Cursor)
212 Cursor->AutoContextMenu(iMenuSelect: -1);
213
214 // decay of dead viewtargets
215 C4ObjectLink *pLnkNext = FoWViewObjs.First, *pLnk;
216 while (pLnk = pLnkNext)
217 {
218 pLnkNext = pLnk->Next;
219 C4Object *pDeadClonk = pLnk->Obj;
220 if (!pDeadClonk->GetAlive() && (pDeadClonk->Category & C4D_Living) && pDeadClonk->Status)
221 {
222 pDeadClonk->PlrViewRange -= 10;
223 if (pDeadClonk->PlrViewRange <= 0)
224 FoWViewObjs.Remove(pObj: pDeadClonk);
225 }
226 }
227
228 // Tick35
229 if (!Tick35 && Status == PS_Normal)
230 {
231 ExecHomeBaseProduction();
232 UpdateValue();
233 CheckElimination();
234 if (pMsgBoardQuery && LocalControl) ExecMsgBoardQueries();
235 }
236
237 // Delays
238 if (MessageStatus > 0) MessageStatus--;
239 if (RetireDelay > 0) RetireDelay--;
240 if (ViewWealth > 0) ViewWealth--;
241 if (ViewValue > 0) ViewValue--;
242 if (CursorFlash > 0) CursorFlash--;
243 if (SelectFlash > 0) SelectFlash--;
244}
245
246bool C4Player::Init(int32_t iNumber, int32_t iAtClient, const char *szAtClientName,
247 const char *szFilename, bool fScenarioInit, class C4PlayerInfo *pInfo)
248{
249 // safety
250 if (!pInfo)
251 {
252 spdlog::error(fmt: "Init player {} failed: No info!", args&: szFilename);
253 assert(false);
254 return false;
255 }
256 // Status init
257 Status = PS_Normal;
258 if (szFilename) SCopy(szSource: szFilename, sTarget: Filename); else *Filename = '\0';
259 Number = iNumber;
260 ID = pInfo->GetID();
261 Team = pInfo->GetTeam();
262 NoEliminationCheck = pInfo->IsNoEliminationCheck();
263
264 // At client
265 AtClient = iAtClient; SCopy(szSource: szAtClientName, sTarget: AtClientName, iMaxL: C4MaxTitle);
266
267 if (szFilename)
268 {
269 // Load core & crew info list
270 // do not load portraits for remote players
271 // this will prevent portraits from being shown for "remotely controlled"-Clonks of other players
272 bool fLoadPortraits = (AtClient == C4ClientIDUnknown) || SEqualNoCase(szStr1: AtClientName, szStr2: Game.Clients.getLocalName());
273 // fLoadPortraits = true
274 if (!Load(szFilename, fSavegame: !fScenarioInit, fLoadPortraits)) return false;
275 }
276 else
277 {
278 // no core file present: Keep defaults
279 // This can happen for script players only
280 assert(pInfo->GetType() == C4PT_Script);
281 }
282
283 // Take player name from player info; forcing overloads by the league or because of doubled player names
284 Name.Copy(pnData: pInfo->GetName());
285
286 // view pos init: Start at center pos
287 ViewX = GBackWdt / 2; ViewY = GBackHgt / 2;
288
289 // Scenario init
290 if (fScenarioInit)
291 {
292 // mark player join in player info list
293 // for non-scenarioinit, player should already be marked as joined
294 pInfo->SetJoined(iNumber);
295
296 // Number might have changed: Recheck list sorting before scenarioinit, which will do script calls
297 Game.Players.RecheckPlayerSort(pForPlayer: this);
298
299 // check for a postponed scenario init, if no team is specified (post-lobby-join in network, or simply non-network)
300 C4Team *pTeam = nullptr;
301 if (Team)
302 {
303 if (Game.Teams.IsAutoGenerateTeams())
304 pTeam = Game.Teams.GetGenerateTeamByID(iID: Team);
305 else
306 pTeam = Game.Teams.GetTeamByID(iID: Team);
307 }
308 if (!pTeam && Game.Teams.IsRuntimeJoinTeamChoice())
309 {
310 if (pInfo->GetType() == C4PT_Script)
311 {
312 // script player without team: This can usually not happen, because RecheckPlayerInfoTeams should have been executed
313 // just leave this player without the team
314 assert(false);
315 }
316 else
317 {
318 // postponed init: Chose team first
319 Status = PS_TeamSelection;
320 }
321 }
322
323 // Init control method before scenario init, because script callbacks may need to know it!
324 InitControl();
325
326 // Special: Script players may skip scenario initialization altogether, and just desire a single callback to all objects
327 // of a given ID
328 if (!pInfo->IsScenarioInitDesired())
329 {
330 // only initialization that's done anyway is team hostility
331 if (Team) SetTeamHostility();
332 // ...and home base material from team
333 if (Game.Rules & C4RULE_TeamHombase) SyncHomebaseMaterialFromTeam();
334 // callback definition passed?
335 C4ID idCallback = pInfo->GetScriptPlayerExtraID();
336 C4Def *pDefCallback;
337 if (idCallback && (pDefCallback = C4Id2Def(id: idCallback)))
338 {
339 pDefCallback->Script.Call(PSF_InitializeScriptPlayer, pPars: {C4VInt(iVal: Number), C4VInt(iVal: Team)});
340 }
341 }
342 else
343 {
344 // player preinit: In case a team needs to be chosen first, no InitializePlayer-broadcast is done
345 // this callback shall give scripters a chance to do stuff like starting an intro or enabling FoW, which might need to be done
346 Game.Script.GRBroadcast(PSF_PreInitializePlayer, pPars: {C4VInt(iVal: Number)});
347 // direct init
348 if (Status != PS_TeamSelection) if (!ScenarioInit()) return false;
349 // ...and home base material from team
350 if (Game.Rules & C4RULE_TeamHombase) SyncHomebaseMaterialFromTeam();
351 }
352 }
353
354 // Load runtime data
355 else
356 {
357 assert(pInfo->IsJoined());
358 // (compile using DefaultRuntimeData) - also check if compilation returned sane results, i.e. ID assigned
359 if (!LoadRuntimeData(hGroup&: Game.ScenarioFile) || !ID)
360 {
361 // for script players in non-savegames, this is OK - it means they get restored using default values
362 // this happens when the users saves a scenario using the "Save scenario"-option while a script player
363 // was joined
364 if (!Game.C4S.Head.SaveGame && pInfo->GetType() == C4PT_Script)
365 {
366 Number = pInfo->GetInGameNumber();
367 ColorDw = pInfo->GetColor();
368 ID = pInfo->GetID();
369 Team = pInfo->GetTeam();
370 }
371 else
372 return false;
373 }
374 // Reset values default-overriden by old runtime data load (safety?)
375 if (Number == C4P_Number_None) Number = iNumber;
376 if (szFilename) SCopy(szSource: szFilename, sTarget: Filename); else *Filename = '\0';
377 // NET2: Direct joins always send accurate client IDs and names in params
378 // do not overwrite them with savegame data, because players might as well
379 // change clients
380 // (only call should be savegame recreation by C4PlayerInfoList::RecreatePlayers)
381 AtClient = iAtClient;
382 SCopy(szSource: szAtClientName, sTarget: AtClientName, iMaxL: C4MaxTitle);
383 // Number might have changed: Recheck list sorting
384 Game.Players.RecheckPlayerSort(pForPlayer: this);
385 // Init control after loading runtime data, because control init will overwrite some of the values
386 InitControl();
387 }
388
389 // store game joining time
390 GameJoinTime = Game.Time;
391
392 // Init FoW-viewobjects: NO_OWNER-FoW-repellers might need to be added
393 for (C4ObjectLink *pLnk = Game.Objects.First; pLnk; pLnk = pLnk->Next)
394 {
395 C4Object *pObj = pLnk->Obj;
396 if (pObj->PlrViewRange && pObj->Owner == NO_OWNER)
397 pObj->PlrFoWActualize();
398 }
399
400 // init graphs
401 if (Game.pNetworkStatistics) CreateGraphs();
402
403 return true;
404}
405
406bool C4Player::Save()
407{
408 C4Group hGroup;
409 // Regular player saving need not be done for script players
410 if (GetType() == C4PT_Script) return false;
411 // Remote players need not be saved if they cannot resume
412 if (!LocalControl)
413 {
414 // This is the case in league
415 if (Game.Parameters.isLeague()) return false;
416 // And also if max player count is set to zero to prevent runtime joins
417 if (Game.Parameters.MaxPlayers <= 0) return false;
418 }
419 // Log
420 Log(id: C4ResStrTableKey::IDS_PRC_SAVEPLR, args: Config.AtExeRelativePath(szFilename: Filename));
421 Game.GraphicsSystem.MessageBoard.EnsureLastMessage();
422 // copy player to save somewhere else
423 char szPath[_MAX_PATH + 1];
424 SCopy(szSource: Config.AtTempPath(C4CFN_TempPlayer), sTarget: szPath, _MAX_PATH);
425 MakeTempFilename(szFileName: szPath);
426 // For local players, we save over the old player file, as there might
427 // be all kinds of non-essential stuff in it. For non-local players, we
428 // just re-create it every time (it's temporary anyway).
429 if (LocalControl)
430 {
431 // But make sure to copy it first so full hard (flgr stupid) disks
432 // won't corrupt any player files...
433 C4Group_CopyItem(szSource: Filename, szTarget: szPath);
434 }
435 else
436 {
437 // For non-local players, we can actually use the loaded definition
438 // list to strip out all non-existent definitions. This is only valid
439 // because we know the file to be temporary.
440 CrewInfoList.Strip(rDefs&: Game.Defs);
441 }
442 // Open group
443 if (!hGroup.Open(szGroupName: szPath, fCreate: true))
444 return false;
445 // Save
446 if (!Save(hGroup, fSavegame: false, fStoreTiny: !LocalControl))
447 {
448 hGroup.Close(); return false;
449 }
450 // Close group
451 if (!hGroup.Close()) return false;
452 // resource
453 C4Network2Res::Ref pRes = Game.Network.ResList.getRefRes(szFile: Filename),
454 pDRes = nullptr;
455 bool fOfficial = pRes && Game.Control.isCtrlHost();
456 if (pRes) pDRes = pRes->Derive();
457 // move back
458 if (ItemExists(szItemName: Filename)) EraseItem(szItemName: Filename);
459 if (!C4Group_MoveItem(szSource: szPath, szTarget: Filename)) return false;
460 // finish update
461 if (pDRes && fOfficial) pDRes->FinishDerive();
462 // Success
463 return true;
464}
465
466bool C4Player::Save(C4Group &hGroup, bool fSavegame, bool fStoreTiny)
467{
468 // Save core
469 if (!C4PlayerInfoCore::Save(hGroup))
470 return false;
471 // Save crew
472 if (!CrewInfoList.Save(hGroup, fSavegame, fStoreTiny, pDefs: &Game.Defs))
473 {
474 hGroup.Close(); return false;
475 }
476 // Sort
477 hGroup.Sort(C4FLS_Player);
478 return true;
479}
480
481void C4Player::PlaceReadyCrew(int32_t tx1, int32_t tx2, int32_t ty, C4Object *FirstBase)
482{
483 int32_t cnt, crewnum, ctx, cty;
484 C4Object *nobj;
485 C4ObjectInfo *pInfo;
486 C4Def *pDef;
487
488 // Set name source
489 const char *cpNames = Game.Names.GetData();
490
491 // Old specification
492 if (Game.C4S.PlrStart[PlrStartIndex].ReadyCrew.IsClear())
493 {
494 // Target number of ready crew
495 crewnum = Game.C4S.PlrStart[PlrStartIndex].Crew.Evaluate();
496 // Place crew
497 for (cnt = 0; cnt < crewnum; cnt++)
498 {
499 // Set standard crew
500 C4ID idStdCrew = Game.C4S.PlrStart[PlrStartIndex].NativeCrew;
501 // Select member from home crew, add new if necessary
502 while (!(pInfo = CrewInfoList.GetIdle(c_id: idStdCrew, rDefs&: Game.Defs)))
503 if (!CrewInfoList.New(n_id: idStdCrew, pDefs: &Game.Defs, cpNames))
504 break;
505 // Crew placement location
506 if (!pInfo || !(pDef = C4Id2Def(id: pInfo->id))) continue;
507 ctx = tx1 + Random(iRange: tx2 - tx1); cty = ty;
508 if (!Game.C4S.PlrStart[PlrStartIndex].EnforcePosition)
509 FindSolidGround(rx&: ctx, ry&: cty, width: pDef->Shape.Wdt * 3);
510 // Create object
511 if (nobj = Game.CreateInfoObject(cinf: pInfo, owner: Number, tx: ctx, ty: cty))
512 {
513 // Add object to crew
514 Crew.Add(nObj: nobj, eSort: C4ObjectList::stMain);
515 // add visibility range
516 nobj->SetPlrViewRange(C4FOW_Def_View_RangeX);
517 // If base is present, enter base
518 if (FirstBase) { nobj->Enter(pTarget: FirstBase); nobj->SetCommand(iCommand: C4CMD_Exit); }
519 // OnJoinCrew callback
520#ifndef DEBUGREC_RECRUITMENT
521 C4DebugRecOff DBGRECOFF;
522#endif
523 nobj->Call(PSF_OnJoinCrew, pPars: {C4VInt(iVal: Number)});
524 }
525 }
526 }
527
528 // New specification
529 else
530 {
531 // Place crew
532 int32_t id, iCount;
533 for (cnt = 0; id = Game.C4S.PlrStart[PlrStartIndex].ReadyCrew.GetID(index: cnt, ipCount: &iCount); cnt++)
534 {
535 // Minimum one clonk if empty id
536 iCount = std::max<int32_t>(a: iCount, b: 1);
537
538 for (int32_t cnt2 = 0; cnt2 < iCount; cnt2++)
539 {
540 // Select member from home crew, add new if necessary
541 while (!(pInfo = CrewInfoList.GetIdle(c_id: id, rDefs&: Game.Defs)))
542 if (!CrewInfoList.New(n_id: id, pDefs: &Game.Defs, cpNames))
543 break;
544 // Safety
545 if (!pInfo || !(pDef = C4Id2Def(id: pInfo->id))) continue;
546 // Crew placement location
547 ctx = tx1 + Random(iRange: tx2 - tx1); cty = ty;
548 if (!Game.C4S.PlrStart[PlrStartIndex].EnforcePosition)
549 FindSolidGround(rx&: ctx, ry&: cty, width: pDef->Shape.Wdt * 3);
550 // Create object
551 if (nobj = Game.CreateInfoObject(cinf: pInfo, owner: Number, tx: ctx, ty: cty))
552 {
553 // Add object to crew
554 Crew.Add(nObj: nobj, eSort: C4ObjectList::stMain);
555 // add visibility range
556 nobj->SetPlrViewRange(C4FOW_Def_View_RangeX);
557 // If base is present, enter base
558 if (FirstBase) { nobj->Enter(pTarget: FirstBase); nobj->SetCommand(iCommand: C4CMD_Exit); }
559 // OnJoinCrew callback
560 {
561#ifndef DEBUGREC_RECRUITMENT
562 C4DebugRecOff DbgRecOff;
563#endif
564 nobj->Call(PSF_OnJoinCrew, pPars: {C4VInt(iVal: Number)});
565 }
566 }
567 }
568 }
569 }
570}
571
572C4Object *CreateLine(C4ID linetype, int32_t owner, C4Object *fobj, C4Object *tobj);
573
574bool CreatePowerConnection(C4Object *fbase, C4Object *tbase)
575{
576 if (CreateLine(linetype: C4ID_PowerLine, owner: fbase->Owner, fobj: fbase, tobj: tbase)) return true;
577 return false;
578}
579
580void C4Player::PlaceReadyBase(int32_t &tx, int32_t &ty, C4Object **pFirstBase)
581{
582 int32_t cnt, cnt2, ctx, cty;
583 C4Def *def;
584 C4ID cid;
585 C4Object *cbase, *fpower = nullptr;
586 // Create ready base structures
587 for (cnt = 0; (cid = Game.C4S.PlrStart[PlrStartIndex].ReadyBase.GetID(index: cnt)); cnt++)
588 {
589 if (def = C4Id2Def(id: cid))
590 for (cnt2 = 0; cnt2 < Game.C4S.PlrStart[PlrStartIndex].ReadyBase.GetCount(index: cnt); cnt2++)
591 {
592 ctx = tx; cty = ty;
593 if (Game.C4S.PlrStart[PlrStartIndex].EnforcePosition
594 || FindConSiteSpot(rx&: ctx, ry&: cty, wdt: def->Shape.Wdt, hgt: def->Shape.Hgt, category: def->Category, hrange: 20))
595 if (cbase = Game.CreateObjectConstruction(type: cid, pCreator: nullptr, owner: Number, ctx, bty: cty, con: FullCon, terrain: true))
596 {
597 // FirstBase
598 if (!(*pFirstBase)) if (cbase->Def->CanBeBase)
599 {
600 *pFirstBase = cbase; tx = (*pFirstBase)->x; ty = (*pFirstBase)->y;
601 }
602 // First power plant
603 if (cbase->Def->LineConnect & C4D_Power_Generator)
604 if (!fpower) fpower = cbase;
605 }
606 }
607 }
608
609 // Power connections
610 C4ObjectLink *clnk; C4Object *cobj;
611 if (Game.Rules & C4RULE_StructuresNeedEnergy)
612 if (fpower)
613 for (clnk = Game.Objects.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next)
614 if (cobj->Owner == Number)
615 if (cobj->Def->LineConnect & C4D_Power_Consumer)
616 CreatePowerConnection(fbase: fpower, tbase: cobj);
617}
618
619void C4Player::PlaceReadyVehic(int32_t tx1, int32_t tx2, int32_t ty, C4Object *FirstBase)
620{
621 int32_t cnt, cnt2, ctx, cty;
622 C4Def *def; C4ID cid; C4Object *cobj;
623 for (cnt = 0; (cid = Game.C4S.PlrStart[PlrStartIndex].ReadyVehic.GetID(index: cnt)); cnt++)
624 {
625 if (def = C4Id2Def(id: cid))
626 for (cnt2 = 0; cnt2 < Game.C4S.PlrStart[PlrStartIndex].ReadyVehic.GetCount(index: cnt); cnt2++)
627 {
628 ctx = tx1 + Random(iRange: tx2 - tx1); cty = ty;
629 if (!Game.C4S.PlrStart[PlrStartIndex].EnforcePosition)
630 FindLevelGround(rx&: ctx, ry&: cty, width: def->Shape.Wdt, hrange: 6);
631 if (cobj = Game.CreateObject(type: cid, pCreator: nullptr, owner: Number, x: ctx, y: cty))
632 {
633 if (FirstBase) // First base overrides target location
634 {
635 cobj->Enter(pTarget: FirstBase); cobj->SetCommand(iCommand: C4CMD_Exit);
636 }
637 }
638 }
639 }
640}
641
642void C4Player::PlaceReadyMaterial(int32_t tx1, int32_t tx2, int32_t ty, C4Object *FirstBase)
643{
644 int32_t cnt, cnt2, ctx, cty;
645 C4Def *def; C4ID cid;
646
647 // In base
648 if (FirstBase)
649 {
650 FirstBase->CreateContentsByList(idlist&: Game.C4S.PlrStart[PlrStartIndex].ReadyMaterial);
651 }
652
653 // Outside
654 else
655 {
656 for (cnt = 0; (cid = Game.C4S.PlrStart[PlrStartIndex].ReadyMaterial.GetID(index: cnt)); cnt++)
657 {
658 if (def = C4Id2Def(id: cid))
659 for (cnt2 = 0; cnt2 < Game.C4S.PlrStart[PlrStartIndex].ReadyMaterial.GetCount(index: cnt); cnt2++)
660 {
661 ctx = tx1 + Random(iRange: tx2 - tx1); cty = ty;
662 if (!Game.C4S.PlrStart[PlrStartIndex].EnforcePosition)
663 FindSolidGround(rx&: ctx, ry&: cty, width: def->Shape.Wdt);
664 Game.CreateObject(type: cid, pCreator: nullptr, owner: Number, x: ctx, y: cty);
665 }
666 }
667 }
668}
669
670bool C4Player::ScenarioInit()
671{
672 int32_t ptx, pty;
673
674 // player start index by team, if specified. Otherwise by player number
675 PlrStartIndex = Number % C4S_MaxPlayer;
676 C4Team *pTeam; int32_t i;
677 if (Team && (pTeam = Game.Teams.GetTeamByID(iID: Team))) if (i = pTeam->GetPlrStartIndex()) PlrStartIndex = i - 1;
678
679 // Set color
680 int32_t iColor = BoundBy<int32_t>(bval: PrefColor, lbound: 0, rbound: C4MaxColor - 1);
681 while (Game.Players.ColorTaken(iColor))
682 {
683 ++iColor %= C4MaxColor; if (iColor == PrefColor) break;
684 }
685 Color = iColor;
686
687 C4PlayerInfo *pInfo = GetInfo();
688 if (!pInfo) { assert(false); spdlog::error(fmt: "Internal error: ScenarioInit for ghost player {}", args: GetName()); return false; }
689
690 // set color by player info class
691 // re-setting, because runtime team choice may have altered color
692 ColorDw = pInfo->GetColor();
693
694 // any team selection is over now
695 Status = PS_Normal;
696
697 // Wealth, home base materials, abilities
698 Wealth = Game.C4S.PlrStart[PlrStartIndex].Wealth.Evaluate();
699 HomeBaseMaterial = Game.C4S.PlrStart[PlrStartIndex].HomeBaseMaterial;
700 HomeBaseMaterial.ConsolidateValids(rDefs&: Game.Defs);
701 HomeBaseProduction = Game.C4S.PlrStart[PlrStartIndex].HomeBaseProduction;
702 HomeBaseProduction.ConsolidateValids(rDefs&: Game.Defs);
703 Knowledge = Game.C4S.PlrStart[PlrStartIndex].BuildKnowledge;
704 Knowledge.ConsolidateValids(rDefs&: Game.Defs);
705 Magic = Game.C4S.PlrStart[PlrStartIndex].Magic;
706 Magic.ConsolidateValids(rDefs&: Game.Defs);
707 if (Magic.IsClear()) Magic.Load(defs&: Game.Defs, category: C4D_Magic); // All magic default if empty
708 Magic.SortByValue(rDefs&: Game.Defs);
709
710 // Starting position
711 ptx = Game.C4S.PlrStart[PlrStartIndex].Position[0];
712 pty = Game.C4S.PlrStart[PlrStartIndex].Position[1];
713
714 // Zoomed position
715 if (ptx > -1) ptx = BoundBy<int32_t>(bval: ptx * Game.C4S.Landscape.MapZoom.Evaluate(), lbound: 0, GBackWdt - 1);
716 if (pty > -1) pty = BoundBy<int32_t>(bval: pty * Game.C4S.Landscape.MapZoom.Evaluate(), lbound: 0, GBackHgt - 1);
717
718 // Standard position (PrefPosition)
719 if (ptx < 0)
720 if (Game.Parameters.StartupPlayerCount >= 2)
721 {
722 int32_t iMaxPos = Game.Parameters.StartupPlayerCount;
723 // Map preferred position to available positions
724 int32_t iStartPos = BoundBy(bval: PrefPosition * iMaxPos / C4P_MaxPosition, lbound: 0, rbound: iMaxPos - 1);
725 int32_t iPosition = iStartPos;
726 // Distribute according to availability
727 while (Game.Players.PositionTaken(iPosition))
728 {
729 ++iPosition %= iMaxPos; if (iPosition == iStartPos) break;
730 }
731 Position = iPosition;
732 // Set x position
733 ptx = BoundBy(bval: 16 + Position * (GBackWdt - 32) / (iMaxPos - 1), lbound: 0, GBackWdt - 16);
734 }
735
736 // All-random position
737 if (ptx < 0) ptx = 16 + Random(GBackWdt - 32);
738 if (pty < 0) pty = 16 + Random(GBackHgt - 32);
739
740 // Place to solid ground
741 if (!Game.C4S.PlrStart[PlrStartIndex].EnforcePosition)
742 {
743 // Use nearest above-ground...
744 FindSolidGround(rx&: ptx, ry&: pty, width: 30);
745 // Might have hit a small lake, or similar: Seach a real site spot from here
746 FindConSiteSpot(rx&: ptx, ry&: pty, wdt: 30, hgt: 50, category: C4D_Structure, hrange: 400);
747 }
748
749 // Place Readies
750 C4Object *FirstBase = nullptr;
751 PlaceReadyBase(tx&: ptx, ty&: pty, pFirstBase: &FirstBase);
752 PlaceReadyMaterial(tx1: ptx - 10, tx2: ptx + 10, ty: pty, FirstBase);
753 PlaceReadyVehic(tx1: ptx - 30, tx2: ptx + 30, ty: pty, FirstBase);
754 PlaceReadyCrew(tx1: ptx - 30, tx2: ptx + 30, ty: pty, FirstBase);
755
756 // set initial hostility by team info
757 if (Team) SetTeamHostility();
758
759 // Mouse control: init fog of war, if not specified otherwise by script
760 if (MouseControl && !bForceFogOfWar)
761 if (!fFogOfWarInitialized)
762 {
763 fFogOfWar = fFogOfWarInitialized = true;
764 // reset view objects
765 Game.Objects.AssignPlrViewRange();
766 }
767
768 // Scenario script initialization
769 Game.Script.GRBroadcast(PSF_InitializePlayer, pPars: {C4VInt(iVal: Number),
770 C4VInt(iVal: ptx),
771 C4VInt(iVal: pty),
772 C4VObj(pObj: FirstBase),
773 C4VInt(iVal: Team),
774 C4VID(idVal: GetInfo()->GetScriptPlayerExtraID())});
775 return true;
776}
777
778bool C4Player::FinalInit(bool fInitialValue)
779{
780 if (!Status) return true;
781
782 // Init player's mouse control
783 if (LocalControl)
784 if (MouseControl)
785 Game.MouseControl.Init(iPlayer: Number);
786
787 // Set initial value
788 if (fInitialValue)
789 {
790 UpdateValue(); InitialValue = Value;
791 }
792
793 // Cursor
794 if (!Cursor) AdjustCursorCommand();
795
796 // Assign Captain
797 if (Game.Objects.Find(id: C4Id(str: "KILC")))
798 if (!Captain) Captain = GetHiRankActiveCrew(fSelectedOnly: false);
799
800 // Update counts, pointers, views, value
801 UpdateValue();
802 Execute();
803
804 // Restore FoW after savegame
805 if (fFogOfWar && !fFogOfWarInitialized)
806 {
807 fFogOfWarInitialized = true;
808 // reset view objects
809 Game.Objects.AssignPlrViewRange();
810 }
811
812 return true;
813}
814
815void C4Player::SetFoW(bool fEnable)
816{
817 // enable FoW
818 if (fEnable && !fFogOfWarInitialized)
819 Game.Objects.AssignPlrViewRange();
820 // set flag
821 fFogOfWar = fFogOfWarInitialized = fEnable;
822 // forced (not activated by mouse)
823 bForceFogOfWar = true;
824}
825
826C4Object *C4Player::Buy(C4ID id, bool fShowErrors, int32_t iForPlr, C4Object *pBuyObj)
827{
828 int32_t iAvailable; C4Def *pDef; C4Object *pThing;
829 // Base owner eliminated
830 if (Eliminated)
831 {
832 if (!fShowErrors) return nullptr;
833 StartSoundEffect(name: "Error", loop: false, volume: 100, obj: pBuyObj);
834 GameMsgPlayer(szText: LoadResStr(id: C4ResStrTableKey::IDS_PLR_ELIMINATED, args: GetName()).c_str(), iPlayer: Number); return nullptr;
835 }
836 // Get def (base owner's homebase material)
837 iAvailable = HomeBaseMaterial.GetIDCount(id);
838 if (!(pDef = C4Id2Def(id))) return nullptr;
839 // Object not available
840 if (iAvailable <= 0) return nullptr;
841 // get value
842 int32_t iValue = pDef->GetValue(pInBase: pBuyObj, iBuyPlayer: Number);
843 // Not enough wealth (base owner's wealth)
844 if (iValue > Wealth)
845 {
846 if (!fShowErrors) return nullptr;
847 GameMsgPlayer(szText: LoadResStr(id: C4ResStrTableKey::IDS_PLR_NOWEALTH), iPlayer: Number);
848 StartSoundEffect(name: "Error", loop: false, volume: 100, obj: pBuyObj); return nullptr;
849 }
850 // Decrease homebase material count
851 HomeBaseMaterial.DecreaseIDCount(id, removeEmpty: false);
852 if (Game.Rules & C4RULE_TeamHombase) SyncHomebaseMaterialToTeam();
853 // Reduce wealth
854 DoWealth(change: -iValue);
855 // Create object (for player)
856 if (!(pThing = Game.CreateObject(type: id, pCreator: pBuyObj, owner: iForPlr))) return nullptr;
857 // Make crew member
858 if (pDef->CrewMember) if (ValidPlr(plr: iForPlr))
859 Game.Players.Get(iPlayer: iForPlr)->MakeCrewMember(pObj: pThing);
860 // success
861 pThing->Call(PSF_Purchase, pPars: {C4VInt(iVal: Number), C4VObj(pObj: pBuyObj)});
862 if (!pThing->Status) return nullptr;
863 return pThing;
864}
865
866bool C4Player::Sell2Home(C4Object *pObj)
867{
868 C4Object *cObj;
869 if (!CanSell(obj: pObj)) return false;
870 // Sell contents first
871 while (cObj = pObj->Contents.GetObject())
872 {
873 if (pObj->Contained) cObj->Enter(pTarget: pObj->Contained); else cObj->Exit(iX: cObj->x, iY: cObj->y);
874 Sell2Home(pObj: cObj);
875 }
876 // Do transaction
877 DoWealth(change: +pObj->GetValue(pInBase: pObj->Contained, iForPlayer: Number));
878 // script handling of SellTo
879 C4ID id = pObj->Def->id;
880 C4Def *pSellDef = pObj->Def;
881 if (C4AulScriptFunc *f = pObj->Def->Script.SFn_SellTo)
882 {
883 id = f->Exec(pObj, pPars: {C4VInt(iVal: Number)}).getC4ID();
884 pSellDef = C4Id2Def(id);
885 }
886 // Add to homebase material
887 if (pSellDef)
888 {
889 HomeBaseMaterial.IncreaseIDCount(id, addNewID: !!pSellDef->Rebuyable);
890 if (Game.Rules & C4RULE_TeamHombase) SyncHomebaseMaterialToTeam();
891 }
892 // Remove object, eject any crew members
893 if (pObj->Contained) pObj->Exit();
894 pObj->Call(PSF_Sale, pPars: {C4VInt(iVal: Number)});
895 pObj->AssignRemoval(fExitContents: true);
896 // Done
897 return true;
898}
899
900bool C4Player::CanSell(C4Object *const obj) const
901{
902 return !Eliminated && obj && obj->Status && !(obj->OCF & OCF_CrewMember);
903}
904
905bool C4Player::DoWealth(int32_t iChange)
906{
907 Wealth = BoundBy<int32_t>(bval: Wealth + iChange, lbound: 0, rbound: 10000);
908 if (LocalControl)
909 {
910 if (iChange > 0) StartSoundEffect(name: "Cash");
911 if (iChange < 0) StartSoundEffect(name: "UnCash");
912 }
913 ViewWealth = C4ViewDelay;
914 return true;
915}
916
917void C4Player::SetViewMode(int32_t iMode, C4Object *pTarget)
918{
919 // safe back
920 ViewMode = iMode; ViewTarget = pTarget;
921}
922
923void C4Player::ResetCursorView()
924{
925 // reset view to cursor if any cursor exists
926 if (!ViewCursor && !Cursor) return;
927 SetViewMode(iMode: C4PVM_Cursor);
928}
929
930void C4Player::Evaluate()
931{
932 // do not evaluate twice
933 if (Evaluated) return;
934
935 const int32_t SuccessBonus = 100;
936
937 // Set last round
938 LastRound.Title = Game.Parameters.ScenarioTitle;
939 time(timer: reinterpret_cast<time_t *>(&LastRound.Date));
940 LastRound.Duration = Game.Time;
941 LastRound.Won = !Eliminated;
942 // Melee: personal value gain score ...check Game.Objects(C4D_Goal)
943 if (Game.C4S.Game.IsMelee()) LastRound.Score = std::max<int32_t>(a: ValueGain, b: 0);
944 // Cooperative: shared score
945 else LastRound.Score = (std::max)(a: Game.Players.AverageValueGain(), b: 0);
946 LastRound.Level = 0; // unknown...
947 LastRound.Bonus = SuccessBonus * LastRound.Won;
948 LastRound.FinalScore = LastRound.Score + LastRound.Bonus;
949 LastRound.TotalScore = Score + LastRound.FinalScore;
950
951 // Update player
952 Rounds++;
953 if (LastRound.Won) RoundsWon++; else RoundsLost++;
954 Score = LastRound.TotalScore;
955 TotalPlayingTime += Game.Time - GameJoinTime;
956
957 // Crew
958 CrewInfoList.Evaluate();
959
960 // league
961 if (Game.Parameters.isLeague())
962 EvaluateLeague(fDisconnected: false, fWon: Game.GameOver && !Eliminated);
963
964 // Player is now evaluated
965 Evaluated = true;
966
967 // round results
968 Game.RoundResults.EvaluatePlayer(pPlr: this);
969}
970
971void C4Player::Surrender()
972{
973 if (Surrendered) return;
974 Surrendered = true;
975 Eliminated = true;
976 RetireDelay = C4RetireDelay;
977 StartSoundEffect(name: "Eliminated");
978 Log(id: C4ResStrTableKey::IDS_PRC_PLRSURRENDERED, args: GetName());
979}
980
981bool C4Player::SetHostility(int32_t iOpponent, int32_t iHostility, bool fSilent)
982{
983 // Check opponent valid
984 if (!ValidPlr(plr: iOpponent) || (iOpponent == Number)) return false;
985 // Set hostility
986 Hostility.SetIDCount(id: iOpponent + 1, count: iHostility, addNewID: true);
987 // no announce in first frame, or if specified
988 if (!Game.FrameCounter || fSilent) return true;
989 // Announce
990 StartSoundEffect(name: "Trumpet");
991 if (iHostility)
992 {
993 Log(id: C4ResStrTableKey::IDS_PLR_HOSTILITY, args: GetName(), args: Game.Players.Get(iPlayer: iOpponent)->GetName());
994 }
995 else
996 {
997 Log(id: C4ResStrTableKey::IDS_PLR_NOHOSTILITY, args: GetName(), args: Game.Players.Get(iPlayer: iOpponent)->GetName());
998 }
999 // Success
1000 return true;
1001}
1002
1003C4Object *C4Player::GetHiRankActiveCrew(bool fSelectOnly)
1004{
1005 C4ObjectLink *clnk;
1006 C4Object *cobj, *hirank = nullptr;
1007 int32_t iHighestRank = -2, iRank;
1008 for (clnk = Crew.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next)
1009 if (!cobj->CrewDisabled)
1010 if (!fSelectOnly || cobj->Select)
1011 {
1012 if (cobj->Info) iRank = cobj->Info->Rank; else iRank = -1;
1013 if (!hirank || (iRank > iHighestRank))
1014 {
1015 hirank = cobj;
1016 iHighestRank = iRank;
1017 }
1018 }
1019 return hirank;
1020}
1021
1022void C4Player::SetTeamHostility()
1023{
1024 // team only
1025 if (!Team) return;
1026 // set hostilities
1027 for (C4Player *pPlr = Game.Players.First; pPlr; pPlr = pPlr->Next)
1028 if (pPlr != this)
1029 {
1030 bool fHostile = (pPlr->Team != Team);
1031 SetHostility(iOpponent: pPlr->Number, iHostility: fHostile, fSilent: true);
1032 pPlr->SetHostility(iOpponent: Number, iHostility: fHostile, fSilent: true);
1033 }
1034}
1035
1036void C4Player::Clear()
1037{
1038 ClearGraphs();
1039 Crew.Clear();
1040 CrewInfoList.Clear();
1041 Menu.Clear();
1042 BigIcon.Clear();
1043 fFogOfWar = false; bForceFogOfWar = false;
1044 FoWViewObjs.Clear();
1045 fFogOfWarInitialized = false;
1046 while (pMsgBoardQuery)
1047 {
1048 C4MessageBoardQuery *pNext = pMsgBoardQuery->pNext;
1049 delete pMsgBoardQuery;
1050 pMsgBoardQuery = pNext;
1051 }
1052 delete pGamepad;
1053 pGamepad = nullptr;
1054 Status = 0;
1055}
1056
1057void C4Player::Default()
1058{
1059 Filename[0] = 0;
1060 Status = 0;
1061 Number = C4P_Number_None;
1062 ID = 0;
1063 Team = 0;
1064 DefaultRuntimeData();
1065 Menu.Default();
1066 Crew.Default();
1067 CrewInfoList.Default();
1068 LocalControl = false;
1069 BigIcon.Default();
1070 Next = nullptr;
1071 fFogOfWar = false; fFogOfWarInitialized = false;
1072 bForceFogOfWar = false;
1073 FoWViewObjs.Default();
1074 LeagueEvaluated = false;
1075 GameJoinTime = 0; // overwritten in Init
1076 pstatControls = pstatActions = nullptr;
1077 ControlCount = ActionCount = 0;
1078 AutoContextMenu = ControlStyle = 0;
1079 LastControlType = PCID_None;
1080 LastControlID = 0;
1081 PressedComs = 0;
1082 pMsgBoardQuery = nullptr;
1083 pGamepad = nullptr;
1084 NoEliminationCheck = false;
1085 Evaluated = false;
1086 ColorDw = 0;
1087}
1088
1089bool C4Player::Load(const char *szFilename, bool fSavegame, bool fLoadPortraits)
1090{
1091 C4Group hGroup;
1092 // Open group
1093 if (!hGroup.Open(szGroupName: szFilename)) return false;
1094 // Load core
1095 if (!C4PlayerInfoCore::Load(hGroup))
1096 {
1097 hGroup.Close(); return false;
1098 }
1099 // Load BigIcon
1100 if (hGroup.FindEntry(C4CFN_BigIcon)) BigIcon.Load(hGroup, C4CFN_BigIcon);
1101 // Load crew info list
1102 CrewInfoList.Load(hGroup, fLoadPortraits);
1103 // Close group
1104 hGroup.Close();
1105 // Success
1106 return true;
1107}
1108
1109bool C4Player::Strip(const char *szFilename, bool fAggressive)
1110{
1111 // Opem group
1112 C4Group Grp;
1113 if (!Grp.Open(szGroupName: szFilename))
1114 return false;
1115 // Which type of stripping?
1116 if (!fAggressive)
1117 {
1118 // remove portrais
1119 Grp.Delete(C4CFN_Portraits, fRecursive: true);
1120 // remove bigicon, if the file size is too large
1121 size_t iBigIconSize = 0;
1122 if (Grp.FindEntry(C4CFN_BigIcon, sFileName: nullptr, iSize: &iBigIconSize))
1123 if (iBigIconSize > C4NetResMaxBigicon * 1024)
1124 Grp.Delete(C4CFN_BigIcon);
1125 Grp.Close();
1126 }
1127 else
1128 {
1129 // Load info core and crew info list
1130 C4PlayerInfoCore PlrInfoCore;
1131 C4ObjectInfoList CrewInfoList;
1132 if (!PlrInfoCore.Load(hGroup&: Grp) || !CrewInfoList.Load(hGroup&: Grp, fLoadPortraits: false))
1133 return false;
1134 // Strip crew info list (remove object infos that are invalid for this scenario)
1135 CrewInfoList.Strip(rDefs&: Game.Defs);
1136 // Create a new group that receives the bare essentials
1137 Grp.Close();
1138 if (!EraseItem(szItemName: szFilename) ||
1139 !Grp.Open(szGroupName: szFilename, fCreate: true))
1140 return false;
1141 // Save info core & crew info list to newly-created file
1142 if (!PlrInfoCore.Save(hGroup&: Grp) || !CrewInfoList.Save(hGroup&: Grp, fSavegame: true, fStoreTiny: true, pDefs: &Game.Defs))
1143 return false;
1144 Grp.Close();
1145 }
1146 return true;
1147}
1148
1149void C4Player::DrawHostility(C4Facet &cgo, int32_t iIndex)
1150{
1151 C4Player *pPlr;
1152 if (pPlr = Game.Players.GetByIndex(iIndex))
1153 {
1154 // Portrait
1155 if (Config.Graphics.ShowPortraits && pPlr->BigIcon.Surface)
1156 pPlr->BigIcon.Draw(cgo);
1157 // Standard player image
1158 else
1159 Game.GraphicsResource.fctCrewClr.DrawClr(cgo, fAspect: true, dwClr: pPlr->ColorDw);
1160 // Other player and hostile
1161 if (pPlr != this)
1162 if (Hostility.GetIDCount(id: pPlr->Number + 1))
1163 Game.GraphicsResource.fctMenu.GetPhase(iPhaseX: 7).Draw(cgo);
1164 }
1165}
1166
1167bool C4Player::MakeCrewMember(C4Object *pObj, bool fForceInfo, bool fDoCalls)
1168{
1169 C4ObjectInfo *cInf = nullptr;
1170 if (!pObj || !pObj->Def->CrewMember || !pObj->Status) return false;
1171
1172 // only if info is not yet assigned
1173 if (!pObj->Info && fForceInfo)
1174 {
1175 // Find crew info by name
1176 if (pObj->nInfo)
1177 cInf = CrewInfoList.GetIdle(szByName: pObj->nInfo.getData());
1178
1179 // Set name source
1180 const char *cpNames = nullptr;
1181 if (pObj->Def->pClonkNames) cpNames = pObj->Def->pClonkNames->GetData();
1182 if (!cpNames) cpNames = Game.Names.GetData();
1183
1184 // Find crew info by id
1185 if (!cInf)
1186 while (!(cInf = CrewInfoList.GetIdle(c_id: pObj->id, rDefs&: Game.Defs)))
1187 if (!CrewInfoList.New(n_id: pObj->id, pDefs: &Game.Defs, cpNames))
1188 return false;
1189
1190 // Set object info
1191 pObj->Info = cInf;
1192 }
1193
1194 // Add to crew
1195 if (!Crew.GetLink(pObj))
1196 Crew.Add(nObj: pObj, eSort: C4ObjectList::stMain);
1197
1198 // add plr view
1199 if (!pObj->PlrViewRange) pObj->SetPlrViewRange(C4FOW_Def_View_RangeX); else pObj->PlrFoWActualize();
1200
1201 // controlled by the player
1202 pObj->Controller = Number;
1203
1204 // OnJoinCrew callback
1205 if (fDoCalls)
1206 {
1207 pObj->Call(PSF_OnJoinCrew, pPars: {C4VInt(iVal: Number)});
1208 }
1209
1210 return true;
1211}
1212
1213void C4Player::ExecuteControl()
1214{
1215 // LastCom
1216 if (LastCom != COM_None)
1217 {
1218 // Advance delay counter
1219 LastComDelay++;
1220 // Check for COM_Single com (after delay)
1221 if (LastComDelay > C4DoubleClick)
1222 {
1223 // Pass additional COM_Single com (unless it already was a single com in the first place)
1224 if (!(LastCom & COM_Single))
1225 DirectCom(byCom: LastCom | COM_Single, iData: 0); // Currently, com data is not stored for single coms...
1226 LastCom = COM_None;
1227 LastComDelay = 0;
1228 }
1229 }
1230
1231 // LastComDownDouble
1232 if (LastComDownDouble > 0) LastComDownDouble--;
1233}
1234
1235void C4Player::AdjustCursorCommand()
1236{
1237 // Reset view
1238 ResetCursorView();
1239 // Set cursor to hirank Select clonk
1240 C4Object *pHiRank = nullptr;
1241 // Find hirank Select
1242 pHiRank = GetHiRankActiveCrew(fSelectOnly: true);
1243 // If none, check non-Selects as well
1244 if (!pHiRank)
1245 pHiRank = GetHiRankActiveCrew(fSelectOnly: false);
1246 // The cursor is on someone else: set the cursor to the hirank
1247 C4Object *pPrev = Cursor;
1248 if (Cursor != pHiRank)
1249 {
1250 Cursor = pHiRank;
1251 UpdateView();
1252 }
1253 // UnSelect previous cursor
1254 if (pPrev && pPrev != Cursor) pPrev->UnSelect(fCursor: true);
1255 // We have a cursor: do select it
1256 if (Cursor) Cursor->DoSelect();
1257 // Updates
1258 CursorFlash = 30;
1259}
1260
1261void C4Player::CursorRight()
1262{
1263 C4ObjectLink *cLnk;
1264 // Get next crew member
1265 if (cLnk = Crew.GetLink(pObj: Cursor))
1266 for (cLnk = cLnk->Next; cLnk; cLnk = cLnk->Next)
1267 if (cLnk->Obj->Status && !cLnk->Obj->CrewDisabled) break;
1268 if (!cLnk)
1269 for (cLnk = Crew.First; cLnk; cLnk = cLnk->Next)
1270 if (cLnk->Obj->Status && !cLnk->Obj->CrewDisabled) break;
1271 if (cLnk) SetCursor(pObj: cLnk->Obj, fSelectFlash: false, fSelectArrow: true);
1272 // Updates
1273 CursorFlash = 30;
1274 CursorSelection = 1;
1275 UpdateView();
1276}
1277
1278void C4Player::CursorLeft()
1279{
1280 C4ObjectLink *cLnk;
1281 // Get prev crew member
1282 if (cLnk = Crew.GetLink(pObj: Cursor))
1283 for (cLnk = cLnk->Prev; cLnk; cLnk = cLnk->Prev)
1284 if (cLnk->Obj->Status && !cLnk->Obj->CrewDisabled) break;
1285 if (!cLnk)
1286 for (cLnk = Crew.Last; cLnk; cLnk = cLnk->Prev)
1287 if (cLnk->Obj->Status && !cLnk->Obj->CrewDisabled) break;
1288 if (cLnk) SetCursor(pObj: cLnk->Obj, fSelectFlash: false, fSelectArrow: true);
1289 // Updates
1290 CursorFlash = 30;
1291 CursorSelection = 1;
1292 UpdateView();
1293}
1294
1295void C4Player::UnselectCrew()
1296{
1297 C4Object *cObj; C4ObjectLink *cLnk; bool fCursorDeselected = false;
1298 for (cLnk = Crew.First; cLnk && (cObj = cLnk->Obj); cLnk = cLnk->Next)
1299 {
1300 if (Cursor == cObj) fCursorDeselected = true;
1301 if (cObj->Status)
1302 cObj->UnSelect();
1303 }
1304 // if cursor is outside crew (done by some scenarios), unselect that one, too! (script callback)
1305 if (Cursor && !fCursorDeselected) Cursor->UnSelect();
1306}
1307
1308void C4Player::SelectSingleByCursor()
1309{
1310 // Unselect crew
1311 UnselectCrew();
1312 // Select cursor
1313 if (Cursor) Cursor->DoSelect();
1314 // Updates
1315 SelectFlash = 30;
1316 AdjustCursorCommand();
1317}
1318
1319void C4Player::CursorToggle()
1320{
1321 C4ObjectLink *clnk;
1322 // Selection mode: toggle cursor select
1323 if (CursorSelection)
1324 {
1325 if (Cursor)
1326 if (Cursor->Select) Cursor->UnSelect(); else Cursor->DoSelect();
1327 CursorToggled = 1;
1328 }
1329 // Pure toggle: toggle all Select
1330 else
1331 {
1332 for (clnk = Crew.First; clnk; clnk = clnk->Next)
1333 if (!clnk->Obj->CrewDisabled)
1334 if (clnk->Obj->Select) clnk->Obj->UnSelect(); else clnk->Obj->DoSelect();
1335 AdjustCursorCommand();
1336 }
1337 // Updates
1338 SelectFlash = 30;
1339}
1340
1341void C4Player::SelectAllCrew()
1342{
1343 C4ObjectLink *clnk;
1344 // Select all crew
1345 for (clnk = Crew.First; clnk; clnk = clnk->Next)
1346 clnk->Obj->DoSelect();
1347 // Updates
1348 AdjustCursorCommand();
1349 CursorSelection = CursorToggled = 0;
1350 SelectFlash = 30;
1351 // Game display
1352 if (LocalControl) StartSoundEffect(name: "Ding");
1353}
1354
1355void C4Player::UpdateSelectionToggleStatus()
1356{
1357 if (CursorSelection)
1358 // Select toggled: cursor to hirank
1359 if (CursorToggled)
1360 AdjustCursorCommand();
1361 // Cursor select only: single control
1362 else
1363 SelectSingleByCursor();
1364 CursorSelection = 0;
1365 CursorToggled = 0;
1366}
1367
1368bool C4Player::ObjectCom(uint8_t byCom, int32_t iData) // By DirectCom
1369{
1370 if (Eliminated) return false;
1371#ifdef DEBUGREC_OBJCOM
1372 C4RCObjectCom rc = { byCom, iData, Number };
1373 AddDbgRec(RCT_PlrCom, &rc, sizeof(C4RCObjectCom));
1374#endif
1375 // Hide startup
1376 ShowStartup = false;
1377 // If regular com, update cursor & selection status
1378 if (!(byCom & COM_Single) && !(byCom & COM_Double) && (byCom < COM_ReleaseFirst || byCom > COM_ReleaseLast))
1379 UpdateSelectionToggleStatus();
1380 // Apply direct com to cursor object
1381 if (Cursor)
1382 {
1383 // update controller
1384 Cursor->Controller = Number;
1385 // send com
1386 Cursor->DirectCom(byCom, iData);
1387 }
1388 // Done
1389 return true;
1390}
1391
1392void C4Player::ClearPressedComsSynced()
1393{
1394 Game.Input.Add(eType: CID_PlrControl, pCtrl: new C4ControlPlayerControl(Number, COM_ClearPressedComs, 0));
1395}
1396
1397bool C4Player::ObjectCommand(int32_t iCommand, C4Object *pTarget, int32_t iX, int32_t iY, C4Object *pTarget2, int32_t iData, int32_t iMode)
1398{
1399 // Eliminated
1400 if (Eliminated) return false;
1401 // Hide startup
1402 if (ShowStartup) ShowStartup = false;
1403 // Update selection & toggle status
1404 UpdateSelectionToggleStatus();
1405 // Apply to all selected crew members (in cursor range) except pTarget.
1406 // Set, Add, Append mode flags may be combined and have a priority order.
1407 C4ObjectLink *cLnk; C4Object *cObj; bool fCursorProcessed = false;
1408 for (cLnk = Crew.First; cLnk && (cObj = cLnk->Obj); cLnk = cLnk->Next)
1409 {
1410 if (cObj == Cursor) fCursorProcessed = true;
1411 if (cObj->Status) if (cObj->Select)
1412 if (!(iMode & C4P_Command_Range) || (Cursor && Inside<int32_t>(ival: cObj->x - Cursor->x, lbound: -15, rbound: +15) && Inside<int32_t>(ival: cObj->y - Cursor->y, lbound: -15, rbound: +15)))
1413 if (cObj != pTarget)
1414 {
1415 // Put command with unspecified put object
1416 if ((iCommand == C4CMD_Put) && !pTarget2)
1417 {
1418 // Special: the put command is only applied by this function if the clonk actually
1419 // has something to put. Also, the put count is adjusted to that the clonk will not try to put
1420 // more items than he actually has. This workaround is needed so the put command can be used
1421 // to tell all selected clonks to put when in a container, simulating the old all-throw behavior.
1422 if (cObj->Contents.ObjectCount(id: iData))
1423 ObjectCommand2Obj(cObj, iCommand, pTarget, iX: std::min<int32_t>(a: iX, b: cObj->Contents.ObjectCount(id: iData)), iY, pTarget2, iData, iMode);
1424 }
1425 // Other command
1426 else
1427 ObjectCommand2Obj(cObj, iCommand, pTarget, iX, iY, pTarget2, iData, iMode);
1428 // don't issue multiple Construct-commands - store previous Clonk as target for next command object
1429 // note that if three Clonks get the command, and the middle one gets deleted (the Clonk, not the command), the third will start to build anyway
1430 // that is very unlikely, though...could be catched in ClearPointers, if need be?
1431 // also, if one Clonk of the chain aborts his command (controlled elsewhere, for instance), the following ones will fail their commands
1432 // It's not a perfect solution, after all. But certainly better than placing tons of construction sites in the first place
1433 if (iCommand == C4CMD_Construct) pTarget = cObj;
1434 }
1435 }
1436 // Always apply to cursor, even if it's not in the crew
1437 if (Cursor && !fCursorProcessed)
1438 if (Cursor->Status && Cursor != pTarget)
1439 ObjectCommand2Obj(cObj: Cursor, iCommand, pTarget, iX, iY, pTarget2, iData, iMode);
1440
1441 // Success
1442 return true;
1443}
1444
1445void C4Player::ObjectCommand2Obj(C4Object *cObj, int32_t iCommand, C4Object *pTarget, int32_t iX, int32_t iY, C4Object *pTarget2, int32_t iData, int32_t iMode)
1446{
1447 // forward to object
1448 if (iMode & C4P_Command_Append) cObj->AddCommand(iCommand, pTarget, iTx: iX, iTy: iY, iUpdateInterval: 0, pTarget2, fInitEvaluation: true, iData, fAppend: true, iRetries: 0, szText: nullptr, iBaseMode: C4CMD_Mode_Base); // append: by Shift-click and for dragging of multiple objects (all independent; thus C4CMD_Mode_Base)
1449 else if (iMode & C4P_Command_Add) cObj->AddCommand(iCommand, pTarget, iTx: iX, iTy: iY, iUpdateInterval: 0, pTarget2, fInitEvaluation: true, iData, fAppend: false, iRetries: 0, szText: nullptr, iBaseMode: C4CMD_Mode_Base); // append: by context menu and keyboard throw command (all independent; thus C4CMD_Mode_Base)
1450 else if (iMode & C4P_Command_Set) cObj->SetCommand(iCommand, pTarget, iTx: iX, iTy: iY, pTarget2, fControl: true, iData);
1451}
1452
1453void C4Player::DirectCom(uint8_t byCom, int32_t iData) // By InCom or ExecuteControl
1454{
1455 switch (byCom)
1456 {
1457 case COM_CursorLeft:
1458 case COM_CursorLeft_D:
1459 case COM_CursorRight:
1460 case COM_CursorRight_D:
1461 case COM_CursorToggle:
1462 case COM_CursorToggle_D:
1463 if (!Eliminated && Cursor)
1464 {
1465 Cursor->Controller = Number;
1466 if (Cursor->CallControl(pPlr: this, byCom))
1467 {
1468 if (!(byCom & COM_Double))
1469 {
1470 UpdateSelectionToggleStatus();
1471 }
1472 return;
1473 }
1474 }
1475 break;
1476 default:
1477 break;
1478 }
1479 switch (byCom)
1480 {
1481 case COM_CursorLeft: case COM_CursorLeft_D: CursorLeft(); break;
1482 case COM_CursorRight: case COM_CursorRight_D: CursorRight(); break;
1483 case COM_CursorToggle: CursorToggle(); break;
1484 case COM_CursorToggle_D: SelectAllCrew(); break;
1485
1486 default: ObjectCom(byCom, iData); break;
1487 }
1488}
1489
1490void C4Player::InCom(uint8_t byCom, int32_t iData)
1491{
1492#ifdef DEBUGREC_OBJCOM
1493 C4RCObjectCom rc = { byCom, iData, Number };
1494 AddDbgRec(RCT_PlrInCom, &rc, sizeof(C4RCObjectCom));
1495#endif
1496 if (byCom == COM_ClearPressedComs)
1497 {
1498 PressedComs = 0;
1499 LastCom = COM_None;
1500 return;
1501 }
1502 // Cursor object menu active: convert regular com to menu com
1503 if (Cursor) if (Cursor->Menu)
1504 {
1505 int32_t iCom = byCom;
1506 Cursor->Menu->ConvertCom(rCom&: iCom, rData&: iData, fAsyncConversion: false);
1507 byCom = iCom;
1508 }
1509 // Menu control: no single/double processing
1510 if (Inside(ival: byCom, lbound: COM_MenuFirst, rbound: COM_MenuLast))
1511 {
1512 DirectCom(byCom, iData); return;
1513 }
1514 // Ignore KeyRelease for Single/Double
1515 if (!Inside(ival: byCom, lbound: COM_ReleaseFirst, rbound: COM_ReleaseLast))
1516 {
1517 // Reset view
1518 ResetCursorView();
1519 // Update state
1520 if (Inside<int>(ival: byCom, lbound: COM_ReleaseFirst - 16, rbound: COM_ReleaseLast - 16))
1521 PressedComs |= 1 << byCom;
1522 // Check LastCom buffer for prior COM_Single
1523 if (LastCom != COM_None)
1524 if (LastCom != byCom)
1525 {
1526 DirectCom(byCom: LastCom | COM_Single, iData);
1527 // AutoStopControl uses a single COM_Down instead of DOM_Down_D for drop
1528 // So a COM_Down_S does what a COM_Down_D normally does, if generated by another key
1529 // instead of a timeout
1530 if (ControlStyle && LastCom == COM_Down) LastComDownDouble = C4DoubleClick;
1531 }
1532 // Check LastCom buffer for COM_Double
1533 if (LastCom == byCom) byCom |= COM_Double;
1534 // LastCom/Del process
1535 // this is set before issuing the DirectCom, so DirectCom-scripts may delete it
1536 LastCom = byCom; LastComDelay = 0;
1537 }
1538 else
1539 {
1540 // Update state
1541 if (Inside(ival: byCom, lbound: COM_ReleaseFirst, rbound: COM_ReleaseLast))
1542 {
1543 if (!(PressedComs & (1 << (byCom - 16))))
1544 {
1545 return;
1546 }
1547 PressedComs &= ~(1 << (byCom - 16));
1548 }
1549 }
1550 // Pass regular/COM_Double byCom to player
1551 DirectCom(byCom, iData);
1552 // LastComDownDouble process
1553 if (byCom == COM_Down_D) LastComDownDouble = C4DoubleClick;
1554}
1555
1556void C4Player::CompileFunc(StdCompiler *pComp)
1557{
1558 assert(ID);
1559
1560 pComp->Value(rStruct: mkNamingAdapt(rValue&: Status, szName: "Status", rDefault: 0));
1561 pComp->Value(rStruct: mkNamingAdapt(rValue&: AtClient, szName: "AtClient", rDefault: C4ClientIDUnknown));
1562 pComp->Value(rStruct: mkNamingAdapt(toC4CStr(AtClientName), szName: "AtClientName", rDefault: "Local"));
1563 pComp->Value(rStruct: mkNamingAdapt(rValue&: Number, szName: "Index", rDefault: C4P_Number_None));
1564 pComp->Value(rStruct: mkNamingAdapt(rValue&: ID, szName: "ID", rDefault: 0));
1565 pComp->Value(rStruct: mkNamingAdapt(rValue&: Eliminated, szName: "Eliminated", rDefault: 0));
1566 pComp->Value(rStruct: mkNamingAdapt(rValue&: Surrendered, szName: "Surrendered", rDefault: 0));
1567 pComp->Value(rStruct: mkNamingAdapt(rValue&: Evaluated, szName: "Evaluated", rDefault: false));
1568 pComp->Value(rStruct: mkNamingAdapt(rValue&: Color, szName: "Color", rDefault: -1));
1569 pComp->Value(rStruct: mkNamingAdapt(rValue&: ColorDw, szName: "ColorDw", rDefault: 0u));
1570 pComp->Value(rStruct: mkNamingAdapt(rValue&: Control, szName: "Control", rDefault: 0));
1571 pComp->Value(rStruct: mkNamingAdapt(rValue&: MouseControl, szName: "MouseControl", rDefault: 0));
1572 pComp->Value(rStruct: mkNamingAdapt(rValue&: AutoContextMenu, szName: "AutoContextMenu", rDefault: 0));
1573 pComp->Value(rStruct: mkNamingAdapt(rValue&: ControlStyle, szName: "AutoStopControl", rDefault: 0));
1574 pComp->Value(rStruct: mkNamingAdapt(rValue&: Position, szName: "Position", rDefault: 0));
1575 pComp->Value(rStruct: mkNamingAdapt(rValue&: ViewMode, szName: "ViewMode", rDefault: C4PVM_Cursor));
1576 pComp->Value(rStruct: mkNamingAdapt(rValue&: ViewX, szName: "ViewX", rDefault: 0));
1577 pComp->Value(rStruct: mkNamingAdapt(rValue&: ViewY, szName: "ViewY", rDefault: 0));
1578 pComp->Value(rStruct: mkNamingAdapt(rValue&: ViewWealth, szName: "ViewWealth", rDefault: 0));
1579 pComp->Value(rStruct: mkNamingAdapt(rValue&: ViewValue, szName: "ViewValue", rDefault: 0));
1580 pComp->Value(rStruct: mkNamingAdapt(rValue&: fFogOfWar, szName: "FogOfWar", rDefault: false));
1581 pComp->Value(rStruct: mkNamingAdapt(rValue&: bForceFogOfWar, szName: "ForceFogOfWar", rDefault: false));
1582 pComp->Value(rStruct: mkNamingAdapt(rValue&: ShowStartup, szName: "ShowStartup", rDefault: false));
1583 pComp->Value(rStruct: mkNamingAdapt(rValue&: ShowControl, szName: "ShowControl", rDefault: 0));
1584 pComp->Value(rStruct: mkNamingAdapt(rValue&: ShowControlPos, szName: "ShowControlPos", rDefault: 0));
1585 pComp->Value(rStruct: mkNamingAdapt(rValue&: Wealth, szName: "Wealth", rDefault: 0));
1586 pComp->Value(rStruct: mkNamingAdapt(rValue&: Points, szName: "Points", rDefault: 0));
1587 pComp->Value(rStruct: mkNamingAdapt(rValue&: Value, szName: "Value", rDefault: 0));
1588 pComp->Value(rStruct: mkNamingAdapt(rValue&: InitialValue, szName: "InitialValue", rDefault: 0));
1589 pComp->Value(rStruct: mkNamingAdapt(rValue&: ValueGain, szName: "ValueGain", rDefault: 0));
1590 pComp->Value(rStruct: mkNamingAdapt(rValue&: ObjectsOwned, szName: "ObjectsOwned", rDefault: 0));
1591 pComp->Value(rStruct: mkNamingAdapt(rValue&: Hostility, szName: "Hostile"));
1592 pComp->Value(rStruct: mkNamingAdapt(rValue&: ProductionDelay, szName: "ProductionDelay", rDefault: 0));
1593 pComp->Value(rStruct: mkNamingAdapt(rValue&: ProductionUnit, szName: "ProductionUnit", rDefault: 0));
1594 pComp->Value(rStruct: mkNamingAdapt(rValue&: SelectCount, szName: "SelectCount", rDefault: 0));
1595 pComp->Value(rStruct: mkNamingAdapt(rValue&: SelectFlash, szName: "SelectFlash", rDefault: 0));
1596 pComp->Value(rStruct: mkNamingAdapt(rValue&: CursorFlash, szName: "CursorFlash", rDefault: 0));
1597 pComp->Value(rStruct: mkNamingAdapt(rValue&: reinterpret_cast<int32_t &>(Cursor), szName: "Cursor", rDefault: 0));
1598 pComp->Value(rStruct: mkNamingAdapt(rValue&: reinterpret_cast<int32_t &>(ViewCursor), szName: "ViewCursor", rDefault: 0));
1599 pComp->Value(rStruct: mkNamingAdapt(rValue&: reinterpret_cast<int32_t &>(Captain), szName: "Captain", rDefault: 0));
1600 pComp->Value(rStruct: mkNamingAdapt(rValue&: LastCom, szName: "LastCom", rDefault: 0));
1601 pComp->Value(rStruct: mkNamingAdapt(rValue&: LastComDelay, szName: "LastComDel", rDefault: 0));
1602 pComp->Value(rStruct: mkNamingAdapt(rValue&: PressedComs, szName: "PressedComs", rDefault: 0));
1603 pComp->Value(rStruct: mkNamingAdapt(rValue&: LastComDownDouble, szName: "LastComDownDouble", rDefault: 0));
1604 pComp->Value(rStruct: mkNamingAdapt(rValue&: CursorSelection, szName: "CursorSelection", rDefault: 0));
1605 pComp->Value(rStruct: mkNamingAdapt(rValue&: CursorToggled, szName: "CursorToggled", rDefault: 0));
1606 pComp->Value(rStruct: mkNamingAdapt(rValue&: MessageStatus, szName: "MessageStatus", rDefault: 0));
1607 pComp->Value(rStruct: mkNamingAdapt(toC4CStr(MessageBuf), szName: "MessageBuf", rDefault: ""));
1608 pComp->Value(rStruct: mkNamingAdapt(rValue&: HomeBaseMaterial, szName: "HomeBaseMaterial"));
1609 pComp->Value(rStruct: mkNamingAdapt(rValue&: HomeBaseProduction, szName: "HomeBaseProduction"));
1610 pComp->Value(rStruct: mkNamingAdapt(rValue&: Knowledge, szName: "Knowledge"));
1611 pComp->Value(rStruct: mkNamingAdapt(rValue&: Magic, szName: "Magic"));
1612 pComp->Value(rStruct: mkNamingAdapt(rValue&: Crew, szName: "Crew"));
1613 pComp->Value(rStruct: mkNamingAdapt(rValue&: CrewInfoList.iNumCreated, szName: "CrewCreated", rDefault: 0));
1614 pComp->Value(rStruct: mkNamingPtrAdapt(rpObj&: pMsgBoardQuery, szNaming: "MsgBoardQueries"));
1615}
1616
1617bool C4Player::LoadRuntimeData(C4Group &hGroup)
1618{
1619 const char *pSource;
1620 // Use loaded game text component
1621 if (!(pSource = Game.GameText.GetData())) return false;
1622 // safety: Do nothing if playeer section is not even present (could kill initialized values)
1623 if (!SSearch(szString: pSource, szIndex: std::format(fmt: "[Player{}]", args&: ID).c_str())) return false;
1624 // Compile (Search player section - runtime data is stored by unique player ID)
1625 assert(ID);
1626 if (!CompileFromBuf_LogWarn<StdCompilerINIRead>(
1627 TargetStruct: mkNamingAdapt(rValue&: *this, szName: std::format(fmt: "Player{}", args&: ID).c_str()),
1628 SrcBuf: StdStrBuf::MakeRef(str: pSource),
1629 szName: Game.GameText.GetFilePath()))
1630 return false;
1631 // Denumerate pointers
1632 DenumeratePointers();
1633 // Success
1634 return true;
1635}
1636
1637void C4Player::ExecHomeBaseProduction()
1638{
1639 const int32_t MaxHomeBaseProduction = 25;
1640 ProductionDelay++;
1641 if (ProductionDelay >= 60) // Minute Production Unit
1642 {
1643 // team home base: Only execute production on first player of team
1644 C4Team *pTeam;
1645 if (Game.Rules & C4RULE_TeamHombase)
1646 if (pTeam = Game.Teams.GetTeamByID(iID: Team))
1647 if (pTeam->GetFirstActivePlayerID() != ID)
1648 // other players will be synced by first player
1649 return;
1650 // do production
1651 bool fAnyChange = false;
1652 ProductionDelay = 0; ProductionUnit++;
1653 for (int32_t cnt = 0; HomeBaseProduction.GetID(index: cnt); cnt++)
1654 if (HomeBaseProduction.GetCount(index: cnt) > 0)
1655 if (ProductionUnit % BoundBy<int32_t>(bval: 11 - HomeBaseProduction.GetCount(index: cnt), lbound: 1, rbound: 10) == 0)
1656 if (HomeBaseMaterial.GetIDCount(id: HomeBaseProduction.GetID(index: cnt)) < MaxHomeBaseProduction)
1657 {
1658 HomeBaseMaterial.IncreaseIDCount(id: HomeBaseProduction.GetID(index: cnt));
1659 fAnyChange = true;
1660 }
1661 // All team members get same production if rule is active
1662 if (fAnyChange) if (Game.Rules & C4RULE_TeamHombase)
1663 SyncHomebaseMaterialToTeam();
1664 }
1665}
1666
1667void C4Player::UpdateCounts()
1668{
1669 int32_t nclkcnt, nselcnt;
1670 C4Object *cobj; C4ObjectLink *clnk;
1671 nclkcnt = nselcnt = 0;
1672 for (clnk = Crew.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next)
1673 {
1674 nclkcnt++; if (cobj->Select) nselcnt++;
1675 }
1676 if (CrewCnt != nclkcnt) CrewCnt = nclkcnt;
1677 if (SelectCount != nselcnt) SelectCount = nselcnt;
1678}
1679
1680void C4Player::CheckElimination()
1681{
1682 // Standard elimination: no crew
1683 if (CrewCnt <= 0)
1684 // Already eliminated safety
1685 if (!Eliminated)
1686 // No automatic elimination desired?
1687 if (!NoEliminationCheck)
1688 // Do elimination!
1689 Eliminate();
1690}
1691
1692void C4Player::UpdateView()
1693{
1694 // view target/cursor
1695 switch (ViewMode)
1696 {
1697 case C4PVM_Cursor:
1698 {
1699 C4Object *pViewObj;
1700 if (!(pViewObj = ViewCursor)) pViewObj = Cursor;
1701 if (pViewObj)
1702 {
1703 ViewX = pViewObj->x; ViewY = pViewObj->y;
1704 }
1705 break;
1706 }
1707 case C4PVM_Target:
1708 if (ViewTarget)
1709 {
1710 ViewX = ViewTarget->x; ViewY = ViewTarget->y;
1711 }
1712 break;
1713 case C4PVM_Scrolling:
1714 break;
1715 }
1716}
1717
1718void C4Player::DefaultRuntimeData()
1719{
1720 Status = 0;
1721 Eliminated = 0;
1722 Surrendered = 0;
1723 AtClient = C4ClientIDUnknown;
1724 SCopy(szSource: "Local", sTarget: AtClientName);
1725 Color = -1;
1726 Control = C4P_Control_None;
1727 MouseControl = false;
1728 Position = -1;
1729 PlrStartIndex = 0;
1730 RetireDelay = 0;
1731 ViewMode = C4PVM_Cursor;
1732 ViewX = ViewY = 0;
1733 ViewTarget = nullptr;
1734 CursorSelection = CursorToggled = 0;
1735 ShowStartup = true;
1736 CrewCnt = 0;
1737 ViewWealth = ViewValue = 0;
1738 ShowControl = ShowControlPos = 0;
1739 Wealth = 0;
1740 Points = 0;
1741 Value = InitialValue = ValueGain = 0;
1742 ObjectsOwned = 0;
1743 Captain = nullptr;
1744 ProductionDelay = ProductionUnit = 0;
1745 Cursor = ViewCursor = nullptr;
1746 SelectCount = 0;
1747 SelectFlash = CursorFlash = 30;
1748 LastCom = 0;
1749 LastComDelay = 0;
1750 LastComDownDouble = 0;
1751 CursorSelection = CursorToggled = 0;
1752 MessageStatus = 0;
1753 MessageBuf[0] = 0;
1754 Hostility.Clear();
1755 HomeBaseMaterial.Clear();
1756 HomeBaseProduction.Clear();
1757 Knowledge.Clear();
1758 Magic.Clear();
1759 FlashCom = 0;
1760}
1761
1762bool C4Player::ActivateMenuTeamSelection(bool fFromMain)
1763{
1764 // Menu symbol/init
1765 bool fSwitch = !(Status == PS_TeamSelection);
1766 Menu.InitRefSym(fctSymbol: C4GUI::Icon::GetIconFacet(icoIconIndex: C4GUI::Ico_Team), szEmpty: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SELTEAM), iPlayer: Number, iExtra: C4MN_Extra_None, iExtraData: 0, iId: fSwitch ? C4MN_TeamSwitch : C4MN_TeamSelection);
1767 Menu.SetAlignment(fSwitch ? C4MN_Align_Left | C4MN_Align_Bottom : 0);
1768 Menu.Refill();
1769 // Go back to options menu on close
1770 if (fFromMain) Menu.SetCloseCommand("ActivateMenu:Main");
1771 return true;
1772}
1773
1774void C4Player::DoTeamSelection(int32_t idTeam)
1775{
1776 // stop team selection. This might close the menu forever if the control gets lost
1777 // let's hope it doesn't!
1778 Status = PS_TeamSelectionPending;
1779 Game.Control.DoInput(eCtrlType: CID_InitScenarioPlayer, pPkt: new C4ControlInitScenarioPlayer(Number, idTeam), eDelivery: CDT_Queue);
1780}
1781
1782void C4Player::EnumeratePointers()
1783{
1784 EnumerateObjectPtrs(args&: Cursor, args&: ViewCursor, args&: Captain);
1785 for (C4MessageBoardQuery *pCheck = pMsgBoardQuery; pCheck; pCheck = pCheck->pNext)
1786 pCheck->pCallbackObj.Enumerate();
1787}
1788
1789void C4Player::DenumeratePointers()
1790{
1791 DenumerateObjectPtrs(args&: Cursor, args&: ViewCursor, args&: Captain);
1792 // Crew
1793 Crew.DenumerateRead();
1794 // messageboard-queries
1795 for (C4MessageBoardQuery *pCheck = pMsgBoardQuery; pCheck; pCheck = pCheck->pNext)
1796 pCheck->pCallbackObj.Denumerate();
1797}
1798
1799void C4Player::RemoveCrewObjects()
1800{
1801 C4Object *pCrew;
1802
1803 // Remove all crew objects
1804 while (pCrew = Crew.GetObject()) pCrew->AssignRemoval(fExitContents: true);
1805}
1806
1807void C4Player::NotifyOwnedObjects()
1808{
1809 C4Object *cobj; C4ObjectLink *clnk;
1810
1811 // notify objects in all object lists
1812 for (C4ObjectList *pList = &Game.Objects; pList; pList = ((pList == &Game.Objects) ? &Game.Objects.InactiveObjects : nullptr))
1813 for (clnk = pList->First; clnk && (cobj = clnk->Obj); clnk = clnk->Next)
1814 if (cobj->Status)
1815 if (cobj->Owner == Number)
1816 {
1817 C4AulFunc *pFn = cobj->Def->Script.GetFuncRecursive(PSF_OnOwnerRemoved);
1818 // PSF_OnOwnerRemoved has an internal fallback function
1819 assert(pFn);
1820 if (pFn) pFn->Exec(pObj: cobj);
1821 }
1822}
1823
1824bool C4Player::DoPoints(int32_t iChange)
1825{
1826 Points = BoundBy<int32_t>(bval: Points + iChange, lbound: -100000, rbound: 100000);
1827 ViewValue = C4ViewDelay;
1828 return true;
1829}
1830
1831void C4Player::SetCursor(C4Object *pObj, bool fSelectFlash, bool fSelectArrow)
1832{
1833 // check disabled
1834 if (pObj) if (pObj->CrewDisabled) return;
1835 bool fChanged = pObj != Cursor;
1836 C4Object *pPrev = Cursor;
1837 // Set cursor
1838 Cursor = pObj;
1839 // unselect previous
1840 if (pPrev && fChanged) pPrev->UnSelect(fCursor: true);
1841 // Select object
1842 if (Cursor) Cursor->DoSelect(fCursor: true);
1843 // View flash
1844 if (fSelectArrow) CursorFlash = 30;
1845 if (fSelectFlash) SelectFlash = 30;
1846}
1847
1848void C4Player::SelectCrew(C4ObjectList &rList)
1849{
1850 // Unselect
1851 UnselectCrew();
1852 // Select (does not check whether objects are in crew)
1853 C4ObjectLink *clnk;
1854 for (clnk = rList.First; clnk; clnk = clnk->Next)
1855 if (clnk->Obj->Status)
1856 clnk->Obj->DoSelect();
1857 // Updates
1858 AdjustCursorCommand();
1859 CursorSelection = CursorToggled = 0;
1860 SelectFlash = 30;
1861}
1862
1863void C4Player::ScrollView(int32_t iX, int32_t iY, int32_t ViewWdt, int32_t ViewHgt)
1864{
1865 SetViewMode(iMode: C4PVM_Scrolling);
1866 int32_t ViewportScrollBorder = Application.isFullScreen ? C4ViewportScrollBorder : 0;
1867 ViewX = BoundBy<int32_t>(bval: ViewX + iX, lbound: ViewWdt / 2 - ViewportScrollBorder, GBackWdt + ViewportScrollBorder - ViewWdt / 2);
1868 ViewY = BoundBy<int32_t>(bval: ViewY + iY, lbound: ViewHgt / 2 - ViewportScrollBorder, GBackHgt + ViewportScrollBorder - ViewHgt / 2);
1869}
1870
1871void C4Player::InitControl()
1872{
1873 // Check local control
1874 LocalControl = false;
1875 if (AtClient == Game.Control.ClientID())
1876 if (!GetInfo() || GetInfo()->GetType() == C4PT_User)
1877 LocalControl = true;
1878 // Set control
1879 Control = C4P_Control_None;
1880 // Preferred control
1881 int32_t iControl = PrefControl;
1882 // gamepad control safety
1883 if (Inside<int32_t>(ival: iControl, lbound: C4P_Control_GamePad1, rbound: C4P_Control_GamePadMax) && !Config.General.GamepadEnabled)
1884 {
1885 iControl = C4P_Control_Keyboard1;
1886 }
1887 // Choose next while control taken
1888 if (Game.Players.ControlTaken(iControl))
1889 {
1890 // Preferred control taken, search for available keyboard control
1891 for (iControl = C4P_Control_Keyboard1; iControl <= C4P_Control_Keyboard4; iControl++)
1892 if (!Game.Players.ControlTaken(iControl)) // Available control found
1893 break;
1894 // No available control found
1895 if (iControl > C4P_Control_Keyboard4)
1896 iControl = C4P_Control_None;
1897 }
1898 // Set control
1899 Control = iControl;
1900 ApplyForcedControl();
1901 // init gamepad
1902 delete pGamepad; pGamepad = nullptr;
1903 if (Inside<int32_t>(ival: Control, lbound: C4P_Control_GamePad1, rbound: C4P_Control_GamePadMax))
1904 {
1905 pGamepad = new C4GamePadOpener(Control - C4P_Control_GamePad1);
1906 }
1907 // Mouse
1908 if (PrefMouse && !Game.Control.isReplay())
1909 if (!Game.C4S.Head.DisableMouse)
1910 if (Inside<int32_t>(ival: Control, lbound: C4P_Control_Keyboard1, rbound: C4P_Control_GamePadMax))
1911 if (!Game.Players.MouseControlTaken())
1912 MouseControl = true;
1913 // no controls issued yet
1914 ControlCount = ActionCount = 0;
1915 LastControlType = PCID_None;
1916 LastControlID = 0;
1917 PressedComs = 0;
1918}
1919
1920void C4Player::FoW2Map(CClrModAddMap &rMap, int iOffX, int iOffY)
1921{
1922 // No fog of war
1923 if (!fFogOfWar) return;
1924 // Add view for all FoW-repellers - keep track of FoW-generators, which should be avaluated finally
1925 // so they override repellers
1926 bool fAnyGenerators = false;
1927 C4Object *cobj; C4ObjectLink *clnk;
1928 for (clnk = FoWViewObjs.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next)
1929 if (!cobj->Contained || cobj->Contained->Def->ClosedContainer != 1)
1930 if (cobj->PlrViewRange > 0)
1931 rMap.ReduceModulation(cx: cobj->x + iOffX, cy: cobj->y + iOffY, iRadius1: cobj->PlrViewRange * 2 / 3, iRadius2: cobj->PlrViewRange);
1932 else
1933 fAnyGenerators = true;
1934 // Add view for target view object
1935 if (ViewMode == C4PVM_Target)
1936 if (ViewTarget)
1937 if (!ViewTarget->Contained || ViewTarget->Contained->Def->ClosedContainer != 1)
1938 {
1939 int iRange = ViewTarget->PlrViewRange;
1940 if (!iRange && Cursor) iRange = Cursor->PlrViewRange;
1941 if (!iRange) iRange = C4FOW_Def_View_RangeX;
1942 rMap.ReduceModulation(cx: ViewTarget->x + iOffX, cy: ViewTarget->y + iOffY, iRadius1: iRange * 2 / 3, iRadius2: iRange);
1943 }
1944 // apply generators
1945 // do this check, be cause in 99% of all normal scenarios, there will be no FoW-generators
1946 if (fAnyGenerators) FoWGenerators2Map(rMap, iOffX, iOffY);
1947}
1948
1949void C4Player::FoWGenerators2Map(CClrModAddMap &rMap, int iOffX, int iOffY)
1950{
1951 // add fog to any generator pos (view range
1952 C4Object *cobj; C4ObjectLink *clnk;
1953 for (clnk = FoWViewObjs.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next)
1954 if (!cobj->Contained || cobj->Contained->Def->ClosedContainer != 1)
1955 if (cobj->PlrViewRange < 0)
1956 rMap.AddModulation(cx: cobj->x + iOffX, cy: cobj->y + iOffY, iRadius1: -cobj->PlrViewRange, iRadius2: -cobj->PlrViewRange + 200, byTransparency: cobj->ColorMod >> 24);
1957}
1958
1959bool C4Player::FoWIsVisible(int32_t x, int32_t y)
1960{
1961 // check repellers and generators and ViewTarget
1962 bool fSeen = false;
1963 C4Object *cobj = nullptr; C4ObjectLink *clnk;
1964 clnk = FoWViewObjs.First;
1965 int32_t iRange;
1966 for (;;)
1967 {
1968 if (clnk)
1969 {
1970 cobj = clnk->Obj;
1971 clnk = clnk->Next;
1972 iRange = cobj->PlrViewRange;
1973 }
1974 else if (ViewMode != C4PVM_Target || !ViewTarget || ViewTarget == cobj)
1975 break;
1976 else
1977 {
1978 cobj = ViewTarget;
1979 iRange = cobj->PlrViewRange;
1980 if (!iRange && Cursor) iRange = Cursor->PlrViewRange;
1981 if (!iRange) iRange = C4FOW_Def_View_RangeX;
1982 }
1983 if (!cobj->Contained || cobj->Contained->Def->ClosedContainer != 1)
1984 if (Distance(iX1: cobj->x, iY1: cobj->y, iX2: x, iY2: y) < Abs(val: iRange))
1985 if (iRange < 0)
1986 {
1987 if (!(cobj->ColorMod & 0xff000000)) // faded generators generate darkness only; no FoW blocking
1988 return false; // shadowed by FoW-generator
1989 }
1990 else
1991 fSeen = true; // made visible by FoW-repeller
1992 }
1993 return fSeen;
1994}
1995
1996void C4Player::SelectCrew(C4Object *pObj, bool fSelect)
1997{
1998 // Not a valid crew member
1999 if (!pObj || !Crew.GetLink(pObj)) return;
2000 // Select/unselect
2001 if (fSelect) pObj->DoSelect();
2002 else pObj->UnSelect();
2003 // Updates
2004 SelectFlash = 30;
2005 CursorSelection = CursorToggled = 0;
2006 AdjustCursorCommand();
2007}
2008
2009void C4Player::CloseMenu()
2010{
2011 // cancel all player menus
2012 Menu.Close(fOK: false);
2013}
2014
2015void C4Player::Eliminate()
2016{
2017 if (Eliminated) return;
2018 Eliminated = true;
2019 RetireDelay = C4RetireDelay;
2020 StartSoundEffect(name: "Eliminated");
2021 Log(id: C4ResStrTableKey::IDS_PRC_PLRELIMINATED, args: GetName());
2022
2023 // Early client deactivation check
2024 if (Game.Control.isCtrlHost() && AtClient > C4ClientIDHost)
2025 {
2026 // Check: Any player left at this client?
2027 C4Player *pPlr = nullptr;
2028 for (int i = 0; pPlr = Game.Players.GetAtClient(iClient: AtClient, iIndex: i); i++)
2029 if (!pPlr->Eliminated)
2030 break;
2031 // If not, deactivate the client
2032 if (!pPlr)
2033 Game.Control.DoInput(eCtrlType: CID_ClientUpdate,
2034 pPkt: new C4ControlClientUpdate(AtClient, CUT_Activate, false),
2035 eDelivery: CDT_Sync);
2036 }
2037}
2038
2039int32_t C4Player::ActiveCrewCount()
2040{
2041 // get number of objects in crew that is not disabled
2042 int32_t iNum = 0;
2043 C4Object *cObj;
2044 for (C4ObjectLink *cLnk = Crew.First; cLnk; cLnk = cLnk->Next)
2045 if (cObj = cLnk->Obj)
2046 if (!cObj->CrewDisabled)
2047 ++iNum;
2048 // return it
2049 return iNum;
2050}
2051
2052int32_t C4Player::GetSelectedCrewCount()
2053{
2054 int32_t iNum = 0;
2055 C4Object *cObj;
2056 for (C4ObjectLink *cLnk = Crew.First; cLnk; cLnk = cLnk->Next)
2057 if (cObj = cLnk->Obj)
2058 if (!cObj->CrewDisabled)
2059 if (cObj->Select)
2060 ++iNum;
2061 // return it
2062 return iNum;
2063}
2064
2065void C4Player::EvaluateLeague(bool fDisconnected, bool fWon)
2066{
2067 // already evaluated?
2068 if (LeagueEvaluated) return; LeagueEvaluated = true;
2069 // set fate
2070 C4PlayerInfo *pInfo = GetInfo();
2071 if (pInfo)
2072 {
2073 if (fDisconnected)
2074 pInfo->SetDisconnected();
2075 if (fWon)
2076 pInfo->SetWinner();
2077 }
2078}
2079
2080bool C4Player::LocalSync()
2081{
2082 // local sync not necessary for script players
2083 if (GetType() == C4PT_Script) return true;
2084 // evaluate total playing time
2085 TotalPlayingTime += Game.Time - GameJoinTime;
2086 GameJoinTime = Game.Time;
2087 // evaluate total playing time of all the crew
2088 for (C4ObjectInfo *pInf = CrewInfoList.GetFirst(); pInf; pInf = pInf->Next)
2089 if (pInf->InAction)
2090 {
2091 pInf->TotalPlayingTime += (Game.Time - pInf->InActionTime);
2092 pInf->InActionTime = Game.Time;
2093 }
2094 // save player
2095 if (!Save())
2096 return false;
2097
2098 // done, success
2099 return true;
2100}
2101
2102C4PlayerInfo *C4Player::GetInfo()
2103{
2104 return Game.PlayerInfos.GetPlayerInfoByID(id: ID);
2105}
2106
2107bool C4Player::SetObjectCrewStatus(C4Object *pCrew, bool fNewStatus)
2108{
2109 // either add...
2110 if (fNewStatus)
2111 {
2112 // is in crew already?
2113 if (Crew.IsContained(pObj: pCrew)) return true;
2114 return MakeCrewMember(pObj: pCrew, fForceInfo: false);
2115 }
2116 else
2117 {
2118 // already outside?
2119 if (!Crew.IsContained(pObj: pCrew)) return true;
2120 // ...or remove
2121 Crew.Remove(pObj: pCrew);
2122 // make sure it's not selected any more
2123 if (pCrew->Select) pCrew->UnSelect();
2124 // remove info, if assigned to this player
2125 // theoretically, info objects could remain when the player is deleted
2126 // but then they would be reassigned to the player crew when loaded in a savegame
2127 // by the crew-assignment code kept for backwards compatibility with pre-4.95.2-savegames
2128 if (pCrew->Info && CrewInfoList.IsElement(pInfo: pCrew->Info))
2129 {
2130 pCrew->Info->Retire();
2131 pCrew->Info = nullptr;
2132 }
2133 }
2134 // done, success
2135 return true;
2136}
2137
2138void C4Player::CreateGraphs()
2139{
2140 // del prev
2141 ClearGraphs();
2142 // create graphs
2143 if (Game.pNetworkStatistics)
2144 {
2145 uint32_t dwGraphClr = ColorDw;
2146 C4PlayerInfo *pInfo;
2147 if (ID && (pInfo = Game.PlayerInfos.GetPlayerInfoByID(id: ID)))
2148 {
2149 // set color by player info class
2150 dwGraphClr = pInfo->GetColor();
2151 }
2152 C4GUI::MakeColorReadableOnBlack(rdwClr&: dwGraphClr); dwGraphClr &= 0xffffff;
2153 pstatControls = new C4TableGraph(C4TableGraph::DefaultBlockLength * 20, Game.pNetworkStatistics->ControlCounter);
2154 pstatControls->SetColorDw(dwGraphClr);
2155 pstatControls->SetTitle(GetName());
2156 pstatActions = new C4TableGraph(C4TableGraph::DefaultBlockLength * 20, Game.pNetworkStatistics->ControlCounter);
2157 pstatActions->SetColorDw(dwGraphClr);
2158 pstatActions->SetTitle(GetName());
2159 // register into
2160 Game.pNetworkStatistics->statControls.AddGraph(pAdd: pstatControls);
2161 Game.pNetworkStatistics->statActions.AddGraph(pAdd: pstatActions);
2162 }
2163}
2164
2165void C4Player::ClearGraphs()
2166{
2167 // del all assigned graphs
2168 if (pstatControls && Game.pNetworkStatistics)
2169 {
2170 Game.pNetworkStatistics->statControls.RemoveGraph(pRemove: pstatControls);
2171 }
2172 delete pstatControls;
2173 pstatControls = nullptr;
2174 if (pstatActions && Game.pNetworkStatistics)
2175 {
2176 Game.pNetworkStatistics->statActions.RemoveGraph(pRemove: pstatActions);
2177 }
2178 delete pstatActions;
2179 pstatActions = nullptr;
2180}
2181
2182void C4Player::CountControl(ControlType eType, int32_t iID, int32_t iCntAdd)
2183{
2184 // count it
2185 ControlCount += iCntAdd;
2186 // catch doubles
2187 if (eType == LastControlType && iID == LastControlID) return;
2188 // no double: count as action
2189 LastControlType = eType;
2190 LastControlID = iID;
2191 ActionCount += iCntAdd;
2192 // and give experience
2193 if (Cursor && Cursor->Info)
2194 {
2195 if (Cursor->Info)
2196 {
2197 Cursor->Info->ControlCount++; if ((Cursor->Info->ControlCount % 5) == 0) Cursor->DoExperience(change: +1);
2198 }
2199 }
2200}
2201
2202void C4Player::ExecMsgBoardQueries()
2203{
2204 // query now possible?
2205 if (!C4GUI::IsGUIValid()) return;
2206 // already active?
2207 if (Game.MessageInput.IsTypeIn()) return;
2208 // find an un-evaluated query
2209 C4MessageBoardQuery *pCheck = pMsgBoardQuery;
2210 while (pCheck) if (!pCheck->fAnswered) break; else pCheck = pCheck->pNext;
2211 if (!pCheck) return;
2212 // open it
2213 Game.MessageInput.StartTypeIn(fObjInput: true, pObj: pCheck->pCallbackObj, fUpperCase: pCheck->fIsUppercase, mode: C4ChatInputDialog::All, iPlr: Number, rsInputQuery: pCheck->sInputQuery);
2214}
2215
2216void C4Player::CallMessageBoard(C4Object *pForObj, const StdStrBuf &sQueryString, bool fIsUppercase)
2217{
2218 // remove any previous query for the same object
2219 RemoveMessageBoardQuery(pForObj);
2220 // sort new query to end of list
2221 C4MessageBoardQuery **ppTarget = &pMsgBoardQuery;
2222 while (*ppTarget) ppTarget = &((*ppTarget)->pNext);
2223 *ppTarget = new C4MessageBoardQuery(pForObj, sQueryString, fIsUppercase);
2224}
2225
2226bool C4Player::RemoveMessageBoardQuery(C4Object *pForObj)
2227{
2228 // get matching query
2229 C4MessageBoardQuery **ppCheck = &pMsgBoardQuery, *pFound;
2230 while (*ppCheck) if ((*ppCheck)->pCallbackObj == pForObj) break; else ppCheck = &((*ppCheck)->pNext);
2231 pFound = *ppCheck;
2232 if (!pFound) return false;
2233 // remove it
2234 *ppCheck = (*ppCheck)->pNext;
2235 delete pFound;
2236 return true;
2237}
2238
2239bool C4Player::MarkMessageBoardQueryAnswered(C4Object *pForObj)
2240{
2241 // get matching query
2242 C4MessageBoardQuery *pCheck = pMsgBoardQuery;
2243 while (pCheck) if (pCheck->pCallbackObj == pForObj && !pCheck->fAnswered) break; else pCheck = pCheck->pNext;
2244 if (!pCheck) return false;
2245 // mark it
2246 pCheck->fAnswered = true;
2247 return true;
2248}
2249
2250bool C4Player::HasMessageBoardQuery()
2251{
2252 // return whether any object has a messageboard-query
2253 return !!pMsgBoardQuery;
2254}
2255
2256void C4Player::OnTeamSelectionFailed()
2257{
2258 // looks like a selected team was not available: Go back to team selection if this is not a mislead call
2259 if (Status == PS_TeamSelectionPending)
2260 Status = PS_TeamSelection;
2261}
2262
2263void C4Player::SetPlayerColor(uint32_t dwNewClr)
2264{
2265 // no change?
2266 if (dwNewClr == ColorDw) return;
2267 // reflect change in all active, player-owned objects
2268 // this can never catch everything (thinking of overlays, etc.); scenarios that allow team changes should take care of the rest
2269 uint32_t dwOldClr = ColorDw;
2270 ColorDw = dwNewClr;
2271 C4Object *pObj;
2272 for (C4ObjectLink *pLnk = Game.Objects.First; pLnk; pLnk = pLnk->Next)
2273 if (pObj = pLnk->Obj)
2274 if (pObj->Status)
2275 if (pObj->Owner == Number)
2276 {
2277 if ((pObj->Color & 0xffffff) == (dwOldClr & 0xffffff))
2278 pObj->Color = (pObj->Color & 0xff000000u) | (dwNewClr & 0xffffff);
2279 }
2280}
2281
2282C4PlayerType C4Player::GetType() const
2283{
2284 // type by info
2285 C4PlayerInfo *pInfo = Game.PlayerInfos.GetPlayerInfoByID(id: ID);
2286 if (pInfo) return pInfo->GetType(); else { assert(false); return C4PT_User; }
2287}
2288
2289bool C4Player::IsInvisible() const
2290{
2291 // invisible by info
2292 C4PlayerInfo *pInfo = Game.PlayerInfos.GetPlayerInfoByID(id: ID);
2293 if (pInfo) return pInfo->IsInvisible(); else { assert(false); return false; }
2294}
2295
2296void C4Player::ToggleMouseControl()
2297{
2298 // Activate mouse control if it's available
2299 if (!MouseControl && !Game.Players.MouseControlTaken())
2300 {
2301 Game.MouseControl.Init(iPlayer: Number);
2302 MouseControl = true;
2303 // init fog of war
2304 if (!fFogOfWar && !bForceFogOfWar)
2305 {
2306 fFogOfWar = fFogOfWarInitialized = true;
2307 Game.Objects.AssignPlrViewRange();
2308 }
2309 }
2310 // Deactivate mouse control
2311 else if (MouseControl)
2312 {
2313 Game.MouseControl.Clear();
2314 Game.MouseControl.Default();
2315 MouseControl = 0;
2316 // Scrolling isn't possible any more
2317 if (ViewMode == C4PVM_Scrolling)
2318 SetViewMode(iMode: C4PVM_Cursor);
2319 if (!bForceFogOfWar)
2320 {
2321 // delete fog of war
2322 fFogOfWarInitialized = fFogOfWar = false;
2323 }
2324 }
2325}
2326
2327bool C4Player::ActivateMenuMain()
2328{
2329 // Not during game over dialog
2330 if (C4GameOverDlg::IsShown()) return false;
2331 // Open menu
2332 return !!Menu.ActivateMain(iPlayer: Number);
2333}
2334
2335void C4Player::SyncHomebaseMaterialToTeam()
2336{
2337 // Copy own home base material to all team members
2338 C4Team *pTeam;
2339 if (Team && ((pTeam = Game.Teams.GetTeamByID(iID: Team))))
2340 {
2341 int32_t iTeamSize = pTeam->GetPlayerCount();
2342 for (int32_t i = 0; i < iTeamSize; ++i)
2343 {
2344 int32_t iTeammate = pTeam->GetIndexedPlayer(iIndex: i);
2345 C4Player *pTeammate;
2346 if (iTeammate != ID && ((pTeammate = Game.Players.GetByInfoID(iInfoID: iTeammate))))
2347 {
2348 pTeammate->HomeBaseMaterial = HomeBaseMaterial;
2349 }
2350 }
2351 }
2352}
2353
2354void C4Player::SyncHomebaseMaterialFromTeam()
2355{
2356 // Copy own home base material from team's first player (unless it's ourselves)
2357 C4Team *pTeam;
2358 if (Team && ((pTeam = Game.Teams.GetTeamByID(iID: Team))))
2359 {
2360 int32_t iTeamCaptain = pTeam->GetIndexedPlayer(iIndex: 0);
2361 C4Player *pTeamCaptain;
2362 if (iTeamCaptain != ID && ((pTeamCaptain = Game.Players.GetByInfoID(iInfoID: iTeamCaptain))))
2363 {
2364 HomeBaseMaterial = pTeamCaptain->HomeBaseMaterial;
2365 }
2366 }
2367}
2368
2369void C4Player::ApplyForcedControl()
2370{
2371 const auto oldControl = ControlStyle;
2372
2373 ControlStyle = ((Game.C4S.Head.ForcedControlStyle > -1) ? Game.C4S.Head.ForcedControlStyle : PrefControlStyle);
2374 AutoContextMenu = ((Game.C4S.Head.ForcedAutoContextMenu > -1) ? Game.C4S.Head.ForcedAutoContextMenu : PrefAutoContextMenu);
2375
2376 if (oldControl != ControlStyle)
2377 {
2378 LastCom = COM_None;
2379 PressedComs = 0;
2380 if (ControlStyle) // AutoStopControl
2381 {
2382 for (auto objLink = Game.Objects.InactiveObjects.First; objLink; objLink = objLink->Next)
2383 {
2384 const auto obj = objLink->Obj;
2385 if (obj->Def->CrewMember && obj->Owner == Number)
2386 {
2387 obj->Action.ComDir = COMD_None;
2388 }
2389 }
2390 }
2391 }
2392}
2393