1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2008, Sven2
6 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// player listbox used in lobby and game over dlg
19
20#include "C4GuiComboBox.h"
21#include "C4GuiResource.h"
22#include <C4Include.h>
23#include <C4PlayerInfoListBox.h>
24
25#include <C4PlayerInfo.h>
26#include <C4Network2Dialogs.h>
27#include <C4GameDialogs.h>
28#include <C4Teams.h>
29#include <C4Game.h>
30#include <C4FileSelDlg.h>
31
32#include <format>
33
34uint32_t GenerateRandomPlayerColor(int32_t iTry); // in C4PlayerInfoConflicts.cpp
35
36// ListItem
37
38// helper
39C4GameLobby::MainDlg *C4PlayerInfoListBox::ListItem::GetLobby() const
40{
41 return Game.Network.GetLobby();
42}
43
44bool C4PlayerInfoListBox::ListItem::CanLocalChooseTeams(int32_t idPlayer) const
45{
46 // whether the local client can change any teams
47 // only if teams are available
48 if (!Game.Teams.IsMultiTeams()) return false;
49 // only if global change allowed
50 C4GameLobby::MainDlg *pLobby = GetLobby();
51 if (!pLobby) return false;
52 if (pLobby->IsCountdown()) return false;
53 // only for unjoined players
54 if (idPlayer)
55 {
56 C4PlayerInfo *pInfo = Game.PlayerInfos.GetPlayerInfoByID(id: idPlayer);
57 if (pInfo && pInfo->HasJoined()) return false;
58 }
59 // finally, only if team settings permit
60 return Game.Teams.CanLocalChooseTeam(idPlayer);
61}
62
63void C4PlayerInfoListBox::ListItem::DrawElement(C4FacetEx &cgo)
64{
65 if (dwBackground) lpDDraw->DrawBoxDw(sfcDest: cgo.Surface, iX1: cgo.TargetX + rcBounds.x, iY1: cgo.TargetY + rcBounds.y, iX2: cgo.TargetX + rcBounds.x + rcBounds.Wdt - 1, iY2: cgo.TargetY + rcBounds.y + rcBounds.Hgt - 1, dwClr: dwBackground);
66 typedef C4GUI::Window BaseClass;
67 BaseClass::DrawElement(cgo);
68}
69
70// PlayerListItem
71
72C4PlayerInfoListBox::PlayerListItem::PlayerListItem(C4PlayerInfoListBox *pForListBox, int32_t idClient,
73 int32_t idPlayer, bool fSavegamePlayer, C4GUI::Element *pInsertBeforeElement)
74 : ListItem(pForListBox), pTeamCombo(nullptr), fIconSet(false), fJoinedInfoSet(false), dwJoinClr(0), dwPlrClr(0),
75 idClient(idClient), idPlayer(idPlayer), fFreeSavegamePlayer(fSavegamePlayer),
76 pScoreLabel(nullptr), pRankIcon(nullptr), pTimeLabel(nullptr), pTeamPic(nullptr), pExtraLabel(nullptr)
77
78{
79 bool fIsEvaluation = pForListBox->IsEvaluation(), fIsLobby = pForListBox->IsLobby();
80 C4PlayerInfo *pInfo = GetPlayerInfo(); assert(pInfo);
81 uint32_t dwTextColor = pForListBox->GetTextColor();
82 CStdFont *pCustomFont = pForListBox->GetCustomFont();
83 uint32_t dwPlayerColor;
84 if (fIsEvaluation)
85 dwPlayerColor = dwTextColor;
86 else
87 dwPlayerColor = pInfo->GetLobbyColor() | C4GUI_MessageFontAlpha;
88 // league account name? Overwrite the shown name
89 StdStrBuf sPlayerName(pInfo->GetLobbyName());
90 // calc height
91 int32_t iHeight = C4GUI::GetRes()->TextFont.GetLineHeight() + C4GUI::ComboBox::GetDefaultHeight() + 3 * IconLabelSpacing;
92 // create subcomponents
93 pIcon = new C4GUI::Icon(C4Rect(0, 0, iHeight, iHeight), C4GUI::Ico_UnknownPlayer);
94 if (Game.Parameters.isLeague())
95 pRankIcon = new C4GUI::Icon(C4Rect(0, 0, C4GUI::ComboBox::GetDefaultHeight(), C4GUI::ComboBox::GetDefaultHeight()), C4GUI::Ico_None);
96 if (Game.Teams.IsMultiTeams() && !(fIsEvaluation && pList->IsTeamFilter()))
97 {
98 // will be moved when the item is added to the list, and the position moved
99 pTeamCombo = new C4GUI::ComboBox(C4Rect(0, 0, 10, 10));
100 pTeamCombo->SetComboCB(new C4GUI::ComboBox_FillCallback<PlayerListItem>(this, &PlayerListItem::OnTeamComboFill, &PlayerListItem::OnTeamComboSelChange));
101 pTeamCombo->SetSimple(true);
102 if (pCustomFont)
103 {
104 pTeamCombo->SetFont(pCustomFont);
105 pTeamCombo->SetColors(dwFontClr: dwTextColor, C4GUI_StandardBGColor, dwBorderClr: 0);
106 }
107 UpdateTeam();
108 }
109 // Evaluation
110 if (fIsEvaluation)
111 {
112 // Team image if known and if not placed on top of box anyway
113 if (!pList->IsTeamFilter())
114 {
115 C4Team *pTeam = Game.Teams.GetTeamByID(iID: pInfo->GetTeam());
116 if (pTeam && pTeam->GetIconSpec() && *pTeam->GetIconSpec())
117 {
118 pTeamPic = new C4GUI::Picture(C4Rect(iHeight + IconLabelSpacing, 0, iHeight, iHeight), true);
119 Game.DrawTextSpecImage(fctTarget&: pTeamPic->GetMFacet(), szSpec: pTeam->GetIconSpec(), dwClr: pTeam->GetColor());
120 pTeamPic->SetDrawColor(pTeam->GetColor());
121 }
122 }
123 // Total playing time (not in team filter because then the second line is taken by the score label)
124 if (!pList->IsTeamFilter())
125 {
126 C4RoundResultsPlayer *pRoundResultsPlr = Game.RoundResults.GetPlayers().GetByID(id: idPlayer);
127 uint32_t iTimeTotal = pRoundResultsPlr ? pRoundResultsPlr->GetTotalPlayingTime() : 0 /* unknown - should not happen */;
128 const std::string timeLabelText{LoadResStr(id: C4ResStrTableKey::IDS_CTL_TOTALPLAYINGTIME, args: iTimeTotal / 3600, args: (iTimeTotal / 60) % 60, args: iTimeTotal % 60)};
129 pTimeLabel = new C4GUI::Label(timeLabelText.c_str(), 0, 0, ARight, dwTextColor, pForListBox->GetCustomFont(), false, true);
130 }
131 // Extra info set by script
132 C4RoundResultsPlayer *pEvaluationPlayer = Game.RoundResults.GetPlayers().GetByID(id: idPlayer);
133 if (pEvaluationPlayer)
134 {
135 const char *szCustomEval = pEvaluationPlayer->GetCustomEvaluationStrings();
136 if (szCustomEval && *szCustomEval)
137 {
138 pExtraLabel = new C4GUI::Label(szCustomEval, 0, 0, ARight); // positioned later
139 iHeight += C4GUI::GetRes()->TextFont.GetLineHeight();
140 }
141 }
142 }
143 pNameLabel = new C4GUI::Label(sPlayerName.getData(), (iHeight + IconLabelSpacing) * (1 + !!pTeamPic), IconLabelSpacing, ALeft, dwPlayerColor, pCustomFont, !fIsEvaluation, true);
144 // calc own bounds - list box needs height only; width and pos will be moved by list
145 SetBounds(C4Rect(0, 0, 10, iHeight));
146 // add components
147 AddElement(pChild: pIcon); AddElement(pChild: pNameLabel);
148 if (pTeamPic) AddElement(pChild: pTeamPic);
149 if (pTimeLabel) AddElement(pChild: pTimeLabel);
150 if (pTeamCombo) AddElement(pChild: pTeamCombo);
151 if (pRankIcon) AddElement(pChild: pRankIcon);
152 if (pExtraLabel) AddElement(pChild: pExtraLabel);
153 // add to listbox (will get resized horizontally and moved)
154 pForListBox->InsertElement(pChild: this, pInsertBefore: pInsertBeforeElement, iIndent: PlayerListBoxIndent);
155 // league score update
156 UpdateScoreLabel(pInfo);
157 // set ID
158 if (fFreeSavegamePlayer)
159 idListItemID.idType = ListItem::ID::PLI_SAVEGAMEPLR;
160 else
161 idListItemID.idType = ListItem::ID::PLI_PLAYER;
162 idListItemID.id = idPlayer;
163 UpdateIcon(pInfo, pJoinedInfo: GetJoinedInfo());
164 // context menu for list item
165 if (fIsLobby) SetContextHandler(new C4GUI::CBContextHandler<PlayerListItem>(this, &PlayerListItem::OnContext));
166 // update collapsed/not collapsed
167 fShownCollapsed = false;
168 UpdateCollapsed();
169
170 if (Game.Network.isHost() && Game.RestartRestoreInfos.What & C4NetworkRestartInfos::PlayerTeams && pInfo->GetType() == C4PT_User)
171 {
172 if (const auto restoreInfo = Game.RestartRestoreInfos.Players.find(x: sPlayerName.getData()); restoreInfo != Game.RestartRestoreInfos.Players.end() && restoreInfo->second.team != pInfo->GetTeam())
173 {
174 C4ClientPlayerInfos infoRequest{*Game.PlayerInfos.GetInfoByClientID(iClientID: idClient)};
175 C4PlayerInfo *info = infoRequest.GetPlayerInfoByID(id: idPlayer);
176 const auto team = restoreInfo->second.team;
177 Game.Teams.GetGenerateTeamByID(iID: team);
178 info->SetTeam(team);
179 Game.Network.Players.RequestPlayerInfoUpdate(rRequest: infoRequest);
180 }
181 }
182}
183
184void C4PlayerInfoListBox::PlayerListItem::UpdateOwnPos()
185{
186 // parent for client rect
187 typedef C4GUI::Window ParentClass;
188 ParentClass::UpdateOwnPos();
189 C4GUI::ComponentAligner caBounds(GetContainedClientRect(), IconLabelSpacing, IconLabelSpacing);
190 // subtract icon(s)
191 caBounds.GetFromLeft(iWdt: pIcon->GetBounds().Wdt);
192 if (pTeamPic) caBounds.GetFromLeft(iWdt: pTeamPic->GetBounds().Wdt - IconLabelSpacing);
193 C4Rect rcExtraDataRect;
194 // extra data label area
195 if (pExtraLabel) rcExtraDataRect = caBounds.GetFromBottom(iHgt: C4GUI::GetRes()->TextFont.GetLineHeight());
196 // second line (team+rank)
197 C4GUI::ComponentAligner caTeamArea(caBounds.GetFromBottom(iHgt: C4GUI::ComboBox::GetDefaultHeight()), 0, 0);
198 C4Rect rcRankIcon;
199 if (pList->IsEvaluation())
200 {
201 if (pRankIcon)
202 {
203 rcRankIcon = caBounds.GetFromRight(iWdt: caBounds.GetInnerHeight());
204 if (pExtraLabel) rcExtraDataRect.Wdt = caBounds.GetInnerWidth(); // In evaluation view, rank icon has its own coloumn
205 }
206 }
207 else
208 {
209 if (pRankIcon) rcRankIcon = caTeamArea.GetFromRight(iWdt: caTeamArea.GetInnerHeight());
210 }
211 C4Rect rcTeam = caTeamArea.GetAll();
212 // item to positions: team combo box
213 if (pTeamCombo)
214 {
215 pTeamCombo->SetBounds(rcTeam);
216 }
217 // rank icon
218 if (pRankIcon)
219 {
220 pRankIcon->SetBounds(rcRankIcon);
221 }
222 // time label
223 if (pTimeLabel)
224 {
225 C4Rect rcUpperBounds = caBounds.GetAll();
226 pTimeLabel->SetBounds(rcTeam);
227 pTimeLabel->SetX0(rcUpperBounds.x + rcUpperBounds.Wdt);
228 }
229 // extra label
230 if (pExtraLabel) pExtraLabel->SetBounds(rcExtraDataRect);
231}
232
233int32_t C4PlayerInfoListBox::PlayerListItem::GetListItemTopSpacing()
234{
235 int32_t iSpacing = C4GUI_DefaultListSpacing;
236 // evaluation: Add some extra spacing between players of different teams
237 if (pList->IsEvaluation())
238 {
239 C4GUI::Element *pPrevItem = GetPrev();
240 if (pPrevItem)
241 {
242 C4PlayerInfoListBox::ListItem *pPrevListItem = static_cast<C4PlayerInfoListBox::ListItem *>(pPrevItem);
243 if (pPrevListItem->idListItemID.idType == ListItem::ID::PLI_PLAYER)
244 {
245 PlayerListItem *pPrevPlayerListItem = static_cast<C4PlayerInfoListBox::PlayerListItem *>(pPrevListItem);
246 C4PlayerInfo *pThisInfo = GetPlayerInfo();
247 C4PlayerInfo *pPrevInfo = pPrevPlayerListItem->GetPlayerInfo();
248 if (pThisInfo && pPrevInfo)
249 {
250 if (pPrevInfo->GetTeam() != pThisInfo->GetTeam())
251 {
252 iSpacing += 10;
253 }
254 }
255 }
256 }
257 }
258 return iSpacing;
259}
260
261void C4PlayerInfoListBox::PlayerListItem::UpdateIcon(C4PlayerInfo *pInfo, C4PlayerInfo *pJoinedInfo)
262{
263 // check whether icon is known
264 bool fResPresent = false;
265 C4Network2Res *pRes = nullptr;
266 if (pInfo)
267 if (pRes = pInfo->GetRes())
268 fResPresent = pRes->isComplete();
269 C4RoundResultsPlayer *pEvaluationPlayer = nullptr;
270 if (pList->IsEvaluation()) pEvaluationPlayer = Game.RoundResults.GetPlayers().GetByID(id: idPlayer);
271 bool fHasIcon = fResPresent || pEvaluationPlayer || (!Game.Network.isEnabled() && pInfo);
272 // check whether joined info is present
273 bool fHasJoinedInfo = !!pJoinedInfo;
274 uint32_t dwJoinedInfoClr = pJoinedInfo ? pJoinedInfo->GetLobbyColor() : 0;
275 uint32_t dwPlayerClr = pInfo ? pInfo->GetLobbyColor() : 0;
276 // already up-to-date?
277 if (fHasIcon == fIconSet && fJoinedInfoSet == fHasJoinedInfo && dwJoinedInfoClr == dwJoinClr && dwPlayerClr == dwPlrClr) return;
278 // update then
279 // redraw player icon
280 if (fHasIcon)
281 {
282 // custom icon?
283 if (pEvaluationPlayer && pEvaluationPlayer->GetBigIcon().Surface)
284 {
285 pIcon->SetFacet(pEvaluationPlayer->GetBigIcon());
286 fIconSet = true;
287 }
288 else
289 fIconSet = pInfo->LoadBigIcon(fctTarget&: pIcon->GetMFacet());
290 if (!fIconSet)
291 {
292 // no custom icon: create default by player color
293 pIcon->GetMFacet().Create(C4GUI_IconWdt, C4GUI_IconHgt);
294 Game.GraphicsResource.fctPlayerClr.DrawClr(cgo&: pIcon->GetMFacet(), fAspect: true, dwClr: dwPlayerClr);
295 }
296 fIconSet = true;
297 }
298 else
299 // no player info known - either res not retrieved yet or script player
300 pIcon->SetIcon((pInfo && pInfo->GetType() == C4PT_Script) ? C4GUI::Ico_Host : C4GUI::Ico_UnknownPlayer);
301 // join
302 if (fHasJoinedInfo)
303 {
304 // make sure we're not drawing on GraphicsRessource
305 if (!pIcon->EnsureOwnSurface()) return;
306 // draw join info
307 C4Facet fctDraw = pIcon->GetFacet();
308 int32_t iSizeMax = std::max<int32_t>(a: fctDraw.Wdt, b: fctDraw.Hgt);
309 int32_t iCrewClrHgt = iSizeMax / 2;
310 fctDraw.Hgt -= iCrewClrHgt; fctDraw.Y += iCrewClrHgt;
311 fctDraw.Wdt = iSizeMax / 2;
312 fctDraw.X = 2;
313 // shadow
314 uint32_t dwPrevMod; bool fPrevMod = lpDDraw->GetBlitModulation(rdwColor&: dwPrevMod);
315 Application.DDraw->ActivateBlitModulation(dwWithClr: 1);
316 Game.GraphicsResource.fctCrewClr.DrawClr(cgo&: fctDraw, fAspect: true, dwClr: dwJoinedInfoClr);
317 if (fPrevMod) lpDDraw->ActivateBlitModulation(dwWithClr: dwPrevMod); else lpDDraw->DeactivateBlitModulation();
318 fctDraw.X = 0;
319 // gfx
320 Game.GraphicsResource.fctCrewClr.DrawClr(cgo&: fctDraw, fAspect: true, dwClr: dwJoinedInfoClr);
321 }
322 fJoinedInfoSet = fHasJoinedInfo;
323 dwJoinClr = dwJoinedInfoClr;
324 dwPlrClr = dwPlayerClr;
325}
326
327void C4PlayerInfoListBox::PlayerListItem::UpdateTeam()
328{
329 if (!pTeamCombo) return; // unassigned for no teams
330 const char *szTeamName = ""; bool fReadOnly = true;
331 fReadOnly = !CanLocalChooseTeam();
332 int32_t idTeam; C4Team *pTeam;
333 C4PlayerInfo *pInfo = GetPlayerInfo();
334 if (!Game.Teams.CanLocalSeeTeam())
335 szTeamName = LoadResStr(id: C4ResStrTableKey::IDS_MSG_RNDTEAM);
336 else if (pInfo)
337 if (idTeam = pInfo->GetTeam())
338 if (pTeam = Game.Teams.GetTeamByID(iID: idTeam))
339 szTeamName = pTeam->GetName();
340 pTeamCombo->SetText(szTeamName);
341 pTeamCombo->SetReadOnly(fReadOnly);
342}
343
344void C4PlayerInfoListBox::PlayerListItem::UpdateScoreLabel(C4PlayerInfo *pInfo)
345{
346 assert(pInfo);
347 C4RoundResultsPlayer *pRoundResultsPlr = nullptr;
348 if (pList->IsEvaluation()) pRoundResultsPlr = Game.RoundResults.GetPlayers().GetByID(id: idPlayer);
349
350 if (pInfo->getLeagueScore() || pInfo->IsLeagueProjectedGainValid() || pRoundResultsPlr)
351 {
352 int32_t iScoreRightPos = ((pRankIcon && pList->IsEvaluation()) ? pRankIcon->GetBounds().x : GetBounds().Wdt) - IconLabelSpacing;
353 int32_t iScoreYPos = IconLabelSpacing;
354 // if evaluation and team lists, move score label into second line - TODO: some hack only, still needs to be done right
355
356 C4RoundResultsPlayer *pEvaluationPlayer = Game.RoundResults.GetPlayers().GetByID(id: pInfo->GetID());
357 bool fPlayerHasEvaluationData = false;
358 if (pEvaluationPlayer)
359 {
360 const char *szCustomEval = pEvaluationPlayer->GetCustomEvaluationStrings();
361 if (szCustomEval && *szCustomEval)
362 fPlayerHasEvaluationData = true;
363 }
364 if (pList->IsEvaluation() && pList->IsTeamFilter())
365 iScoreYPos = GetBounds().Hgt - (C4GUI::ComboBox::GetDefaultHeight() * (1 + fPlayerHasEvaluationData)) - IconLabelSpacing;
366
367 // score label visible
368 if (!pScoreLabel)
369 {
370 AddElement(pChild: pScoreLabel = new C4GUI::Label("", iScoreRightPos, iScoreYPos, ARight, pList->GetTextColor(), pList->GetCustomFont(), false));
371 if (pList->IsEvaluation())
372 pScoreLabel->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_OLDANDNEWSCORE));
373 else
374 pScoreLabel->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_LEAGUESCOREANDPROJECTEDGA));
375 }
376 std::string text;
377 // Evaluation (GameOver)
378 if (pList->IsEvaluation())
379 {
380 if (pInfo->getLeagueScore() || pInfo->IsLeagueProjectedGainValid() || (pRoundResultsPlr && pRoundResultsPlr->IsLeagueScoreNewValid()))
381 {
382 if (pRoundResultsPlr && pRoundResultsPlr->IsLeagueScoreNewValid())
383 {
384 // Show old league score, gain, and new league score
385 // Normally, the league server should make sure that [Old score] + [Gain] == [New score]
386 int32_t iOldScore = pInfo->getLeagueScore(), iScoreGain = pRoundResultsPlr->GetLeagueScoreGain(), iNewScore = pRoundResultsPlr->GetLeagueScoreNew();
387 int32_t iDiscrepancy = iNewScore - (iOldScore + iScoreGain);
388 if (!iDiscrepancy)
389 {
390 text = std::format(fmt: "{{{{Ico:League}}}}<c afafaf>{} ({:+})</c> {} {}", args&: iOldScore, args&: iScoreGain, args&: iNewScore, args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SCORE));
391 }
392 else
393 {
394 // If there's a discrepancy, there must have been some kind of admin intervention during the game - display it in red!
395 text = std::format(fmt: "{{{{Ico:League}}}}<c afafaf>{} ({:+})</c><c ff0000>({:+})</c> {} {}", args&: iOldScore, args&: iScoreGain, args&: iDiscrepancy, args&: iNewScore, args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SCORE));
396 }
397 }
398 // Show old league score only
399 else
400 {
401 text = std::format(fmt: "{{{{Ico:League}}}}<c afafaf>({})</c> {}", args: pInfo->getLeagueScore(), args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SCORE));
402 }
403 }
404 else if (pRoundResultsPlr && pRoundResultsPlr->IsScoreNewValid() && !Game.RoundResults.SettlementScoreIsHidden())
405 {
406 // new score known
407 text = std::format(fmt: "{{{{Ico:Settlement}}}}<c afafaf>{} ({:+})</c> {} {}", args: pRoundResultsPlr->GetScoreOld(), args: pRoundResultsPlr->GetScoreNew() - pRoundResultsPlr->GetScoreOld(), args: pRoundResultsPlr->GetScoreNew(), args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SCORE));
408 }
409 else if (pRoundResultsPlr && !pRoundResultsPlr->IsScoreNewValid() && !Game.RoundResults.SettlementScoreIsHidden())
410 {
411 // only old score known (e.g., player disconnected)
412 text = std::format(fmt: "{{{{Ico:Settlement}}}}<c afafaf>({})</c> {}", args: pRoundResultsPlr->GetScoreOld(), args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SCORE));
413 }
414 else
415 {
416 // nothing known. Shouldn't really happen.
417 }
418 }
419 // Pre-evaluation (Lobby)
420 else
421 {
422 // Show current league score and projected gain
423 // Don't show if team invisible, so random surprise teams don't get spoiled
424 if (pInfo->IsLeagueProjectedGainValid() && Game.Teams.IsTeamVisible())
425 text = std::format(fmt: "{} ({:+})", args: pInfo->getLeagueScore(), args: pInfo->GetLeagueProjectedGain());
426 // Show current league score only
427 else
428 text = std::format(fmt: "{}", args: pInfo->getLeagueScore());
429 }
430 pScoreLabel->SetX0(iScoreRightPos);
431 pScoreLabel->SetText(szText: text.c_str(), fAllowHotkey: false);
432 }
433 else
434 {
435 // score label invisible
436 delete pScoreLabel;
437 pScoreLabel = nullptr;
438 }
439 if (pRankIcon)
440 {
441 int32_t iSym = 0;
442 if (pRoundResultsPlr && pRoundResultsPlr->IsLeagueScoreNewValid())
443 iSym = pRoundResultsPlr->GetLeagueRankSymbolNew();
444 if (!iSym)
445 iSym = pInfo->getLeagueRankSymbol();
446 if (iSym && !fShownCollapsed)
447 {
448 C4GUI::Icons eRankIcon = static_cast<C4GUI::Icons>(C4GUI::Ico_Rank1 + BoundBy<int32_t>(bval: iSym - 1, lbound: 0, rbound: C4GUI::Ico_Rank9 - C4GUI::Ico_Rank1));
449 pRankIcon->SetVisibility(true);
450 pRankIcon->SetIcon(eRankIcon);
451 }
452 else
453 {
454 pRankIcon->SetVisibility(false);
455 }
456 }
457}
458
459void C4PlayerInfoListBox::PlayerListItem::UpdateCollapsed()
460{
461 bool fShouldBeCollapsed = pList->IsPlayerItemCollapsed(pItem: this);
462 if (fShouldBeCollapsed == fShownCollapsed) return;
463 // so update collapsed state
464 int32_t iHeight; int32_t iNameLblX0;
465 if (fShownCollapsed = fShouldBeCollapsed)
466 {
467 // calc height
468 iHeight = C4GUI::GetRes()->TextFont.GetLineHeight() + 2 * IconLabelSpacing;
469 // teamcombo not visible if collapsed
470 if (pTeamCombo) pTeamCombo->SetVisibility(false);
471 }
472 else
473 {
474 // calc height
475 iHeight = C4GUI::GetRes()->TextFont.GetLineHeight() + C4GUI::ComboBox::GetDefaultHeight() + 3 * IconLabelSpacing;
476 // teamcombo visible if not collapsed
477 if (pTeamCombo) pTeamCombo->SetVisibility(true);
478 }
479 // update subcomponents
480 iNameLblX0 = iHeight + IconLabelSpacing;
481 pIcon->GetBounds() = C4Rect(0, 0, iHeight, iHeight);
482 pIcon->UpdateOwnPos();
483 pNameLabel->SetX0(iNameLblX0);
484 // calc own bounds - use icon bounds only, because only the height is used when the item is added
485 SetBounds(pIcon->GetBounds());
486 // update positions
487 pList->UpdateElementPosition(pOfElement: this, iIndent: PlayerListBoxIndent);
488}
489
490C4GUI::ContextMenu *C4PlayerInfoListBox::PlayerListItem::OnContext(C4GUI::Element *pListItem, int32_t iX, int32_t iY)
491{
492 C4PlayerInfo *pInfo = GetPlayerInfo();
493 assert(pInfo);
494 // no context menu for evaluation
495 if (!GetLobby()) return nullptr;
496 // create context menu
497 C4GUI::ContextMenu *pMenu = new C4GUI::ContextMenu();
498 // if this is a free player, add an option to take it over
499 if (fFreeSavegamePlayer)
500 {
501 if (pInfo->GetType() != C4PT_Script)
502 {
503 StdStrBuf strTakeOver(LoadResStr(id: C4ResStrTableKey::IDS_MSG_TAKEOVERPLR));
504 pMenu->AddItem(szText: strTakeOver.getData(), szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_MSG_TAKEOVERPLR_DESC), icoIcon: C4GUI::Ico_Player, pMenuHandler: nullptr,
505 pSubmenuHandler: new C4GUI::CBContextHandler<PlayerListItem>(this, &PlayerListItem::OnContextTakeOver));
506 }
507 }
508 else
509 {
510 // owned players or host can manipulate players
511 if (Game.Network.isHost() || IsLocalClientPlayer())
512 {
513 // player removal (except for joined script players)
514 if (pInfo->GetType() != C4PT_Script || !pInfo->GetAssociatedSavegamePlayerID())
515 {
516 StdStrBuf strRemove(LoadResStr(id: C4ResStrTableKey::IDS_MSG_REMOVEPLR));
517 pMenu->AddItem(szText: strRemove.getData(), szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_MSG_REMOVEPLR_DESC), icoIcon: C4GUI::Ico_Close,
518 pMenuHandler: new C4GUI::CBMenuHandler<PlayerListItem>(this, &PlayerListItem::OnCtxRemove), pSubmenuHandler: nullptr);
519 }
520 // color was changed: Add option to assign a new color
521 C4PlayerInfo *pInfo = GetPlayerInfo();
522 assert(pInfo);
523 if (pInfo && pInfo->HasAutoGeneratedColor() && (!Game.Teams.IsTeamColors() || !pInfo->GetTeam()))
524 {
525 StdStrBuf strNewColor(LoadResStr(id: C4ResStrTableKey::IDS_MSG_NEWPLRCOLOR));
526 pMenu->AddItem(szText: strNewColor.getData(), szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_MSG_NEWPLRCOLOR_DESC), icoIcon: C4GUI::Ico_Player,
527 pMenuHandler: new C4GUI::CBMenuHandler<PlayerListItem>(this, &PlayerListItem::OnCtxNewColor), pSubmenuHandler: nullptr);
528 }
529 }
530 }
531 // open it
532 return pMenu;
533}
534
535C4GUI::ContextMenu *C4PlayerInfoListBox::PlayerListItem::OnContextTakeOver(C4GUI::Element *pListItem, int32_t iX, int32_t iY)
536{
537 // create context menu
538 C4GUI::ContextMenu *pMenu = new C4GUI::ContextMenu();
539 // add options for all own, unassigned players
540 C4ClientPlayerInfos *pkInfo = Game.Network.Players.GetLocalPlayerInfoPacket();
541 if (pkInfo)
542 {
543 int32_t i = 0; C4PlayerInfo *pInfo;
544 while (pInfo = pkInfo->GetPlayerInfo(iIndex: i++))
545 if (!pInfo->HasJoinIssued())
546 if (!pInfo->GetAssociatedSavegamePlayerID())
547 {
548 pMenu->AddItem(szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_USINGPLR, args: pInfo->GetName()).c_str(), szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_MSG_USINGPLR_DESC), icoIcon: C4GUI::Ico_Player,
549 pMenuHandler: new C4GUI::CBMenuHandlerEx<PlayerListItem, int32_t>(this, &PlayerListItem::OnCtxTakeOver, pInfo->GetID()));
550 }
551 }
552 // 2do: add option to use a new one...
553 // 2do: add option to take over from savegame player
554 // open it
555 return pMenu;
556}
557
558void C4PlayerInfoListBox::PlayerListItem::OnCtxTakeOver(C4GUI::Element *pListItem, const int32_t &idPlayer)
559{
560 // use player idPlayer to take over this one
561 // this must be processed as a request by the host
562 // some safety first...
563 C4ClientPlayerInfos *pLocalInfo = Game.Network.Players.GetLocalPlayerInfoPacket();
564 if (!fFreeSavegamePlayer || !idPlayer || !pLocalInfo) return;
565 C4ClientPlayerInfos LocalInfoRequest(*pLocalInfo);
566 C4PlayerInfo *pGrabbingInfo = LocalInfoRequest.GetPlayerInfoByID(id: idPlayer);
567 if (!pGrabbingInfo) return;
568 // now adjust info packet
569 pGrabbingInfo->SetAssociatedSavegamePlayer(this->idPlayer);
570 // and request this update (host processes it directly)
571 Game.Network.Players.RequestPlayerInfoUpdate(rRequest: LocalInfoRequest);
572}
573
574void C4PlayerInfoListBox::PlayerListItem::OnCtxRemove(C4GUI::Element *pListItem)
575{
576 // only host or own player
577 if (!Game.Network.isEnabled() || (!Game.Network.isHost() && !IsLocalClientPlayer())) return;
578 // check countdown state
579 if (GetLobby()->IsCountdown())
580 if (Game.Network.isHost())
581 Game.Network.AbortLobbyCountdown();
582 else
583 return;
584 // remove the player
585 // this must be processed as a request by the host
586 // now change it in its own request packet
587 C4ClientPlayerInfos *pChangeInfo = Game.PlayerInfos.GetInfoByClientID(iClientID: idClient);
588 if (!pChangeInfo || !idPlayer) return;
589 C4ClientPlayerInfos LocalInfoRequest(*pChangeInfo);
590 if (!LocalInfoRequest.GetPlayerInfoByID(id: idPlayer)) return;
591 LocalInfoRequest.RemoveInfo(idPlr: idPlayer);
592 // and request this update (host processes it directly)
593 Game.Network.Players.RequestPlayerInfoUpdate(rRequest: LocalInfoRequest);
594
595 if (Game.Parameters.isLeague())
596 {
597 // rerequire the password in order to make it possible to change the account
598 Config.Network.LeaguePassword.Clear();
599 }
600}
601
602void C4PlayerInfoListBox::PlayerListItem::OnCtxNewColor(C4GUI::Element *pListItem)
603{
604 // only host or own player
605 if (!Game.Network.isEnabled() || (!Game.Network.isHost() && !IsLocalClientPlayer())) return;
606 // just send a request to reclaim the original color to the host
607 // the host will deny this and decide on a new color
608 C4ClientPlayerInfos *pChangeInfo = Game.PlayerInfos.GetInfoByClientID(iClientID: idClient);
609 if (!pChangeInfo || !idPlayer) return;
610 C4ClientPlayerInfos LocalInfoRequest(*pChangeInfo);
611 C4PlayerInfo *pPlrInfo = LocalInfoRequest.GetPlayerInfoByID(id: idPlayer);
612 if (!pPlrInfo) return;
613 pPlrInfo->SetColor(pPlrInfo->GetOriginalColor());
614 // and request this update (host processes it directly)
615 Game.Network.Players.RequestPlayerInfoUpdate(rRequest: LocalInfoRequest);
616}
617
618void C4PlayerInfoListBox::PlayerListItem::OnTeamComboFill(C4GUI::ComboBox_FillCB *pFiller)
619{
620 // add all possible teams
621 C4Team *pTeam; int32_t i = 0;
622 while (pTeam = Game.Teams.GetTeamByIndex(iIndex: i++))
623 if (!pTeam->IsFull() || GetPlayerInfo()->GetTeam() == pTeam->GetID())
624 pFiller->AddEntry(szText: pTeam->GetName(), id: pTeam->GetID());
625}
626
627bool C4PlayerInfoListBox::PlayerListItem::OnTeamComboSelChange(C4GUI::ComboBox *pForCombo, int32_t idNewSelection)
628{
629 // always return true to mark combo sel as processed, so the GUI won't change the team text
630 // get new team id by name
631 C4Team *pNewTeam = Game.Teams.GetTeamByID(iID: idNewSelection);
632 // some safety first...
633 if (!CanLocalChooseTeam() || !pNewTeam) return true;
634 C4ClientPlayerInfos *pChangeInfo = Game.PlayerInfos.GetInfoByClientID(iClientID: idClient);
635 if (!pChangeInfo || !idPlayer) return true;
636 // this must be processed as a request by the host
637 // now change it in its own request packet
638 C4ClientPlayerInfos LocalInfoRequest(*pChangeInfo);
639 C4PlayerInfo *pChangedInfo = LocalInfoRequest.GetPlayerInfoByID(id: idPlayer);
640 if (!pChangedInfo) return true;
641 pChangedInfo->SetTeam(pNewTeam->GetID());
642 // and request this update (host processes it directly)
643 Game.Network.Players.RequestPlayerInfoUpdate(rRequest: LocalInfoRequest);
644 // next update will change the combo box text
645 return true;
646}
647
648void C4PlayerInfoListBox::PlayerListItem::Update()
649{
650 UpdateCollapsed();
651 UpdateIcon(pInfo: GetPlayerInfo(), pJoinedInfo: GetJoinedInfo());
652 UpdateTeam();
653 C4PlayerInfo *pNfo = GetPlayerInfo();
654 if (pNfo)
655 {
656 UpdateScoreLabel(pInfo: pNfo);
657 // update name + color
658 std::string showName{pNfo->GetLobbyName().getData()};
659 if (pList->IsEvaluation())
660 {
661 bool fShowWinners = (pList->GetMode() != PILBM_EvaluationNoWinners);
662 bool fHasWon = fShowWinners && pNfo->HasTeamWon();
663 // Append "winner" or "loser" to player name
664 if (fShowWinners)
665 {
666 showName = std::format(fmt: "{} ({})", args&: showName, args: LoadResStrChoice(condition: fHasWon, ifTrue: C4ResStrTableKey::IDS_CTL_WON, ifFalse: C4ResStrTableKey::IDS_CTL_LOST));
667 }
668 // evaluation: Golden color+background for winners; gray for losers or no winner show
669 if (fHasWon)
670 {
671 pNameLabel->SetColor(C4GUI_WinningTextColor, fMakeReadableOnBlack: false);
672 dwBackground = C4GUI_WinningBackgroundColor;
673 }
674 else
675 {
676 pNameLabel->SetColor(C4GUI_LosingTextColor, fMakeReadableOnBlack: false);
677 dwBackground = C4GUI_LosingBackgroundColor;
678 }
679 }
680 else
681 {
682 // lobby: Label color by player color
683 pNameLabel->SetColor(dwToClr: pNfo->GetLobbyColor());
684 }
685 pNameLabel->SetText(szText: showName.c_str(), fAllowHotkey: false);
686 }
687}
688
689C4PlayerInfo *C4PlayerInfoListBox::PlayerListItem::GetPlayerInfo() const
690{
691 return fFreeSavegamePlayer ? Game.RestorePlayerInfos.GetPlayerInfoByID(id: idPlayer) : Game.PlayerInfos.GetPlayerInfoByID(id: idPlayer);
692}
693
694C4PlayerInfo *C4PlayerInfoListBox::PlayerListItem::GetJoinedInfo() const
695{
696 // safety
697 C4PlayerInfo *pInfo = GetPlayerInfo();
698 if (!pInfo) return nullptr;
699 // is it a joined savegame player?
700 if (fFreeSavegamePlayer)
701 // then this is the joined player
702 return pInfo;
703 // otherwise, does it have a savegame association?
704 int32_t idSavegameInfo;
705 if (idSavegameInfo = pInfo->GetAssociatedSavegamePlayerID())
706 // then return the respective info from savegame recreation list
707 return Game.RestorePlayerInfos.GetPlayerInfoByID(id: idSavegameInfo);
708 // not joined
709 return nullptr;
710}
711
712bool C4PlayerInfoListBox::PlayerListItem::CanLocalChooseTeam() const
713{
714 // never on savegame players
715 if (fFreeSavegamePlayer || GetJoinedInfo()) return false;
716 // only host or own player
717 if (!Game.Network.isHost() && !IsLocalClientPlayer()) return false;
718 // finally, only if team settings permit
719 return CanLocalChooseTeams(idPlayer);
720}
721
722bool C4PlayerInfoListBox::PlayerListItem::IsLocalClientPlayer() const
723{
724 // check whether client is local
725 // if no client can be found, assume network disconnect and everythign local then
726 C4Network2Client *pClient = GetNetClient();
727 return !pClient || pClient->isLocal();
728}
729
730C4Network2Client *C4PlayerInfoListBox::PlayerListItem::GetNetClient() const
731{
732 return Game.Network.Clients.GetClientByID(iID: idClient);
733}
734
735// ClientListItem
736
737C4PlayerInfoListBox::ClientListItem::ClientListItem(C4PlayerInfoListBox *pForListBox, const C4ClientCore &rClientInfo, ListItem *pInsertBefore)
738 : ListItem(pForListBox), idClient(rClientInfo.getID()), dwClientClr(0xffffff), tLastSoundTime(0)
739{
740 // set current active-flag (not really needed until player info is retrieved)
741 fIsShownActive = rClientInfo.isActivated();
742 // set ID
743 idListItemID.idType = ListItem::ID::PLI_CLIENT;
744 idListItemID.id = idClient;
745 // get height
746 int32_t iIconSize = C4GUI::GetRes()->TextFont.GetLineHeight();
747 // create subcomponents
748 pStatusIcon = new C4GUI::Icon(C4Rect(0, 0, iIconSize, iIconSize), GetCurrentStatusIcon());
749 pNameLabel = new C4GUI::Label(rClientInfo.getName(), iIconSize + IconLabelSpacing, 0, ALeft, dwClientClr | C4GUI_MessageFontAlpha, nullptr, true, false);
750 pPingLabel = nullptr;
751 C4GUI::CallbackButton<ClientListItem, C4GUI::IconButton> *btnAddPlayer = nullptr;
752 if (IsLocalClientPlayer())
753 {
754 // this computer: add player button
755 btnAddPlayer = new C4GUI::CallbackButton<ClientListItem, C4GUI::IconButton>(C4GUI::Ico_AddPlr, C4Rect(0, 0, iIconSize, iIconSize), 'P' /* 2do TODO */, &ClientListItem::OnBtnAddPlr, this);
756 }
757 // calc own bounds
758 C4Rect rcOwnBounds = pNameLabel->GetBounds();
759 rcOwnBounds.Wdt += rcOwnBounds.x; rcOwnBounds.x = 0;
760 rcOwnBounds.Hgt += rcOwnBounds.y; rcOwnBounds.y = 0;
761 SetBounds(rcOwnBounds);
762 // add components
763 AddElement(pChild: pStatusIcon); AddElement(pChild: pNameLabel);
764 if (btnAddPlayer) AddElement(pChild: btnAddPlayer);
765 // tooltip (same for all components for now. separate tooltip for status icon later?)
766 SetToolTip(std::format(fmt: "Client {} ({})", args: rClientInfo.getName(), args: rClientInfo.getNick()).c_str());
767 // insert into listbox at correct order
768 // (will eventually get resized horizontally and moved)
769 pForListBox->InsertElement(pChild: this, pInsertBefore);
770 // after move: update add player button pos
771 if (btnAddPlayer)
772 {
773 int32_t iHgt = GetClientRect().Hgt;
774 btnAddPlayer->GetBounds() = GetToprightCornerRect(iWidth: iHgt, iHeight: iHgt, iHIndent: 2, iVIndent: 0);
775 }
776 // context menu for list item
777 SetContextHandler(new C4GUI::CBContextHandler<ClientListItem>(this, &ClientListItem::OnContext));
778 // update (also sets color)
779 Update();
780}
781
782void C4PlayerInfoListBox::ClientListItem::SetPing(int32_t iToPing)
783{
784 // no ping?
785 if (iToPing == -1)
786 {
787 // remove any ping label
788 delete pPingLabel; pPingLabel = nullptr;
789 return;
790 }
791 // get ping as text
792 const auto &text = std::to_string(val: iToPing) + " ms";
793 // create ping label if necessary
794 if (!pPingLabel)
795 {
796 pPingLabel = new C4GUI::Label(text.c_str(), GetBounds().Wdt, 0, ARight, C4GUI_MessageFontClr);
797 pPingLabel->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PING));
798 AddElement(pChild: pPingLabel);
799 }
800 else
801 // or just set updated text
802 pPingLabel->SetText(szText: text.c_str());
803}
804
805void C4PlayerInfoListBox::ClientListItem::UpdateInfo()
806{
807 // Append resource load progress to caption
808 C4Client *pClient = GetClient();
809 auto *pNetClient = GetNetClient();
810 if (pClient && !pClient->isLocal()) // Do not show progress for local client
811 {
812 StdStrBuf sCaption;
813 if (pNetClient->isConnected())
814 {
815 sCaption.Copy(pnData: std::format(fmt: "({}%) {}", args: Game.Network.ResList.GetClientProgress(clientID: pClient->getID()), args: pClient->getName()).c_str());
816 }
817 else
818 {
819 sCaption.Ref(pnData: pClient->getName());
820 }
821 this->pNameLabel->SetText(szText: sCaption.getData(), fAllowHotkey: false);
822 }
823
824 // update color (always, because it can change silently)
825 SetColor(Game.Network.Players.GetClientChatColor(idForClient: idClient, fLobby: true));
826 // update activation status
827 fIsShownActive = GetClient() && GetClient()->isActivated();
828 // update status icon
829 SetStatus(GetCurrentStatusIcon());
830}
831
832C4Client *C4PlayerInfoListBox::ClientListItem::GetClient() const
833{
834 // search (let's hope it exists)
835 return Game.Clients.getClientByID(iID: idClient);
836}
837
838bool C4PlayerInfoListBox::ClientListItem::IsLocalClientPlayer() const
839{
840 // check whether client is local
841 // if no client can be found, something is wrong - assume network disconnect and everything local then
842 C4Network2Client *pClient = GetNetClient();
843 assert(pClient);
844 return !pClient || pClient->isLocal();
845}
846
847C4Network2Client *C4PlayerInfoListBox::ClientListItem::GetNetClient() const
848{
849 return Game.Network.Clients.GetClientByID(iID: idClient);
850}
851
852C4GUI::Icons C4PlayerInfoListBox::ClientListItem::GetCurrentStatusIcon()
853{
854 // sound icon?
855 if (tLastSoundTime)
856 {
857 time_t dt = time(timer: nullptr) - tLastSoundTime;
858 if (dt >= SoundIconShowTime)
859 {
860 // stop showing sound icon
861 tLastSoundTime = 0;
862 }
863 else
864 {
865 // time not up yet: show sound icon
866 return GetClient()->isMuted() ? C4GUI::Ico_NoSound : C4GUI::Ico_Sound;
867 }
868 }
869 // info present?
870 C4ClientPlayerInfos *pInfoPacket = Game.PlayerInfos.GetInfoByClientID(iClientID: idClient);
871 if (!pInfoPacket || !GetClient())
872 // unknown status
873 return C4GUI::Ico_UnknownClient;
874 // host?
875 if (GetClient()->isHost()) return C4GUI::Ico_Host;
876 // active client?
877 if (GetClient()->isActivated())
878 {
879 return GetClient()->isLobbyReady() ? C4GUI::Ico_Ready : C4GUI::Ico_Client;
880 }
881 // observer
882 return C4GUI::Ico_ObserverClient;
883}
884
885void C4PlayerInfoListBox::ClientListItem::UpdatePing()
886{
887 // safety for removed clients
888 if (!GetClient()) return;
889 // default value indicating no ping
890 int32_t iPing = -1;
891 C4Network2Client *pClient = GetNetClient();
892 C4Network2IOConnection *pConn;
893 // must be a remote client
894 if (pClient && !pClient->isLocal())
895 {
896 // must have a connection
897 if (pConn = pClient->getMsgConn())
898 // get ping of that connection
899 iPing = pConn->getLag();
900 // check data connection if msg conn gave no value
901 // what's the meaning of those two connections anyway? o_O
902 if (iPing <= 0)
903 if (pConn = pClient->getDataConn())
904 iPing = pConn->getLag();
905 }
906 // set that ping in label
907 SetPing(iPing);
908}
909
910void C4PlayerInfoListBox::ClientListItem::SetSoundIcon()
911{
912 // remember time for reset
913 tLastSoundTime = time(timer: nullptr);
914 // force icon
915 SetStatus(GetCurrentStatusIcon());
916}
917
918C4GUI::ContextMenu *C4PlayerInfoListBox::ClientListItem::OnContext(C4GUI::Element *pListItem, int32_t iX, int32_t iY)
919{
920 // safety
921 if (!Game.Network.isEnabled()) return nullptr;
922 // get associated client
923 C4Client *pClient = GetClient();
924 // create context menu
925 C4GUI::ContextMenu *pMenu = new C4GUI::ContextMenu();
926 // helper function
927 auto AddMenuItem = [this, pMenu](const char *const text, const char *const description, auto callbackFn)
928 {
929 pMenu->AddItem(szText: text, szToolTip: description, icoIcon: C4GUI::Ico_None,
930 pMenuHandler: new C4GUI::CBMenuHandler<ClientListItem>{this, callbackFn});
931 };
932
933 // mute / unmute
934 if (!pClient->isLocal())
935 {
936 bool muted = pClient->isMuted();
937 AddMenuItem(LoadResStrChoice(condition: muted, ifTrue: C4ResStrTableKey::IDS_NET_UNMUTE, ifFalse: C4ResStrTableKey::IDS_NET_MUTE), LoadResStrChoice(condition: muted, ifTrue: C4ResStrTableKey::IDS_NET_UNMUTE_DESC, ifFalse: C4ResStrTableKey::IDS_NET_MUTE_DESC), &ClientListItem::OnCtxToggleMute);
938 }
939
940 // host options
941 if (Game.Network.isHost() && GetNetClient())
942 {
943 AddMenuItem(LoadResStr(id: C4ResStrTableKey::IDS_NET_KICKCLIENT), LoadResStr(id: C4ResStrTableKey::IDS_NET_KICKCLIENT_DESC), &ClientListItem::OnCtxKick);
944 AddMenuItem(LoadResStrChoice(condition: pClient->isActivated(), ifTrue: C4ResStrTableKey::IDS_NET_DEACTIVATECLIENT, ifFalse: C4ResStrTableKey::IDS_NET_ACTIVATECLIENT), LoadResStr(id: C4ResStrTableKey::IDS_NET_ACTIVATECLIENT_DESC), &ClientListItem::OnCtxActivate);
945 }
946 // info
947 AddMenuItem(LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENTINFO), LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENTINFO_DESC), &ClientListItem::OnCtxInfo);
948 // open it
949 return pMenu;
950}
951
952void C4PlayerInfoListBox::ClientListItem::OnCtxKick(C4GUI::Element *pListItem)
953{
954 // host only
955 if (!Game.Network.isEnabled() || !Game.Network.isHost()) return;
956 // add control
957 Game.Clients.CtrlRemove(pClient: GetClient(), szReason: LoadResStr(id: C4ResStrTableKey::IDS_MSG_KICKFROMLOBBY));
958}
959
960void C4PlayerInfoListBox::ClientListItem::OnCtxActivate(C4GUI::Element *pListItem)
961{
962 // host only
963 C4Client *pClient = GetClient();
964 if (!Game.Network.isEnabled() || !Game.Network.isHost() || !pClient) return;
965 // add control
966 Game.Control.DoInput(eCtrlType: CID_ClientUpdate, pPkt: new C4ControlClientUpdate(idClient, CUT_Activate, !pClient->isActivated()), eDelivery: CDT_Sync);
967}
968
969void C4PlayerInfoListBox::ClientListItem::OnCtxInfo(C4GUI::Element *pListItem)
970{
971 // show client info dialog
972 Game.pGUI->ShowRemoveDlg(pDlg: new C4Network2ClientDlg(idClient));
973}
974
975void C4PlayerInfoListBox::ClientListItem::OnCtxToggleMute(C4GUI::Element *pListItem)
976{
977 if (C4Client *client = GetClient(); client)
978 client->ToggleMuted();
979}
980
981void C4PlayerInfoListBox::ClientListItem::OnBtnAddPlr(C4GUI::Control *btn)
982{
983 // show player add dialog
984 GetScreen()->ShowRemoveDlg(pDlg: new C4PlayerSelDlg(new C4FileSel_CBEx<C4GameLobby::MainDlg>(GetLobby(), &C4GameLobby::MainDlg::OnClientAddPlayer, idClient)));
985}
986
987// TeamListItem
988
989C4PlayerInfoListBox::TeamListItem::TeamListItem(C4PlayerInfoListBox *pForListBox, int32_t idTeam, ListItem *pInsertBefore)
990 : ListItem(pForListBox), idTeam(idTeam)
991{
992 bool fEvaluation = pList->IsEvaluation();
993 // get team data
994 const char *szTeamName;
995 C4Team *pTeam = nullptr;
996 if (idTeam == TEAMID_Unknown)
997 szTeamName = LoadResStr(id: C4ResStrTableKey::IDS_MSG_RNDTEAM);
998 else
999 {
1000 pTeam = Game.Teams.GetTeamByID(iID: idTeam); assert(pTeam);
1001 if (pTeam) szTeamName = pTeam->GetName(); else szTeamName = "INTERNAL TEAM ERROR";
1002 }
1003 // set ID
1004 idListItemID.idType = ListItem::ID::PLI_TEAM;
1005 idListItemID.id = idTeam;
1006 // get height
1007 int32_t iIconSize; CStdFont *pFont;
1008 if (!fEvaluation)
1009 {
1010 pFont = &C4GUI::GetRes()->TextFont;
1011 iIconSize = pFont->GetLineHeight();
1012 }
1013 else
1014 {
1015 pFont = &C4GUI::GetRes()->TitleFont;
1016 iIconSize = C4SymbolSize; // C4PictureSize doesn't fit...
1017 }
1018 // create subcomponents
1019 pIcon = new C4GUI::Icon(C4Rect(0, 0, iIconSize, iIconSize), C4GUI::Ico_Team);
1020 pNameLabel = new C4GUI::Label(szTeamName, iIconSize + IconLabelSpacing, (iIconSize - pFont->GetLineHeight()) / 2, ALeft, pList->GetTextColor(), pFont, false);
1021 if (fEvaluation && pTeam && pTeam->GetIconSpec() && *pTeam->GetIconSpec())
1022 {
1023 Game.DrawTextSpecImage(fctTarget&: pIcon->GetMFacet(), szSpec: pTeam->GetIconSpec(), dwClr: pTeam->GetColor());
1024 }
1025 // calc own bounds
1026 C4Rect rcOwnBounds = pNameLabel->GetBounds();
1027 rcOwnBounds.Wdt += rcOwnBounds.x; rcOwnBounds.x = 0;
1028 rcOwnBounds.Hgt += rcOwnBounds.y; rcOwnBounds.y = 0;
1029 SetBounds(rcOwnBounds);
1030 // add components
1031 AddElement(pChild: pIcon); AddElement(pChild: pNameLabel);
1032 // tooltip
1033 SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_TEAM, args&: szTeamName).c_str());
1034 // insert into listbox at correct order
1035 // (will eventually get resized horizontally and moved)
1036 pForListBox->InsertElement(pChild: this, pInsertBefore);
1037}
1038
1039void C4PlayerInfoListBox::TeamListItem::MouseInput(C4GUI::CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, uint32_t dwKeyParam)
1040{
1041 // double click on team to enter it with all local players
1042 if (iButton == C4MC_Button_LeftDouble)
1043 {
1044 MoveLocalPlayersIntoTeam();
1045 }
1046 else
1047 ListItem::MouseInput(rMouse, iButton, iX, iY, dwKeyParam);
1048}
1049
1050void C4PlayerInfoListBox::TeamListItem::UpdateOwnPos()
1051{
1052 // parent for client rect
1053 typedef C4GUI::Window ParentClass;
1054 ParentClass::UpdateOwnPos();
1055 // evaluation: Center team label
1056 if (pList->IsEvaluation())
1057 {
1058 int32_t iTotalWdt = pIcon->GetBounds().Wdt + IconLabelSpacing + C4GUI::GetRes()->TitleFont.GetTextWidth(szText: pNameLabel->GetText());
1059 C4GUI::ComponentAligner caAll(GetContainedClientRect(), 0, 0);
1060 C4GUI::ComponentAligner caBounds(caAll.GetCentered(iWdt: iTotalWdt, iHgt: caAll.GetInnerHeight()), 0, 0);
1061 pIcon->SetBounds(caBounds.GetFromLeft(iWdt: pIcon->GetBounds().Wdt, iHgt: pIcon->GetBounds().Hgt));
1062 pNameLabel->SetBounds(caBounds.GetCentered(iWdt: caBounds.GetInnerWidth(), iHgt: C4GUI::GetRes()->TitleFont.GetLineHeight()));
1063 }
1064}
1065
1066void C4PlayerInfoListBox::TeamListItem::MoveLocalPlayersIntoTeam()
1067{
1068 // check if changing teams is allowed
1069 if (!CanLocalChooseTeams()) return;
1070 // safety: Clicked team must exist
1071 if (!Game.Teams.GetTeamByID(iID: idTeam)) return;
1072 // get local client to change teams of
1073 bool fAnyChange = false;
1074 C4ClientPlayerInfos *pChangeInfo = Game.PlayerInfos.GetInfoByClientID(iClientID: Game.Clients.getLocalID());
1075 if (!pChangeInfo) return;
1076 // this must be processed as a request by the host
1077 // now change it in its own request packet
1078 C4ClientPlayerInfos LocalInfoRequest(*pChangeInfo);
1079 C4PlayerInfo *pInfo; int32_t i = 0;
1080 while (pInfo = LocalInfoRequest.GetPlayerInfo(iIndex: i++))
1081 if (pInfo->GetTeam() != idTeam)
1082 if (pInfo->GetType() == C4PT_User)
1083 {
1084 pInfo->SetTeam(idTeam);
1085 fAnyChange = true;
1086 }
1087 if (!fAnyChange) return;
1088 // and request this update (host processes it directly)
1089 Game.Network.Players.RequestPlayerInfoUpdate(rRequest: LocalInfoRequest);
1090 // next update will move the player labels
1091}
1092
1093void C4PlayerInfoListBox::TeamListItem::Update()
1094{
1095 // evaluation: update color by team winning status
1096 if (pList->IsEvaluation())
1097 {
1098 C4Team *pTeam = Game.Teams.GetTeamByID(iID: idTeam);
1099 if (pTeam && pTeam->HasWon())
1100 {
1101 pNameLabel->SetColor(C4GUI_WinningTextColor, fMakeReadableOnBlack: false);
1102 }
1103 else
1104 {
1105 pNameLabel->SetColor(C4GUI_LosingTextColor, fMakeReadableOnBlack: false);
1106 }
1107 }
1108}
1109
1110// FreeSavegamePlayersListItem
1111
1112C4PlayerInfoListBox::FreeSavegamePlayersListItem::FreeSavegamePlayersListItem(C4PlayerInfoListBox *pForListBox, ListItem *pInsertBefore)
1113 : ListItem(pForListBox)
1114{
1115 // set ID
1116 idListItemID.idType = ListItem::ID::PLI_SAVEGAMEPLR;
1117 idListItemID.id = 0;
1118 // get height
1119 int32_t iIconSize = C4GUI::GetRes()->TextFont.GetLineHeight();
1120 // create subcomponents
1121 pIcon = new C4GUI::Icon(C4Rect(0, 0, iIconSize, iIconSize), C4GUI::Ico_SavegamePlayer);
1122 pNameLabel = new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_MSG_FREESAVEGAMEPLRS), iIconSize + IconLabelSpacing, 0, ALeft);
1123 // calc own bounds
1124 C4Rect rcOwnBounds = pNameLabel->GetBounds();
1125 rcOwnBounds.Wdt += rcOwnBounds.x; rcOwnBounds.x = 0;
1126 rcOwnBounds.Hgt += rcOwnBounds.y; rcOwnBounds.y = 0;
1127 SetBounds(rcOwnBounds);
1128 // add components
1129 AddElement(pChild: pIcon); AddElement(pChild: pNameLabel);
1130 // tooltip
1131 SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_UNASSOCIATEDSAVEGAMEPLAYE));
1132 // insert into listbox at correct order
1133 // (will eventually get resized horizontally and moved)
1134 pForListBox->InsertElement(pChild: this, pInsertBefore);
1135 // initial update
1136 Update();
1137}
1138
1139void C4PlayerInfoListBox::FreeSavegamePlayersListItem::Update()
1140{
1141 // 2do: none-label
1142}
1143
1144// ScriptPlayersListItem
1145
1146C4PlayerInfoListBox::ScriptPlayersListItem::ScriptPlayersListItem(C4PlayerInfoListBox *pForListBox, ListItem *pInsertBefore)
1147 : ListItem(pForListBox)
1148{
1149 // set ID
1150 idListItemID.idType = ListItem::ID::PLI_SCRIPTPLR;
1151 idListItemID.id = 0;
1152 // get height
1153 int32_t iIconSize = C4GUI::GetRes()->TextFont.GetLineHeight();
1154 // create subcomponents
1155 pIcon = new C4GUI::Icon(C4Rect(0, 0, iIconSize, iIconSize), C4GUI::Ico_Record);
1156 pNameLabel = new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_CTL_SCRIPTPLAYERS), iIconSize + IconLabelSpacing, 0, ALeft);
1157 btnAddPlayer = nullptr;
1158 if (Game.Control.isCtrlHost())
1159 {
1160 btnAddPlayer = new C4GUI::CallbackButton<ScriptPlayersListItem, C4GUI::IconButton>(C4GUI::Ico_AddPlr, C4Rect(0, 0, iIconSize, iIconSize), 'A' /* 2do TODO */, &ScriptPlayersListItem::OnBtnAddPlr, this);
1161 }
1162 // calc own bounds
1163 C4Rect rcOwnBounds = pNameLabel->GetBounds();
1164 rcOwnBounds.Wdt += rcOwnBounds.x; rcOwnBounds.x = 0;
1165 rcOwnBounds.Hgt += rcOwnBounds.y; rcOwnBounds.y = 0;
1166 SetBounds(rcOwnBounds);
1167 // add components
1168 AddElement(pChild: pIcon); AddElement(pChild: pNameLabel);
1169 if (btnAddPlayer) AddElement(pChild: btnAddPlayer);
1170 // tooltip
1171 SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_PLAYERSCONTROLLEDBYCOMPUT));
1172 // insert into listbox at correct order
1173 // (will eventually get resized horizontally and moved)
1174 pForListBox->InsertElement(pChild: this, pInsertBefore);
1175 // after move: update add player button pos
1176 if (btnAddPlayer)
1177 {
1178 int32_t iHgt = GetClientRect().Hgt;
1179 btnAddPlayer->GetBounds() = GetToprightCornerRect(iWidth: iHgt, iHeight: iHgt, iHIndent: 2, iVIndent: 0);
1180 }
1181 // initial update
1182 Update();
1183}
1184
1185void C4PlayerInfoListBox::ScriptPlayersListItem::Update()
1186{
1187 // player join button: Visible if there's still some room for script players
1188 if (btnAddPlayer)
1189 {
1190 bool fCanJoinScriptPlayers = (Game.Teams.GetMaxScriptPlayers() - Game.PlayerInfos.GetActiveScriptPlayerCount(fCountSavegameResumes: true, fCountInvisible: true) > 0);
1191 btnAddPlayer->SetVisibility(fCanJoinScriptPlayers);
1192 }
1193}
1194
1195void C4PlayerInfoListBox::ScriptPlayersListItem::OnBtnAddPlr(C4GUI::Control *btn)
1196{
1197 // safety
1198 int32_t iCurrScriptPlrCount = Game.PlayerInfos.GetActiveScriptPlayerCount(fCountSavegameResumes: true, fCountInvisible: true);
1199 bool fCanJoinScriptPlayers = (Game.Teams.GetMaxScriptPlayers() - iCurrScriptPlrCount > 0);
1200 if (!fCanJoinScriptPlayers) return;
1201 if (!Game.Control.isCtrlHost()) return;
1202 // request a script player join
1203 C4PlayerInfo *pScriptPlrInfo = new C4PlayerInfo();
1204 pScriptPlrInfo->SetAsScriptPlayer(szName: Game.Teams.GetScriptPlayerName().getData(), dwColor: GenerateRandomPlayerColor(iTry: iCurrScriptPlrCount), dwFlags: 0, idExtra: C4ID_None);
1205 C4ClientPlayerInfos JoinPkt(nullptr, true, pScriptPlrInfo);
1206 // add to queue!
1207 Game.PlayerInfos.DoPlayerInfoUpdate(pUpdate: &JoinPkt);
1208}
1209
1210// ReplayPlayersListItem
1211
1212C4PlayerInfoListBox::ReplayPlayersListItem::ReplayPlayersListItem(C4PlayerInfoListBox *pForListBox, ListItem *pInsertBefore)
1213 : ListItem(pForListBox)
1214{
1215 // set ID
1216 idListItemID.idType = ListItem::ID::PLI_REPLAY;
1217 idListItemID.id = 0;
1218 // get height
1219 int32_t iIconSize = C4GUI::GetRes()->TextFont.GetLineHeight();
1220 // create subcomponents
1221 pIcon = new C4GUI::Icon(C4Rect(0, 0, iIconSize, iIconSize), C4GUI::Ico_Record);
1222 pNameLabel = new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_MSG_REPLAYPLRS), iIconSize + IconLabelSpacing, 0, ALeft);
1223 // calc own bounds
1224 C4Rect rcOwnBounds = pNameLabel->GetBounds();
1225 rcOwnBounds.Wdt += rcOwnBounds.x; rcOwnBounds.x = 0;
1226 rcOwnBounds.Hgt += rcOwnBounds.y; rcOwnBounds.y = 0;
1227 SetBounds(rcOwnBounds);
1228 // add components
1229 AddElement(pChild: pIcon); AddElement(pChild: pNameLabel);
1230 // tooltip
1231 SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_MSG_REPLAYPLRS_DESC));
1232 // insert into listbox at correct order
1233 // (will eventually get resized horizontally and moved)
1234 pForListBox->InsertElement(pChild: this, pInsertBefore);
1235}
1236
1237// C4PlayerInfoListBox
1238
1239C4PlayerInfoListBox::C4PlayerInfoListBox(const C4Rect &rcBounds, Mode eMode, int32_t iTeamFilter)
1240 : C4GUI::ListBox(rcBounds), fIsCollapsed(false), iMaxUncollapsedPlayers(10), eMode(eMode), iTeamFilter(iTeamFilter), dwTextColor(C4GUI_MessageFontClr), pCustomFont(nullptr)
1241{
1242 // update if client listbox selection changes
1243 SetSelectionChangeCallbackFn(new C4GUI::CallbackHandler<C4PlayerInfoListBox>(this, &C4PlayerInfoListBox::OnPlrListSelChange));
1244 // initial update
1245 Update();
1246}
1247
1248void C4PlayerInfoListBox::SetClientSoundIcon(int32_t iForClientID)
1249{
1250 // get client element
1251 ListItem *pItem = GetPlayerListItem(eType: ListItem::ID::PLI_CLIENT, id: iForClientID);
1252 if (pItem)
1253 {
1254 ClientListItem *pClientItem = static_cast<ClientListItem *>(pItem);
1255 pClientItem->SetSoundIcon();
1256 }
1257}
1258
1259C4PlayerInfoListBox::ListItem *C4PlayerInfoListBox::GetPlayerListItem(ListItem::ID::IDType eType, int32_t id)
1260{
1261 // safety
1262 if (!C4GUI::IsGUIValid()) return nullptr;
1263 ListItem::ID idSearch(eType, id);
1264 // search through listbox
1265 for (C4GUI::Element *pEItem = GetFirst(); pEItem; pEItem = pEItem->GetNext())
1266 {
1267 // only playerlistitems in this box
1268 ListItem *pItem = static_cast<ListItem *>(pEItem);
1269 if (pItem->idListItemID == idSearch) return pItem;
1270 }
1271 // nothing found
1272 return nullptr;
1273}
1274
1275bool C4PlayerInfoListBox::PlrListItemUpdate(ListItem::ID::IDType eType, int32_t id, class ListItem **pEnsurePos)
1276{
1277 assert(pEnsurePos);
1278 // search item
1279 ListItem *pItem = GetPlayerListItem(eType, id);
1280 if (!pItem) return false;
1281 // ensure its position is correct
1282 if (pItem != *pEnsurePos)
1283 {
1284 RemoveElement(pChild: pItem);
1285 InsertElement(pChild: pItem, pInsertBefore: *pEnsurePos);
1286 }
1287 else
1288 {
1289 // pos correct; advance past it
1290 *pEnsurePos = static_cast<ListItem *>(pItem->GetNext());
1291 }
1292 // update item
1293 pItem->Update();
1294 // done, success
1295 return true;
1296}
1297
1298// static safety var to prevent recusive updates
1299static bool fPlayerListUpdating = false;
1300
1301void C4PlayerInfoListBox::Update()
1302{
1303 // safety
1304 if (!C4GUI::IsGUIValid()) return;
1305
1306 if (fPlayerListUpdating) return;
1307 fPlayerListUpdating = true;
1308
1309 // synchronize current list with what it should be
1310 // call update on all other list items
1311 ListItem *pCurrInList = static_cast<ListItem *>(GetFirst()); // list item being compared with the searched item
1312
1313 // free savegame players first
1314 UpdateSavegamePlayers(ppCurrInList: &pCurrInList);
1315
1316 // next comes the regular players, sorted either by clients or teams, or special sort for evaluation mode
1317 switch (eMode)
1318 {
1319 case PILBM_LobbyTeamSort:
1320 // sort by team
1321 if (Game.Teams.CanLocalSeeTeam())
1322 UpdatePlayersByTeam(ppCurrInList: &pCurrInList);
1323 else
1324 UpdatePlayersByRandomTeam(ppCurrInList: &pCurrInList);
1325 break;
1326
1327 case PILBM_LobbyClientSort:
1328 // sort by client
1329 // replay players first?
1330 if (Game.C4S.Head.Replay) UpdateReplayPlayers(ppCurrInList: &pCurrInList);
1331 // script controlled players from the main list
1332 UpdateScriptPlayers(ppCurrInList: &pCurrInList);
1333 // regular players
1334 UpdatePlayersByClient(ppCurrInList: &pCurrInList);
1335 break;
1336
1337 case PILBM_Evaluation:
1338 case PILBM_EvaluationNoWinners:
1339 UpdatePlayersByEvaluation(ppCurrInList: &pCurrInList, fShowWinners: eMode == PILBM_Evaluation);
1340 break;
1341 }
1342
1343 // finally: remove any remaining list items at the end
1344 while (pCurrInList)
1345 {
1346 ListItem *pDel = pCurrInList;
1347 pCurrInList = static_cast<ListItem *>(pCurrInList->GetNext());
1348 delete pDel;
1349 }
1350
1351 // update done
1352 fPlayerListUpdating = false;
1353
1354 // check whether view needs to be collapsed
1355 if (!fIsCollapsed && IsScrollingActive() && !IsEvaluation())
1356 {
1357 // then collapse it, and update window
1358 iMaxUncollapsedPlayers = Game.PlayerInfos.GetPlayerCount() - 1;
1359 fIsCollapsed = true;
1360 Update(); // recursive call!
1361 }
1362 else if (fIsCollapsed && Game.PlayerInfos.GetPlayerCount() <= iMaxUncollapsedPlayers)
1363 {
1364 // player count dropped below collapse-limit: uncollapse
1365 // note that this may again cause a collapse after that update, if scrolling was still necessary
1366 // however, it will then not recurse any further, because iMaxUncollapsedPlayers will have been updated
1367 fIsCollapsed = false;
1368 Update();
1369 }
1370}
1371
1372void C4PlayerInfoListBox::UpdateSavegamePlayers(ListItem **ppCurrInList)
1373{
1374 // add unassociated savegame players (script players excluded)
1375 if (Game.RestorePlayerInfos.GetActivePlayerCount(fCountInvisible: true) - Game.RestorePlayerInfos.GetActiveScriptPlayerCount(fCountSavegameResumes: true, fCountInvisible: true))
1376 {
1377 // caption
1378 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_SAVEGAMEPLR, id: 0, pEnsurePos: ppCurrInList))
1379 new FreeSavegamePlayersListItem(this, *ppCurrInList);
1380 // the players
1381 bool fAnyPlayers = false;
1382 C4PlayerInfo *pInfo; int32_t iInfoID = 0;
1383 while (pInfo = Game.RestorePlayerInfos.GetNextPlayerInfoByID(id: iInfoID))
1384 {
1385 iInfoID = pInfo->GetID();
1386 // skip assigned
1387 if (Game.PlayerInfos.GetPlayerInfoBySavegameID(id: iInfoID)) continue;
1388 // skip script controlled - those are put into the script controlled player list
1389 if (pInfo->GetType() == C4PT_Script) continue;
1390 // players are in the list
1391 fAnyPlayers = true;
1392 // show them
1393 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_SAVEGAMEPLR, id: iInfoID, pEnsurePos: ppCurrInList))
1394 new PlayerListItem(this, -1, iInfoID, true, *ppCurrInList);
1395 }
1396 // 2do: none-label
1397 }
1398}
1399
1400void C4PlayerInfoListBox::UpdateReplayPlayers(ListItem **ppCurrInList)
1401{
1402 // header
1403 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_REPLAY, id: 0, pEnsurePos: ppCurrInList))
1404 new ReplayPlayersListItem(this, *ppCurrInList);
1405 // players
1406 bool fAnyPlayers = false;
1407 C4PlayerInfo *pInfo; int32_t iInfoID = 0;
1408 while (pInfo = Game.PlayerInfos.GetNextPlayerInfoByID(id: iInfoID))
1409 {
1410 if (pInfo->IsInvisible()) continue;
1411 iInfoID = pInfo->GetID();
1412 // players are in the list
1413 fAnyPlayers = true;
1414 // show them
1415 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_PLAYER, id: iInfoID, pEnsurePos: ppCurrInList))
1416 new PlayerListItem(this, -1, iInfoID, false, *ppCurrInList);
1417 }
1418 // 2do: none-label
1419}
1420
1421void C4PlayerInfoListBox::UpdateScriptPlayers(ListItem **ppCurrInList)
1422{
1423 // script controlled players from the main list
1424 // processing the restore list would be redundant, because all script players should have been taken over by a new script player join automatically
1425 // also show the label if script players can be joined
1426 if (Game.Teams.GetMaxScriptPlayers() || Game.PlayerInfos.GetActiveScriptPlayerCount(fCountSavegameResumes: true, fCountInvisible: false))
1427 {
1428 // header
1429 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_SCRIPTPLR, id: 0, pEnsurePos: ppCurrInList))
1430 new ScriptPlayersListItem(this, *ppCurrInList);
1431 // players
1432 C4PlayerInfo *pInfo; int32_t iClientIdx = 0; C4ClientPlayerInfos *pInfos;
1433 while (pInfos = Game.PlayerInfos.GetIndexedInfo(iIndex: iClientIdx++))
1434 {
1435 int32_t iInfoIdx = 0;
1436 while (pInfo = pInfos->GetPlayerInfo(iIndex: iInfoIdx++))
1437 {
1438 if (pInfo->GetType() != C4PT_Script) continue;
1439 if (pInfo->IsRemoved()) continue;
1440 if (pInfo->IsInvisible()) continue;
1441 // show them
1442 int32_t iInfoID = pInfo->GetID();
1443 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_PLAYER, id: iInfoID, pEnsurePos: ppCurrInList))
1444 new PlayerListItem(this, pInfos->GetClientID(), iInfoID, false, *ppCurrInList);
1445 }
1446 }
1447 }
1448}
1449
1450void C4PlayerInfoListBox::UpdatePlayersByTeam(ListItem **ppCurrInList)
1451{
1452 // sort by team
1453 C4Team *pTeam; int32_t i = 0;
1454 while (pTeam = Game.Teams.GetTeamByIndex(iIndex: i++))
1455 {
1456 // no empty teams that are not used
1457 if (Game.Teams.IsAutoGenerateTeams() && !pTeam->GetPlayerCount()) continue;
1458 // the team label
1459 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_TEAM, id: pTeam->GetID(), pEnsurePos: ppCurrInList))
1460 new TeamListItem(this, pTeam->GetID(), *ppCurrInList);
1461 // players for this team
1462 int32_t idPlr, j = 0; int32_t idClient; C4Client *pClient; C4PlayerInfo *pPlrInfo;
1463 while (idPlr = pTeam->GetIndexedPlayer(iIndex: j++))
1464 if (pPlrInfo = Game.PlayerInfos.GetPlayerInfoByID(id: idPlr, pidClient: &idClient))
1465 if (!pPlrInfo->IsInvisible())
1466 if ((pClient = Game.Clients.getClientByID(iID: idClient)) && pClient->isActivated())
1467 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_PLAYER, id: idPlr, pEnsurePos: ppCurrInList))
1468 new PlayerListItem(this, idClient, idPlr, false, *ppCurrInList);
1469 }
1470}
1471
1472void C4PlayerInfoListBox::UpdatePlayersByRandomTeam(ListItem **ppCurrInList)
1473{
1474 // team sort but teams set to random and invisible: Show all players within one "Random Team"-label
1475 bool fTeamLabelPut = false;
1476 C4Client *pClient = nullptr;
1477 while (pClient = Game.Clients.getClient(pAfter: pClient))
1478 {
1479 // player infos for this client - not for deactivated, and never in replays
1480 if (Game.C4S.Head.Replay || !pClient->isActivated()) continue;
1481 C4ClientPlayerInfos *pInfoPacket = Game.PlayerInfos.GetInfoByClientID(iClientID: pClient->getID());
1482 if (pInfoPacket)
1483 {
1484 C4PlayerInfo *pPlrInfo; int32_t i = 0;
1485 while (pPlrInfo = pInfoPacket->GetPlayerInfo(iIndex: i++))
1486 {
1487 if (pPlrInfo->IsInvisible()) continue;
1488 if (!fTeamLabelPut)
1489 {
1490 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_TEAM, id: TEAMID_Unknown, pEnsurePos: ppCurrInList))
1491 new TeamListItem(this, TEAMID_Unknown, *ppCurrInList);
1492 fTeamLabelPut = true;
1493 }
1494 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_PLAYER, id: pPlrInfo->GetID(), pEnsurePos: ppCurrInList))
1495 new PlayerListItem(this, pClient->getID(), pPlrInfo->GetID(), false, *ppCurrInList);
1496 }
1497 }
1498 }
1499}
1500
1501void C4PlayerInfoListBox::UpdatePlayersByClient(ListItem **ppCurrInList)
1502{
1503 // regular players
1504 C4Client *pClient = nullptr;
1505 while (pClient = Game.Clients.getClient(pAfter: pClient))
1506 {
1507 // the client label
1508 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_CLIENT, id: pClient->getID(), pEnsurePos: ppCurrInList))
1509 new ClientListItem(this, pClient->getCore(), *ppCurrInList);
1510 // player infos for this client - not for observers, and never in replays
1511 // could also check for activated here. However, non-observers will usually be activated later and thus be using their players
1512 if (Game.C4S.Head.Replay || pClient->isObserver()) continue;
1513 C4ClientPlayerInfos *pInfoPacket = Game.PlayerInfos.GetInfoByClientID(iClientID: pClient->getID());
1514 if (pInfoPacket)
1515 {
1516 C4PlayerInfo *pPlrInfo; int32_t i = 0;
1517 while (pPlrInfo = Game.PlayerInfos.GetInfoByClientID(iClientID: pClient->getID())->GetPlayerInfo(iIndex: i++))
1518 {
1519 if (pPlrInfo->GetType() == C4PT_Script) continue;
1520 if (pPlrInfo->IsRemoved()) continue;
1521 if (pPlrInfo->IsInvisible()) continue;
1522 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_PLAYER, id: pPlrInfo->GetID(), pEnsurePos: ppCurrInList))
1523 new PlayerListItem(this, pClient->getID(), pPlrInfo->GetID(), false, *ppCurrInList);
1524 }
1525 }
1526 }
1527}
1528
1529void C4PlayerInfoListBox::UpdatePlayersByEvaluation(ListItem **ppCurrInList, bool fShowWinners)
1530{
1531 // if a team filter is provided, add team label first
1532 if (iTeamFilter)
1533 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_TEAM, id: iTeamFilter, pEnsurePos: ppCurrInList))
1534 new TeamListItem(this, iTeamFilter, *ppCurrInList);
1535 // Add by teams: In show-winner-mode winning teams first
1536 // Otherwise, just add all
1537 AddMode pShowWinnersAddModes[] = { AM_Winners, AM_Losers };
1538 AddMode pHideWinnersAddModes[] = { AM_All };
1539 AddMode *pAddModes; int32_t iAddModeCount;
1540 if (fShowWinners)
1541 {
1542 pAddModes = pShowWinnersAddModes; iAddModeCount = 2;
1543 }
1544 else
1545 {
1546 pAddModes = pHideWinnersAddModes; iAddModeCount = 1;
1547 }
1548 for (int32_t iAddMode = 0; iAddMode < iAddModeCount; ++iAddMode)
1549 {
1550 AddMode eAddMode = pAddModes[iAddMode];
1551 if (iTeamFilter)
1552 {
1553 // Team filter mode: Add only players of specified team
1554 UpdatePlayersByEvaluation(ppCurrInList, pTeam: Game.Teams.GetTeamByID(iID: iTeamFilter), eAddMode);
1555 }
1556 else
1557 {
1558 // Normal mode: Add all teams of winning status
1559 C4Team *pTeam; int32_t i = 0;
1560 while (pTeam = Game.Teams.GetTeamByIndex(iIndex: i++))
1561 {
1562 UpdatePlayersByEvaluation(ppCurrInList, pTeam, eAddMode);
1563 }
1564 // Add teamless players of winning status
1565 UpdatePlayersByEvaluation(ppCurrInList, pTeam: nullptr, eAddMode);
1566 }
1567 }
1568}
1569
1570void C4PlayerInfoListBox::UpdatePlayersByEvaluation(ListItem **ppCurrInList, C4Team *pTeam, C4PlayerInfoListBox::AddMode eWinMode)
1571{
1572 // check winning status of team first
1573 if (pTeam && eWinMode != AM_All) if (pTeam->HasWon() != (eWinMode == AM_Winners)) return;
1574 // now add all matching players
1575 int32_t iTeamID = pTeam ? pTeam->GetID() : 0;
1576 C4ClientPlayerInfos *pInfoPacket; int32_t iClient = 0;
1577 while (pInfoPacket = Game.PlayerInfos.GetIndexedInfo(iIndex: iClient++))
1578 {
1579 C4PlayerInfo *pPlrInfo; int32_t i = 0;
1580 while (pPlrInfo = pInfoPacket->GetPlayerInfo(iIndex: i++))
1581 {
1582 if (!pPlrInfo->HasJoined()) continue;
1583 if (pPlrInfo->GetTeam() != iTeamID) continue;
1584 if (pPlrInfo->IsInvisible()) continue;
1585 if (!pTeam && eWinMode != AM_All && pPlrInfo->HasWon() != (eWinMode == AM_Winners)) continue;
1586 if (!PlrListItemUpdate(eType: ListItem::ID::PLI_PLAYER, id: pPlrInfo->GetID(), pEnsurePos: ppCurrInList))
1587 new PlayerListItem(this, pInfoPacket->GetClientID(), pPlrInfo->GetID(), false, *ppCurrInList);
1588 }
1589 }
1590}
1591
1592bool C4PlayerInfoListBox::IsPlayerItemCollapsed(PlayerListItem *pItem)
1593{
1594 // never if view is not collapsed
1595 if (!fIsCollapsed) return false;
1596 // collapsed if not selected
1597 return GetSelectedItem() != pItem;
1598}
1599
1600void C4PlayerInfoListBox::SetMode(Mode eNewMode)
1601{
1602 if (eMode != eNewMode)
1603 {
1604 eMode = eNewMode;
1605 Update();
1606 }
1607}
1608