1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2001, Sven2
6 * Copyright (c) 2017-2022, 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// the ingame-lobby
19
20#include <C4Include.h>
21#include <C4GameLobby.h>
22#include "C4GameControl.h"
23
24#include "C4FullScreen.h"
25#include "C4GuiEdit.h"
26#include "C4GuiResource.h"
27#include "C4GuiTabular.h"
28#include "C4Network2Dialogs.h"
29#include "C4GameOptions.h"
30#include "C4GameDialogs.h"
31#include "C4Wrappers.h"
32#include "C4RTF.h"
33#include "C4ChatDlg.h"
34#include "C4PlayerInfoListBox.h"
35
36#include <format>
37
38namespace C4GameLobby
39{
40
41bool UserAbort = false;
42
43// C4PacketCountdown
44
45void C4PacketCountdown::CompileFunc(StdCompiler *pComp)
46{
47 pComp->Value(rStruct: mkNamingAdapt(rValue&: iCountdown, szName: "Countdown", rDefault: 0));
48}
49
50std::string C4PacketCountdown::GetCountdownMsg(bool fInitialMsg) const
51{
52 if (iCountdown < AlmostStartCountdownTime && !fInitialMsg)
53 {
54 return std::format(fmt: "{}...", args: iCountdown);
55 }
56 else
57 {
58 return LoadResStr(id: C4ResStrTableKey::IDS_PRC_COUNTDOWN, args: iCountdown);
59 }
60}
61
62// ScenDescs
63
64ScenDesc::ScenDesc(const C4Rect &rcBounds, bool fActive) : C4GUI::Window(), fDescFinished(false), pSec1Timer(nullptr)
65{
66 // build components
67 SetBounds(rcBounds);
68 C4GUI::ComponentAligner caMain(GetClientRect(), 0, 0, true);
69 AddElement(pChild: pDescBox = new C4GUI::TextWindow(caMain.GetAll(), 0, 0, 0, 100, 4096, "", true));
70 pDescBox->SetDecoration(fDrawBG: false, fDrawFrame: false, pToGfx: nullptr, fAutoScroll: true);
71 // initial update to set current data
72 if (fActive) Activate();
73}
74
75void ScenDesc::Update()
76{
77 // scenario present?
78 C4Network2Res *pRes = Game.Parameters.Scenario.getNetRes();
79 if (!pRes) return; // something's wrong
80 CStdFont &rTitleFont = C4GUI::GetRes()->CaptionFont;
81 CStdFont &rTextFont = C4GUI::GetRes()->TextFont;
82 pDescBox->ClearText(fDoUpdate: false);
83 if (pRes->isComplete())
84 {
85 C4Group ScenarioFile;
86 if (!ScenarioFile.Open(szGroupName: pRes->getFile()))
87 {
88 pDescBox->AddTextLine(szText: "scenario file load error", pFont: &rTextFont, C4GUI_MessageFontClr, fDoUpdate: false, fMakeReadableOnBlack: true);
89 }
90 else
91 {
92 StdStrBuf sDesc;
93 // load desc
94 C4ComponentHost DefDesc;
95 if (DefDesc.LoadEx(szName: "Desc", hGroup&: ScenarioFile, C4CFN_ScenarioDesc, szLanguage: Config.General.LanguageEx))
96 {
97 C4RTFFile rtf;
98 rtf.Load(sContents: StdBuf::MakeRef(pData: DefDesc.GetData(), iSize: SLen(sptr: DefDesc.GetData())));
99 sDesc.Take(Buf2: rtf.GetPlainText());
100 }
101 DefDesc.Close();
102 if (!!sDesc)
103 pDescBox->AddTextLine(szText: sDesc.getData(), pFont: &rTextFont, C4GUI_MessageFontClr, fDoUpdate: false, fMakeReadableOnBlack: true, pCaptionFont: &rTitleFont);
104 else
105 pDescBox->AddTextLine(szText: Game.Parameters.ScenarioTitle.getData(), pFont: &rTitleFont, C4GUI_CaptionFontClr, fDoUpdate: false, fMakeReadableOnBlack: true);
106 }
107 // okay, done loading. No more updates.
108 fDescFinished = true;
109 Deactivate();
110 }
111 else
112 {
113 pDescBox->AddTextLine(szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SCENARIODESC_LOADING, args: static_cast<int>(pRes->getPresentPercent())).c_str(),
114 pFont: &rTextFont, C4GUI_MessageFontClr, fDoUpdate: false, fMakeReadableOnBlack: true);
115 }
116 pDescBox->UpdateHeight();
117}
118
119void ScenDesc::Activate()
120{
121 // final desc set? no update then
122 if (fDescFinished) return;
123 // create timer if necessary
124 if (!pSec1Timer) pSec1Timer = new C4Sec1TimerCallback<ScenDesc>(this);
125 // force an update
126 Update();
127}
128
129void ScenDesc::Deactivate()
130{
131 // release timer if set
132 if (pSec1Timer)
133 {
134 pSec1Timer->Release();
135 pSec1Timer = nullptr;
136 }
137}
138
139// MainDlg
140
141MainDlg::MainDlg(bool fHost)
142 : C4GUI::FullscreenDialog(!Game.Parameters.ScenarioTitle ?
143 LoadResStr(id: C4ResStrTableKey::IDS_DLG_LOBBY) :
144 std::format(fmt: "{} - {}", args: Game.Parameters.ScenarioTitle.getData(), args: LoadResStr(id: C4ResStrTableKey::IDS_DLG_LOBBY)).c_str(),
145 Game.Parameters.ScenarioTitle.getData()),
146 pPlayerList(nullptr), pResList(nullptr), pChatBox(nullptr), pRightTabLbl(nullptr), pRightTab(nullptr),
147 pEdt(nullptr), btnRun(nullptr), btnPlayers(nullptr), btnResources(nullptr), btnTeams(nullptr), btnChat(nullptr)
148{
149 // key bindings
150 pKeyHistoryUp = new C4KeyBinding(C4KeyCodeEx(K_UP), "LobbyChatHistoryUp", KEYSCOPE_Gui, new C4GUI::DlgKeyCBEx<MainDlg, bool>(*this, true, &MainDlg::KeyHistoryUpDown), C4CustomKey::PRIO_CtrlOverride);
151 pKeyHistoryDown = new C4KeyBinding(C4KeyCodeEx(K_DOWN), "LobbyChatHistoryDown", KEYSCOPE_Gui, new C4GUI::DlgKeyCBEx<MainDlg, bool>(*this, false, &MainDlg::KeyHistoryUpDown), C4CustomKey::PRIO_CtrlOverride);
152 // timer
153 pSec1Timer = new C4Sec1TimerCallback<MainDlg>(this);
154 // indents / sizes
155 int32_t iIndentX1, iIndentX2, iIndentX3;
156 int32_t iIndentY1, iIndentY2, iIndentY3, iIndentY4;
157 int32_t iClientListWdt;
158 if (GetClientRect().Wdt > 500)
159 {
160 // normal dlg
161 iIndentX1 = 10; // lower button area
162 iIndentX2 = 20; // center area (chat)
163 iIndentX3 = 5; // client/player list
164 iClientListWdt = GetClientRect().Wdt / 3;
165 }
166 else
167 {
168 // small dlg
169 iIndentX1 = 2; // lower button area
170 iIndentX2 = 2; // center area (chat)
171 iIndentX3 = 1; // client/player list
172 iClientListWdt = GetClientRect().Wdt / 2;
173 }
174 if (GetClientRect().Hgt > 320)
175 {
176 // normal dlg
177 iIndentY1 = 16; // lower button area
178 iIndentY2 = 20; // status bar offset
179 iIndentY3 = 8; // center area (chat)
180 iIndentY4 = 8; // client/player list
181 }
182 else
183 {
184 // small dlg
185 iIndentY1 = 2; // lower button area
186 iIndentY2 = 2; // status bar offset
187 iIndentY3 = 1; // center area (chat)
188 iIndentY4 = 1; // client/player list
189 }
190 // set subtitle ToolTip
191 if (pSubTitle)
192 pSubTitle->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLG_SCENARIOTITLE));
193 C4GUI::Label *pLbl;
194 // main screen components
195 C4GUI::ComponentAligner caMain(GetClientRect(), 0, 0, true);
196 caMain.GetFromBottom(iHgt: iIndentY2);
197 // lower button-area
198 C4GUI::ComponentAligner caBottom(caMain.GetFromBottom(C4GUI_ButtonHgt + iIndentY1 * 2), iIndentX1, iIndentY1);
199 // add buttons
200 C4GUI::CallbackButton<MainDlg> *btnExit;
201 btnExit = new C4GUI::CallbackButton<MainDlg>(LoadResStr(id: C4ResStrTableKey::IDS_DLG_EXIT), caBottom.GetFromLeft(iWdt: 100), &MainDlg::OnExitBtn);
202 if (fHost)
203 {
204 btnRun = new C4GUI::CallbackButton<MainDlg>(LoadResStr(id: C4ResStrTableKey::IDS_DLG_GAMEGO), caBottom.GetFromRight(iWdt: 100), &MainDlg::OnRunBtn);
205 }
206
207 checkReady = new C4GUI::CheckBox(caBottom.GetFromRight(iWdt: 110), LoadResStr(id: C4ResStrTableKey::IDS_DLG_READY), false);
208 checkReady->SetOnChecked(new C4GUI::CallbackHandler<MainDlg>(this, &MainDlg::OnReadyCheck));
209
210 if (!fHost)
211 {
212 caBottom.GetFromLeft(iWdt: 10); // for centering the buttons between
213 }
214 pGameOptionButtons = new C4GameOptionButtons(caBottom.GetCentered(iWdt: caBottom.GetInnerWidth(), iHgt: std::min<int32_t>(C4GUI_IconExHgt, b: caBottom.GetHeight())), true, fHost, true);
215
216 // players / ressources sidebar
217 C4GUI::ComponentAligner caRight(caMain.GetFromRight(iWdt: iClientListWdt), iIndentX3, iIndentY4);
218 pRightTabLbl = new C4GUI::WoodenLabel("", caRight.GetFromTop(iHgt: C4GUI::WoodenLabel::GetDefaultHeight(pUseFont: &(C4GUI::GetRes()->TextFont))), C4GUI_CaptionFontClr, &C4GUI::GetRes()->TextFont, ALeft);
219 caRight.ExpandTop(iByHgt: iIndentY4 * 2 + 1); // undo margin, so client list is located directly under label
220 pRightTab = new C4GUI::Tabular(caRight.GetAll(), C4GUI::Tabular::tbNone);
221 C4GUI::Tabular::Sheet *pPlayerSheet = pRightTab->AddSheet(szTitle: LoadResStr(id: C4ResStrTableKey::IDS_DLG_PLAYERS, args: -1, args: -1).c_str());
222 C4GUI::Tabular::Sheet *pResSheet = pRightTab->AddSheet(szTitle: LoadResStr(id: C4ResStrTableKey::IDS_DLG_RESOURCES));
223 C4GUI::Tabular::Sheet *pOptionsSheet = pRightTab->AddSheet(szTitle: LoadResStr(id: C4ResStrTableKey::IDS_DLG_OPTIONS));
224 C4GUI::Tabular::Sheet *pScenarioSheet = pRightTab->AddSheet(szTitle: LoadResStr(id: C4ResStrTableKey::IDS_DLG_SCENARIO));
225 pPlayerList = new C4PlayerInfoListBox(pPlayerSheet->GetContainedClientRect(), C4PlayerInfoListBox::PILBM_LobbyClientSort);
226 pPlayerSheet->AddElement(pChild: pPlayerList);
227
228 const C4Rect resSheetBounds{pResSheet->GetContainedClientRect()};
229 C4Rect resDlgBounds{resSheetBounds};
230
231 if (!Config.General.Preloading)
232 {
233 resDlgBounds.Hgt -= C4GUI_ButtonHgt;
234 }
235
236 pResList = new C4Network2ResDlg(resDlgBounds, false);
237 pResSheet->AddElement(pChild: pResList);
238
239 if (!Config.General.Preloading)
240 {
241 const C4Rect btnPreloadBounds{resSheetBounds.x, resSheetBounds.Hgt - C4GUI_ButtonHgt, resSheetBounds.Wdt, C4GUI_ButtonHgt};
242 btnPreload = new C4GUI::CallbackButton<MainDlg>{LoadResStr(id: C4ResStrTableKey::IDS_DLG_PRELOAD), btnPreloadBounds, &MainDlg::OnBtnPreload, this};
243 btnPreload->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PRELOAD));
244 pResSheet->AddElement(pChild: btnPreload);
245 }
246
247 pOptionsList = new C4GameOptionsList(pResSheet->GetContainedClientRect(), false, false);
248 pOptionsSheet->AddElement(pChild: pOptionsList);
249 pScenarioInfo = new ScenDesc(pResSheet->GetContainedClientRect(), false);
250 pScenarioSheet->AddElement(pChild: pScenarioInfo);
251 pRightTabLbl->SetContextHandler(new C4GUI::CBContextHandler<C4GameLobby::MainDlg>(this, &MainDlg::OnRightTabContext));
252 pRightTabLbl->SetClickFocusControl(pPlayerList);
253
254 bool fHasTeams = Game.Teams.IsMultiTeams();
255 bool fHasChat = C4ChatDlg::IsChatActive();
256 int32_t iBtnNum = 4 + fHasTeams + fHasChat;
257 if (fHasTeams)
258 btnTeams = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Team, pRightTabLbl->GetToprightCornerRect(iWidth: 16, iHeight: 16, iHIndent: 4, iVIndent: 4, iIndexX: --iBtnNum), pPlayerSheet->GetHotkey(), &MainDlg::OnTabTeams);
259 btnPlayers = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Player, pRightTabLbl->GetToprightCornerRect(iWidth: 16, iHeight: 16, iHIndent: 4, iVIndent: 4, iIndexX: --iBtnNum), pPlayerSheet->GetHotkey(), &MainDlg::OnTabPlayers);
260 btnResources = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Resource, pRightTabLbl->GetToprightCornerRect(iWidth: 16, iHeight: 16, iHIndent: 4, iVIndent: 4, iIndexX: --iBtnNum), pResSheet->GetHotkey(), &MainDlg::OnTabRes);
261 btnOptions = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Options, pRightTabLbl->GetToprightCornerRect(iWidth: 16, iHeight: 16, iHIndent: 4, iVIndent: 4, iIndexX: --iBtnNum), pOptionsSheet->GetHotkey(), &MainDlg::OnTabOptions);
262 btnScenario = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Gfx, pRightTabLbl->GetToprightCornerRect(iWidth: 16, iHeight: 16, iHIndent: 4, iVIndent: 4, iIndexX: --iBtnNum), pOptionsSheet->GetHotkey(), &MainDlg::OnTabScenario);
263 if (fHasChat)
264 btnChat = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Ex_Chat, pRightTabLbl->GetToprightCornerRect(iWidth: 16, iHeight: 16, iHIndent: 4, iVIndent: 4, iIndexX: --iBtnNum), 0 /* 2do*/, &MainDlg::OnBtnChat);
265
266 // update labels and tooltips for player list
267 UpdateRightTab();
268
269 // chat area
270 C4GUI::ComponentAligner caCenter(caMain.GetAll(), iIndentX2, iIndentY3);
271 // chat input box
272 C4GUI::ComponentAligner caChat(caCenter.GetFromBottom(iHgt: C4GUI::Edit::GetDefaultEditHeight()), 0, 0);
273 pLbl = new C4GUI::WoodenLabel(LoadResStr(id: C4ResStrTableKey::IDS_CTL_CHAT), caChat.GetFromLeft(iWdt: 40), C4GUI_CaptionFontClr, &C4GUI::GetRes()->TextFont);
274 pEdt = new C4GUI::CallbackEdit<MainDlg>(caChat.GetAll(), this, &MainDlg::OnChatInput);
275 pEdt->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_CHAT)); pLbl->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_CHAT));
276 pLbl->SetClickFocusControl(pEdt);
277 // log box
278 pChatBox = new C4GUI::TextWindow(caCenter.GetAll(), 0, 0, 0, 100, 4096, "", false, nullptr, 0, true);
279 // add components in tab-order
280 AddElement(pChild: pChatBox);
281 AddElement(pChild: pLbl); AddElement(pChild: pEdt); // chat
282
283 AddElement(pChild: pRightTabLbl);
284 if (btnTeams) AddElement(pChild: btnTeams);
285 AddElement(pChild: btnPlayers);
286 AddElement(pChild: btnResources);
287 AddElement(pChild: btnOptions);
288 AddElement(pChild: btnScenario);
289 if (btnChat) AddElement(pChild: btnChat);
290
291 AddElement(pChild: pRightTab);
292 AddElement(pChild: btnExit); btnExit->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_EXIT));
293 AddElement(pChild: pGameOptionButtons);
294 if (fHost)
295 {
296 AddElement(pChild: btnRun);
297 btnRun->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_GAMEGO));
298 }
299
300 AddElement(pChild: checkReady);
301
302 // set initial button state
303 UpdatePreloadingGUIState(isComplete: false);
304
305 // set initial focus
306 SetFocus(pCtrl: pEdt, fByMouse: false);
307
308 // stuff
309 eCountdownState = CDS_None;
310 iBackBufferIndex = -1;
311
312 // initial player list update
313 UpdatePlayerList();
314}
315
316MainDlg::~MainDlg()
317{
318 pSec1Timer->Release();
319 delete pKeyHistoryUp;
320 delete pKeyHistoryDown;
321}
322
323void MainDlg::OnExitBtn(C4GUI::Control *btn)
324{
325 // abort dlg
326 Close(fOK: false);
327}
328
329void MainDlg::OnReadyCheck(C4GUI::Element *pCheckBox)
330{
331 auto *const checkBox = static_cast<C4GUI::CheckBox *>(pCheckBox);
332 const bool isOn{checkBox->GetChecked()};
333
334 if (!readyButtonCooldown.TryReset())
335 {
336 checkBox->SetChecked(!isOn);
337 return;
338 }
339
340 ::Game.Network.Clients.BroadcastMsgToClients(rPkt: MkC4NetIOPacket(cStatus: PID_ReadyCheck, Pkt: C4PacketReadyCheck{Game.Clients.getLocalID(), isOn ? C4PacketReadyCheck::Ready : C4PacketReadyCheck::NotReady}), includeHost: true);
341 C4Client *const local{Game.Clients.getLocal()};
342 local->SetLobbyReady(isOn);
343 OnClientReadyStateChange(client: local);
344}
345
346void MainDlg::SetCountdownState(CountdownState eToState, int32_t iTimer)
347{
348 // no change?
349 if (eToState == eCountdownState) return;
350 // changing away from countdown?
351 if (eCountdownState == CDS_Countdown)
352 {
353 StopSoundEffect(name: "Elevator", obj: nullptr);
354 if (eToState != CDS_Start) StartSoundEffect(name: "Pshshsh");
355 }
356 // change to game start?
357 if (eToState == CDS_Start)
358 {
359 // announce it!
360 StartSoundEffect(name: "Blast3");
361 }
362 else if (eToState == CDS_Countdown)
363 {
364 StartSoundEffect(name: "Fuse");
365 StartSoundEffect(name: "Elevator", loop: true);
366 }
367 if (eToState == CDS_Countdown || eToState == CDS_LongCountdown)
368 {
369 // game start notify
370 Application.NotifyUserIfInactive();
371 if (!eCountdownState)
372 {
373 // host update start button to be abort button
374 if (btnRun) btnRun->SetText(LoadResStr(id: C4ResStrTableKey::IDS_DLG_CANCEL));
375 }
376 }
377 // countdown abort?
378 if (!eToState)
379 {
380 // host update start button to be start button again
381 if (btnRun) btnRun->SetText(LoadResStr(id: C4ResStrTableKey::IDS_DLG_GAMEGO));
382 // countdown abort message
383 OnLog(szLogMsg: LoadResStr(id: C4ResStrTableKey::IDS_PRC_STARTABORTED), C4GUI_LogFontClr2);
384 }
385 // set new state
386 eCountdownState = eToState;
387 // update stuff (makes team sel and fair crew btn available)
388 pGameOptionButtons->SetCountdown(IsCountdown());
389 UpdatePlayerList();
390}
391
392void MainDlg::OnCountdownPacket(const C4PacketCountdown &Pkt)
393{
394 // determine new countdown state
395 int32_t iTimer = 0;
396 CountdownState eNewState;
397 if (Pkt.IsAbort())
398 eNewState = CDS_None;
399 else
400 {
401 iTimer = Pkt.GetCountdown();
402 if (!iTimer)
403 eNewState = CDS_Start; // game is about to be started (boom)
404 else if (iTimer <= AlmostStartCountdownTime)
405 eNewState = CDS_Countdown; // eToState
406 else
407 eNewState = CDS_LongCountdown;
408 }
409 bool fWasCountdown = !!eCountdownState;
410 SetCountdownState(eToState: eNewState, iTimer);
411 // display countdown (except last, which ends the lobby anyway)
412 if (iTimer)
413 {
414 // first countdown message
415 OnLog(szLogMsg: Pkt.GetCountdownMsg(fInitialMsg: !fWasCountdown).c_str(), C4GUI_LogFontClr2);
416 StartSoundEffect(name: "Command");
417 }
418}
419
420bool MainDlg::IsCountdown()
421{
422 // flag as countdown if countdown running or game is about to start
423 // so team choice, etc. will not become available in the last split-second
424 return eCountdownState >= CDS_Countdown;
425}
426
427bool MainDlg::IsLongCountdown() const
428{
429 return eCountdownState >= CDS_LongCountdown;
430}
431
432void MainDlg::OnClosed(bool fOK)
433{
434 // lobby aborted by user: remember not to display error log
435 if (!fOK)
436 C4GameLobby::UserAbort = true;
437 // finish countdown if running
438 // (may not be finished if status change packet from host is faster than the countdown-initiate)
439 if (eCountdownState) SetCountdownState(eToState: fOK ? CDS_Start : CDS_None, iTimer: 0);
440}
441
442void MainDlg::OnRunBtn(C4GUI::Control *btn)
443{
444 // only for host
445 if (!Game.Network.isHost()) return;
446 // already started? then abort
447 if (eCountdownState) { Game.Network.AbortLobbyCountdown(); return; }
448 // otherwise start, utilizing correct countdown time
449 Start(iCountdownTime: Config.Lobby.CountdownTime);
450}
451
452void MainDlg::Start(int32_t iCountdownTime)
453{
454 // check league rules
455 if (!Game.Parameters.CheckLeagueRulesStart(fFixIt: true))
456 return;
457 // network savegame resumes: Warn if not all players have been associated
458 if (Game.C4S.Head.SaveGame)
459 if (Game.PlayerInfos.FindUnassociatedRestoreInfo(rRestoreInfoList: Game.RestorePlayerInfos))
460 {
461 StdStrBuf sMsg; sMsg.Ref(pnData: LoadResStr(id: C4ResStrTableKey::IDS_MSG_NOTALLSAVEGAMEPLAYERSHAVE));
462 if (!GetScreen()->ShowMessageModal(szMessage: sMsg.getData(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_MSG_FREESAVEGAMEPLRS), dwButtons: C4GUI::MessageDialog::btnYesNo, icoIcon: C4GUI::Ico_SavegamePlayer, pbConfigDontShowAgainSetting: &Config.Startup.HideMsgPlrNoTakeOver))
463 return;
464 }
465 // validate countdown time
466 iCountdownTime = ValidatedCountdownTime(iTimeout: iCountdownTime);
467 // either direct start...
468 if (!iCountdownTime)
469 Game.Network.Start();
470 else
471 // ...or countdown
472 Game.Network.StartLobbyCountdown(iCountdownTime);
473}
474
475C4GUI::InputResult MainDlg::OnChatInput(C4GUI::Edit *pEdt, bool fPasting, bool fPastingMore)
476{
477 // get edit text
478 const char *szInputText = pEdt->GetText();
479 // no input?
480 if (!szInputText || !*szInputText)
481 {
482 // do some error sound then
483 C4GUI::GUISound(szSound: "Error");
484 }
485 else
486 {
487 // store input in backbuffer before processing commands
488 // because those might kill the edit field
489 Game.MessageInput.StoreBackBuffer(szMessage: szInputText);
490 bool fProcessed = false;
491 // CAUTION when implementing special commands (like /quit) here:
492 // those must not be executed when text is pasted, because that could crash the GUI system
493 // when there are additional lines to paste, but the edit field is destructed by the command
494 if (*szInputText == '/')
495 {
496 // must be 1 char longer than the longest command only. If given commands are longer, they will be truncated, and such a command won't exist anyway
497 const int32_t MaxCommandLen = 20;
498 char Command[MaxCommandLen + 1];
499 const char *szPar = szInputText;
500 // parse command until first space
501 int32_t iParPos = SCharPos(cTarget: ' ', szInStr: szInputText);
502 if (iParPos < 0)
503 {
504 // command w/o par
505 SCopy(szSource: szInputText, sTarget: Command, iMaxL: MaxCommandLen);
506 szPar += SLen(sptr: szPar);
507 }
508 else
509 {
510 // command with following par
511 SCopy(szSource: szInputText, sTarget: Command, iMaxL: (std::min)(a: MaxCommandLen, b: iParPos));
512 szPar += iParPos + 1;
513 }
514 fProcessed = true;
515 // evaluate lobby-only commands
516 if (SEqualNoCase(szStr1: Command, szStr2: "/joinplr"))
517 {
518 // compose path from given filename
519 const std::string plrPath{std::format(fmt: "{}{}", args: +Config.General.PlayerPath, args&: szPar)};
520 // player join - check filename
521 if (!ItemExists(szItemName: plrPath.c_str()))
522 {
523 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CMD_JOINPLR_NOFILE, args: plrPath.c_str()).c_str());
524 }
525 else
526 Game.Network.Players.JoinLocalPlayer(szLocalPlayerFilename: plrPath.c_str(), fAdd: true);
527 }
528 else if (SEqualNoCase(szStr1: Command, szStr2: "/plrclr"))
529 {
530 // get player name from input text
531 int iSepPos = SCharPos(cTarget: ' ', szInStr: szPar, iIndex: 0);
532 C4PlayerInfo *pNfo = nullptr;
533 int32_t idLocalClient = -1;
534 if (Game.Network.Clients.GetLocal()) idLocalClient = Game.Network.Clients.GetLocal()->getID();
535 if (iSepPos > 0)
536 {
537 // a player name is given: Parse it
538 StdStrBuf sPlrName;
539 sPlrName.Copy(pnData: szPar, iChars: iSepPos);
540 szPar += iSepPos + 1; int32_t id = 0;
541 while (pNfo = Game.PlayerInfos.GetNextPlayerInfoByID(id))
542 {
543 id = pNfo->GetID();
544 if (WildcardMatch(szFName1: sPlrName.getData(), szFName2: pNfo->GetName())) break;
545 }
546 }
547 else
548 // no player name: Set local player
549 pNfo = Game.PlayerInfos.GetPrimaryInfoByClientID(iClientID: idLocalClient);
550 C4ClientPlayerInfos *pCltNfo = nullptr;
551 if (pNfo) pCltNfo = Game.PlayerInfos.GetClientInfoByPlayerID(id: pNfo->GetID());
552 if (!pCltNfo)
553 {
554 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CMD_PLRCLR_NOPLAYER));
555 }
556 else
557 {
558 // may color of this client be set?
559 if (pCltNfo->GetClientID() != idLocalClient && !Game.Network.isHost())
560 {
561 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CMD_PLRCLR_NOACCESS));
562 }
563 else
564 {
565 // get color to set
566 uint32_t dwNewClr;
567 if (sscanf(s: szPar, format: "%x", &dwNewClr) != 1)
568 {
569 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CMD_PLRCLR_USAGE));
570 }
571 else
572 {
573 // color validation
574 dwNewClr &= 0xffffff;
575 if (!dwNewClr) ++dwNewClr;
576 // request a color change to this color
577 C4ClientPlayerInfos LocalInfoRequest = *pCltNfo;
578 C4PlayerInfo *pPlrInfo = LocalInfoRequest.GetPlayerInfoByID(id: pNfo->GetID());
579 assert(pPlrInfo);
580 if (pPlrInfo)
581 {
582 pPlrInfo->SetOriginalColor(dwNewClr); // set this as a new color wish
583 Game.Network.Players.RequestPlayerInfoUpdate(rRequest: LocalInfoRequest);
584 }
585 }
586 }
587 }
588 }
589 else if (SEqualNoCase(szStr1: Command, szStr2: "/start"))
590 {
591 // timeout given?
592 int32_t iTimeout = Config.Lobby.CountdownTime;
593 if (!Game.Network.isHost())
594 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CMD_HOSTONLY));
595 else if (szPar && *szPar && (!sscanf(s: szPar, format: "%d", &iTimeout) || iTimeout < 0))
596 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CMD_START_USAGE));
597 else
598 {
599 // abort previous countdown
600 if (eCountdownState) Game.Network.AbortLobbyCountdown();
601 // start new countdown (aborts previous if necessary)
602 Start(iCountdownTime: iTimeout);
603 }
604 }
605 else if (SEqualNoCase(szStr1: Command, szStr2: "/abort"))
606 {
607 if (!Game.Network.isHost())
608 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CMD_HOSTONLY));
609 else if (eCountdownState)
610 Game.Network.AbortLobbyCountdown();
611 else
612 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CMD_ABORT_NOCOUNTDOWN));
613 }
614 else if (SEqualNoCase(szStr1: Command, szStr2: "/readycheck"))
615 {
616 if (!Game.Network.isHost())
617 {
618 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CMD_HOSTONLY));
619 }
620 else if (Config.Cooldowns.ReadyCheck.TryReset())
621 {
622 RequestReadyCheck();
623 }
624 else
625 {
626 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CMD_COOLDOWN, args: std::to_string(val: Config.Cooldowns.ReadyCheck.GetRemainingTime().count()).c_str()).c_str());
627 }
628 }
629 else if (SEqualNoCase(szStr1: Command, szStr2: "/help"))
630 {
631 Log(id: C4ResStrTableKey::IDS_TEXT_COMMANDSAVAILABLEDURINGLO);
632 LogNTr(fmt: "/start [time] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_STARTTHEROUNDWITHSPECIFIE));
633 LogNTr(fmt: "/abort - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_ABORTSTARTCOUNTDOWN));
634 LogNTr(fmt: "/alert - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_ALERTTHEHOSTIFTHEHOSTISAW));
635 LogNTr(fmt: "/joinplr [filename] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_JOINALOCALPLAYERFROMTHESP));
636 LogNTr(fmt: "/kick [client] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_KICKTHESPECIFIEDCLIENT));
637 LogNTr(fmt: "/observer [client] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SETTHESPECIFIEDCLIENTTOOB));
638 LogNTr(fmt: "/me [action] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_PERFORMANACTIONINYOURNAME));
639 LogNTr(fmt: "/sound [sound] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_PLAYASOUNDFROMTHEGLOBALSO));
640 LogNTr(fmt: "/mute [client] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_NET_MUTE_DESC));
641 LogNTr(fmt: "/unmute [client] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_NET_UNMUTE_DESC));
642 LogNTr(fmt: "/team [message] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SENDAPRIVATEMESSAGETOYOUR));
643 LogNTr(fmt: "/plrclr [player] [RGB] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_CHANGETHECOLOROFTHESPECIF));
644 LogNTr(fmt: "/plrclr [RGB] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_CHANGEYOUROWNPLAYERCOLOR));
645 LogNTr(fmt: "/set comment [comment] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SETANEWNETWORKCOMMENT));
646 LogNTr(fmt: "/set password [password] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SETANEWNETWORKPASSWORD));
647 LogNTr(fmt: "/set faircrew [on/off] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_ENABLEORDISABLEFAIRCREW));
648 LogNTr(fmt: "/set maxplayer [number] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SETANEWMAXIMUMNUMBEROFPLA));
649 LogNTr(fmt: "/clear - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CLEARTHEMESSAGEBOARD));
650 LogNTr(fmt: "/readycheck - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_MSG_READYCHECK));
651 }
652 else
653 {
654 // command not known or not a specific lobby command - forward to messageinput
655 fProcessed = false;
656 }
657 }
658 // not processed? Then forward to messageinput, which parses additional commands and sends regular messages
659 if (!fProcessed) Game.MessageInput.ProcessInput(szText: szInputText);
660 }
661 // clear edit field after text has been processed
662 pEdt->SelectAll(); pEdt->DeleteSelection();
663 // reset backbuffer-index of chat history
664 iBackBufferIndex = -1;
665 // OK, on we go. Leave edit intact
666 return C4GUI::IR_None;
667}
668
669void MainDlg::OnClientJoin(C4Client *pNewClient)
670{
671 // update list
672 UpdatePlayerList();
673}
674
675void MainDlg::OnClientConnect(C4Client *pClient, C4Network2IOConnection *pConn) {}
676
677void MainDlg::OnClientPart(C4Client *pPartClient)
678{
679 // update list
680 UpdatePlayerList();
681}
682
683void MainDlg::HandlePacket(char cStatus, const C4PacketBase *pPacket, C4Network2Client *pClient)
684{
685 // note that player info update packets are not handled by this function,
686 // but by player info list and then forwarded to MainDlg::OnPlayerUpdate
687 // this is necessary because there might be changes (e.g. duplicate colors)
688 // done by player info list
689 // besides, this releases the lobby from doing any host/client-specializations
690#define GETPKT(type, name) \
691 assert(pPacket); \
692 const type &name = static_cast<const type &>(*pPacket);
693 switch (cStatus)
694 {
695 case PID_LobbyCountdown: // initiate or abort countdown
696 {
697 GETPKT(C4PacketCountdown, Pkt);
698 // do countdown
699 OnCountdownPacket(Pkt);
700 }
701 break;
702 };
703#undef GETPKT
704}
705
706bool MainDlg::OnMessage(C4Client *pOfClient, const char *szMessage)
707{
708 // output message should be prefixed with client already
709 if (pChatBox && C4GUI::GetRes())
710 {
711 StdStrBuf text;
712
713 if (Config.General.ShowLogTimestamps)
714 {
715 text.Append(pnData: GetCurrentTimeStamp());
716 text.AppendChar(cChar: ' ');
717 }
718 text.Append(pnData: szMessage);
719
720 pChatBox->AddTextLine(szText: text.getData(), pFont: &C4GUI::GetRes()->TextFont, dwClr: Game.Network.Players.GetClientChatColor(idForClient: pOfClient ? pOfClient->getID() : Game.Clients.getLocalID(), fLobby: true) | C4GUI_MessageFontAlpha, fDoUpdate: true, fMakeReadableOnBlack: true);
721 pChatBox->ScrollToBottom();
722 }
723 // log it
724 spdlog::info(msg: szMessage);
725 // done, success
726 return true;
727}
728
729void MainDlg::OnClientSound(C4Client *pOfClient)
730{
731 // show that someone played a sound
732 if (pOfClient && pPlayerList)
733 {
734 pPlayerList->SetClientSoundIcon(pOfClient->getID());
735 }
736}
737
738void MainDlg::OnLog(const char *szLogMsg, uint32_t dwClr)
739{
740 if (pChatBox && C4GUI::GetRes())
741 {
742 StdStrBuf text;
743
744 if (Config.General.ShowLogTimestamps)
745 {
746 text.Append(pnData: GetCurrentTimeStamp());
747 text.AppendChar(cChar: ' ');
748 }
749 text.Append(pnData: szLogMsg);
750
751 pChatBox->AddTextLine(szText: text.getData(), pFont: &C4GUI::GetRes()->TextFont, dwClr, fDoUpdate: true, fMakeReadableOnBlack: true);
752 pChatBox->ScrollToBottom();
753 }
754}
755
756void MainDlg::OnError(const char *szErrMsg)
757{
758 if (pChatBox && C4GUI::GetRes())
759 {
760 StartSoundEffect(name: "Error");
761 pChatBox->AddTextLine(szText: szErrMsg, pFont: &C4GUI::GetRes()->TextFont, C4GUI_ErrorFontClr, fDoUpdate: true, fMakeReadableOnBlack: true);
762 pChatBox->ScrollToBottom();
763 }
764}
765
766void MainDlg::OnSec1Timer()
767{
768 UpdatePlayerList();
769 UpdateResourceProgress();
770}
771
772void MainDlg::UpdatePlayerList()
773{
774 // this updates ping label texts and teams
775 if (pPlayerList) pPlayerList->Update();
776 UpdatePlayerCountDisplay();
777}
778
779void MainDlg::UpdateResourceProgress()
780{
781 bool isComplete{true};
782 std::int32_t resID{-1};
783 for (C4Network2Res::Ref res; (res = Game.Network.ResList.getRefNextRes(iResID: resID)); ++resID)
784 {
785 resID = res->getResID();
786 if (res->getType() != NRT_Player)
787 {
788 isComplete &= res->isComplete();
789 }
790 }
791
792 if (resourcesLoaded != isComplete)
793 {
794 resourcesLoaded = isComplete;
795 UpdatePreloadingGUIState(isComplete);
796 }
797}
798
799void MainDlg::UpdatePreloadingGUIState(const bool isComplete)
800{
801 assert(checkReady);
802 checkReady->SetEnabled(isComplete);
803
804 if (btnPreload)
805 {
806 bool active{isComplete && Game.CanPreload()};
807 btnPreload->SetEnabled(active);
808 btnPreload->SetVisibility(active);
809 }
810
811 if (isComplete)
812 {
813 checkReady->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_READY));
814 checkReady->SetCaption(LoadResStr(id: C4ResStrTableKey::IDS_DLG_READY));
815
816 if (Config.General.Preloading)
817 {
818 Preload();
819 }
820 }
821 else
822 {
823 checkReady->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_READYNOTAVAILABLE));
824 checkReady->SetCaption(LoadResStr(id: C4ResStrTableKey::IDS_DLG_STILLLOADING));
825 }
826}
827
828bool MainDlg::Preload()
829{
830 if (!Game.Preload())
831 {
832 const char *const message{LoadResStr(id: C4ResStrTableKey::IDS_ERR_PRELOADING)};
833
834 // Don't use Log here since we want a red message
835 OnLog(szLogMsg: message, C4GUI_ErrorFontClr);
836 spdlog::info(msg: message);
837
838 return false;
839 }
840
841 return true;
842}
843
844C4GUI::ContextMenu *MainDlg::OnRightTabContext(C4GUI::Element *pLabel, int32_t iX, int32_t iY)
845{
846 // create context menu
847 C4GUI::ContextMenu *pMenu = new C4GUI::ContextMenu();
848 // players/ressources
849 C4GUI::Tabular::Sheet *pPlayerSheet = pRightTab->GetSheet(iIndex: 0);
850 C4GUI::Tabular::Sheet *pResSheet = pRightTab->GetSheet(iIndex: 1);
851 C4GUI::Tabular::Sheet *pOptionsSheet = pRightTab->GetSheet(iIndex: 2);
852 pMenu->AddItem(szText: pPlayerSheet->GetTitle(), szToolTip: pPlayerSheet->GetToolTip(), icoIcon: C4GUI::Ico_Player,
853 pMenuHandler: new C4GUI::CBMenuHandler<MainDlg>(this, &MainDlg::OnCtxTabPlayers));
854 if (Game.Teams.IsMultiTeams())
855 {
856 StdStrBuf strShowTeamsDesc(LoadResStr(id: C4ResStrTableKey::IDS_MSG_SHOWTEAMS_DESC));
857 pMenu->AddItem(szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SHOWTEAMS), szToolTip: strShowTeamsDesc.getData(), icoIcon: C4GUI::Ico_Team,
858 pMenuHandler: new C4GUI::CBMenuHandler<MainDlg>(this, &MainDlg::OnCtxTabTeams));
859 }
860 pMenu->AddItem(szText: pResSheet->GetTitle(), szToolTip: pResSheet->GetToolTip(), icoIcon: C4GUI::Ico_Resource,
861 pMenuHandler: new C4GUI::CBMenuHandler<MainDlg>(this, &MainDlg::OnCtxTabRes));
862 pMenu->AddItem(szText: pOptionsSheet->GetTitle(), szToolTip: pOptionsSheet->GetToolTip(), icoIcon: C4GUI::Ico_Options,
863 pMenuHandler: new C4GUI::CBMenuHandler<MainDlg>(this, &MainDlg::OnCtxTabOptions));
864 // open it
865 return pMenu;
866}
867
868void MainDlg::OnClientReadyStateChange(C4Client *client)
869{
870 UpdatePlayerList();
871
872 for (C4Client *clnt = nullptr; (clnt = Game.Clients.getClient(pAfter: clnt)); )
873 {
874 // does the client have players?
875 if (C4ClientPlayerInfos *const infos{Game.PlayerInfos.GetInfoByClientID(iClientID: clnt->getID())}; clnt->isHost() || (infos && infos->GetPlayerCount()))
876 {
877 if (!clnt->isLobbyReady())
878 {
879 // the client was ready and now isn't? stop the countdown
880 if (client->getID() == clnt->getID() && Game.Network.isHost() && IsLongCountdown())
881 {
882 Game.Network.AbortLobbyCountdown();
883 }
884
885 return;
886 }
887 }
888 }
889
890 if (Game.Network.isHost() && !IsLongCountdown())
891 {
892 Start(iCountdownTime: Config.Lobby.CountdownTime);
893 }
894}
895
896void MainDlg::OnClientAddPlayer(const char *szFilename, int32_t idClient)
897{
898 // check client number
899 if (idClient != Game.Clients.getLocalID())
900 {
901 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_ERR_JOINPLR_NOLOCALCLIENT, args&: szFilename, args&: idClient).c_str());
902 return;
903 }
904 // player join - check filename
905 if (!ItemExists(szItemName: szFilename))
906 {
907 LobbyError(szErrorMsg: LoadResStr(id: C4ResStrTableKey::IDS_ERR_JOINPLR_NOFILE, args&: szFilename).c_str());
908 return;
909 }
910 // check countdown state
911 if (IsCountdown())
912 {
913 if (Game.Network.isHost())
914 Game.Network.AbortLobbyCountdown();
915 else
916 return;
917 }
918 // join!
919 Game.Network.Players.JoinLocalPlayer(szLocalPlayerFilename: Config.AtExeRelativePath(szFilename), fAdd: true);
920}
921
922void MainDlg::OnTabPlayers(C4GUI::Control *btn)
923{
924 if (pPlayerList) pPlayerList->SetMode(C4PlayerInfoListBox::PILBM_LobbyClientSort);
925 if (pRightTab)
926 {
927 pRightTab->SelectSheet(iIndex: SheetIdx_PlayerList, fByUser: true);
928 UpdateRightTab();
929 }
930}
931
932void MainDlg::OnTabTeams(C4GUI::Control *btn)
933{
934 if (pPlayerList) pPlayerList->SetMode(C4PlayerInfoListBox::PILBM_LobbyTeamSort);
935 if (pRightTab)
936 {
937 pRightTab->SelectSheet(iIndex: SheetIdx_PlayerList, fByUser: true);
938 UpdateRightTab();
939 }
940}
941
942void MainDlg::OnTabRes(C4GUI::Control *btn)
943{
944 if (pRightTab)
945 {
946 pRightTab->SelectSheet(iIndex: SheetIdx_Res, fByUser: true);
947 UpdateRightTab();
948 }
949}
950
951void MainDlg::OnTabOptions(C4GUI::Control *btn)
952{
953 if (pRightTab)
954 {
955 pRightTab->SelectSheet(iIndex: SheetIdx_Options, fByUser: true);
956 UpdateRightTab();
957 }
958}
959
960void MainDlg::OnTabScenario(C4GUI::Control *btn)
961{
962 if (pRightTab)
963 {
964 pRightTab->SelectSheet(iIndex: SheetIdx_Scenario, fByUser: true);
965 UpdateRightTab();
966 }
967}
968
969void MainDlg::UpdateRightTab()
970{
971 if (!pRightTabLbl || !pRightTab || !pRightTab->GetActiveSheet() || !pPlayerList) return;
972
973 // update
974 if (pRightTab->GetActiveSheetIndex() == SheetIdx_PlayerList) UpdatePlayerList();
975 if (pRightTab->GetActiveSheetIndex() == SheetIdx_Res) pResList->Activate(); else pResList->Deactivate();
976 if (pRightTab->GetActiveSheetIndex() == SheetIdx_Options) pOptionsList->Activate(); else pOptionsList->Deactivate();
977 if (pRightTab->GetActiveSheetIndex() == SheetIdx_Scenario) pScenarioInfo->Activate(); else pScenarioInfo->Deactivate();
978 // update selection buttons
979 if (btnPlayers) btnPlayers->SetHighlight(pRightTab->GetActiveSheetIndex() == SheetIdx_PlayerList && pPlayerList->GetMode() == C4PlayerInfoListBox::PILBM_LobbyClientSort);
980 if (btnResources) btnResources->SetHighlight(pRightTab->GetActiveSheetIndex() == SheetIdx_Res);
981 if (btnTeams) btnTeams->SetHighlight(pRightTab->GetActiveSheetIndex() == SheetIdx_PlayerList && pPlayerList->GetMode() == C4PlayerInfoListBox::PILBM_LobbyTeamSort);
982 if (btnOptions) btnOptions->SetHighlight(pRightTab->GetActiveSheetIndex() == SheetIdx_Options);
983 if (btnScenario) btnScenario->SetHighlight(pRightTab->GetActiveSheetIndex() == SheetIdx_Scenario);
984
985 UpdateRightTabTitle();
986}
987
988void MainDlg::UpdateRightTabTitle()
989{
990 // copy active sheet data to label
991 pRightTabLbl->SetText(szText: pRightTab->GetActiveSheet()->GetTitle());
992 pRightTabLbl->SetToolTip(pRightTab->GetActiveSheet()->GetToolTip());
993}
994
995void MainDlg::OnBtnChat(C4GUI::Control *btn)
996{
997 // open chat dialog
998 C4ChatDlg::ShowChat();
999}
1000
1001void MainDlg::OnBtnPreload(C4GUI::Control *)
1002{
1003 if (Preload())
1004 {
1005 // disable it for the max. one second delay until C4Network2ResDlg's callback resets it again
1006 const int32_t buttonHeight{btnPreload->GetHeight()};
1007 RemoveElement(pChild: btnPreload);
1008 delete btnPreload;
1009 btnPreload = nullptr;
1010
1011 if (pResList)
1012 {
1013 pResList->GetBounds().Hgt += buttonHeight;
1014 }
1015 }
1016}
1017
1018bool MainDlg::KeyHistoryUpDown(bool fUp)
1019{
1020 // chat input only
1021 if (!IsFocused(pCtrl: pEdt)) return false;
1022 pEdt->SelectAll(); pEdt->DeleteSelection();
1023 const char *szPrevInput = Game.MessageInput.GetBackBuffer(iIndex: fUp ? (++iBackBufferIndex) : (--iBackBufferIndex));
1024 if (!szPrevInput || !*szPrevInput)
1025 iBackBufferIndex = -1;
1026 else
1027 {
1028 pEdt->InsertText(text: szPrevInput, fUser: true);
1029 pEdt->SelectAll();
1030 }
1031 return true;
1032}
1033
1034void MainDlg::UpdateFairCrew()
1035{
1036 // if the fair crew setting has changed, make sure the buttons reflect this change
1037 pGameOptionButtons->UpdateFairCrewBtn();
1038}
1039
1040void MainDlg::UpdatePassword()
1041{
1042 // if the password setting has changed, make sure the buttons reflect this change
1043 pGameOptionButtons->UpdatePasswordBtn();
1044}
1045
1046void MainDlg::UpdatePlayerCountDisplay()
1047{
1048 if (pRightTab)
1049 {
1050 pRightTab->GetSheet(iIndex: 0)->SetTitle(LoadResStr(id: C4ResStrTableKey::IDS_DLG_PLAYERS, args: Game.PlayerInfos.GetActivePlayerCount(fCountInvisible: true), args&: Game.Parameters.MaxPlayers).c_str());
1051 if (pRightTab->GetActiveSheetIndex() == SheetIdx_PlayerList)
1052 {
1053 UpdateRightTabTitle();
1054 }
1055 }
1056}
1057
1058int32_t MainDlg::ValidatedCountdownTime(int32_t iTimeout)
1059{
1060 // no negative timeouts
1061 if (iTimeout < 0) iTimeout = 5;
1062 // in leage mode, there must be at least five seconds timeout
1063 if (Game.Parameters.isLeague() && iTimeout < 5) iTimeout = 5;
1064 return iTimeout;
1065}
1066
1067void MainDlg::ClearLog()
1068{
1069 pChatBox->ClearText(fDoUpdate: true);
1070}
1071
1072void MainDlg::RequestReadyCheck()
1073{
1074 if (IsLongCountdown())
1075 {
1076 Game.Network.AbortLobbyCountdown();
1077 }
1078
1079 for (C4Client *client{nullptr}; (client = Game.Clients.getClient(pAfter: client)); )
1080 {
1081 if (!client->isHost())
1082 {
1083 client->SetLobbyReady(false);
1084 }
1085 }
1086
1087 UpdatePlayerList();
1088 Game.Network.Clients.BroadcastMsgToClients(rPkt: MkC4NetIOPacket(cStatus: PID_ReadyCheck, Pkt: C4PacketReadyCheck{Game.Clients.getLocalID(), C4PacketReadyCheck::Request}));
1089}
1090
1091void MainDlg::CheckReady(bool check)
1092{
1093 checkReady->SetChecked(check);
1094 UpdatePlayerList();
1095}
1096
1097C4GUI::Control *MainDlg::GetDefaultControl()
1098{
1099 return pEdt;
1100}
1101
1102void LobbyError(const char *szErrorMsg)
1103{
1104 // get lobby
1105 MainDlg *pLobby = Game.Network.GetLobby();
1106 if (pLobby) pLobby->OnError(szErrMsg: szErrorMsg);
1107}
1108
1109/* Countdown */
1110
1111Countdown::Countdown(int32_t iStartTimer) : iStartTimer(iStartTimer), pSec1Timer(nullptr)
1112{
1113 // only on network hosts
1114 assert(Game.Network.isHost());
1115 // Init; sends initial countdown packet
1116 C4PacketCountdown pck(iStartTimer);
1117 Game.Network.Clients.BroadcastMsgToClients(rPkt: MkC4NetIOPacket(cStatus: PID_LobbyCountdown, Pkt: pck));
1118 // also process on host
1119 MainDlg *pLobby = Game.Network.GetLobby();
1120 if (pLobby)
1121 {
1122 pLobby->OnCountdownPacket(Pkt: pck);
1123 }
1124 else
1125 {
1126 // no lobby: Message to log for dedicated/console hosts
1127 LogNTr(message: pck.GetCountdownMsg());
1128 }
1129
1130 // init timer callback
1131 pSec1Timer = new C4Sec1TimerCallback<Countdown>(this);
1132}
1133
1134Countdown::~Countdown()
1135{
1136 // release timer
1137 if (pSec1Timer) pSec1Timer->Release();
1138}
1139
1140void Countdown::OnSec1Timer()
1141{
1142 // count down
1143 iStartTimer = std::max<int32_t>(a: iStartTimer - 1, b: 0);
1144 // only send "important" start timer numbers to all clients
1145 if (iStartTimer <= AlmostStartCountdownTime || // last seconds
1146 (iStartTimer <= 600 && !(iStartTimer % 10)) || // last minute: 10s interval
1147 !(iStartTimer % 60)) // otherwise, minute interval
1148 {
1149 C4PacketCountdown pck(iStartTimer);
1150 Game.Network.Clients.BroadcastMsgToClients(rPkt: MkC4NetIOPacket(cStatus: PID_LobbyCountdown, Pkt: pck));
1151 // also process on host
1152 MainDlg *pLobby = Game.Network.GetLobby();
1153 if (pLobby)
1154 pLobby->OnCountdownPacket(Pkt: pck);
1155 else if (iStartTimer)
1156 {
1157 // no lobby: Message to log for dedicated/console hosts
1158 LogNTr(message: pck.GetCountdownMsg());
1159 }
1160 }
1161 // countdown done
1162 if (!iStartTimer)
1163 {
1164 // Dedicated server: if there are not enough players for this game, abort and quit the application
1165 if (!Game.Network.GetLobby() && (Game.PlayerInfos.GetPlayerCount() < Game.C4S.GetMinPlayer()))
1166 {
1167 Log(id: C4ResStrTableKey::IDS_MSG_NOTENOUGHPLAYERSFORTHISRO); // it would also be nice to send this message to all clients...
1168 Application.Quit();
1169 }
1170 // Start the game
1171 else
1172 Game.Network.Start();
1173 }
1174}
1175
1176void Countdown::Abort()
1177{
1178 // host sends packets
1179 if (!Game.Network.isHost()) return;
1180 C4PacketCountdown pck(C4PacketCountdown::Abort);
1181 Game.Network.Clients.BroadcastMsgToClients(rPkt: MkC4NetIOPacket(cStatus: PID_LobbyCountdown, Pkt: pck));
1182 // also process on host
1183 MainDlg *pLobby = Game.Network.GetLobby();
1184 if (pLobby)
1185 {
1186 pLobby->OnCountdownPacket(Pkt: pck);
1187 }
1188 else
1189 {
1190 // no lobby: Message to log for dedicated/console hosts
1191 Log(id: C4ResStrTableKey::IDS_PRC_STARTABORTED);
1192 }
1193}
1194
1195} // end of namespace
1196