1 /* 2 * OpenClonk, http://www.openclonk.org 3 * 4 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ 5 * Copyright (c) 2009-2016, The OpenClonk Team and contributors 6 * 7 * Distributed under the terms of the ISC license; see accompanying file 8 * "COPYING" for details. 9 * 10 * "Clonk" is a registered trademark of Matthes Bender, used with permission. 11 * See accompanying file "TRADEMARK" for details. 12 * 13 * To redistribute this file separately, substitute the full license texts 14 * for the above references. 15 */ 16 // the ingame-lobby 17 18 #include "C4Include.h" 19 #include "C4ForbidLibraryCompilation.h" 20 #include "gui/C4GameLobby.h" 21 22 #include "c4group/C4Components.h" 23 #include "control/C4GameControl.h" 24 #include "game/C4Application.h" 25 #include "graphics/C4GraphicsResource.h" 26 #include "gui/C4ChatDlg.h" 27 #include "gui/C4GameOptions.h" 28 #include "gui/C4MessageInput.h" 29 #include "gui/C4PlayerInfoListBox.h" 30 #include "network/C4Network2.h" 31 #include "network/C4Network2Dialogs.h" 32 33 namespace C4GameLobby 34 { 35 36 bool UserAbort = false; 37 38 // ----------- C4PacketCountdown --------------------------------------------- 39 CompileFunc(StdCompiler * pComp)40 void C4PacketCountdown::CompileFunc(StdCompiler *pComp) 41 { 42 pComp->Value(mkNamingAdapt(iCountdown, "Countdown", 0)); 43 } 44 GetCountdownMsg(bool fInitialMsg) const45 StdStrBuf C4PacketCountdown::GetCountdownMsg(bool fInitialMsg) const 46 { 47 const char *szCountdownMsg; 48 if (iCountdown < AlmostStartCountdownTime && !fInitialMsg) szCountdownMsg = "%d..."; else szCountdownMsg = LoadResStr("IDS_PRC_COUNTDOWN"); 49 return FormatString(szCountdownMsg, (int)iCountdown); 50 } 51 52 53 // ----------- C4PacketSetScenarioParameter --------------------------------------------- 54 CompileFunc(StdCompiler * pComp)55 void C4PacketSetScenarioParameter::CompileFunc(StdCompiler *pComp) 56 { 57 pComp->Value(mkNamingAdapt(mkParAdapt(ID, StdCompiler::RCT_Idtf), "ID", StdCopyStrBuf())); 58 pComp->Value(mkNamingAdapt(Value, "Value", 0)); 59 } 60 61 62 // ----------- ScenDescs --------------------------------------------- 63 ScenDesc(const C4Rect & rcBounds,bool fActive)64 ScenDesc::ScenDesc(const C4Rect &rcBounds, bool fActive) : C4GUI::Window(), fDescFinished(false) 65 { 66 // build components 67 SetBounds(rcBounds); 68 C4GUI::ComponentAligner caMain(GetClientRect(), 0,0, true); 69 AddElement(pDescBox = new C4GUI::TextWindow(caMain.GetAll(), 0, 0, 0, 100, 4096, "", true)); 70 pDescBox->SetDecoration(false, false, nullptr, true); 71 // initial update to set current data 72 if (fActive) Activate(); 73 } 74 Update()75 void ScenDesc::Update() 76 { 77 // scenario present? 78 C4Network2Res *pRes = Game.Parameters.Scenario.getNetRes(); 79 if (!pRes) return; // something's wrong 80 CStdFont &rTitleFont = ::GraphicsResource.CaptionFont; 81 CStdFont &rTextFont = ::GraphicsResource.TextFont; 82 pDescBox->ClearText(false); 83 if (pRes->isComplete()) 84 { 85 C4Group ScenarioFile; 86 if (!ScenarioFile.Open(pRes->getFile())) 87 { 88 pDescBox->AddTextLine("scenario file load error", &rTextFont, C4GUI_MessageFontClr, false, true); 89 } 90 else 91 { 92 // load desc 93 C4ComponentHost DefDesc; 94 if (C4Language::LoadComponentHost(&DefDesc, ScenarioFile, C4CFN_ScenarioDesc, Config.General.LanguageEx)) 95 pDescBox->AddTextLine(DefDesc.GetData(), &rTextFont, C4GUI_MessageFontClr, false, true, &rTitleFont); 96 else 97 pDescBox->AddTextLine(Game.ScenarioTitle.getData(), &rTitleFont, C4GUI_CaptionFontClr, false, true); 98 } 99 // okay, done loading. No more updates. 100 fDescFinished = true; 101 Deactivate(); 102 } 103 else 104 { 105 pDescBox->AddTextLine(FormatString(LoadResStr("IDS_MSG_SCENARIODESC_LOADING"), (int) pRes->getPresentPercent()).getData(), 106 &rTextFont, C4GUI_MessageFontClr, false, true); 107 } 108 pDescBox->UpdateHeight(); 109 } 110 Activate()111 void ScenDesc::Activate() 112 { 113 // final desc set? no update then 114 if (fDescFinished) return; 115 // register timer if necessary 116 Application.Add(this); 117 // force an update 118 Update(); 119 } 120 Deactivate()121 void ScenDesc::Deactivate() 122 { 123 // release timer if set 124 Application.Remove(this); 125 } 126 127 // ----------- MainDlg ----------------------------------------------------------------------------- 128 MainDlg(bool fHost)129 MainDlg::MainDlg(bool fHost) 130 : C4GUI::FullscreenDialog(!Game.ScenarioTitle ? 131 (const char *) LoadResStr("IDS_DLG_LOBBY"): 132 FormatString("%s - %s", Game.ScenarioTitle.getData(), LoadResStr("IDS_DLG_LOBBY")).getData(), 133 Game.ScenarioTitle.getData()), 134 pPlayerList(nullptr), pResList(nullptr), pChatBox(nullptr), pRightTabLbl(nullptr), pRightTab(nullptr), 135 pEdt(nullptr), btnRun(nullptr), btnPlayers(nullptr), btnResources(nullptr), btnTeams(nullptr), btnChat(nullptr) 136 { 137 // key bindings 138 pKeyHistoryUp = new C4KeyBinding(C4KeyCodeEx(K_UP ), "LobbyChatHistoryUp" , KEYSCOPE_Gui, new C4GUI::DlgKeyCBEx<MainDlg, bool>(*this, true , &MainDlg::KeyHistoryUpDown), C4CustomKey::PRIO_CtrlOverride); 139 pKeyHistoryDown= new C4KeyBinding(C4KeyCodeEx(K_DOWN), "LobbyChatHistoryDown", KEYSCOPE_Gui, new C4GUI::DlgKeyCBEx<MainDlg, bool>(*this, false, &MainDlg::KeyHistoryUpDown), C4CustomKey::PRIO_CtrlOverride); 140 // timer 141 Application.Add(this); 142 // indents / sizes 143 int32_t iDefBtnHeight = 32; 144 int32_t iIndentX1, iIndentX2, iIndentX3; 145 int32_t iIndentY1, iIndentY2, iIndentY3, iIndentY4; 146 int32_t iClientListWdt; 147 if (GetClientRect().Wdt > 500) 148 { 149 // normal dlg 150 iIndentX1 = 10; // lower button area 151 iIndentX2 = 20; // center area (chat) 152 iIndentX3 = 5; // client/player list 153 iClientListWdt = GetClientRect().Wdt / 3; 154 } 155 else 156 { 157 // small dlg 158 iIndentX1 = 2; // lower button area 159 iIndentX2 = 2; // center area (chat) 160 iIndentX3 = 1; // client/player list 161 iClientListWdt = GetClientRect().Wdt / 2; 162 } 163 if (GetClientRect().Hgt > 320) 164 { 165 // normal dlg 166 iIndentY1 = 16; // lower button area 167 iIndentY2 = 20; // status bar offset 168 iIndentY3 = 8; // center area (chat) 169 iIndentY4 = 8; // client/player list 170 } 171 else 172 { 173 // small dlg 174 iIndentY1 = 2; // lower button area 175 iIndentY2 = 2; // status bar offset 176 iIndentY3 = 1; // center area (chat) 177 iIndentY4 = 1; // client/player list 178 } 179 // set subtitle ToolTip 180 if (pSubTitle) 181 pSubTitle->SetToolTip(LoadResStr("IDS_DLG_SCENARIOTITLE")); 182 C4GUI::Label *pLbl; 183 // main screen components 184 C4GUI::ComponentAligner caMain(GetClientRect(), 0,0,true); 185 caMain.GetFromBottom(iIndentY2); 186 // lower button-area 187 C4GUI::ComponentAligner caBottom(caMain.GetFromBottom(iDefBtnHeight+iIndentY1*2), iIndentX1,iIndentY1); 188 // add buttons 189 C4GUI::CallbackButton<MainDlg> *btnExit; 190 btnExit = new C4GUI::CallbackButton<MainDlg>(LoadResStr("IDS_DLG_EXIT"), caBottom.GetFromLeft(100), &MainDlg::OnExitBtn); 191 if (fHost) 192 { 193 btnRun = new C4GUI::CallbackButton<MainDlg>(LoadResStr("IDS_DLG_GAMEGO"), caBottom.GetFromRight(100), &MainDlg::OnRunBtn); 194 checkReady = nullptr; 195 } 196 else 197 { 198 checkReady = new C4GUI::CheckBox(caBottom.GetFromRight(90), LoadResStr("IDS_DLG_READY"), false); 199 checkReady->SetOnChecked(new C4GUI::CallbackHandler<MainDlg>(this, &MainDlg::OnReadyCheck)); 200 caBottom.GetFromRight(90); 201 } 202 pGameOptionButtons = new C4GameOptionButtons(caBottom.GetCentered(caBottom.GetInnerWidth(), std::min<int32_t>(C4GUI_IconExHgt, caBottom.GetHeight())), true, fHost, true); 203 204 // players / resources sidebar 205 C4GUI::ComponentAligner caRight(caMain.GetFromRight(iClientListWdt), iIndentX3,iIndentY4); 206 pRightTabLbl = new C4GUI::WoodenLabel("", caRight.GetFromTop(C4GUI::WoodenLabel::GetDefaultHeight(&(::GraphicsResource.TextFont))), C4GUI_CaptionFontClr, &::GraphicsResource.TextFont, ALeft); 207 caRight.ExpandTop(iIndentY4*2 + 1); // undo margin, so client list is located directly under label 208 pRightTab = new C4GUI::Tabular(caRight.GetAll(), C4GUI::Tabular::tbNone); 209 C4GUI::Tabular::Sheet *pPlayerSheet = pRightTab->AddSheet(LoadResStr("IDS_DLG_PLAYERS")); 210 C4GUI::Tabular::Sheet *pResSheet = pRightTab->AddSheet(LoadResStr("IDS_DLG_RESOURCES")); 211 C4GUI::Tabular::Sheet *pOptionsSheet = pRightTab->AddSheet(LoadResStr("IDS_DLG_OPTIONS")); 212 C4GUI::Tabular::Sheet *pScenarioSheet = pRightTab->AddSheet(LoadResStr("IDS_DLG_SCENARIO")); 213 pPlayerList = new C4PlayerInfoListBox(pPlayerSheet->GetContainedClientRect(), C4PlayerInfoListBox::PILBM_LobbyClientSort); 214 pPlayerSheet->AddElement(pPlayerList); 215 pResList = new C4Network2ResDlg(pResSheet->GetContainedClientRect(), false); 216 pResSheet->AddElement(pResList); 217 pOptionsList = new C4GameOptionsList(pResSheet->GetContainedClientRect(), false, C4GameOptionsList::GOLS_Lobby); 218 pOptionsSheet->AddElement(pOptionsList); 219 pScenarioInfo = new ScenDesc(pResSheet->GetContainedClientRect(), false); 220 pScenarioSheet->AddElement(pScenarioInfo); 221 pRightTabLbl->SetContextHandler(new C4GUI::CBContextHandler<C4GameLobby::MainDlg>(this, &MainDlg::OnRightTabContext)); 222 pRightTabLbl->SetClickFocusControl(pPlayerList); 223 224 bool fHasTeams = Game.Teams.IsMultiTeams(); 225 bool fHasChat = C4ChatDlg::IsChatActive(); 226 int32_t iBtnNum = 4+fHasTeams+fHasChat; 227 if (fHasTeams) 228 { 229 btnTeams = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Team, pRightTabLbl->GetToprightCornerRect(16, 16, 4, 4, --iBtnNum), LoadResStr("IDS_DLG_PLAYERSBYTEAM"), &MainDlg::OnTabTeams); 230 } 231 btnPlayers = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Player, pRightTabLbl->GetToprightCornerRect(16,16,4,4,--iBtnNum), LoadResStr("IDS_DLG_PLAYERS"), &MainDlg::OnTabPlayers); 232 btnResources = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Resource, pRightTabLbl->GetToprightCornerRect(16,16,4,4,--iBtnNum), LoadResStr("IDS_DLG_RESOURCES"), &MainDlg::OnTabRes); 233 btnOptions = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Options, pRightTabLbl->GetToprightCornerRect(16,16,4,4,--iBtnNum), LoadResStr("IDS_DLG_OPTIONS"), &MainDlg::OnTabOptions); 234 btnScenario = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Gfx, pRightTabLbl->GetToprightCornerRect(16,16,4,4,--iBtnNum), LoadResStr("IDS_DLG_SCENARIO"), &MainDlg::OnTabScenario); 235 if (fHasChat) 236 btnChat = new C4GUI::CallbackButton<MainDlg, C4GUI::IconButton>(C4GUI::Ico_Ex_Chat, pRightTabLbl->GetToprightCornerRect(16,16,4,4,--iBtnNum), LoadResStr("IDS_CTL_CHAT"), &MainDlg::OnBtnChat); 237 238 // update labels and tooltips for player list 239 UpdateRightTab(); 240 241 // chat area 242 C4GUI::ComponentAligner caCenter(caMain.GetAll(), iIndentX2, iIndentY3); 243 // chat input box 244 C4GUI::ComponentAligner caChat(caCenter.GetFromBottom(C4GUI::Edit::GetDefaultEditHeight()), 0,0); 245 pLbl = new C4GUI::WoodenLabel(LoadResStr("IDS_CTL_CHAT"), caChat.GetFromLeft(40), C4GUI_CaptionFontClr, &::GraphicsResource.TextFont); 246 pEdt = new C4GUI::CallbackEdit<MainDlg>(caChat.GetAll(), this, &MainDlg::OnChatInput); 247 pEdt->SetToolTip(LoadResStr("IDS_DLGTIP_CHAT")); pLbl->SetToolTip(LoadResStr("IDS_DLGTIP_CHAT")); 248 pLbl->SetClickFocusControl(pEdt); 249 // log box 250 pChatBox = new C4GUI::TextWindow(caCenter.GetAll()); 251 // add components in tab-order 252 AddElement(pChatBox); 253 AddElement(pLbl); AddElement(pEdt); // chat 254 255 AddElement(pRightTabLbl); 256 if (btnTeams) AddElement(btnTeams); 257 AddElement(btnPlayers); 258 AddElement(btnResources); 259 AddElement(btnOptions); 260 AddElement(btnScenario); 261 if (btnChat) AddElement(btnChat); 262 263 AddElement(pRightTab); 264 AddElement(btnExit); btnExit->SetToolTip(LoadResStr("IDS_DLGTIP_EXIT")); 265 AddElement(pGameOptionButtons); 266 if (fHost) 267 { 268 AddElement(btnRun); 269 btnRun->SetToolTip(LoadResStr("IDS_DLGTIP_GAMEGO")); 270 } 271 else 272 { 273 AddElement(checkReady); 274 checkReady->SetToolTip(LoadResStr("IDS_DLGTIP_READY")); 275 } 276 // set initial focus 277 SetFocus(pEdt, false); 278 279 // stuff 280 eCountdownState = CDS_None; 281 iBackBufferIndex = -1; 282 283 // initial player list update 284 UpdatePlayerList(); 285 } 286 ~MainDlg()287 MainDlg::~MainDlg() 288 { 289 Application.Remove(this); 290 delete pKeyHistoryUp; 291 delete pKeyHistoryDown; 292 } 293 OnExitBtn(C4GUI::Control * btn)294 void MainDlg::OnExitBtn(C4GUI::Control *btn) 295 { 296 // abort dlg 297 Close(false); 298 } 299 OnReadyCheck(C4GUI::Element * pCheckBox)300 void MainDlg::OnReadyCheck(C4GUI::Element *pCheckBox) 301 { 302 bool rIsOn = static_cast<C4GUI::CheckBox *>(pCheckBox)->GetChecked(); 303 ::Control.DoInput(CID_ClientUpdate, new C4ControlClientUpdate(::Game.Clients.getLocalID(), CUT_SetReady, rIsOn), CDT_Direct); 304 } 305 SetCountdownState(CountdownState eToState,int32_t iTimer)306 void MainDlg::SetCountdownState(CountdownState eToState, int32_t iTimer) 307 { 308 // no change? 309 if (eToState == eCountdownState) return; 310 // changing away from countdown? 311 if (eCountdownState == CDS_Countdown) 312 { 313 StopSoundEffect("Structures::Elevator::Moving", nullptr); 314 if (eToState != CDS_Start) StartSoundEffect("Liquids::Pshshsh"); 315 } 316 // change to game start? 317 if (eToState == CDS_Start) 318 { 319 // announce it! 320 StartSoundEffect("Fire::Blast3"); 321 } 322 else if (eToState == CDS_Countdown) 323 { 324 StartSoundEffect("Fire::Fuse"); 325 } 326 if (eToState == CDS_Countdown || eToState == CDS_LongCountdown) 327 { 328 // game start notify 329 Application.NotifyUserIfInactive(); 330 if (!eCountdownState) 331 { 332 // host update start button to be abort button 333 if (btnRun) btnRun->SetText(LoadResStr("IDS_DLG_CANCEL")); 334 } 335 } 336 // countdown abort? 337 if (!eToState) 338 { 339 // host update start button to be start button again 340 if (btnRun) btnRun->SetText(LoadResStr("IDS_DLG_GAMEGO")); 341 // countdown abort message 342 OnLog(LoadResStr("IDS_PRC_STARTABORTED"), C4GUI_LogFontClr2); 343 } 344 // set new state 345 eCountdownState = eToState; 346 // update stuff (makes team sel and fair crew btn available) 347 pGameOptionButtons->SetCountdown(IsCountdown()); 348 UpdatePlayerList(); 349 } 350 OnCountdownPacket(const C4PacketCountdown & Pkt)351 void MainDlg::OnCountdownPacket(const C4PacketCountdown &Pkt) 352 { 353 // determine new countdown state 354 int32_t iTimer = 0; 355 CountdownState eNewState; 356 if (Pkt.IsAbort()) 357 eNewState = CDS_None; 358 else 359 { 360 iTimer = Pkt.GetCountdown(); 361 if (!iTimer) 362 eNewState = CDS_Start; // game is about to be started (boom) 363 else if (iTimer <= AlmostStartCountdownTime) 364 eNewState = CDS_Countdown; // eToState 365 else 366 eNewState = CDS_LongCountdown; 367 } 368 bool fWasCountdown = !!eCountdownState; 369 SetCountdownState(eNewState, iTimer); 370 // display countdown (except last, which ends the lobby anyway) 371 if (iTimer) 372 { 373 // first countdown message 374 OnLog(Pkt.GetCountdownMsg(!fWasCountdown).getData(), C4GUI_LogFontClr2); 375 StartSoundEffect("UI::Tick"); 376 } 377 } 378 IsCountdown()379 bool MainDlg::IsCountdown() 380 { 381 // flag as countdown if countdown running or game is about to start 382 // so team choice, etc. will not become available in the last split-second 383 return eCountdownState >= CDS_Countdown; 384 } 385 OnClosed(bool fOK)386 void MainDlg::OnClosed(bool fOK) 387 { 388 // lobby aborted by user: remember not to display error log 389 if (!fOK) 390 C4GameLobby::UserAbort = true; 391 // finish countdown if running 392 // (may not be finished if status change packet from host is faster than the countdown-initiate) 393 if (eCountdownState) SetCountdownState(fOK ? CDS_Start : CDS_None, 0); 394 } 395 OnRunBtn(C4GUI::Control * btn)396 void MainDlg::OnRunBtn(C4GUI::Control *btn) 397 { 398 // only for host 399 if (!::Network.isHost()) return; 400 // already started? then abort 401 if (eCountdownState) { ::Network.AbortLobbyCountdown(); return; } 402 // otherwise start, utilizing correct countdown time 403 Start(Config.Lobby.CountdownTime); 404 } 405 Start(int32_t iCountdownTime)406 void MainDlg::Start(int32_t iCountdownTime) 407 { 408 // network savegame resumes: Warn if not all players have been associated 409 if (Game.C4S.Head.SaveGame) 410 { 411 if (Game.PlayerInfos.FindUnassociatedRestoreInfo(Game.RestorePlayerInfos)) 412 { 413 StdStrBuf sMsg; sMsg.Ref(LoadResStr("IDS_MSG_NOTALLSAVEGAMEPLAYERSHAVE")); 414 if (!GetScreen()->ShowMessageModal(sMsg.getData(), LoadResStr("IDS_MSG_FREESAVEGAMEPLRS"), C4GUI::MessageDialog::btnYesNo, C4GUI::Ico_SavegamePlayer, &Config.Startup.HideMsgPlrNoTakeOver)) 415 return; 416 } 417 418 // warning about desync bug #1965 419 int i=0; C4ClientPlayerInfos *pkInfo; 420 while ((pkInfo = Game.PlayerInfos.GetIndexedInfo(i++))) { 421 C4PlayerInfo *pPlrInfo; int32_t iInfo=0; 422 while ((pPlrInfo = pkInfo->GetPlayerInfo(iInfo++))) 423 if (!pPlrInfo->GetAssociatedSavegamePlayerID()) 424 { 425 bool ignore = GetScreen()->ShowMessageModal( 426 LoadResStr("IDS_DLG_NETRESUME"), 427 LoadResStr("IDS_MSG_FREESAVEGAMEPLRS"), 428 C4GUI::MessageDialog::btnYesNo, 429 C4GUI::Ico_Error 430 ); 431 if (ignore) 432 break; 433 else 434 return; 435 } 436 } 437 } 438 // validate countdown time 439 iCountdownTime = ValidatedCountdownTime(iCountdownTime); 440 // either direct start... 441 if (!iCountdownTime) 442 ::Network.Start(); 443 else 444 // ...or countdown 445 ::Network.StartLobbyCountdown(iCountdownTime); 446 } 447 OnChatInput(C4GUI::Edit * edt,bool fPasting,bool fPastingMore)448 C4GUI::Edit::InputResult MainDlg::OnChatInput(C4GUI::Edit *edt, bool fPasting, bool fPastingMore) 449 { 450 // get edit text 451 C4GUI::Edit *pEdt = reinterpret_cast<C4GUI::Edit *>(edt); 452 const char *szInputText = pEdt->GetText(); 453 // no input? 454 if (!szInputText || !*szInputText) 455 { 456 // do some error sound then 457 C4GUI::GUISound("UI::Error"); 458 } 459 else 460 { 461 // store input in backbuffer before processing commands 462 // because those might kill the edit field 463 ::MessageInput.StoreBackBuffer(szInputText); 464 ::MessageInput.ProcessInput(szInputText); 465 } 466 // clear edit field after text has been processed 467 pEdt->SelectAll(); pEdt->DeleteSelection(); 468 // reset backbuffer-index of chat history 469 iBackBufferIndex = -1; 470 // OK, on we go. Leave edit intact 471 return C4GUI::Edit::IR_None; 472 } 473 OnClientJoin(C4Client * pNewClient)474 void MainDlg::OnClientJoin(C4Client *pNewClient) 475 { 476 // update list 477 UpdatePlayerList(); 478 } 479 OnClientConnect(C4Client * pClient,C4Network2IOConnection * pConn)480 void MainDlg::OnClientConnect(C4Client *pClient, C4Network2IOConnection *pConn) 481 { 482 } 483 OnClientPart(C4Client * pPartClient)484 void MainDlg::OnClientPart(C4Client *pPartClient) 485 { 486 // update list 487 UpdatePlayerList(); 488 } 489 HandlePacket(char cStatus,const C4PacketBase * pPacket,C4Network2Client * pClient)490 void MainDlg::HandlePacket(char cStatus, const C4PacketBase *pPacket, C4Network2Client *pClient) 491 { 492 // note that player info update packets are not handled by this function, 493 // but by player info list and then forwarded to MainDlg::OnPlayerUpdate 494 // this is necessary because there might be changes (e.g. duplicate colors) 495 // done by player info list 496 // besides, this releases the lobby from doing any host/client-specializations 497 #define GETPKT(type, name) \ 498 assert(pPacket); const type &name = \ 499 static_cast<const type &>(*pPacket); 500 switch (cStatus) 501 { 502 case PID_LobbyCountdown: // initiate or abort countdown 503 { 504 GETPKT(C4PacketCountdown, Pkt); 505 // do countdown 506 OnCountdownPacket(Pkt); 507 } 508 break; 509 case PID_SetScenarioParameter: // set a scenario parameter value 510 { 511 GETPKT(C4PacketSetScenarioParameter, Pkt); 512 ::Game.Parameters.ScenarioParameters.SetValue(Pkt.GetID(), Pkt.GetValue(), false); 513 // reflect updated value immediately on clients 514 if (pRightTab->GetActiveSheetIndex() == SheetIdx_Options) if (pOptionsList) pOptionsList->Update(); 515 } 516 }; 517 #undef GETPKT 518 } 519 OnMessage(C4Client * pOfClient,const char * szMessage)520 bool MainDlg::OnMessage(C4Client *pOfClient, const char *szMessage) 521 { 522 // output message should be prefixed with client already 523 const char *szMsgBuf = szMessage; 524 // 2do: log with player colors? 525 if (pChatBox && !pOfClient->IsIgnored()) 526 { 527 pChatBox->AddTextLine(szMsgBuf, &::GraphicsResource.TextFont, ::Network.Players.GetClientChatColor(pOfClient ? pOfClient->getID() : Game.Clients.getLocalID(), true) | C4GUI_MessageFontAlpha, true, true); 528 pChatBox->ScrollToBottom(); 529 } 530 // log it 531 LogSilent(szMsgBuf); 532 // done, success 533 return true; 534 } 535 OnClientSound(C4Client * pOfClient)536 void MainDlg::OnClientSound(C4Client *pOfClient) 537 { 538 // show that someone played a sound 539 if (pOfClient && pPlayerList) 540 { 541 pPlayerList->SetClientSoundIcon(pOfClient->getID()); 542 } 543 } 544 OnLog(const char * szLogMsg,DWORD dwClr)545 void MainDlg::OnLog(const char *szLogMsg, DWORD dwClr) 546 { 547 if (pChatBox) 548 { 549 pChatBox->AddTextLine(szLogMsg, &::GraphicsResource.TextFont, dwClr, true, true); 550 pChatBox->ScrollToBottom(); 551 } 552 } 553 OnError(const char * szErrMsg)554 void MainDlg::OnError(const char *szErrMsg) 555 { 556 if (pChatBox) 557 { 558 StartSoundEffect("UI::Error"); 559 pChatBox->AddTextLine(szErrMsg, &::GraphicsResource.TextFont, C4GUI_ErrorFontClr, true, true); 560 pChatBox->ScrollToBottom(); 561 } 562 } 563 OnSec1Timer()564 void MainDlg::OnSec1Timer() 565 { 566 UpdatePlayerList(); 567 } 568 UpdatePlayerList()569 void MainDlg::UpdatePlayerList() 570 { 571 // this updates ping label texts and teams 572 if (pPlayerList) pPlayerList->Update(); 573 } 574 OnRightTabContext(C4GUI::Element * pLabel,int32_t iX,int32_t iY)575 C4GUI::ContextMenu *MainDlg::OnRightTabContext(C4GUI::Element *pLabel, int32_t iX, int32_t iY) 576 { 577 // create context menu 578 C4GUI::ContextMenu *pMenu = new C4GUI::ContextMenu(); 579 // players/resources 580 C4GUI::Tabular::Sheet *pPlayerSheet = pRightTab->GetSheet(0); 581 C4GUI::Tabular::Sheet *pResSheet = pRightTab->GetSheet(1); 582 C4GUI::Tabular::Sheet *pOptionsSheet = pRightTab->GetSheet(2); 583 pMenu->AddItem(pPlayerSheet->GetTitle(), pPlayerSheet->GetToolTip(), C4GUI::Ico_Player, 584 new C4GUI::CBMenuHandler<MainDlg>(this, &MainDlg::OnCtxTabPlayers)); 585 if (Game.Teams.IsMultiTeams()) 586 { 587 StdCopyStrBuf strShowTeamsDesc(LoadResStr("IDS_MSG_SHOWTEAMS_DESC")); 588 pMenu->AddItem(LoadResStr("IDS_MSG_SHOWTEAMS"), strShowTeamsDesc.getData(), C4GUI::Ico_Team, 589 new C4GUI::CBMenuHandler<MainDlg>(this, &MainDlg::OnCtxTabTeams)); 590 } 591 pMenu->AddItem(pResSheet->GetTitle(), pResSheet->GetToolTip(), C4GUI::Ico_Resource, 592 new C4GUI::CBMenuHandler<MainDlg>(this, &MainDlg::OnCtxTabRes)); 593 pMenu->AddItem(pOptionsSheet->GetTitle(), pOptionsSheet->GetToolTip(), C4GUI::Ico_Options, 594 new C4GUI::CBMenuHandler<MainDlg>(this, &MainDlg::OnCtxTabOptions)); 595 // open it 596 return pMenu; 597 } 598 OnClientAddPlayer(const char * szFilename,int32_t idClient)599 void MainDlg::OnClientAddPlayer(const char *szFilename, int32_t idClient) 600 { 601 // check client number 602 if (idClient != Game.Clients.getLocalID()) 603 { 604 LobbyError(FormatString(LoadResStr("IDS_ERR_JOINPLR_NOLOCALCLIENT"), szFilename, idClient).getData()); 605 return; 606 } 607 // player join - check filename 608 if (!ItemExists(szFilename)) 609 { 610 LobbyError(FormatString(LoadResStr("IDS_ERR_JOINPLR_NOFILE"), szFilename).getData()); 611 return; 612 } 613 // join! 614 ::Network.Players.JoinLocalPlayer(Config.AtRelativePath(szFilename)); 615 } 616 OnTabPlayers(C4GUI::Control * btn)617 void MainDlg::OnTabPlayers(C4GUI::Control *btn) 618 { 619 if (pPlayerList) pPlayerList->SetMode(C4PlayerInfoListBox::PILBM_LobbyClientSort); 620 if (pRightTab) 621 { 622 pRightTab->SelectSheet(SheetIdx_PlayerList, true); 623 UpdateRightTab(); 624 } 625 } 626 OnTabTeams(C4GUI::Control * btn)627 void MainDlg::OnTabTeams(C4GUI::Control *btn) 628 { 629 if (pPlayerList) pPlayerList->SetMode(C4PlayerInfoListBox::PILBM_LobbyTeamSort); 630 if (pRightTab) 631 { 632 pRightTab->SelectSheet(SheetIdx_PlayerList, true); 633 UpdateRightTab(); 634 } 635 } 636 OnTabRes(C4GUI::Control * btn)637 void MainDlg::OnTabRes(C4GUI::Control *btn) 638 { 639 if (pRightTab) 640 { 641 pRightTab->SelectSheet(SheetIdx_Res, true); 642 UpdateRightTab(); 643 } 644 } 645 OnTabOptions(C4GUI::Control * btn)646 void MainDlg::OnTabOptions(C4GUI::Control *btn) 647 { 648 if (pRightTab) 649 { 650 pRightTab->SelectSheet(SheetIdx_Options, true); 651 UpdateRightTab(); 652 } 653 } 654 OnTabScenario(C4GUI::Control * btn)655 void MainDlg::OnTabScenario(C4GUI::Control *btn) 656 { 657 if (pRightTab) 658 { 659 pRightTab->SelectSheet(SheetIdx_Scenario, true); 660 UpdateRightTab(); 661 } 662 } 663 UpdateRightTab()664 void MainDlg::UpdateRightTab() 665 { 666 if (!pRightTabLbl || !pRightTab || !pRightTab->GetActiveSheet() || !pPlayerList) return; 667 // copy active sheet data to label 668 pRightTabLbl->SetText(pRightTab->GetActiveSheet()->GetTitle()); 669 pRightTabLbl->SetToolTip(pRightTab->GetActiveSheet()->GetToolTip()); 670 // update 671 if (pRightTab->GetActiveSheetIndex() == SheetIdx_PlayerList) UpdatePlayerList(); 672 if (pRightTab->GetActiveSheetIndex() == SheetIdx_Res) pResList->Activate(); else pResList->Deactivate(); 673 if (pRightTab->GetActiveSheetIndex() == SheetIdx_Options) pOptionsList->Activate(); else pOptionsList->Deactivate(); 674 if (pRightTab->GetActiveSheetIndex() == SheetIdx_Scenario) pScenarioInfo->Activate(); else pScenarioInfo->Deactivate(); 675 // update selection buttons 676 if (btnPlayers) btnPlayers->SetHighlight(pRightTab->GetActiveSheetIndex() == SheetIdx_PlayerList && pPlayerList->GetMode() == C4PlayerInfoListBox::PILBM_LobbyClientSort); 677 if (btnResources) btnResources->SetHighlight(pRightTab->GetActiveSheetIndex() == SheetIdx_Res); 678 if (btnTeams) btnTeams->SetHighlight(pRightTab->GetActiveSheetIndex() == SheetIdx_PlayerList && pPlayerList->GetMode() == C4PlayerInfoListBox::PILBM_LobbyTeamSort); 679 if (btnOptions) btnOptions->SetHighlight(pRightTab->GetActiveSheetIndex() == SheetIdx_Options); 680 if (btnScenario) btnScenario->SetHighlight(pRightTab->GetActiveSheetIndex() == SheetIdx_Scenario); 681 } 682 OnBtnChat(C4GUI::Control * btn)683 void MainDlg::OnBtnChat(C4GUI::Control *btn) 684 { 685 // open chat dialog 686 C4ChatDlg::ShowChat(); 687 } 688 KeyHistoryUpDown(bool fUp)689 bool MainDlg::KeyHistoryUpDown(bool fUp) 690 { 691 // chat input only 692 if (!IsFocused(pEdt)) return false; 693 pEdt->SelectAll(); pEdt->DeleteSelection(); 694 const char *szPrevInput = ::MessageInput.GetBackBuffer(fUp ? (++iBackBufferIndex) : (--iBackBufferIndex)); 695 if (!szPrevInput || !*szPrevInput) 696 iBackBufferIndex = -1; 697 else 698 { 699 pEdt->InsertText(szPrevInput, true); 700 pEdt->SelectAll(); 701 } 702 return true; 703 } 704 UpdatePassword()705 void MainDlg::UpdatePassword() 706 { 707 // if the password setting has changed, make sure the buttons reflect this change 708 pGameOptionButtons->UpdatePasswordBtn(); 709 } 710 ValidatedCountdownTime(int32_t iTimeout)711 int32_t MainDlg::ValidatedCountdownTime(int32_t iTimeout) 712 { 713 // no negative timeouts 714 if (iTimeout < 0) iTimeout = 5; 715 // in leage mode, there must be at least five seconds timeout 716 if (Game.Parameters.isLeague() && iTimeout < 5) iTimeout = 5; 717 return iTimeout; 718 } 719 ClearLog()720 void MainDlg::ClearLog() 721 { 722 pChatBox->ClearText(true); 723 } 724 LobbyError(const char * szErrorMsg)725 void LobbyError(const char *szErrorMsg) 726 { 727 // get lobby 728 MainDlg *pLobby = ::Network.GetLobby(); 729 if (pLobby) pLobby->OnError(szErrorMsg); 730 else Log(szErrorMsg); 731 } 732 733 734 /* Countdown */ 735 Countdown(int32_t iStartTimer)736 Countdown::Countdown(int32_t iStartTimer) : iStartTimer(iStartTimer) 737 { 738 // only on network hosts 739 assert(::Network.isHost()); 740 // ctor: Init; sends initial countdown packet 741 C4PacketCountdown pck(iStartTimer); 742 ::Network.Clients.BroadcastMsgToClients(MkC4NetIOPacket(PID_LobbyCountdown, pck)); 743 // also process on host 744 MainDlg *pLobby = ::Network.GetLobby(); 745 if (pLobby) 746 { 747 pLobby->OnCountdownPacket(pck); 748 } 749 else 750 { 751 // no lobby: Message to log for dedicated/console hosts 752 Log(pck.GetCountdownMsg().getData()); 753 } 754 755 // init timer callback 756 Application.Add(this); 757 } 758 ~Countdown()759 Countdown::~Countdown() 760 { 761 // release timer 762 Application.Remove(this); 763 } 764 OnSec1Timer()765 void Countdown::OnSec1Timer() 766 { 767 // count down 768 iStartTimer = std::max<int32_t>(iStartTimer - 1, 0); 769 // only send "important" start timer numbers to all clients 770 if (iStartTimer <= AlmostStartCountdownTime || // last seconds 771 (iStartTimer <= 600 && !(iStartTimer % 10)) || // last minute: 10s interval 772 !(iStartTimer % 60)) // otherwise, minute interval 773 { 774 C4PacketCountdown pck(iStartTimer); 775 ::Network.Clients.BroadcastMsgToClients(MkC4NetIOPacket(PID_LobbyCountdown, pck)); 776 // also process on host 777 MainDlg *pLobby = ::Network.GetLobby(); 778 if (pLobby) 779 pLobby->OnCountdownPacket(pck); 780 else if (iStartTimer) 781 { 782 // no lobby: Message to log for dedicated/console hosts 783 Log(pck.GetCountdownMsg().getData()); 784 } 785 } 786 // countdown done 787 if (!iStartTimer) 788 { 789 #ifdef USE_CONSOLE 790 // Dedicated server: if there are not enough players for this game, abort and quit the application 791 if (Game.PlayerInfos.GetPlayerCount() < Game.C4S.GetMinPlayer() 792 || ::Network.Clients.Count() <= 2) 793 { 794 Log(LoadResStr("IDS_MSG_NOTENOUGHPLAYERSFORTHISRO")); // it would also be nice to send this message to all clients... 795 Application.Quit(); 796 } 797 // Start the game 798 else 799 #endif // USE_CONSOLE 800 ::Network.Start(); 801 } 802 } 803 Abort()804 void Countdown::Abort() 805 { 806 // host sends packets 807 if (!::Network.isHost()) return; 808 C4PacketCountdown pck(C4PacketCountdown::Abort); 809 ::Network.Clients.BroadcastMsgToClients(MkC4NetIOPacket(PID_LobbyCountdown, pck)); 810 // also process on host 811 MainDlg *pLobby = ::Network.GetLobby(); 812 if (pLobby) 813 { 814 pLobby->OnCountdownPacket(pck); 815 } 816 else 817 { 818 // no lobby: Message to log for dedicated/console hosts 819 Log(LoadResStr("IDS_PRC_STARTABORTED")); 820 } 821 } 822 823 } // end of namespace 824 825 826