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
54C4Network2Status::C4Network2Status()
55 : eState(GS_None), iTargetCtrlTick(-1) {}
56
57const 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
70const 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
83void C4Network2Status::Set(C4NetGameState enState, int32_t inTargetTick)
84{
85 eState = enState; iTargetCtrlTick = inTargetTick;
86}
87
88void C4Network2Status::SetCtrlMode(int32_t inCtrlMode)
89{
90 iCtrlMode = inCtrlMode;
91}
92
93void C4Network2Status::SetTargetTick(int32_t inTargetCtrlTick)
94{
95 iTargetCtrlTick = inTargetCtrlTick;
96}
97
98void C4Network2Status::Clear()
99{
100 eState = GS_None; iTargetCtrlTick = -1;
101}
102
103void C4Network2Status::CompileFunc(StdCompiler *pComp)
104{
105 CompileFunc(pComp, fReference: false);
106}
107
108void 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
129C4Network2::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
142void 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
176void C4Network2::ReadyCheckDialog::OnClosed(bool)
177{
178 if (toast)
179 {
180 toast->SetEventHandler(nullptr);
181 toast->Hide();
182 }
183}
184
185void C4Network2::ReadyCheckDialog::Activated()
186{
187 Close(fOK: true);
188}
189
190void C4Network2::ReadyCheckDialog::OnAction(std::string_view action)
191{
192 Close(fOK: action == LoadResStrNoAmp(id: C4ResStrTableKey::IDS_DLG_YES));
193}
194
195#endif
196
197C4Network2::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
222bool 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
281C4Network2::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
347C4Network2::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
445bool 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
526bool 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
534bool 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
541bool 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
558bool 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
619bool 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
674void C4Network2::OnSec1Timer()
675{
676 Execute();
677}
678
679void 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
746void 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
792bool C4Network2::ToggleAllowJoin()
793{
794 // just toggle
795 AllowJoin(fAllow: !fAllowJoin);
796 return true; // toggled
797}
798
799bool C4Network2::ToggleClientListDlg()
800{
801 C4Network2ClientListDlg::Toggle();
802 return true;
803}
804
805void 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
818StdStrBuf 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
835void 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
846void 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
858void 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
866void 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
908void 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
998void 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
1007bool 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
1044C4NetpuncherID::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
1057void 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
1084void 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
1099void 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
1119void 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
1231bool 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
1266void 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
1366bool 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
1383bool 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
1395bool 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
1448void 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
1501void 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
1513void 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
1553void 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
1574void 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
1625void 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
1734void 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
1745void 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
1758void 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
1768void 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
1786void 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
1820void 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
1852C4Network2Res::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
1945bool 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
1974void 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
1982bool 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
1994bool 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
2017void 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
2062void 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
2081void 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
2091void 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
2116void 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
2148void 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
2161void 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
2186void 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
2196void 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
2206bool 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
2217void C4Network2::InvalidateReference()
2218{
2219 // Update both local and league reference as soon as possible
2220 iLastReferenceUpdate = 0;
2221 iLeagueUpdateDelay = C4NetMinLeagueUpdateInterval;
2222}
2223
2224bool 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
2283void 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
2292bool 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
2439bool 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
2468bool 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
2513bool 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
2596bool 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
2740bool 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
2779void 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
2809void 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
2823void 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
2830void 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
2842void 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
2871void 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
2888C4IDPacket *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
2900void 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
2941void 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
2974void 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
2985void C4Network2::OnVoteDialogClosed()
2986{
2987 pVoteDialog = nullptr;
2988}
2989
2990// *** C4VoteDialog
2991
2992C4VoteDialog::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
2996void 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
3038void C4Network2::StartLobbyCountdown(int32_t iCountdownTime)
3039{
3040 // abort previous
3041 if (pLobbyCountdown) AbortLobbyCountdown();
3042 // start new
3043 pLobbyCountdown = new C4GameLobby::Countdown(iCountdownTime);
3044}
3045
3046void C4Network2::AbortLobbyCountdown()
3047{
3048 // aboert lobby countdown
3049 if (pLobbyCountdown) pLobbyCountdown->Abort();
3050 delete pLobbyCountdown;
3051 pLobbyCountdown = nullptr;
3052}
3053
3054/* Streaming */
3055
3056bool 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
3082bool 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
3099bool 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
3115bool 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
3156bool 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
3213bool C4Network2::isStreaming() const
3214{
3215 // Streaming must be active and there must still be anything to stream
3216 return fStreaming;
3217}
3218