1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2005, Sven2
6 * Copyright (c) 2017-2020, 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// Startup screen for non-parameterized engine start: Player selection dialog
19// Also contains player creation, editing and crew management
20
21#include "C4GuiEdit.h"
22#include "C4GuiListBox.h"
23#include <C4Include.h>
24#include <C4StartupPlrSelDlg.h>
25
26#include <C4StartupMainDlg.h>
27#include <C4Random.h>
28#include <C4Game.h>
29#include <C4FileSelDlg.h>
30#include <C4Log.h>
31#include "C4TextEncoding.h"
32
33#include <format>
34
35// font clrs
36const uint32_t ClrPlayerItem = 0xff000000;
37
38// C4Utilities
39
40static std::string TimeString(int iSeconds)
41{
42 int iHours = iSeconds / 3600; iSeconds -= 3600 * iHours;
43 int iMinutes = iSeconds / 60; iSeconds -= 60 * iMinutes;
44 return std::format(fmt: "{:02}:{:02}:{:02}", args&: iHours, args&: iMinutes, args&: iSeconds);
45}
46
47static std::string DateString(int iTime)
48{
49 if (!iTime) return "";
50 time_t tTime = iTime;
51 struct tm *pLocalTime;
52 pLocalTime = localtime(timer: &tTime);
53 return std::format(fmt: "{:02}.{:02}.{} {:02}:{:02}",
54 args&: pLocalTime->tm_mday,
55 args: pLocalTime->tm_mon + 1,
56 args: pLocalTime->tm_year + 1900,
57 args&: pLocalTime->tm_hour,
58 args&: pLocalTime->tm_min);
59}
60
61// Fixme: This should use the already open group from C4GraphicsResource
62static bool GetPortrait(char **ppBytes, size_t *ipSize)
63{
64 // select random portrait from Graphics.c4g
65 C4Group GfxGroup;
66 int iCount;
67 if (!GfxGroup.Open(szGroupName: Config.AtExePath(C4CFN_Graphics))) return false;
68 if ((iCount = GfxGroup.EntryCount(szWildCard: "Portrait*.png")) < 1) return false;
69 if (!GfxGroup.LoadEntry(szEntryName: std::format(fmt: "Portrait{}.png", args: SafeRandom(range: iCount) + 1).c_str(), lpbpBuf: ppBytes, ipSize)) return false;
70 GfxGroup.Close();
71 return true;
72}
73
74// C4StartupPlrSelDlg::ListItem
75
76C4StartupPlrSelDlg::ListItem::ListItem(C4StartupPlrSelDlg *pForDlg, C4GUI::ListBox *pForListBox, C4GUI::Element *pInsertBeforeElement, bool fActivated)
77 : Control(C4Rect(0, 0, 0, 0)), pCheck(nullptr), pIcon(nullptr), pNameLabel(nullptr), pPlrSelDlg(pForDlg)
78{
79 CStdFont &rUseFont = C4Startup::Get()->Graphics.BookFont;
80 // calc height
81 int32_t iHeight = rUseFont.GetLineHeight() + 2 * IconLabelSpacing;
82 // create subcomponents
83 pCheck = new C4GUI::CheckBox(C4Rect(0, 0, iHeight, iHeight), "", fActivated);
84 pCheck->SetOnChecked(new C4GUI::CallbackHandler<C4StartupPlrSelDlg>(pForDlg, &C4StartupPlrSelDlg::OnItemCheckChange));
85 pKeyCheck = new C4KeyBinding(C4KeyCodeEx(K_SPACE), "StartupPlrSelTogglePlayerActive", KEYSCOPE_Gui,
86 new C4GUI::ControlKeyCB<ListItem>(*this, &ListItem::KeyCheck), C4CustomKey::PRIO_Ctrl);
87 pIcon = new C4GUI::Icon(C4Rect(iHeight + IconLabelSpacing, 0, iHeight, iHeight), C4GUI::Ico_Player);
88 pNameLabel = new C4GUI::Label("Q", (iHeight + IconLabelSpacing) * 2, IconLabelSpacing, ALeft, ClrPlayerItem, &rUseFont, false, false);
89 pNameLabel->SetAutosize(false);
90 // calc own bounds - use icon bounds only, because only the height is used when the item is added
91 SetBounds(pIcon->GetBounds());
92 // add components
93 AddElement(pChild: pCheck);
94 AddElement(pChild: pIcon); AddElement(pChild: pNameLabel);
95 // add to listbox (will get resized horizontally and moved) - zero indent; no tree structure in this dialog
96 pForListBox->InsertElement(pChild: this, pInsertBefore: pInsertBeforeElement, iIndent: 0);
97 // update name label width to stretch max listbox width
98 C4Rect rcNameLabelBounds = pNameLabel->GetBounds();
99 rcNameLabelBounds.Wdt = GetClientRect().Wdt - rcNameLabelBounds.x - IconLabelSpacing;
100 pNameLabel->SetBounds(rcNameLabelBounds);
101 // context menu
102 SetContextHandler(new C4GUI::CBContextHandler<C4StartupPlrSelDlg::ListItem>(this, &C4StartupPlrSelDlg::ListItem::ContextMenu));
103}
104
105C4StartupPlrSelDlg::ListItem::~ListItem()
106{
107 delete pKeyCheck;
108}
109
110const char *C4StartupPlrSelDlg::ListItem::GetName() const
111{
112 // name is stored in label only
113 return pNameLabel->GetText();
114}
115
116void C4StartupPlrSelDlg::ListItem::SetName(const char *szNewName)
117{
118 // update name in label
119 pNameLabel->SetText(szText: szNewName);
120 // tooltip by name, so long names can be read via tooltip
121 SetToolTip(szNewName);
122}
123
124void C4StartupPlrSelDlg::ListItem::GrabIcon(C4FacetExSurface &rFromFacet)
125{
126 // take over icon gfx from facet - deletes them from source facet!
127 if (rFromFacet.Surface)
128 {
129 pIcon->GetMFacet().GrabFrom(rSource&: rFromFacet);
130 }
131 else
132 {
133 // reset custom icon
134 // following update-call will reset to default icon
135 pIcon->GetMFacet().Clear();
136 }
137}
138
139void C4StartupPlrSelDlg::ListItem::SetIcon(C4GUI::Icons icoNew)
140{
141 pIcon->SetIcon(icoNew);
142}
143
144void C4StartupPlrSelDlg::ListItem::LoadPortrait(C4Group &rGrp, bool fUseDefault)
145{
146 bool fPortraitLinked = false;
147 if (!rGrp.FindEntry(C4CFN_Portrait) || !fctPortraitBase.Load(hGroup&: rGrp, C4CFN_Portrait))
148 if (!rGrp.FindEntry(C4CFN_Portrait_Old) || !fctPortraitBase.Load(hGroup&: rGrp, C4CFN_Portrait_Old))
149 {
150 // no custom portrait: Link to some default if desired
151 if (!fUseDefault) return;
152 SetDefaultPortrait();
153 fPortraitLinked = true;
154 }
155 if (!fPortraitLinked) CreateColoredPortrait();
156}
157
158void C4StartupPlrSelDlg::ListItem::CreateColoredPortrait()
159{
160 if (fctPortrait.CreateClrByOwner(pBySurface: fctPortraitBase.Surface))
161 {
162 fctPortrait.Wdt = fctPortraitBase.Wdt;
163 fctPortrait.Hgt = fctPortraitBase.Hgt;
164 }
165}
166
167void C4StartupPlrSelDlg::ListItem::SetDefaultPortrait()
168{
169 fctPortrait.Set(Game.GraphicsResource.fctPlayerClr);
170}
171
172void C4StartupPlrSelDlg::ListItem::GrabPortrait(C4FacetExSurface *pFromFacet)
173{
174 if (pFromFacet && pFromFacet->Surface)
175 {
176 fctPortraitBase.GrabFrom(rSource&: *pFromFacet);
177 CreateColoredPortrait();
178 }
179 else
180 {
181 SetDefaultPortrait();
182 }
183}
184
185void C4StartupPlrSelDlg::ListItem::UpdateOwnPos()
186{
187 // parent for client rect
188 typedef C4GUI::Window ParentClass;
189 ParentClass::UpdateOwnPos();
190 // reposition items
191 C4GUI::ComponentAligner caBounds(GetContainedClientRect(), IconLabelSpacing, IconLabelSpacing);
192 // nothing to reposition for now...
193}
194
195void C4StartupPlrSelDlg::ListItem::SetFilename(const StdStrBuf &sNewFN)
196{
197 // just set fn - UpdateCore-call will follow later
198 Filename.Copy(Buf2: sNewFN);
199}
200
201bool C4StartupPlrSelDlg::ListItem::CheckNameHotkey(const char *c)
202{
203 // return whether this item can be selected by entering given char:
204 // first char of name must match
205 // FIXME: Unicode
206 if (!pNameLabel) return false;
207 const char *szName = pNameLabel->GetText();
208 return szName && (toupper(c: *szName) == toupper(c: c[0]));
209}
210
211// C4StartupPlrSelDlg::PlayerListItem
212
213C4StartupPlrSelDlg::PlayerListItem::PlayerListItem(C4StartupPlrSelDlg *pForDlg, C4GUI::ListBox *pForListBox, C4GUI::Element *pInsertBeforeElement, bool fActivated)
214 : ListItem(pForDlg, pForListBox, pInsertBeforeElement, fActivated), fHasCustomIcon(false) {}
215
216void C4StartupPlrSelDlg::PlayerListItem::Load(const StdStrBuf &rsFilename)
217{
218 int32_t iHeight = GetBounds().Hgt;
219 // backup filename
220 SetFilename(rsFilename);
221 // load player info
222 C4Group PlrGroup;
223 if (!PlrGroup.Open(szGroupName: rsFilename.getData()))
224 throw LoadError(std::format(fmt: "Error loading player file from {}: Error opening group: {}", args: rsFilename.getData(), args: PlrGroup.GetError()));
225 if (!Core.Load(hGroup&: PlrGroup))
226 throw LoadError(std::format(fmt: "Error loading player file from {}: Core data invalid or missing (Group: {})!", args: rsFilename.getData(), args: PlrGroup.GetError()));
227 // load icon
228 C4FacetExSurface fctIcon;
229 if (PlrGroup.FindEntry(C4CFN_BigIcon) && fctIcon.Load(hGroup&: PlrGroup, C4CFN_BigIcon))
230 fHasCustomIcon = true;
231 else
232 {
233 // no custom icon: create default by player color
234 fctIcon.Create(iWdt: iHeight, iHgt: iHeight);
235 Game.GraphicsResource.fctPlayerClr.DrawClr(cgo&: fctIcon, fAspect: true, dwClr: Core.PrefColorDw);
236 }
237 GrabIcon(rFromFacet&: fctIcon);
238 // load portrait
239 LoadPortrait(rGrp&: PlrGroup, fUseDefault: true);
240 // done loading
241 if (!PlrGroup.Close())
242 throw LoadError(std::format(fmt: "Error loading player file from {}: Error closing group: {}", args: rsFilename.getData(), args: PlrGroup.GetError()));
243 // default name
244 if (!*Core.PrefName) SCopy(szSource: GetFilenameOnly(strFilename: rsFilename.getData()), sTarget: Core.PrefName, iMaxL: sizeof(Core.PrefName) - 1);
245 SetName(Core.PrefName);
246}
247
248C4GUI::ContextMenu *C4StartupPlrSelDlg::PlayerListItem::ContextMenu()
249{
250 // menu operations work on selected item only
251 pPlrSelDlg->SetSelection(this);
252 // context menu operations
253 C4GUI::ContextMenu *pCtx = new C4GUI::ContextMenu();
254 pCtx->AddItem(szText: LoadResStr(id: C4ResStrTableKey::IDS_BTN_PROPERTIES), szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PLAYERPROPERTIES), icoIcon: C4GUI::Ico_None, pMenuHandler: new C4GUI::CBMenuHandler<C4StartupPlrSelDlg>(pPlrSelDlg, &C4StartupPlrSelDlg::OnPropertyCtx));
255 pCtx->AddItem(szText: LoadResStr(id: C4ResStrTableKey::IDS_BTN_DELETE), szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PLAYERDELETE), icoIcon: C4GUI::Ico_None, pMenuHandler: new C4GUI::CBMenuHandler<C4StartupPlrSelDlg>(pPlrSelDlg, &C4StartupPlrSelDlg::OnDelCtx));
256 return pCtx;
257}
258
259void C4StartupPlrSelDlg::PlayerListItem::GrabCustomIcon(C4FacetExSurface &fctGrabFrom)
260{
261 // set flag
262 fHasCustomIcon = !!fctGrabFrom.Surface;
263 // base class grab
264 GrabIcon(rFromFacet&: fctGrabFrom);
265}
266
267void C4StartupPlrSelDlg::PlayerListItem::UpdateCore(C4PlayerInfoCore &NewCore)
268{
269 C4Group PlrGroup;
270 if (!PlrGroup.Open(szGroupName: GetFilename().getData())
271 || !NewCore.Save(hGroup&: PlrGroup)
272 || !PlrGroup.Close())
273 {
274 GetScreen()->ShowMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_FAIL_MODIFY), szCaption: "", icoIcon: C4GUI::Ico_Error);
275 return;
276 }
277 Core = NewCore;
278 SetName(Core.PrefName);
279 // re-set non-custom icons
280 if (!fHasCustomIcon)
281 {
282 fHasCustomIcon = false;
283 int32_t iHeight = GetBounds().Hgt;
284 C4FacetExSurface fctIcon; fctIcon.Create(iWdt: iHeight, iHgt: iHeight);
285 Game.GraphicsResource.fctPlayerClr.DrawClr(cgo&: fctIcon, fAspect: true, dwClr: Core.PrefColorDw);
286 GrabIcon(rFromFacet&: fctIcon);
287 }
288 // update in selection
289 C4StartupPlrSelDlg *pDlg = static_cast<C4StartupPlrSelDlg *>(GetDlg());
290 if (pDlg && pDlg->GetSelection() == this) pDlg->UpdateSelection();
291}
292
293void C4StartupPlrSelDlg::PlayerListItem::SetSelectionInfo(C4GUI::TextWindow *pSelectionInfo)
294{
295 // write info text for player
296 pSelectionInfo->ClearText(fDoUpdate: false);
297 pSelectionInfo->AddTextLine(szText: Core.PrefName, pFont: &C4Startup::Get()->Graphics.BookFontCapt, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
298 pSelectionInfo->AddTextLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_DESC_PLAYER, args: static_cast<int>(Core.Score), args: static_cast<int>(Core.Rounds), args: static_cast<int>(Core.RoundsWon), args: static_cast<int>(Core.RoundsLost), args: TimeString(iSeconds: Core.TotalPlayingTime), args&: Core.Comment).c_str(), pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
299 if (Core.LastRound.Title[0])
300 pSelectionInfo->AddTextLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_DESC_LASTGAME, args: Core.LastRound.Title.getData(), args: DateString(iTime: Core.LastRound.Date), args: TimeString(iSeconds: Core.LastRound.Duration), args: static_cast<int>(Core.LastRound.FinalScore)).c_str(), pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
301 pSelectionInfo->UpdateHeight();
302}
303
304std::string C4StartupPlrSelDlg::PlayerListItem::GetDelWarning()
305{
306 std::string warning{LoadResStr(id: C4ResStrTableKey::IDS_MSG_DELETEPLR, args&: Core.PrefName)};
307 int32_t iPlrTime = Core.TotalPlayingTime;
308 if (iPlrTime > 60 * 60 * 10)
309 warning += LoadResStr(id: C4ResStrTableKey::IDS_MSG_DELETEPLR_PLAYTIME,
310 args: TimeString(iSeconds: iPlrTime));
311 return warning;
312}
313
314bool C4StartupPlrSelDlg::PlayerListItem::MoveFilename(const char *szToFilename)
315{
316 // anything to do?
317 if (ItemIdentical(szFilename1: GetFilename().getData(), szFilename2: szToFilename)) return true;
318 // do it
319 if (!MoveItem(szSource: GetFilename().getData(), szTarget: szToFilename)) return false;
320 // reflect change in class
321 SetFilename(StdStrBuf::MakeRef(str: szToFilename));
322 return true;
323}
324
325// C4StartupPlrSelDlg::CrewListItem
326
327C4StartupPlrSelDlg::CrewListItem::CrewListItem(C4StartupPlrSelDlg *pForDlg, C4GUI::ListBox *pForListBox, uint32_t dwPlrClr)
328 : ListItem(pForDlg, pForListBox, nullptr, false), fLoaded(false), dwPlrClr(dwPlrClr), pParentGrp(nullptr)
329{
330 SetIcon(C4GUI::Ico_Wait);
331}
332
333void C4StartupPlrSelDlg::CrewListItem::UpdateClonkEnabled()
334{
335 if (!fLoaded) return;
336 Core.Participation = pCheck->GetChecked();
337 // immediate save of changes
338 RewriteCore();
339}
340
341void C4StartupPlrSelDlg::CrewListItem::Load(C4Group &rGrp, const StdStrBuf &rsFilename)
342{
343 // backup filename (doesn't include path)
344 SetFilename(rsFilename);
345 // load core
346 C4Group CrewGroup;
347 if (!CrewGroup.OpenAsChild(pMother: &rGrp, szEntryName: rsFilename.getData()))
348 throw LoadError(std::format(fmt: "Error loading crew file from {} in {}: Error opening group: {}",
349 args: rsFilename.getData(), args: rGrp.GetFullName().getData(), args: CrewGroup.GetError()));
350 if (!Core.Load(hGroup&: CrewGroup))
351 throw LoadError(std::format(fmt: "Error loading crew file from {}: Core data invalid or missing (Group: {})!",
352 args: CrewGroup.GetFullName().getData(), args: CrewGroup.GetError()));
353 ListItem::SetName(Core.Name);
354 pCheck->SetChecked(!!Core.Participation);
355 // load rank as icon
356 C4FacetExSurface fctIcon;
357 if (fctIcon.Load(hGroup&: CrewGroup, C4CFN_ClonkRank, iWdt: C4FCT_Full, iHgt: C4FCT_Full, fOwnPal: false, fNoErrIfNotFound: true))
358 {
359 GrabIcon(rFromFacet&: fctIcon);
360 }
361 else
362 {
363 // no custom icon: create default by rank system
364 if (C4RankSystem::DrawRankSymbol(fctSymbol: &fctIcon, iRank: Core.Rank, pfctRankSymbols: &Game.GraphicsResource.fctRank, iRankSymbolCount: Game.GraphicsResource.iNumRanks, fOwnSurface: true))
365 GrabIcon(rFromFacet&: fctIcon);
366 }
367 // load portrait; empty by default
368 LoadPortrait(rGrp&: CrewGroup, fUseDefault: false);
369 // backup group loaded from - assumes it stays valid!
370 pParentGrp = &rGrp;
371 // load success!
372 fLoaded = true;
373}
374
375C4GUI::ContextMenu *C4StartupPlrSelDlg::CrewListItem::ContextMenu()
376{
377 // menu operations work on selected item only
378 pPlrSelDlg->SetSelection(this);
379 // context menu operations
380 C4GUI::ContextMenu *pCtx = new C4GUI::ContextMenu();
381 pCtx->AddItem(szText: LoadResStr(id: C4ResStrTableKey::IDS_BTN_RENAME), szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_DESC_CREWRENAME), icoIcon: C4GUI::Ico_None, pMenuHandler: new C4GUI::CBMenuHandler<C4StartupPlrSelDlg>(pPlrSelDlg, &C4StartupPlrSelDlg::OnPropertyCtx));
382 pCtx->AddItem(szText: LoadResStr(id: C4ResStrTableKey::IDS_BTN_DELETE), szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_MSG_DELETECLONK_DESC), icoIcon: C4GUI::Ico_None, pMenuHandler: new C4GUI::CBMenuHandler<C4StartupPlrSelDlg>(pPlrSelDlg, &C4StartupPlrSelDlg::OnDelCtx));
383 pCtx->AddItem(szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SETDEATHMESSAGE), szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SETTHEMESSAGETHATAPPEARWH), icoIcon: C4GUI::Ico_None, pMenuHandler: new C4GUI::CBMenuHandler<C4StartupPlrSelDlg::CrewListItem>(this, &C4StartupPlrSelDlg::CrewListItem::OnDeathMessageCtx));
384 return pCtx;
385}
386
387void C4StartupPlrSelDlg::CrewListItem::OnDeathMessageCtx(C4GUI::Element *el)
388{
389 // Death message dialog
390 C4GUI::InputDialog *pDlg;
391 GetScreen()->ShowRemoveDlg(pDlg: pDlg = new C4GUI::InputDialog(LoadResStr(id: C4ResStrTableKey::IDS_MSG_ENTERNEWDEATHMESSAGE), LoadResStr(id: C4ResStrTableKey::IDS_MSG_SETDEATHMESSAGE), C4GUI::Ico_Ex_Comment, new C4GUI::InputCallback<C4StartupPlrSelDlg::CrewListItem>(this, &C4StartupPlrSelDlg::CrewListItem::OnDeathMessageSet), false));
392 pDlg->SetMaxText(C4MaxDeathMsg);
393 pDlg->SetInputText(Core.DeathMessage);
394}
395
396void C4StartupPlrSelDlg::CrewListItem::OnDeathMessageSet(const StdStrBuf &rsNewMessage)
397{
398 // copy msg
399 if (!rsNewMessage) *Core.DeathMessage = '\0'; else SCopy(szSource: rsNewMessage.getData(), sTarget: Core.DeathMessage, iMaxL: C4MaxDeathMsg);
400 // save
401 RewriteCore();
402 // acoustic feedback
403 C4GUI::GUISound(szSound: "Connect");
404}
405
406void C4StartupPlrSelDlg::CrewListItem::RewriteCore()
407{
408 if (!fLoaded) return;
409 C4Group CrewGroup;
410 if (!CrewGroup.OpenAsChild(pMother: pParentGrp, szEntryName: GetFilename().getData())
411 || !Core.Save(hGroup&: CrewGroup, pDefs: nullptr)
412 || !CrewGroup.Close() || !pParentGrp->Save(fReOpen: true))
413 {
414 GetScreen()->ShowMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_FAIL_MODIFY), szCaption: "", icoIcon: C4GUI::Ico_Error);
415 return;
416 }
417}
418
419bool C4StartupPlrSelDlg::CrewListItem::SetName(const char *szNewName)
420{
421 if (!fLoaded) return false;
422 // validate name
423 if (!szNewName || !*szNewName) return false;
424 if (SEqual(szStr1: szNewName, szStr2: Core.Name)) return true;
425 // generate filename from new name
426 char fn[_MAX_PATH + 1];
427 SCopy(szSource: szNewName, sTarget: fn, _MAX_PATH);
428 MakeFilenameFromTitle(szTitle: fn);
429 if (!*fn) return false;
430 SAppend(szSource: ".c4i", szTarget: fn, _MAX_PATH);
431 // check if a rename is due
432 if (!ItemIdentical(szFilename1: fn, szFilename2: GetFilename().getData()))
433 {
434 // check for duplicate filename
435 if (pParentGrp->FindEntry(szWildCard: fn))
436 {
437 const std::string msg{LoadResStr(id: C4ResStrTableKey::IDS_ERR_CLONKCOLLISION, args&: fn)};
438 Game.pGUI->ShowMessageModal(szMessage: msg.c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_FAIL_RENAME), dwButtons: C4GUI::MessageDialog::btnOK, icoIcon: C4GUI::Ico_Error);
439 return false;
440 }
441 // OK; then rename
442 if (!pParentGrp->Rename(szFile: GetFilename().getData(), szNewName: fn) || !pParentGrp->Save(fReOpen: true))
443 {
444 const std::string msg{LoadResStr(id: C4ResStrTableKey::IDS_ERR_RENAMEFILE, args: GetFilename().getData(), args&: fn)};
445 Game.pGUI->ShowMessageModal(szMessage: msg.c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_FAIL_RENAME), dwButtons: C4GUI::MessageDialog::btnOK, icoIcon: C4GUI::Ico_Error);
446 return false;
447 }
448 const char *szConstFn = fn;
449 SetFilename(StdStrBuf::MakeRef(str: szConstFn));
450 }
451 // update clonk name and core
452 ListItem::SetName(szNewName);
453 SCopy(szSource: szNewName, sTarget: Core.Name, iMaxL: C4MaxName);
454 RewriteCore();
455 return true;
456}
457
458std::string C4StartupPlrSelDlg::CrewListItem::GetPhysicalTextLine(int32_t iPhysValue, const C4ResStrTableKeyFormat<> idsName)
459{
460 const int32_t iMaxBars = 10;
461 std::string result{LoadResStr(id: idsName)};
462 result += ' ';
463 result.append(n: iMaxBars * iPhysValue / C4MaxPhysical, c: '\xb7' /*·*/);
464 return result;
465}
466
467void C4StartupPlrSelDlg::CrewListItem::SetSelectionInfo(C4GUI::TextWindow *pSelectionInfo)
468{
469 // write info text for player
470 pSelectionInfo->ClearText(fDoUpdate: false);
471 pSelectionInfo->AddTextLine(szText: std::format(fmt: "{} {}", args: Core.sRankName.getData(), args: +Core.Name).c_str(), pFont: &C4Startup::Get()->Graphics.BookFontCapt, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
472 std::string promo;
473 int32_t iNextRankExp; StdStrBuf sNextRankName;
474 if (Core.GetNextRankInfo(rDefaultRanks&: Game.Rank, piNextRankExp: &iNextRankExp, psNextRankName: &sNextRankName))
475 promo = LoadResStr(id: C4ResStrTableKey::IDS_DESC_PROMO, args: sNextRankName.getData(), args: static_cast<int>(iNextRankExp));
476 else
477 promo = LoadResStr(id: C4ResStrTableKey::IDS_DESC_NOPROMO);
478 pSelectionInfo->AddTextLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_DESC_OBJECT,
479 args&: Core.TypeName, args&: Core.Experience, args&: Core.Rounds, args&: Core.DeathCount,
480 args: promo.c_str(), args: TimeString(iSeconds: Core.TotalPlayingTime), args: DateString(iTime: Core.Birthday)).c_str(),
481 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
482 pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Energy, idsName: C4ResStrTableKey::IDS_DESC_ENERGY).c_str(),
483 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
484 pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Breath, idsName: C4ResStrTableKey::IDS_DESC_BREATH).c_str(),
485 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
486 pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Walk, idsName: C4ResStrTableKey::IDS_DESC_WALK).c_str(),
487 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
488 pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Jump, idsName: C4ResStrTableKey::IDS_DESC_JUMP).c_str(),
489 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
490 if (Core.Physical.CanScale) pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Scale, idsName: C4ResStrTableKey::IDS_DESC_SCALE).c_str(),
491 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
492 if (Core.Physical.CanHangle) pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Hangle, idsName: C4ResStrTableKey::IDS_DESC_HANGLE).c_str(),
493 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
494 pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Dig, idsName: C4ResStrTableKey::IDS_DESC_DIG).c_str(),
495 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
496 pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Swim, idsName: C4ResStrTableKey::IDS_DESC_SWIM).c_str(),
497 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
498 pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Throw, idsName: C4ResStrTableKey::IDS_DESC_THROW).c_str(),
499 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
500 pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Push, idsName: C4ResStrTableKey::IDS_DESC_PUSH).c_str(),
501 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
502 pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Fight, idsName: C4ResStrTableKey::IDS_DESC_FIGHT).c_str(),
503 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
504 if (Core.Physical.Magic) pSelectionInfo->AddTextLine(szText: GetPhysicalTextLine(iPhysValue: Core.Physical.Magic, idsName: C4ResStrTableKey::IDS_DESC_MAGIC).c_str(),
505 pFont: &C4Startup::Get()->Graphics.BookFont, dwClr: ClrPlayerItem, fDoUpdate: false, fMakeReadableOnBlack: false);
506 pSelectionInfo->UpdateHeight();
507}
508
509std::string C4StartupPlrSelDlg::CrewListItem::GetDelWarning()
510{
511 std::string warning{LoadResStr(id: C4ResStrTableKey::IDS_MSG_DELETECLONK,
512 args: Core.sRankName.getData(), args&: Core.Name)};
513 int32_t iPlrTime = Core.TotalPlayingTime;
514 if (iPlrTime > 60 * 60 * 10)
515 warning += LoadResStr(id: C4ResStrTableKey::IDS_MSG_DELETECLONK_PLAYTIME, args: TimeString(iSeconds: iPlrTime));
516 return warning;
517}
518
519void C4StartupPlrSelDlg::CrewListItem::CrewRename()
520{
521 if (pPlrSelDlg->pRenameEdit) return;
522 // rename this entry
523 pPlrSelDlg->pRenameEdit = new C4GUI::CallbackRenameEdit<C4StartupPlrSelDlg::CrewListItem, RenameParams>(pNameLabel, this, RenameParams(), &C4StartupPlrSelDlg::CrewListItem::DoRenaming, &C4StartupPlrSelDlg::CrewListItem::AbortRenaming);
524}
525
526void C4StartupPlrSelDlg::CrewListItem::AbortRenaming(RenameParams par)
527{
528 // no renaming
529 pPlrSelDlg->pRenameEdit = nullptr;
530}
531
532C4GUI::RenameResult C4StartupPlrSelDlg::CrewListItem::DoRenaming(RenameParams par, const char *szNewName)
533{
534 // accept if name can be set; will fail if name is invalid or already given to another Crew member
535 if (!SetName(szNewName)) return C4GUI::RR_Invalid;
536 pPlrSelDlg->pRenameEdit = nullptr;
537 // update in selection
538 C4StartupPlrSelDlg *pDlg = static_cast<C4StartupPlrSelDlg *>(GetDlg());
539 if (pDlg && pDlg->GetSelection() == this) pDlg->UpdateSelection();
540 return C4GUI::RR_Accepted;
541}
542
543// C4StartupPlrSelDlg
544
545C4StartupPlrSelDlg::C4StartupPlrSelDlg() : C4StartupDlg("W"), eMode(PSDM_Player), pRenameEdit(nullptr)
546{
547 UpdateSize(); // for clientrect
548
549 // screen calculations
550 int iButtonHeight = C4GUI_ButtonHgt;
551 int iButtonXSpacing = (GetClientRect().Wdt > 700) ? GetClientRect().Wdt / 58 : 2;
552 int iButtonCount = 6;
553 C4GUI::ComponentAligner caMain(GetClientRect(), 0, 0, true);
554 C4GUI::ComponentAligner caButtonArea(caMain.GetFromBottom(iHgt: (std::max)(a: caMain.GetHeight() / 15, b: iButtonHeight)), 0, 0);
555 rcBottomButtons = caButtonArea.GetCentered(iWdt: caMain.GetWidth(), iHgt: iButtonHeight);
556 iBottomButtonWidth = (caButtonArea.GetWidth() - iButtonXSpacing * (iButtonCount - 1)) / iButtonCount;
557 C4Rect rcMain = caMain.GetAll();
558 C4Rect rcPlrList = C4Rect(rcMain.Wdt / 10, rcMain.Hgt * 10 / 36, rcMain.Wdt * 25 / 81, rcMain.Hgt * 2 / 3);
559 C4Rect rcInfoWindow = C4Rect(rcMain.Wdt * 371 / 768, rcMain.Hgt * 197 / 451, rcMain.Wdt * 121 / 384, rcMain.Hgt * 242 / 451);
560 int iPictureWidth = (std::min)(a: rcMain.Wdt * 121 / 384, b: 200);
561 int iPictureHeight = iPictureWidth * 3 / 4;
562 C4Rect rcPictureArea = C4Rect(rcMain.Wdt * 613 / 768 - iPictureWidth, rcMain.Hgt * 197 / 451 - iPictureHeight, iPictureWidth, iPictureHeight);
563
564 AddElement(pChild: pPlrListBox = new C4GUI::ListBox(rcPlrList));
565 pPlrListBox->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PLAYERFILES));
566 pPlrListBox->SetDecoration(fDrawBG: false, pToGfx: &C4Startup::Get()->Graphics.sfctBookScroll, fAutoScroll: true);
567 pPlrListBox->UpdateElementPositions();
568 pPlrListBox->SetSelectionChangeCallbackFn(new C4GUI::CallbackHandler<C4StartupPlrSelDlg>(this, &C4StartupPlrSelDlg::OnSelChange));
569 pPlrListBox->SetSelectionDblClickFn(new C4GUI::CallbackHandler<C4StartupPlrSelDlg>(this, &C4StartupPlrSelDlg::OnSelDblClick));
570 AddElement(pChild: pSelectionInfo = new C4GUI::TextWindow(rcInfoWindow));
571 pSelectionInfo->SetDecoration(fDrawBG: false, fDrawFrame: false, pToGfx: &C4Startup::Get()->Graphics.sfctBookScroll, fAutoScroll: true);
572 pSelectionInfo->UpdateHeight();
573 AddElement(pChild: pPortraitPict = new C4GUI::Picture(rcPictureArea, true));
574
575 // bottom line buttons - positioning done in UpdateBottomButtons by UpdatePlayerList
576 C4Rect rcDefault(0, 0, 10, 10);
577 AddElement(pChild: btnBack = new C4GUI::CallbackButton<C4StartupPlrSelDlg>(LoadResStr(id: C4ResStrTableKey::IDS_BTN_BACK), rcDefault, &C4StartupPlrSelDlg::OnBackBtn));
578 AddElement(pChild: btnNew = new C4GUI::CallbackButton<C4StartupPlrSelDlg>(LoadResStr(id: C4ResStrTableKey::IDS_BTN_NEW), rcDefault, &C4StartupPlrSelDlg::OnNewBtn));
579 btnNew->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_NEWPLAYER));
580 AddElement(pChild: btnActivatePlr = new C4GUI::CallbackButton<C4StartupPlrSelDlg>(nullptr, rcDefault, &C4StartupPlrSelDlg::OnActivateBtn));
581 AddElement(pChild: btnDelete = new C4GUI::CallbackButton<C4StartupPlrSelDlg>(LoadResStr(id: C4ResStrTableKey::IDS_BTN_DELETE), rcDefault, &C4StartupPlrSelDlg::OnDelBtn));
582 AddElement(pChild: btnProperties = new C4GUI::CallbackButton<C4StartupPlrSelDlg>(nullptr, rcDefault, &C4StartupPlrSelDlg::OnPropertyBtn));
583 AddElement(pChild: btnCrew = new C4GUI::CallbackButton<C4StartupPlrSelDlg>(LoadResStr(id: C4ResStrTableKey::IDS_SELECT_CREW), rcDefault, &C4StartupPlrSelDlg::OnCrewBtn));
584 btnCrew->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PLAYERCREW));
585
586 // refill listboxes
587 UpdatePlayerList();
588 // Just safety incase listbox was empty, in which case no selection change callback will have been done:
589 // Update current listbox selection to that of no selected item
590 if (!pPlrListBox->GetFirst()) UpdateSelection();
591
592 // initial focus on player list
593 SetFocus(pCtrl: pPlrListBox, fByMouse: false);
594
595 // key bindings
596 C4CustomKey::CodeList keys;
597 keys.push_back(x: C4KeyCodeEx(K_BACK));
598 keys.push_back(x: C4KeyCodeEx(K_LEFT));
599 keys.push_back(x: C4KeyCodeEx(K_ESCAPE));
600 if (Config.Controls.GamepadGuiControl)
601 {
602 keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_AnyHighButton)));
603 }
604 pKeyBack = new C4KeyBinding(keys, "StartupPlrSelBack", KEYSCOPE_Gui,
605 new C4GUI::DlgKeyCB<C4StartupPlrSelDlg>(*this, &C4StartupPlrSelDlg::KeyBack), C4CustomKey::PRIO_CtrlOverride);
606 pKeyProperties = new C4KeyBinding(C4KeyCodeEx(K_F2), "StartupPlrSelProp", KEYSCOPE_Gui,
607 new C4GUI::DlgKeyCB<C4StartupPlrSelDlg>(*this, &C4StartupPlrSelDlg::KeyProperties), C4CustomKey::PRIO_CtrlOverride);
608 pKeyCrew = new C4KeyBinding(C4KeyCodeEx(K_RIGHT), "StartupPlrSelCrew", KEYSCOPE_Gui,
609 new C4GUI::ControlKeyDlgCB<C4StartupPlrSelDlg>(pPlrListBox, *this, &C4StartupPlrSelDlg::KeyCrew), C4CustomKey::PRIO_CtrlOverride);
610 pKeyDelete = new C4KeyBinding(C4KeyCodeEx(K_DELETE), "StartupPlrSelDelete", KEYSCOPE_Gui,
611 new C4GUI::DlgKeyCB<C4StartupPlrSelDlg>(*this, &C4StartupPlrSelDlg::KeyDelete), C4CustomKey::PRIO_CtrlOverride);
612 pKeyNew = new C4KeyBinding(C4KeyCodeEx(K_INSERT), "StartupPlrSelNew", KEYSCOPE_Gui,
613 new C4GUI::DlgKeyCB<C4StartupPlrSelDlg>(*this, &C4StartupPlrSelDlg::KeyNew), C4CustomKey::PRIO_CtrlOverride);
614}
615
616C4StartupPlrSelDlg::~C4StartupPlrSelDlg()
617{
618 delete pKeyDelete;
619 delete pKeyCrew;
620 delete pKeyProperties;
621 delete pKeyBack;
622 delete pKeyNew;
623}
624
625void C4StartupPlrSelDlg::AbortRenaming()
626{
627 if (pRenameEdit) pRenameEdit->Abort();
628}
629
630void C4StartupPlrSelDlg::DrawElement(C4FacetEx &cgo)
631{
632 // draw background
633 DrawBackground(cgo, rFromFct&: C4Startup::Get()->Graphics.fctPlrSelBG);
634}
635
636void C4StartupPlrSelDlg::UpdateBottomButtons()
637{
638 // bottom line buttons depend on list mode
639 C4GUI::ComponentAligner caBottomButtons(rcBottomButtons, 0, 0);
640 switch (eMode)
641 {
642 case PSDM_Player:
643 {
644 // update some buttons for player mode
645 btnProperties->SetText(LoadResStr(id: C4ResStrTableKey::IDS_BTN_PROPERTIES));
646 btnProperties->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PLAYERPROPERTIES));
647 btnNew->SetVisibility(true);
648 btnCrew->SetVisibility(true);
649 btnDelete->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PLAYERDELETE));
650 btnBack->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_BACKMAIN));
651 btnBack->SetBounds(caBottomButtons.GetGridCell(iSectX: 0, iSectXMax: 6, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonWidth, C4GUI_ButtonHgt, fCenterPos: true));
652 btnNew->SetBounds(caBottomButtons.GetGridCell(iSectX: 1, iSectXMax: 6, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonWidth, C4GUI_ButtonHgt, fCenterPos: true));
653 btnActivatePlr->SetBounds(caBottomButtons.GetGridCell(iSectX: 2, iSectXMax: 6, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonWidth, C4GUI_ButtonHgt, fCenterPos: true));
654 btnDelete->SetBounds(caBottomButtons.GetGridCell(iSectX: 3, iSectXMax: 6, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonWidth, C4GUI_ButtonHgt, fCenterPos: true));
655 btnProperties->SetBounds(caBottomButtons.GetGridCell(iSectX: 4, iSectXMax: 6, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonWidth, C4GUI_ButtonHgt, fCenterPos: true));
656 btnCrew->SetBounds(caBottomButtons.GetGridCell(iSectX: 5, iSectXMax: 6, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonWidth, C4GUI_ButtonHgt, fCenterPos: true));
657 }
658 break;
659
660 case PSDM_Crew:
661 {
662 // update some buttons for player mode
663 btnProperties->SetText(LoadResStr(id: C4ResStrTableKey::IDS_BTN_RENAME));
664 btnProperties->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_CREWRENAME));
665 btnNew->SetVisibility(false);
666 btnCrew->SetVisibility(false);
667 btnDelete->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_MSG_DELETECLONK_DESC));
668 btnBack->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_MSG_BACKTOPLAYERDLG));
669 btnBack->SetBounds(caBottomButtons.GetGridCell(iSectX: 0, iSectXMax: 4, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonWidth, C4GUI_ButtonHgt, fCenterPos: true));
670 btnActivatePlr->SetBounds(caBottomButtons.GetGridCell(iSectX: 1, iSectXMax: 4, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonWidth, C4GUI_ButtonHgt, fCenterPos: true));
671 btnDelete->SetBounds(caBottomButtons.GetGridCell(iSectX: 2, iSectXMax: 4, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonWidth, C4GUI_ButtonHgt, fCenterPos: true));
672 btnProperties->SetBounds(caBottomButtons.GetGridCell(iSectX: 3, iSectXMax: 4, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonWidth, C4GUI_ButtonHgt, fCenterPos: true));
673 }
674 break;
675 };
676}
677
678void C4StartupPlrSelDlg::UpdatePlayerList()
679{
680 // something has changed!
681 AbortRenaming();
682 // refill pPlrListBox with players in player folder or crew
683 // clear old items
684 C4GUI::Element *pEl;
685 while (pEl = pPlrListBox->GetFirst()) delete pEl;
686 // update command buttons
687 UpdateBottomButtons();
688 // create new
689 switch (eMode)
690 {
691 case PSDM_Player:
692 {
693 SetTitle(LoadResStrNoAmp(id: C4ResStrTableKey::IDS_DLG_PLAYERSELECTION).c_str());
694 // player mode: insert all players
695 const char *szFn;
696 const std::string searchPath{std::format(fmt: "{}{}", args: +Config.General.ExePath, args: +Config.General.PlayerPath)};
697 PlayerListItem *pFirstActivatedPlrItem = nullptr, *pFirstDeactivatedPlrItem = nullptr, *pPlrItem = nullptr;
698 for (DirectoryIterator i(searchPath.c_str()); szFn = *i; i++)
699 {
700 szFn = Config.AtExeRelativePath(szFilename: szFn);
701 if (*GetFilename(path: szFn) == '.') continue; // ignore ".", ".." and private files (".*")
702 if (!WildcardMatch(C4CFN_PlayerFiles, szFName2: GetFilename(path: szFn))) continue;
703 bool fIsParticipating = !!SIsModule(szList: Config.General.Participants, szString: szFn, ipIndex: nullptr, fCaseSensitive: false);
704 pPlrItem = new PlayerListItem(this, pPlrListBox, nullptr, fIsParticipating);
705 try
706 {
707 pPlrItem->Load(rsFilename: StdStrBuf::MakeRef(str: szFn));
708 }
709 catch (ListItem::LoadError &e)
710 {
711 // invalid player: ignore but log error message
712 DebugLog(message: e.what());
713 delete pPlrItem;
714 continue;
715 }
716 if (fIsParticipating)
717 {
718 if (!pFirstActivatedPlrItem) pFirstActivatedPlrItem = pPlrItem;
719 }
720 else if (!pFirstDeactivatedPlrItem) pFirstDeactivatedPlrItem = pPlrItem;
721 }
722 // select first element; prefer activated player
723 if (!(pPlrItem = pFirstActivatedPlrItem))
724 pPlrItem = pFirstDeactivatedPlrItem;
725 if (pPlrItem)
726 pPlrListBox->SelectEntry(pNewSel: pPlrItem, fByUser: false);
727 // re-validate Game.PlayerFilename
728 UpdateActivatedPlayers();
729 break;
730 }
731
732 case PSDM_Crew:
733 {
734 SetTitle(std::format(fmt: "{} {}", args: LoadResStrNoAmp(id: C4ResStrTableKey::IDS_CTL_CREW).c_str(), args: +CurrPlayer.Core.PrefName).c_str());
735 // crew mode: Insert complete crew of player (2do: sort)
736 bool fSucc; char szFn[_MAX_PATH + 1];
737 for (fSucc = CurrPlayer.Grp.FindEntry(C4CFN_ObjectInfoFiles, sFileName: szFn); fSucc; fSucc = CurrPlayer.Grp.FindNextEntry(C4CFN_ObjectInfoFiles, sFileName: szFn, iSize: nullptr, fChild: nullptr, fStartAtFilename: true))
738 {
739 CrewListItem *pCrewItem = new CrewListItem(this, pPlrListBox, CurrPlayer.Core.PrefColorDw);
740 try
741 {
742 pCrewItem->Load(rGrp&: CurrPlayer.Grp, rsFilename: StdStrBuf(szFn));
743 }
744 catch (ListItem::LoadError &e)
745 {
746 // invalid player: ignore but log error message
747 DebugLog(message: e.what());
748 delete pCrewItem;
749 continue;
750 }
751 }
752 // resort crew by type and experience
753 ResortCrew();
754 pPlrListBox->SelectFirstEntry(fByUser: false);
755 break;
756 }
757 }
758}
759
760C4StartupPlrSelDlg::ListItem *C4StartupPlrSelDlg::GetSelection()
761{
762 // get selected item: There's only instances of PlrListItem in this list
763 return static_cast<ListItem *>(pPlrListBox->GetSelectedItem());
764}
765
766void C4StartupPlrSelDlg::SetSelection(ListItem *pNewItem)
767{
768 // update selection in listbox
769 pPlrListBox->SelectEntry(pNewSel: pNewItem, fByUser: false);
770}
771
772void C4StartupPlrSelDlg::UpdateSelection()
773{
774 // something has changed!
775 AbortRenaming();
776 // get currently selected player
777 ListItem *pSel = GetSelection();
778 // button text 'activate' if current player is deactivated; 'deactivate' otherwise
779 if (pSel && pSel->IsActivated())
780 {
781 btnActivatePlr->SetText(LoadResStr(id: C4ResStrTableKey::IDS_BTN_DEACTIVATE));
782 btnActivatePlr->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_MSG_NOPARTICIPATE_DESC, args: pSel->GetName()).c_str());
783 }
784 else
785 {
786 btnActivatePlr->SetText(LoadResStr(id: C4ResStrTableKey::IDS_BTN_ACTIVATE));
787 btnActivatePlr->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_MSG_PARTICIPATE_DESC, args: pSel ? pSel->GetName() : "").c_str());
788 }
789 // no item selected?
790 if (!pSel)
791 {
792 pSelectionInfo->ClearText(fDoUpdate: true);
793 pPortraitPict->GetMFacet().Clear();
794 // 2do: disable buttons
795 return;
796 }
797 // info text for selection
798 pSel->SetSelectionInfo(pSelectionInfo);
799 // portrait for selection
800 pPortraitPict->SetFacet(pSel->GetPortrait());
801 pPortraitPict->SetDrawColor(pSel->GetColorDw());
802}
803
804void C4StartupPlrSelDlg::OnItemCheckChange(C4GUI::Element *pCheckBox)
805{
806 switch (eMode)
807 {
808 case PSDM_Player:
809 // update Config.General.Participants
810 UpdateActivatedPlayers();
811 break;
812 case PSDM_Crew:
813 // update affected crew item
814 if (pCheckBox) static_cast<CrewListItem *>(pCheckBox->GetParent())->UpdateClonkEnabled();
815 break;
816 }
817 // update player selection text
818 UpdateSelection();
819}
820
821void C4StartupPlrSelDlg::UpdateActivatedPlayers()
822{
823 assert(eMode == PSDM_Player);
824 // refill Config.General.Participants-list
825 *Config.General.Participants = '\0';
826 for (ListItem *pPlrItem = static_cast<ListItem *>(pPlrListBox->GetFirst()); pPlrItem; pPlrItem = pPlrItem->GetNext())
827 if (pPlrItem->IsActivated())
828 {
829 const char *szAddFilename = pPlrItem->GetFilename().getData();
830 if (SLen(sptr: Config.General.Participants) + 1 + SLen(sptr: szAddFilename) < sizeof(Config.General.Participants))
831 SAddModule(szList: Config.General.Participants, szModule: szAddFilename);
832 else
833 {
834 pPlrItem->SetActivated(false);
835 GetScreen()->ShowMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_ERR_PLAYERSTOOLONG, args: pPlrItem->GetName()).c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_ERR_TITLE), icoIcon: C4GUI::Ico_Error);
836 }
837 }
838}
839
840void C4StartupPlrSelDlg::OnActivateBtn(C4GUI::Control *btn)
841{
842 // toggle activation state of current item
843 // get currently selected player
844 ListItem *pSel = GetSelection();
845 if (!pSel) return;
846 pSel->SetActivated(!pSel->IsActivated());
847 // update stuff
848 OnItemCheckChange(pCheckBox: nullptr);
849}
850
851void C4StartupPlrSelDlg::DoBack()
852{
853 switch (eMode)
854 {
855 case PSDM_Player:
856 {
857 // back 2 main
858 C4Startup::Get()->SwitchDialog(eToDlg: C4Startup::SDID_Back);
859 break;
860 }
861
862 case PSDM_Crew:
863 // back 2 player list
864 SetPlayerMode();
865 break;
866 }
867}
868
869void C4StartupPlrSelDlg::OnNewBtn(C4GUI::Control *btn)
870{
871 if (eMode != PSDM_Player) return;
872 C4GUI::Dialog *pDlg;
873 GetScreen()->ShowRemoveDlg(pDlg: pDlg = new C4StartupPlrPropertiesDlg(nullptr, this));
874 pDlg->SetPos(iXPos: std::min<int32_t>(a: GetBounds().Wdt / 10, b: GetBounds().Wdt - pDlg->GetBounds().Wdt), iYPos: std::min<int32_t>(a: GetBounds().Hgt / 4, b: GetBounds().Hgt - pDlg->GetBounds().Hgt));
875}
876
877bool C4StartupPlrSelDlg::CheckPlayerName(const StdStrBuf &Playername, std::string &filename, const StdStrBuf *pPrevFilename, bool fWarnEmpty)
878{
879 // must not be empty
880 if (!Playername.getLength())
881 {
882 if (fWarnEmpty) C4GUI::Screen::GetScreenS()->ShowMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_ERR_PLRNAME_EMPTY), szCaption: "", icoIcon: C4GUI::Ico_Error);
883 return false;
884 }
885 // generate valid filename
886 filename = TextEncodingConverter.ClonkToSystem(input: Playername.getData());
887 // Slashes in Filenames are no good
888 SReplaceChar(str: filename.data(), fc: '\\', tc: '_');
889 SReplaceChar(str: filename.data(), fc: '/', tc: '_');
890 SReplaceChar(str: filename.data(), fc: ':', tc: '_');
891 SReplaceChar(str: filename.data(), fc: '*', tc: '_');
892 SReplaceChar(str: filename.data(), fc: '?', tc: '_');
893 SReplaceChar(str: filename.data(), fc: '"', tc: '_');
894 SReplaceChar(str: filename.data(), fc: '<', tc: '_');
895 SReplaceChar(str: filename.data(), fc: '>', tc: '_');
896 SReplaceChar(str: filename.data(), fc: '|', tc: '_');
897 if (filename[0] == '.') filename[0] = '_';
898 filename.append(s: ".c4p");
899 std::string path{Config.General.PlayerPath}; // start at local path
900 path.append(str: filename);
901 // validity check: Must not exist yet if renamed
902 if (!pPrevFilename || !ItemIdentical(szFilename1: path.c_str(), szFilename2: pPrevFilename->getData())) if (ItemExists(szItemName: path.c_str()))
903 {
904 C4GUI::Screen::GetScreenS()->ShowMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_ERR_PLRNAME_TAKEN,
905 args: Playername.getData()).c_str(), szCaption: "", icoIcon: C4GUI::Ico_Error);
906 return false;
907 }
908 filename = std::move(path);
909 return true;
910}
911
912void C4StartupPlrSelDlg::OnCrewBtn(C4GUI::Control *btn)
913{
914 // display crew for activated player
915 if (eMode != PSDM_Player) return;
916 PlayerListItem *pSel = static_cast<PlayerListItem *>(GetSelection());
917 if (!pSel) return;
918 SetCrewMode(pSel);
919}
920
921void C4StartupPlrSelDlg::SetPlayerMode()
922{
923 // change view to listing players
924 C4GUI::GUISound(szSound: "DoorClose");
925 StdStrBuf fullName{CurrPlayer.Grp.GetFullName()};
926 std::string lastPlrFilename{fullName.getData(), fullName.getLength()};
927 CurrPlayer.Grp.Close();
928 eMode = PSDM_Player;
929 UpdatePlayerList();
930 SelectItem(filename: lastPlrFilename, fActivate: false);
931 UpdateSelection();
932}
933
934void C4StartupPlrSelDlg::SetCrewMode(PlayerListItem *pSel)
935{
936 // change view to listing crew of a player
937 CurrPlayer.Core = pSel->GetCore();
938 if (!CurrPlayer.Grp.Open(szGroupName: pSel->GetFilename().getData())) return;
939 if (!CurrPlayer.Grp.FindEntry(C4CFN_ObjectInfoFiles))
940 {
941 const std::string crew{std::format(fmt: "{} {}", args: LoadResStrNoAmp(id: C4ResStrTableKey::IDS_CTL_CREW), args: +CurrPlayer.Core.PrefName)};
942 // player has no crew!
943 GetScreen()->ShowMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_ERR_PLRNOCREW,
944 args&: CurrPlayer.Core.PrefName).c_str(),
945 szCaption: crew.c_str(), icoIcon: C4GUI::Ico_Player);
946 return;
947 }
948 C4GUI::GUISound(szSound: "DoorOpen");
949 eMode = PSDM_Crew;
950 UpdatePlayerList();
951 UpdateSelection();
952}
953
954void C4StartupPlrSelDlg::OnDelBtn(C4GUI::Control *btn)
955{
956 // something has changed!
957 AbortRenaming();
958 // delete selected player
959 ListItem *pSel = GetSelection();
960 if (!pSel) return;
961 const std::string warning{pSel->GetDelWarning()};
962 GetScreen()->ShowRemoveDlg(pDlg: new C4GUI::ConfirmationDialog(warning.c_str(), LoadResStr(id: C4ResStrTableKey::IDS_BTN_DELETE),
963 new C4GUI::CallbackHandlerExPar<C4StartupPlrSelDlg, ListItem *>(this, &C4StartupPlrSelDlg::OnDelBtnConfirm, pSel), C4GUI::MessageDialog::btnYesNo));
964}
965
966void C4StartupPlrSelDlg::OnDelBtnConfirm(ListItem *pSel)
967{
968 switch (eMode)
969 {
970 case PSDM_Player:
971 if (!C4Group_DeleteItem(szItem: pSel->GetFilename().getData()))
972 {
973 StdStrBuf sMsg; sMsg.Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_FAIL_DELETE));
974 GetScreen()->ShowMessage(szMessage: sMsg.getData(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_DLG_CLEAR), icoIcon: C4GUI::Ico_Error);
975 }
976 break;
977
978 case PSDM_Crew:
979 if (!CurrPlayer.Grp.Delete(szFiles: pSel->GetFilename().getData()))
980 {
981 StdStrBuf sMsg; sMsg.Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_FAIL_DELETE));
982 GetScreen()->ShowMessage(szMessage: sMsg.getData(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_DLG_CLEAR), icoIcon: C4GUI::Ico_Error);
983 }
984 break;
985 }
986 // update buttons 'n stuff
987 UpdatePlayerList();
988}
989
990void C4StartupPlrSelDlg::SelectItem(const std::string &filename, bool fActivate)
991{
992 // find item
993 for (ListItem *pPlrItem = static_cast<ListItem *>(pPlrListBox->GetFirst()); pPlrItem; pPlrItem = pPlrItem->GetNext())
994 if (ItemIdentical(szFilename1: pPlrItem->GetFilename().getData(), szFilename2: filename.c_str()))
995 {
996 // select it
997 pPlrListBox->SelectEntry(pNewSel: pPlrItem, fByUser: false);
998 // activate it
999 if (fActivate)
1000 {
1001 pPlrItem->SetActivated(true);
1002 // player activation updates
1003 OnItemCheckChange(pCheckBox: nullptr);
1004 }
1005 // max one
1006 return;
1007 }
1008}
1009
1010void C4StartupPlrSelDlg::OnPropertyBtn(C4GUI::Control *btn)
1011{
1012 // something has changed!
1013 AbortRenaming();
1014 switch (eMode)
1015 {
1016 case PSDM_Player:
1017 {
1018 // show property dialog for selected player
1019 PlayerListItem *pSel = static_cast<PlayerListItem *>(GetSelection());
1020 if (!pSel) return;
1021 C4GUI::Dialog *pDlg;
1022 GetScreen()->ShowRemoveDlg(pDlg: pDlg = new C4StartupPlrPropertiesDlg(pSel, this));
1023 pDlg->SetPos(iXPos: std::min<int32_t>(a: GetBounds().Wdt / 10, b: GetBounds().Wdt - pDlg->GetBounds().Wdt),
1024 iYPos: (GetBounds().Hgt - pDlg->GetBounds().Hgt) / 2);
1025 }
1026 break;
1027
1028 case PSDM_Crew:
1029 // rename crew
1030 CrewListItem *pSel = static_cast<CrewListItem *>(GetSelection());
1031 if (!pSel) return;
1032 pSel->CrewRename();
1033 break;
1034 }
1035}
1036
1037// Crew sorting
1038
1039struct C4StartupPlrSelDlg_CrewSortDataEntry
1040{
1041 int32_t iMaxExp;
1042 C4ID idType;
1043
1044 C4StartupPlrSelDlg_CrewSortDataEntry(int32_t iMaxExp, C4ID idType) : iMaxExp(iMaxExp), idType(idType) {}
1045};
1046
1047class C4StartupPlrSelDlg_CrewSortDataMatchType
1048{
1049 C4ID idType;
1050
1051public:
1052 C4StartupPlrSelDlg_CrewSortDataMatchType(C4ID idType) : idType(idType) {}
1053 bool operator()(C4StartupPlrSelDlg_CrewSortDataEntry Check) { return Check.idType == idType; }
1054};
1055
1056typedef std::vector<C4StartupPlrSelDlg_CrewSortDataEntry> C4StartupPlrSelDlg_CrewSortData;
1057
1058int32_t C4StartupPlrSelDlg::CrewSortFunc(const C4GUI::Element *pEl1, const C4GUI::Element *pEl2, void *par)
1059{
1060 const CrewListItem *pItem1 = static_cast<const CrewListItem *>(pEl1);
1061 const CrewListItem *pItem2 = static_cast<const CrewListItem *>(pEl2);
1062 C4StartupPlrSelDlg_CrewSortData &rSortData = *static_cast<C4StartupPlrSelDlg_CrewSortData *>(par);
1063 C4StartupPlrSelDlg_CrewSortData::iterator i = std::find_if(first: rSortData.begin(), last: rSortData.end(), pred: C4StartupPlrSelDlg_CrewSortDataMatchType(pItem1->GetCore().id)),
1064 j = std::find_if(first: rSortData.begin(), last: rSortData.end(), pred: C4StartupPlrSelDlg_CrewSortDataMatchType(pItem2->GetCore().id));
1065 // primary sort: By Clonk type, where high exp Clonk types are sorted atop low exp Clonk types
1066 if (i != j)
1067 {
1068 if (i == rSortData.end()) return -1; else if (j == rSortData.end()) return +1; // can't really happen
1069 return (*i).iMaxExp - (*j).iMaxExp;
1070 }
1071 // secondary: By experience
1072 return pItem1->GetCore().Experience - pItem2->GetCore().Experience;
1073}
1074
1075void C4StartupPlrSelDlg::ResortCrew()
1076{
1077 assert(eMode == PSDM_Crew);
1078 // create a list of Clonk types and their respective maximum experience
1079 C4StartupPlrSelDlg_CrewSortData SortData;
1080 for (CrewListItem *pCrewItem = static_cast<CrewListItem *>(pPlrListBox->GetFirst()); pCrewItem; pCrewItem = pCrewItem->GetNext())
1081 {
1082 C4StartupPlrSelDlg_CrewSortData::iterator i = std::find_if(first: SortData.begin(), last: SortData.end(), pred: C4StartupPlrSelDlg_CrewSortDataMatchType(pCrewItem->GetCore().id));
1083 if (i == SortData.end())
1084 SortData.push_back(x: C4StartupPlrSelDlg_CrewSortDataEntry(pCrewItem->GetCore().Experience, pCrewItem->GetCore().id));
1085 else
1086 (*i).iMaxExp = std::max<int32_t>(a: (*i).iMaxExp, b: pCrewItem->GetCore().Experience);
1087 }
1088 pPlrListBox->SortElements(SortFunc: &CrewSortFunc, par: &SortData);
1089}
1090
1091// Player property dlg
1092
1093C4StartupPlrPropertiesDlg::C4StartupPlrPropertiesDlg(C4StartupPlrSelDlg::PlayerListItem *pForPlayer, C4StartupPlrSelDlg *pParentDlg)
1094 : Dialog(C4Startup::Get()->Graphics.fctPlrPropBG.Wdt, C4Startup::Get()->Graphics.fctPlrPropBG.Hgt, "", false), pForPlayer(pForPlayer), pMainDlg(pParentDlg),
1095 fClearPicture(false), fClearBigIcon(false)
1096{
1097 if (pForPlayer)
1098 {
1099 // edit existing player
1100 C4P = pForPlayer->GetCore();
1101 }
1102 else
1103 {
1104 // create new player: Use default C4P values, with a few exceptions
1105 // FIXME: Use Player, not Clonkranks
1106 C4P.Default(pRanks: &Game.Rank);
1107 // Set name, color, comment
1108 SCopy(szSource: LoadResStr(id: C4ResStrTableKey::IDS_PLR_NEWCOMMENT), sTarget: C4P.Comment, iMaxL: C4MaxComment);
1109 C4P.PrefColor = SafeRandom(range: 8);
1110 C4P.PrefColorDw = C4P.GetPrefColorValue(iPrefColor: C4P.PrefColor);
1111 C4P.PrefControlStyle = 1;
1112 C4P.PrefAutoContextMenu = 1;
1113 C4P.PrefControl = C4P_Control_Keyboard1;
1114 }
1115 const int32_t BetweenElementDist = 2;
1116 // use black fonts here
1117 CStdFont *pUseFont = &C4Startup::Get()->Graphics.BookFont;
1118 CStdFont *pSmallFont = &C4Startup::Get()->Graphics.BookSmallFont;
1119 // get positions
1120 UpdateSize();
1121 C4GUI::ComponentAligner caMain(GetClientRect(), 0, 1, true);
1122 C4GUI::ComponentAligner caButtonArea(caMain.GetFromBottom(C4GUI_ButtonAreaHgt), 0, 0);
1123 // dlg title
1124 const char *szTitle;
1125 if (pForPlayer)
1126 {
1127 szTitle = LoadResStr(id: C4ResStrTableKey::IDS_DLG_PLAYER2);
1128 }
1129 else
1130 {
1131 szTitle = LoadResStr(id: C4ResStrTableKey::IDS_PLR_NEWPLAYER);
1132 }
1133 C4GUI::Label *pLbl = new C4GUI::Label(szTitle, caMain.GetFromTop(iHgt: pUseFont->GetLineHeight()), ALeft, C4StartupFontClr, pUseFont, false);
1134 AddElement(pChild: pLbl);
1135 caMain.ExpandTop(iByHgt: -BetweenElementDist);
1136 // place name label
1137 AddElement(pChild: new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_CTL_NAME2), caMain.GetFromTop(iHgt: pSmallFont->GetLineHeight()), ALeft, C4StartupFontClr, pSmallFont, false));
1138 // place name edit
1139 pNameEdit = new C4GUI::Edit(caMain.GetFromTop(iHgt: C4GUI::Edit::GetCustomEditHeight(pUseFont)));
1140 pNameEdit->SetFont(pUseFont);
1141 pNameEdit->SetColors(dwNewBGClr: C4StartupEditBGColor, dwNewFontClr: C4StartupFontClr, dwNewBorderColor: C4StartupEditBorderColor);
1142 pNameEdit->InsertText(text: C4P.PrefName, fUser: false);
1143 pNameEdit->SetMaxText(C4MaxName);
1144 AddElement(pChild: pNameEdit);
1145 SetFocus(pCtrl: pNameEdit, fByMouse: false);
1146 caMain.ExpandTop(iByHgt: -BetweenElementDist);
1147 // place color label
1148 AddElement(pChild: new C4GUI::Label(std::format(fmt: "{}:", args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_COLOR)).c_str(), caMain.GetFromTop(iHgt: pSmallFont->GetLineHeight()), ALeft, C4StartupFontClr, pSmallFont, false));
1149 // place color controls
1150 C4GUI::ComponentAligner caColorArea(caMain.GetFromTop(iHgt: C4GUI::ArrowButton::GetDefaultHeight()), 2, 0);
1151 caColorArea.ExpandLeft(iByWdt: 2);
1152 C4GUI::Button *pBtn; const char *szTip;
1153 szTip = LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PLAYERCOLORS);
1154 AddElement(pChild: pBtn = new C4GUI::CallbackButton<C4StartupPlrPropertiesDlg, C4GUI::ArrowButton>(C4GUI::ArrowButton::Left, caColorArea.GetFromLeft(iWdt: C4GUI::ArrowButton::GetDefaultWidth()), &C4StartupPlrPropertiesDlg::OnClrChangeLeft));
1155 pBtn->SetToolTip(szTip);
1156 C4Facet &rfctClrPreviewPic = Game.GraphicsResource.fctFlagClr;
1157 pClrPreview = new C4GUI::Picture(caColorArea.GetFromLeft(iWdt: rfctClrPreviewPic.GetWidthByHeight(iHeight: caColorArea.GetHeight())), true);
1158 pClrPreview->SetFacet(rfctClrPreviewPic);
1159 AddElement(pChild: pClrPreview);
1160 AddElement(pChild: pBtn = new C4GUI::CallbackButton<C4StartupPlrPropertiesDlg, C4GUI::ArrowButton>(C4GUI::ArrowButton::Right, caColorArea.GetFromLeft(iWdt: C4GUI::ArrowButton::GetDefaultWidth()), &C4StartupPlrPropertiesDlg::OnClrChangeRight));
1161 pBtn->SetToolTip(szTip);
1162 szTip = LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PLAYERCOLORSTGB);
1163 int32_t iSliderYDiff = (caColorArea.GetHeight() - 3 * C4GUI_ScrollBarHgt) / 2;
1164 pClrSliderR = new C4GUI::ScrollBar(caColorArea.GetFromTop(C4GUI_ScrollBarHgt), true, new C4GUI::ParCallbackHandler<C4StartupPlrPropertiesDlg, int32_t>(this, &C4StartupPlrPropertiesDlg::OnClrSliderRChange));
1165 pClrSliderR->SetDecoration(pToGfx: &C4Startup::Get()->Graphics.sfctBookScrollR, fAutoHide: false);
1166 pClrSliderR->SetToolTip(szTip);
1167 caColorArea.ExpandTop(iByHgt: -iSliderYDiff);
1168 pClrSliderG = new C4GUI::ScrollBar(caColorArea.GetFromTop(C4GUI_ScrollBarHgt), true, new C4GUI::ParCallbackHandler<C4StartupPlrPropertiesDlg, int32_t>(this, &C4StartupPlrPropertiesDlg::OnClrSliderGChange));
1169 pClrSliderG->SetDecoration(pToGfx: &C4Startup::Get()->Graphics.sfctBookScrollG, fAutoHide: false);
1170 pClrSliderG->SetToolTip(szTip);
1171 caColorArea.ExpandTop(iByHgt: -iSliderYDiff);
1172 pClrSliderB = new C4GUI::ScrollBar(caColorArea.GetFromTop(C4GUI_ScrollBarHgt), true, new C4GUI::ParCallbackHandler<C4StartupPlrPropertiesDlg, int32_t>(this, &C4StartupPlrPropertiesDlg::OnClrSliderBChange));
1173 pClrSliderB->SetDecoration(pToGfx: &C4Startup::Get()->Graphics.sfctBookScrollB, fAutoHide: false);
1174 pClrSliderB->SetToolTip(szTip);
1175 AddElement(pChild: pClrSliderR);
1176 AddElement(pChild: pClrSliderG);
1177 AddElement(pChild: pClrSliderB);
1178 if (!C4P.PrefColorDw) C4P.PrefColorDw = 0xff;
1179 caMain.ExpandTop(iByHgt: -BetweenElementDist);
1180 // place control and picture label
1181 int32_t iControlPicSize = C4GUI::ArrowButton::GetDefaultHeight();
1182 C4GUI::ComponentAligner caControlArea(caMain.GetFromTop(iHgt: iControlPicSize + pSmallFont->GetLineHeight() + BetweenElementDist), 0, 0, false);
1183 C4GUI::ComponentAligner caPictureArea(caControlArea.GetFromRight(iWdt: iControlPicSize), 0, 0, false);
1184 AddElement(pChild: new C4GUI::Label(std::format(fmt: "{}:", args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_CONTROL)).c_str(), caControlArea.GetFromTop(iHgt: pSmallFont->GetLineHeight()), ALeft, C4StartupFontClr, pSmallFont, false));
1185 AddElement(pChild: new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_CTL_PICTURE), caPictureArea.GetFromTop(iHgt: pSmallFont->GetLineHeight()), ACenter, C4StartupFontClr, pSmallFont, false));
1186 caControlArea.ExpandTop(iByHgt: -BetweenElementDist); caPictureArea.ExpandTop(iByHgt: -BetweenElementDist);
1187 // place control controls
1188 C4GUI::ComponentAligner caControl(caControlArea.GetFromTop(iHgt: iControlPicSize), 2, 0);
1189 szTip = LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PLAYERCONTROL);
1190 AddElement(pChild: pBtn = new C4GUI::CallbackButton<C4StartupPlrPropertiesDlg, C4GUI::ArrowButton>(C4GUI::ArrowButton::Left, caControl.GetFromLeft(iWdt: C4GUI::ArrowButton::GetDefaultWidth()), &C4StartupPlrPropertiesDlg::OnCtrlChangeLeft));
1191 pBtn->SetToolTip(szTip);
1192 C4Facet &rfctCtrlPic = Game.GraphicsResource.fctKeyboard; // UpdatePlayerControl() will alternatively set fctGamepad
1193 AddElement(pChild: pCtrlImg = new C4GUI::Picture(caControl.GetFromLeft(iWdt: rfctCtrlPic.GetWidthByHeight(iHeight: caControl.GetHeight())), true));
1194 pCtrlImg->SetToolTip(szTip);
1195 AddElement(pChild: pBtn = new C4GUI::CallbackButton<C4StartupPlrPropertiesDlg, C4GUI::ArrowButton>(C4GUI::ArrowButton::Right, caControl.GetFromLeft(iWdt: C4GUI::ArrowButton::GetDefaultWidth()), &C4StartupPlrPropertiesDlg::OnCtrlChangeRight));
1196 pBtn->SetToolTip(szTip);
1197 caControl.ExpandLeft(iByWdt: -10);
1198 AddElement(pChild: pMouseBtn = new C4GUI::CallbackButton<C4StartupPlrPropertiesDlg, C4GUI::IconButton>(C4GUI::Ico_MouseOff, caControl.GetFromLeft(iWdt: caControl.GetHeight()), 'M' /* 2do */, &C4StartupPlrPropertiesDlg::OnCtrlChangeMouse));
1199 pMouseBtn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PLAYERCONTROLMOUSE));
1200 C4P.PrefControl = BoundBy<int32_t>(bval: C4P.PrefControl, lbound: 0, rbound: C4MaxControlSet - 1);
1201 UpdatePlayerControl();
1202 // place picture button
1203 AddElement(pChild: pPictureBtn = new C4GUI::CallbackButton<C4StartupPlrPropertiesDlg, C4GUI::IconButton>(C4GUI::Ico_Player, caPictureArea.GetAll(), 'P' /* 2do */, &C4StartupPlrPropertiesDlg::OnPictureBtn));
1204 pPictureBtn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_SELECTAPICTUREANDORLOBBYI));
1205 UpdateBigIcon();
1206 UpdatePlayerColor(fUpdateSliders: true);
1207 caMain.ExpandTop(iByHgt: -BetweenElementDist);
1208 // place AutoStopControl label
1209 AddElement(pChild: new C4GUI::Label(std::format(fmt: "{}:", args: LoadResStr(id: C4ResStrTableKey::IDS_DLG_MOVEMENT)).c_str(), caMain.GetFromTop(iHgt: pSmallFont->GetLineHeight()), ALeft, C4StartupFontClr, pSmallFont, false));
1210 // place AutoStopControl controls
1211 C4Facet &rfctMovementIcons = C4Startup::Get()->Graphics.fctPlrCtrlType;
1212 C4GUI::ComponentAligner caMovement(caMain.GetFromTop(iHgt: rfctMovementIcons.Hgt), 5, 0);
1213 C4Rect rcBtn = caMovement.GetFromLeft(iWdt: rfctMovementIcons.GetWidthByHeight(iHeight: caMovement.GetHeight()));
1214 AddElement(pChild: pLbl = new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_DLG_JUMPANDRUN), rcBtn.x + rcBtn.Wdt / 2, rcBtn.y + rcBtn.Hgt - 6, ACenter, C4StartupFontClr, pSmallFont, false));
1215 szTip = LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_JUMPANDRUN);
1216 pLbl->SetToolTip(szTip);
1217 AddElement(pChild: pJumpNRunBtn = new C4GUI::CallbackButton<C4StartupPlrPropertiesDlg, C4GUI::IconButton>(C4GUI::Ico_None, rcBtn, 'J' /* 2do */, &C4StartupPlrPropertiesDlg::OnMovementBtn));
1218 pJumpNRunBtn->SetToolTip(szTip);
1219 rcBtn = caMovement.GetFromRight(iWdt: rfctMovementIcons.GetWidthByHeight(iHeight: caMovement.GetHeight()));
1220 AddElement(pChild: pLbl = new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_DLG_CLASSIC), rcBtn.x + rcBtn.Wdt / 2, rcBtn.y + rcBtn.Hgt - 6, ACenter, C4StartupFontClr, pSmallFont, false));
1221 szTip = LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_CLASSIC);
1222 pLbl->SetToolTip(szTip);
1223 AddElement(pChild: pClassicBtn = new C4GUI::CallbackButton<C4StartupPlrPropertiesDlg, C4GUI::IconButton>(C4GUI::Ico_None, rcBtn, 'C' /* 2do */, &C4StartupPlrPropertiesDlg::OnMovementBtn));
1224 pClassicBtn->SetToolTip(szTip);
1225 UpdatePlayerMovement();
1226 // place buttons
1227 // OK
1228 C4GUI::Button *pBtnOK = C4GUI::newOKIconButton(bounds: C4Rect(147 - GetMarginLeft(), 295 + 35 - GetMarginTop(), 54, 33), icon: C4GUI::Ico_None);
1229 AddElement(pChild: pBtnOK);
1230 // Cancel
1231 C4GUI::Button *pBtnAbort = C4GUI::newCancelIconButton(bounds: C4Rect(317 - GetMarginLeft(), 16 - GetMarginTop(), 21, 21), icon: C4GUI::Ico_None);
1232 AddElement(pChild: pBtnAbort);
1233 // New player
1234 if (!pForPlayer)
1235 {
1236 // Set initial portrait and bigicon
1237 C4Group hGroup;
1238 const std::string portrait{std::format(fmt: "Portrait{}.png", args: 1 + Random(iRange: 5))};
1239 if (hGroup.Open(szGroupName: Config.AtExePath(C4CFN_Graphics)))
1240 {
1241 hGroup.Extract(szFiles: portrait.c_str(), szExtractTo: Config.AtTempPath(szFilename: "Portrait.png"));
1242 hGroup.Close();
1243 SetNewPicture(szFromFilename: Config.AtTempPath(szFilename: "Portrait.png"), fSetPicture: true, fSetBigIcon: true);
1244 EraseItem(szItemName: Config.AtTempPath(szFilename: "Portrait.png"));
1245 }
1246 }
1247 // when called from player selection screen: input dlg always closed in the end
1248 // otherwise, modal proc will delete
1249 if (pMainDlg) SetDelOnClose();
1250}
1251
1252void C4StartupPlrPropertiesDlg::DrawElement(C4FacetEx &cgo)
1253{
1254 C4Startup::Get()->Graphics.fctPlrPropBG.Draw(sfcTarget: cgo.Surface, iX: rcBounds.x + cgo.TargetX, iY: rcBounds.y + cgo.TargetY);
1255}
1256
1257bool IsColorConflict(uint32_t dwClr1, uint32_t dwClr2);
1258
1259void C4StartupPlrPropertiesDlg::UpdatePlayerColor(bool fUpdateSliders)
1260{
1261 if (!C4P.PrefColorDw) C4P.PrefColorDw = 1; // no black! Would turn to blue in some instances
1262 pClrPreview->SetDrawColor(C4P.PrefColorDw);
1263 pPictureBtn->SetColor(C4P.PrefColorDw);
1264 if (fUpdateSliders)
1265 {
1266 pClrSliderR->SetScrollPos((C4P.PrefColorDw >> 16) & 0xff);
1267 pClrSliderG->SetScrollPos((C4P.PrefColorDw >> 8) & 0xff);
1268 pClrSliderB->SetScrollPos(C4P.PrefColorDw & 0xff);
1269 }
1270}
1271
1272void C4StartupPlrPropertiesDlg::OnClrChangeLeft(C4GUI::Control *pBtn)
1273{
1274 // previous standard color in list
1275 C4P.PrefColor = C4P.PrefColor ? C4P.PrefColor - 1 : 11;
1276 C4P.PrefColorDw = C4PlayerInfoCore::GetPrefColorValue(iPrefColor: C4P.PrefColor);
1277 UpdatePlayerColor(fUpdateSliders: true);
1278}
1279
1280void C4StartupPlrPropertiesDlg::OnClrChangeRight(C4GUI::Control *pBtn)
1281{
1282 // next standard color in list
1283 C4P.PrefColor = (C4P.PrefColor + 1) % 12;
1284 C4P.PrefColorDw = C4PlayerInfoCore::GetPrefColorValue(iPrefColor: C4P.PrefColor);
1285 UpdatePlayerColor(fUpdateSliders: true);
1286}
1287
1288void C4StartupPlrPropertiesDlg::OnClrSliderRChange(int32_t iNewVal)
1289{
1290 // update red component of color
1291 C4P.PrefColorDw = (C4P.PrefColorDw & 0xffff) + (iNewVal << 16);
1292 UpdatePlayerColor(fUpdateSliders: false);
1293}
1294
1295void C4StartupPlrPropertiesDlg::OnClrSliderGChange(int32_t iNewVal)
1296{
1297 // update green component of color
1298 C4P.PrefColorDw = (C4P.PrefColorDw & 0xff00ff) + (iNewVal << 8);
1299 UpdatePlayerColor(fUpdateSliders: false);
1300}
1301
1302void C4StartupPlrPropertiesDlg::OnClrSliderBChange(int32_t iNewVal)
1303{
1304 // update blue component of color
1305 C4P.PrefColorDw = (C4P.PrefColorDw & 0xffff00) + iNewVal;
1306 UpdatePlayerColor(fUpdateSliders: false);
1307}
1308
1309void C4StartupPlrPropertiesDlg::UpdatePlayerControl()
1310{
1311 // update keyboard image of selected control
1312 C4Facet &rfctCtrlPic = (C4P.PrefControl < C4P_Control_GamePad1) ? Game.GraphicsResource.fctKeyboard : Game.GraphicsResource.fctGamepad;
1313 pCtrlImg->SetFacet(rfctCtrlPic);
1314 pCtrlImg->GetMFacet().X += rfctCtrlPic.Wdt * (C4P.PrefControl - ((C4P.PrefControl < C4P_Control_GamePad1) ? 0 : C4P_Control_GamePad1));
1315 // update mouse image
1316 pMouseBtn->SetIcon(C4P.PrefMouse ? C4GUI::Ico_MouseOn : C4GUI::Ico_MouseOff);
1317}
1318
1319void C4StartupPlrPropertiesDlg::OnCtrlChangeLeft(C4GUI::Control *pBtn)
1320{
1321 // previous control set in list
1322 C4P.PrefControl = C4P.PrefControl ? C4P.PrefControl - 1 : C4MaxControlSet - 1;
1323 UpdatePlayerControl();
1324}
1325
1326void C4StartupPlrPropertiesDlg::OnCtrlChangeRight(C4GUI::Control *pBtn)
1327{
1328 // next control set in list
1329 C4P.PrefControl = (C4P.PrefControl + 1) % C4MaxControlSet;
1330 UpdatePlayerControl();
1331}
1332
1333void C4StartupPlrPropertiesDlg::OnCtrlChangeMouse(C4GUI::Control *pBtn)
1334{
1335 // toggle mouse usage
1336 C4P.PrefMouse = !C4P.PrefMouse;
1337 UpdatePlayerControl();
1338}
1339
1340void C4StartupPlrPropertiesDlg::UpdatePlayerMovement()
1341{
1342 // hightlight to control tyope that is selected
1343 pJumpNRunBtn->SetFacet(rCpy: C4Startup::Get()->Graphics.fctPlrCtrlType.GetPhase(iPhaseX: C4P.PrefControlStyle ? 1 : 0, iPhaseY: 1));
1344 pClassicBtn->SetFacet(rCpy: C4Startup::Get()->Graphics.fctPlrCtrlType.GetPhase(iPhaseX: C4P.PrefControlStyle ? 0 : 1, iPhaseY: 0));
1345}
1346
1347void C4StartupPlrPropertiesDlg::OnMovementBtn(C4GUI::Control *pBtn)
1348{
1349 // Set new control style
1350 C4P.PrefControlStyle = (pBtn == pJumpNRunBtn);
1351 // Adjust pref for AutoContextMenus along with control style
1352 C4P.PrefAutoContextMenu = C4P.PrefControlStyle;
1353 // Update dialog
1354 UpdatePlayerMovement();
1355}
1356
1357void C4StartupPlrPropertiesDlg::UserClose(bool fOK)
1358{
1359 // check name validity
1360 if (fOK)
1361 {
1362 StdStrBuf PlrName(pNameEdit->GetText(), false);
1363 std::string filename;
1364 if (!C4StartupPlrSelDlg::CheckPlayerName(Playername: PlrName, filename, pPrevFilename: pForPlayer ? &pForPlayer->GetFilename() : nullptr, fWarnEmpty: true)) return;
1365 }
1366 Close(fOK);
1367}
1368
1369void C4StartupPlrPropertiesDlg::OnClosed(bool fOK)
1370{
1371 if (fOK)
1372 {
1373 // store selected data if desired
1374 StdStrBuf PlrName(pNameEdit->GetText(), false);
1375 std::string filename;
1376 if (C4StartupPlrSelDlg::CheckPlayerName(Playername: PlrName, filename, pPrevFilename: pForPlayer ? &pForPlayer->GetFilename() : nullptr, fWarnEmpty: true))
1377 {
1378 SCopy(szSource: PlrName.getData(), sTarget: C4P.PrefName, iMaxL: C4MaxName);
1379 C4Group PlrGroup;
1380 bool fSucc = false;
1381 // existent player: update file
1382 if (pForPlayer)
1383 {
1384 if (!pForPlayer->MoveFilename(szToFilename: filename.c_str()))
1385 GetScreen()->ShowMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_FAIL_RENAME), szCaption: "", icoIcon: C4GUI::Ico_Error);
1386 // update picture/bigicon
1387 if (fClearPicture || fClearBigIcon || fctNewPicture.Surface || fctNewBigIcon.Surface)
1388 {
1389 C4Group PlrGroup;
1390 if (PlrGroup.Open(szGroupName: filename.c_str()))
1391 {
1392 if (fClearPicture || fctNewPicture.Surface) PlrGroup.Delete(C4CFN_Portrait);
1393 if (fClearBigIcon || fctNewBigIcon.Surface) PlrGroup.Delete(C4CFN_BigIcon);
1394 if (fctNewPicture.Surface) fctNewPicture.GetFace().SavePNG(hGroup&: PlrGroup, C4CFN_Portrait);
1395 if (fctNewBigIcon.Surface) fctNewBigIcon.GetFace().SavePNG(hGroup&: PlrGroup, C4CFN_BigIcon);
1396 if (PlrGroup.Close()) fSucc = true;
1397 if (fClearBigIcon || fctNewBigIcon.Surface) pForPlayer->GrabCustomIcon(fctGrabFrom&: fctNewBigIcon);
1398 if (fClearPicture || fctNewPicture.Surface) pForPlayer->GrabPortrait(pFromFacet: &fctNewPicture);
1399 }
1400 }
1401 else
1402 {
1403 fSucc = true;
1404 }
1405 pForPlayer->UpdateCore(NewCore&: C4P);
1406 // player may have been activated: Make sure any new filename is reflected in participants list
1407 if (pMainDlg) pMainDlg->UpdateActivatedPlayers();
1408 }
1409 else
1410 {
1411 // NewPlayer: Open new player group
1412 if (PlrGroup.Open(szGroupName: filename.c_str(), fCreate: true))
1413 {
1414 // Do not overwrite (should have been caught earlier anyway)
1415 if (PlrGroup.FindEntry(C4CFN_PlayerInfoCore)) return;
1416 // Save info core
1417 C4P.Save(hGroup&: PlrGroup);
1418 // Add portrait
1419 if (fctNewPicture.Surface)
1420 {
1421 fctNewPicture.GetFace().SavePNG(hGroup&: PlrGroup, C4CFN_Portrait);
1422 }
1423 else if (!fClearPicture)
1424 {
1425 // default picture
1426 char *pBytes; size_t iSize;
1427 if (GetPortrait(ppBytes: &pBytes, ipSize: &iSize))
1428 {
1429 PlrGroup.Add(C4CFN_Portrait, pBuffer: pBytes, iSize, fChild: false, fHoldBuffer: true);
1430 }
1431 }
1432 // Add BigIcon
1433 if (fctNewBigIcon.Surface)
1434 {
1435 fctNewBigIcon.GetFace().SavePNG(hGroup&: PlrGroup, C4CFN_BigIcon);
1436 }
1437 // Close group
1438 if (PlrGroup.Close()) fSucc = true;
1439 // update activate button text
1440 if (pMainDlg)
1441 {
1442 pMainDlg->UpdatePlayerList();
1443 pMainDlg->SelectItem(filename, fActivate: true);
1444 }
1445 else
1446 {
1447 // no main player selection dialog: This means that this dlg was shown as a creation dialog from the main startup dlg
1448 // Just set the newly created player as current selection
1449 SCopy(szSource: Config.AtExeRelativePath(szFilename: filename.c_str()), sTarget: Config.General.Participants, iMaxL: sizeof(Config.General.Participants));
1450 }
1451 }
1452 }
1453 if (!fSucc) GetScreen()->ShowErrorMessage(szMessage: PlrGroup.GetError());
1454 }
1455 }
1456 // Make the dialog go away
1457 Dialog::OnClosed(fOK);
1458}
1459
1460bool C4StartupPlrPropertiesDlg::SetNewPicture(C4Surface &srcSfc, C4FacetExSurface *trgFct, int32_t iMaxSize, bool fColorize)
1461{
1462 if (fColorize)
1463 {
1464 C4Surface srcSfcClr;
1465 if (!srcSfcClr.CreateColorByOwner(pBySurface: &srcSfc)) return false;
1466 return trgFct->CopyFromSfcMaxSize(srcSfc&: srcSfcClr, iMaxSize, dwColor: C4P.PrefColorDw);
1467 }
1468 else
1469 {
1470 return trgFct->CopyFromSfcMaxSize(srcSfc, iMaxSize);
1471 }
1472}
1473
1474void C4StartupPlrPropertiesDlg::SetNewPicture(const char *szFromFilename, bool fSetPicture, bool fSetBigIcon)
1475{
1476 if (!szFromFilename)
1477 {
1478 // If szFromFilename==nullptr, clear picture/bigicon
1479 if (fSetPicture) { fClearPicture = true; fctNewPicture.Clear(); }
1480 if (fSetBigIcon) { fClearBigIcon = true; fctNewBigIcon.Clear(); }
1481 }
1482 else if (fSetPicture || fSetBigIcon)
1483 {
1484 // else set new picture/bigicon by loading and scaling if necessary.
1485 C4Surface sfcNewPic;
1486 C4Group SrcGrp;
1487 StdStrBuf sParentPath;
1488 GetParentPath(szFilename: szFromFilename, outBuf: &sParentPath);
1489 bool fSucc = false;
1490 if (SrcGrp.Open(szGroupName: sParentPath.getData()))
1491 {
1492 if (sfcNewPic.Load(hGroup&: SrcGrp, szFilename: GetFilename(path: szFromFilename)))
1493 {
1494 fSucc = true;
1495 if (fSetPicture) if (!SetNewPicture(srcSfc&: sfcNewPic, trgFct: &fctNewPicture, iMaxSize: C4MaxPictureSize, fColorize: false)) fSucc = false;
1496 if (fSetBigIcon) if (!SetNewPicture(srcSfc&: sfcNewPic, trgFct: &fctNewBigIcon, iMaxSize: C4MaxBigIconSize, fColorize: true)) fSucc = false;
1497 }
1498 }
1499 if (!fSucc)
1500 {
1501 // error!
1502 GetScreen()->ShowErrorMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_PRC_NOGFXFILE, args&: szFromFilename, args: SrcGrp.GetError()).c_str());
1503 }
1504 }
1505 // update icon
1506 if (fSetBigIcon) UpdateBigIcon();
1507}
1508
1509void C4StartupPlrPropertiesDlg::OnPictureBtn(C4GUI::Control *pBtn)
1510{
1511 std::string newPic;
1512 bool setPicture = true;
1513 bool setBigIcon = true;
1514 if (C4PortraitSelDlg::SelectPortrait(pOnScreen: GetScreen(), selection&: newPic, pfSetPicture: &setPicture, pfSetBigIcon: &setBigIcon))
1515 {
1516 SetNewPicture(szFromFilename: newPic.c_str(), fSetPicture: setPicture, fSetBigIcon: setBigIcon);
1517 }
1518}
1519
1520void C4StartupPlrPropertiesDlg::UpdateBigIcon()
1521{
1522 // new icon?
1523 bool fHasIcon = false;
1524 if (fctNewBigIcon.Surface)
1525 {
1526 pPictureBtn->SetFacet(rCpy: fctNewBigIcon);
1527 fHasIcon = true;
1528 }
1529 // old icon in existing player?
1530 else if (!fClearBigIcon && pForPlayer)
1531 {
1532 C4Group PlrGroup;
1533 if (PlrGroup.Open(szGroupName: pForPlayer->GetFilename().getData()))
1534 {
1535 if (PlrGroup.FindEntry(C4CFN_BigIcon))
1536 {
1537 if (fctOldBigIcon.Load(hGroup&: PlrGroup, C4CFN_BigIcon))
1538 {
1539 pPictureBtn->SetFacet(rCpy: fctOldBigIcon);
1540 fHasIcon = true;
1541 }
1542 }
1543 }
1544 }
1545 // no icon: Set default
1546 if (!fHasIcon)
1547 {
1548 pPictureBtn->SetFacet(rCpy: Game.GraphicsResource.fctPlayerClr);
1549 }
1550}
1551