1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2006, Sven2
6 * Copyright (c) 2013, The OpenClonk Team and contributors
7 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
8 *
9 * Distributed under the terms of the ISC license; see accompanying file
10 * "COPYING" for details.
11 *
12 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
13 * See accompanying file "TRADEMARK" for details.
14 *
15 * To redistribute this file separately, substitute the full license texts
16 * for the above references.
17 */
18
19// Startup screen for non-parameterized engine start: Network game selection dialog
20
21#include <C4Include.h>
22#include <C4StartupNetDlg.h>
23
24#include <C4StartupScenSelDlg.h>
25#include <C4StartupMainDlg.h>
26#include <C4Game.h>
27#include <C4Log.h>
28#include "C4ChatDlg.h"
29#include "C4GuiEdit.h"
30#include "C4GuiListBox.h"
31#include "C4GuiResource.h"
32#include "C4GuiTabular.h"
33
34#include <cassert>
35#include <format>
36
37// C4StartupNetListEntry
38
39C4StartupNetListEntry::C4StartupNetListEntry(C4GUI::ListBox *pForListBox, C4GUI::Element *pInsertBefore, C4StartupNetDlg *pNetDlg)
40 : pList(pForListBox), pRefClient(nullptr), pRef(nullptr), iTimeout(0), eQueryType(NRQT_Unknown), fError(false), fIsCollapsed(false), pNetDlg(pNetDlg), fIsSmall(false), fIsEnabled(true), iInfoIconCount(0), fIsImportant(false), iSortOrder(0), iNumFails(0), iInfoLink(-1)
41{
42 // calc height
43 int32_t iLineHgt = C4GUI::GetRes()->TextFont.GetLineHeight(), iHeight = iLineHgt * 2 + 4;
44 // add icons - normal icons use small size, only animated netgetref uses full size
45 rctIconLarge.Set(iX: 0, iY: 0, iWdt: iHeight, iHgt: iHeight);
46 int32_t iSmallIcon = iHeight * 2 / 3; rctIconSmall.Set(iX: (iHeight - iSmallIcon) / 2, iY: (iHeight - iSmallIcon) / 2, iWdt: iSmallIcon, iHgt: iSmallIcon);
47 pIcon = new C4GUI::Icon(rctIconSmall, C4GUI::Ico_Host);
48 AddElement(pChild: pIcon);
49 SetBounds(pIcon->GetBounds());
50 // add to listbox (will get resized horizontally and moved)
51 pForListBox->InsertElement(pChild: this, pInsertBefore);
52 // add status icons and text labels now that width is known
53 CStdFont *pUseFont = &(C4GUI::GetRes()->TextFont);
54 int32_t iIconSize = pUseFont->GetLineHeight();
55 C4Rect rcIconRect = GetContainedClientRect();
56 int32_t iThisWdt = rcIconRect.Wdt;
57 rcIconRect.x = iThisWdt - iIconSize * (iInfoIconCount + 1);
58 rcIconRect.Wdt = rcIconRect.Hgt = iIconSize;
59 for (int32_t iIcon = 0; iIcon < MaxInfoIconCount; ++iIcon)
60 {
61 AddElement(pChild: pInfoIcons[iIcon] = new C4GUI::Icon(rcIconRect, C4GUI::Ico_None));
62 rcIconRect.x -= rcIconRect.Wdt;
63 }
64 C4Rect rcLabelBounds;
65 rcLabelBounds.x = iHeight + 3;
66 rcLabelBounds.Hgt = iLineHgt;
67 for (int i = 0; i < InfoLabelCount; ++i)
68 {
69 C4GUI::Label *pLbl;
70 rcLabelBounds.y = 1 + i * (iLineHgt + 2);
71 rcLabelBounds.Wdt = iThisWdt - rcLabelBounds.x - 1;
72 if (!i) rcLabelBounds.Wdt -= iLineHgt; // leave space for topright extra icon
73 AddElement(pChild: pLbl = pInfoLbl[i] = new C4GUI::Label("", rcLabelBounds, ALeft, C4GUI_CaptionFontClr));
74 // label will have collapsed due to no text: Repair it
75 pLbl->SetAutosize(false);
76 pLbl->SetBounds(rcLabelBounds);
77 }
78 // update small state, which will resize this to a small entry
79 UpdateSmallState();
80 // Set*-function will fill icon and text and calculate actual size
81}
82
83C4StartupNetListEntry::~C4StartupNetListEntry()
84{
85 ClearRef();
86}
87
88void C4StartupNetListEntry::DrawElement(C4FacetEx &cgo)
89{
90 typedef C4GUI::Window ParentClass;
91 // background if important and not selected
92 if (fIsImportant && !IsSelectedChild(pChild: this))
93 {
94 int32_t x1 = cgo.X + cgo.TargetX + rcBounds.x;
95 int32_t y1 = cgo.Y + cgo.TargetY + rcBounds.y;
96 lpDDraw->DrawBoxDw(sfcDest: cgo.Surface, iX1: x1, iY1: y1, iX2: x1 + rcBounds.Wdt, iY2: y1 + rcBounds.Hgt, C4GUI_ImportantBGColor);
97 }
98 // inherited
99 ParentClass::DrawElement(cgo);
100}
101
102void C4StartupNetListEntry::ClearRef()
103{
104 // del old ref data
105 if (pRefClient)
106 {
107 C4InteractiveThread &Thread = Application.InteractiveThread;
108 Thread.RemoveProc(pProc: pRefClient);
109 delete pRefClient; pRefClient = nullptr;
110 }
111 delete pRef; pRef = nullptr;
112 eQueryType = NRQT_Unknown;
113 iTimeout = iRequestTimeout = 0;
114 fError = false;
115 sError.Clear();
116 int32_t i;
117 for (i = 0; i < InfoLabelCount; ++i) sInfoText[i].Clear();
118 iInfoLink = -1;
119 InvalidateStatusIcons();
120 sRefClientAddress.Clear();
121 fIsEnabled = true;
122 fIsImportant = false;
123}
124
125const char *C4StartupNetListEntry::GetQueryTypeName(QueryType eQueryType)
126{
127 switch (eQueryType)
128 {
129 case NRQT_GameDiscovery: return LoadResStr(id: C4ResStrTableKey::IDS_NET_QUERY_LOCALNET);
130 case NRQT_Masterserver: return LoadResStr(id: C4ResStrTableKey::IDS_NET_QUERY_MASTERSRV);
131 case NRQT_DirectJoin: return LoadResStr(id: C4ResStrTableKey::IDS_NET_QUERY_DIRECTJOIN);
132 case NRQT_Unknown:
133 assert(!"Unknown QueryType");
134 return "";
135 };
136 return "";
137}
138
139void C4StartupNetListEntry::SetRefQuery(const char *szAddress, enum QueryType eQueryType)
140{
141 // safety: clear previous
142 ClearRef();
143 // setup layout
144 const_cast<C4Facet &>(static_cast<const C4Facet &>(pIcon->GetFacet())) = static_cast<const C4Facet &>(C4Startup::Get()->Graphics.fctNetGetRef);
145 pIcon->SetAnimated(fEnabled: true, iDelay: 1);
146 pIcon->SetBounds(rctIconLarge);
147 // init a new ref client to query
148 sRefClientAddress.Copy(pnData: szAddress);
149 this->eQueryType = eQueryType;
150 pRefClient = new C4Network2RefClient();
151 if (!pRefClient->Init() || !pRefClient->SetServer(serverAddress: szAddress, defaultPort: eQueryType != NRQT_Masterserver ? Config.Network.PortRefServer : 0))
152 {
153 // should not happen
154 sInfoText[0].Clear();
155 SetError(szErrorText: pRefClient->GetError(), eTimeout: TT_RefReqWait);
156 return;
157 }
158 // set info
159 sInfoText[0].Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENTONNET, args: GetQueryTypeName(eQueryType), args: pRefClient->getServerName()).c_str());
160 sInfoText[1].Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_NET_INFOQUERY));
161 UpdateSmallState(); UpdateText();
162 pRefClient->SetNotify(&Application.InteractiveThread);
163 // masterserver: always on top
164 if (eQueryType == NRQT_Masterserver)
165 iSortOrder = 100;
166 // register proc
167 C4InteractiveThread &Thread = Application.InteractiveThread;
168 Thread.AddProc(pProc: pRefClient);
169 // start querying!
170 QueryReferences();
171}
172
173bool C4StartupNetListEntry::QueryReferences()
174{
175 // begin querying
176 if (!pRefClient->QueryReferences())
177 {
178 SetError(szErrorText: pRefClient->GetError(), eTimeout: TT_RefReqWait);
179 return false;
180 }
181 // set up timeout
182 iRequestTimeout = time(timer: nullptr) + C4NetRefRequestTimeout;
183 return true;
184}
185
186bool C4StartupNetListEntry::Execute()
187{
188 // update entries
189 // if the return value is false, this entry will be deleted
190 // timer running?
191 if (iTimeout) if (time(timer: nullptr) >= iTimeout)
192 {
193 // timeout!
194 // for internet servers, this means refresh needed - search anew!
195 if (pRefClient && eQueryType == NRQT_Masterserver)
196 {
197 fError = false;
198 sError.Clear();
199 const_cast<C4Facet &>(static_cast<const C4Facet &>(pIcon->GetFacet())) = static_cast<const C4Facet &>(C4Startup::Get()->Graphics.fctNetGetRef);
200 pIcon->SetAnimated(fEnabled: true, iDelay: 1);
201 pIcon->SetBounds(rctIconLarge);
202 sInfoText[1].Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_NET_INFOQUERY));
203 iTimeout = 0;
204 QueryReferences();
205 // always keep item even if query failed
206 return true;
207 }
208 // any other item is just removed - return value marks this
209 return false;
210 }
211 // failed without a timer. Nothing to be done about it.
212 if (fError) { OnRequestFailed(); return true; }
213 // updates need to be done for references being retrieved only
214 if (!pRefClient) return true;
215 // check if it has arrived
216 if (pRefClient->isBusy())
217 // still requesting - but do not wait forever
218 if (time(timer: nullptr) >= iRequestTimeout)
219 {
220 SetError(szErrorText: LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_REFREQTIMEOUT), eTimeout: TT_RefReqWait);
221 pRefClient->Cancel(reason: "Timeout");
222 OnRequestFailed();
223 }
224 return true;
225}
226
227void C4StartupNetListEntry::OnRequestFailed()
228{
229 ++iNumFails;
230 // special clonk.de handling: Alternate between ports 80 and 84 and re-request direcly (since we've already waited for the timeout)
231 if (iNumFails <= 2 && eQueryType == NRQT_Masterserver)
232 {
233 if (sRefClientAddress == C4CFG_LeagueServer)
234 SetRefQuery(C4CFG_FallbackServer, eQueryType: NRQT_Masterserver);
235 else if (sRefClientAddress == C4CFG_FallbackServer)
236 SetRefQuery(C4CFG_LeagueServer, eQueryType: NRQT_Masterserver);
237 }
238}
239
240bool C4StartupNetListEntry::OnReference()
241{
242 // wrong type / still busy?
243 if (!pRefClient || pRefClient->isBusy())
244 return true;
245 // successful?
246 if (!pRefClient->isSuccess())
247 {
248 // couldn't get references
249 SetError(szErrorText: pRefClient->GetError(), eTimeout: TT_RefReqWait);
250 return true;
251 }
252 // Ref getting done!
253 pIcon->SetAnimated(fEnabled: false, iDelay: 1);
254 // Get reference information from client
255 C4Network2Reference **ppNewRefs = nullptr; int32_t iNewRefCount;
256 if (!pRefClient->GetReferences(rpReferences&: ppNewRefs, rRefCount&: iNewRefCount))
257 {
258 // References could be retrieved but not read
259 SetError(szErrorText: LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_REFINVALID), eTimeout: TT_RefReqWait);
260 delete[] ppNewRefs;
261 return true;
262 }
263 if (!iNewRefCount)
264 {
265 // References retrieved but no game open: Inform user
266 sInfoText[1].Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_NET_INFONOGAME));
267 UpdateText();
268 }
269 else
270 {
271 // Grab references, count players
272 C4StartupNetListEntry *pNewRefEntry = this; int iPlayerCount = 0;
273 for (int i = 0; i < iNewRefCount; i++)
274 {
275 pNewRefEntry = AddReference(pAddRef: ppNewRefs[i], pInsertBefore: pNewRefEntry->GetNextLower(sortOrder: ppNewRefs[i]->getSortOrder()));
276 iPlayerCount += ppNewRefs[i]->Parameters.PlayerInfos.GetActivePlayerCount(fCountInvisible: false);
277 }
278 // Update text accordingly
279 sInfoText[1].Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_NET_INFOGAMES, args: static_cast<int>(iNewRefCount), args&: iPlayerCount).c_str());
280 UpdateText();
281 }
282 delete[] ppNewRefs;
283 // special masterserver handling
284 if (eQueryType == NRQT_Masterserver)
285 {
286 // show message of the day, if any
287 int32_t iMasterServerMessages = 0;
288 if (pRefClient->GetMessageOfTheDay() && *pRefClient->GetMessageOfTheDay())
289 sInfoText[1 + ++iMasterServerMessages].Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_NET_MOTD, args: pRefClient->GetMessageOfTheDay()).c_str());
290 const char *szMotDLink = pRefClient->GetMessageOfTheDayHyperlink();
291 if (szMotDLink && *szMotDLink)
292 {
293 sInfoText[1 + ++iMasterServerMessages].Copy(pnData: szMotDLink);
294 iInfoLink = 1 + iMasterServerMessages;
295 }
296 if (iMasterServerMessages)
297 {
298 UpdateSmallState();
299 UpdateText();
300 }
301 // Check if the server has delivered a redirect from itself
302 // (alternate servers may not redirect)
303 if (pRefClient->GetLeagueServerRedirect() && (!Config.Network.UseAlternateServer || SEqual(szStr1: Config.Network.ServerAddress, szStr2: Config.Network.AlternateServerAddress)) && !pNetDlg->GetIgnoreUpdate())
304 {
305 const char *newLeagueServer = pRefClient->GetLeagueServerRedirect();
306 if (newLeagueServer && !SEqual(szStr1: newLeagueServer, szStr2: Config.Network.ServerAddress))
307 {
308 // this is a new redirect. Inform the user and auto-change servers if desired
309 const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_NET_SERVERREDIRECTMSG, args&: newLeagueServer)};
310 if (GetScreen()->ShowMessageModal(szMessage: message.c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_NET_SERVERREDIRECT), dwButtons: C4GUI::MessageDialog::btnYesNo, icoIcon: C4GUI::Ico_OfficialServer))
311 {
312 // apply new server setting
313 SCopy(szSource: newLeagueServer, sTarget: Config.Network.ServerAddress, iMaxL: CFG_MaxString);
314 Config.Save();
315 GetScreen()->ShowMessageModal(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_NET_SERVERREDIRECTDONE), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_NET_SERVERREDIRECT), dwButtons: C4GUI::MessageDialog::btnOK, icoIcon: C4GUI::Ico_OfficialServer);
316 SetTimeout(TT_Refresh);
317 return true;
318 }
319 else
320 {
321 pNetDlg->SetIgnoreUpdate(true);
322 }
323 }
324 }
325 // masterserver: schedule next query
326 SetTimeout(TT_Masterserver);
327 return true;
328 }
329 // non-masterserver
330 if (iNewRefCount)
331 {
332 // this item has been "converted" into the references - remove without further feedback
333 return false;
334 }
335 else
336 // no ref found on custom adress: Schedule re-check
337 SetTimeout(TT_RefReqWait);
338 return true;
339}
340
341C4GUI::Element *C4StartupNetListEntry::GetNextLower(int32_t sortOrder)
342{
343 // search list for the next element of a lower sort order
344 for (C4GUI::Element *pElem = pList->GetFirst(); pElem; pElem = pElem->GetNext())
345 {
346 C4StartupNetListEntry *pEntry = static_cast<C4StartupNetListEntry *>(pElem);
347 if (pEntry->iSortOrder < sortOrder)
348 return pElem;
349 }
350 // none found: insert at start
351 return nullptr;
352}
353
354void C4StartupNetListEntry::UpdateCollapsed(bool fToCollapseValue)
355{
356 // if collapsed state changed, update the text
357 if (fIsCollapsed == fToCollapseValue) return;
358 fIsCollapsed = fToCollapseValue;
359 UpdateSmallState();
360}
361
362void C4StartupNetListEntry::UpdateSmallState()
363{
364 // small view: Always collapsed if there is no extended text
365 bool fNewIsSmall = !sInfoText[2].getLength() || fIsCollapsed;
366 if (fNewIsSmall == fIsSmall) return;
367 fIsSmall = fNewIsSmall;
368 for (int i = 2; i < InfoLabelCount; ++i) pInfoLbl[i]->SetVisibility(!fIsSmall);
369 UpdateEntrySize();
370}
371
372void C4StartupNetListEntry::UpdateEntrySize()
373{
374 // restack all labels by their size
375 int32_t iLblCnt = (fIsSmall ? 2 : InfoLabelCount), iY = 1;
376 while (iLblCnt > 2 && !sInfoText[iLblCnt - 1])
377 iLblCnt--;
378 for (int i = 0; i < iLblCnt; ++i)
379 {
380 C4Rect rcBounds = pInfoLbl[i]->GetBounds();
381 rcBounds.y = iY;
382 iY += rcBounds.Hgt + 2;
383 pInfoLbl[i]->SetBounds(rcBounds);
384 }
385 // resize this control
386 GetBounds().Hgt = iY - 1;
387 UpdateSize();
388}
389
390void C4StartupNetListEntry::UpdateText()
391{
392 bool fRestackElements = false;
393 CStdFont *pUseFont = &(C4GUI::GetRes()->TextFont);
394 // adjust icons
395 int32_t sx = iInfoIconCount * pUseFont->GetLineHeight();
396 int32_t i;
397 for (i = iInfoIconCount; i < MaxInfoIconCount; ++i)
398 {
399 pInfoIcons[i]->SetIcon(C4GUI::Ico_None);
400 pInfoIcons[i]->SetToolTip(nullptr);
401 }
402 // text to labels
403 for (i = 0; i < InfoLabelCount; ++i)
404 {
405 int iAvailableWdt = GetClientRect().Wdt - pInfoLbl[i]->GetBounds().x - 1;
406 if (!i) iAvailableWdt -= sx;
407 StdStrBuf BrokenText;
408 pUseFont->BreakMessage(szMsg: sInfoText[i].getData(), iWdt: iAvailableWdt, pOut: &BrokenText, fCheckMarkup: true);
409 int32_t iHgt, iWdt;
410 if (pUseFont->GetTextExtent(szText: BrokenText.getData(), rsx&: iWdt, rsy&: iHgt, fCheckMarkup: true))
411 {
412 if ((pInfoLbl[i]->GetBounds().Hgt != iHgt) || (pInfoLbl[i]->GetBounds().Wdt != iAvailableWdt))
413 {
414 C4Rect rcBounds = pInfoLbl[i]->GetBounds();
415 rcBounds.Wdt = iAvailableWdt;
416 rcBounds.Hgt = iHgt;
417 pInfoLbl[i]->SetBounds(rcBounds);
418 fRestackElements = true;
419 }
420 }
421 pInfoLbl[i]->SetText(szText: BrokenText.getData());
422 if (iInfoLink == i)
423 pInfoLbl[i]->SetHyperlink(sInfoText[i].getData());
424 else
425 pInfoLbl[i]->SetColor(dwToClr: fIsEnabled ? C4GUI_MessageFontClr : C4GUI_InactMessageFontClr);
426 pInfoLbl[i]->SetVisibility(BrokenText.getLength());
427 }
428 if (fRestackElements) UpdateEntrySize();
429}
430
431void C4StartupNetListEntry::AddStatusIcon(C4GUI::Icons eIcon, const char *szToolTip)
432{
433 // safety
434 if (iInfoIconCount == MaxInfoIconCount) return;
435 // set icon to the left of the existing icons to the desired data
436 pInfoIcons[iInfoIconCount]->SetIcon(eIcon);
437 pInfoIcons[iInfoIconCount]->SetToolTip(szToolTip);
438 ++iInfoIconCount;
439}
440
441void C4StartupNetListEntry::SetReference(C4Network2Reference *pRef)
442{
443 // safety: clear previous
444 ClearRef();
445 // set info
446 this->pRef = pRef;
447 int32_t iIcon = pRef->getIcon();
448 if (!Inside<int32_t>(ival: iIcon, lbound: 0, rbound: C4StartupScenSel_IconCount - 1)) iIcon = C4StartupScenSel_DefaultIcon_Scenario;
449 pIcon->SetFacet(C4Startup::Get()->Graphics.fctScenSelIcons.GetPhase(iPhaseX: iIcon));
450 pIcon->SetAnimated(fEnabled: false, iDelay: 0);
451 pIcon->SetBounds(rctIconSmall);
452 int32_t iPlrCnt = pRef->Parameters.PlayerInfos.GetActivePlayerCount(fCountInvisible: false);
453 C4Client *pHost = pRef->Parameters.Clients.getHost();
454 sInfoText[0].Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_NET_REFONCLIENT, args: pRef->getTitle(), args: pHost ? pHost->getName() : "unknown").c_str());
455 sInfoText[1].Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_NET_INFOPLRSGOALDESC,
456 args: static_cast<int>(iPlrCnt),
457 args: static_cast<int>(pRef->Parameters.MaxPlayers),
458 args: pRef->Parameters.GetGameGoalString(),
459 args: StdStrBuf(pRef->getGameStatus().getDescription(), true).getData()).c_str());
460 if (pRef->getTime() > 0)
461 {
462 const std::string duration{std::format(fmt: "{:02}:{:02}:{:02}", args: pRef->getTime() / 3600, args: (pRef->getTime() % 3600) / 60, args: pRef->getTime() % 60)};
463 sInfoText[1].Append(pnData: " - "); sInfoText[1].Append(pnData: duration.c_str());
464 }
465 sInfoText[2].Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_DESC_VERSION, args: pRef->getGameVersion().GetString()).c_str());
466 sInfoText[3].Copy(pnData: std::format(fmt: "{}: {}", args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_COMMENT), args: pRef->getComment()).c_str());
467 // password
468 if (pRef->isPasswordNeeded())
469 AddStatusIcon(eIcon: C4GUI::Ico_Ex_LockedFrontal, szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_NET_INFOPASSWORD));
470 // league
471 if (pRef->Parameters.isLeague())
472 AddStatusIcon(eIcon: C4GUI::Ico_Ex_League, szToolTip: pRef->Parameters.getLeague());
473 // lobby active
474 if (pRef->getGameStatus().isLobbyActive())
475 AddStatusIcon(eIcon: C4GUI::Ico_Lobby, szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_DESC_EXPECTING));
476 // game running
477 if (pRef->getGameStatus().isPastLobby())
478 AddStatusIcon(eIcon: C4GUI::Ico_GameRunning, szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_NET_INFOINPROGR));
479 // runtime join
480 if (pRef->isJoinAllowed() && pRef->getGameStatus().isPastLobby()) // A little workaround to determine RuntimeJoin...
481 AddStatusIcon(eIcon: C4GUI::Ico_RuntimeJoin, szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_NET_RUNTIMEJOINFREE));
482 // fair crew
483 if (pRef->Parameters.UseFairCrew)
484 AddStatusIcon(eIcon: C4GUI::Ico_Ex_FairCrew, szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_CTL_FAIRCREW_DESC));
485 // official server
486 if (pRef->isOfficialServer() && !Config.Network.UseAlternateServer) // Offical server icon is only displayed if references are obtained from official league server
487 {
488 fIsImportant = true;
489 AddStatusIcon(eIcon: C4GUI::Ico_OfficialServer, szToolTip: LoadResStr(id: C4ResStrTableKey::IDS_NET_OFFICIALSERVER));
490 }
491 // list participating player names
492 sInfoText[4].Copy(pnData: std::format(fmt: "{}: {}", args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_PLAYER), args: iPlrCnt ? pRef->Parameters.PlayerInfos.GetActivePlayerNames(fCountInvisible: false).getData() : LoadResStr(id: C4ResStrTableKey::IDS_CTL_NONE)).c_str());
493 // disabled if join is not possible for some reason
494 C4GameVersion verThis;
495 if (!pRef->isJoinAllowed() || !(pRef->getGameVersion() == verThis))
496 {
497 fIsEnabled = false;
498 }
499 // store sort order
500 iSortOrder = pRef->getSortOrder();
501 // all references expire after a while
502 SetTimeout(TT_Reference);
503 UpdateSmallState(); UpdateText();
504}
505
506void C4StartupNetListEntry::SetError(const char *szErrorText, TimeoutType eTimeout)
507{
508 // set error message
509 fError = true;
510 sInfoText[1].Copy(pnData: szErrorText);
511 for (int i = 2; i < InfoLabelCount; ++i) sInfoText[i].Clear();
512 InvalidateStatusIcons();
513 UpdateSmallState(); UpdateText();
514 pIcon->SetIcon(C4GUI::Ico_Close);
515 pIcon->SetAnimated(fEnabled: false, iDelay: 0);
516 pIcon->SetBounds(rctIconSmall);
517 SetTimeout(eTimeout);
518}
519
520void C4StartupNetListEntry::SetTimeout(TimeoutType eTimeout)
521{
522 int iTime = 0;
523 switch (eTimeout)
524 {
525 case TT_RefReqWait: iTime = (eQueryType == NRQT_Masterserver) ? C4NetMasterServerQueryInterval : C4NetErrorRefTimeout; break;
526 case TT_Reference: iTime = C4NetReferenceTimeout; break;
527 case TT_Masterserver: iTime = C4NetMasterServerQueryInterval; break;
528 case TT_Refresh: iTime = 1; break; // refresh ASAP
529 };
530 if (!iTime) return;
531 iTimeout = time(timer: nullptr) + iTime;
532}
533
534C4StartupNetListEntry *C4StartupNetListEntry::AddReference(C4Network2Reference *pAddRef, C4GUI::Element *pInsertBefore)
535{
536 // check list whether the same reference has been added already
537 for (C4GUI::Element *pElem = pList->GetFirst(); pElem; pElem = pElem->GetNext())
538 {
539 C4StartupNetListEntry *pEntry = static_cast<C4StartupNetListEntry *>(pElem);
540 // match to existing reference entry:
541 // * same host (checking for same name and nick)
542 // * at least one match in address and port
543 // * the incoming reference is newer than (or same as) the current one
544 if (pEntry->IsSameHost(pRef2: pAddRef)
545 && pEntry->IsSameAddress(pRef2: pAddRef)
546 && (pEntry->GetReference()->getStartTime() <= pAddRef->getStartTime()))
547 {
548 // update existing entry
549 pEntry->SetReference(pAddRef);
550 return pEntry;
551 }
552 }
553 // no update - just add
554 C4StartupNetListEntry *pNewRefEntry = new C4StartupNetListEntry(pList, pInsertBefore, pNetDlg);
555 pNewRefEntry->SetReference(pAddRef);
556 pNetDlg->OnReferenceEntryAdd(pEntry: pNewRefEntry);
557 return pNewRefEntry;
558}
559
560bool C4StartupNetListEntry::IsSameHost(const C4Network2Reference *pRef2)
561{
562 // not if ref has not been retrieved yet
563 if (!pRef) return false;
564 C4Client *pHost1 = pRef->Parameters.Clients.getHost();
565 C4Client *pHost2 = pRef2->Parameters.Clients.getHost();
566 if (!pHost1 || !pHost2) return false;
567 // check
568 return SEqual(szStr1: pHost1->getName(), szStr2: pHost2->getName());
569}
570
571bool C4StartupNetListEntry::IsSameAddress(const C4Network2Reference *pRef2)
572{
573 // not if ref has not been retrieved yet
574 if (!pRef) return false;
575 // check for at least one matching address
576 for (const auto &ownAddr : pRef->getAddresses())
577 {
578 for (const auto &otherAddr : pRef2->getAddresses())
579 {
580 if (ownAddr == otherAddr)
581 {
582 return true;
583 }
584 }
585 }
586 // no match
587 return false;
588}
589
590bool C4StartupNetListEntry::IsSameRefQueryAddress(const char *szJoinaddress)
591{
592 // only unretrieved references
593 if (!pRefClient) return false;
594 // if request failed, create a duplicate anyway in case the game is opened now
595 // except masterservers, which would re-search some time later anyway
596 if (fError && eQueryType != NRQT_Masterserver) return false;
597 // check equality of address
598 // do it the simple way for now
599 return SEqualNoCase(szStr1: sRefClientAddress.getData(), szStr2: szJoinaddress);
600}
601
602const char *C4StartupNetListEntry::GetJoinAddress()
603{
604 // only unresolved references
605 if (!pRefClient) return nullptr;
606 // not masterservers (cannot join directly on clonk.de)
607 if (eQueryType == NRQT_Masterserver) return nullptr;
608 // return join address
609 return pRefClient->getServerName();
610}
611
612C4Network2Reference *C4StartupNetListEntry::GrabReference()
613{
614 C4Network2Reference *pOldRef = pRef;
615 pRef = nullptr;
616 return pOldRef;
617}
618
619// C4StartupNetDlg
620
621C4StartupNetDlg::C4StartupNetDlg() : C4StartupDlg(LoadResStr(id: C4ResStrTableKey::IDS_DLG_NETSTART)), iGameDiscoverInterval(0), pMasterserverClient(nullptr), fIsCollapsed(false), fUpdatingList(false), tLastRefresh(0), pChatTitleLabel(nullptr), fIgnoreUpdate(false)
622{
623 // key bindings
624 C4CustomKey::CodeList keys;
625 keys.push_back(x: C4KeyCodeEx(K_BACK)); keys.push_back(x: C4KeyCodeEx(K_LEFT));
626 pKeyBack = new C4KeyBinding(keys, "StartupNetBack", KEYSCOPE_Gui,
627 new C4GUI::DlgKeyCB<C4StartupNetDlg>(*this, &C4StartupNetDlg::KeyBack), C4CustomKey::PRIO_Dlg);
628 pKeyRefresh = new C4KeyBinding(C4KeyCodeEx(K_F5), "StartupNetReload", KEYSCOPE_Gui,
629 new C4GUI::DlgKeyCB<C4StartupNetDlg>(*this, &C4StartupNetDlg::KeyRefresh), C4CustomKey::PRIO_CtrlOverride);
630
631 // screen calculations
632 UpdateSize();
633 int32_t iIconSize = C4GUI_IconExWdt;
634 int32_t iButtonWidth, iCaptionFontHgt, iSideSize = std::max<int32_t>(a: GetBounds().Wdt / 6, b: iIconSize);
635 int32_t iButtonHeight = C4GUI_ButtonHgt, iButtonIndent = GetBounds().Wdt / 40;
636 C4GUI::GetRes()->CaptionFont.GetTextExtent(szText: "<< BACK", rsx&: iButtonWidth, rsy&: iCaptionFontHgt, fCheckMarkup: true);
637 iButtonWidth *= 3;
638 C4GUI::ComponentAligner caMain(GetClientRect(), 0, 0, true);
639 C4GUI::ComponentAligner caButtonArea(caMain.GetFromBottom(iHgt: caMain.GetHeight() / 7), 0, 0);
640 int32_t iButtonAreaWdt = caButtonArea.GetWidth() * 7 / 8;
641 iButtonWidth = std::min<int32_t>(a: iButtonWidth, b: (iButtonAreaWdt - 8 * iButtonIndent) / 4);
642 iButtonIndent = (iButtonAreaWdt - 4 * iButtonWidth) / 8;
643 C4GUI::ComponentAligner caButtons(caButtonArea.GetCentered(iWdt: iButtonAreaWdt, iHgt: iButtonHeight), iButtonIndent, 0);
644 C4GUI::ComponentAligner caLeftBtnArea(caMain.GetFromLeft(iWdt: iSideSize), std::min<int32_t>(a: caMain.GetWidth() / 20, b: (iSideSize - C4GUI_IconExWdt) / 2), caMain.GetHeight() / 40);
645 C4GUI::ComponentAligner caConfigArea(caMain.GetFromRight(iWdt: iSideSize), std::min<int32_t>(a: caMain.GetWidth() / 20, b: (iSideSize - C4GUI_IconExWdt) / 2), caMain.GetHeight() / 40);
646
647 // left button area: Switch between chat and game list
648 if (C4ChatDlg::IsChatEnabled())
649 {
650 btnGameList = new C4GUI::CallbackButton<C4StartupNetDlg, C4GUI::IconButton>(C4GUI::Ico_Ex_GameList, caLeftBtnArea.GetFromTop(iHgt: iIconSize, iWdt: iIconSize), '\0', &C4StartupNetDlg::OnBtnGameList);
651 btnGameList->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_SHOWSAVAILABLENETWORKGAME));
652 btnGameList->SetText(LoadResStr(id: C4ResStrTableKey::IDS_BTN_GAMES));
653 AddElement(pChild: btnGameList);
654 btnChat = new C4GUI::CallbackButton<C4StartupNetDlg, C4GUI::IconButton>(C4GUI::Ico_Ex_Chat, caLeftBtnArea.GetFromTop(iHgt: iIconSize, iWdt: iIconSize), '\0', &C4StartupNetDlg::OnBtnChat);
655 btnChat->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_CONNECTSTOANIRCCHATSERVER));
656 btnChat->SetText(LoadResStr(id: C4ResStrTableKey::IDS_BTN_CHAT));
657 AddElement(pChild: btnChat);
658 }
659 else btnChat = nullptr;
660
661 // main area: Tabular to switch between game list and chat
662 pMainTabular = new C4GUI::Tabular(caMain.GetAll(), C4GUI::Tabular::tbNone);
663 pMainTabular->SetDrawDecoration(false);
664 pMainTabular->SetSheetMargin(0);
665 AddElement(pChild: pMainTabular);
666
667 // main area: game selection sheet
668 C4GUI::Tabular::Sheet *pSheetGameList = pMainTabular->AddSheet(szTitle: nullptr);
669 C4GUI::ComponentAligner caGameList(pSheetGameList->GetContainedClientRect(), 0, 0, false);
670 C4GUI::WoodenLabel *pGameListLbl; int32_t iCaptHgt = C4GUI::WoodenLabel::GetDefaultHeight(pUseFont: &C4GUI::GetRes()->TextFont);
671 pGameListLbl = new C4GUI::WoodenLabel(LoadResStr(id: C4ResStrTableKey::IDS_NET_GAMELIST), caGameList.GetFromTop(iHgt: iCaptHgt), C4GUI_Caption2FontClr, &C4GUI::GetRes()->TextFont, ALeft);
672 pSheetGameList->AddElement(pChild: pGameListLbl);
673 pGameSelList = new C4GUI::ListBox(caGameList.GetFromTop(iHgt: caGameList.GetHeight() - iCaptHgt));
674 pGameSelList->SetDecoration(fDrawBG: true, pToGfx: nullptr, fAutoScroll: true, fDrawBorder: true);
675 pGameSelList->UpdateElementPositions();
676 pGameSelList->SetSelectionDblClickFn(new C4GUI::CallbackHandler<C4StartupNetDlg>(this, &C4StartupNetDlg::OnSelDblClick));
677 pGameSelList->SetSelectionChangeCallbackFn(new C4GUI::CallbackHandler<C4StartupNetDlg>(this, &C4StartupNetDlg::OnSelChange));
678 pSheetGameList->AddElement(pChild: pGameSelList);
679 C4GUI::ComponentAligner caIP(caGameList.GetAll(), 0, 0);
680 C4GUI::WoodenLabel *pIPLbl;
681 const char *szIPLblText = LoadResStr(id: C4ResStrTableKey::IDS_NET_IP);
682 int32_t iIPWdt = 100, Q;
683 C4GUI::GetRes()->TextFont.GetTextExtent(szText: szIPLblText, rsx&: iIPWdt, rsy&: Q, fCheckMarkup: true);
684 pIPLbl = new C4GUI::WoodenLabel(szIPLblText, caIP.GetFromLeft(iWdt: iIPWdt + 10), C4GUI_Caption2FontClr, &C4GUI::GetRes()->TextFont);
685 const char *szIPTip = LoadResStr(id: C4ResStrTableKey::IDS_NET_IP_DESC);
686 pIPLbl->SetToolTip(szIPTip);
687 pSheetGameList->AddElement(pChild: pIPLbl);
688 pJoinAddressEdt = new C4GUI::CallbackEdit<C4StartupNetDlg>(caIP.GetAll(), this, &C4StartupNetDlg::OnJoinAddressEnter);
689 pJoinAddressEdt->SetToolTip(szIPTip);
690 pSheetGameList->AddElement(pChild: pJoinAddressEdt);
691
692 // main area: chat sheet
693 if (C4ChatDlg::IsChatEnabled())
694 {
695 C4GUI::Tabular::Sheet *pSheetChat = pMainTabular->AddSheet(szTitle: nullptr);
696 C4GUI::ComponentAligner caChat(pSheetChat->GetContainedClientRect(), 0, 0, false);
697 pSheetChat->AddElement(pChild: pChatTitleLabel = new C4GUI::WoodenLabel("", caChat.GetFromTop(iHgt: iCaptHgt), C4GUI_Caption2FontClr, &C4GUI::GetRes()->TextFont, ALeft, false));
698 C4GUI::GroupBox *pChatGroup = new C4GUI::GroupBox(caChat.GetAll());
699 pChatGroup->SetColors(dwFrameClr: 0u, C4GUI_CaptionFontClr, C4GUI_StandardBGColor);
700 pChatGroup->SetMargin(2);
701 pSheetChat->AddElement(pChild: pChatGroup);
702 pChatCtrl = new C4ChatControl(&Application.IRCClient);
703 pChatCtrl->SetBounds(pChatGroup->GetContainedClientRect());
704 pChatCtrl->SetTitleChangeCB(new C4GUI::InputCallback<C4StartupNetDlg>(this, &C4StartupNetDlg::OnChatTitleChange));
705 StdStrBuf sCurrTitle; sCurrTitle.Ref(pnData: pChatCtrl->GetTitle()); OnChatTitleChange(sNewTitle: sCurrTitle);
706 pChatGroup->AddElement(pChild: pChatCtrl);
707 }
708
709 // config area
710 btnInternet = new C4GUI::CallbackButton<C4StartupNetDlg, C4GUI::IconButton>(Config.Network.MasterServerSignUp ? C4GUI::Ico_Ex_InternetOn : C4GUI::Ico_Ex_InternetOff, caConfigArea.GetFromTop(iHgt: iIconSize, iWdt: iIconSize), '\0', &C4StartupNetDlg::OnBtnInternet);
711 btnInternet->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_SEARCHINTERNETGAME));
712 btnInternet->SetText(LoadResStr(id: C4ResStrTableKey::IDS_CTL_INETSERVER));
713 AddElement(pChild: btnInternet);
714 btnRecord = new C4GUI::CallbackButton<C4StartupNetDlg, C4GUI::IconButton>(Config.General.Record ? C4GUI::Ico_Ex_RecordOn : C4GUI::Ico_Ex_RecordOff, caConfigArea.GetFromTop(iHgt: iIconSize, iWdt: iIconSize), '\0', &C4StartupNetDlg::OnBtnRecord);
715 btnRecord->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_RECORD));
716 btnRecord->SetText(LoadResStr(id: C4ResStrTableKey::IDS_CTL_RECORD));
717 AddElement(pChild: btnRecord);
718
719 // button area
720 C4GUI::CallbackButton<C4StartupNetDlg> *btn;
721 AddElement(pChild: btn = new C4GUI::CallbackButton<C4StartupNetDlg>(LoadResStr(id: C4ResStrTableKey::IDS_BTN_BACK), caButtons.GetFromLeft(iWdt: iButtonWidth), &C4StartupNetDlg::OnBackBtn));
722 btn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_BACKMAIN));
723 AddElement(pChild: btnRefresh = new C4GUI::CallbackButton<C4StartupNetDlg>(LoadResStr(id: C4ResStrTableKey::IDS_BTN_RELOAD), caButtons.GetFromLeft(iWdt: iButtonWidth), &C4StartupNetDlg::OnRefreshBtn));
724 btnRefresh->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_NET_RELOAD_DESC));
725 AddElement(pChild: btnJoin = new C4GUI::CallbackButton<C4StartupNetDlg>(LoadResStr(id: C4ResStrTableKey::IDS_NET_JOINGAME_BTN), caButtons.GetFromLeft(iWdt: iButtonWidth), &C4StartupNetDlg::OnJoinGameBtn));
726 btnJoin->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_NET_JOINGAME_DESC));
727 AddElement(pChild: btn = new C4GUI::CallbackButton<C4StartupNetDlg>(LoadResStr(id: C4ResStrTableKey::IDS_NET_NEWGAME), caButtons.GetFromLeft(iWdt: iButtonWidth), &C4StartupNetDlg::OnCreateGameBtn));
728 btn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_NET_NEWGAME_DESC));
729
730 // initial dlg mode
731 UpdateDlgMode();
732
733 // initial focus
734 SetFocus(pCtrl: GetDlgModeFocusControl(), fByMouse: false);
735
736 // initialize discovery
737 DiscoverClient.Init(iPort: Config.Network.PortDiscovery);
738 DiscoverClient.StartDiscovery();
739 iGameDiscoverInterval = C4NetGameDiscoveryInterval;
740
741 // create timer
742 pSec1Timer = new C4Sec1TimerCallback<C4StartupNetDlg>(this);
743
744 // register as receiver of reference notifies
745 Application.InteractiveThread.SetCallback(eEvent: Ev_HTTP_Response, pnNetworkCallback: this);
746}
747
748C4StartupNetDlg::~C4StartupNetDlg()
749{
750 // disable notifies
751 Application.InteractiveThread.ClearCallback(eEvent: Ev_HTTP_Response, pnNetworkCallback: this);
752 DiscoverClient.Close();
753 pSec1Timer->Release();
754 delete pMasterserverClient;
755
756 delete pKeyBack;
757 delete pKeyRefresh;
758}
759
760void C4StartupNetDlg::DrawElement(C4FacetEx &cgo)
761{
762 // draw background
763 DrawBackground(cgo, rFromFct&: C4Startup::Get()->Graphics.fctNetBG);
764}
765
766bool C4StartupNetDlg::IsOpen(C4StartupNetDlg *const instance)
767{
768 return C4Startup::Get() && C4Startup::Get()->pCurrDlg == instance;
769}
770
771void C4StartupNetDlg::OnShown()
772{
773 // callback when shown: Start searching for games
774 fIgnoreUpdate = false;
775 C4StartupDlg::OnShown();
776 UpdateList();
777 UpdateMasterserver();
778 OnSec1Timer();
779 tLastRefresh = time(timer: nullptr);
780 // also update chat
781 if (pChatCtrl) pChatCtrl->OnShown();
782}
783
784void C4StartupNetDlg::OnClosed(bool fOK)
785{
786 // dlg abort: return to main screen
787 pGameSelList->SelectNone(fByUser: false);
788 delete pMasterserverClient; pMasterserverClient = nullptr;
789 if (!fOK) DoBack();
790}
791
792C4GUI::Control *C4StartupNetDlg::GetDefaultControl()
793{
794 // default control depends on whether dlg is in chat or game list mode
795 if (GetDlgMode() == SNDM_Chat && pChatCtrl)
796 // chat mode: Chat input edit
797 return pChatCtrl->GetDefaultControl();
798 else
799 // game list mode: No default control, because it would move focus away from IP input edit
800 return nullptr;
801}
802
803C4GUI::Control *C4StartupNetDlg::GetDlgModeFocusControl()
804{
805 // default control depends on whether dlg is in chat or game list mode
806 if (GetDlgMode() == SNDM_Chat && pChatCtrl)
807 // chat mode: Chat input edit
808 return pChatCtrl->GetDefaultControl();
809 else
810 // game list mode: Game list box
811 return pGameSelList;
812}
813
814void C4StartupNetDlg::OnBtnGameList(C4GUI::Control *btn)
815{
816 // switch to game list dialog
817 pMainTabular->SelectSheet(iIndex: SNDM_GameList, fByUser: true);
818 UpdateDlgMode();
819}
820
821void C4StartupNetDlg::OnBtnChat(C4GUI::Control *btn)
822{
823 // toggle chat / game list
824 if (pChatCtrl)
825 if (pMainTabular->GetActiveSheetIndex() == SNDM_GameList)
826 {
827 pMainTabular->SelectSheet(iIndex: SNDM_Chat, fByUser: true);
828 pChatCtrl->OnShown();
829 UpdateDlgMode();
830 }
831 else
832 {
833 pMainTabular->SelectSheet(iIndex: SNDM_GameList, fByUser: true);
834 UpdateDlgMode();
835 }
836}
837
838void C4StartupNetDlg::OnBtnInternet(C4GUI::Control *btn)
839{
840 // toggle masterserver game search
841 Config.Network.MasterServerSignUp = !Config.Network.MasterServerSignUp;
842 UpdateMasterserver();
843}
844
845void C4StartupNetDlg::OnBtnRecord(C4GUI::Control *btn)
846{
847 // toggle league signup flag
848 btnRecord->SetIcon((Config.General.Record = !Config.General.Record) ? C4GUI::Ico_Ex_RecordOn : C4GUI::Ico_Ex_RecordOff);
849}
850
851void C4StartupNetDlg::UpdateMasterserver()
852{
853 // update button icon to current state
854 btnInternet->SetIcon(Config.Network.MasterServerSignUp ? C4GUI::Ico_Ex_InternetOn : C4GUI::Ico_Ex_InternetOff);
855 // creates masterserver object if masterserver is enabled; destroy otherwise
856 if (!Config.Network.MasterServerSignUp == !pMasterserverClient) return;
857 if (!Config.Network.MasterServerSignUp)
858 {
859 delete pMasterserverClient;
860 pMasterserverClient = nullptr;
861 }
862 else
863 {
864 pMasterserverClient = new C4StartupNetListEntry(pGameSelList, nullptr, this);
865 pMasterserverClient->SetRefQuery(szAddress: Config.Network.GetLeagueServerAddress(), eQueryType: C4StartupNetListEntry::NRQT_Masterserver);
866 }
867}
868
869void C4StartupNetDlg::UpdateList(bool fGotReference)
870{
871 // recursion check
872 if (fUpdatingList) return;
873 fUpdatingList = true;
874 pGameSelList->FreezeScrolling();
875 // Update all child entries
876 bool fAnyRemoval = false;
877 C4GUI::Element *pElem, *pNextElem = pGameSelList->GetFirst();
878 while (pElem = pNextElem)
879 {
880 pNextElem = pElem->GetNext(); // determine next exec element now - execution
881 C4StartupNetListEntry *pEntry = static_cast<C4StartupNetListEntry *>(pElem);
882 // do item updates
883 bool fKeepEntry = true;
884 if (fGotReference)
885 fKeepEntry = pEntry->OnReference();
886 if (fKeepEntry)
887 fKeepEntry = pEntry->Execute();
888 // remove?
889 if (!fKeepEntry)
890 {
891 // entry wishes to be removed
892 // if the selected entry is being removed, the next entry should be selected (which might be the ref for a finished refquery)
893 if (pGameSelList->GetSelectedItem() == pEntry)
894 if (pEntry->GetNext())
895 {
896 pGameSelList->SelectEntry(pNewSel: pEntry->GetNext(), fByUser: false);
897 }
898 delete pEntry;
899 fAnyRemoval = true; // setting any removal will also update collapsed state of all entries; so no need to do updates because of selection change here
900 }
901 }
902
903 // Add LAN games
904 C4NetIO::addr_t Discover;
905 while (DiscoverClient.PopDiscover(Discover))
906 {
907 AddReferenceQuery(szAddress: Discover.ToString().c_str(), eQueryType: C4StartupNetListEntry::NRQT_GameDiscovery);
908 }
909
910 // check whether view needs to be collapsed or uncollapsed
911 if (fIsCollapsed && fAnyRemoval)
912 {
913 // try uncollapsing
914 fIsCollapsed = false;
915 UpdateCollapsed();
916 // if scrolling is still necessary, the view will be collapsed again immediately
917 }
918 if (!fIsCollapsed && pGameSelList->IsScrollingNecessary())
919 {
920 fIsCollapsed = true;
921 UpdateCollapsed();
922 }
923
924 fUpdatingList = false;
925 // done; selection might have changed
926 pGameSelList->UnFreezeScrolling();
927 UpdateSelection(fUpdateCollapsed: false);
928}
929
930void C4StartupNetDlg::UpdateCollapsed()
931{
932 // Uncollapse one element even if list is collapsed. Choose masterserver if no element is currently selected.
933 C4GUI::Element *pUncollapseElement = pGameSelList->GetSelectedItem();
934 if (!pUncollapseElement) pUncollapseElement = pMasterserverClient;
935 // update collapsed state for all child entries
936 for (C4GUI::Element *pElem = pGameSelList->GetFirst(); pElem; pElem = pElem->GetNext())
937 {
938 C4StartupNetListEntry *pEntry = static_cast<C4StartupNetListEntry *>(pElem);
939 pEntry->UpdateCollapsed(fToCollapseValue: fIsCollapsed && pElem != pUncollapseElement);
940 }
941}
942
943void C4StartupNetDlg::UpdateSelection(bool fUpdateCollapsed)
944{
945 // not during list updates - list update call will do this
946 if (fUpdatingList) return;
947 // in collapsed view, updating the selection may uncollapse something
948 if (fIsCollapsed && fUpdateCollapsed) UpdateCollapsed();
949 // 2do: no selection: join button disabled
950}
951
952void C4StartupNetDlg::UpdateDlgMode()
953{
954 DlgMode eMode = GetDlgMode();
955 // buttons for game joining only visible in game list mode
956 btnInternet->SetVisibility(eMode == SNDM_GameList);
957 btnRecord->SetVisibility(eMode == SNDM_GameList);
958 btnJoin->SetVisibility(eMode == SNDM_GameList);
959 btnRefresh->SetVisibility(eMode == SNDM_GameList);
960 // focus update
961 if (!GetFocus()) SetFocus(pCtrl: GetDlgModeFocusControl(), fByMouse: false);
962}
963
964C4StartupNetDlg::DlgMode C4StartupNetDlg::GetDlgMode()
965{
966 // dlg mode determined by active tabular sheet
967 if (pMainTabular->GetActiveSheetIndex() == SNDM_Chat) return SNDM_Chat; else return SNDM_GameList;
968}
969
970void C4StartupNetDlg::OnThreadEvent(C4InteractiveEventType, const std::any &)
971{
972 UpdateList(fGotReference: true);
973}
974
975bool C4StartupNetDlg::DoOK()
976{
977 // OK in chat mode? Forward to chat control
978 if (GetDlgMode() == SNDM_Chat) return pChatCtrl->DlgEnter();
979 // OK on editbox with text enetered: Add the specified IP for reference retrieval
980 if (GetFocus() == pJoinAddressEdt)
981 {
982 const char *szDirectJoinAddress = pJoinAddressEdt->GetText();
983 if (szDirectJoinAddress && *szDirectJoinAddress)
984 {
985 AddReferenceQuery(szAddress: szDirectJoinAddress, eQueryType: C4StartupNetListEntry::NRQT_DirectJoin);
986 // Switch focus to list so another OK joins the specified address
987 SetFocus(pCtrl: pGameSelList, fByMouse: true);
988 return true;
989 }
990 }
991 // get currently selected item
992 C4GUI::Element *pSelection = pGameSelList->GetSelectedItem();
993 StdStrBuf strNoJoin(LoadResStr(id: C4ResStrTableKey::IDS_NET_NOJOIN));
994 if (!pSelection)
995 {
996 // no ref selected: Oh noes!
997 Game.pGUI->ShowMessageModal(
998 szMessage: LoadResStr(id: C4ResStrTableKey::IDS_NET_NOJOIN_NOREF),
999 szCaption: strNoJoin.getData(),
1000 dwButtons: C4GUI::MessageDialog::btnOK,
1001 icoIcon: C4GUI::Ico_Error);
1002 return true;
1003 }
1004 C4StartupNetListEntry *pRefEntry = static_cast<C4StartupNetListEntry *>(pSelection);
1005 const char *szError;
1006 if (szError = pRefEntry->GetError())
1007 {
1008 // erroneous ref selected: Oh noes!
1009 Game.pGUI->ShowMessageModal(
1010 szMessage: LoadResStr(id: C4ResStrTableKey::IDS_NET_NOJOIN_BADREF, args&: szError).c_str(),
1011 szCaption: strNoJoin.getData(),
1012 dwButtons: C4GUI::MessageDialog::btnOK,
1013 icoIcon: C4GUI::Ico_Error);
1014 return true;
1015 }
1016 C4Network2Reference *pRef = pRefEntry->GetReference();
1017 const char *szDirectJoinAddress = pRefEntry->GetJoinAddress();
1018 if (!pRef && !(szDirectJoinAddress && *szDirectJoinAddress))
1019 {
1020 // something strange has been selected (e.g., a masterserver entry). Error.
1021 Game.pGUI->ShowMessageModal(
1022 szMessage: LoadResStr(id: C4ResStrTableKey::IDS_NET_NOJOIN_NOREF),
1023 szCaption: strNoJoin.getData(),
1024 dwButtons: C4GUI::MessageDialog::btnOK,
1025 icoIcon: C4GUI::Ico_Error);
1026 return true;
1027 }
1028 // check if join to this reference is possible at all
1029 if (pRef)
1030 {
1031 // version mismatch
1032 C4GameVersion verThis;
1033 if (!(pRef->getGameVersion() == verThis))
1034 {
1035 Game.pGUI->ShowMessageModal(
1036 szMessage: LoadResStr(id: C4ResStrTableKey::IDS_NET_NOJOIN_BADVER,
1037 args: pRef->getGameVersion().GetString(),
1038 args: verThis.GetString()).c_str(),
1039 szCaption: strNoJoin.getData(),
1040 dwButtons: C4GUI::MessageDialog::btnOK,
1041 icoIcon: C4GUI::Ico_Error);
1042 return true;
1043 }
1044 // no runtime join
1045 if (!pRef->isJoinAllowed())
1046 {
1047 if (!Game.pGUI->ShowMessageModal(
1048 szMessage: LoadResStr(id: C4ResStrTableKey::IDS_NET_NOJOIN_NORUNTIME),
1049 szCaption: strNoJoin.getData(),
1050 dwButtons: C4GUI::MessageDialog::btnYes | C4GUI::MessageDialog::btnNo,
1051 icoIcon: C4GUI::Ico_Error))
1052 {
1053 return true;
1054 }
1055 }
1056 }
1057 // OK; joining! Take over reference
1058 pRefEntry->GrabReference();
1059 // Set join parameters
1060 *Game.ScenarioFilename = '\0';
1061 if (szDirectJoinAddress) SCopy(szSource: szDirectJoinAddress, sTarget: Game.DirectJoinAddress, _MAX_PATH); else *Game.DirectJoinAddress = '\0';
1062 Game.DefinitionFilenames.push_back(x: "Objects.c4d");
1063 Game.NetworkActive = true;
1064 Game.fObserve = false;
1065 Game.pJoinReference = pRef;
1066 // start with this set!
1067 C4Startup::Get()->Start();
1068 return true;
1069}
1070
1071bool C4StartupNetDlg::DoBack()
1072{
1073 // abort dialog: Back to main
1074 C4Startup::Get()->SwitchDialog(eToDlg: C4Startup::SDID_Back);
1075 return true;
1076}
1077
1078void C4StartupNetDlg::DoRefresh()
1079{
1080 // check min refresh timer
1081 time_t tNow = time(timer: nullptr);
1082 if (tLastRefresh && tNow < tLastRefresh + C4NetMinRefreshInterval)
1083 {
1084 // avoid hammering on refresh key
1085 C4GUI::GUISound(szSound: "Error");
1086 return;
1087 }
1088 tLastRefresh = tNow;
1089 // empty list of all old entries
1090 fUpdatingList = true;
1091 while (pGameSelList->GetFirst()) delete pGameSelList->GetFirst();
1092 pMasterserverClient = nullptr;
1093 // (Re-)Start discovery
1094 if (!DiscoverClient.StartDiscovery())
1095 {
1096 StdStrBuf strNoDiscovery(LoadResStr(id: C4ResStrTableKey::IDS_NET_NODISCOVERY));
1097 Game.pGUI->ShowMessageModal(
1098 szMessage: LoadResStr(id: C4ResStrTableKey::IDS_NET_NODISCOVERY_DESC, args: DiscoverClient.GetError()).c_str(),
1099 szCaption: strNoDiscovery.getData(),
1100 dwButtons: C4GUI::MessageDialog::btnAbort,
1101 icoIcon: C4GUI::Ico_Error);
1102 }
1103 iGameDiscoverInterval = C4NetGameDiscoveryInterval;
1104 // restart masterserver query
1105 UpdateMasterserver();
1106 // done; update stuff
1107 fUpdatingList = false;
1108 UpdateList();
1109}
1110
1111void C4StartupNetDlg::CreateGame()
1112{
1113 C4Startup::Get()->SwitchDialog(eToDlg: C4Startup::SDID_ScenSelNetwork);
1114}
1115
1116void C4StartupNetDlg::OnSec1Timer()
1117{
1118 // no updates if dialog is inactive (e.g., because a join password dlg is shown!)
1119 if (!IsActive(fForKeyboard: true))
1120 return;
1121
1122 // Execute discovery
1123 if (!iGameDiscoverInterval--)
1124 {
1125 DiscoverClient.StartDiscovery();
1126 iGameDiscoverInterval = C4NetGameDiscoveryInterval;
1127 }
1128 DiscoverClient.Execute(iMaxTime: 0);
1129
1130 UpdateList(fGotReference: false);
1131}
1132
1133void C4StartupNetDlg::AddReferenceQuery(const char *szAddress, C4StartupNetListEntry::QueryType eQueryType)
1134{
1135 // Check for an active reference query to the same address
1136 for (C4GUI::Element *pElem = pGameSelList->GetFirst(); pElem; pElem = pElem->GetNext())
1137 {
1138 C4StartupNetListEntry *pEntry = static_cast<C4StartupNetListEntry *>(pElem);
1139 // same address
1140 if (pEntry->IsSameRefQueryAddress(szJoinaddress: szAddress))
1141 {
1142 // nothing to do, xcept maybe select it
1143 if (eQueryType == C4StartupNetListEntry::NRQT_DirectJoin) pGameSelList->SelectEntry(pNewSel: pEntry, fByUser: true);
1144 return;
1145 }
1146 }
1147 // No reference from same host found - create a new entry
1148 C4StartupNetListEntry *pEntry = new C4StartupNetListEntry(pGameSelList, nullptr, this);
1149 pEntry->SetRefQuery(szAddress, eQueryType);
1150 if (eQueryType == C4StartupNetListEntry::NRQT_DirectJoin)
1151 pGameSelList->SelectEntry(pNewSel: pEntry, fByUser: true);
1152 else if (fIsCollapsed)
1153 pEntry->UpdateCollapsed(fToCollapseValue: true);
1154}
1155
1156void C4StartupNetDlg::OnReferenceEntryAdd(C4StartupNetListEntry *pEntry)
1157{
1158 // collapse the new entry if desired
1159 if (fIsCollapsed && pEntry != pGameSelList->GetSelectedItem())
1160 pEntry->UpdateCollapsed(fToCollapseValue: true);
1161}
1162
1163void C4StartupNetDlg::OnChatTitleChange(const StdStrBuf &sNewTitle)
1164{
1165 // update label
1166 if (pChatTitleLabel) pChatTitleLabel->SetText(szText: std::format(fmt: "{} - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_DLG_CHAT), args: sNewTitle.getData()).c_str());
1167}
1168