1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2004, Sven2
6 * Copyright (c) 2013-2016, 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// dialogs for network information
20
21#include "C4GuiResource.h"
22#include "C4GuiTabular.h"
23#include "C4Include.h"
24#include "C4Network2Dialogs.h"
25
26#include "C4Network2.h"
27#include "C4Network2Stats.h"
28#include "C4Game.h"
29#include "C4Viewport.h"
30#include "C4GameOptions.h"
31
32#include <format>
33
34#ifndef _WIN32
35#include <sys/socket.h>
36#include <netinet/in.h>
37#include <arpa/inet.h>
38#endif
39
40// C4Network2ClientDlg
41
42C4Network2ClientDlg::C4Network2ClientDlg(int iForClientID)
43 : iClientID(iForClientID), C4GUI::InfoDialog(LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENT_INFO), 10)
44{
45 // initial text update
46 UpdateText();
47}
48
49void C4Network2ClientDlg::UpdateText()
50{
51 // begin updating (clears previous text)
52 BeginUpdateText();
53 // get core
54 const C4Client *pClient = Game.Clients.getClientByID(iID: iClientID);
55 if (!pClient)
56 {
57 // client ID unknown
58 AddLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENT_INFO_UNKNOWNID, args&: iClientID).c_str());
59 }
60 else
61 {
62 // get client (may be nullptr for local info)
63 C4Network2Client *pNetClient = pClient->getNetClient();
64 // show some info
65 const std::string_view activated{LoadResStrChoice(condition: pClient->isActivated(), ifTrue: C4ResStrTableKey::IDS_MSG_ACTIVE, ifFalse: C4ResStrTableKey::IDS_MSG_INACTIVE)};
66 const std::string_view local{LoadResStrChoice(condition: pClient->isLocal(), ifTrue: C4ResStrTableKey::IDS_MSG_LOCAL, ifFalse: C4ResStrTableKey::IDS_MSG_REMOTE)};
67 const std::string_view host{LoadResStrChoice(condition: pClient->isHost(), ifTrue: C4ResStrTableKey::IDS_MSG_HOST, ifFalse: C4ResStrTableKey::IDS_MSG_CLIENT)};
68 AddLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENT_INFO_FORMAT,
69 args: activated.data(), args: local.data(), args: host.data(),
70 args: pClient->getName(), args&: iClientID,
71 args: Game.Network.isHost() && pNetClient && !pNetClient->isReady() ? " (!ack)" : "").c_str());
72 // show addresses
73 int iCnt;
74 if (iCnt = pNetClient->getAddrCnt())
75 {
76 AddLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENT_INFO_ADDRESSES));
77 for (int i = 0; i < iCnt; ++i)
78 {
79 C4Network2Address addr = pNetClient->getAddr(i);
80 AddLine(szText: std::format(fmt: " {}: {}",
81 args&: i, // adress index
82 args: addr.ToString()).c_str());
83 }
84 }
85 else
86 AddLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENT_INFO_NOADDRESSES));
87 // show connection
88 if (pNetClient)
89 {
90 // connections
91 if (pNetClient->isConnected())
92 {
93 AddLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENT_INFO_CONNECTIONS,
94 args: pNetClient->getMsgConn() == pNetClient->getDataConn() ? "Msg/Data" : "Msg",
95 args: Game.Network.NetIO.getNetIOName(pNetIO: pNetClient->getMsgConn()->getNetClass()),
96 args: pNetClient->getMsgConn()->getPeerAddr().ToString(),
97 args: pNetClient->getMsgConn()->getPingTime()).c_str());
98 if (pNetClient->getMsgConn() != pNetClient->getDataConn())
99 AddLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENT_INFO_CONNDATA,
100 args: Game.Network.NetIO.getNetIOName(pNetIO: pNetClient->getDataConn()->getNetClass()),
101 args: pNetClient->getDataConn()->getPeerAddr().ToString(),
102 args: pNetClient->getDataConn()->getPingTime()).c_str());
103 }
104 else
105 AddLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENT_INFO_NOCONNECTIONS));
106 }
107 }
108 // update done
109 EndUpdateText();
110}
111
112// C4Network2ClientListBox::ClientListItem
113
114C4Network2ClientListBox::ClientListItem::ClientListItem(class C4Network2ClientListBox *pForDlg, int iClientID)
115 : ListItem{pForDlg, iClientID}, pStatusIcon{nullptr}, pName{nullptr}, pPing{nullptr}, pMuteBtn{nullptr}, pActivateBtn{nullptr}, pKickBtn{nullptr}, fShownActive{false}
116{
117 // get associated client
118 const C4Client *pClient = GetClient();
119 if (!pClient) return;
120
121 // get size
122 int iIconSize = C4GUI::GetRes()->TextFont.GetLineHeight();
123 bool startup{pForDlg->IsStartup()};
124
125 if (startup) iIconSize *= 2;
126 int iWidth = pForDlg->GetItemWidth();
127 int iVerticalIndent = 2;
128 SetBounds(C4Rect(0, 0, iWidth, iIconSize + 2 * iVerticalIndent));
129 C4GUI::ComponentAligner ca(GetContainedClientRect(), 0, iVerticalIndent);
130 // create subcomponents
131 bool fIsHost{pClient->isHost()};
132 pStatusIcon = new C4GUI::Icon(ca.GetFromLeft(iWdt: iIconSize), fIsHost ? C4GUI::Ico_Host : C4GUI::Ico_Client);
133
134 bool local{pClient->isLocal()};
135 const std::string nameLabel{GetNameLabel()};
136
137 pName = new C4GUI::Label(nameLabel.c_str(), iIconSize + IconLabelSpacing, iVerticalIndent, ALeft);
138
139 auto pos = 0;
140 if (Game.Network.isHost() && !fIsHost)
141 {
142 // activate/deactivate and kick btns for clients at host
143 pKickBtn = new C4GUI::CallbackButtonEx<C4Network2ClientListBox::ClientListItem, C4GUI::IconButton>(C4GUI::Ico_Kick, GetToprightCornerRect(iWidth: (std::max)(a: iIconSize, b: 16), iHeight: (std::max)(a: iIconSize, b: 16), iHIndent: 2, iVIndent: 1, iIndexX: pos++), 0, this, &ClientListItem::OnButtonKick);
144 pKickBtn->SetToolTip(LoadResStrNoAmp(id: C4ResStrTableKey::IDS_NET_KICKCLIENT).c_str());
145
146 if (!startup)
147 {
148 pActivateBtn = new C4GUI::CallbackButtonEx<C4Network2ClientListBox::ClientListItem, C4GUI::IconButton>(C4GUI::Ico_Active, GetToprightCornerRect(iWidth: (std::max)(a: iIconSize, b: 16), iHeight: (std::max)(a: iIconSize, b: 16), iHIndent: 2, iVIndent: 1, iIndexX: pos++), 0, this, &ClientListItem::OnButtonActivate);
149 fShownActive = true;
150 }
151 }
152
153 if (!local && !startup)
154 {
155 // mute button
156 pMuteBtn = new C4GUI::CallbackButtonEx<C4Network2ClientListBox::ClientListItem, C4GUI::IconButton>{C4GUI::Ico_Sound, GetToprightCornerRect(iWidth: (std::max)(a: iIconSize, b: 16), iHeight: (std::max)(a: iIconSize, b: 16), iHIndent: 2, iVIndent: 1, iIndexX: pos++), 0, this, &ClientListItem::OnButtonToggleMute};
157 pMuteBtn->SetToolTip(LoadResStrNoAmpChoice(condition: pClient && pClient->isMuted(), ifTrue: C4ResStrTableKey::IDS_NET_UNMUTE_DESC, ifFalse: C4ResStrTableKey::IDS_NET_MUTE_DESC).c_str());
158
159 // wait time
160 pPing = new C4GUI::Label("???", GetBounds().Wdt - IconLabelSpacing - pos * 24, iVerticalIndent, ARight);
161 pPing->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_CONTROLWAITTIME));
162 }
163
164 // add components
165 AddElement(pChild: pStatusIcon); AddElement(pChild: pName);
166 if (pPing) AddElement(pChild: pPing);
167 if (pMuteBtn) AddElement(pChild: pMuteBtn);
168 if (pActivateBtn) AddElement(pChild: pActivateBtn);
169 if (pKickBtn) AddElement(pChild: pKickBtn);
170 // add to listbox (will eventually get moved)
171 pForDlg->AddElement(pChild: this);
172 // first-time update
173 Update();
174}
175
176void C4Network2ClientListBox::ClientListItem::Update()
177{
178 // update wait label
179 if (pPing)
180 {
181 int iWait = Game.Control.Network.ClientPerfStat(iClientID);
182 pPing->SetText(szText: (std::to_string(val: iWait) + " ms").c_str());
183 pPing->SetColor(dwToClr: RGB(
184 r: BoundBy(bval: 255 - Abs(val: iWait) * 5, lbound: 0, rbound: 255),
185 g: BoundBy(bval: 255 - iWait * 5, lbound: 0, rbound: 255),
186 b: BoundBy(bval: 255 + iWait * 5, lbound: 0, rbound: 255)));
187 }
188 // update activation status
189 const C4Client *pClient = GetClient(); if (!pClient) return;
190
191 bool fIsActive = pClient->isActivated();
192 if (fIsActive != fShownActive)
193 {
194 fShownActive = fIsActive;
195 if (!pClient->isHost()) pStatusIcon->SetIcon(fIsActive ? C4GUI::Ico_Client : C4GUI::Ico_ObserverClient);
196 if (pActivateBtn)
197 {
198 pActivateBtn->SetIcon(fIsActive ? C4GUI::Ico_Active : C4GUI::Ico_Inactive);
199 pActivateBtn->SetToolTip(LoadResStrNoAmpChoice(condition: fIsActive, ifTrue: C4ResStrTableKey::IDS_NET_DEACTIVATECLIENT, ifFalse: C4ResStrTableKey::IDS_NET_ACTIVATECLIENT).c_str());
200 }
201 }
202 // update players in tooltip
203 StdStrBuf sCltPlrs(Game.PlayerInfos.GetActivePlayerNames(fCountInvisible: false, iAtClientID: iClientID));
204 pName->SetToolTip(sCltPlrs.getData());
205 // update icon: Network status
206 C4GUI::Icons icoStatus = C4GUI::Ico_UnknownClient;
207 C4Network2Client *pClt = pClient->getNetClient();
208 if (pClt)
209 {
210 switch (pClt->getStatus())
211 {
212 case NCS_Joining: // waiting for join data
213 case NCS_Chasing: // client is behind (status not acknowledged, isn't waited for)
214 case NCS_NotReady: // client is behind (status not acknowledged)
215 icoStatus = C4GUI::Ico_Loading;
216 break;
217
218 case NCS_Ready: // client acknowledged network status
219 icoStatus = C4GUI::Ico_Ready;
220 break;
221
222 case NCS_Remove: // client is to be removed
223 icoStatus = C4GUI::Ico_Kick;
224 break;
225
226 default: // whatever
227 assert(false);
228 icoStatus = C4GUI::Ico_Loading;
229 break;
230 }
231 }
232 // network OK - control ready?
233 if (!pForDlg->IsStartup() && (icoStatus == C4GUI::Ico_Ready))
234 {
235 if (!Game.Control.Network.ClientReady(iClientID, iTick: Game.Control.ControlTick))
236 {
237 // control not ready
238 icoStatus = C4GUI::Ico_NetWait;
239 }
240 }
241 // set new icon
242 pStatusIcon->SetIcon(icoStatus);
243}
244
245C4Client *C4Network2ClientListBox::ClientListItem::GetClient() const
246{
247 return Game.Clients.getClientByID(iID: iClientID);
248}
249
250void C4Network2ClientListBox::ClientListItem::OnButtonToggleMute(C4GUI::Control *pButton)
251{
252 if (auto *client = GetClient(); client)
253 {
254 client->ToggleMuted();
255 UpdateMuteButton();
256 }
257}
258
259void C4Network2ClientListBox::ClientListItem::OnButtonActivate(C4GUI::Control *pButton)
260{
261 // league: Do not deactivate clients with players
262 if (Game.Parameters.isLeague() && Game.Players.GetAtClient(iClient: iClientID))
263 {
264 Log(id: C4ResStrTableKey::IDS_LOG_COMMANDNOTALLOWEDINLEAGUE);
265 return;
266 }
267 // change to status that is not currently shown
268 Game.Control.DoInput(eCtrlType: CID_ClientUpdate, pPkt: new C4ControlClientUpdate(iClientID, CUT_Activate, !fShownActive), eDelivery: CDT_Sync);
269}
270
271void C4Network2ClientListBox::ClientListItem::OnButtonKick(C4GUI::Control *pButton)
272{
273 // try kick
274 // league: Kick needs voting
275 if (Game.Parameters.isLeague() && Game.Players.GetAtClient(iClient: iClientID))
276 Game.Network.Vote(eType: VT_Kick, fApprove: true, iData: iClientID);
277 else
278 Game.Clients.CtrlRemove(pClient: GetClient(), szReason: LoadResStrChoice(condition: pForDlg->IsStartup(), ifTrue: C4ResStrTableKey::IDS_MSG_KICKFROMSTARTUPDLG, ifFalse: C4ResStrTableKey::IDS_MSG_KICKFROMCLIENTLIST));
279}
280
281void C4Network2ClientListBox::ClientListItem::UpdateMuteButton()
282{
283 auto *client = GetClient();
284 pMuteBtn->SetIcon(client->isMuted() ? C4GUI::Ico_NoSound : C4GUI::Ico_Sound);
285 pMuteBtn->SetToolTip(LoadResStrNoAmpChoice(condition: client && client->isMuted(), ifTrue: C4ResStrTableKey::IDS_NET_UNMUTE_DESC, ifFalse: C4ResStrTableKey::IDS_NET_MUTE_DESC).c_str());
286}
287
288std::string C4Network2ClientListBox::ClientListItem::GetNameLabel() const
289{
290 if (auto *client = GetClient(); client)
291 {
292 if (pForDlg->IsStartup())
293 {
294 return client->getName();
295 }
296 else
297 {
298 return std::format(fmt: "{}:{}", args: client->getName(), args: client->getNick());
299 }
300 }
301 else
302 {
303 return "???";
304 }
305}
306
307// C4Network2ClientListBox::ConnectionListItem
308
309C4Network2ClientListBox::ConnectionListItem::ConnectionListItem(class C4Network2ClientListBox *pForDlg, int32_t iClientID, std::size_t iConnectionID)
310 : ListItem(pForDlg, iClientID), iConnID(iConnectionID), pDesc(nullptr), pPing(nullptr), pDisconnectBtn(nullptr)
311{
312 // get size
313 CStdFont &rUseFont = C4GUI::GetRes()->TextFont;
314 int iIconSize = rUseFont.GetLineHeight();
315 int iWidth = pForDlg->GetItemWidth();
316 int iVerticalIndent = 2;
317 SetBounds(C4Rect(0, 0, iWidth, iIconSize + 2 * iVerticalIndent));
318 C4GUI::ComponentAligner ca(GetContainedClientRect(), 0, iVerticalIndent);
319 // left indent
320 ca.ExpandLeft(iByWdt: -iIconSize * 2);
321 // create subcomponents
322 // disconnect button
323 if (!Game.Parameters.isLeague())
324 {
325 pDisconnectBtn = new C4GUI::CallbackButtonEx<C4Network2ClientListBox::ConnectionListItem, C4GUI::IconButton>(C4GUI::Ico_Disconnect, ca.GetFromRight(iWdt: iIconSize, iHgt: iIconSize), 0, this, &ConnectionListItem::OnButtonDisconnect);
326 pDisconnectBtn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_MENU_DISCONNECT));
327 }
328 else
329 pDisconnectBtn = nullptr;
330 // ping time
331 int32_t sx = 40, sy = iIconSize;
332 rUseFont.GetTextExtent(szText: "???? ms", rsx&: sx, rsy&: sy, fCheckMarkup: true);
333 pPing = new C4GUI::Label("???", ca.GetFromRight(iWdt: sx, iHgt: sy), ARight);
334 pPing->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_NET_CONTROL_PING));
335 // main description item
336 pDesc = new C4GUI::Label("???", ca.GetAll(), ALeft);
337 // add components
338 AddElement(pChild: pDesc);
339 AddElement(pChild: pPing);
340 if (pDisconnectBtn) AddElement(pChild: pDisconnectBtn);
341 // add to listbox (will eventually get moved)
342 pForDlg->AddElement(pChild: this);
343 // first-time update
344 Update();
345}
346
347C4Network2IOConnection *C4Network2ClientListBox::ConnectionListItem::GetConnection() const
348{
349 // get connection by connection ID
350 C4Network2Client *pNetClient = Game.Network.Clients.GetClientByID(iID: iClientID);
351 if (!pNetClient) return nullptr;
352 if (iConnID == 0) return pNetClient->getDataConn();
353 if (iConnID == 1) return pNetClient->getMsgConn();
354 return nullptr;
355}
356
357void C4Network2ClientListBox::ConnectionListItem::Update()
358{
359 C4Network2IOConnection *pConn = GetConnection();
360 if (!pConn)
361 {
362 // No connection: Shouldn't happen
363 pDesc->SetText(szText: "???");
364 pPing->SetText(szText: "???");
365 return;
366 }
367 // update connection ping
368 int iPing = pConn->getLag();
369 pPing->SetText(szText: (std::to_string(val: iPing) + " ms").c_str());
370
371 // update description
372 // get connection usage
373 const char *szConnType;
374 C4Network2Client *pNetClient = Game.Network.Clients.GetClientByID(iID: iClientID);
375 if (pNetClient->getDataConn() == pNetClient->getMsgConn())
376 szConnType = "Data/Msg";
377 else if (iConnID)
378 szConnType = "Msg";
379 else
380 szConnType = "Data";
381 // display info
382 pDesc->SetText(szText: std::format(fmt: "{}: {} ({} l{})",
383 args&: szConnType,
384 args: Game.Network.NetIO.getNetIOName(pNetIO: pConn->getNetClass()),
385 args: pConn->getPeerAddr().ToString(),
386 args: pConn->getPacketLoss()).c_str());
387}
388
389void C4Network2ClientListBox::ConnectionListItem::OnButtonDisconnect(C4GUI::Control *pButton)
390{
391 // close connection
392 C4Network2IOConnection *pConn = GetConnection();
393 if (pConn)
394 {
395 pConn->Close();
396 }
397}
398
399// C4Network2ClientListBox
400
401C4Network2ClientListBox::C4Network2ClientListBox(C4Rect &rcBounds, bool fStartup) : ListBox(rcBounds), pSec1Timer(nullptr), fStartup(fStartup)
402{
403 // hook into timer callback
404 pSec1Timer = new C4Sec1TimerCallback<C4Network2ClientListBox>(this);
405 // initial update
406 Update();
407}
408
409C4Network2ClientListBox::~C4Network2ClientListBox()
410{
411 pSec1Timer->Release();
412}
413
414void C4Network2ClientListBox::Update()
415{
416 // sync with client list
417 ListItem *pItem = static_cast<ListItem *>(pClientWindow->GetFirst()), *pNext;
418 const C4Client *pClient = nullptr;
419 while (pClient = Game.Clients.getClient(pAfter: pClient))
420 {
421 // skip host in startup board
422 if (IsStartup() && pClient->isHost()) continue;
423 // deleted client(s) present? this will also delete unneeded client connections of previous client
424 while (pItem && (pItem->GetClientID() < pClient->getID()))
425 {
426 pNext = static_cast<ListItem *>(pItem->GetNext());
427 delete pItem; pItem = pNext;
428 }
429 // same present for update?
430 // need not check for connection ID, because a client item will always be placed before the corresponding connection items
431 if (pItem && pItem->GetClientID() == pClient->getID())
432 {
433 pItem->Update();
434 pItem = static_cast<ListItem *>(pItem->GetNext());
435 }
436 else
437 // not present: insert (or add if pItem=nullptr)
438 InsertElement(pChild: new ClientListItem(this, pClient->getID()), pInsertBefore: pItem);
439 // update connections for client
440 // but no connections in startup board
441 if (IsStartup()) continue;
442 // enumerate client connections
443 C4Network2Client *pNetClient = pClient->getNetClient();
444 if (!pNetClient) continue; // local client does not have connections
445 C4Network2IOConnection *pLastConn = nullptr;
446 for (std::size_t i = 0; i < 2; ++i)
447 {
448 C4Network2IOConnection *pConn = i ? pNetClient->getMsgConn() : pNetClient->getDataConn();
449 if (!pConn) continue;
450 if (pConn == pLastConn) continue; // combined connection: Display only one
451 pLastConn = pConn;
452 // del leading items
453 while (pItem && ((pItem->GetClientID() < pClient->getID()) || ((pItem->GetClientID() == pClient->getID()) && (pItem->GetConnectionID() < i))))
454 {
455 pNext = static_cast<ListItem *>(pItem->GetNext());
456 delete pItem; pItem = pNext;
457 }
458 // update connection item
459 if (pItem && pItem->GetClientID() == pClient->getID() && pItem->GetConnectionID() == i)
460 {
461 pItem->Update();
462 pItem = static_cast<ListItem *>(pItem->GetNext());
463 }
464 else
465 {
466 // new connection: create it
467 InsertElement(pChild: new ConnectionListItem(this, pClient->getID(), i), pInsertBefore: pItem);
468 }
469 }
470 }
471 // del trailing items
472 while (pItem)
473 {
474 pNext = static_cast<ListItem *>(pItem->GetNext());
475 delete pItem; pItem = pNext;
476 }
477}
478
479// C4Network2ClientListDlg
480
481namespace
482{
483 C4KeyBinding *registerStrongerEscape(C4GUI::Dialog *dialog)
484 {
485 C4CustomKey::CodeList keys;
486 keys.push_back(x: C4KeyCodeEx(K_ESCAPE));
487 if (Config.Controls.GamepadGuiControl)
488 {
489 keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_AnyHighButton)));
490 }
491 return new C4KeyBinding(keys, "GUIDialogAbort", KEYSCOPE_Fullscreen, new C4GUI::DlgKeyCB<C4GUI::Dialog>(*dialog, &C4GUI::Dialog::KeyEscape), C4CustomKey::PRIO_Dlg);
492 }
493}
494
495// singleton
496C4Network2ClientListDlg *C4Network2ClientListDlg::pInstance = nullptr;
497
498C4Network2ClientListDlg::C4Network2ClientListDlg()
499 : Dialog(Game.pGUI->GetPreferredDlgRect().Wdt * 3 / 4, Game.pGUI->GetPreferredDlgRect().Hgt * 3 / 4, LoadResStr(id: C4ResStrTableKey::IDS_NET_CAPTION), false)
500{
501 // component layout
502 CStdFont *pUseFont = &C4GUI::GetRes()->TextFont;
503 C4GUI::ComponentAligner caAll(GetContainedClientRect(), 0, 0);
504 C4Rect rcStatus = caAll.GetFromBottom(iHgt: pUseFont->GetLineHeight());
505 // create game options; max 1/2 of dialog height
506 pGameOptions = new C4GameOptionsList(caAll.GetFromTop(iHgt: caAll.GetInnerHeight() / 2), true, true);
507 pGameOptions->SetDecoration(fDrawBG: false, pToGfx: nullptr, fAutoScroll: true, fDrawBorder: false);
508 pGameOptions->SetSelectionDiabled();
509 // but resize to actually used height
510 int32_t iFreedHeight = pGameOptions->ContractToElementHeight();
511 caAll.ExpandTop(iByHgt: iFreedHeight);
512 AddElement(pChild: pGameOptions);
513 // create client list box
514 AddElement(pChild: pListBox = new C4Network2ClientListBox(caAll.GetAll(), false));
515 // create status label
516 AddElement(pChild: pStatusLabel = new C4GUI::Label("", rcStatus));
517
518 keyEscape.reset(p: registerStrongerEscape(dialog: this));
519 // add timer
520 pSec1Timer = new C4Sec1TimerCallback<C4Network2ClientListDlg>(this);
521 // initial update
522 Update();
523}
524
525C4Network2ClientListDlg::~C4Network2ClientListDlg()
526{
527 if (this == pInstance) pInstance = nullptr;
528 pSec1Timer->Release();
529}
530
531void C4Network2ClientListDlg::Update()
532{
533 // Compose status text
534 const std::string statusText{std::format(fmt: "Tick {}, Behind {}, Rate {}, PreSend {}, ACT: {}",
535 args&: Game.Control.ControlTick, args: Game.Control.Network.GetBehind(iTick: Game.Control.ControlTick),
536 args&: Game.Control.ControlRate, args: Game.Control.Network.getControlPreSend(),
537 args: Game.Control.Network.getAvgControlSendTime())};
538 // Update status label
539 pStatusLabel->SetText(szText: statusText.c_str());
540}
541
542bool C4Network2ClientListDlg::Toggle()
543{
544 // safety
545 if (!C4GUI::IsGUIValid()) return false;
546 // toggle off?
547 if (pInstance) { pInstance->Close(fOK: true); return true; }
548 // toggle on!
549 return Game.pGUI->ShowRemoveDlg(pDlg: pInstance = new C4Network2ClientListDlg());
550}
551
552// C4Network2StartWaitDlg
553
554C4Network2StartWaitDlg::C4Network2StartWaitDlg()
555 : C4GUI::Dialog(
556 Config.Graphics.ResX > 800 ? DialogWidthLarge : DialogWidth,
557 Config.Graphics.ResY > 600 ? DialogHeightLarge : DialogHeight,
558 LoadResStr(id: C4ResStrTableKey::IDS_NET_CAPTION),
559 false
560 ), pClientListBox(nullptr)
561{
562 C4GUI::ComponentAligner caAll(GetContainedClientRect(), C4GUI_DefDlgIndent, C4GUI_DefDlgIndent);
563 C4GUI::ComponentAligner caButtonArea(caAll.GetFromBottom(C4GUI_ButtonAreaHgt), 0, 0);
564 // create top label
565 C4GUI::Label *pLbl;
566 AddElement(pChild: pLbl = new C4GUI::Label(LoadResStr(id: C4ResStrTableKey::IDS_NET_WAITFORSTART), caAll.GetFromTop(iHgt: 25), ACenter));
567 // create client list box
568 AddElement(pChild: pClientListBox = new C4Network2ClientListBox(caAll.GetAll(), true));
569
570 C4Rect bounds{caButtonArea.GetCentered(iWdt: 2 * C4GUI_DefButton2Wdt + C4GUI_DefButton2HSpace, C4GUI_ButtonHgt)};
571 bounds.Wdt = C4GUI_DefButton2Wdt;
572
573 // place restart button
574 AddElement(pChild: new C4GUI::CallbackButton<C4Network2StartWaitDlg>{LoadResStr(id: C4ResStrTableKey::IDS_BTN_RESTART), bounds, &C4Network2StartWaitDlg::OnBtnRestart, this});
575 bounds.x += C4GUI_DefButton2Wdt + C4GUI_DefButton2HSpace;
576 // place abort button
577 AddElement(pChild: C4GUI::newCancelButton(bounds));
578}
579
580void C4Network2StartWaitDlg::OnBtnRestart(C4GUI::Control *)
581{
582 Application.SetNextMission(Game.ScenarioFilename);
583 UserClose(fOK: false);
584}
585
586// C4GameOptionButtons
587
588C4GameOptionButtons::C4GameOptionButtons(const C4Rect &rcBounds, bool fNetwork, bool fHost, bool fLobby)
589 : C4GUI::Window(), fNetwork(fNetwork), fHost(fHost), fLobby(fLobby), eForceFairCrewState(C4SFairCrew_Free), fCountdown(false)
590{
591 SetBounds(rcBounds);
592 // calculate button size from area
593 int32_t iButtonCount = fNetwork ? fHost ? 6 : 3 : 2;
594 int32_t iIconSize = std::min<int32_t>(C4GUI_IconExHgt, b: rcBounds.Hgt), iIconSpacing = rcBounds.Wdt / (rcBounds.Wdt >= 400 ? 64 : 128);
595 if ((iIconSize + iIconSpacing * 2) * iButtonCount > rcBounds.Wdt)
596 {
597 if (iIconSize * iButtonCount <= rcBounds.Wdt)
598 {
599 iIconSpacing = std::max<int32_t>(a: 0, b: (rcBounds.Wdt - iIconSize * iButtonCount) / (iButtonCount * 2) - 1);
600 }
601 else
602 {
603 iIconSpacing = 0;
604 iIconSize = rcBounds.Wdt / iButtonCount;
605 }
606 }
607 C4GUI::ComponentAligner caButtonArea(rcBounds, 0, 0, true);
608 C4GUI::ComponentAligner caButtons(caButtonArea.GetCentered(iWdt: (iIconSize + 2 * iIconSpacing) * iButtonCount, iHgt: iIconSize), iIconSpacing, 0);
609 // add buttons
610 if (fNetwork && fHost)
611 {
612 bool fIsInternet = !!Config.Network.MasterServerSignUp, fIsDisabled = false;
613 // league currently implies master server signup, and can thus not be turned off
614 if (fLobby && Game.Parameters.isLeague())
615 {
616 fIsInternet = true;
617 fIsDisabled = true;
618 }
619 btnInternet = new C4GUI::CallbackButton<C4GameOptionButtons, C4GUI::IconButton>(fIsInternet ? C4GUI::Ico_Ex_InternetOn : C4GUI::Ico_Ex_InternetOff, caButtons.GetFromLeft(iWdt: iIconSize, iHgt: iIconSize), 'I' /* 2do */, &C4GameOptionButtons::OnBtnInternet, this);
620 btnInternet->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_STARTINTERNETGAME));
621 btnInternet->SetEnabled(!fIsDisabled);
622 AddElement(pChild: btnInternet);
623 }
624 else btnInternet = nullptr;
625 bool fIsLeague = false;
626 if (fNetwork)
627 {
628 C4GUI::Icons eLeagueIcon;
629 fIsLeague = fLobby ? Game.Parameters.isLeague() : !!Config.Network.LeagueServerSignUp;
630 eLeagueIcon = fIsLeague ? C4GUI::Ico_Ex_LeagueOn : C4GUI::Ico_Ex_LeagueOff;
631 btnLeague = new C4GUI::CallbackButton<C4GameOptionButtons, C4GUI::IconButton>(eLeagueIcon, caButtons.GetFromLeft(iWdt: iIconSize, iHgt: iIconSize), 'L' /* 2do */, &C4GameOptionButtons::OnBtnLeague, this);
632 btnLeague->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_STARTLEAGUEGAME));
633 btnLeague->SetEnabled(fHost && !fLobby);
634 AddElement(pChild: btnLeague);
635 }
636 else btnLeague = nullptr;
637 if (fNetwork && fHost)
638 {
639 btnPassword = new C4GUI::CallbackButton<C4GameOptionButtons, C4GUI::IconButton>(Game.Network.isPassworded() ? C4GUI::Ico_Ex_Locked : C4GUI::Ico_Ex_Unlocked, caButtons.GetFromLeft(iWdt: iIconSize, iHgt: iIconSize), 'P' /* 2do */, &C4GameOptionButtons::OnBtnPassword, this);
640 btnPassword->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_NET_PASSWORD_DESC));
641 AddElement(pChild: btnPassword);
642 btnComment = new C4GUI::CallbackButton<C4GameOptionButtons, C4GUI::IconButton>(C4GUI::Ico_Ex_Comment, caButtons.GetFromLeft(iWdt: iIconSize, iHgt: iIconSize), 'M' /* 2do */, &C4GameOptionButtons::OnBtnComment, this);
643 btnComment->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_COMMENTDESCRIPTIONFORTHIS));
644 AddElement(pChild: btnComment);
645 }
646 else btnPassword = btnComment = nullptr;
647 btnFairCrew = new C4GUI::CallbackButton<C4GameOptionButtons, C4GUI::IconButton>(C4GUI::Ico_Ex_NormalCrew, caButtons.GetFromLeft(iWdt: iIconSize, iHgt: iIconSize), 'F' /* 2do */, &C4GameOptionButtons::OnBtnFairCrew, this);
648 btnRecord = new C4GUI::CallbackButton<C4GameOptionButtons, C4GUI::IconButton>(Config.General.Record || fIsLeague ? C4GUI::Ico_Ex_RecordOn : C4GUI::Ico_Ex_RecordOff, caButtons.GetFromLeft(iWdt: iIconSize, iHgt: iIconSize), 'R' /* 2do */, &C4GameOptionButtons::OnBtnRecord, this);
649 btnRecord->SetEnabled(!fIsLeague);
650 btnRecord->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_RECORD));
651 AddElement(pChild: btnFairCrew);
652 AddElement(pChild: btnRecord);
653 UpdateFairCrewBtn();
654}
655
656void C4GameOptionButtons::OnBtnInternet(C4GUI::Control *btn)
657{
658 if (!fNetwork || !fHost) return;
659 bool fCheck = Config.Network.MasterServerSignUp = !Config.Network.MasterServerSignUp;
660 // in lobby mode, do actual termination from masterserver
661 if (fLobby)
662 {
663 if (fCheck)
664 {
665 fCheck = Game.Network.LeagueSignupEnable();
666 }
667 else
668 {
669 Game.Network.LeagueSignupDisable();
670 }
671 }
672 btnInternet->SetIcon(fCheck ? C4GUI::Ico_Ex_InternetOn : C4GUI::Ico_Ex_InternetOff);
673 // also update league button, because turning off masterserver also turns off the league
674 if (!fCheck)
675 {
676 Config.Network.LeagueServerSignUp = false;
677 btnLeague->SetIcon(C4GUI::Ico_Ex_LeagueOff);
678 }
679 // re-set in config for the case of failure
680 Config.Network.MasterServerSignUp = fCheck;
681}
682
683void C4GameOptionButtons::OnBtnLeague(C4GUI::Control *btn)
684{
685 if (!fNetwork || !fHost) return;
686 bool fCheck = Config.Network.LeagueServerSignUp = !Config.Network.LeagueServerSignUp;
687 btnLeague->SetIcon(fCheck ? C4GUI::Ico_Ex_LeagueOn : C4GUI::Ico_Ex_LeagueOff);
688 btnRecord->SetIcon(fCheck || Config.General.Record ? C4GUI::Ico_Ex_RecordOn : C4GUI::Ico_Ex_RecordOff);
689 btnRecord->SetEnabled(!fCheck);
690 // if the league is turned on, the game must be signed up at the masterserver
691 if (fCheck && !Config.Network.MasterServerSignUp) OnBtnInternet(btn: btnInternet);
692}
693
694void C4GameOptionButtons::OnBtnFairCrew(C4GUI::Control *btn)
695{
696 if (!fHost) return;
697 if (fLobby)
698 {
699 // altering button in lobby: Must be distributed as a control to all clients
700 if (Game.Parameters.FairCrewForced) return;
701 Game.Control.DoInput(eCtrlType: CID_Set, pPkt: new C4ControlSet(C4CVT_FairCrew, Game.Parameters.UseFairCrew ? -1 : Config.General.FairCrewStrength), eDelivery: CDT_Sync);
702 // button will be updated through control
703 }
704 else
705 {
706 // altering scenario selection setting: Simply changes config setting
707 if (eForceFairCrewState != C4SFairCrew_Free) return;
708 Config.General.FairCrew = !Config.General.FairCrew;
709 UpdateFairCrewBtn();
710 }
711}
712
713void C4GameOptionButtons::OnBtnRecord(C4GUI::Control *btn)
714{
715 btnRecord->SetIcon((Config.General.Record = !Config.General.Record) ? C4GUI::Ico_Ex_RecordOn : C4GUI::Ico_Ex_RecordOff);
716}
717
718void C4GameOptionButtons::OnBtnPassword(C4GUI::Control *btn)
719{
720 if (!fNetwork || !fHost) return;
721 // password is currently set - a single click only clears the password
722 if (Game.Network.GetPassword() && *Game.Network.GetPassword())
723 {
724 StdStrBuf empty;
725 OnPasswordSet(rsNewPassword: empty);
726 return;
727 }
728 // password button pressed: Show dialog to set/change current password
729 C4GUI::InputDialog *pDlg;
730 GetScreen()->ShowRemoveDlg(pDlg: pDlg = new C4GUI::InputDialog(LoadResStr(id: C4ResStrTableKey::IDS_MSG_ENTERPASSWORD), LoadResStr(id: C4ResStrTableKey::IDS_DLG_PASSWORD), C4GUI::Ico_Ex_LockedFrontal, new C4GUI::InputCallback<C4GameOptionButtons>(this, &C4GameOptionButtons::OnPasswordSet), false));
731 pDlg->SetMaxText(CFG_MaxString);
732 const char *szPassPreset = Game.Network.GetPassword();
733 if (!szPassPreset || !*szPassPreset) szPassPreset = Config.Network.LastPassword;
734 if (*szPassPreset) pDlg->SetInputText(szPassPreset);
735}
736
737void C4GameOptionButtons::OnPasswordSet(const StdStrBuf &rsNewPassword)
738{
739 // password input dialog answered with OK: Set/clear network password
740 const char *szPass;
741 Game.Network.SetPassword(szPass = rsNewPassword.getData());
742 // update icon to reflect if a password is set
743 UpdatePasswordBtn();
744 // remember password for next round
745 bool fHasPassword = (szPass && *szPass);
746 if (fHasPassword)
747 {
748 SCopy(szSource: szPass, sTarget: Config.Network.LastPassword, iMaxL: CFG_MaxString);
749 }
750 // acoustic feedback
751 C4GUI::GUISound(szSound: "Connect");
752}
753
754void C4GameOptionButtons::UpdatePasswordBtn()
755{
756 // update icon to reflect if a password is set
757 const char *szPass = Game.Network.GetPassword();
758 bool fHasPassword = szPass && *szPass;
759 btnPassword->SetIcon(fHasPassword ? C4GUI::Ico_Ex_Locked : C4GUI::Ico_Ex_Unlocked);
760}
761
762void C4GameOptionButtons::OnBtnComment(C4GUI::Control *btn)
763{
764 // password button pressed: Show dialog to set/change current password
765 C4GUI::InputDialog *pDlg;
766 GetScreen()->ShowRemoveDlg(pDlg: pDlg = new C4GUI::InputDialog(LoadResStr(id: C4ResStrTableKey::IDS_CTL_ENTERCOMMENT), LoadResStr(id: C4ResStrTableKey::IDS_CTL_COMMENT), C4GUI::Ico_Ex_Comment, new C4GUI::InputCallback<C4GameOptionButtons>(this, &C4GameOptionButtons::OnCommentSet), false));
767 pDlg->SetMaxText(C4MaxComment);
768 pDlg->SetInputText(Config.Network.Comment.getData());
769}
770
771void C4GameOptionButtons::OnCommentSet(const StdStrBuf &rsNewComment)
772{
773 // check for change; no reference invalidation if not changed
774 if (rsNewComment == Config.Network.Comment) return;
775 // Set in configuration, update reference
776 Config.Network.Comment.CopyValidated(szFromVal: rsNewComment.getData());
777 Game.Network.InvalidateReference();
778 // message feedback
779 Log(id: C4ResStrTableKey::IDS_NET_COMMENTCHANGED);
780 // acoustic feedback
781 C4GUI::GUISound(szSound: "Connect");
782}
783
784void C4GameOptionButtons::SetForceFairCrewState(C4SForceFairCrew eToState)
785{
786 eForceFairCrewState = eToState;
787 UpdateFairCrewBtn();
788}
789
790void C4GameOptionButtons::SetCountdown(bool fToVal)
791{
792 fCountdown = fToVal;
793 UpdateFairCrewBtn();
794}
795
796void C4GameOptionButtons::UpdateFairCrewBtn()
797{
798 if (!btnFairCrew) return;
799 bool fFairCrew, fChoiceFree;
800 if (fLobby)
801 {
802 // the host may change the fair crew state unless countdown is running (so noone is tricked into an unfair-crew-game) or the scenario fixes the setting
803 fChoiceFree = !fCountdown && fHost && !Game.Parameters.FairCrewForced;
804 fFairCrew = Game.Parameters.UseFairCrew;
805 }
806 else
807 {
808 fChoiceFree = (eForceFairCrewState == C4SFairCrew_Free);
809 fFairCrew = fChoiceFree ? !!Config.General.FairCrew : (eForceFairCrewState == C4SFairCrew_FairCrew);
810 }
811 btnFairCrew->SetIcon(fChoiceFree ?
812 (!fFairCrew ? C4GUI::Ico_Ex_NormalCrew : C4GUI::Ico_Ex_FairCrew) // fair crew setting by user
813 : (!fFairCrew ? C4GUI::Ico_Ex_NormalCrewGray : C4GUI::Ico_Ex_FairCrewGray)); // fair crew setting by scenario preset or host
814 btnFairCrew->SetToolTip(LoadResStrChoice(condition: fFairCrew, ifTrue: C4ResStrTableKey::IDS_CTL_FAIRCREW_DESC, ifFalse: C4ResStrTableKey::IDS_CTL_NORMALCREW_DESC));
815 btnFairCrew->SetEnabled(fChoiceFree);
816}
817
818// C4Chart
819
820int GetValueDecade(int iVal)
821{
822 // get enclosing decade
823 int iDec = 1;
824 while (iVal) { iVal /= 10; iDec *= 10; }
825 return iDec;
826}
827
828int GetAxisStepRange(int iRange, int iMaxSteps)
829{
830 // try in steps of 5s and 10s
831 int iDec = GetValueDecade(iVal: iRange);
832 if (iDec == 1) return 1;
833 int iNextStepDivider = 2;
834 while (iDec >= iNextStepDivider && iNextStepDivider * iRange / iDec <= iMaxSteps)
835 {
836 iDec /= iNextStepDivider;
837 iNextStepDivider = 7 - iNextStepDivider;
838 }
839 return iDec;
840}
841
842void C4Chart::DrawElement(C4FacetEx &cgo)
843{
844 typedef C4Graph::ValueType ValueType;
845 typedef C4Graph::TimeType TimeType;
846 // transparent w/o graph
847 if (!pDisplayGraph) return;
848 int iSeriesCount = pDisplayGraph->GetSeriesCount();
849 if (!iSeriesCount) return;
850 assert(iSeriesCount > 0);
851 std::string sbuf;
852 pDisplayGraph->Update(); // update averages, etc.
853 // calc metrics
854 CStdFont &rFont = C4GUI::GetRes()->MiniFont;
855 int YAxisWdt = 5,
856 XAxisHgt = 15;
857 const int AxisArrowLen = 6,
858 AxisMarkerLen = 5,
859 AxisArrowThickness = 3,
860 AxisArrowIndent = 2; // margin between axis arrow and last value
861 int32_t YAxisMinStepHgt, XAxisMinStepWdt;
862 // get value range
863 int iMinTime = pDisplayGraph->GetStartTime();
864 int iMaxTime = pDisplayGraph->GetEndTime() - 1;
865 if (iMinTime >= iMaxTime) return;
866 ValueType iMinVal = pDisplayGraph->GetMinValue();
867 ValueType iMaxVal = pDisplayGraph->GetMaxValue();
868 if (iMinVal == iMaxVal) ++iMaxVal;
869 if (iMinVal > 0 && iMaxVal / iMinVal >= 2) iMinVal = 0; // go zero-based if this creates less than 50% unused space
870 else if (iMaxVal < 0 && iMinVal / iMaxVal >= 2) iMaxVal = 0;
871 int ddv;
872 if (iMaxVal > 0 && (ddv = GetValueDecade(iVal: int(iMaxVal)) / 50))
873 iMaxVal = ((iMaxVal - (iMaxVal > 0)) / ddv + (iMaxVal > 0)) * ddv;
874 if (iMinVal && (ddv = GetValueDecade(iVal: int(iMinVal)) / 50))
875 iMinVal = ((iMinVal - (iMinVal < 0)) / ddv + (iMinVal < 0)) * ddv;
876 ValueType dv = iMaxVal - iMinVal; TimeType dt = iMaxTime - iMinTime;
877 // axis calculations
878 sbuf = std::format(fmt: "-{}", args: static_cast<int>((std::max)(a: Abs(val: iMaxVal), b: Abs(val: iMinVal))));
879 rFont.GetTextExtent(szText: sbuf.c_str(), rsx&: XAxisMinStepWdt, rsy&: YAxisMinStepHgt, fCheckMarkup: false);
880 YAxisWdt += XAxisMinStepWdt; XAxisHgt += YAxisMinStepHgt;
881 XAxisMinStepWdt += 2; YAxisMinStepHgt += 2;
882 int tw = rcBounds.Wdt - YAxisWdt;
883 int th = rcBounds.Hgt - XAxisHgt;
884 int tx = rcBounds.x + cgo.TargetX + YAxisWdt;
885 int ty = rcBounds.y + cgo.TargetY;
886 // show a legend, if more than one graph is shown
887 if (iSeriesCount > 1)
888 {
889 int iSeries = 0; const C4Graph *pSeries;
890 int32_t iLegendWdt = 0, Q, W;
891 while (pSeries = pDisplayGraph->GetSeries(iIndex: iSeries++))
892 {
893 rFont.GetTextExtent(szText: pSeries->GetTitle(), rsx&: W, rsy&: Q, fCheckMarkup: true);
894 iLegendWdt = (std::max)(a: iLegendWdt, b: W);
895 }
896 tw -= iLegendWdt + 1;
897 iSeries = 0;
898 int iYLegendDraw = (th - iSeriesCount * Q) / 2 + ty;
899 while (pSeries = pDisplayGraph->GetSeries(iIndex: iSeries++))
900 {
901 lpDDraw->TextOut(szText: pSeries->GetTitle(), rFont, fZoom: 1.0f, sfcDest: cgo.Surface, iTx: tx + tw, iTy: iYLegendDraw, dwFCol: pSeries->GetColorDw() | 0xff000000, byForm: ALeft, fDoMarkup: true);
902 iYLegendDraw += Q;
903 }
904 }
905 // safety: too small?
906 if (tw < 10 || th < 10) return;
907 // draw axis
908 lpDDraw->DrawHorizontalLine(sfcDest: cgo.Surface, x1: tx, x2: tx + tw - 1, y: ty + th, col: CGray3);
909 lpDDraw->DrawLine(sfcTarget: cgo.Surface, x1: tx + tw - 1, y1: ty + th, x2: tx + tw - 1 - AxisArrowLen, y2: ty + th - AxisArrowThickness, byCol: CGray3);
910 lpDDraw->DrawLine(sfcTarget: cgo.Surface, x1: tx + tw - 1, y1: ty + th, x2: tx + tw - 1 - AxisArrowLen, y2: ty + th + AxisArrowThickness, byCol: CGray3);
911 lpDDraw->DrawVerticalLine(sfcDest: cgo.Surface, x: tx, y1: ty, y2: ty + th, col: CGray3);
912 lpDDraw->DrawLine(sfcTarget: cgo.Surface, x1: tx, y1: ty, x2: tx - AxisArrowThickness, y2: ty + AxisArrowLen, byCol: CGray3);
913 lpDDraw->DrawLine(sfcTarget: cgo.Surface, x1: tx, y1: ty, x2: tx + AxisArrowThickness, y2: ty + AxisArrowLen, byCol: CGray3);
914 tw -= AxisArrowLen + AxisArrowIndent;
915 th -= AxisArrowLen + AxisArrowIndent; ty += AxisArrowLen + AxisArrowIndent;
916 // do axis numbering
917 int iXAxisSteps = GetAxisStepRange(iRange: dt, iMaxSteps: tw / XAxisMinStepWdt),
918 iYAxisSteps = GetAxisStepRange(iRange: int(dv), iMaxSteps: th / YAxisMinStepHgt);
919 int iX, iY, iTime, iVal;
920 iTime = ((iMinTime - (iMinTime > 0)) / iXAxisSteps + (iMinTime > 0)) * iXAxisSteps;
921 for (; iTime <= iMaxTime; iTime += iXAxisSteps)
922 {
923 iX = tx + tw * (iTime - iMinTime) / dt;
924 lpDDraw->DrawVerticalLine(sfcDest: cgo.Surface, x: iX, y1: ty + th + 1, y2: ty + th + AxisMarkerLen, col: CGray3);
925 sbuf = std::to_string(val: static_cast<int>(iTime));
926 lpDDraw->TextOut(szText: sbuf.c_str(), rFont, fZoom: 1.0f, sfcDest: cgo.Surface, iTx: iX, iTy: ty + th + AxisMarkerLen, dwFCol: 0xff7f7f7f, byForm: ACenter, fDoMarkup: false);
927 }
928 iVal = int(((iMinVal - (iMinVal > 0)) / iYAxisSteps + (iMinVal > 0)) * iYAxisSteps);
929 for (; iVal <= iMaxVal; iVal += iYAxisSteps)
930 {
931 iY = ty + th - int((iVal - iMinVal) / dv * th);
932 lpDDraw->DrawHorizontalLine(sfcDest: cgo.Surface, x1: tx - AxisMarkerLen, x2: tx - 1, y: iY, col: CGray3);
933 sbuf = std::to_string(val: static_cast<int>(iVal));
934 lpDDraw->TextOut(szText: sbuf.c_str(), rFont, fZoom: 1.0f, sfcDest: cgo.Surface, iTx: tx - AxisMarkerLen, iTy: iY - rFont.GetLineHeight() / 2, dwFCol: 0xff7f7f7f, byForm: ARight, fDoMarkup: false);
935 }
936 // draw graph series(es)
937 int iSeries = 0;
938 while (const C4Graph *pSeries = pDisplayGraph->GetSeries(iIndex: iSeries++))
939 {
940 int iThisMinTime = (std::max)(a: iMinTime, b: pSeries->GetStartTime());
941 int iThisMaxTime = (std::min)(a: iMaxTime, b: pSeries->GetEndTime());
942 bool fAnyVal = false;
943 for (iX = 0; iX < tw; ++iX)
944 {
945 iTime = iMinTime + dt * iX / tw;
946 if (!Inside(ival: iTime, lbound: iThisMinTime, rbound: iThisMaxTime)) continue;
947 int iY2 = int((-pSeries->GetValue(iAtTime: iTime) + iMinVal) * th / dv) + ty + th;
948 if (fAnyVal) lpDDraw->DrawLineDw(sfcTarget: cgo.Surface, x1: static_cast<float>(tx + iX - 1), y1: static_cast<float>(iY), x2: static_cast<float>(tx + iX), y2: static_cast<float>(iY2), dwClr: pSeries->GetColorDw());
949 iY = iY2;
950 fAnyVal = true;
951 }
952 }
953}
954
955C4Chart::C4Chart(C4Rect &rcBounds) : Element(), pDisplayGraph(nullptr), fOwnGraph(false)
956{
957 this->rcBounds = rcBounds;
958}
959
960C4Chart::~C4Chart()
961{
962 if (fOwnGraph) delete pDisplayGraph;
963}
964
965// singleton
966C4ChartDialog *C4ChartDialog::pChartDlg = nullptr;
967
968C4ChartDialog::C4ChartDialog() : Dialog(DialogWidth, DialogHeight, LoadResStr(id: C4ResStrTableKey::IDS_NET_STATISTICS), false), pChartTabular(nullptr)
969{
970 // register singleton
971 pChartDlg = this;
972 // add main chart switch component
973 C4GUI::ComponentAligner caAll(GetContainedClientRect(), 5, 5);
974 pChartTabular = new C4GUI::Tabular(caAll.GetAll(), C4GUI::Tabular::tbTop);
975 AddElement(pChild: pChartTabular);
976 // add some graphs as subcomponents
977 AddChart(rszName: StdStrBuf("oc"));
978 AddChart(rszName: StdStrBuf("FPS"));
979 AddChart(rszName: StdStrBuf("NetIO"));
980 if (Game.Network.isEnabled())
981 AddChart(rszName: StdStrBuf("Pings"));
982 AddChart(rszName: StdStrBuf("Control"));
983 AddChart(rszName: StdStrBuf("APM"));
984
985 keyEscape.reset(p: registerStrongerEscape(dialog: this));
986}
987
988void C4ChartDialog::AddChart(const StdStrBuf &rszName)
989{
990 // get graph by name
991 if (!Game.pNetworkStatistics || !pChartTabular || !C4GUI::IsGUIValid()) return;
992 bool fOwnGraph = false;
993 C4Graph *pGraph = Game.pNetworkStatistics->GetGraphByName(rszName, rfIsTemp&: fOwnGraph);
994 if (!pGraph) return;
995 // add sheet for name
996 C4GUI::Tabular::Sheet *pSheet = pChartTabular->AddSheet(szTitle: rszName.getData());
997 if (!pSheet) { if (fOwnGraph) delete pGraph; return; }
998 // add chart to sheet
999 C4Chart *pNewChart = new C4Chart(pSheet->GetClientRect());
1000 pNewChart->SetGraph(pNewGraph: pGraph, fOwn: fOwnGraph);
1001 pSheet->AddElement(pChild: pNewChart);
1002}
1003
1004void C4ChartDialog::Toggle()
1005{
1006 if (!C4GUI::IsGUIValid()) return;
1007 // close if open
1008 if (pChartDlg) { pChartDlg->Close(fOK: false); return; }
1009 // otherwise, open
1010 C4ChartDialog *pDlg = new C4ChartDialog();
1011 if (!Game.pGUI->ShowRemoveDlg(pDlg)) delete pChartDlg;
1012}
1013