1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2020, 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/* Dynamic list to hold runtime player data */
18
19#include <C4Include.h>
20#include <C4PlayerList.h>
21
22#include <C4Components.h>
23#include <C4FullScreen.h>
24#include <C4Console.h>
25#include <C4Log.h>
26#include <C4Player.h>
27#include <C4Object.h>
28
29C4PlayerList::C4PlayerList()
30{
31 Default();
32}
33
34C4PlayerList::~C4PlayerList()
35{
36 Clear();
37}
38
39void C4PlayerList::Default()
40{
41 First = nullptr;
42}
43
44void C4PlayerList::Clear()
45{
46 C4Player *pPlr;
47 while (pPlr = First)
48 {
49 First = pPlr->Next; delete pPlr;
50 }
51 First = nullptr;
52}
53
54void C4PlayerList::Execute()
55{
56 C4Player *pPlr;
57 // Execute
58 for (pPlr = First; pPlr; pPlr = pPlr->Next)
59 pPlr->Execute();
60 // Check retirement
61 for (pPlr = First; pPlr; pPlr = pPlr->Next)
62 if (pPlr->Eliminated && !pPlr->RetireDelay)
63 {
64 Retire(pPlr); break;
65 }
66}
67
68void C4PlayerList::ClearPointers(C4Object *pObj)
69{
70 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
71 pPlr->ClearPointers(tptr: pObj, fDeath: false);
72}
73
74bool C4PlayerList::Valid(int iPlayer) const
75{
76 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
77 if (pPlr->Number == iPlayer)
78 return true;
79 return false;
80}
81
82bool C4PlayerList::Hostile(int iPlayer1, int iPlayer2) const
83{
84 C4Player *pPlr1 = Get(iPlayer: iPlayer1);
85 C4Player *pPlr2 = Get(iPlayer: iPlayer2);
86 if (!pPlr1 || !pPlr2) return false;
87 if (pPlr1->Number == pPlr2->Number) return false;
88 if (pPlr1->Hostility.GetIDCount(id: pPlr2->Number + 1)
89 || pPlr2->Hostility.GetIDCount(id: pPlr1->Number + 1))
90 return true;
91 return false;
92}
93
94bool C4PlayerList::HostilityDeclared(int iPlayer1, int iPlayer2) const
95{
96 // check one-way-hostility
97 C4Player *pPlr1 = Get(iPlayer: iPlayer1);
98 C4Player *pPlr2 = Get(iPlayer: iPlayer2);
99 if (!pPlr1 || !pPlr2) return false;
100 if (pPlr1->Number == pPlr2->Number) return false;
101 if (pPlr1->Hostility.GetIDCount(id: pPlr2->Number + 1))
102 return true;
103 return false;
104}
105
106bool C4PlayerList::PositionTaken(int iPosition) const
107{
108 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
109 if (pPlr->Position == iPosition)
110 return true;
111 return false;
112}
113
114bool C4PlayerList::ColorTaken(int iColor) const
115{
116 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
117 if (pPlr->Color == iColor)
118 return true;
119 return false;
120}
121
122bool C4PlayerList::ControlTaken(int iControl) const
123{
124 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
125 if (pPlr->Control == iControl)
126 if (pPlr->LocalControl)
127 return true;
128 return false;
129}
130
131C4Player *C4PlayerList::Get(int iNumber) const
132{
133 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
134 if (pPlr->Number == iNumber)
135 return pPlr;
136 return nullptr;
137}
138
139C4Player *C4PlayerList::GetByIndex(int iIndex) const
140{
141 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
142 if (!iIndex--)
143 return pPlr;
144 return nullptr;
145}
146
147C4Player *C4PlayerList::GetByIndex(int iIndex, C4PlayerType eType) const
148{
149 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
150 if (pPlr->GetType() == eType)
151 if (!iIndex--)
152 return pPlr;
153 return nullptr;
154}
155
156C4Player *C4PlayerList::GetLocalByKbdSet(int iKbdSet) const
157{
158 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
159 if (pPlr->LocalControl)
160 if (pPlr->Control == iKbdSet)
161 return pPlr;
162 return nullptr;
163}
164
165C4Player *C4PlayerList::GetByInfoID(int iInfoID) const
166{
167 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
168 if (pPlr->ID == iInfoID) return pPlr;
169 return nullptr;
170}
171
172int C4PlayerList::GetCount() const
173{
174 int iCount = 0;
175 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
176 iCount++;
177 return iCount;
178}
179
180int C4PlayerList::GetCount(C4PlayerType eType) const
181{
182 int iCount = 0;
183 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
184 if (pPlr->GetType() == eType)
185 iCount++;
186 return iCount;
187}
188
189int C4PlayerList::GetFreeNumber() const
190{
191 int iNumber = -1;
192 bool fFree;
193 do
194 {
195 iNumber++; fFree = true;
196 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
197 if (pPlr->Number == iNumber)
198 fFree = false;
199 } while (!fFree);
200 return iNumber;
201}
202
203bool C4PlayerList::Remove(int iPlayer, bool fDisconnect, bool fNoCalls)
204{
205 return Remove(pPlr: Get(iNumber: iPlayer), fDisonnected: fDisconnect, fNoCalls);
206}
207
208bool C4PlayerList::RemoveUnjoined(int32_t iPlayer)
209{
210 // Savegame resume missing player: Remove player objects only
211 C4Object *pObj;
212 for (C4ObjectLink *clnk = Game.Objects.First; clnk && (pObj = clnk->Obj); clnk = clnk->Next)
213 if (pObj->Status)
214 if (pObj->IsPlayerObject(iPlayerNumber: iPlayer))
215 pObj->AssignRemoval(fExitContents: true);
216 return true;
217}
218
219bool C4PlayerList::Remove(C4Player *pPlr, bool fDisconnect, bool fNoCalls)
220{
221 if (!pPlr) return false;
222
223 // inform script
224 if (!fNoCalls)
225 Game.Script.GRBroadcast(PSF_RemovePlayer, pPars: {C4VInt(iVal: pPlr->Number), C4VInt(iVal: pPlr->Team)});
226
227 // Transfer ownership of other objects to team members
228 if (!fNoCalls) pPlr->NotifyOwnedObjects();
229
230 // NET2: update player info list
231 if (pPlr->ID)
232 {
233 C4PlayerInfo *pInfo = Game.PlayerInfos.GetPlayerInfoByID(id: pPlr->ID);
234 if (pInfo)
235 {
236 pInfo->SetRemoved();
237 if (fDisconnect)
238 pInfo->SetDisconnected();
239 }
240 // if player wasn't evaluated, store round results anyway
241 if (!pPlr->Evaluated) Game.RoundResults.EvaluatePlayer(pPlr);
242 }
243
244 C4Player *pPrev = First;
245 while (pPrev && pPrev->Next != pPlr) pPrev = pPrev->Next;
246 if (pPrev) pPrev->Next = pPlr->Next;
247 else First = pPlr->Next;
248
249 // Remove eliminated crew
250 if (!fNoCalls) pPlr->RemoveCrewObjects();
251
252 // Clear object info pointers
253 pPlr->CrewInfoList.DetachFromObjects();
254
255 // Clear viewports
256 Game.GraphicsSystem.CloseViewport(iPlayer: pPlr->Number, fSilent: fNoCalls);
257 // Check fullscreen viewports
258 FullScreen.ViewportCheck();
259
260 // Remove player
261 delete pPlr;
262
263 // Validate object owners
264 Game.Objects.ValidateOwners();
265
266 // Update console
267 Console.UpdateMenus();
268 return true;
269}
270
271C4Player *C4PlayerList::Join(const char *szFilename, bool fScenarioInit, int iAtClient, const char *szAtClientName, C4PlayerInfo *pInfo)
272{
273 assert(pInfo);
274
275 // safeties
276 if (szFilename && !*szFilename) szFilename = nullptr;
277
278 // Log
279 if (fScenarioInit)
280 {
281 Log(id: C4ResStrTableKey::IDS_PRC_JOINPLR, args: pInfo->GetName());
282 }
283 else
284 {
285 Log(id: C4ResStrTableKey::IDS_PRC_RECREATE, args: pInfo->GetName());
286 }
287
288 // Too many players
289 if (1) // replay needs to check, too!
290 if (GetCount() + 1 > Game.Parameters.MaxPlayers)
291 {
292 Log(id: C4ResStrTableKey::IDS_PRC_TOOMANYPLRS, args&: Game.Parameters.MaxPlayers);
293 return nullptr;
294 }
295
296 // Check duplicate file usage
297 if (szFilename) if (FileInUse(szFilename))
298 {
299 Log(id: C4ResStrTableKey::IDS_PRC_PLRFILEINUSE); return nullptr;
300 }
301
302 // Create
303 C4Player *pPlr = new C4Player;
304
305 // Append to player list
306 C4Player *pLast = First;
307 for (; pLast && pLast->Next; pLast = pLast->Next);
308 if (pLast) pLast->Next = pPlr; else First = pPlr;
309
310 // Init
311 if (!pPlr->Init(iNumber: GetFreeNumber(), iAtClient, szAtClientName, szFilename, fScenarioInit, pInfo))
312 {
313 Remove(pPlr, fDisconnect: false, fNoCalls: false); Log(id: C4ResStrTableKey::IDS_PRC_JOINFAIL); return nullptr;
314 }
315
316 // Done
317 return pPlr;
318}
319
320bool C4PlayerList::CtrlJoinLocalNoNetwork(const char *szFilename, int iAtClient, const char *szAtClientName)
321{
322 assert(!Game.Network.isEnabled());
323 // security
324 if (!ItemExists(szItemName: szFilename)) return false;
325 // join via player info
326 bool fSuccess = Game.PlayerInfos.DoLocalNonNetworkPlayerJoin(szPlayerFile: szFilename);
327
328 // Done
329 return fSuccess;
330}
331
332void SetClientPrefix(char *szFilename, const char *szClient)
333{
334 char szTemp[1024 + 1];
335 // Compose prefix
336 char szPrefix[1024 + 1];
337 SCopy(szSource: szClient, sTarget: szPrefix); SAppendChar(cChar: '-', szStr: szPrefix);
338 // Prefix already set?
339 SCopy(szSource: GetFilename(path: szFilename), sTarget: szTemp, iMaxL: SLen(sptr: szPrefix));
340 if (SEqualNoCase(szStr1: szTemp, szStr2: szPrefix)) return;
341 // Insert prefix
342 SCopy(szSource: GetFilename(path: szFilename), sTarget: szTemp);
343 SCopy(szSource: szPrefix, sTarget: GetFilename(path: szFilename));
344 SAppend(szSource: szTemp, szTarget: szFilename);
345}
346
347bool C4PlayerList::Save(C4Group &hGroup, bool fStoreTiny, const C4PlayerInfoList &rStoreList)
348{
349 StdStrBuf sTempFilename;
350 bool fSuccess = true;
351 // Save to external player files and add to group
352 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
353 {
354 // save only those in the list, and only those with a filename
355 C4PlayerInfo *pNfo = rStoreList.GetPlayerInfoByID(id: pPlr->ID);
356 if (!pNfo) continue;
357 if (!pNfo->GetFilename() || !*pNfo->GetFilename()) continue;
358 // save over original file?
359 bool fStoreOnOriginal = (!fStoreTiny && pNfo->GetType() == C4PT_User);
360 // Create temporary file
361 sTempFilename.Copy(pnData: Config.AtTempPath(szFilename: pNfo->GetFilename()));
362 if (fStoreOnOriginal)
363 if (!C4Group_CopyItem(szSource: pPlr->Filename, szTarget: sTempFilename.getData()))
364 return false;
365 // Open group
366 C4Group PlrGroup;
367 if (!PlrGroup.Open(szGroupName: sTempFilename.getData(), fCreate: !fStoreOnOriginal))
368 return false;
369 // Save player
370 if (!pPlr->Save(hGroup&: PlrGroup, fSavegame: true, fStoreTiny: fStoreOnOriginal)) return false;
371 PlrGroup.Close();
372 // Add temp file to group
373 if (!hGroup.Move(szFile: sTempFilename.getData(), szAddAs: pNfo->GetFilename())) return false;
374 }
375 return fSuccess;
376}
377
378bool C4PlayerList::Save(bool fSaveLocalOnly)
379{
380 // do not save in replays
381 if (Game.C4S.Head.Replay) return true;
382 // Save to external player files
383 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
384 if (pPlr->GetType() != C4PT_Script)
385 if (!fSaveLocalOnly || pPlr->LocalControl)
386 if (!pPlr->Save())
387 return false;
388 return true;
389}
390
391bool C4PlayerList::Evaluate()
392{
393 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
394 pPlr->Evaluate();
395 return true;
396}
397
398bool C4PlayerList::Retire(C4Player *pPlr)
399{
400 if (!pPlr) return false;
401
402 if (!pPlr->Evaluated)
403 {
404 pPlr->Evaluate();
405 if (!Game.Control.isReplay() && pPlr->GetType() != C4PT_Script) pPlr->Save();
406 }
407 Remove(pPlr, fDisconnect: false, fNoCalls: false);
408
409 return true;
410}
411
412int C4PlayerList::AverageValueGain() const
413{
414 int iResult = 0;
415 if (First)
416 {
417 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
418 iResult += std::max<int32_t>(a: pPlr->ValueGain, b: 0);
419 iResult /= GetCount();
420 }
421 return iResult;
422}
423
424C4Player *C4PlayerList::GetByName(const char *szName, int iExcluding) const
425{
426 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
427 if (SEqual(szStr1: pPlr->GetName(), szStr2: szName))
428 if (pPlr->Number != iExcluding)
429 return pPlr;
430 return nullptr;
431}
432
433bool C4PlayerList::FileInUse(const char *szFilename) const
434{
435 // Check original player files
436 C4Player *cPlr = First;
437 for (; cPlr; cPlr = cPlr->Next)
438 if (ItemIdentical(szFilename1: cPlr->Filename, szFilename2: szFilename))
439 return true;
440 // Compare to any network path player files with prefix (hack)
441 if (Game.Network.isEnabled())
442 {
443 char szWithPrefix[_MAX_PATH + 1];
444 SCopy(szSource: GetFilename(path: szFilename), sTarget: szWithPrefix);
445 SetClientPrefix(szFilename: szWithPrefix, szClient: Game.Clients.getLocalName());
446 for (cPlr = First; cPlr; cPlr = cPlr->Next)
447 if (SEqualNoCase(szStr1: GetFilename(path: cPlr->Filename), szStr2: szWithPrefix))
448 return true;
449 }
450 // Not in use
451 return false;
452}
453
454C4Player *C4PlayerList::GetLocalByIndex(int iIndex) const
455{
456 int cindex = 0;
457 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
458 if (pPlr->LocalControl)
459 {
460 if (cindex == iIndex) return pPlr;
461 cindex++;
462 }
463 return nullptr;
464}
465
466bool C4PlayerList::RemoveAtClient(int iClient, bool fDisconnect)
467{
468 C4Player *pPlr;
469 // Get players
470 while (pPlr = GetAtClient(iClient))
471 {
472 // Log
473 Log(id: C4ResStrTableKey::IDS_PRC_REMOVEPLR, args: pPlr->GetName());
474 // Remove
475 Remove(pPlr, fDisconnect, fNoCalls: false);
476 }
477 return true;
478}
479
480bool C4PlayerList::CtrlRemove(int iPlayer, bool fDisconnect)
481{
482 // Add packet to input
483 Game.Input.Add(eType: CID_RemovePlr, pCtrl: new C4ControlRemovePlr(iPlayer, fDisconnect));
484 return true;
485}
486
487C4Player *C4PlayerList::GetAtClient(int iClient, int iIndex) const
488{
489 int cindex = 0;
490 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
491 if (pPlr->AtClient == iClient)
492 {
493 if (cindex == iIndex) return pPlr;
494 cindex++;
495 }
496 return nullptr;
497}
498
499bool C4PlayerList::RemoveAtRemoteClient(bool fDisconnect, bool fNoCalls)
500{
501 C4Player *pPlr;
502 // Get players
503 while (pPlr = GetAtRemoteClient())
504 {
505 // Log
506 Log(id: C4ResStrTableKey::IDS_PRC_REMOVEPLR, args: pPlr->GetName());
507 // Remove
508 Remove(pPlr, fDisconnect, fNoCalls);
509 }
510 return true;
511}
512
513C4Player *C4PlayerList::GetAtRemoteClient(int iIndex) const
514{
515 int cindex = 0;
516 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
517 if (pPlr->AtClient != Game.Control.ClientID())
518 {
519 if (cindex == iIndex) return pPlr;
520 cindex++;
521 }
522 return nullptr;
523}
524
525bool C4PlayerList::RemoveLocal(bool fDisconnect, bool fNoCalls)
526{
527 // (used by league system the set local fate)
528 C4Player *pPlr;
529 do
530 for (pPlr = First; pPlr; pPlr = pPlr->Next)
531 if (pPlr->LocalControl)
532 {
533 // Log
534 Log(id: C4ResStrTableKey::IDS_PRC_REMOVEPLR, args: pPlr->GetName());
535 // Remove
536 Remove(pPlr, fDisconnect, fNoCalls);
537 break;
538 }
539 while (pPlr);
540
541 return true;
542}
543
544void C4PlayerList::EnumeratePointers()
545{
546 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
547 pPlr->EnumeratePointers();
548}
549
550void C4PlayerList::DenumeratePointers()
551{
552 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
553 pPlr->DenumeratePointers();
554}
555
556bool C4PlayerList::MouseControlTaken() const
557{
558 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
559 if (pPlr->MouseControl)
560 if (pPlr->LocalControl)
561 return true;
562 return false;
563}
564
565int C4PlayerList::GetCountNotEliminated() const
566{
567 int iCount = 0;
568 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
569 if (!pPlr->Eliminated)
570 iCount++;
571 return iCount;
572}
573
574bool C4PlayerList::SynchronizeLocalFiles()
575{
576 // message
577 Log(id: C4ResStrTableKey::IDS_PRC_SYNCPLRS);
578 bool fSuccess = true;
579 // check all players
580 for (C4Player *pPlr = First; pPlr; pPlr = pPlr->Next)
581 // eliminated players will be saved soon, anyway
582 if (!pPlr->Eliminated)
583 if (!pPlr->LocalSync()) fSuccess = false;
584 // done
585 return fSuccess;
586}
587
588void C4PlayerList::ClearLocalPlayerPressedComs()
589{
590 C4Player *plr;
591 for (int cnt = 0; plr = Game.Players.GetLocalByIndex(iIndex: cnt); cnt++)
592 {
593 plr->ClearPressedComsSynced();
594 }
595}
596
597void C4PlayerList::RecheckPlayerSort(C4Player *pForPlayer)
598{
599 if (!pForPlayer || !First) return;
600 int iNumber = pForPlayer->Number;
601 // get entry that should be the previous
602 // (use '<=' to run past pForPlayer itself)
603 C4Player *pPrev = First;
604 while (pPrev->Next && pPrev->Next->Number <= iNumber)
605 pPrev = pPrev->Next;
606 // if it's correctly sorted, pPrev should point to pForPlayer itself
607 if (pPrev == pForPlayer) return;
608 // otherwise, pPrev points to the entry that should be the new previous
609 // or to First if pForPlayer should be the head entry
610 // re-link it there
611 // first remove from old link pos
612 C4Player **pOldLink = &First;
613 while (*pOldLink && *pOldLink != pForPlayer) pOldLink = &((*pOldLink)->Next);
614 if (*pOldLink) *pOldLink = pForPlayer->Next;
615 // then link into new
616 if (pPrev == First && pPrev->Number > iNumber)
617 {
618 // at head
619 pForPlayer->Next = pPrev;
620 First = pForPlayer;
621 }
622 else
623 {
624 // after prev
625 pForPlayer->Next = pPrev->Next;
626 pPrev->Next = pForPlayer;
627 }
628}
629