| 1 | /* |
| 2 | * LegacyClonk |
| 3 | * |
| 4 | * Copyright (c) RedWolf Design |
| 5 | * Copyright (c) 2013-2018, The OpenClonk Team and contributors |
| 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 | #include "C4GuiResource.h" |
| 19 | #include <C4Include.h> |
| 20 | #include <C4Network2.h> |
| 21 | #include <C4Version.h> |
| 22 | |
| 23 | #include <C4Log.h> |
| 24 | #include <C4Application.h> |
| 25 | #include <C4Console.h> |
| 26 | #include <C4GameSave.h> |
| 27 | #include <C4RoundResults.h> |
| 28 | |
| 29 | // lobby |
| 30 | #include <C4Gui.h> |
| 31 | #include <C4GameLobby.h> |
| 32 | |
| 33 | #include <C4Network2Dialogs.h> |
| 34 | #include <C4League.h> |
| 35 | |
| 36 | #ifndef USE_CONSOLE |
| 37 | #include "C4Toast.h" |
| 38 | #endif |
| 39 | |
| 40 | #ifdef _WIN32 |
| 41 | #include <direct.h> |
| 42 | #else |
| 43 | #include <sys/socket.h> |
| 44 | #include <netinet/in.h> |
| 45 | #include <arpa/inet.h> |
| 46 | #endif |
| 47 | |
| 48 | #include <cassert> |
| 49 | #include <format> |
| 50 | #include <ranges> |
| 51 | |
| 52 | // *** C4Network2Status |
| 53 | |
| 54 | C4Network2Status::C4Network2Status() |
| 55 | : eState(GS_None), iTargetCtrlTick(-1) {} |
| 56 | |
| 57 | const char *C4Network2Status::getStateName() const |
| 58 | { |
| 59 | switch (eState) |
| 60 | { |
| 61 | case GS_None: return "none" ; |
| 62 | case GS_Init: return "init" ; |
| 63 | case GS_Lobby: return "lobby" ; |
| 64 | case GS_Pause: return "pause" ; |
| 65 | case GS_Go: return "go" ; |
| 66 | } |
| 67 | return "???" ; |
| 68 | } |
| 69 | |
| 70 | const char *C4Network2Status::getDescription() const |
| 71 | { |
| 72 | switch (eState) |
| 73 | { |
| 74 | case GS_None: return LoadResStr(id: C4ResStrTableKey::IDS_DESC_NOTINITED); |
| 75 | case GS_Init: return LoadResStr(id: C4ResStrTableKey::IDS_DESC_WAITFORHOST); |
| 76 | case GS_Lobby: return LoadResStr(id: C4ResStrTableKey::IDS_DESC_EXPECTING); |
| 77 | case GS_Pause: return LoadResStr(id: C4ResStrTableKey::IDS_DESC_GAMEPAUSED); |
| 78 | case GS_Go: return LoadResStr(id: C4ResStrTableKey::IDS_DESC_GAMERUNNING); |
| 79 | } |
| 80 | return LoadResStr(id: C4ResStrTableKey::IDS_DESC_UNKNOWNGAMESTATE); |
| 81 | } |
| 82 | |
| 83 | void C4Network2Status::Set(C4NetGameState enState, int32_t inTargetTick) |
| 84 | { |
| 85 | eState = enState; iTargetCtrlTick = inTargetTick; |
| 86 | } |
| 87 | |
| 88 | void C4Network2Status::SetCtrlMode(int32_t inCtrlMode) |
| 89 | { |
| 90 | iCtrlMode = inCtrlMode; |
| 91 | } |
| 92 | |
| 93 | void C4Network2Status::SetTargetTick(int32_t inTargetCtrlTick) |
| 94 | { |
| 95 | iTargetCtrlTick = inTargetCtrlTick; |
| 96 | } |
| 97 | |
| 98 | void C4Network2Status::Clear() |
| 99 | { |
| 100 | eState = GS_None; iTargetCtrlTick = -1; |
| 101 | } |
| 102 | |
| 103 | void C4Network2Status::CompileFunc(StdCompiler *pComp) |
| 104 | { |
| 105 | CompileFunc(pComp, fReference: false); |
| 106 | } |
| 107 | |
| 108 | void C4Network2Status::CompileFunc(StdCompiler *pComp, bool fReference) |
| 109 | { |
| 110 | StdEnumEntry<C4NetGameState> GameStates[] = |
| 111 | { |
| 112 | { .Name: "None" , .Val: GS_None }, |
| 113 | { .Name: "Init" , .Val: GS_Init }, |
| 114 | { .Name: "Lobby" , .Val: GS_Lobby }, |
| 115 | { .Name: "Paused" , .Val: GS_Pause }, |
| 116 | { .Name: "Running" , .Val: GS_Go }, |
| 117 | }; |
| 118 | pComp->Value(rStruct: mkNamingAdapt(rValue: mkEnumAdaptT<uint8_t>(rVal&: eState, pNames: GameStates), szName: "State" , rDefault: GS_None)); |
| 119 | pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iCtrlMode), szName: "CtrlMode" , rDefault: -1)); |
| 120 | |
| 121 | if (!fReference) |
| 122 | pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iTargetCtrlTick), szName: "TargetTick" , rDefault: -1)); |
| 123 | } |
| 124 | |
| 125 | // *** C4Network2 |
| 126 | |
| 127 | #ifndef USE_CONSOLE |
| 128 | |
| 129 | C4Network2::ReadyCheckDialog::ReadyCheckDialog() |
| 130 | : TimedDialog{15, "" , LoadResStr(id: C4ResStrTableKey::IDS_DLG_READYCHECK), btnYesNo, C4GUI::Ico_GameRunning} |
| 131 | { |
| 132 | SetFocus(pCtrl: nullptr, fByMouse: false); |
| 133 | UpdateText(); |
| 134 | |
| 135 | if (toast) |
| 136 | { |
| 137 | toast->SetEventHandler(this); |
| 138 | toast->Show(); |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | void C4Network2::ReadyCheckDialog::UpdateText() |
| 143 | { |
| 144 | StdStrBuf text; |
| 145 | C4GUI::GetRes()->TextFont.BreakMessage( |
| 146 | szMsg: LoadResStr(id: C4ResStrTableKey::IDS_DLG_READYCHECKTEXT, args: GetRemainingTime()).c_str(), |
| 147 | iWdt: GetClientRect().Wdt, |
| 148 | pOut: &text, |
| 149 | fCheckMarkup: false |
| 150 | ); |
| 151 | |
| 152 | if (Config.Toasts.ReadyCheck && !toast && Application.ToastSystem) |
| 153 | { |
| 154 | try |
| 155 | { |
| 156 | toast = Application.ToastSystem->CreateToast(); |
| 157 | |
| 158 | StdStrBuf toastText; |
| 159 | toastText.Copy(Buf2: text); |
| 160 | toastText.Replace(szOld: "|" , szNew: "\n" ); |
| 161 | |
| 162 | toast->AddAction(action: LoadResStrNoAmp(id: C4ResStrTableKey::IDS_DLG_YES)); |
| 163 | toast->AddAction(action: LoadResStrNoAmp(id: C4ResStrTableKey::IDS_DLG_NO)); |
| 164 | toast->SetTitle(LoadResStr(id: C4ResStrTableKey::IDS_DLG_READYCHECK)); |
| 165 | toast->SetText(toastText.getData()); |
| 166 | toast->SetExpiration(1000 * GetRemainingTime()); |
| 167 | } |
| 168 | catch (const std::runtime_error &) |
| 169 | { |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | SetText(text.getData()); |
| 174 | } |
| 175 | |
| 176 | void C4Network2::ReadyCheckDialog::OnClosed(bool) |
| 177 | { |
| 178 | if (toast) |
| 179 | { |
| 180 | toast->SetEventHandler(nullptr); |
| 181 | toast->Hide(); |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | void C4Network2::ReadyCheckDialog::Activated() |
| 186 | { |
| 187 | Close(fOK: true); |
| 188 | } |
| 189 | |
| 190 | void C4Network2::ReadyCheckDialog::OnAction(std::string_view action) |
| 191 | { |
| 192 | Close(fOK: action == LoadResStrNoAmp(id: C4ResStrTableKey::IDS_DLG_YES)); |
| 193 | } |
| 194 | |
| 195 | #endif |
| 196 | |
| 197 | C4Network2::C4Network2() |
| 198 | : Clients(&NetIO), |
| 199 | fAllowJoin(false), |
| 200 | iDynamicTick(-1), fDynamicNeeded(false), |
| 201 | fStatusAck(false), fStatusReached(false), |
| 202 | fChasing(false), |
| 203 | pLobby(nullptr), fLobbyRunning(false), pLobbyCountdown(nullptr), |
| 204 | #ifndef USE_CONSOLE |
| 205 | readyCheckDialog{nullptr}, |
| 206 | #endif |
| 207 | pSec1Timer(nullptr), |
| 208 | pControl(nullptr), |
| 209 | iNextClientID(0), |
| 210 | iLastActivateRequest(0), |
| 211 | iLastChaseTargetUpdate(0), |
| 212 | iLastReferenceUpdate(0), |
| 213 | iLastLeagueUpdate(0), |
| 214 | pLeagueClient(nullptr), |
| 215 | fDelayedActivateReq(false), |
| 216 | pVoteDialog(nullptr), |
| 217 | fPausedForVote(false), |
| 218 | iLastOwnVoting(0), |
| 219 | fStreaming{false}, |
| 220 | NetpuncherGameID{} {} |
| 221 | |
| 222 | bool C4Network2::InitHost(bool fLobby) |
| 223 | { |
| 224 | if (isEnabled()) Clear(); |
| 225 | if (!Logger) |
| 226 | { |
| 227 | Logger = Application.LogSystem.CreateLogger(config&: Config.Logging.Network); |
| 228 | } |
| 229 | // initialize everything |
| 230 | Status.Set(enState: fLobby ? GS_Lobby : GS_Go, inTargetTick: Game.Control.ControlTick); |
| 231 | Status.SetCtrlMode(Config.Network.ControlMode); |
| 232 | fHost = true; |
| 233 | fStatusAck = fStatusReached = true; |
| 234 | fChasing = false; |
| 235 | fAllowJoin = false; |
| 236 | iNextClientID = C4ClientIDStart; |
| 237 | NetpuncherGameID = {}; |
| 238 | NetpuncherAddr = Config.Network.PuncherAddress; |
| 239 | // initialize client list |
| 240 | Clients.Init(logger: Logger, pClientList: &Game.Clients, fHost: true); |
| 241 | // initialize resource list |
| 242 | if (!ResList.Init(logger: Logger, iClientID: Game.Clients.getLocalID(), pIOClass: &NetIO)) |
| 243 | { |
| 244 | LogFatalNTr(message: "Network: failed to initialize resource list!" ); Clear(); return false; |
| 245 | } |
| 246 | if (!Game.Parameters.InitNetwork(pResList: &ResList)) |
| 247 | return false; |
| 248 | // create initial dynamic |
| 249 | if (!CreateDynamic(fInit: true)) |
| 250 | return false; |
| 251 | // initialize net i/o |
| 252 | if (!InitNetIO(fNoClientID: false, fHost: true)) |
| 253 | { |
| 254 | Clear(); return false; |
| 255 | } |
| 256 | // init network control |
| 257 | pControl = &Game.Control.Network; |
| 258 | pControl->Init(iClientID: C4ClientIDHost, fHost: true, iStartTick: Game.Control.getNextControlTick(), fActivated: true, pNetwork: this); |
| 259 | // init league |
| 260 | bool fCancel = true; |
| 261 | if (!InitLeague(pCancel: &fCancel) || !LeagueStart(pCancel: &fCancel)) |
| 262 | { |
| 263 | // deinit league |
| 264 | DeinitLeague(); |
| 265 | // user cancelled? |
| 266 | if (fCancel) |
| 267 | return false; |
| 268 | // in console mode, bail out |
| 269 | #ifdef USE_CONSOLE |
| 270 | return false; |
| 271 | #endif |
| 272 | } |
| 273 | // allow connect |
| 274 | NetIO.SetAcceptMode(true); |
| 275 | // timer |
| 276 | pSec1Timer = new C4Sec1TimerCallback<C4Network2>(this); |
| 277 | // ok |
| 278 | return true; |
| 279 | } |
| 280 | |
| 281 | C4Network2::InitResult C4Network2::InitClient(const C4Network2Reference &Ref, bool fObserver) |
| 282 | { |
| 283 | if (isEnabled()) Clear(); |
| 284 | if (!Logger) |
| 285 | { |
| 286 | Logger = Application.LogSystem.CreateLogger(config&: Config.Logging.Network); |
| 287 | } |
| 288 | // Get host core |
| 289 | const C4ClientCore &HostCore = Ref.Parameters.Clients.getHost()->getCore(); |
| 290 | // repeat if wrong password |
| 291 | fWrongPassword = Ref.isPasswordNeeded(); |
| 292 | NetpuncherGameID = Ref.getNetpuncherGameID(); |
| 293 | NetpuncherAddr = Ref.getNetpuncherAddr(); |
| 294 | StdStrBuf Password; |
| 295 | |
| 296 | // Copy addresses |
| 297 | auto addrs{Ref.getAddresses()}; |
| 298 | const auto scopeId = Ref.GetSourceAddress().GetScopeId(); |
| 299 | for (auto &addr : addrs) |
| 300 | { |
| 301 | addr.GetAddr().SetScopeId(scopeId); |
| 302 | } |
| 303 | C4NetIO::SortAddresses(addresses&: addrs); |
| 304 | |
| 305 | for (;;) |
| 306 | { |
| 307 | // ask for password (again)? |
| 308 | if (fWrongPassword) |
| 309 | { |
| 310 | Password.Take(Buf2: QueryClientPassword()); |
| 311 | if (!Password.getLength()) |
| 312 | return IR_Error; |
| 313 | fWrongPassword = false; |
| 314 | } |
| 315 | |
| 316 | // Try to connect to host |
| 317 | if (InitClient(addrs, HostCore, szPassword: Password.getData()) == IR_Fatal) |
| 318 | return IR_Fatal; |
| 319 | // success? |
| 320 | if (isEnabled()) |
| 321 | break; |
| 322 | // Retry only for wrong password |
| 323 | if (!fWrongPassword) |
| 324 | { |
| 325 | Logger->error(msg: "Could not connect!" ); |
| 326 | return IR_Error; |
| 327 | } |
| 328 | } |
| 329 | // initialize ressources |
| 330 | if (!Game.Parameters.InitNetwork(pResList: &ResList)) |
| 331 | return IR_Fatal; |
| 332 | // init league |
| 333 | if (!InitLeague(pCancel: nullptr)) |
| 334 | { |
| 335 | // deinit league |
| 336 | DeinitLeague(); |
| 337 | return IR_Fatal; |
| 338 | } |
| 339 | // allow connect |
| 340 | NetIO.SetAcceptMode(true); |
| 341 | // timer |
| 342 | pSec1Timer = new C4Sec1TimerCallback<C4Network2>(this); |
| 343 | // ok, success |
| 344 | return IR_Success; |
| 345 | } |
| 346 | |
| 347 | C4Network2::InitResult C4Network2::InitClient(const std::vector<class C4Network2Address> &addrs, const C4ClientCore &HostCore, const char *szPassword) |
| 348 | { |
| 349 | // initialization |
| 350 | Status.Set(enState: GS_Init, inTargetTick: -1); |
| 351 | fHost = false; |
| 352 | fStatusAck = fStatusReached = true; |
| 353 | fChasing = true; |
| 354 | fAllowJoin = false; |
| 355 | // initialize client list |
| 356 | Game.Clients.Init(iLocalClientID: C4ClientIDUnknown); |
| 357 | Clients.Init(logger: Logger, pClientList: &Game.Clients, fHost: false); |
| 358 | // initialize resource list |
| 359 | if (!ResList.Init(logger: Logger, iClientID: Game.Clients.getLocalID(), pIOClass: &NetIO)) |
| 360 | { |
| 361 | LogFatal(id: C4ResStrTableKey::IDS_NET_ERR_INITRESLIST); Clear(); return IR_Fatal; |
| 362 | } |
| 363 | // initialize net i/o |
| 364 | if (!InitNetIO(fNoClientID: true, fHost: false)) |
| 365 | { |
| 366 | Clear(); return IR_Fatal; |
| 367 | } |
| 368 | // set network control |
| 369 | pControl = &Game.Control.Network; |
| 370 | // set exclusive connection mode |
| 371 | NetIO.SetExclusiveConnMode(true); |
| 372 | // Warm up netpuncher |
| 373 | InitPuncher(); |
| 374 | // try to connect host |
| 375 | std::string addresses; int iSuccesses = 0; |
| 376 | for (const auto &addr : addrs) |
| 377 | { |
| 378 | if (!addr.isIPNull()) |
| 379 | { |
| 380 | auto connAddr = addr.GetAddr(); |
| 381 | std::vector<C4NetIO::addr_t> connAddrs; |
| 382 | if (connAddr.IsLocal()) |
| 383 | { |
| 384 | // Local IPv6 addresses need a scope id. |
| 385 | for (const auto &id : Clients.GetLocal()->getInterfaceIDs()) |
| 386 | { |
| 387 | connAddr.SetScopeId(id); |
| 388 | connAddrs.push_back(x: connAddr); |
| 389 | } |
| 390 | } |
| 391 | else |
| 392 | { |
| 393 | connAddrs.push_back(x: connAddr); |
| 394 | } |
| 395 | // connection |
| 396 | const auto cnt = std::count_if(first: connAddrs.cbegin(), last: connAddrs.cend(), pred: [&](const auto &a) { |
| 397 | return NetIO.Connect(addr: a, prot: addr.GetProtocol(), ccore: HostCore, password: szPassword); }); |
| 398 | if (cnt == 0) continue; |
| 399 | // format for message |
| 400 | if (!addresses.empty()) |
| 401 | addresses += ", " ; |
| 402 | addresses += addr.ToString(); |
| 403 | ++iSuccesses; |
| 404 | } |
| 405 | } |
| 406 | // no connection attempt running? |
| 407 | if (!iSuccesses) |
| 408 | { |
| 409 | Clear(); return IR_Error; |
| 410 | } |
| 411 | // log |
| 412 | const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_NET_CONNECTHOST, args&: addresses)}; |
| 413 | LogNTr(message); |
| 414 | // show box |
| 415 | C4GUI::MessageDialog *pDlg = nullptr; |
| 416 | if (Game.pGUI && !Console.Active) |
| 417 | { |
| 418 | // create & show |
| 419 | pDlg = new C4GUI::MessageDialog(message.c_str(), LoadResStr(id: C4ResStrTableKey::IDS_NET_JOINGAME), |
| 420 | C4GUI::MessageDialog::btnAbort, C4GUI::Ico_NetWait, C4GUI::MessageDialog::dsRegular); |
| 421 | if (!pDlg->Show(pOnScreen: Game.pGUI, fCB: true)) { Clear(); return IR_Fatal; } |
| 422 | } |
| 423 | // wait for connect / timeout / abort by user (host will change status on succesful connect) |
| 424 | while (Status.getState() == GS_Init) |
| 425 | { |
| 426 | if (Application.HandleMessage(iTimeout: 100) == HR_Failure) |
| 427 | { |
| 428 | if (Game.pGUI) delete pDlg; return IR_Fatal; |
| 429 | } |
| 430 | if (pDlg && pDlg->IsAborted()) |
| 431 | { |
| 432 | if (Game.pGUI) delete pDlg; return IR_Fatal; |
| 433 | } |
| 434 | } |
| 435 | // Close dialog |
| 436 | if (Game.pGUI) delete pDlg; |
| 437 | // error? |
| 438 | if (!isEnabled()) |
| 439 | return IR_Error; |
| 440 | // deactivate exclusive connection mode |
| 441 | NetIO.SetExclusiveConnMode(false); |
| 442 | return IR_Success; |
| 443 | } |
| 444 | |
| 445 | bool C4Network2::DoLobby() |
| 446 | { |
| 447 | // shouldn't do lobby? |
| 448 | if (!isEnabled() || (!isHost() && !isLobbyActive())) |
| 449 | return true; |
| 450 | |
| 451 | // lobby runs |
| 452 | fLobbyRunning = true; |
| 453 | fAllowJoin = true; |
| 454 | Log(id: C4ResStrTableKey::IDS_NET_LOBBYWAITING); |
| 455 | |
| 456 | // client: lobby status reached, message to host |
| 457 | if (!isHost()) |
| 458 | CheckStatusReached(); |
| 459 | // host: set lobby mode |
| 460 | else |
| 461 | ChangeGameStatus(enState: GS_Lobby, iTargetCtrlTick: 0); |
| 462 | |
| 463 | // determine lobby type |
| 464 | bool fFullscreenLobby = !Console.Active && (lpDDraw->GetEngine() != GFXENGN_NOGFX); |
| 465 | |
| 466 | if (!fFullscreenLobby) |
| 467 | { |
| 468 | // console lobby - update console |
| 469 | if (Console.Active) Console.UpdateMenus(); |
| 470 | // init lobby countdown if specified |
| 471 | if (Game.iLobbyTimeout) StartLobbyCountdown(iCountdownTime: Game.iLobbyTimeout); |
| 472 | // do console lobby |
| 473 | while (isLobbyActive()) |
| 474 | if (Application.HandleMessage() == HR_Failure) |
| 475 | { |
| 476 | Clear(); return false; |
| 477 | } |
| 478 | } |
| 479 | else |
| 480 | { |
| 481 | // fullscreen lobby |
| 482 | |
| 483 | // init lobby dialog |
| 484 | pLobby = new C4GameLobby::MainDlg(isHost()); |
| 485 | if (!pLobby->FadeIn(pOnScreen: Game.pGUI)) { delete pLobby; pLobby = nullptr; Clear(); return false; } |
| 486 | |
| 487 | // init lobby countdown if specified |
| 488 | if (Game.iLobbyTimeout) StartLobbyCountdown(iCountdownTime: Game.iLobbyTimeout); |
| 489 | |
| 490 | // while state lobby: keep looping |
| 491 | while (isLobbyActive() && Game.pGUI && pLobby && pLobby->IsShown()) |
| 492 | if (Application.HandleMessage() == HR_Failure) |
| 493 | { |
| 494 | Clear(); return false; |
| 495 | } |
| 496 | |
| 497 | // check whether lobby was aborted; first checking Game.pGUI |
| 498 | // (because an external call to Game.Clear() would invalidate pLobby) |
| 499 | if (!Game.pGUI) { pLobby = nullptr; Clear(); return false; } |
| 500 | if (pLobby && pLobby->IsAborted()) { delete pLobby; pLobby = nullptr; Clear(); return false; } |
| 501 | |
| 502 | // deinit lobby |
| 503 | if (pLobby && pLobby->IsShown()) pLobby->Close(fOK: true); |
| 504 | delete pLobby; pLobby = nullptr; |
| 505 | |
| 506 | // close any other dialogs |
| 507 | if (Game.pGUI) Game.pGUI->CloseAllDialogs(fWithOK: false); |
| 508 | } |
| 509 | |
| 510 | // lobby end |
| 511 | delete pLobbyCountdown; pLobbyCountdown = nullptr; |
| 512 | fLobbyRunning = false; |
| 513 | fAllowJoin = !Config.Network.NoRuntimeJoin; |
| 514 | |
| 515 | // notify user that the lobby has ended (for people who tasked out) |
| 516 | Application.NotifyUserIfInactive(); |
| 517 | |
| 518 | // notify lobby end |
| 519 | bool fGameGo = isEnabled(); |
| 520 | if (fGameGo) Log(id: C4ResStrTableKey::IDS_PRC_GAMEGO); |
| 521 | |
| 522 | // disabled? |
| 523 | return fGameGo; |
| 524 | } |
| 525 | |
| 526 | bool C4Network2::Start() |
| 527 | { |
| 528 | if (!isEnabled() || !isHost()) return false; |
| 529 | // change mode: go |
| 530 | ChangeGameStatus(enState: GS_Go, iTargetCtrlTick: Game.Control.ControlTick); |
| 531 | return true; |
| 532 | } |
| 533 | |
| 534 | bool C4Network2::Pause() |
| 535 | { |
| 536 | if (!isEnabled() || !isHost()) return false; |
| 537 | // change mode: pause |
| 538 | return ChangeGameStatus(enState: GS_Pause, iTargetCtrlTick: Game.Control.getNextControlTick()); |
| 539 | } |
| 540 | |
| 541 | bool C4Network2::Sync() |
| 542 | { |
| 543 | // host only |
| 544 | if (!isEnabled() || !isHost()) return false; |
| 545 | // already syncing the network? |
| 546 | if (!fStatusAck) |
| 547 | { |
| 548 | // maybe we are already sync? |
| 549 | if (fStatusReached) CheckStatusAck(); |
| 550 | return true; |
| 551 | } |
| 552 | // already sync? |
| 553 | if (isFrozen()) return true; |
| 554 | // ok, so let's do a sync: change in the same state we are already in |
| 555 | return ChangeGameStatus(enState: Status.getState(), iTargetCtrlTick: Game.Control.getNextControlTick()); |
| 556 | } |
| 557 | |
| 558 | bool C4Network2::FinalInit() |
| 559 | { |
| 560 | // check reach |
| 561 | CheckStatusReached(fFromFinalInit: true); |
| 562 | // reached, waiting for ack? |
| 563 | if (fStatusReached && !fStatusAck) |
| 564 | { |
| 565 | // wait for go acknowledgement |
| 566 | Log(id: C4ResStrTableKey::IDS_NET_JOINREADY); |
| 567 | |
| 568 | // any pending keyboard commands should not be routed to cancel the wait dialog - flish the message queue! |
| 569 | while (Application.HandleMessage(iTimeout: 0, fCheckTimer: false) == HR_Message) {} |
| 570 | |
| 571 | // show box |
| 572 | C4GUI::Dialog *pDlg = nullptr; |
| 573 | if (Game.pGUI && !Console.Active) |
| 574 | { |
| 575 | // separate dlgs for host/client |
| 576 | if (isHost()) |
| 577 | { |
| 578 | pDlg = new C4Network2StartWaitDlg(); |
| 579 | } |
| 580 | else |
| 581 | { |
| 582 | pDlg = new C4GUI::MessageDialog(LoadResStr(id: C4ResStrTableKey::IDS_NET_WAITFORSTART), LoadResStr(id: C4ResStrTableKey::IDS_NET_CAPTION), |
| 583 | C4GUI::MessageDialog::btnAbort, C4GUI::Ico_NetWait, C4GUI::MessageDialog::dsSmall, nullptr, false, C4GUI_Z_DEFAULT); |
| 584 | |
| 585 | // unfocus the abort button |
| 586 | pDlg->SetFocus(pCtrl: nullptr, fByMouse: false); |
| 587 | } |
| 588 | // show it |
| 589 | if (!pDlg->Show(pOnScreen: Game.pGUI, fCB: true)) return false; |
| 590 | } |
| 591 | |
| 592 | // wait for acknowledgement |
| 593 | while (fStatusReached && !fStatusAck) |
| 594 | { |
| 595 | if (pDlg) |
| 596 | { |
| 597 | // execute |
| 598 | if (!pDlg->Execute()) { delete pDlg; Clear(); return false; } |
| 599 | // aborted? |
| 600 | if (!Game.pGUI) { Clear(); return false; } |
| 601 | if (pDlg->IsAborted()) { delete pDlg; Clear(); return false; } |
| 602 | } |
| 603 | else if (Application.HandleMessage() == HR_Failure) |
| 604 | { |
| 605 | Clear(); return false; |
| 606 | } |
| 607 | } |
| 608 | if (Game.pGUI) delete pDlg; |
| 609 | // log |
| 610 | Log(id: C4ResStrTableKey::IDS_NET_START); |
| 611 | } |
| 612 | // synchronize |
| 613 | Game.SyncClearance(); |
| 614 | Game.Synchronize(fSavePlayerFiles: false); |
| 615 | // finished |
| 616 | return isEnabled(); |
| 617 | } |
| 618 | |
| 619 | bool C4Network2::RetrieveScenario(char *szScenario) |
| 620 | { |
| 621 | // client only |
| 622 | if (isHost()) return false; |
| 623 | |
| 624 | // wait for scenario |
| 625 | C4Network2Res::Ref pScenario = RetrieveRes(Core: *Game.Parameters.Scenario.getResCore(), |
| 626 | iTimeout: C4NetResRetrieveTimeout, szResName: LoadResStr(id: C4ResStrTableKey::IDS_NET_RES_SCENARIO)); |
| 627 | if (!pScenario) |
| 628 | return false; |
| 629 | |
| 630 | // wait for dynamic data |
| 631 | C4Network2Res::Ref pDynamic = RetrieveRes(Core: ResDynamic, iTimeout: C4NetResRetrieveTimeout, szResName: LoadResStr(id: C4ResStrTableKey::IDS_NET_RES_DYNAMIC)); |
| 632 | if (!pDynamic) |
| 633 | return false; |
| 634 | |
| 635 | // create unpacked copy of scenario |
| 636 | if (!ResList.FindTempResFileName(szFilename: std::format(fmt: "Combined{}.c4s" , args: Game.Clients.getLocalID()).c_str(), pTarget: szScenario) || |
| 637 | !C4Group_CopyItem(szSource: pScenario->getFile(), szTarget: szScenario) || |
| 638 | !C4Group_UnpackDirectory(szFilename: szScenario)) |
| 639 | return false; |
| 640 | |
| 641 | // create unpacked copy of dynamic data |
| 642 | char szTempDynamic[_MAX_PATH + 1]; |
| 643 | if (!ResList.FindTempResFileName(szFilename: pDynamic->getFile(), pTarget: szTempDynamic) || |
| 644 | !C4Group_CopyItem(szSource: pDynamic->getFile(), szTarget: szTempDynamic) || |
| 645 | !C4Group_UnpackDirectory(szFilename: szTempDynamic)) |
| 646 | return false; |
| 647 | |
| 648 | // unpack Material.c4g if materials need to be merged |
| 649 | const std::string materialScenario{std::format(fmt: "{}" DirSep C4CFN_Material, args: +szScenario)}; |
| 650 | const std::string materialDynamic{std::format(fmt: "{}" DirSep C4CFN_Material, args: +szTempDynamic)}; |
| 651 | if (FileExists(szFileName: materialScenario.c_str()) && FileExists(szFileName: materialDynamic.c_str())) |
| 652 | if (!C4Group_UnpackDirectory(szFilename: materialScenario.c_str()) || |
| 653 | !C4Group_UnpackDirectory(szFilename: materialDynamic.c_str())) |
| 654 | return false; |
| 655 | |
| 656 | // move all dynamic files to scenario |
| 657 | C4Group ScenGrp; |
| 658 | if (!ScenGrp.Open(szGroupName: szScenario) || |
| 659 | !ScenGrp.Merge(szFolders: szTempDynamic)) |
| 660 | return false; |
| 661 | ScenGrp.Close(); |
| 662 | |
| 663 | // remove dynamic temp file |
| 664 | EraseDirectory(szDirName: szTempDynamic); |
| 665 | |
| 666 | // remove dynamic - isn't needed any more and will soon be out-of-date |
| 667 | pDynamic->Remove(); |
| 668 | |
| 669 | C4Group_PackDirectory(szFilename: szScenario); |
| 670 | |
| 671 | return true; |
| 672 | } |
| 673 | |
| 674 | void C4Network2::OnSec1Timer() |
| 675 | { |
| 676 | Execute(); |
| 677 | } |
| 678 | |
| 679 | void C4Network2::Execute() |
| 680 | { |
| 681 | if (pLeagueClient) |
| 682 | pLeagueClient->Execute(iMaxTime: 0); |
| 683 | if (pStreamer) |
| 684 | pStreamer->Execute(iMaxTime: 0); |
| 685 | |
| 686 | // client connections |
| 687 | Clients.DoConnectAttempts(); |
| 688 | |
| 689 | // status reached? |
| 690 | CheckStatusReached(); |
| 691 | |
| 692 | if (isHost()) |
| 693 | { |
| 694 | // remove dynamic |
| 695 | if (!ResDynamic.isNull() && Game.Control.ControlTick > iDynamicTick) |
| 696 | RemoveDynamic(); |
| 697 | // Set chase target |
| 698 | UpdateChaseTarget(); |
| 699 | // check for inactive clients and deactivate them |
| 700 | DeactivateInactiveClients(); |
| 701 | // reference |
| 702 | if (!iLastReferenceUpdate || time(timer: nullptr) > static_cast<time_t>(iLastReferenceUpdate + C4NetReferenceUpdateInterval)) |
| 703 | if (NetIO.IsReferenceNeeded()) |
| 704 | { |
| 705 | // create |
| 706 | C4Network2Reference *pRef = new C4Network2Reference(); |
| 707 | pRef->InitLocal(pGame: &Game); |
| 708 | // set |
| 709 | NetIO.SetReference(pRef); |
| 710 | iLastReferenceUpdate = time(timer: nullptr); |
| 711 | } |
| 712 | // league server reference |
| 713 | if (!iLastLeagueUpdate || time(timer: nullptr) > static_cast<time_t>(iLastLeagueUpdate + iLeagueUpdateDelay)) |
| 714 | { |
| 715 | LeagueUpdate(); |
| 716 | } |
| 717 | // league update reply receive |
| 718 | if (pLeagueClient && fHost && !pLeagueClient->isBusy() && pLeagueClient->getCurrentAction() == C4LA_Update) |
| 719 | { |
| 720 | LeagueUpdateProcessReply(); |
| 721 | } |
| 722 | // voting timeout |
| 723 | if (Votes.firstPkt() && time(timer: nullptr) > static_cast<time_t>(iVoteStartTime + C4NetVotingTimeout)) |
| 724 | { |
| 725 | C4ControlVote *pVote = static_cast<C4ControlVote *>(Votes.firstPkt()->getPkt()); |
| 726 | Game.Control.DoInput( |
| 727 | eCtrlType: CID_VoteEnd, |
| 728 | pPkt: new C4ControlVoteEnd(pVote->getType(), false, pVote->getData()), |
| 729 | eDelivery: CDT_Sync); |
| 730 | iVoteStartTime = time(timer: nullptr); |
| 731 | } |
| 732 | // record streaming |
| 733 | if (fStreaming) |
| 734 | { |
| 735 | StreamIn(fFinish: false); |
| 736 | StreamOut(); |
| 737 | } |
| 738 | } |
| 739 | else |
| 740 | { |
| 741 | // request activate, if neccessary |
| 742 | if (iLastActivateRequest) RequestActivate(); |
| 743 | } |
| 744 | } |
| 745 | |
| 746 | void C4Network2::Clear() |
| 747 | { |
| 748 | // stop timer |
| 749 | if (pSec1Timer) pSec1Timer->Release(); pSec1Timer = nullptr; |
| 750 | // stop streaming |
| 751 | StopStreaming(); |
| 752 | // clear league |
| 753 | if (pLeagueClient) |
| 754 | { |
| 755 | LeagueEnd(); |
| 756 | DeinitLeague(); |
| 757 | } |
| 758 | // stop lobby countdown |
| 759 | delete pLobbyCountdown; pLobbyCountdown = nullptr; |
| 760 | // cancel lobby |
| 761 | delete pLobby; pLobby = nullptr; |
| 762 | fLobbyRunning = false; |
| 763 | // deactivate |
| 764 | Status.Clear(); |
| 765 | fStatusAck = fStatusReached = true; |
| 766 | // if control mode is network: change to local |
| 767 | if (Game.Control.isNetwork()) |
| 768 | Game.Control.ChangeToLocal(); |
| 769 | // clear all player infos |
| 770 | Players.Clear(); |
| 771 | // remove all clients |
| 772 | Clients.Clear(); |
| 773 | // close net classes |
| 774 | NetIO.Clear(); |
| 775 | // clear ressources |
| 776 | ResList.Clear(); |
| 777 | // clear password |
| 778 | sPassword.Clear(); |
| 779 | // stuff |
| 780 | fAllowJoin = false; |
| 781 | iDynamicTick = -1; fDynamicNeeded = false; |
| 782 | iLastActivateRequest = iLastChaseTargetUpdate = iLastReferenceUpdate = iLastLeagueUpdate = 0; |
| 783 | fDelayedActivateReq = false; |
| 784 | if (Game.pGUI) delete pVoteDialog; pVoteDialog = nullptr; |
| 785 | fPausedForVote = false; |
| 786 | iLastOwnVoting = 0; |
| 787 | NetpuncherGameID = {}; |
| 788 | Votes.Clear(); |
| 789 | // don't clear fPasswordNeeded here, it's needed by InitClient |
| 790 | } |
| 791 | |
| 792 | bool C4Network2::ToggleAllowJoin() |
| 793 | { |
| 794 | // just toggle |
| 795 | AllowJoin(fAllow: !fAllowJoin); |
| 796 | return true; // toggled |
| 797 | } |
| 798 | |
| 799 | bool C4Network2::ToggleClientListDlg() |
| 800 | { |
| 801 | C4Network2ClientListDlg::Toggle(); |
| 802 | return true; |
| 803 | } |
| 804 | |
| 805 | void C4Network2::SetPassword(const char *szToPassword) |
| 806 | { |
| 807 | bool fHadPassword = isPassworded(); |
| 808 | // clear password? |
| 809 | if (!szToPassword || !*szToPassword) |
| 810 | sPassword.Clear(); |
| 811 | else |
| 812 | // no? then set it |
| 813 | sPassword.Copy(pnData: szToPassword); |
| 814 | // if the has-password-state has changed, the reference is invalidated |
| 815 | if (fHadPassword != isPassworded()) InvalidateReference(); |
| 816 | } |
| 817 | |
| 818 | StdStrBuf C4Network2::QueryClientPassword() |
| 819 | { |
| 820 | // ask client for a password; return nothing if user canceled |
| 821 | StdStrBuf sCaption; sCaption.Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_MSG_ENTERPASSWORD)); |
| 822 | C4GUI::InputDialog *pInputDlg = new C4GUI::InputDialog(LoadResStr(id: C4ResStrTableKey::IDS_MSG_ENTERPASSWORD), sCaption.getData(), C4GUI::Ico_Ex_Locked, nullptr, false); |
| 823 | pInputDlg->SetDelOnClose(false); |
| 824 | if (!Game.pGUI->ShowModalDlg(pDlg: pInputDlg, fDestruct: false)) |
| 825 | { |
| 826 | if (C4GUI::IsGUIValid()) delete pInputDlg; |
| 827 | return StdStrBuf(); |
| 828 | } |
| 829 | // copy to buffer |
| 830 | StdStrBuf Buf; Buf.Copy(pnData: pInputDlg->GetInputText()); |
| 831 | delete pInputDlg; |
| 832 | return Buf; |
| 833 | } |
| 834 | |
| 835 | void C4Network2::AllowJoin(bool fAllow) |
| 836 | { |
| 837 | if (!isHost()) return; |
| 838 | fAllowJoin = fAllow; |
| 839 | if (Game.IsRunning) |
| 840 | { |
| 841 | Game.GraphicsSystem.FlashMessage(szMessage: LoadResStrChoice(condition: fAllowJoin, ifTrue: C4ResStrTableKey::IDS_NET_RUNTIMEJOINFREE, ifFalse: C4ResStrTableKey::IDS_NET_RUNTIMEJOINBARRED)); |
| 842 | Config.Network.NoRuntimeJoin = !fAllowJoin; |
| 843 | } |
| 844 | } |
| 845 | |
| 846 | void C4Network2::SetCtrlMode(int32_t iCtrlMode) |
| 847 | { |
| 848 | if (!isHost()) return; |
| 849 | // no change? |
| 850 | if (iCtrlMode == Status.getCtrlMode()) return; |
| 851 | |
| 852 | // update config value |
| 853 | Config.Network.ControlMode = iCtrlMode; |
| 854 | // change game status |
| 855 | ChangeGameStatus(enState: Status.getState(), iTargetCtrlTick: Game.Control.ControlTick, iCtrlMode); |
| 856 | } |
| 857 | |
| 858 | void C4Network2::OnConn(C4Network2IOConnection *pConn) |
| 859 | { |
| 860 | // Nothing to do atm... New pending connections are managed mainly by C4Network2IO |
| 861 | // until they are accepted, see PID_Conn/PID_ConnRe handlers in HandlePacket. |
| 862 | |
| 863 | // Note this won't get called anymore because of this (see C4Network2IO::OnConn) |
| 864 | } |
| 865 | |
| 866 | void C4Network2::OnDisconn(C4Network2IOConnection *pConn) |
| 867 | { |
| 868 | // could not establish host connection? |
| 869 | if (Status.getState() == GS_Init && !isHost()) |
| 870 | { |
| 871 | if (!NetIO.getConnectionCount()) |
| 872 | Clear(); |
| 873 | return; |
| 874 | } |
| 875 | |
| 876 | // connection failed? |
| 877 | if (pConn->isFailed()) |
| 878 | { |
| 879 | // call handler |
| 880 | OnConnectFail(pConn); |
| 881 | return; |
| 882 | } |
| 883 | |
| 884 | // search client |
| 885 | C4Network2Client *pClient = Clients.GetClient(pConn); |
| 886 | // not found? Search by ID (not associated yet, half-accepted connection) |
| 887 | if (!pClient) pClient = Clients.GetClientByID(iID: pConn->getClientID()); |
| 888 | // not found? ignore |
| 889 | if (!pClient) return; |
| 890 | // remove connection |
| 891 | pClient->RemoveConn(pConn); |
| 892 | |
| 893 | // create post-mortem if needed |
| 894 | C4PacketPostMortem PostMortem; |
| 895 | if (pConn->CreatePostMortem(pPkt: &PostMortem)) |
| 896 | { |
| 897 | Logger->info(fmt: "Sending {} packets for recovery ({}-{})" , args: PostMortem.getPacketCount(), args: pConn->getOutPacketCounter() - PostMortem.getPacketCount(), args: pConn->getOutPacketCounter() - 1); |
| 898 | // This might fail because of this disconnect |
| 899 | // (If it's the only host connection. We're toast then anyway.) |
| 900 | if (!Clients.SendMsgToClient(iClient: pConn->getClientID(), rPkt: MkC4NetIOPacket(cStatus: PID_PostMortem, Pkt: PostMortem))) |
| 901 | assert(isHost() || !Clients.GetHost()->isConnected()); |
| 902 | } |
| 903 | |
| 904 | // call handler |
| 905 | OnDisconnect(pClient, pConn); |
| 906 | } |
| 907 | |
| 908 | void C4Network2::HandlePacket(char cStatus, const C4PacketBase *pPacket, C4Network2IOConnection *pConn) |
| 909 | { |
| 910 | // find associated client |
| 911 | C4Network2Client *pClient = Clients.GetClient(pConn); |
| 912 | if (!pClient) pClient = Clients.GetClientByID(iID: pConn->getClientID()); |
| 913 | |
| 914 | // local? ignore |
| 915 | if (pClient && pClient->isLocal()) { pConn->Close(); return; } |
| 916 | |
| 917 | #define GETPKT(type, name) \ |
| 918 | assert(pPacket); \ |
| 919 | const type &name = static_cast<const type &>(*pPacket); |
| 920 | |
| 921 | switch (cStatus) |
| 922 | { |
| 923 | case PID_Conn: // connection request |
| 924 | { |
| 925 | if (!pConn->isOpen()) break; |
| 926 | GETPKT(C4PacketConn, rPkt); |
| 927 | HandleConn(Pkt: rPkt, pConn, pClient); |
| 928 | } |
| 929 | break; |
| 930 | |
| 931 | case PID_ConnRe: // connection request reply |
| 932 | { |
| 933 | GETPKT(C4PacketConnRe, rPkt); |
| 934 | HandleConnRe(Pkt: rPkt, pConn, pClient); |
| 935 | } |
| 936 | break; |
| 937 | |
| 938 | case PID_JoinData: |
| 939 | { |
| 940 | // host->client only |
| 941 | if (isHost() || !pClient || !pClient->isHost()) break; |
| 942 | if (!pConn->isOpen()) break; |
| 943 | // handle |
| 944 | GETPKT(C4PacketJoinData, rPkt); |
| 945 | HandleJoinData(rPkt); |
| 946 | } |
| 947 | break; |
| 948 | |
| 949 | case PID_ReadyCheck: |
| 950 | { |
| 951 | GETPKT(C4PacketReadyCheck, rPkt); |
| 952 | HandleReadyCheck(packet: rPkt); |
| 953 | } |
| 954 | break; |
| 955 | |
| 956 | case PID_Status: // status change |
| 957 | { |
| 958 | // by host only |
| 959 | if (isHost() || !pClient || !pClient->isHost()) break; |
| 960 | if (!pConn->isOpen()) break; |
| 961 | // must be initialized |
| 962 | if (Status.getState() == GS_Init) break; |
| 963 | // handle |
| 964 | GETPKT(C4Network2Status, rPkt); |
| 965 | HandleStatus(nStatus: rPkt); |
| 966 | } |
| 967 | break; |
| 968 | |
| 969 | case PID_StatusAck: // status change acknowledgement |
| 970 | { |
| 971 | // host->client / client->host only |
| 972 | if (!pClient) break; |
| 973 | if (!isHost() && !pClient->isHost()) break; |
| 974 | // must be initialized |
| 975 | if (Status.getState() == GS_Init) break; |
| 976 | // handle |
| 977 | GETPKT(C4Network2Status, rPkt); |
| 978 | HandleStatusAck(nStatus: rPkt, pClient); |
| 979 | } |
| 980 | break; |
| 981 | |
| 982 | case PID_ClientActReq: // client activation request |
| 983 | { |
| 984 | // client->host only |
| 985 | if (!isHost() || !pClient || pClient->isHost()) break; |
| 986 | // must be initialized |
| 987 | if (Status.getState() == GS_Init) break; |
| 988 | // handle |
| 989 | GETPKT(C4PacketActivateReq, rPkt); |
| 990 | HandleActivateReq(iTick: rPkt.getTick(), pClient); |
| 991 | } |
| 992 | break; |
| 993 | } |
| 994 | |
| 995 | #undef GETPKT |
| 996 | } |
| 997 | |
| 998 | void C4Network2::HandleLobbyPacket(char cStatus, const C4PacketBase *pBasePkt, C4Network2IOConnection *pConn) |
| 999 | { |
| 1000 | // find associated client |
| 1001 | C4Network2Client *pClient = Clients.GetClient(pConn); |
| 1002 | if (!pClient) pClient = Clients.GetClientByID(iID: pConn->getClientID()); |
| 1003 | // forward directly to lobby |
| 1004 | if (pLobby) pLobby->HandlePacket(cStatus, pBasePkt, pClient); |
| 1005 | } |
| 1006 | |
| 1007 | bool C4Network2::HandlePuncherPacket(const C4NetpuncherPacket::uptr pkt, const C4Network2HostAddress::AddressFamily family) |
| 1008 | { |
| 1009 | // TODO: is this all thread-safe? |
| 1010 | assert(pkt); |
| 1011 | #pragma push_macro("GETPKT") |
| 1012 | #undef GETPKT |
| 1013 | #define GETPKT(c) dynamic_cast<C4NetpuncherPacket ##c *>(pkt.get()) |
| 1014 | switch (pkt->GetType()) |
| 1015 | { |
| 1016 | case PID_Puncher_CReq: |
| 1017 | if (isHost()) |
| 1018 | { |
| 1019 | NetIO.Punch(GETPKT(CReq)->GetAddr()); |
| 1020 | return true; |
| 1021 | } |
| 1022 | else |
| 1023 | { |
| 1024 | // The IP/Port should be already in the masterserver list, so just keep trying. |
| 1025 | return Status.getState() == GS_Init; |
| 1026 | } |
| 1027 | case PID_Puncher_AssID: |
| 1028 | if (isHost()) |
| 1029 | { |
| 1030 | getNetpuncherGameID(family) = GETPKT(AssID)->GetID(); |
| 1031 | InvalidateReference(); |
| 1032 | } |
| 1033 | else |
| 1034 | { |
| 1035 | // The netpuncher hands out IDs for everyone, but clients have no use for them. |
| 1036 | } |
| 1037 | return true; |
| 1038 | default: |
| 1039 | return false; |
| 1040 | } |
| 1041 | #pragma pop_macro("GETPKT") |
| 1042 | } |
| 1043 | |
| 1044 | C4NetpuncherID::value &C4Network2::getNetpuncherGameID(const C4Network2HostAddress::AddressFamily family) |
| 1045 | { |
| 1046 | switch (family) |
| 1047 | { |
| 1048 | case C4Network2HostAddress::IPv4: return NetpuncherGameID.v4; |
| 1049 | case C4Network2HostAddress::IPv6: return NetpuncherGameID.v6; |
| 1050 | case C4Network2HostAddress::UnknownFamily: ; // fallthrough |
| 1051 | } |
| 1052 | assert(!"Unexpected address family" ); |
| 1053 | // We need to return a valid reference to satisfy the compiler, even though the code here is unreachable. |
| 1054 | return NetpuncherGameID.v4; |
| 1055 | } |
| 1056 | |
| 1057 | void C4Network2::OnPuncherConnect(const C4NetIO::addr_t addr) |
| 1058 | { |
| 1059 | // NAT punching is only relevant for IPv4, so convert here to show a proper address. |
| 1060 | const auto &maybeV4 = addr.AsIPv4(); |
| 1061 | Logger->info(fmt: "Adding address from puncher: {}" , args: maybeV4.ToString()); |
| 1062 | // Add for local client |
| 1063 | if (const auto &local = Clients.GetLocal()) |
| 1064 | { |
| 1065 | local->AddAddrFromPuncher(addr: maybeV4); |
| 1066 | // Do not Game.Network.InvalidateReference(); yet, we're expecting an ID from the netpuncher |
| 1067 | } |
| 1068 | |
| 1069 | const auto &family = maybeV4.GetFamily(); |
| 1070 | if (isHost()) |
| 1071 | { |
| 1072 | // Host connection: request ID from netpuncher |
| 1073 | NetIO.SendPuncherPacket(C4NetpuncherPacketIDReq{}, family); |
| 1074 | } |
| 1075 | else |
| 1076 | { |
| 1077 | if (Status.getState() == GS_Init && getNetpuncherGameID(family)) |
| 1078 | { |
| 1079 | NetIO.SendPuncherPacket(C4NetpuncherPacketSReq{getNetpuncherGameID(family)}, family); |
| 1080 | } |
| 1081 | } |
| 1082 | } |
| 1083 | |
| 1084 | void C4Network2::InitPuncher() |
| 1085 | { |
| 1086 | // We have an internet connection, so let's punch the puncher server here in order to open an udp port |
| 1087 | for (const auto &family : { C4Network2HostAddress::IPv4, C4Network2HostAddress::IPv6 }) |
| 1088 | { |
| 1089 | C4NetIO::addr_t puncherAddr{}; |
| 1090 | puncherAddr.SetAddress(addr: getNetpuncherAddr(), family); |
| 1091 | if (!puncherAddr.IsNull()) |
| 1092 | { |
| 1093 | puncherAddr.SetDefaultPort(C4NetStdPortPuncher); |
| 1094 | NetIO.InitPuncher(puncherAddr); |
| 1095 | } |
| 1096 | } |
| 1097 | } |
| 1098 | |
| 1099 | void C4Network2::OnGameSynchronized() |
| 1100 | { |
| 1101 | // savegame needed? |
| 1102 | if (fDynamicNeeded) |
| 1103 | { |
| 1104 | // create dynamic |
| 1105 | bool fSuccess = CreateDynamic(fInit: false); |
| 1106 | // check for clients that still need join-data |
| 1107 | C4Network2Client *pClient = nullptr; |
| 1108 | while (pClient = Clients.GetNextClient(pClient)) |
| 1109 | if (!pClient->hasJoinData()) |
| 1110 | if (fSuccess) |
| 1111 | // now we can provide join data: send it |
| 1112 | SendJoinData(pClient); |
| 1113 | else |
| 1114 | // join data could not be created: emergency kick |
| 1115 | Game.Clients.CtrlRemove(pClient: pClient->getClient(), szReason: LoadResStr(id: C4ResStrTableKey::IDS_ERR_ERRORWHILECREATINGJOINDAT)); |
| 1116 | } |
| 1117 | } |
| 1118 | |
| 1119 | void C4Network2::DrawStatus(C4FacetEx &cgo) |
| 1120 | { |
| 1121 | if (!isEnabled()) return; |
| 1122 | |
| 1123 | C4Network2Client *pLocal = Clients.GetLocal(); |
| 1124 | |
| 1125 | std::string stat{std::format(fmt: "Local: {} {} {} (ID {})|Game Status: {} (tick {}){}{}" , |
| 1126 | args: pLocal->isObserver() ? "Observing" : pLocal->isActivated() ? "Active" : "Inactive" , args: pLocal->isHost() ? "host" : "client" , |
| 1127 | args: pLocal->getName(), args: pLocal->getID(), |
| 1128 | args: Status.getStateName(), args: Status.getTargetCtrlTick(), |
| 1129 | args: fStatusReached ? " reached" : "" , args: fStatusAck ? " ack" : "" |
| 1130 | )}; |
| 1131 | |
| 1132 | // available protocols |
| 1133 | C4NetIO *pMsgIO = NetIO.MsgIO(), *pDataIO = NetIO.DataIO(); |
| 1134 | if (pMsgIO && pDataIO) |
| 1135 | { |
| 1136 | C4Network2IOProtocol eMsgProt = NetIO.getNetIOProt(pNetIO: pMsgIO), |
| 1137 | eDataProt = NetIO.getNetIOProt(pNetIO: pDataIO); |
| 1138 | int32_t iMsgPort = 0, iDataPort = 0; |
| 1139 | switch (eMsgProt) |
| 1140 | { |
| 1141 | case P_TCP: iMsgPort = Config.Network.PortTCP; break; |
| 1142 | case P_UDP: iMsgPort = Config.Network.PortUDP; break; |
| 1143 | case P_NONE: |
| 1144 | assert(!"C4Network2IOProtocol of protocol P_NONE" ); |
| 1145 | break; |
| 1146 | } |
| 1147 | switch (eDataProt) |
| 1148 | { |
| 1149 | case P_TCP: iDataPort = Config.Network.PortTCP; break; |
| 1150 | case P_UDP: iDataPort = Config.Network.PortUDP; break; |
| 1151 | case P_NONE: |
| 1152 | assert(!"C4Network2IOProtocol of protocol P_NONE" ); |
| 1153 | break; |
| 1154 | } |
| 1155 | stat += std::format(fmt: "|Protocols: {}: {} ({} i{} o{} bc{})" , |
| 1156 | args: pMsgIO != pDataIO ? "Msg" : "Msg/Data" , |
| 1157 | args: NetIO.getNetIOName(pNetIO: pMsgIO), args&: iMsgPort, |
| 1158 | args: NetIO.getProtIRate(eProt: eMsgProt), args: NetIO.getProtORate(eProt: eMsgProt), args: NetIO.getProtBCRate(eProt: eMsgProt)); |
| 1159 | if (pMsgIO != pDataIO) |
| 1160 | stat += std::format(fmt: ", Data: {} ({} i{} o{} bcv)" , |
| 1161 | args: NetIO.getNetIOName(pNetIO: pDataIO), args&: iDataPort, |
| 1162 | args: NetIO.getProtIRate(eProt: eDataProt), args: NetIO.getProtORate(eProt: eDataProt), args: NetIO.getProtBCRate(eProt: eDataProt)); |
| 1163 | } |
| 1164 | else |
| 1165 | stat += "|Protocols: none" ; |
| 1166 | |
| 1167 | // some control statistics |
| 1168 | stat += std::format(fmt: "|Control: {}, Tick {}, Behind {}, Rate {}, PreSend {}, ACT: {}" , |
| 1169 | args: Status.getCtrlMode() == CNM_Decentral ? "Decentral" : Status.getCtrlMode() == CNM_Central ? "Central" : "Async" , |
| 1170 | args&: Game.Control.ControlTick, args: pControl->GetBehind(iTick: Game.Control.ControlTick), |
| 1171 | args&: Game.Control.ControlRate, args: pControl->getControlPreSend(), args: pControl->getAvgControlSendTime()); |
| 1172 | |
| 1173 | // Streaming statistics |
| 1174 | if (fStreaming) |
| 1175 | stat += std::format(fmt: "|Streaming: {} waiting, {} in, {} out, {} sent" , |
| 1176 | args: pStreamedRecord ? pStreamedRecord->GetStreamingBuf().getSize() : 0, |
| 1177 | args: pStreamedRecord ? pStreamedRecord->GetStreamingPos() : 0, |
| 1178 | args: getPendingStreamData(), |
| 1179 | args&: iCurrentStreamPosition); |
| 1180 | |
| 1181 | // clients |
| 1182 | stat += "|Clients:" ; |
| 1183 | for (C4Network2Client *pClient = Clients.GetNextClient(pClient: nullptr); pClient; pClient = Clients.GetNextClient(pClient)) |
| 1184 | { |
| 1185 | // ignore local |
| 1186 | if (pClient->isLocal()) continue; |
| 1187 | // client status |
| 1188 | const C4ClientCore &Core = pClient->getCore(); |
| 1189 | const char *szClientStatus = "" ; |
| 1190 | switch (pClient->getStatus()) |
| 1191 | { |
| 1192 | case NCS_Joining: szClientStatus = " (joining)" ; break; |
| 1193 | case NCS_Chasing: szClientStatus = " (chasing)" ; break; |
| 1194 | case NCS_NotReady: szClientStatus = " (!rdy)" ; break; |
| 1195 | case NCS_Remove: szClientStatus = " (removed)" ; break; |
| 1196 | case NCS_Ready: szClientStatus = " (ready to start)" ; break; |
| 1197 | } |
| 1198 | stat += std::format(fmt: "|- {} {} {} (ID {}) (wait {} ms, behind {}){}{}" , |
| 1199 | args: Core.isObserver() ? "Observing" : Core.isActivated() ? "Active" : "Inactive" , args: Core.isHost() ? "host" : "client" , |
| 1200 | args: Core.getName(), args: Core.getID(), |
| 1201 | args: pControl->ClientPerfStat(iClientID: pClient->getID()), |
| 1202 | args: Game.Control.ControlTick - pControl->ClientNextControl(iClientID: pClient->getID()), |
| 1203 | args&: szClientStatus, |
| 1204 | args: pClient->isActivated() && !pControl->ClientReady(iClientID: pClient->getID(), iTick: Game.Control.ControlTick) ? " (!ctrl)" : "" ); |
| 1205 | // connections |
| 1206 | if (pClient->isConnected()) |
| 1207 | { |
| 1208 | stat += std::format( fmt: "| Connections: {}: {} ({} p{} l{})" , |
| 1209 | args: pClient->getMsgConn() == pClient->getDataConn() ? "Msg/Data" : "Msg" , |
| 1210 | args: NetIO.getNetIOName(pNetIO: pClient->getMsgConn()->getNetClass()), |
| 1211 | args: pClient->getMsgConn()->getPeerAddr().ToString(), |
| 1212 | args: pClient->getMsgConn()->getPingTime(), |
| 1213 | args: pClient->getMsgConn()->getPacketLoss()); |
| 1214 | if (pClient->getMsgConn() != pClient->getDataConn()) |
| 1215 | stat += std::format(fmt: ", Data: {} ({} p{} l{})" , |
| 1216 | args: NetIO.getNetIOName(pNetIO: pClient->getDataConn()->getNetClass()), |
| 1217 | args: pClient->getDataConn()->getPeerAddr().ToString(), |
| 1218 | args: pClient->getDataConn()->getPingTime(), |
| 1219 | args: pClient->getDataConn()->getPacketLoss()); |
| 1220 | } |
| 1221 | else |
| 1222 | stat += "| Not connected" ; |
| 1223 | } |
| 1224 | if (!Clients.GetNextClient(pClient: nullptr)) |
| 1225 | stat += "| - none -" ; |
| 1226 | |
| 1227 | // draw |
| 1228 | Application.DDraw->TextOut(szText: stat.c_str(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgo.Surface, iTx: cgo.X + 20, iTy: cgo.Y + 50); |
| 1229 | } |
| 1230 | |
| 1231 | bool C4Network2::InitNetIO(bool fNoClientID, bool fHost) |
| 1232 | { |
| 1233 | // clear |
| 1234 | NetIO.Clear(); |
| 1235 | // check for port collisions |
| 1236 | if (Config.Network.PortTCP != 0 && Config.Network.PortTCP == Config.Network.PortRefServer) |
| 1237 | { |
| 1238 | Logger->info(msg: "TCP Port collision, setting defaults" ); |
| 1239 | Config.Network.PortTCP = C4NetStdPortTCP; |
| 1240 | Config.Network.PortRefServer = C4NetStdPortRefServer; |
| 1241 | } |
| 1242 | if (Config.Network.PortUDP != 0 && Config.Network.PortUDP == Config.Network.PortDiscovery) |
| 1243 | { |
| 1244 | Logger->info(msg: "UDP Port collision, setting defaults" ); |
| 1245 | Config.Network.PortUDP = C4NetStdPortUDP; |
| 1246 | Config.Network.PortDiscovery = C4NetStdPortDiscovery; |
| 1247 | } |
| 1248 | // discovery: disable for client |
| 1249 | const std::uint16_t iPortDiscovery = fHost ? Config.Network.PortDiscovery : 0; |
| 1250 | const std::uint16_t iPortRefServer = fHost ? Config.Network.PortRefServer : 0; |
| 1251 | // init subclass |
| 1252 | if (!NetIO.Init(iPortTCP: Config.Network.PortTCP, iPortUDP: Config.Network.PortUDP, iPortDiscovery, iPortRefServer)) |
| 1253 | return false; |
| 1254 | // set core (unset ID if sepecified, has to be set later) |
| 1255 | C4ClientCore Core = Game.Clients.getLocalCore(); |
| 1256 | if (fNoClientID) Core.SetID(C4ClientIDUnknown); |
| 1257 | NetIO.SetLocalCCore(Core); |
| 1258 | // safe addresses of local client |
| 1259 | Clients.GetLocal()->AddLocalAddrs( |
| 1260 | iPortTCP: NetIO.hasTCP() ? Config.Network.PortTCP : 0, |
| 1261 | iPortUDP: NetIO.hasUDP() ? Config.Network.PortUDP : 0); |
| 1262 | // ok |
| 1263 | return true; |
| 1264 | } |
| 1265 | |
| 1266 | void C4Network2::HandleConn(const C4PacketConn &Pkt, C4Network2IOConnection *pConn, C4Network2Client *pClient) |
| 1267 | { |
| 1268 | // security |
| 1269 | if (!pConn) return; |
| 1270 | |
| 1271 | // Handles a connect request (packet PID_Conn). |
| 1272 | // Check if this peer should be allowed to connect, make space for the new connection. |
| 1273 | |
| 1274 | // connection is closed? |
| 1275 | if (pConn->isClosed()) |
| 1276 | return; |
| 1277 | |
| 1278 | // set up core |
| 1279 | const C4ClientCore &CCore = Pkt.getCCore(); |
| 1280 | C4ClientCore NewCCore = CCore; |
| 1281 | |
| 1282 | // accept connection? |
| 1283 | const char *szReply = nullptr; |
| 1284 | bool fOK = false; |
| 1285 | |
| 1286 | // search client |
| 1287 | if (!pClient && Pkt.getCCore().getID() != C4ClientIDUnknown) |
| 1288 | pClient = Clients.GetClient(CCore: Pkt.getCCore()); |
| 1289 | |
| 1290 | std::string tmp; |
| 1291 | // check engine version |
| 1292 | bool fWrongPassword = false; |
| 1293 | if (Pkt.getVer() != C4XVERBUILD) |
| 1294 | { |
| 1295 | tmp = std::format(fmt: "wrong engine ({}, I have {})" , args: Pkt.getVer(), C4XVERBUILD); |
| 1296 | szReply = tmp.c_str(); |
| 1297 | fOK = false; |
| 1298 | } |
| 1299 | else |
| 1300 | { |
| 1301 | if (pClient) |
| 1302 | if (CheckConn(CCore: NewCCore, pConn, pClient, szReply)) |
| 1303 | { |
| 1304 | pConn->SetCCore(Pkt.getCCore()); |
| 1305 | // accept |
| 1306 | if (!szReply) szReply = "connection accepted" ; |
| 1307 | fOK = true; |
| 1308 | } |
| 1309 | // client: host connection? |
| 1310 | if (!fOK && !isHost() && Status.getState() == GS_Init && !Clients.GetHost()) |
| 1311 | if (HostConnect(CCore: NewCCore, pConn, szReply)) |
| 1312 | { |
| 1313 | // accept |
| 1314 | if (!szReply) szReply = "host connection accepted" ; |
| 1315 | fOK = true; |
| 1316 | } |
| 1317 | // host: client join? (NewCCore will be changed by Join()!) |
| 1318 | if (!fOK && isHost() && !pClient) |
| 1319 | { |
| 1320 | // check password |
| 1321 | if (!sPassword.isNull() && !SEqual(szStr1: Pkt.getPassword(), szStr2: sPassword.getData())) |
| 1322 | { |
| 1323 | szReply = "wrong password" ; |
| 1324 | fWrongPassword = true; |
| 1325 | } |
| 1326 | // accept join |
| 1327 | else if (Join(CCore&: NewCCore, pConn, szReply)) |
| 1328 | { |
| 1329 | // save core |
| 1330 | pConn->SetCCore(NewCCore); |
| 1331 | // accept |
| 1332 | if (!szReply) szReply = "join accepted" ; |
| 1333 | fOK = true; |
| 1334 | } |
| 1335 | } |
| 1336 | } |
| 1337 | |
| 1338 | // denied? set default reason |
| 1339 | if (!fOK && !szReply) szReply = "connection denied" ; |
| 1340 | |
| 1341 | // OK and already half accepted? Skip (double-checked: ok). |
| 1342 | if (fOK && pConn->isHalfAccepted()) |
| 1343 | return; |
| 1344 | |
| 1345 | // send answer |
| 1346 | C4PacketConnRe pcr(fOK, fWrongPassword, szReply); |
| 1347 | if (!pConn->Send(rPkt: MkC4NetIOPacket(cStatus: PID_ConnRe, Pkt: pcr))) |
| 1348 | return; |
| 1349 | |
| 1350 | // accepted? |
| 1351 | if (fOK) |
| 1352 | { |
| 1353 | // set status |
| 1354 | if (!pConn->isClosed()) |
| 1355 | pConn->SetHalfAccepted(); |
| 1356 | } |
| 1357 | // denied? close |
| 1358 | else |
| 1359 | { |
| 1360 | // log & close |
| 1361 | Logger->info(fmt: "connection by {} ({}) blocked: {}" , args: CCore.getName(), args: pConn->getPeerAddr().ToString(), args&: szReply); |
| 1362 | pConn->Close(); |
| 1363 | } |
| 1364 | } |
| 1365 | |
| 1366 | bool C4Network2::CheckConn(const C4ClientCore &CCore, C4Network2IOConnection *pConn, C4Network2Client *pClient, const char *&szReply) |
| 1367 | { |
| 1368 | if (!pConn || !pClient) return false; |
| 1369 | // already connected? (shouldn't happen really) |
| 1370 | if (pClient->hasConn(pConn)) |
| 1371 | { |
| 1372 | szReply = "already connected" ; return true; |
| 1373 | } |
| 1374 | // check core |
| 1375 | if (CCore.getDiffLevel(CCore2: pClient->getCore()) > C4ClientCoreDL_IDMatch) |
| 1376 | { |
| 1377 | szReply = "wrong client core" ; return false; |
| 1378 | } |
| 1379 | // accept |
| 1380 | return true; |
| 1381 | } |
| 1382 | |
| 1383 | bool C4Network2::HostConnect(const C4ClientCore &CCore, C4Network2IOConnection *pConn, const char *&szReply) |
| 1384 | { |
| 1385 | if (!pConn) return false; |
| 1386 | if (!CCore.isHost()) { szReply = "not host" ; return false; } |
| 1387 | // create client class for host |
| 1388 | // (core is unofficial, see InitClient() - will be overwritten later in HandleJoinData) |
| 1389 | C4Client *pClient = Game.Clients.Add(Core: CCore); |
| 1390 | if (!pClient) return false; |
| 1391 | // accept |
| 1392 | return true; |
| 1393 | } |
| 1394 | |
| 1395 | bool C4Network2::Join(C4ClientCore &CCore, C4Network2IOConnection *pConn, const char *&szReply) |
| 1396 | { |
| 1397 | if (!pConn) return false; |
| 1398 | // security |
| 1399 | if (!isHost()) { szReply = "not host" ; return false; } |
| 1400 | if (!fAllowJoin) { szReply = "join denied" ; return false; } |
| 1401 | if (CCore.getID() != C4ClientIDUnknown) { szReply = "join with set id not allowed" ; return false; } |
| 1402 | // find free client id |
| 1403 | CCore.SetID(iNextClientID++); |
| 1404 | // deactivate - client will have to ask for activation. |
| 1405 | CCore.SetActivated(false); |
| 1406 | // Name already in use? Find unused one |
| 1407 | if (Clients.GetClient(szName: CCore.getName())) |
| 1408 | { |
| 1409 | static_assert(C4Strings::NumberOfCharactersForDigits<std::int32_t> <= C4MaxName); |
| 1410 | |
| 1411 | std::array<char, C4MaxName + 1> newName; |
| 1412 | newName[C4MaxName] = '\0'; |
| 1413 | std::int32_t i{1}; |
| 1414 | std::string_view ccoreName{CCore.getName()}; |
| 1415 | do |
| 1416 | { |
| 1417 | const std::string numberString{std::format(fmt: "{}" , args&: ++i)}; |
| 1418 | const std::size_t numberStartIndex{newName.size() - 1 - numberString.size()}; |
| 1419 | |
| 1420 | const std::size_t copied{ccoreName.copy(str: newName.data(), n: newName.size() - 1)}; |
| 1421 | if (copied < numberStartIndex) |
| 1422 | { |
| 1423 | newName[copied + numberString.copy(s: newName.data() + copied, n: numberString.size())] = '\0'; |
| 1424 | } |
| 1425 | else |
| 1426 | { |
| 1427 | numberString.copy(s: newName.data() + numberStartIndex, n: numberString.size()); |
| 1428 | } |
| 1429 | } |
| 1430 | while (Clients.GetClient(szName: newName.data())); |
| 1431 | |
| 1432 | CCore.SetName(newName.data()); |
| 1433 | } |
| 1434 | // join client |
| 1435 | Game.Control.DoInput(eCtrlType: CID_ClientJoin, pPkt: new C4ControlClientJoin(CCore), eDelivery: CDT_Direct); |
| 1436 | // get client, set status |
| 1437 | C4Network2Client *pClient = Clients.GetClient(CCore); |
| 1438 | if (pClient) pClient->SetStatus(NCS_Joining); |
| 1439 | // ok, client joined. |
| 1440 | return true; |
| 1441 | // Note that the connection isn't fully accepted at this point and won't be |
| 1442 | // associated with the client. The new-created client is waiting for connect. |
| 1443 | // Somewhat ironically, the connection may still timeout (resulting in an instant |
| 1444 | // removal and maybe some funny message sequences). |
| 1445 | // The final client initialization will be done at OnClientConnect. |
| 1446 | } |
| 1447 | |
| 1448 | void C4Network2::HandleConnRe(const C4PacketConnRe &Pkt, C4Network2IOConnection *pConn, C4Network2Client *pClient) |
| 1449 | { |
| 1450 | // Handle the connection request reply. After this handling, the connection should |
| 1451 | // be either fully associated with a client (fully accepted) or closed. |
| 1452 | // Note that auto-accepted connection have to processed here once, too, as the |
| 1453 | // client must get associated with the connection. After doing so, the connection |
| 1454 | // auto-accept flag will be reset to mark the connection fully accepted. |
| 1455 | |
| 1456 | // security |
| 1457 | if (!pConn) return; |
| 1458 | if (!pClient) { pConn->Close(); return; } |
| 1459 | |
| 1460 | // negative reply? |
| 1461 | if (!Pkt.isOK()) |
| 1462 | { |
| 1463 | // wrong password? |
| 1464 | fWrongPassword = Pkt.isPasswordWrong(); |
| 1465 | // show message |
| 1466 | Logger->info(fmt: "connection to {} ({}) refused: {}" , args: pClient->getName(), args: pConn->getPeerAddr().ToString(), args: Pkt.getMsg()); |
| 1467 | // close connection |
| 1468 | pConn->Close(); |
| 1469 | return; |
| 1470 | } |
| 1471 | |
| 1472 | // connection is closed? |
| 1473 | if (!pConn->isOpen()) |
| 1474 | return; |
| 1475 | |
| 1476 | // already accepted? ignore |
| 1477 | if (pConn->isAccepted() && !pConn->isAutoAccepted()) return; |
| 1478 | |
| 1479 | // first connection? |
| 1480 | bool fFirstConnection = !pClient->isConnected(); |
| 1481 | |
| 1482 | // accept connection |
| 1483 | pConn->SetAccepted(); pConn->ResetAutoAccepted(); |
| 1484 | |
| 1485 | // add connection |
| 1486 | pConn->SetCCore(pClient->getCore()); |
| 1487 | if (pConn->getNetClass() == NetIO.MsgIO()) pClient->SetMsgConn(pConn); |
| 1488 | if (pConn->getNetClass() == NetIO.DataIO()) pClient->SetDataConn(pConn); |
| 1489 | |
| 1490 | // add peer connect address to client address list |
| 1491 | if (!pConn->getConnectAddr().IsNull()) |
| 1492 | { |
| 1493 | C4Network2Address Addr(pConn->getConnectAddr(), pConn->getProtocol()); |
| 1494 | pClient->AddAddr(addr: Addr, fAnnounce: Status.getState() != GS_Init); |
| 1495 | } |
| 1496 | |
| 1497 | // handle |
| 1498 | OnConnect(pClient, pConn, szMsg: Pkt.getMsg(), fFirstConnection); |
| 1499 | } |
| 1500 | |
| 1501 | void C4Network2::HandleStatus(const C4Network2Status &nStatus) |
| 1502 | { |
| 1503 | // set |
| 1504 | Status = nStatus; |
| 1505 | // log |
| 1506 | Logger->info(fmt: "going into status {} (tick {})" , args: Status.getStateName(), args: nStatus.getTargetCtrlTick()); |
| 1507 | // reset flags |
| 1508 | fStatusReached = fStatusAck = false; |
| 1509 | // check: reached? |
| 1510 | CheckStatusReached(); |
| 1511 | } |
| 1512 | |
| 1513 | void C4Network2::HandleStatusAck(const C4Network2Status &nStatus, C4Network2Client *pClient) |
| 1514 | { |
| 1515 | // security |
| 1516 | if (!pClient->hasJoinData() || pClient->isRemoved()) return; |
| 1517 | // status doesn't match? |
| 1518 | if (nStatus.getState() != Status.getState() || nStatus.getTargetCtrlTick() < Status.getTargetCtrlTick()) |
| 1519 | return; |
| 1520 | // host: wait until all clients are ready |
| 1521 | if (isHost()) |
| 1522 | { |
| 1523 | // check: target tick change? |
| 1524 | if (!fStatusAck && nStatus.getTargetCtrlTick() > Status.getTargetCtrlTick()) |
| 1525 | // take the new status |
| 1526 | ChangeGameStatus(enState: nStatus.getState(), iTargetCtrlTick: nStatus.getTargetCtrlTick()); |
| 1527 | // already acknowledged? Send another ack |
| 1528 | if (fStatusAck) |
| 1529 | pClient->SendMsg(rPkt: MkC4NetIOPacket(cStatus: PID_StatusAck, Pkt: nStatus)); |
| 1530 | // mark as ready (will clear chase-flag) |
| 1531 | pClient->SetStatus(NCS_Ready); |
| 1532 | // check: everyone ready? |
| 1533 | if (!fStatusAck && fStatusReached) |
| 1534 | CheckStatusAck(); |
| 1535 | } |
| 1536 | else |
| 1537 | { |
| 1538 | // target tick doesn't match? ignore |
| 1539 | if (nStatus.getTargetCtrlTick() != Status.getTargetCtrlTick()) |
| 1540 | return; |
| 1541 | // reached? |
| 1542 | // can be ignored safely otherwise - when the status is reached, we will send |
| 1543 | // status ack on which the host should generate another status ack (see above) |
| 1544 | if (fStatusReached) |
| 1545 | { |
| 1546 | // client: set flags, call handler |
| 1547 | fStatusAck = true; fChasing = false; |
| 1548 | OnStatusAck(); |
| 1549 | } |
| 1550 | } |
| 1551 | } |
| 1552 | |
| 1553 | void C4Network2::HandleActivateReq(int32_t iTick, C4Network2Client *pByClient) |
| 1554 | { |
| 1555 | if (!isHost()) return; |
| 1556 | // not allowed or already activated? ignore |
| 1557 | if (pByClient->isObserver() || pByClient->isActivated()) return; |
| 1558 | // not joined completely yet? ignore |
| 1559 | if (!pByClient->isWaitedFor()) return; |
| 1560 | // check behind limit |
| 1561 | if (isRunning()) |
| 1562 | { |
| 1563 | // make a guess how much the client lags. |
| 1564 | int32_t iLagFrames = BoundBy(bval: pByClient->getMsgConn()->getPingTime() * Game.FPS / 500, lbound: 0, rbound: 100); |
| 1565 | if (iTick < Game.FrameCounter - iLagFrames - C4NetMaxBehind4Activation) |
| 1566 | return; |
| 1567 | } |
| 1568 | // activate him |
| 1569 | Game.Control.DoInput(eCtrlType: CID_ClientUpdate, |
| 1570 | pPkt: new C4ControlClientUpdate(pByClient->getID(), CUT_Activate, true), |
| 1571 | eDelivery: CDT_Sync); |
| 1572 | } |
| 1573 | |
| 1574 | void C4Network2::HandleJoinData(const C4PacketJoinData &rPkt) |
| 1575 | { |
| 1576 | // init only |
| 1577 | if (Status.getState() != GS_Init) |
| 1578 | { |
| 1579 | Logger->info(msg: "unexpected join data received!" ); return; |
| 1580 | } |
| 1581 | // get client ID |
| 1582 | if (rPkt.getClientID() == C4ClientIDUnknown) |
| 1583 | { |
| 1584 | Logger->info(msg: "host didn't set client ID!" ); Clear(); return; |
| 1585 | } |
| 1586 | // set local ID |
| 1587 | ResList.SetLocalID(rPkt.getClientID()); |
| 1588 | Game.Parameters.Clients.SetLocalID(rPkt.getClientID()); |
| 1589 | // read and validate status |
| 1590 | HandleStatus(nStatus: rPkt.getStatus()); |
| 1591 | if (Status.getState() != GS_Lobby && Status.getState() != GS_Pause && Status.getState() != GS_Go) |
| 1592 | { |
| 1593 | Logger->info(fmt: "join data has bad game status: {}" , args: Status.getStateName()); Clear(); return; |
| 1594 | } |
| 1595 | // copy parameters |
| 1596 | Game.Parameters = rPkt.Parameters; |
| 1597 | // set local client |
| 1598 | C4Client *pLocalClient = Game.Clients.getClientByID(iID: rPkt.getClientID()); |
| 1599 | if (!pLocalClient) |
| 1600 | { |
| 1601 | Logger->info(msg: "Could not find local client in join data!" ); Clear(); return; |
| 1602 | } |
| 1603 | // save back dynamic data |
| 1604 | ResDynamic = rPkt.getDynamicCore(); |
| 1605 | iDynamicTick = rPkt.getStartCtrlTick(); |
| 1606 | // initialize control |
| 1607 | Game.Control.ControlRate = rPkt.Parameters.ControlRate; |
| 1608 | pControl->Init(iClientID: rPkt.getClientID(), fHost: false, iStartTick: rPkt.getStartCtrlTick(), fActivated: pLocalClient->isActivated(), pNetwork: this); |
| 1609 | pControl->CopyClientList(rClients: Game.Parameters.Clients); |
| 1610 | // set local core |
| 1611 | NetIO.SetLocalCCore(pLocalClient->getCore()); |
| 1612 | // add the resources to the network ressource list |
| 1613 | Game.Parameters.GameRes.InitNetwork(pNetResList: &ResList); |
| 1614 | // load dynamic |
| 1615 | if (!ResList.AddByCore(Core: ResDynamic)) |
| 1616 | { |
| 1617 | LogFatalNTr(message: "Network: can not not retrieve dynamic!" ); Clear(); return; |
| 1618 | } |
| 1619 | // load player ressources |
| 1620 | Game.Parameters.PlayerInfos.LoadResources(); |
| 1621 | // send additional addresses |
| 1622 | Clients.SendAddresses(pConn: nullptr); |
| 1623 | } |
| 1624 | |
| 1625 | void C4Network2::HandleReadyCheck(const C4PacketReadyCheck &packet) |
| 1626 | { |
| 1627 | C4Client *client{Game.Clients.getClientByID(iID: packet.GetClientID())}; |
| 1628 | if (!client) |
| 1629 | { |
| 1630 | return; |
| 1631 | } |
| 1632 | |
| 1633 | bool ready; |
| 1634 | |
| 1635 | if (packet.VoteRequested()) |
| 1636 | { |
| 1637 | #ifndef USE_CONSOLE |
| 1638 | // ShowModalDlg calls HandleMessage, which can handle additional ready check packets, |
| 1639 | // leading to multiple dialogs. |
| 1640 | if (readyCheckDialog) |
| 1641 | { |
| 1642 | return; |
| 1643 | } |
| 1644 | #endif |
| 1645 | |
| 1646 | if (!client->isHost()) |
| 1647 | { |
| 1648 | Logger->error(fmt: "Got ready check request from non-host client {}!" , args: client->getName()); |
| 1649 | return; |
| 1650 | } |
| 1651 | else if (isHost()) |
| 1652 | { |
| 1653 | Logger->error(fmt: "Got ready check request from client {}, but is host!" , args: client->getName()); |
| 1654 | return; |
| 1655 | } |
| 1656 | |
| 1657 | for (C4Client *clnt{nullptr}; (clnt = Game.Clients.getClient(pAfter: clnt)); ) |
| 1658 | { |
| 1659 | if (!clnt->isHost()) // host state isn't changed |
| 1660 | { |
| 1661 | clnt->SetLobbyReady(false); |
| 1662 | } |
| 1663 | } |
| 1664 | |
| 1665 | #ifndef USE_CONSOLE |
| 1666 | if (pLobby) |
| 1667 | { |
| 1668 | pLobby->CheckReady(check: false); |
| 1669 | #endif |
| 1670 | Application.NotifyUserIfInactive(); |
| 1671 | |
| 1672 | #ifndef USE_CONSOLE |
| 1673 | if (pLobby->CanBeReady()) |
| 1674 | { |
| 1675 | readyCheckDialog = new ReadyCheckDialog; |
| 1676 | ready = Game.pGUI ? Game.pGUI->ShowModalDlg(pDlg: readyCheckDialog, fDestruct: true) : false; |
| 1677 | readyCheckDialog = nullptr; |
| 1678 | } |
| 1679 | else |
| 1680 | #endif |
| 1681 | ready = false; |
| 1682 | |
| 1683 | if (!isLobbyActive()) |
| 1684 | { |
| 1685 | return; |
| 1686 | } |
| 1687 | |
| 1688 | Clients.BroadcastMsgToClients(rPkt: MkC4NetIOPacket(cStatus: PID_ReadyCheck, Pkt: C4PacketReadyCheck{Clients.GetLocal()->getID(), ready ? C4PacketReadyCheck::Ready : C4PacketReadyCheck::NotReady}), includeHost: true); |
| 1689 | |
| 1690 | #ifndef USE_CONSOLE |
| 1691 | pLobby->CheckReady(check: ready); |
| 1692 | #endif |
| 1693 | |
| 1694 | client = Game.Clients.getLocal(); |
| 1695 | |
| 1696 | #ifndef USE_CONSOLE |
| 1697 | } |
| 1698 | else |
| 1699 | { |
| 1700 | ready = false; |
| 1701 | } |
| 1702 | #endif |
| 1703 | } |
| 1704 | else |
| 1705 | { |
| 1706 | ready = packet.IsReady(); |
| 1707 | } |
| 1708 | |
| 1709 | if (!client->isLocal()) |
| 1710 | { |
| 1711 | if (ready) |
| 1712 | { |
| 1713 | Log(id: C4ResStrTableKey::IDS_NET_CLIENT_READY, args: client->getName()); |
| 1714 | } |
| 1715 | else |
| 1716 | { |
| 1717 | Log(id: C4ResStrTableKey::IDS_NET_CLIENT_UNREADY, args: client->getName()); |
| 1718 | } |
| 1719 | } |
| 1720 | |
| 1721 | if (ready != client->isLobbyReady()) |
| 1722 | { |
| 1723 | client->SetLobbyReady(ready); |
| 1724 | |
| 1725 | #ifndef USE_CONSOLE |
| 1726 | if (pLobby) |
| 1727 | { |
| 1728 | pLobby->OnClientReadyStateChange(client); |
| 1729 | } |
| 1730 | #endif |
| 1731 | } |
| 1732 | } |
| 1733 | |
| 1734 | void C4Network2::OnConnect(C4Network2Client *pClient, C4Network2IOConnection *pConn, const char *szMsg, bool fFirstConnection) |
| 1735 | { |
| 1736 | // log |
| 1737 | Logger->info(fmt: "{} {} connected ({}/{}) ({})" , args: (pClient->isHost() ? "host" : "client" ), |
| 1738 | args: pClient->getName(), args: pConn->getPeerAddr().ToString(), |
| 1739 | args: NetIO.getNetIOName(pNetIO: pConn->getNetClass()), args: (szMsg ? szMsg : "" )); |
| 1740 | |
| 1741 | // first connection for this peer? call special handler |
| 1742 | if (fFirstConnection) OnClientConnect(pClient, pConn); |
| 1743 | } |
| 1744 | |
| 1745 | void C4Network2::OnConnectFail(C4Network2IOConnection *pConn) |
| 1746 | { |
| 1747 | Logger->info(fmt: "{} connection to {} failed" , args: NetIO.getNetIOName(pNetIO: pConn->getNetClass()), |
| 1748 | args: pConn->getPeerAddr().ToString()); |
| 1749 | |
| 1750 | // maybe client connection failure |
| 1751 | // (happens if the connection is not fully accepted and the client disconnects. |
| 1752 | // See C4Network2::Join) |
| 1753 | C4Network2Client *pClient = Clients.GetClientByID(iID: pConn->getClientID()); |
| 1754 | if (pClient && !pClient->isConnected()) |
| 1755 | OnClientDisconnect(pClient); |
| 1756 | } |
| 1757 | |
| 1758 | void C4Network2::OnDisconnect(C4Network2Client *pClient, C4Network2IOConnection *pConn) |
| 1759 | { |
| 1760 | Logger->info(fmt: "{} connection to {} ({}) lost" , args: NetIO.getNetIOName(pNetIO: pConn->getNetClass()), |
| 1761 | args: pClient->getName(), args: pConn->getPeerAddr().ToString()); |
| 1762 | |
| 1763 | // connection lost? |
| 1764 | if (!pClient->isConnected()) |
| 1765 | OnClientDisconnect(pClient); |
| 1766 | } |
| 1767 | |
| 1768 | void C4Network2::OnClientConnect(C4Network2Client *pClient, C4Network2IOConnection *pConn) |
| 1769 | { |
| 1770 | // host: new client? |
| 1771 | if (isHost()) |
| 1772 | { |
| 1773 | // dynamic available? |
| 1774 | if (!pClient->hasJoinData()) |
| 1775 | SendJoinData(pClient); |
| 1776 | |
| 1777 | // notice lobby (doesn't do anything atm?) |
| 1778 | C4GameLobby::MainDlg *pDlg = GetLobby(); |
| 1779 | if (isLobbyActive()) pDlg->OnClientConnect(pClient: pClient->getClient(), pConn); |
| 1780 | } |
| 1781 | |
| 1782 | // discover resources |
| 1783 | ResList.OnClientConnect(pConn); |
| 1784 | } |
| 1785 | |
| 1786 | void C4Network2::OnClientDisconnect(C4Network2Client *pClient) |
| 1787 | { |
| 1788 | // league: Notify regular client disconnect within the game |
| 1789 | if (pLeagueClient && (isHost() || pClient->isHost())) LeagueNotifyDisconnect(iClientID: pClient->getID(), eReason: C4LDR_ConnectionFailed); |
| 1790 | // host? Remove this client from the game. |
| 1791 | if (isHost()) |
| 1792 | { |
| 1793 | bool fHadPlayers = !!Game.PlayerInfos.GetPrimaryInfoByClientID(iClientID: pClient->getID()); |
| 1794 | // log |
| 1795 | Logger->info(msg: LoadResStr(id: C4ResStrTableKey::IDS_NET_CLIENTDISCONNECTED, args: pClient->getName())); // silent, because a duplicate message with disconnect reason will follow |
| 1796 | // remove the client |
| 1797 | Game.Clients.CtrlRemove(pClient: pClient->getClient(), szReason: LoadResStr(id: C4ResStrTableKey::IDS_MSG_DISCONNECTED)); |
| 1798 | // stop lobby countdown if players where removed |
| 1799 | if (fHadPlayers && GetLobby() && GetLobby()->IsCountdown()) |
| 1800 | AbortLobbyCountdown(); |
| 1801 | // check status ack (disconnected client might be the last that was waited for) |
| 1802 | CheckStatusAck(); |
| 1803 | // unreached pause/go? retry setting the state with current control tick |
| 1804 | // (client might be the only one claiming to have the given control) |
| 1805 | if (!fStatusReached) |
| 1806 | if (Status.getState() == GS_Go || Status.getState() == GS_Pause) |
| 1807 | ChangeGameStatus(enState: Status.getState(), iTargetCtrlTick: Game.Control.ControlTick); |
| 1808 | } |
| 1809 | // host disconnected? Clear up# |
| 1810 | if (!isHost() && pClient->isHost()) |
| 1811 | { |
| 1812 | const std::string msg{LoadResStr(id: C4ResStrTableKey::IDS_NET_HOSTDISCONNECTED, args: pClient->getName())}; |
| 1813 | LogNTr(message: msg); |
| 1814 | // host connection lost: clear up everything |
| 1815 | Game.RoundResults.EvaluateNetwork(eResult: C4RoundResults::NR_NetError, szResultsString: msg.c_str()); |
| 1816 | Clear(); |
| 1817 | } |
| 1818 | } |
| 1819 | |
| 1820 | void C4Network2::SendJoinData(C4Network2Client *pClient) |
| 1821 | { |
| 1822 | if (pClient->hasJoinData()) return; |
| 1823 | // host only, scenario must be available |
| 1824 | assert(isHost()); |
| 1825 | // dynamic available? |
| 1826 | if (ResDynamic.isNull() || iDynamicTick < Game.Control.ControlTick) |
| 1827 | { |
| 1828 | fDynamicNeeded = true; |
| 1829 | // add synchronization control (will callback, see C4Game::Synchronize) |
| 1830 | Game.Control.DoInput(eCtrlType: CID_Synchronize, pPkt: new C4ControlSynchronize(false, true), eDelivery: CDT_Sync); |
| 1831 | return; |
| 1832 | } |
| 1833 | // save his client ID |
| 1834 | C4PacketJoinData JoinData; |
| 1835 | JoinData.SetClientID(pClient->getID()); |
| 1836 | // save status into packet |
| 1837 | JoinData.SetGameStatus(Status); |
| 1838 | // parameters |
| 1839 | JoinData.Parameters = Game.Parameters; |
| 1840 | // core join data |
| 1841 | JoinData.SetStartCtrlTick(iDynamicTick); |
| 1842 | JoinData.SetDynamicCore(ResDynamic); |
| 1843 | // send |
| 1844 | pClient->SendMsg(rPkt: MkC4NetIOPacket(cStatus: PID_JoinData, Pkt: JoinData)); |
| 1845 | // send addresses |
| 1846 | Clients.SendAddresses(pConn: pClient->getMsgConn()); |
| 1847 | // flag client (he will have to accept the network status sent next) |
| 1848 | pClient->SetStatus(NCS_Chasing); |
| 1849 | if (!iLastChaseTargetUpdate) iLastChaseTargetUpdate = time(timer: nullptr); |
| 1850 | } |
| 1851 | |
| 1852 | C4Network2Res::Ref C4Network2::RetrieveRes(const C4Network2ResCore &Core, int32_t iTimeoutLen, const char *szResName, bool fWaitForCore) |
| 1853 | { |
| 1854 | C4GUI::ProgressDialog *pDlg = nullptr; |
| 1855 | bool fLog = false; |
| 1856 | int32_t iProcess = -1; uint32_t iTimeout = timeGetTime() + iTimeoutLen; |
| 1857 | // wait for ressource |
| 1858 | while (isEnabled()) |
| 1859 | { |
| 1860 | // find ressource |
| 1861 | C4Network2Res::Ref pRes = ResList.getRefRes(iResID: Core.getID()); |
| 1862 | // res not found? |
| 1863 | if (!pRes) |
| 1864 | if (Core.isNull()) |
| 1865 | { |
| 1866 | // should wait for core? |
| 1867 | if (!fWaitForCore) return nullptr; |
| 1868 | } |
| 1869 | else |
| 1870 | { |
| 1871 | // start loading |
| 1872 | pRes = ResList.AddByCore(Core); |
| 1873 | } |
| 1874 | // res found and loaded completely |
| 1875 | else if (!pRes->isLoading()) |
| 1876 | { |
| 1877 | // log |
| 1878 | if (fLog) Log(id: C4ResStrTableKey::IDS_NET_RECEIVED, args&: szResName, args: pRes->getCore().getFileName()); |
| 1879 | // return |
| 1880 | delete pDlg; |
| 1881 | return pRes; |
| 1882 | } |
| 1883 | |
| 1884 | // check: progress? |
| 1885 | if (pRes && pRes->getPresentPercent() != iProcess) |
| 1886 | { |
| 1887 | iProcess = pRes->getPresentPercent(); |
| 1888 | iTimeout = timeGetTime() + iTimeoutLen; |
| 1889 | } |
| 1890 | else |
| 1891 | { |
| 1892 | // if not: check timeout |
| 1893 | if (timeGetTime() > iTimeout) |
| 1894 | { |
| 1895 | LogFatal(id: C4ResStrTableKey::IDS_NET_ERR_RESTIMEOUT, args&: szResName); |
| 1896 | delete pDlg; |
| 1897 | return nullptr; |
| 1898 | } |
| 1899 | } |
| 1900 | |
| 1901 | // log |
| 1902 | if (!fLog) |
| 1903 | { |
| 1904 | Log(id: C4ResStrTableKey::IDS_NET_WAITFORRES, args&: szResName); |
| 1905 | fLog = true; |
| 1906 | } |
| 1907 | // show progress dialog |
| 1908 | if (!pDlg && !Console.Active && Game.pGUI) |
| 1909 | { |
| 1910 | // create |
| 1911 | pDlg = new C4GUI::ProgressDialog(LoadResStr(id: C4ResStrTableKey::IDS_NET_WAITFORRES, args&: szResName).c_str(), |
| 1912 | LoadResStr(id: C4ResStrTableKey::IDS_NET_CAPTION), 100, 0, C4GUI::Ico_NetWait); |
| 1913 | // show dialog |
| 1914 | if (!pDlg->Show(pOnScreen: Game.pGUI, fCB: true)) { delete pDlg; return nullptr; } |
| 1915 | } |
| 1916 | |
| 1917 | // wait |
| 1918 | if (pDlg) |
| 1919 | { |
| 1920 | // set progress bar |
| 1921 | pDlg->SetProgress(iProcess); |
| 1922 | // execute (will do message handling) |
| 1923 | if (!pDlg->Execute()) |
| 1924 | { |
| 1925 | delete pDlg; return nullptr; |
| 1926 | } |
| 1927 | // aborted? |
| 1928 | if (!Game.pGUI) return nullptr; |
| 1929 | if (pDlg->IsAborted()) break; |
| 1930 | } |
| 1931 | else |
| 1932 | { |
| 1933 | if (Application.HandleMessage(iTimeout: iTimeout - timeGetTime()) == HR_Failure) |
| 1934 | { |
| 1935 | return nullptr; |
| 1936 | } |
| 1937 | } |
| 1938 | } |
| 1939 | // aborted |
| 1940 | if (!Game.pGUI) return nullptr; |
| 1941 | delete pDlg; |
| 1942 | return nullptr; |
| 1943 | } |
| 1944 | |
| 1945 | bool C4Network2::CreateDynamic(bool fInit) |
| 1946 | { |
| 1947 | if (!isHost()) return false; |
| 1948 | // remove all existing dynamic data |
| 1949 | RemoveDynamic(); |
| 1950 | // log |
| 1951 | Log(id: C4ResStrTableKey::IDS_NET_SAVING); |
| 1952 | // compose file name |
| 1953 | char szDynamicBase[_MAX_PATH + 1], szDynamicFilename[_MAX_PATH + 1]; |
| 1954 | FormatWithNull(buf&: szDynamicBase, fmt: "{}Dyn{}" , args: +Config.Network.WorkPath, args: GetFilename(path: Game.ScenarioFilename)); |
| 1955 | if (!ResList.FindTempResFileName(szFilename: szDynamicBase, pTarget: szDynamicFilename)) |
| 1956 | Log(id: C4ResStrTableKey::IDS_NET_SAVE_ERR_CREATEDYNFILE); |
| 1957 | // save dynamic data |
| 1958 | C4GameSaveNetwork SaveGame(fInit); |
| 1959 | if (!SaveGame.Save(szFilename: szDynamicFilename) || !SaveGame.Close()) |
| 1960 | { |
| 1961 | Log(id: C4ResStrTableKey::IDS_NET_SAVE_ERR_SAVEDYNFILE); return false; |
| 1962 | } |
| 1963 | // add ressource |
| 1964 | C4Network2Res::Ref pRes = ResList.AddByFile(strFilePath: szDynamicFilename, fTemp: true, eType: NRT_Dynamic); |
| 1965 | if (!pRes) { Log(id: C4ResStrTableKey::IDS_NET_SAVE_ERR_ADDDYNDATARES); return false; } |
| 1966 | // save |
| 1967 | ResDynamic = pRes->getCore(); |
| 1968 | iDynamicTick = Game.Control.getNextControlTick(); |
| 1969 | fDynamicNeeded = false; |
| 1970 | // ok |
| 1971 | return true; |
| 1972 | } |
| 1973 | |
| 1974 | void C4Network2::RemoveDynamic() |
| 1975 | { |
| 1976 | C4Network2Res::Ref pRes = ResList.getRefRes(iResID: ResDynamic.getID()); |
| 1977 | if (pRes) pRes->Remove(); |
| 1978 | ResDynamic.Clear(); |
| 1979 | iDynamicTick = -1; |
| 1980 | } |
| 1981 | |
| 1982 | bool C4Network2::isFrozen() const |
| 1983 | { |
| 1984 | // "frozen" means all clients are garantueed to be in the same tick. |
| 1985 | // This is only the case if the game is not started yet (lobby) or the |
| 1986 | // tick has been ensured (pause) and acknowledged by all joined clients. |
| 1987 | // Note unjoined clients must be ignored here - they can't be faster than |
| 1988 | // the host, anyway. |
| 1989 | if (Status.getState() == GS_Lobby) return true; |
| 1990 | if (Status.getState() == GS_Pause && fStatusAck) return true; |
| 1991 | return false; |
| 1992 | } |
| 1993 | |
| 1994 | bool C4Network2::ChangeGameStatus(C4NetGameState enState, int32_t iTargetCtrlTick, int32_t iCtrlMode) |
| 1995 | { |
| 1996 | // change game status, announce. Can only be done by host. |
| 1997 | if (!isHost()) return false; |
| 1998 | // set status |
| 1999 | Status.Set(enState, inTargetTick: iTargetCtrlTick); |
| 2000 | // update reference |
| 2001 | InvalidateReference(); |
| 2002 | // control mode change? |
| 2003 | if (iCtrlMode >= 0) Status.SetCtrlMode(iCtrlMode); |
| 2004 | // log |
| 2005 | Logger->info(fmt: "going into status {} (tick {})" , args: Status.getStateName(), args&: iTargetCtrlTick); |
| 2006 | // set flags |
| 2007 | Clients.ResetReady(); |
| 2008 | fStatusReached = fStatusAck = false; |
| 2009 | // send new status to all clients |
| 2010 | Clients.BroadcastMsgToClients(rPkt: MkC4NetIOPacket(cStatus: PID_Status, Pkt: Status)); |
| 2011 | // check reach/ack |
| 2012 | CheckStatusReached(); |
| 2013 | // ok |
| 2014 | return true; |
| 2015 | } |
| 2016 | |
| 2017 | void C4Network2::CheckStatusReached(bool fFromFinalInit) |
| 2018 | { |
| 2019 | // already reached? |
| 2020 | if (fStatusReached) return; |
| 2021 | if (Status.getState() == GS_Lobby) |
| 2022 | fStatusReached = fLobbyRunning; |
| 2023 | // game go / pause: control must be initialized and target tick reached |
| 2024 | else if (Status.getState() == GS_Go || Status.getState() == GS_Pause) |
| 2025 | { |
| 2026 | if (Game.IsRunning || fFromFinalInit) |
| 2027 | { |
| 2028 | // Make sure we have reached the tick and the control queue is empty (except for chasing) |
| 2029 | if (Game.Control.CtrlTickReached(iTick: Status.getTargetCtrlTick()) && |
| 2030 | (fChasing || !pControl->CtrlReady(iTick: Game.Control.ControlTick))) |
| 2031 | fStatusReached = true; |
| 2032 | else |
| 2033 | { |
| 2034 | // run ctrl so the tick can be reached |
| 2035 | pControl->SetRunning(fnRunning: true, inTargetTick: Status.getTargetCtrlTick()); |
| 2036 | Game.HaltCount = 0; |
| 2037 | Console.UpdateHaltCtrls(fHalt: !!Game.HaltCount); |
| 2038 | } |
| 2039 | } |
| 2040 | } |
| 2041 | if (!fStatusReached) return; |
| 2042 | // call handler |
| 2043 | OnStatusReached(); |
| 2044 | // host? |
| 2045 | if (isHost()) |
| 2046 | // all clients ready? |
| 2047 | CheckStatusAck(); |
| 2048 | else |
| 2049 | { |
| 2050 | Status.SetTargetTick(Game.Control.ControlTick); |
| 2051 | // send response to host |
| 2052 | Clients.SendMsgToHost(rPkt: MkC4NetIOPacket(cStatus: PID_StatusAck, Pkt: Status)); |
| 2053 | // do delayed activation request |
| 2054 | if (fDelayedActivateReq) |
| 2055 | { |
| 2056 | fDelayedActivateReq = false; |
| 2057 | RequestActivate(); |
| 2058 | } |
| 2059 | } |
| 2060 | } |
| 2061 | |
| 2062 | void C4Network2::CheckStatusAck() |
| 2063 | { |
| 2064 | // host only |
| 2065 | if (!isHost()) return; |
| 2066 | // status must be reached and not yet acknowledged |
| 2067 | if (!fStatusReached || fStatusAck) return; |
| 2068 | // all clients ready? |
| 2069 | if (fStatusAck = Clients.AllClientsReady()) |
| 2070 | { |
| 2071 | // pause/go: check for sync control that can be executed |
| 2072 | if (Status.getState() == GS_Go || Status.getState() == GS_Pause) |
| 2073 | pControl->ExecSyncControl(); |
| 2074 | // broadcast ack |
| 2075 | Clients.BroadcastMsgToClients(rPkt: MkC4NetIOPacket(cStatus: PID_StatusAck, Pkt: Status)); |
| 2076 | // handle |
| 2077 | OnStatusAck(); |
| 2078 | } |
| 2079 | } |
| 2080 | |
| 2081 | void C4Network2::OnStatusReached() |
| 2082 | { |
| 2083 | // stop ctrl, wait for ack |
| 2084 | if (pControl->IsEnabled()) |
| 2085 | { |
| 2086 | Console.UpdateHaltCtrls(fHalt: !!++Game.HaltCount); |
| 2087 | pControl->SetRunning(fnRunning: false); |
| 2088 | } |
| 2089 | } |
| 2090 | |
| 2091 | void C4Network2::OnStatusAck() |
| 2092 | { |
| 2093 | // log it |
| 2094 | Logger->info(fmt: "status {} (tick {}) reached" , args: Status.getStateName(), args: Status.getTargetCtrlTick()); |
| 2095 | // pause? |
| 2096 | if (Status.getState() == GS_Pause) |
| 2097 | { |
| 2098 | // set halt-flag (show hold message) |
| 2099 | Console.UpdateHaltCtrls(fHalt: !!++Game.HaltCount); |
| 2100 | } |
| 2101 | // go? |
| 2102 | if (Status.getState() == GS_Go) |
| 2103 | { |
| 2104 | // set mode |
| 2105 | pControl->SetCtrlMode(static_cast<C4GameControlNetworkMode>(Status.getCtrlMode())); |
| 2106 | // notify player list of reached status - will add some input to the queue |
| 2107 | Players.OnStatusGoReached(); |
| 2108 | // start ctrl |
| 2109 | pControl->SetRunning(fnRunning: true); |
| 2110 | // reset halt-flag |
| 2111 | Game.HaltCount = 0; |
| 2112 | Console.UpdateHaltCtrls(fHalt: !!Game.HaltCount); |
| 2113 | } |
| 2114 | } |
| 2115 | |
| 2116 | void C4Network2::RequestActivate() |
| 2117 | { |
| 2118 | // neither observer nor activated? |
| 2119 | if (Game.Clients.getLocal()->isObserver() || Game.Clients.getLocal()->isActivated()) |
| 2120 | { |
| 2121 | iLastActivateRequest = 0; |
| 2122 | return; |
| 2123 | } |
| 2124 | // host? just do it |
| 2125 | if (fHost) |
| 2126 | { |
| 2127 | // activate him |
| 2128 | Game.Control.DoInput(eCtrlType: CID_ClientUpdate, |
| 2129 | pPkt: new C4ControlClientUpdate(C4ClientIDHost, CUT_Activate, true), |
| 2130 | eDelivery: CDT_Sync); |
| 2131 | return; |
| 2132 | } |
| 2133 | // ensure interval |
| 2134 | if (iLastActivateRequest && timeGetTime() < iLastActivateRequest + C4NetActivationReqInterval) |
| 2135 | return; |
| 2136 | // status not reached yet? May be chasing, let's delay this. |
| 2137 | if (!fStatusReached) |
| 2138 | { |
| 2139 | fDelayedActivateReq = true; |
| 2140 | return; |
| 2141 | } |
| 2142 | // request |
| 2143 | Clients.SendMsgToHost(rPkt: MkC4NetIOPacket(cStatus: PID_ClientActReq, Pkt: C4PacketActivateReq(Game.FrameCounter))); |
| 2144 | // store time |
| 2145 | iLastActivateRequest = timeGetTime(); |
| 2146 | } |
| 2147 | |
| 2148 | void C4Network2::DeactivateInactiveClients() |
| 2149 | { |
| 2150 | // host only |
| 2151 | if (!isHost()) return; |
| 2152 | // update activity |
| 2153 | Clients.UpdateClientActivity(); |
| 2154 | // find clients to deactivate |
| 2155 | for (C4Network2Client *pClient = Clients.GetNextClient(pClient: nullptr); pClient; pClient = Clients.GetNextClient(pClient)) |
| 2156 | if (!pClient->isLocal() && pClient->isActivated()) |
| 2157 | if (pClient->getLastActivity() + C4NetDeactivationDelay < Game.FrameCounter) |
| 2158 | Game.Control.DoInput(eCtrlType: CID_ClientUpdate, pPkt: new C4ControlClientUpdate(pClient->getID(), CUT_Activate, false), eDelivery: CDT_Sync); |
| 2159 | } |
| 2160 | |
| 2161 | void C4Network2::UpdateChaseTarget() |
| 2162 | { |
| 2163 | // no chasing clients? |
| 2164 | C4Network2Client *pClient; |
| 2165 | for (pClient = Clients.GetNextClient(pClient: nullptr); pClient; pClient = Clients.GetNextClient(pClient)) |
| 2166 | if (pClient->isChasing()) |
| 2167 | break; |
| 2168 | if (!pClient) |
| 2169 | { |
| 2170 | iLastChaseTargetUpdate = 0; |
| 2171 | return; |
| 2172 | } |
| 2173 | // not time for an update? |
| 2174 | if (!iLastChaseTargetUpdate || static_cast<time_t>(iLastChaseTargetUpdate + C4NetChaseTargetUpdateInterval) > time(timer: nullptr)) |
| 2175 | return; |
| 2176 | // copy status, set current tick |
| 2177 | C4Network2Status ChaseTarget = Status; |
| 2178 | ChaseTarget.SetTargetTick(Game.Control.ControlTick); |
| 2179 | // send to everyone involved |
| 2180 | for (pClient = Clients.GetNextClient(pClient: nullptr); pClient; pClient = Clients.GetNextClient(pClient)) |
| 2181 | if (pClient->isChasing()) |
| 2182 | pClient->SendMsg(rPkt: MkC4NetIOPacket(cStatus: PID_Status, Pkt: ChaseTarget)); |
| 2183 | iLastChaseTargetUpdate = time(timer: nullptr); |
| 2184 | } |
| 2185 | |
| 2186 | void C4Network2::LeagueGameEvaluate(const char *szRecordName, const uint8_t *pRecordSHA) |
| 2187 | { |
| 2188 | // already off? |
| 2189 | if (!pLeagueClient) return; |
| 2190 | // already evaluated? |
| 2191 | if (fLeagueEndSent) return; |
| 2192 | // end |
| 2193 | LeagueEnd(szRecordName, pRecordSHA); |
| 2194 | } |
| 2195 | |
| 2196 | void C4Network2::LeagueSignupDisable() |
| 2197 | { |
| 2198 | // already off? |
| 2199 | if (!pLeagueClient) return; |
| 2200 | // no post-disable if league is active |
| 2201 | if (pLeagueClient && Game.Parameters.isLeague()) return; |
| 2202 | // signup end |
| 2203 | LeagueEnd(); DeinitLeague(); |
| 2204 | } |
| 2205 | |
| 2206 | bool C4Network2::LeagueSignupEnable() |
| 2207 | { |
| 2208 | // already running? |
| 2209 | if (pLeagueClient) return true; |
| 2210 | // Start it! |
| 2211 | if (InitLeague(pCancel: nullptr) && LeagueStart(pCancel: nullptr)) return true; |
| 2212 | // Failure :'( |
| 2213 | DeinitLeague(); |
| 2214 | return false; |
| 2215 | } |
| 2216 | |
| 2217 | void C4Network2::InvalidateReference() |
| 2218 | { |
| 2219 | // Update both local and league reference as soon as possible |
| 2220 | iLastReferenceUpdate = 0; |
| 2221 | iLeagueUpdateDelay = C4NetMinLeagueUpdateInterval; |
| 2222 | } |
| 2223 | |
| 2224 | bool C4Network2::InitLeague(bool *pCancel) |
| 2225 | { |
| 2226 | if (fHost) |
| 2227 | { |
| 2228 | // Clear parameters |
| 2229 | MasterServerAddress.Clear(); |
| 2230 | Game.Parameters.League.Clear(); |
| 2231 | Game.Parameters.LeagueAddress.Clear(); |
| 2232 | delete pLeagueClient; pLeagueClient = nullptr; |
| 2233 | |
| 2234 | // Not needed? |
| 2235 | if (!Config.Network.MasterServerSignUp && !Config.Network.LeagueServerSignUp) |
| 2236 | return true; |
| 2237 | |
| 2238 | // Save address |
| 2239 | MasterServerAddress = Config.Network.GetLeagueServerAddress(); |
| 2240 | if (Config.Network.LeagueServerSignUp) |
| 2241 | { |
| 2242 | Game.Parameters.LeagueAddress = MasterServerAddress; |
| 2243 | // enforce some league rules |
| 2244 | Game.Parameters.EnforceLeagueRules(pScenario: &Game.C4S); |
| 2245 | } |
| 2246 | } |
| 2247 | else |
| 2248 | { |
| 2249 | // Get league server from parameters |
| 2250 | MasterServerAddress = Game.Parameters.LeagueAddress; |
| 2251 | |
| 2252 | // Not needed? |
| 2253 | if (!MasterServerAddress.getLength()) |
| 2254 | return true; |
| 2255 | } |
| 2256 | |
| 2257 | // Init |
| 2258 | pLeagueClient = new C4LeagueClient(); |
| 2259 | if (!pLeagueClient->Init() || |
| 2260 | !pLeagueClient->SetServer(serverAddress: MasterServerAddress.getData())) |
| 2261 | { |
| 2262 | // Log message |
| 2263 | const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUEINIT, args: pLeagueClient->GetError())}; |
| 2264 | LogFatalNTr(message: message.c_str()); |
| 2265 | // Clear league |
| 2266 | delete pLeagueClient; pLeagueClient = nullptr; |
| 2267 | if (fHost) |
| 2268 | Game.Parameters.LeagueAddress.Clear(); |
| 2269 | // Show message, allow abort |
| 2270 | bool fResult = true; |
| 2271 | if (Game.pGUI && !Console.Active) |
| 2272 | fResult = Game.pGUI->ShowMessageModal(szMessage: message.c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE), |
| 2273 | dwButtons: (pCancel ? C4GUI::MessageDialog::btnOK : 0) | C4GUI::MessageDialog::btnAbort, |
| 2274 | icoIcon: C4GUI::Ico_Error); |
| 2275 | if (pCancel) *pCancel = fResult; |
| 2276 | return false; |
| 2277 | } |
| 2278 | |
| 2279 | // OK |
| 2280 | return true; |
| 2281 | } |
| 2282 | |
| 2283 | void C4Network2::DeinitLeague() |
| 2284 | { |
| 2285 | // league clear |
| 2286 | MasterServerAddress.Clear(); |
| 2287 | Game.Parameters.League.Clear(); |
| 2288 | Game.Parameters.LeagueAddress.Clear(); |
| 2289 | delete pLeagueClient; pLeagueClient = nullptr; |
| 2290 | } |
| 2291 | |
| 2292 | bool C4Network2::LeagueStart(bool *pCancel) |
| 2293 | { |
| 2294 | // Not needed? |
| 2295 | if (!pLeagueClient || !fHost) |
| 2296 | return true; |
| 2297 | |
| 2298 | // Default |
| 2299 | if (pCancel) *pCancel = true; |
| 2300 | |
| 2301 | // Do update |
| 2302 | C4Network2Reference Ref; |
| 2303 | Ref.InitLocal(pGame: &Game); |
| 2304 | if (!pLeagueClient->Start(Ref)) |
| 2305 | { |
| 2306 | // Log message |
| 2307 | const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE_STARTGAME, args: pLeagueClient->GetError())}; |
| 2308 | LogFatalNTr(message: message.c_str()); |
| 2309 | // Show message |
| 2310 | if (Game.pGUI && !Console.Active) |
| 2311 | { |
| 2312 | // Show option to cancel, if possible |
| 2313 | bool fResult = Game.pGUI->ShowMessageModal( |
| 2314 | szMessage: message.c_str(), |
| 2315 | szCaption: LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE), |
| 2316 | dwButtons: pCancel ? (C4GUI::MessageDialog::btnOK | C4GUI::MessageDialog::btnAbort) : C4GUI::MessageDialog::btnOK, |
| 2317 | icoIcon: C4GUI::Ico_Error); |
| 2318 | if (pCancel) |
| 2319 | *pCancel = !fResult; |
| 2320 | } |
| 2321 | // Failed |
| 2322 | return false; |
| 2323 | } |
| 2324 | |
| 2325 | InitPuncher(); |
| 2326 | |
| 2327 | // Let's wait for response |
| 2328 | const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_NET_LEAGUE_REGGAME, args: pLeagueClient->getServerName())}; |
| 2329 | LogNTr(message); |
| 2330 | // Set up a dialog |
| 2331 | C4GUI::MessageDialog *pDlg = nullptr; |
| 2332 | if (Game.pGUI && !Console.Active) |
| 2333 | { |
| 2334 | // create & show |
| 2335 | pDlg = new C4GUI::MessageDialog(message.c_str(), LoadResStr(id: C4ResStrTableKey::IDS_NET_LEAGUE_STARTGAME), |
| 2336 | C4GUI::MessageDialog::btnAbort, C4GUI::Ico_NetWait, C4GUI::MessageDialog::dsRegular); |
| 2337 | if (!pDlg || !pDlg->Show(pOnScreen: Game.pGUI, fCB: true)) return false; |
| 2338 | } |
| 2339 | // Wait for response |
| 2340 | while (pLeagueClient->isBusy()) |
| 2341 | { |
| 2342 | // Execute GUI |
| 2343 | if (Application.HandleMessage(iTimeout: 100) == HR_Failure || |
| 2344 | (pDlg && pDlg->IsAborted())) |
| 2345 | { |
| 2346 | // Clear up |
| 2347 | if (Game.pGUI) delete pDlg; |
| 2348 | return false; |
| 2349 | } |
| 2350 | // Check if league server has responded |
| 2351 | if (!pLeagueClient->Execute(iMaxTime: 0)) |
| 2352 | break; |
| 2353 | } |
| 2354 | // Close dialog |
| 2355 | if (Game.pGUI && pDlg) |
| 2356 | { |
| 2357 | pDlg->Close(fOK: true); |
| 2358 | delete pDlg; |
| 2359 | } |
| 2360 | // Error? |
| 2361 | StdStrBuf LeagueServerMessage, League, StreamingAddr; |
| 2362 | int32_t Seed = Game.Parameters.RandomSeed, MaxPlayersLeague = 0; |
| 2363 | if (!pLeagueClient->isSuccess() || |
| 2364 | !pLeagueClient->GetStartReply(pMessage: &LeagueServerMessage, pLeague: &League, pStreamingAddr: &StreamingAddr, pSeed: &Seed, pMaxPlayers: &MaxPlayersLeague)) |
| 2365 | { |
| 2366 | const char *pError = pLeagueClient->GetError() ? pLeagueClient->GetError() : |
| 2367 | LeagueServerMessage.getLength() ? LeagueServerMessage.getData() : |
| 2368 | LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE_EMPTYREPLY); |
| 2369 | const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE_REGGAME, args&: pError)}; |
| 2370 | // Log message |
| 2371 | LogNTr(message); |
| 2372 | // Show message |
| 2373 | if (Game.pGUI && !Console.Active) |
| 2374 | { |
| 2375 | // Show option to cancel, if possible |
| 2376 | bool fResult = Game.pGUI->ShowMessageModal( |
| 2377 | szMessage: message.c_str(), |
| 2378 | szCaption: LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE), |
| 2379 | dwButtons: pCancel ? (C4GUI::MessageDialog::btnOK | C4GUI::MessageDialog::btnAbort) : C4GUI::MessageDialog::btnOK, |
| 2380 | icoIcon: C4GUI::Ico_Error); |
| 2381 | if (pCancel) |
| 2382 | *pCancel = !fResult; |
| 2383 | } |
| 2384 | // Failed |
| 2385 | return false; |
| 2386 | } |
| 2387 | |
| 2388 | // Show message |
| 2389 | if (LeagueServerMessage.getLength()) |
| 2390 | { |
| 2391 | const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_MSG_LEAGUEGAMESIGNUP, args: pLeagueClient->getServerName(), args: LeagueServerMessage.getData())}; |
| 2392 | // Log message |
| 2393 | LogNTr(message); |
| 2394 | // Show message |
| 2395 | if (Game.pGUI && !Console.Active) |
| 2396 | { |
| 2397 | // Show option to cancel, if possible |
| 2398 | bool fResult = Game.pGUI->ShowMessageModal( |
| 2399 | szMessage: message.c_str(), |
| 2400 | szCaption: LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE), |
| 2401 | dwButtons: pCancel ? (C4GUI::MessageDialog::btnOK | C4GUI::MessageDialog::btnAbort) : C4GUI::MessageDialog::btnOK, |
| 2402 | icoIcon: C4GUI::Ico_Error); |
| 2403 | if (pCancel) |
| 2404 | *pCancel = !fResult; |
| 2405 | if (!fResult) |
| 2406 | { |
| 2407 | LeagueEnd(); DeinitLeague(); |
| 2408 | return false; |
| 2409 | } |
| 2410 | } |
| 2411 | } |
| 2412 | |
| 2413 | // Set game parameters for league game |
| 2414 | Game.Parameters.League = League; |
| 2415 | Game.Parameters.RandomSeed = Seed; |
| 2416 | if (MaxPlayersLeague) |
| 2417 | Game.Parameters.MaxPlayers = MaxPlayersLeague; |
| 2418 | if (!League.getLength()) |
| 2419 | { |
| 2420 | Game.Parameters.LeagueAddress.Clear(); |
| 2421 | Game.Parameters.StreamAddress.Clear(); |
| 2422 | } |
| 2423 | else |
| 2424 | { |
| 2425 | Game.Parameters.StreamAddress = StreamingAddr; |
| 2426 | |
| 2427 | #ifndef USE_CONSOLE |
| 2428 | // Make sure to deactivate async control mode (except for dedicated builds) |
| 2429 | if (Status.getCtrlMode() == CNM_Async) |
| 2430 | Status.SetCtrlMode(CNM_Central); |
| 2431 | #endif |
| 2432 | } |
| 2433 | |
| 2434 | // All ok |
| 2435 | fLeagueEndSent = false; |
| 2436 | return true; |
| 2437 | } |
| 2438 | |
| 2439 | bool C4Network2::LeagueUpdate() |
| 2440 | { |
| 2441 | // Not needed? |
| 2442 | if (!pLeagueClient || !fHost) |
| 2443 | return true; |
| 2444 | |
| 2445 | // League client currently busy? |
| 2446 | if (pLeagueClient->isBusy()) |
| 2447 | return true; |
| 2448 | |
| 2449 | // Create reference |
| 2450 | C4Network2Reference Ref; |
| 2451 | Ref.InitLocal(pGame: &Game); |
| 2452 | |
| 2453 | // Do update |
| 2454 | if (!pLeagueClient->Update(Ref)) |
| 2455 | { |
| 2456 | // Log |
| 2457 | Log(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE_UPDATEGAME, args: pLeagueClient->GetError()); |
| 2458 | return false; |
| 2459 | } |
| 2460 | |
| 2461 | // Timing |
| 2462 | iLastLeagueUpdate = time(timer: nullptr); |
| 2463 | iLeagueUpdateDelay = Config.Network.MasterReferencePeriod; |
| 2464 | |
| 2465 | return true; |
| 2466 | } |
| 2467 | |
| 2468 | bool C4Network2::LeagueUpdateProcessReply() |
| 2469 | { |
| 2470 | // safety: A reply must be present |
| 2471 | assert(pLeagueClient); |
| 2472 | assert(fHost); |
| 2473 | assert(!pLeagueClient->isBusy()); |
| 2474 | assert(pLeagueClient->getCurrentAction() == C4LA_Update); |
| 2475 | // check reply success |
| 2476 | C4ClientPlayerInfos PlayerLeagueInfos; |
| 2477 | StdStrBuf LeagueServerMessage; |
| 2478 | bool fSucc = pLeagueClient->isSuccess() && pLeagueClient->GetUpdateReply(pMessage: &LeagueServerMessage, pPlayerLeagueInfos: &PlayerLeagueInfos); |
| 2479 | pLeagueClient->ResetCurrentAction(); |
| 2480 | if (!fSucc) |
| 2481 | { |
| 2482 | const char *pError = pLeagueClient->GetError() ? pLeagueClient->GetError() : |
| 2483 | LeagueServerMessage.getLength() ? LeagueServerMessage.getData() : |
| 2484 | LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE_EMPTYREPLY); |
| 2485 | const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE_UPDATEGAME, args&: pError)}; |
| 2486 | // Show message - no dialog, because it's not really fatal and might happen in the running game |
| 2487 | LogNTr(message); |
| 2488 | return false; |
| 2489 | } |
| 2490 | // evaluate reply: Transfer data to players |
| 2491 | // Take round results |
| 2492 | C4PlayerInfoList &TargetList = Game.PlayerInfos; |
| 2493 | C4ClientPlayerInfos *pInfos; C4PlayerInfo *pInfo, *pResultInfo; |
| 2494 | for (int iClient = 0; pInfos = TargetList.GetIndexedInfo(iIndex: iClient); iClient++) |
| 2495 | for (int iInfo = 0; pInfo = pInfos->GetPlayerInfo(iIndex: iInfo); iInfo++) |
| 2496 | if (pResultInfo = PlayerLeagueInfos.GetPlayerInfoByID(id: pInfo->GetID())) |
| 2497 | { |
| 2498 | int32_t iLeagueProjectedGain = pResultInfo->GetLeagueProjectedGain(); |
| 2499 | if (iLeagueProjectedGain != pInfo->GetLeagueProjectedGain()) |
| 2500 | { |
| 2501 | pInfo->SetLeagueProjectedGain(iLeagueProjectedGain); |
| 2502 | pInfos->SetUpdated(); |
| 2503 | } |
| 2504 | } |
| 2505 | // transfer info update to other clients |
| 2506 | Players.SendUpdatedPlayers(); |
| 2507 | // if lobby is open, notify lobby of updated players |
| 2508 | if (pLobby) pLobby->OnPlayersChange(); |
| 2509 | // OMFG SUCCESS! |
| 2510 | return true; |
| 2511 | } |
| 2512 | |
| 2513 | bool C4Network2::LeagueEnd(const char *szRecordName, const uint8_t *pRecordSHA) |
| 2514 | { |
| 2515 | C4RoundResultsPlayers RoundResults; |
| 2516 | std::string resultMessage; |
| 2517 | bool fIsError = true; |
| 2518 | |
| 2519 | // Not needed? |
| 2520 | if (!pLeagueClient || !fHost || fLeagueEndSent) |
| 2521 | return true; |
| 2522 | |
| 2523 | // Make sure league client is available |
| 2524 | LeagueWaitNotBusy(); |
| 2525 | |
| 2526 | // Try until either aborted or successful |
| 2527 | const int MAX_RETRIES = 10; |
| 2528 | for (int iRetry = 0; iRetry < MAX_RETRIES; iRetry++) |
| 2529 | { |
| 2530 | // Do update |
| 2531 | C4Network2Reference Ref; |
| 2532 | Ref.InitLocal(pGame: &Game); |
| 2533 | if (!pLeagueClient->End(Ref, szRecordName, pRecordSHA)) |
| 2534 | { |
| 2535 | // Log message |
| 2536 | resultMessage = LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE_FINISHGAME, args: pLeagueClient->GetError()); |
| 2537 | LogNTr(message: resultMessage); |
| 2538 | // Show message, allow retry |
| 2539 | if (!Game.pGUI || Console.Active) break; |
| 2540 | bool fRetry = Game.pGUI->ShowMessageModal(szMessage: resultMessage.c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE), |
| 2541 | dwButtons: C4GUI::MessageDialog::btnRetryAbort, icoIcon: C4GUI::Ico_Error); |
| 2542 | if (fRetry) continue; |
| 2543 | break; |
| 2544 | } |
| 2545 | // Let's wait for response |
| 2546 | const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_NET_LEAGUE_SENDRESULT, args: pLeagueClient->getServerName())}; |
| 2547 | LogNTr(message); |
| 2548 | // Wait for response |
| 2549 | while (pLeagueClient->isBusy()) |
| 2550 | { |
| 2551 | // Check if league server has responded |
| 2552 | if (!pLeagueClient->Execute(iMaxTime: 100)) |
| 2553 | break; |
| 2554 | } |
| 2555 | // Error? |
| 2556 | StdStrBuf LeagueServerMessage; |
| 2557 | if (!pLeagueClient->isSuccess() || !pLeagueClient->GetEndReply(pMessage: &LeagueServerMessage, pRoundResults: &RoundResults)) |
| 2558 | { |
| 2559 | const char *pError = pLeagueClient->GetError() ? pLeagueClient->GetError() : |
| 2560 | LeagueServerMessage.getLength() ? LeagueServerMessage.getData() : |
| 2561 | LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE_EMPTYREPLY); |
| 2562 | resultMessage = LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE_SENDRESULT, args&: pError); |
| 2563 | if (!Game.pGUI || Console.Active) continue; |
| 2564 | // Only retry if we didn't get an answer from the league server |
| 2565 | bool fRetry = !pLeagueClient->isSuccess(); |
| 2566 | fRetry = Game.pGUI->ShowMessageModal(szMessage: resultMessage.c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_NET_ERR_LEAGUE), |
| 2567 | dwButtons: fRetry ? C4GUI::MessageDialog::btnRetryAbort : C4GUI::MessageDialog::btnAbort, |
| 2568 | icoIcon: C4GUI::Ico_Error); |
| 2569 | if (fRetry) continue; |
| 2570 | } |
| 2571 | else |
| 2572 | { |
| 2573 | // All OK! |
| 2574 | resultMessage = LoadResStrChoice(condition: Game.Parameters.isLeague(), ifTrue: C4ResStrTableKey::IDS_MSG_LEAGUEEVALUATIONSUCCESSFU, ifFalse: C4ResStrTableKey::IDS_MSG_INTERNETGAMEEVALUATED); |
| 2575 | fIsError = false; |
| 2576 | } |
| 2577 | // Done |
| 2578 | break; |
| 2579 | } |
| 2580 | |
| 2581 | // Show message |
| 2582 | LogNTr(message: resultMessage); |
| 2583 | |
| 2584 | // Take round results |
| 2585 | Game.RoundResults.EvaluateLeague(szResultMsg: resultMessage.c_str(), fSuccess: !fIsError, rLeagueInfo: RoundResults); |
| 2586 | |
| 2587 | // Send round results to other clients |
| 2588 | C4PacketLeagueRoundResults LeagueUpdatePacket(resultMessage.c_str(), !fIsError, RoundResults); |
| 2589 | Clients.BroadcastMsgToClients(rPkt: MkC4NetIOPacket(cStatus: PID_LeagueRoundResults, Pkt: LeagueUpdatePacket)); |
| 2590 | |
| 2591 | // All done |
| 2592 | fLeagueEndSent = true; |
| 2593 | return true; |
| 2594 | } |
| 2595 | |
| 2596 | bool C4Network2::LeaguePlrAuth(C4PlayerInfo *pInfo) |
| 2597 | { |
| 2598 | // Not possible? |
| 2599 | if (!pLeagueClient) |
| 2600 | return false; |
| 2601 | |
| 2602 | // Make sure league client is avilable |
| 2603 | LeagueWaitNotBusy(); |
| 2604 | |
| 2605 | // Official league? |
| 2606 | bool fOfficialLeague = SEqual(szStr1: pLeagueClient->getServerName(), C4CFG_OfficialLeagueServer); |
| 2607 | |
| 2608 | StdStrBuf Account, Password; |
| 2609 | bool fRegister = false; |
| 2610 | |
| 2611 | for (;;) |
| 2612 | { |
| 2613 | StdStrBuf NewAccount, NewPassword; |
| 2614 | |
| 2615 | // Ask for registration information |
| 2616 | if (fRegister) |
| 2617 | { |
| 2618 | // Use local nick as default |
| 2619 | NewAccount.Copy(Buf2: Config.Network.Nick); |
| 2620 | if (!C4LeagueSignupDialog::ShowModal(szPlayerName: pInfo->GetName(), szLeagueName: "" , szLeagueServerName: pLeagueClient->getServerName(), psAccount: &NewAccount, psPass: &NewPassword, fWarnThirdParty: !fOfficialLeague, fRegister: true)) |
| 2621 | return false; |
| 2622 | if (!NewPassword.getLength()) |
| 2623 | NewPassword.Copy(Buf2: Password); |
| 2624 | } |
| 2625 | else |
| 2626 | { |
| 2627 | if (!Config.Network.LeaguePassword.getLength() || !Config.Network.LeagueAutoLogin) |
| 2628 | { |
| 2629 | // ask for account |
| 2630 | if (!C4LeagueSignupDialog::ShowModal(szPlayerName: pInfo->GetName(), szLeagueName: "" , szLeagueServerName: pLeagueClient->getServerName(), psAccount: &Config.Network.LeagueAccount, psPass: &Config.Network.LeaguePassword, fWarnThirdParty: !fOfficialLeague, fRegister: false)) |
| 2631 | return false; |
| 2632 | } |
| 2633 | Account.Copy(Buf2: Config.Network.LeagueAccount); |
| 2634 | Password.Copy(Buf2: Config.Network.LeaguePassword); |
| 2635 | } |
| 2636 | |
| 2637 | // safety (modal dlg may have deleted network) |
| 2638 | if (!pLeagueClient) return false; |
| 2639 | |
| 2640 | // Send authentication request |
| 2641 | if (!pLeagueClient->Auth(PlrInfo: *pInfo, szAccount: Account.getData(), szPassword: Password.getData(), szNewAccount: NewAccount.getLength() ? NewAccount.getData() : nullptr, szNewPassword: NewPassword.getLength() ? NewPassword.getData() : nullptr)) |
| 2642 | return false; |
| 2643 | |
| 2644 | // safety (modal dlg may have deleted network) |
| 2645 | if (!pLeagueClient) return false; |
| 2646 | |
| 2647 | // Wait for a response |
| 2648 | const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_MSG_TRYLEAGUESIGNUP, args: pInfo->GetName(), args: pLeagueClient->getServerName())}; |
| 2649 | LogNTr(message); |
| 2650 | // Set up a dialog |
| 2651 | C4GUI::MessageDialog *pDlg = nullptr; |
| 2652 | if (Game.pGUI && !Console.Active) |
| 2653 | { |
| 2654 | // create & show |
| 2655 | pDlg = new C4GUI::MessageDialog(message.c_str(), LoadResStr(id: C4ResStrTableKey::IDS_DLG_LEAGUESIGNUP), C4GUI::MessageDialog::btnAbort, C4GUI::Ico_NetWait, C4GUI::MessageDialog::dsRegular); |
| 2656 | if (!pDlg || !pDlg->Show(pOnScreen: Game.pGUI, fCB: true)) return false; |
| 2657 | } |
| 2658 | // Wait for response |
| 2659 | while (pLeagueClient->isBusy()) |
| 2660 | { |
| 2661 | // Execute GUI |
| 2662 | if (Application.HandleMessage(iTimeout: 100) == HR_Failure || |
| 2663 | (pDlg && pDlg->IsAborted())) |
| 2664 | { |
| 2665 | // Clear up |
| 2666 | if (Game.pGUI) delete pDlg; |
| 2667 | return false; |
| 2668 | } |
| 2669 | // Check if league server has responded |
| 2670 | if (!pLeagueClient->Execute(iMaxTime: 0)) |
| 2671 | break; |
| 2672 | } |
| 2673 | // Close dialog |
| 2674 | if (Game.pGUI && pDlg) |
| 2675 | { |
| 2676 | pDlg->Close(fOK: true); |
| 2677 | delete pDlg; |
| 2678 | } |
| 2679 | |
| 2680 | // Success? |
| 2681 | StdStrBuf AUID, AccountMaster; bool fUnregistered = false; |
| 2682 | if (StdStrBuf Message; pLeagueClient->GetAuthReply(pMessage: &Message, pAUID: &AUID, pAccount: &AccountMaster, pRegister: &fUnregistered)) |
| 2683 | { |
| 2684 | // Set AUID |
| 2685 | pInfo->SetAuthID(AUID.getData()); |
| 2686 | |
| 2687 | if (Config.Network.LeagueAutoLogin) |
| 2688 | return true; |
| 2689 | |
| 2690 | // Show welcome message, if any |
| 2691 | bool fSuccess; |
| 2692 | if (Message.getLength()) |
| 2693 | fSuccess = Game.pGUI->ShowMessageModal( |
| 2694 | szMessage: Message.getData(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_DLG_LEAGUESIGNUPCONFIRM), |
| 2695 | dwButtons: C4GUI::MessageDialog::btnOKAbort, icoIcon: C4GUI::Ico_Ex_League); |
| 2696 | else if (AccountMaster.getLength()) |
| 2697 | fSuccess = Game.pGUI->ShowMessageModal( |
| 2698 | szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_LEAGUEPLAYERSIGNUPAS, args: pInfo->GetName(), args: AccountMaster.getData(), args: pLeagueClient->getServerName()).c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_DLG_LEAGUESIGNUPCONFIRM), |
| 2699 | dwButtons: C4GUI::MessageDialog::btnOKAbort, icoIcon: C4GUI::Ico_Ex_League); |
| 2700 | else |
| 2701 | fSuccess = Game.pGUI->ShowMessageModal( |
| 2702 | szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_LEAGUEPLAYERSIGNUP, args: pInfo->GetName(), args: pLeagueClient->getServerName()).c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_DLG_LEAGUESIGNUPCONFIRM), |
| 2703 | dwButtons: C4GUI::MessageDialog::btnOKAbort, icoIcon: C4GUI::Ico_Ex_League); |
| 2704 | |
| 2705 | // Approved? |
| 2706 | if (fSuccess) |
| 2707 | // Done |
| 2708 | return true; |
| 2709 | else |
| 2710 | { |
| 2711 | // Sign-up was cancelled by user |
| 2712 | Game.pGUI->ShowMessageModal(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_LEAGUESIGNUPCANCELLED, args: pInfo->GetName()).c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_DLG_LEAGUESIGNUP), dwButtons: C4GUI::MessageDialog::btnOK, icoIcon: C4GUI::Ico_Notify); |
| 2713 | Config.Network.LeaguePassword.Clear(); |
| 2714 | } |
| 2715 | } |
| 2716 | else |
| 2717 | { |
| 2718 | // Error with first-time registration or manual password entry |
| 2719 | if (!fUnregistered || fRegister) |
| 2720 | { |
| 2721 | Log(id: C4ResStrTableKey::IDS_MSG_LEAGUESIGNUPERROR, args: Message.getData()); |
| 2722 | Game.pGUI->ShowMessageModal(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_LEAGUESERVERMSG, args: Message.getData()).c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_DLG_LEAGUESIGNUPFAILED), dwButtons: C4GUI::MessageDialog::btnOK, icoIcon: C4GUI::Ico_Error); |
| 2723 | Config.Network.LeaguePassword.Clear(); |
| 2724 | // after a league server error message, always fall-through to try again |
| 2725 | } |
| 2726 | } |
| 2727 | |
| 2728 | // No account yet? Try to register. |
| 2729 | if (fUnregistered && !fRegister) fRegister = true; |
| 2730 | |
| 2731 | // Try given account name as default next time |
| 2732 | if (AccountMaster.getLength()) |
| 2733 | Account.Take(Buf2&: AccountMaster); |
| 2734 | |
| 2735 | // safety (modal dlg may have deleted network) |
| 2736 | if (!pLeagueClient) return false; |
| 2737 | } |
| 2738 | } |
| 2739 | |
| 2740 | bool C4Network2::LeaguePlrAuthCheck(C4PlayerInfo *pInfo) |
| 2741 | { |
| 2742 | // Not possible? |
| 2743 | if (!pLeagueClient) |
| 2744 | return false; |
| 2745 | |
| 2746 | // Make sure league client is available |
| 2747 | LeagueWaitNotBusy(); |
| 2748 | |
| 2749 | // Ask league server to check the code |
| 2750 | if (!pLeagueClient->AuthCheck(PlrInfo: *pInfo)) |
| 2751 | return false; |
| 2752 | |
| 2753 | // Log |
| 2754 | const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_MSG_LEAGUEJOINING, args: pInfo->GetName())}; |
| 2755 | LogNTr(message); |
| 2756 | |
| 2757 | // Wait for response |
| 2758 | while (pLeagueClient->isBusy()) |
| 2759 | if (!pLeagueClient->Execute(iMaxTime: 100)) |
| 2760 | break; |
| 2761 | |
| 2762 | // Check response validity |
| 2763 | if (!pLeagueClient->isSuccess()) |
| 2764 | { |
| 2765 | LeagueShowError(szMsg: pLeagueClient->GetError()); |
| 2766 | return false; |
| 2767 | } |
| 2768 | |
| 2769 | // Check if league server approves. pInfo will have league info if this call is successful. |
| 2770 | if (StdStrBuf Message; !pLeagueClient->GetAuthCheckReply(pMessage: &Message, szLeague: Game.Parameters.League.getData(), pPlrInfo: pInfo)) |
| 2771 | { |
| 2772 | LeagueShowError(szMsg: LoadResStr(id: C4ResStrTableKey::IDS_MSG_LEAGUEJOINREFUSED, args: pInfo->GetName(), args: Message.getData()).c_str()); |
| 2773 | return false; |
| 2774 | } |
| 2775 | |
| 2776 | return true; |
| 2777 | } |
| 2778 | |
| 2779 | void C4Network2::LeagueNotifyDisconnect(int32_t iClientID, C4LeagueDisconnectReason eReason) |
| 2780 | { |
| 2781 | // league active? |
| 2782 | if (!pLeagueClient || !Game.Parameters.isLeague()) return; |
| 2783 | // only in running game |
| 2784 | if (!Game.IsRunning || Game.GameOver) return; |
| 2785 | // clients send notifications for their own players; host sends for the affected client players |
| 2786 | if (!isHost()) { if (!Clients.GetLocal()) return; iClientID = Clients.GetLocal()->getID(); } |
| 2787 | // clients only need notifications if they have players in the game |
| 2788 | const C4ClientPlayerInfos *pInfos = Game.PlayerInfos.GetInfoByClientID(iClientID); |
| 2789 | if (!pInfos) return; |
| 2790 | int32_t i = 0; C4PlayerInfo *pInfo; |
| 2791 | while (pInfo = pInfos->GetPlayerInfo(iIndex: i++)) if (pInfo->IsJoined() && !pInfo->IsRemoved()) break; |
| 2792 | if (!pInfo) return; |
| 2793 | // Make sure league client is avilable |
| 2794 | LeagueWaitNotBusy(); |
| 2795 | // report the disconnect! |
| 2796 | Log(id: C4ResStrTableKey::IDS_LEAGUE_LEAGUEREPORTINGUNEXPECTED, args: static_cast<int>(eReason)); |
| 2797 | pLeagueClient->ReportDisconnect(rSendPlayerFBIDs: *pInfos, eReason); |
| 2798 | // wait for the reply |
| 2799 | LeagueWaitNotBusy(); |
| 2800 | // display it |
| 2801 | const char *szMsg; |
| 2802 | StdStrBuf sMessage; |
| 2803 | if (pLeagueClient->GetReportDisconnectReply(pMessage: &sMessage)) |
| 2804 | Log(id: C4ResStrTableKey::IDS_MSG_LEAGUEUNEXPECTEDDISCONNEC, args: sMessage.getData()); |
| 2805 | else |
| 2806 | Log(id: C4ResStrTableKey::IDS_ERR_LEAGUEERRORREPORTINGUNEXP, args: sMessage.getData()); |
| 2807 | } |
| 2808 | |
| 2809 | void C4Network2::LeagueWaitNotBusy() |
| 2810 | { |
| 2811 | // league client busy? |
| 2812 | if (!pLeagueClient || !pLeagueClient->isBusy()) return; |
| 2813 | // wait for it |
| 2814 | Log(id: C4ResStrTableKey::IDS_LEAGUE_WAITINGFORLASTLEAGUESERVE); |
| 2815 | while (pLeagueClient->isBusy()) |
| 2816 | if (!pLeagueClient->Execute(iMaxTime: 100)) |
| 2817 | break; |
| 2818 | // if last request was an update request, process it |
| 2819 | if (pLeagueClient->getCurrentAction() == C4LA_Update) |
| 2820 | LeagueUpdateProcessReply(); |
| 2821 | } |
| 2822 | |
| 2823 | void C4Network2::LeagueSurrender() |
| 2824 | { |
| 2825 | // there's currently no functionality to surrender in the league |
| 2826 | // just stop responding so other clients will notify the disconnect |
| 2827 | DeinitLeague(); |
| 2828 | } |
| 2829 | |
| 2830 | void C4Network2::LeagueShowError(const char *szMsg) |
| 2831 | { |
| 2832 | if (Game.pGUI && Application.isFullScreen) |
| 2833 | { |
| 2834 | Game.pGUI->ShowErrorMessage(szMessage: szMsg); |
| 2835 | } |
| 2836 | else |
| 2837 | { |
| 2838 | Log(id: C4ResStrTableKey::IDS_LGA_SERVERFAILURE, args&: szMsg); |
| 2839 | } |
| 2840 | } |
| 2841 | |
| 2842 | void C4Network2::Vote(C4ControlVoteType eType, bool fApprove, int32_t iData) |
| 2843 | { |
| 2844 | // Original vote? |
| 2845 | if (!GetVote(iClientID: C4ClientIDUnknown, eType, iData)) |
| 2846 | { |
| 2847 | // Too fast? |
| 2848 | if (time(timer: nullptr) < static_cast<time_t>(iLastOwnVoting + C4NetMinVotingInterval)) |
| 2849 | { |
| 2850 | Log(id: C4ResStrTableKey::IDS_TEXT_YOUCANONLYSTARTONEVOTINGE); |
| 2851 | if ((eType == VT_Kick && iData == Game.Clients.getLocalID()) || eType == VT_Cancel) |
| 2852 | OpenSurrenderDialog(eType, iData); |
| 2853 | return; |
| 2854 | } |
| 2855 | // Save timestamp |
| 2856 | iLastOwnVoting = time(timer: nullptr); |
| 2857 | } |
| 2858 | // Already voted? Ignore |
| 2859 | if (GetVote(iClientID: Game.Control.ClientID(), eType, iData)) |
| 2860 | return; |
| 2861 | // Set pause mode if this is the host |
| 2862 | if (isHost() && isRunning()) |
| 2863 | { |
| 2864 | Pause(); |
| 2865 | fPausedForVote = true; |
| 2866 | } |
| 2867 | // send vote control |
| 2868 | Game.Control.DoInput(eCtrlType: CID_Vote, pPkt: new C4ControlVote(eType, fApprove, iData), eDelivery: CDT_Direct); |
| 2869 | } |
| 2870 | |
| 2871 | void C4Network2::AddVote(const C4ControlVote &Vote) |
| 2872 | { |
| 2873 | // Save back timestamp |
| 2874 | if (!Votes.firstPkt()) |
| 2875 | iVoteStartTime = time(timer: nullptr); |
| 2876 | // Save vote back |
| 2877 | Votes.Add(eType: CID_Vote, pCtrl: new C4ControlVote(Vote)); |
| 2878 | // Set pause mode if this is the host |
| 2879 | if (isHost() && isRunning()) |
| 2880 | { |
| 2881 | Pause(); |
| 2882 | fPausedForVote = true; |
| 2883 | } |
| 2884 | // Check if the dialog should be opened |
| 2885 | OpenVoteDialog(); |
| 2886 | } |
| 2887 | |
| 2888 | C4IDPacket *C4Network2::GetVote(int32_t iClientID, C4ControlVoteType eType, int32_t iData) |
| 2889 | { |
| 2890 | C4ControlVote *pVote; |
| 2891 | for (C4IDPacket *pPkt = Votes.firstPkt(); pPkt; pPkt = Votes.nextPkt(pPkt)) |
| 2892 | if (pPkt->getPktType() == CID_Vote) |
| 2893 | if (pVote = static_cast<C4ControlVote *>(pPkt->getPkt())) |
| 2894 | if (iClientID == C4ClientIDUnknown || pVote->getByClient() == iClientID) |
| 2895 | if (pVote->getType() == eType && pVote->getData() == iData) |
| 2896 | return pPkt; |
| 2897 | return nullptr; |
| 2898 | } |
| 2899 | |
| 2900 | void C4Network2::EndVote(C4ControlVoteType eType, bool fApprove, int32_t iData) |
| 2901 | { |
| 2902 | // Remove all vote packets |
| 2903 | C4IDPacket *pPkt; int32_t iOrigin = C4ClientIDUnknown; |
| 2904 | while (pPkt = GetVote(iClientID: C4ClientIDAll, eType, iData)) |
| 2905 | { |
| 2906 | if (iOrigin == C4ClientIDUnknown) |
| 2907 | iOrigin = static_cast<C4ControlVote *>(pPkt->getPkt())->getByClient(); |
| 2908 | Votes.Delete(pPkt); |
| 2909 | } |
| 2910 | // Reset timestamp |
| 2911 | iVoteStartTime = time(timer: nullptr); |
| 2912 | // Approved own voting? Reset voting block |
| 2913 | if (fApprove && iOrigin == Game.Clients.getLocalID()) |
| 2914 | iLastOwnVoting = 0; |
| 2915 | // Dialog open? |
| 2916 | if (pVoteDialog) |
| 2917 | if (pVoteDialog->getVoteType() == eType && pVoteDialog->getVoteData() == iData) |
| 2918 | { |
| 2919 | // close |
| 2920 | delete pVoteDialog; |
| 2921 | pVoteDialog = nullptr; |
| 2922 | } |
| 2923 | // Did we try to kick ourself? Ask if we'd like to surrender |
| 2924 | bool fCancelVote = (eType == VT_Kick && iData == Game.Clients.getLocalID()) || eType == VT_Cancel; |
| 2925 | if (!fApprove && fCancelVote && iOrigin == Game.Clients.getLocalID()) |
| 2926 | OpenSurrenderDialog(eType, iData); |
| 2927 | // Check if the dialog should be opened |
| 2928 | OpenVoteDialog(); |
| 2929 | // Pause/unpause voting? |
| 2930 | if (fApprove && eType == VT_Pause) |
| 2931 | fPausedForVote = !iData; |
| 2932 | // No voting left? Reset pause. |
| 2933 | if (!Votes.firstPkt()) |
| 2934 | if (fPausedForVote) |
| 2935 | { |
| 2936 | Start(); |
| 2937 | fPausedForVote = false; |
| 2938 | } |
| 2939 | } |
| 2940 | |
| 2941 | void C4Network2::OpenVoteDialog() |
| 2942 | { |
| 2943 | // Dialog already open? |
| 2944 | if (pVoteDialog) return; |
| 2945 | // No GUI? |
| 2946 | if (!Game.pGUI) return; |
| 2947 | // No vote available? |
| 2948 | if (!Votes.firstPkt()) return; |
| 2949 | // Can't vote? |
| 2950 | C4ClientPlayerInfos *pPlayerInfos = Game.PlayerInfos.GetInfoByClientID(iClientID: Game.Clients.getLocalID()); |
| 2951 | if (!pPlayerInfos || !pPlayerInfos->GetPlayerCount() || !pPlayerInfos->GetJoinedPlayerCount()) |
| 2952 | return; |
| 2953 | // Search a voting we have to vote on |
| 2954 | for (C4IDPacket *pPkt = Votes.firstPkt(); pPkt; pPkt = Votes.nextPkt(pPkt)) |
| 2955 | { |
| 2956 | // Already voted on this matter? |
| 2957 | C4ControlVote *pVote = static_cast<C4ControlVote *>(pPkt->getPkt()); |
| 2958 | if (!GetVote(iClientID: Game.Control.ClientID(), eType: pVote->getType(), iData: pVote->getData())) |
| 2959 | { |
| 2960 | // Compose message |
| 2961 | C4Client *pSrcClient = Game.Clients.getClientByID(iID: pVote->getByClient()); |
| 2962 | const std::string msg{std::format(fmt: "{}|{}" , args: LoadResStr(id: C4ResStrTableKey::IDS_VOTE_WANTSTOALLOW, args: pSrcClient ? pSrcClient->getName() : "???" , args: pVote->getDesc().getData()), args: pVote->getDescWarning().getData())}; |
| 2963 | |
| 2964 | // Open dialog |
| 2965 | pVoteDialog = new C4VoteDialog(msg.c_str(), pVote->getType(), pVote->getData(), false); |
| 2966 | pVoteDialog->SetDelOnClose(); |
| 2967 | pVoteDialog->Show(pOnScreen: Game.pGUI, fCB: true); |
| 2968 | |
| 2969 | break; |
| 2970 | } |
| 2971 | } |
| 2972 | } |
| 2973 | |
| 2974 | void C4Network2::OpenSurrenderDialog(C4ControlVoteType eType, int32_t iData) |
| 2975 | { |
| 2976 | if (!pVoteDialog) |
| 2977 | { |
| 2978 | pVoteDialog = new C4VoteDialog( |
| 2979 | LoadResStr(id: C4ResStrTableKey::IDS_VOTE_SURRENDERWARNING), eType, iData, true); |
| 2980 | pVoteDialog->SetDelOnClose(); |
| 2981 | pVoteDialog->Show(pOnScreen: Game.pGUI, fCB: true); |
| 2982 | } |
| 2983 | } |
| 2984 | |
| 2985 | void C4Network2::OnVoteDialogClosed() |
| 2986 | { |
| 2987 | pVoteDialog = nullptr; |
| 2988 | } |
| 2989 | |
| 2990 | // *** C4VoteDialog |
| 2991 | |
| 2992 | C4VoteDialog::C4VoteDialog(const char *szText, C4ControlVoteType eVoteType, int32_t iVoteData, bool fSurrender) |
| 2993 | : eVoteType(eVoteType), iVoteData(iVoteData), fSurrender(fSurrender), |
| 2994 | MessageDialog(szText, LoadResStr(id: C4ResStrTableKey::IDS_DLG_VOTING), C4GUI::MessageDialog::btnYesNo, C4GUI::Ico_Confirm, C4GUI::MessageDialog::dsRegular, nullptr, true) {} |
| 2995 | |
| 2996 | void C4VoteDialog::OnClosed(bool fOK) |
| 2997 | { |
| 2998 | bool fAbortGame = false; |
| 2999 | // notify that this object will be deleted shortly |
| 3000 | Game.Network.OnVoteDialogClosed(); |
| 3001 | // Was league surrender dialog |
| 3002 | if (fSurrender) |
| 3003 | { |
| 3004 | // League surrender accepted |
| 3005 | if (fOK) |
| 3006 | { |
| 3007 | // set game leave reason, although round results dialog isn't showing it ATM |
| 3008 | Game.RoundResults.EvaluateNetwork(eResult: C4RoundResults::NR_NetError, szResultsString: LoadResStr(id: C4ResStrTableKey::IDS_ERR_YOUSURRENDEREDTHELEAGUEGA)); |
| 3009 | // leave game |
| 3010 | Game.Network.LeagueSurrender(); |
| 3011 | Game.Network.Clear(); |
| 3012 | // We have just league-surrendered. Abort the game - that is what we originally wanted. |
| 3013 | // Note: as we are losing league points and this is a relevant game, it would actually be |
| 3014 | // nice to show an evaluation dialog which tells us that we have lost and how many league |
| 3015 | // points we have lost. But until the evaluation dialog can actually do that, it is better |
| 3016 | // to abort completely. |
| 3017 | // Note2: The league dialog will never know that, because the game will usually not be over yet. |
| 3018 | // Scores are not calculated until after the game. |
| 3019 | fAbortGame = true; |
| 3020 | } |
| 3021 | } |
| 3022 | // Was normal vote dialog |
| 3023 | else |
| 3024 | { |
| 3025 | // Vote still active? Then vote. |
| 3026 | if (Game.Network.GetVote(iClientID: C4ClientIDUnknown, eType: eVoteType, iData: iVoteData)) |
| 3027 | Game.Network.Vote(eType: eVoteType, fApprove: fOK, iData: iVoteData); |
| 3028 | } |
| 3029 | // notify base class |
| 3030 | MessageDialog::OnClosed(fOK); |
| 3031 | // Abort game |
| 3032 | if (fAbortGame) |
| 3033 | Game.Abort(fApproved: true); |
| 3034 | } |
| 3035 | |
| 3036 | /* Lobby countdown */ |
| 3037 | |
| 3038 | void C4Network2::StartLobbyCountdown(int32_t iCountdownTime) |
| 3039 | { |
| 3040 | // abort previous |
| 3041 | if (pLobbyCountdown) AbortLobbyCountdown(); |
| 3042 | // start new |
| 3043 | pLobbyCountdown = new C4GameLobby::Countdown(iCountdownTime); |
| 3044 | } |
| 3045 | |
| 3046 | void C4Network2::AbortLobbyCountdown() |
| 3047 | { |
| 3048 | // aboert lobby countdown |
| 3049 | if (pLobbyCountdown) pLobbyCountdown->Abort(); |
| 3050 | delete pLobbyCountdown; |
| 3051 | pLobbyCountdown = nullptr; |
| 3052 | } |
| 3053 | |
| 3054 | /* Streaming */ |
| 3055 | |
| 3056 | bool C4Network2::StartStreaming(C4Record *pRecord) |
| 3057 | { |
| 3058 | // Save back |
| 3059 | fStreaming = true; |
| 3060 | pStreamedRecord = pRecord; |
| 3061 | iLastStreamAttempt = time(timer: nullptr); |
| 3062 | |
| 3063 | // Initialize compressor |
| 3064 | StreamCompressor = {}; |
| 3065 | if (deflateInit(&StreamCompressor, 9) != Z_OK) |
| 3066 | return false; |
| 3067 | |
| 3068 | // Create stream buffer |
| 3069 | StreamingBuf.New(inSize: C4NetStreamingMaxBlockSize); |
| 3070 | StreamCompressor.next_out = reinterpret_cast<uint8_t *>(StreamingBuf.getMData()); |
| 3071 | StreamCompressor.avail_out = C4NetStreamingMaxBlockSize; |
| 3072 | |
| 3073 | // Initialize HTTP client |
| 3074 | pStreamer = new C4Network2HTTPClient(); |
| 3075 | if (!pStreamer->Init()) |
| 3076 | return false; |
| 3077 | |
| 3078 | // ... more initialization? |
| 3079 | return true; |
| 3080 | } |
| 3081 | |
| 3082 | bool C4Network2::FinishStreaming() |
| 3083 | { |
| 3084 | if (!fStreaming) return false; |
| 3085 | |
| 3086 | // Stream |
| 3087 | StreamIn(fFinish: true); |
| 3088 | |
| 3089 | // Reset record pointer |
| 3090 | pStreamedRecord = nullptr; |
| 3091 | |
| 3092 | // Try to get rid of remaining data immediately |
| 3093 | iLastStreamAttempt = 0; |
| 3094 | StreamOut(); |
| 3095 | |
| 3096 | return true; |
| 3097 | } |
| 3098 | |
| 3099 | bool C4Network2::StopStreaming() |
| 3100 | { |
| 3101 | if (!fStreaming) return false; |
| 3102 | |
| 3103 | // Clear |
| 3104 | fStreaming = false; |
| 3105 | pStreamedRecord = nullptr; |
| 3106 | deflateEnd(strm: &StreamCompressor); |
| 3107 | StreamingBuf.Clear(); |
| 3108 | delete pStreamer; |
| 3109 | pStreamer = nullptr; |
| 3110 | |
| 3111 | // ... finalization? |
| 3112 | return true; |
| 3113 | } |
| 3114 | |
| 3115 | bool C4Network2::StreamIn(bool fFinish) |
| 3116 | { |
| 3117 | if (!pStreamedRecord) return false; |
| 3118 | |
| 3119 | // Get data from record |
| 3120 | const StdBuf &Data = pStreamedRecord->GetStreamingBuf(); |
| 3121 | if (!fFinish) |
| 3122 | if (!Data.getSize() || !StreamCompressor.avail_out) |
| 3123 | return false; |
| 3124 | |
| 3125 | do |
| 3126 | { |
| 3127 | // Compress |
| 3128 | StreamCompressor.next_in = const_cast<uint8_t *>(Data.getPtr<uint8_t>()); |
| 3129 | StreamCompressor.avail_in = Data.getSize(); |
| 3130 | int ret = deflate(strm: &StreamCompressor, flush: fFinish ? Z_FINISH : Z_NO_FLUSH); |
| 3131 | |
| 3132 | // Anything consumed? |
| 3133 | unsigned int iInAmount = Data.getSize() - StreamCompressor.avail_in; |
| 3134 | if (iInAmount > 0) |
| 3135 | pStreamedRecord->ClearStreamingBuf(iAmount: iInAmount); |
| 3136 | |
| 3137 | // Done? |
| 3138 | if (!fFinish || ret == Z_STREAM_END) |
| 3139 | break; |
| 3140 | |
| 3141 | // Error while finishing? |
| 3142 | if (ret != Z_OK) |
| 3143 | return false; |
| 3144 | |
| 3145 | // Enlarge buffer, if neccessary |
| 3146 | size_t iPending = getPendingStreamData(); |
| 3147 | size_t iGrow = StreamingBuf.getSize(); |
| 3148 | StreamingBuf.Grow(iGrow); |
| 3149 | StreamCompressor.avail_out += iGrow; |
| 3150 | StreamCompressor.next_out = StreamingBuf.getMPtr<uint8_t>(pos: iPending); |
| 3151 | } while (true); |
| 3152 | |
| 3153 | return true; |
| 3154 | } |
| 3155 | |
| 3156 | bool C4Network2::StreamOut() |
| 3157 | { |
| 3158 | // Streamer busy? |
| 3159 | if (!pStreamer || pStreamer->isBusy()) |
| 3160 | return false; |
| 3161 | |
| 3162 | // Streamer done? |
| 3163 | if (pStreamer->isSuccess()) |
| 3164 | { |
| 3165 | // Move new data to front of buffer |
| 3166 | if (getPendingStreamData() != iCurrentStreamAmount) |
| 3167 | StreamingBuf.Move(iFrom: iCurrentStreamAmount, inSize: getPendingStreamData() - iCurrentStreamAmount); |
| 3168 | |
| 3169 | // Free buffer space |
| 3170 | StreamCompressor.next_out -= iCurrentStreamAmount; |
| 3171 | StreamCompressor.avail_out += iCurrentStreamAmount; |
| 3172 | |
| 3173 | // Advance stream |
| 3174 | iCurrentStreamPosition += iCurrentStreamAmount; |
| 3175 | |
| 3176 | // Get input |
| 3177 | StreamIn(fFinish: false); |
| 3178 | } |
| 3179 | |
| 3180 | // Clear streamer |
| 3181 | pStreamer->Clear(); |
| 3182 | |
| 3183 | // Record is still running? |
| 3184 | if (pStreamedRecord) |
| 3185 | { |
| 3186 | // Enough available to send? |
| 3187 | if (getPendingStreamData() < C4NetStreamingMinBlockSize) |
| 3188 | return false; |
| 3189 | |
| 3190 | // Overflow protection |
| 3191 | if (iLastStreamAttempt && iLastStreamAttempt + C4NetStreamingInterval >= time(timer: nullptr)) |
| 3192 | return false; |
| 3193 | } |
| 3194 | // All data finished? |
| 3195 | else if (!getPendingStreamData()) |
| 3196 | { |
| 3197 | // Then we're done. |
| 3198 | StopStreaming(); |
| 3199 | return false; |
| 3200 | } |
| 3201 | |
| 3202 | // Set stream address |
| 3203 | const std::string streamAddress{std::format(fmt: "{}pos={}&end={}" , args: Game.Parameters.StreamAddress.getData(), args&: iCurrentStreamPosition, args: !pStreamedRecord)}; |
| 3204 | pStreamer->SetServer(serverAddress: streamAddress); |
| 3205 | |
| 3206 | // Send data |
| 3207 | size_t iStreamAmount = getPendingStreamData(); |
| 3208 | iCurrentStreamAmount = iStreamAmount; |
| 3209 | iLastStreamAttempt = time(timer: nullptr); |
| 3210 | return pStreamer->Query(Data: StdBuf::MakeRef(pData: StreamingBuf.getData(), iSize: iStreamAmount), binary: false); |
| 3211 | } |
| 3212 | |
| 3213 | bool C4Network2::isStreaming() const |
| 3214 | { |
| 3215 | // Streaming must be active and there must still be anything to stream |
| 3216 | return fStreaming; |
| 3217 | } |
| 3218 | |