Client LuaCsForBarotrauma
GameClient.cs
2 using Barotrauma.IO;
4 using Barotrauma.Steam;
5 using Microsoft.Xna.Framework;
6 using Microsoft.Xna.Framework.Input;
7 using System;
8 using System.Collections.Generic;
9 using System.Collections.Immutable;
10 using System.Linq;
11 using System.Threading.Tasks;
12 using System.Xml.Linq;
13 
14 namespace Barotrauma.Networking
15 {
16  sealed class GameClient : NetworkMember
17  {
18  public static readonly TimeSpan CampaignSaveTransferTimeOut = new TimeSpan(0, 0, seconds: 100);
19  //this should be longer than CampaignSaveTransferTimeOut - we shouldn't give up starting the round if we're still waiting for the save file
20  public static readonly TimeSpan LevelTransitionTimeOut = new TimeSpan(0, 0, seconds: 150);
21 
22  public override bool IsClient => true;
23  public override bool IsServer => false;
24 
25 #if DEBUG
26  public float DebugServerVoipAmplitude;
27 #endif
28 
29  public override Voting Voting { get; }
30 
31  private UInt16 nameId = 0;
32 
33  public string Name { get; private set; }
34 
35  public string PendingName = string.Empty;
36 
37  public void SetName(string value)
38  {
39  if (string.IsNullOrEmpty(value)) { return; }
40  Name = value;
41  nameId++;
42  }
43 
44  public void ForceNameAndJobUpdate()
45  {
46  nameId++;
47  }
48 
49  public ClientPeer ClientPeer { get; private set; }
50 
51  private GUIMessageBox reconnectBox, waitInServerQueueBox;
52 
53  //TODO: move these to NetLobbyScreen
56  private readonly GUIComponent buttonContainer;
57 
58  public readonly NetStats NetStats;
59 
62 
64  GameStarted && Screen.Selected == GameMain.GameScreen &&
66 
68 
69  public bool LateCampaignJoin = false;
70 
71  private ClientPermissions permissions = ClientPermissions.None;
72  private List<Identifier> permittedConsoleCommands = new List<Identifier>();
73 
74  private bool connected;
75 
76  private enum RoundInitStatus
77  {
78  NotStarted,
79  Starting,
80  WaitingForStartGameFinalize,
81  Started,
82  Error,
83  Interrupted
84  }
85 
86  private UInt16? debugStartGameCampaignSaveID;
87 
88  private RoundInitStatus roundInitStatus = RoundInitStatus.NotStarted;
89 
90  public bool RoundStarting => roundInitStatus == RoundInitStatus.Starting || roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize;
91 
92  private readonly List<Client> otherClients;
93 
94  public readonly List<SubmarineInfo> ServerSubmarines = new List<SubmarineInfo>();
95 
97 
98  private bool canStart;
99 
100  private UInt16 lastSentChatMsgID = 0; //last message this client has successfully sent
101  private UInt16 lastQueueChatMsgID = 0; //last message added to the queue
102  private readonly List<ChatMessage> chatMsgQueue = new List<ChatMessage>();
103 
104  public UInt16 LastSentEntityEventID;
105 
106 #if DEBUG
107  public void PrintReceiverTransters()
108  {
109  foreach (var transfer in FileReceiver.ActiveTransfers)
110  {
111  DebugConsole.NewMessage(transfer.FileName + " " + transfer.Progress.ToString());
112  }
113  }
114 #endif
115 
116  //has the client been given a character to control this round
117  public bool HasSpawned;
118 
119  public float EndRoundTimeRemaining { get; private set; }
120 
123 
124  public byte SessionId { get; private set; }
125 
127  {
128  get;
129  private set;
130  }
131 
132  public override IReadOnlyList<Client> ConnectedClients
133  {
134  get
135  {
136  return otherClients;
137  }
138  }
139 
140  public Option<int> Ping
141  {
142  get
143  {
144  Client selfClient = ConnectedClients.FirstOrDefault(c => c.SessionId == SessionId);
145  if (selfClient is null || selfClient.Ping == 0) { return Option<int>.None(); }
146  return Option<int>.Some(selfClient.Ping);
147  }
148  }
149 
150  private readonly List<Client> previouslyConnectedClients = new List<Client>();
151  public IEnumerable<Client> PreviouslyConnectedClients
152  {
153  get { return previouslyConnectedClients; }
154  }
155 
156  public readonly FileReceiver FileReceiver;
157 
158  public bool MidRoundSyncing
159  {
160  get { return EntityEventManager.MidRoundSyncing; }
161  }
162 
164 
166  {
167  get;
168  set;
169  }
170 
171  private readonly ImmutableArray<Endpoint> serverEndpoints;
172  private readonly Option<int> ownerKey;
173 
174  public bool IsServerOwner => ownerKey.IsSome();
175 
176  internal readonly struct PermissionChangedEvent
177  {
178  public readonly ClientPermissions NewPermissions;
179  public readonly ImmutableArray<Identifier> NewPermittedConsoleCommands;
180 
181  public PermissionChangedEvent(ClientPermissions newPermissions, IReadOnlyList<Identifier> newPermittedConsoleCommands)
182  {
183  NewPermissions = newPermissions;
184  NewPermittedConsoleCommands = newPermittedConsoleCommands.ToImmutableArray();
185  }
186  }
187 
188  public readonly NamedEvent<PermissionChangedEvent> OnPermissionChanged = new NamedEvent<PermissionChangedEvent>();
189 
190  public GameClient(string newName, Endpoint endpoint, string serverName, Option<int> ownerKey)
191  : this(newName, endpoint.ToEnumerable().ToImmutableArray(), serverName, ownerKey) { }
192 
193  public GameClient(string newName, ImmutableArray<Endpoint> endpoints, string serverName, Option<int> ownerKey)
194  {
195  //TODO: gui stuff should probably not be here?
196  this.ownerKey = ownerKey;
197 
198  roundInitStatus = RoundInitStatus.NotStarted;
199 
200  NetStats = new NetStats();
201 
202  inGameHUD = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null)
203  {
204  CanBeFocused = false
205  };
206 
207  chatBox = new ChatBox(inGameHUD, isSinglePlayer: false);
210 
211  buttonContainer = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ButtonAreaTop, inGameHUD.RectTransform),
212  isHorizontal: true, childAnchor: Anchor.CenterRight)
213  {
214  AbsoluteSpacing = 5,
215  CanBeFocused = false
216  };
217 
218  endRoundVoteText = TextManager.Get("EndRound");
219  EndVoteTickBox = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.4f), buttonContainer.RectTransform) { MinSize = new Point(150, 0) },
221  {
222  OnSelected = ToggleEndRoundVote,
223  Visible = false
224  };
226 
227  ShowLogButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.6f), buttonContainer.RectTransform) { MinSize = new Point(150, 0) },
228  TextManager.Get("ServerLog"))
229  {
230  OnClicked = (GUIButton button, object userData) =>
231  {
232  if (ServerSettings.ServerLog.LogFrame == null)
233  {
235  }
236  else
237  {
239  GUI.KeyboardDispatcher.Subscriber = null;
240  }
241  return true;
242  }
243  };
245 
246  cameraFollowsSub = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.4f), buttonContainer.RectTransform)
247  {
248  MinSize = new Point(150, 0)
249  }, TextManager.Get("CamFollowSubmarine"))
250  {
252  OnSelected = (tbox) =>
253  {
254  Camera.FollowSub = tbox.Selected;
255  return true;
256  }
257  };
258 
259  GameMain.DebugDraw = false;
260  Hull.EditFire = false;
261  Hull.EditWater = false;
262 
263  SetName(newName);
264 
266 
267  FileReceiver = new FileReceiver();
268  FileReceiver.OnFinished += OnFileReceived;
269  FileReceiver.OnTransferFailed += OnTransferFailed;
270 
272  {
273  Job = null
274  };
275 
276  otherClients = new List<Client>();
277 
278  ServerSettings = new ServerSettings(this, serverName, 0, 0, 0, false, false, System.Net.IPAddress.Any);
279  Voting = new Voting();
280 
281  serverEndpoints = endpoints;
282  InitiateServerJoin();
283 
284  //ServerLog = new ServerLog("");
285 
286  ChatMessage.LastID = 0;
288  }
289 
291  {
292  var serverInfo = ServerInfo.FromServerEndpoints(ClientPeer.AllServerEndpoints, ServerSettings);
294  return serverInfo;
295  }
296 
297  private void InitiateServerJoin()
298  {
299  LastClientListUpdateID = 0;
300 
301  foreach (var c in ConnectedClients)
302  {
304  c.Dispose();
305  }
306  otherClients.Clear();
307 
308  chatBox.InputBox.Enabled = false;
309  if (GameMain.NetLobbyScreen?.ChatInput != null)
310  {
312  }
313 
315  ChatMessage.LastID = 0;
316 
317  ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
318  ClientPeer = CreateNetPeer();
319  ClientPeer.Start();
320 
321  CoroutineManager.StartCoroutine(WaitForStartingInfo(), "WaitForStartingInfo");
322  }
323 
324  public static void SetLobbyPublic(bool isPublic)
325  {
326  SteamManager.SetLobbyPublic(isPublic);
327  }
328 
329  private ClientPeer CreateNetPeer()
330  {
331  Networking.ClientPeer.Callbacks callbacks = new ClientPeer.Callbacks(
332  ReadDataMessage,
333  OnClientPeerDisconnect,
334  OnConnectionInitializationComplete);
335  return serverEndpoints.First() switch
336  {
337  LidgrenEndpoint lidgrenEndpoint
338  => new LidgrenClientPeer(lidgrenEndpoint, callbacks, ownerKey),
339  P2PEndpoint when ownerKey.TryUnwrap(out int key)
340  => new P2POwnerPeer(callbacks, key, serverEndpoints.Cast<P2PEndpoint>().ToImmutableArray()),
341  P2PEndpoint when ownerKey.IsNone()
342  => new P2PClientPeer(serverEndpoints.Cast<P2PEndpoint>().ToImmutableArray(), callbacks),
343  _ => throw new ArgumentOutOfRangeException()
344  };
345  }
346 
348  {
349  // Close any message boxes that say "The server has crashed."
350  var basicServerCrashMsg = TextManager.Get($"{nameof(DisconnectReason)}.{nameof(DisconnectReason.ServerCrashed)}");
352  .OfType<GUIMessageBox>()
353  .Where(mb => mb.Text?.Text == basicServerCrashMsg)
354  .ToArray()
355  .ForEach(mb => mb.Close());
356 
357  // Open a new message box with the crash report path
358  if (GUIMessageBox.MessageBoxes.All(
359  mb => (mb as GUIMessageBox)?.Text?.Text != ChildServerRelay.CrashMessage))
360  {
361  var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
362  msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu;
363  }
364  }
365 
366  private bool ReturnToPreviousMenu(GUIButton button, object obj)
367  {
368  Submarine.Unload();
369  GameMain.Client = null;
370  GameMain.GameSession = null;
371  if (IsServerOwner)
372  {
374  }
375  else
376  {
378  }
379 
380  GUIMessageBox.MessageBoxes.Clear();
381 
382  return true;
383  }
384 
385  private bool connectCancelled;
386  private void CancelConnect()
387  {
388  Quit();
389  }
390 
391  // Before main looping starts, we loop here and wait for approval message
392  private IEnumerable<CoroutineStatus> WaitForStartingInfo()
393  {
394  GUI.SetCursorWaiting();
395 
396  connectCancelled = false;
397  // When this is set to true, we are approved and ready to go
398  canStart = false;
399 
400  DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 200);
401 
402  // Loop until we are approved
403  LocalizedString connectingText = TextManager.Get("Connecting");
404  while (!canStart && !connectCancelled)
405  {
406  if (reconnectBox == null && waitInServerQueueBox == null)
407  {
408  string serverDisplayName = ServerName;
409  if (string.IsNullOrEmpty(serverDisplayName) && ClientPeer?.ServerConnection is SteamP2PConnection steamConnection)
410  {
411  if (SteamManager.IsInitialized && steamConnection.AccountInfo.AccountId.TryUnwrap(out var accountId) && accountId is SteamId steamId)
412  {
413  serverDisplayName = steamId.ToString();
414  string steamUserName = new Steamworks.Friend(steamId.Value).Name;
415  if (!string.IsNullOrEmpty(steamUserName) && steamUserName != "[unknown]")
416  {
417  serverDisplayName = steamUserName;
418  }
419  }
420  }
421  if (string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = TextManager.Get("Unknown").Value; }
422 
423  CreateReconnectBox(
424  connectingText,
425  TextManager.GetWithVariable("ConnectingTo", "[serverip]", serverDisplayName));
426  }
427 
428  if (reconnectBox != null)
429  {
430  reconnectBox.Header.Text = connectingText + new string('.', ((int)Timing.TotalTime % 3 + 1));
431  }
432 
433  yield return CoroutineStatus.Running;
434 
435  if (DateTime.Now > timeOut)
436  {
437  ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
438  var msgBox = new GUIMessageBox(TextManager.Get("ConnectionFailed"), TextManager.Get("CouldNotConnectToServer"))
439  {
440  DisplayInLoadingScreens = true
441  };
442  msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu;
443  CloseReconnectBox();
444  break;
445  }
446 
447  if (ClientPeer.WaitingForPassword && !canStart && !connectCancelled)
448  {
449  GUI.ClearCursorWait();
450  CloseReconnectBox();
451 
452  while (ClientPeer.WaitingForPassword)
453  {
454  yield return CoroutineStatus.Running;
455  }
456  }
457  }
458 
459  CloseReconnectBox();
460 
461  GUI.ClearCursorWait();
462  if (connectCancelled) { yield return CoroutineStatus.Success; }
463 
464  yield return CoroutineStatus.Success;
465  }
466 
467  public void Update(float deltaTime)
468  {
469 #if DEBUG
470  if (PlayerInput.GetKeyboardState.IsKeyDown(Keys.P)) return;
471 
472  if (PlayerInput.KeyHit(Keys.Home))
473  {
474  OnPermissionChanged.Invoke(new PermissionChangedEvent(permissions, permittedConsoleCommands));
475  }
476 #endif
477 
478  foreach (Client c in ConnectedClients)
479  {
480  if (c.Character != null && c.Character.Removed) { c.Character = null; }
481  c.UpdateVoipSound();
482  }
483 
484  if (VoipCapture.Instance != null)
485  {
486  if (VoipCapture.Instance.LastEnqueueAudio > DateTime.Now - new TimeSpan(0, 0, 0, 0, milliseconds: 100))
487  {
488  var myClient = ConnectedClients.Find(c => c.SessionId == SessionId);
490  {
492  }
493  else
494  {
496  }
497  }
498  }
499 
500  NetStats.Update(deltaTime);
501 
502  UpdateHUD(deltaTime);
503 
504  try
505  {
506  incomingMessagesToProcess.Clear();
507  incomingMessagesToProcess.AddRange(pendingIncomingMessages);
508  foreach (var inc in incomingMessagesToProcess)
509  {
510  ReadDataMessage(inc);
511  }
512  pendingIncomingMessages.Clear();
513  ClientPeer?.Update(deltaTime);
514  }
515  catch (Exception e)
516  {
517  string errorMsg = "Error while reading a message from server. ";
518  if (GameMain.Client == null) { errorMsg += "Client disposed."; }
519  AppendExceptionInfo(ref errorMsg, e);
520  GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
521  DebugConsole.ThrowError(errorMsg);
522  new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", e.TargetSite.ToString())))
523  {
524  DisplayInLoadingScreens = true
525  };
526  Quit();
527  GUI.DisableHUD = false;
529  return;
530  }
531 
532  if (!connected) { return; }
533 
534  CloseReconnectBox();
535 
536  if (GameStarted && Screen.Selected == GameMain.GameScreen)
537  {
539 
540  RespawnManager?.Update(deltaTime);
541 
542  if (updateTimer <= DateTime.Now)
543  {
544  SendIngameUpdate();
545  }
546  }
547  else
548  {
549  if (updateTimer <= DateTime.Now)
550  {
551  SendLobbyUpdate();
552  }
553  }
554 
556  {
558  }
559 
560  if (IsServerOwner && connected && !connectCancelled)
561  {
563  {
564  if (!ChildServerRelay.IsProcessAlive)
565  {
566  Quit();
568  }
569  }
570  }
571 
572  if (updateTimer <= DateTime.Now)
573  {
574  // Update current time
575  updateTimer = DateTime.Now + UpdateInterval;
576  }
577  }
578 
579  private readonly List<IReadMessage> pendingIncomingMessages = new List<IReadMessage>();
580  private readonly List<IReadMessage> incomingMessagesToProcess = new List<IReadMessage>();
581 
582  private void ReadDataMessage(IReadMessage inc)
583  {
585 
587 
588  if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize
589  && header is not (
590  ServerPacketHeader.STARTGAMEFINALIZE
591  or ServerPacketHeader.ENDGAME
592  or ServerPacketHeader.PING_REQUEST
593  or ServerPacketHeader.FILE_TRANSFER))
594  {
595  //rewind the header byte we just read
596  inc.BitPosition -= 8;
597  pendingIncomingMessages.Add(inc);
598  return;
599  }
600 
603 
605  {
606  switch (header)
607  {
608  case ServerPacketHeader.UPDATE_LOBBY:
609  case ServerPacketHeader.PING_REQUEST:
610  case ServerPacketHeader.FILE_TRANSFER:
611  case ServerPacketHeader.PERMISSIONS:
612  case ServerPacketHeader.CHEATS_ENABLED:
613  //allow interpreting this packet
614  break;
615  case ServerPacketHeader.STARTGAME:
616  GameStarted = true;
617  return;
618  case ServerPacketHeader.ENDGAME:
619  GameStarted = false;
620  return;
621  default:
622  return; //ignore any other packets
623  }
624  }
625 
626  switch (header)
627  {
628  case ServerPacketHeader.PING_REQUEST:
629  IWriteMessage response = new WriteOnlyMessage();
630  response.WriteByte((byte)ClientPacketHeader.PING_RESPONSE);
631  byte requestLen = inc.ReadByte();
632  response.WriteByte(requestLen);
633  for (int i = 0; i < requestLen; i++)
634  {
635  byte b = inc.ReadByte();
636  response.WriteByte(b);
637  }
638  ClientPeer.Send(response, DeliveryMethod.Unreliable);
639  break;
640  case ServerPacketHeader.CLIENT_PINGS:
641  byte clientCount = inc.ReadByte();
642  for (int i = 0; i < clientCount; i++)
643  {
644  byte clientId = inc.ReadByte();
645  UInt16 clientPing = inc.ReadUInt16();
646  Client client = ConnectedClients.Find(c => c.SessionId == clientId);
647  if (client != null)
648  {
649  client.Ping = clientPing;
650  }
651  }
652  break;
653  case ServerPacketHeader.UPDATE_LOBBY:
654  ReadLobbyUpdate(inc);
655  break;
656  case ServerPacketHeader.UPDATE_INGAME:
657  try
658  {
659  ReadIngameUpdate(inc);
660  }
661  catch (Exception e)
662  {
663  string errorMsg = "Error while reading an ingame update message from server.";
664  AppendExceptionInfo(ref errorMsg, e);
665  GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
666  throw;
667  }
668  break;
669  case ServerPacketHeader.VOICE:
670  if (VoipClient == null)
671  {
672  string errorMsg = "Failed to read a voice packet from the server (VoipClient == null). ";
673  if (GameMain.Client == null) { errorMsg += "Client disposed. "; }
674  errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace();
675  GameAnalyticsManager.AddErrorEventOnce(
676  "GameClient.ReadDataMessage:VoipClientNull",
677  GameMain.Client == null ? GameAnalyticsManager.ErrorSeverity.Error : GameAnalyticsManager.ErrorSeverity.Warning,
678  errorMsg);
679  return;
680  }
681 
682  VoipClient.Read(inc);
683  break;
684 #if DEBUG
685  case ServerPacketHeader.VOICE_AMPLITUDE_DEBUG:
686  GameMain.Client.DebugServerVoipAmplitude = inc.ReadRangedSingle(min: 0, max: 1, bitCount: 8);
687  break;
688 #endif
689  case ServerPacketHeader.QUERY_STARTGAME:
690  DebugConsole.Log("Received QUERY_STARTGAME packet.");
691  string subName = inc.ReadString();
692  string subHash = inc.ReadString();
693 
694  bool usingShuttle = inc.ReadBoolean();
695  string shuttleName = inc.ReadString();
696  string shuttleHash = inc.ReadString();
697 
698  byte campaignID = inc.ReadByte();
699  UInt16 campaignSaveID = inc.ReadUInt16();
700  Dictionary<MultiPlayerCampaign.NetFlags, UInt16> campaignUpdateIDs = new Dictionary<MultiPlayerCampaign.NetFlags, ushort>();
701  foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
702  {
703  campaignUpdateIDs[flag] = inc.ReadUInt16();
704  }
705 
706  IWriteMessage readyToStartMsg = new WriteOnlyMessage();
707  readyToStartMsg.WriteByte((byte)ClientPacketHeader.RESPONSE_STARTGAME);
708 
709  if (campaign != null) { campaign.PendingSubmarineSwitch = null; }
710  GameMain.NetLobbyScreen.UsingShuttle = usingShuttle;
711  bool readyToStart;
712  if (campaign == null && campaignID == 0)
713  {
714  readyToStart = GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList) &&
715  GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox);
716  }
717  else
718  {
719  readyToStart =
720  campaign != null &&
721  campaign.CampaignID == campaignID &&
722  campaign.LastSaveID == campaignSaveID &&
723  campaignUpdateIDs.All(kvp => campaign.GetLastUpdateIdForFlag(kvp.Key) == kvp.Value);
724  }
725  readyToStartMsg.WriteBoolean(readyToStart);
726 
727  DebugConsole.Log(readyToStart ? "Ready to start." : "Not ready to start.");
728 
729  WriteCharacterInfo(readyToStartMsg);
730 
731  ClientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable);
732 
733  if (readyToStart && !CoroutineManager.IsCoroutineRunning("WaitForStartRound"))
734  {
735  CoroutineManager.StartCoroutine(NetLobbyScreen.WaitForStartRound(startButton: null), "WaitForStartRound");
736  }
737  break;
738  case ServerPacketHeader.STARTGAME:
739  DebugConsole.Log("Received STARTGAME packet.");
740  if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is CampaignMode)
741  {
742  //start without a loading screen if playing a campaign round
743  CoroutineManager.StartCoroutine(StartGame(inc));
744  }
745  else
746  {
747  GUIMessageBox.CloseAll();
748  GameMain.Instance.ShowLoading(StartGame(inc), false);
749  }
750  break;
751  case ServerPacketHeader.STARTGAMEFINALIZE:
752  DebugConsole.NewMessage("Received STARTGAMEFINALIZE packet. Round init status: " + roundInitStatus);
753  if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize)
754  {
755  //waiting for a save file
756  if (campaign != null &&
757  NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID) &&
758  FileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave))
759  {
760  return;
761  }
762  ReadStartGameFinalize(inc);
763  }
764  break;
765  case ServerPacketHeader.ENDGAME:
766  CampaignMode.TransitionType transitionType = (CampaignMode.TransitionType)inc.ReadByte();
767  bool save = inc.ReadBoolean();
768  string endMessage = string.Empty;
769 
770  endMessage = inc.ReadString();
771  byte missionCount = inc.ReadByte();
772  for (int i = 0; i < missionCount; i++)
773  {
774  bool missionSuccessful = inc.ReadBoolean();
775  var mission = GameMain.GameSession?.GetMission(i);
776  if (mission != null)
777  {
778  mission.Completed = missionSuccessful;
779  }
780  }
781  CharacterTeamType winningTeam = (CharacterTeamType)inc.ReadByte();
782  if (winningTeam != CharacterTeamType.None)
783  {
784  GameMain.GameSession.WinningTeam = winningTeam;
785  var combatMission = GameMain.GameSession.Missions.FirstOrDefault(m => m is CombatMission);
786  if (combatMission != null)
787  {
788  combatMission.Completed = true;
789  }
790  }
791 
792  bool includesTraitorInfo = inc.ReadBoolean();
793  TraitorManager.TraitorResults? traitorResults = null;
794  if (includesTraitorInfo)
795  {
796  traitorResults = INetSerializableStruct.Read<TraitorManager.TraitorResults>(inc);
797  }
798 
799  roundInitStatus = RoundInitStatus.Interrupted;
800  CoroutineManager.StartCoroutine(EndGame(endMessage, transitionType, traitorResults), "EndGame");
801  GUI.SetSavingIndicatorState(save);
802  break;
803  case ServerPacketHeader.CAMPAIGN_SETUP_INFO:
804  byte saveCount = inc.ReadByte();
805  List<CampaignMode.SaveInfo> saveInfos = new List<CampaignMode.SaveInfo>();
806  for (int i = 0; i < saveCount; i++)
807  {
808  saveInfos.Add(INetSerializableStruct.Read<CampaignMode.SaveInfo>(inc));
809  }
810  MultiPlayerCampaign.StartCampaignSetup(saveInfos);
811  break;
812  case ServerPacketHeader.PERMISSIONS:
813  ReadPermissions(inc);
814  break;
815  case ServerPacketHeader.ACHIEVEMENT:
816  ReadAchievement(inc);
817  break;
818  case ServerPacketHeader.ACHIEVEMENT_STAT:
819  ReadAchievementStat(inc);
820  break;
821  case ServerPacketHeader.CHEATS_ENABLED:
822  bool cheatsEnabled = inc.ReadBoolean();
823  inc.ReadPadBits();
824  if (cheatsEnabled == DebugConsole.CheatsEnabled)
825  {
826  return;
827  }
828  else
829  {
830  DebugConsole.CheatsEnabled = cheatsEnabled;
831  AchievementManager.CheatsEnabled = cheatsEnabled;
832  if (cheatsEnabled)
833  {
834  var cheatMessageBox = new GUIMessageBox(TextManager.Get("CheatsEnabledTitle"), TextManager.Get("CheatsEnabledDescription"));
835  cheatMessageBox.Buttons[0].OnClicked += (btn, userdata) =>
836  {
837  DebugConsole.TextBox.Select();
838  return true;
839  };
840  }
841  }
842  break;
843  case ServerPacketHeader.CREW:
844  campaign?.ClientReadCrew(inc);
845  break;
846  case ServerPacketHeader.MEDICAL:
847  campaign?.MedicalClinic?.ClientRead(inc);
848  break;
849  case ServerPacketHeader.CIRCUITBOX:
850  ReadCircuitBoxMessage(inc);
851  break;
852  case ServerPacketHeader.MONEY:
853  campaign?.ClientReadMoney(inc);
854  break;
855  case ServerPacketHeader.READY_CHECK:
856  ReadyCheck.ClientRead(inc);
857  break;
858  case ServerPacketHeader.TRAITOR_MESSAGE:
859  TraitorManager.ClientRead(inc);
860  break;
861  case ServerPacketHeader.FILE_TRANSFER:
863  break;
864  case ServerPacketHeader.MISSION:
865  {
866  int missionIndex = inc.ReadByte();
867  Mission mission = GameMain.GameSession?.GetMission(missionIndex);
868  mission?.ClientRead(inc);
869  }
870  break;
871  case ServerPacketHeader.EVENTACTION:
872  GameMain.GameSession?.EventManager.ClientRead(inc);
873  break;
874  }
875  }
876 
877  private void ReadStartGameFinalize(IReadMessage inc)
878  {
879  TaskPool.ListTasks(DebugConsole.Log);
880  ushort contentToPreloadCount = inc.ReadUInt16();
881  List<ContentFile> contentToPreload = new List<ContentFile>();
882  for (int i = 0; i < contentToPreloadCount; i++)
883  {
884  string filePath = inc.ReadString();
885  ContentFile file = ContentPackageManager.EnabledPackages.All
886  .Select(p =>
887  p.Files.FirstOrDefault(f => f.Path == filePath))
888  .FirstOrDefault(f => f is not null);
889  contentToPreload.AddIfNotNull(file);
890  }
891 
892  string campaignErrorInfo = string.Empty;
893  if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign)
894  {
895  campaignErrorInfo = $" Round start save ID: {debugStartGameCampaignSaveID}, last save id: {campaign.LastSaveID}, pending save id: {campaign.PendingSaveID}.";
896  }
897 
898  GameMain.GameSession.EventManager.PreloadContent(contentToPreload);
899 
900  int subEqualityCheckValue = inc.ReadInt32();
901  if (subEqualityCheckValue != (Submarine.MainSub?.Info?.EqualityCheckVal ?? 0))
902  {
903  string errorMsg =
904  "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server. " +
905  $"There may have been an error in receiving the up-to-date submarine file from the server. Round init status: {roundInitStatus}." + campaignErrorInfo;
906  GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:SubsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
907  throw new Exception(errorMsg);
908  }
909 
910  byte missionCount = inc.ReadByte();
911  List<Identifier> serverMissionIdentifiers = new List<Identifier>();
912  for (int i = 0; i < missionCount; i++)
913  {
914  serverMissionIdentifiers.Add(inc.ReadIdentifier());
915  }
916  if (missionCount != GameMain.GameSession.GameMode.Missions.Count())
917  {
918  string errorMsg =
919  $"Mission equality check failed. Mission count doesn't match the server. " +
920  $"Server: {string.Join(", ", serverMissionIdentifiers)}, " +
921  $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
922  $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo;
923  GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
924  throw new Exception(errorMsg);
925  }
926 
927  if (missionCount > 0)
928  {
929  if (!GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier).OrderBy(id => id).SequenceEqual(serverMissionIdentifiers.OrderBy(id => id)))
930  {
931  string errorMsg =
932  $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " +
933  $"Server: {string.Join(", ", serverMissionIdentifiers)}, " +
934  $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
935  $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo;
936  GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
937  throw new Exception(errorMsg);
938  }
939  GameMain.GameSession.EnforceMissionOrder(serverMissionIdentifiers);
940  }
941 
942  var levelEqualityCheckValues = new Dictionary<Level.LevelGenStage, int>();
943  foreach (Level.LevelGenStage stage in Enum.GetValues(typeof(Level.LevelGenStage)).OfType<Level.LevelGenStage>().OrderBy(s => s))
944  {
945  levelEqualityCheckValues.Add(stage, inc.ReadInt32());
946  }
947 
948  foreach (var stage in levelEqualityCheckValues.Keys)
949  {
950  if (Level.Loaded.EqualityCheckValues[stage] != levelEqualityCheckValues[stage])
951  {
952  string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server" +
953  " (client value " + stage + ": " + Level.Loaded.EqualityCheckValues[stage].ToString("X") +
954  ", server value " + stage + ": " + levelEqualityCheckValues[stage].ToString("X") +
955  ", level value count: " + levelEqualityCheckValues.Count +
956  ", seed: " + Level.Loaded.Seed +
957  ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" +
958  ", mirrored: " + Level.Loaded.Mirrored + "). Round init status: " + roundInitStatus + "." + campaignErrorInfo;
959  GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
960  throw new Exception(errorMsg);
961  }
962  }
963 
964  foreach (Mission mission in GameMain.GameSession.Missions)
965  {
966  mission.ClientReadInitial(inc);
967  }
968 
969  if (inc.ReadBoolean())
970  {
971  CrewManager.ClientReadActiveOrders(inc);
972  }
973 
974  roundInitStatus = RoundInitStatus.Started;
975  }
976 
980  private void OnClientPeerDisconnect(PeerDisconnectPacket disconnectPacket)
981  {
982  bool wasConnected = connected;
983  connected = false;
984  connectCancelled = true;
985 
986  CoroutineManager.StopCoroutines("WaitForStartingInfo");
987  CloseReconnectBox();
988 
989  GUI.ClearCursorWait();
990 
991  if (disconnectPacket.ShouldCreateAnalyticsEvent)
992  {
993  GameAnalyticsManager.AddErrorEventOnce(
994  "GameClient.HandleDisconnectMessage",
995  GameAnalyticsManager.ErrorSeverity.Debug,
996  $"Client received a disconnect message. Reason: {disconnectPacket.DisconnectReason}");
997  }
998 
999  if (disconnectPacket.DisconnectReason == DisconnectReason.ServerFull)
1000  {
1001  AskToWaitInQueue();
1002  }
1003  else if (disconnectPacket.ShouldAttemptReconnect && !IsServerOwner && wasConnected)
1004  {
1005  if (disconnectPacket.IsEventSyncError)
1006  {
1007  GameMain.NetLobbyScreen.Select();
1008  GameMain.GameSession?.EndRound("");
1009  GameStarted = false;
1010  myCharacter = null;
1011  }
1012  AttemptReconnect(disconnectPacket);
1013  }
1014  else
1015  {
1016  if (ClientPeer is P2PClientPeer or P2POwnerPeer)
1017  {
1018  Eos.EosSessionManager.LeaveSession();
1019  SteamManager.LeaveLobby();
1020  }
1021 
1022  GameMain.ModDownloadScreen.Reset();
1023  ContentPackageManager.EnabledPackages.Restore();
1024 
1025  GameMain.GameSession?.Campaign?.CancelStartRound();
1026 
1027  UpdatePresence("");
1028  foreach (var fileTransfer in FileReceiver.ActiveTransfers.ToArray())
1029  {
1030  FileReceiver.StopTransfer(fileTransfer, deleteFile: true);
1031  }
1032 
1033  ChildServerRelay.AttemptGracefulShutDown();
1034  GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary);
1035 
1036  characterInfo?.Remove();
1037 
1038  VoipClient?.Dispose();
1039  VoipClient = null;
1040  GameMain.Client = null;
1041  GameMain.GameSession = null;
1042 
1043  ReturnToPreviousMenu(null, null);
1044  if (disconnectPacket.DisconnectReason != DisconnectReason.Disconnected)
1045  {
1046  new GUIMessageBox(TextManager.Get(wasConnected ? "ConnectionLost" : "CouldNotConnectToServer"), disconnectPacket.PopupMessage)
1047  {
1048  DisplayInLoadingScreens = true
1049  };
1050  }
1051  }
1052  }
1053 
1054  private void CreateReconnectBox(LocalizedString headerText, LocalizedString bodyText)
1055  {
1056  reconnectBox = new GUIMessageBox(
1057  headerText,
1058  bodyText,
1059  new LocalizedString[] { TextManager.Get("Cancel") })
1060  {
1061  DisplayInLoadingScreens = true
1062  };
1063  reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; };
1064  reconnectBox.Buttons[0].OnClicked += reconnectBox.Close;
1065  }
1066 
1067  private void CloseReconnectBox()
1068  {
1069  reconnectBox?.Close();
1070  reconnectBox = null;
1071  }
1072 
1073  private void AskToWaitInQueue()
1074  {
1075  CoroutineManager.StopCoroutines("WaitForStartingInfo");
1076  //already waiting for a slot to free up, stop waiting for starting info and
1077  //let WaitInServerQueue reattempt connecting later
1078  if (CoroutineManager.IsCoroutineRunning("WaitInServerQueue"))
1079  {
1080  return;
1081  }
1082 
1083  var queueBox = new GUIMessageBox(
1084  TextManager.Get("DisconnectReason.ServerFull"),
1085  TextManager.Get("ServerFullQuestionPrompt"), new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("ServerQueue") });
1086 
1087  queueBox.Buttons[0].OnClicked += queueBox.Close;
1088  queueBox.Buttons[1].OnClicked += queueBox.Close;
1089  queueBox.Buttons[1].OnClicked += (btn, userdata) =>
1090  {
1091  CloseReconnectBox();
1092  CoroutineManager.StartCoroutine(WaitInServerQueue(), "WaitInServerQueue");
1093  return true;
1094  };
1095  }
1096 
1097  private void AttemptReconnect(PeerDisconnectPacket peerDisconnectPacket)
1098  {
1099  connectCancelled = false;
1100 
1101  CreateReconnectBox(
1102  TextManager.Get("ConnectionLost"),
1103  peerDisconnectPacket.ReconnectMessage);
1104 
1105  var prevContentPackages = ClientPeer.ServerContentPackages;
1106  //decrement lobby update ID to make sure we update the lobby when we reconnect
1107  GameMain.NetLobbyScreen.LastUpdateID--;
1108  InitiateServerJoin();
1109  if (ClientPeer != null)
1110  {
1111  //restore the previous list of content packages so we can reconnect immediately without having to recheck that the packages match
1112  ClientPeer.ContentPackageOrderReceived = true;
1113  ClientPeer.ServerContentPackages = prevContentPackages;
1114  }
1115  }
1116 
1117  private void UpdatePresence(string connectCommand)
1118  {
1119  #warning TODO: use store localization functionality
1120  var desc = TextManager.GetWithVariable("FriendPlayingOnServer", "[servername]", ServerName);
1121 
1122  async Task updateEosPresence()
1123  {
1124  var epicIds = EosInterface.IdQueries.GetLoggedInEpicIds();
1125  if (!epicIds.FirstOrNone().TryUnwrap(out var epicAccountId)) { return; }
1126 
1127  var setPresenceResult = await EosInterface.Presence.SetJoinCommand(
1128  epicAccountId: epicAccountId,
1129  desc: desc.Value,
1130  serverName: ServerName,
1131  joinCommand: connectCommand);
1132  DebugConsole.NewMessage($"Set connect command: {connectCommand}, result: {setPresenceResult}");
1133  }
1134 
1135  TaskPool.Add(
1136  "UpdateEosPresence",
1137  updateEosPresence(),
1138  static _ => { });
1139 
1140  if (SteamManager.IsInitialized)
1141  {
1142  Steamworks.SteamFriends.ClearRichPresence();
1143  if (!connectCommand.IsNullOrWhiteSpace())
1144  {
1145  Steamworks.SteamFriends.SetRichPresence("servername", ServerName);
1146  Steamworks.SteamFriends.SetRichPresence("status",
1147  desc.Value);
1148  Steamworks.SteamFriends.SetRichPresence("connect",
1149  connectCommand);
1150  }
1151  }
1152  }
1153 
1154  private void OnConnectionInitializationComplete()
1155  {
1156  UpdatePresence($"-connect \"{ToolBox.EscapeCharacters(ServerName)}\" {string.Join(",", serverEndpoints.Select(e => e.StringRepresentation))}");
1157 
1158  canStart = true;
1159  connected = true;
1160 
1161  VoipClient = new VoipClient(this, ClientPeer);
1162 
1163  //if we're still in the game, roundsummary or lobby screen, we don't need to redownload the mods
1164  if (Screen.Selected is GameScreen or RoundSummaryScreen or NetLobbyScreen)
1165  {
1167  foreach (Character c in Character.CharacterList)
1168  {
1169  c.ResetNetState();
1170  }
1171  }
1172  else
1173  {
1174  GameMain.ModDownloadScreen.Select();
1175  }
1176 
1177  chatBox.InputBox.Enabled = true;
1178  if (GameMain.NetLobbyScreen?.ChatInput != null)
1179  {
1180  GameMain.NetLobbyScreen.ChatInput.Enabled = true;
1181  }
1182  }
1183 
1184  private IEnumerable<CoroutineStatus> WaitInServerQueue()
1185  {
1186  waitInServerQueueBox = new GUIMessageBox(
1187  TextManager.Get("ServerQueuePleaseWait"),
1188  TextManager.Get("WaitingInServerQueue"), new LocalizedString[] { TextManager.Get("Cancel") });
1189  waitInServerQueueBox.Buttons[0].OnClicked += (btn, userdata) =>
1190  {
1191  CoroutineManager.StopCoroutines("WaitInServerQueue");
1192  waitInServerQueueBox?.Close();
1193  waitInServerQueueBox = null;
1194  return true;
1195  };
1196 
1197  while (!connected)
1198  {
1199  if (!CoroutineManager.IsCoroutineRunning("WaitForStartingInfo"))
1200  {
1201  InitiateServerJoin();
1202  yield return new WaitForSeconds(5.0f);
1203  }
1204  yield return new WaitForSeconds(0.5f);
1205  }
1206 
1207  waitInServerQueueBox?.Close();
1208  waitInServerQueueBox = null;
1209 
1210  yield return CoroutineStatus.Success;
1211  }
1212 
1213 
1214  private static void ReadAchievement(IReadMessage inc)
1215  {
1216  Identifier achievementIdentifier = inc.ReadIdentifier();
1217  AchievementManager.UnlockAchievement(achievementIdentifier);
1218  }
1219 
1220  private static void ReadAchievementStat(IReadMessage inc)
1221  {
1222  var netStat = INetSerializableStruct.Read<NetIncrementedStat>(inc);
1223  AchievementManager.IncrementStat(netStat.Stat, netStat.Amount);
1224  }
1225 
1226  private static void ReadCircuitBoxMessage(IReadMessage inc)
1227  {
1228  var header = INetSerializableStruct.Read<NetCircuitBoxHeader>(inc);
1229 
1230  INetSerializableStruct data = header.Opcode switch
1231  {
1232  CircuitBoxOpcode.Cursor => INetSerializableStruct.Read<NetCircuitBoxCursorInfo>(inc),
1233  CircuitBoxOpcode.Error => INetSerializableStruct.Read<CircuitBoxErrorEvent>(inc),
1234  _ => throw new ArgumentOutOfRangeException(nameof(header.Opcode), header.Opcode, "This data cannot be handled using direct network messages.")
1235  };
1236 
1237  if (header.FindTarget().TryUnwrap(out CircuitBox box))
1238  {
1239  box.ClientRead(data);
1240  }
1241  }
1242 
1243  private void ReadPermissions(IReadMessage inc)
1244  {
1245  List<string> permittedConsoleCommands = new List<string>();
1246  byte clientId = inc.ReadByte();
1247 
1248  ClientPermissions permissions = ClientPermissions.None;
1249  List<DebugConsole.Command> permittedCommands = new List<DebugConsole.Command>();
1250  Client.ReadPermissions(inc, out permissions, out permittedCommands);
1251 
1252  Client targetClient = ConnectedClients.Find(c => c.SessionId == clientId);
1253  targetClient?.SetPermissions(permissions, permittedCommands);
1254  if (clientId == SessionId)
1255  {
1256  SetMyPermissions(permissions, permittedCommands.Select(command => command.Names[0]));
1257  }
1258  }
1259 
1260  private void SetMyPermissions(ClientPermissions newPermissions, IEnumerable<Identifier> permittedConsoleCommands)
1261  {
1262  if (!(this.permittedConsoleCommands.Any(c => !permittedConsoleCommands.Contains(c)) ||
1263  permittedConsoleCommands.Any(c => !this.permittedConsoleCommands.Contains(c))))
1264  {
1265  if (newPermissions == permissions) { return; }
1266  }
1267 
1268  bool refreshCampaignUI = permissions.HasFlag(ClientPermissions.ManageCampaign) != newPermissions.HasFlag(ClientPermissions.ManageCampaign) ||
1269  permissions.HasFlag(ClientPermissions.ManageRound) != newPermissions.HasFlag(ClientPermissions.ManageRound);
1270 
1271  permissions = newPermissions;
1272  this.permittedConsoleCommands = permittedConsoleCommands.ToList();
1273  //don't show the "permissions changed" popup if the client owns the server
1274  if (!IsServerOwner)
1275  {
1276  GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "permissions");
1277  GUIMessageBox msgBox = new GUIMessageBox("", "") { UserData = "permissions" };
1278  msgBox.Content.ClearChildren();
1279  msgBox.Content.RectTransform.RelativeSize = new Vector2(0.95f, 0.9f);
1280 
1281  var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgBox.Content.RectTransform), TextManager.Get("PermissionsChanged"), textAlignment: Alignment.Center, font: GUIStyle.LargeFont);
1282  header.RectTransform.IsFixedSize = true;
1283 
1284  var permissionArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), msgBox.Content.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f };
1285  var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), permissionArea.RectTransform)) { Stretch = true, RelativeSpacing = 0.05f };
1286  var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), permissionArea.RectTransform)) { Stretch = true, RelativeSpacing = 0.05f };
1287 
1288  var permissionsLabel = new GUITextBlock(new RectTransform(new Vector2(newPermissions == ClientPermissions.None ? 2.0f : 1.0f, 0.0f), leftColumn.RectTransform),
1289  TextManager.Get(newPermissions == ClientPermissions.None ? "PermissionsRemoved" : "CurrentPermissions"),
1290  wrap: true, font: (newPermissions == ClientPermissions.None ? GUIStyle.Font : GUIStyle.SubHeadingFont));
1291  permissionsLabel.RectTransform.NonScaledSize = new Point(permissionsLabel.Rect.Width, permissionsLabel.Rect.Height);
1292  permissionsLabel.RectTransform.IsFixedSize = true;
1293  if (newPermissions != ClientPermissions.None)
1294  {
1295  LocalizedString permissionList = "";
1296  foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions)))
1297  {
1298  if (!newPermissions.HasFlag(permission) || permission == ClientPermissions.None) { continue; }
1299  permissionList += " - " + TextManager.Get("ClientPermission." + permission) + "\n";
1300  }
1301  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), leftColumn.RectTransform),
1302  permissionList);
1303  }
1304 
1305  if (newPermissions.HasFlag(ClientPermissions.ConsoleCommands))
1306  {
1307  var commandsLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform),
1308  TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUIStyle.SubHeadingFont);
1309  var commandList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform));
1310  foreach (Identifier permittedCommand in permittedConsoleCommands)
1311  {
1312  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), commandList.Content.RectTransform, minSize: new Point(0, 15)),
1313  permittedCommand.Value, font: GUIStyle.SmallFont)
1314  {
1315  CanBeFocused = false
1316  };
1317  }
1318  permissionsLabel.RectTransform.NonScaledSize = commandsLabel.RectTransform.NonScaledSize =
1319  new Point(permissionsLabel.Rect.Width, Math.Max(permissionsLabel.Rect.Height, commandsLabel.Rect.Height));
1320  commandsLabel.RectTransform.IsFixedSize = true;
1321  }
1322 
1323  new GUIButton(new RectTransform(new Vector2(0.5f, 0.05f), msgBox.Content.RectTransform), TextManager.Get("ok"))
1324  {
1325  OnClicked = msgBox.Close
1326  };
1327 
1328  permissionArea.RectTransform.MinSize = new Point(0, Math.Max(leftColumn.RectTransform.Children.Sum(c => c.Rect.Height), rightColumn.RectTransform.Children.Sum(c => c.Rect.Height)));
1329  permissionArea.RectTransform.IsFixedSize = true;
1330  int contentHeight = (int)(msgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height + msgBox.Content.AbsoluteSpacing) * 1.05f);
1331  msgBox.Content.ChildAnchor = Anchor.TopCenter;
1332  msgBox.Content.Stretch = true;
1333  msgBox.Content.RectTransform.MinSize = new Point(0, contentHeight);
1334  msgBox.InnerFrame.RectTransform.MinSize = new Point(0, (int)(contentHeight / permissionArea.RectTransform.RelativeSize.Y / msgBox.Content.RectTransform.RelativeSize.Y));
1335  }
1336 
1337  if (refreshCampaignUI)
1338  {
1339  if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1340  {
1341  campaign.CampaignUI?.UpgradeStore?.RequestRefresh();
1342  campaign.CampaignUI?.HRManagerUI?.RefreshUI();
1343  }
1344  }
1345 
1346  GameMain.NetLobbyScreen.RefreshEnabledElements();
1347  //close settings menu in case it was open
1349  OnPermissionChanged.Invoke(new PermissionChangedEvent(permissions, this.permittedConsoleCommands));
1350  }
1351 
1352  private IEnumerable<CoroutineStatus> StartGame(IReadMessage inc)
1353  {
1354  Character?.Remove();
1355  Character = null;
1356  HasSpawned = false;
1357  eventErrorWritten = false;
1358  GameMain.NetLobbyScreen.StopWaitingForStartRound();
1359 
1360  debugStartGameCampaignSaveID = null;
1361 
1362  while (CoroutineManager.IsCoroutineRunning("EndGame"))
1363  {
1364  EndCinematic?.Stop();
1365  yield return CoroutineStatus.Running;
1366  }
1367 
1368  //enable spectate button in case we fail to start the round now
1369  //(for example, due to a missing sub file or an error)
1370  GameMain.NetLobbyScreen.ShowSpectateButton();
1371 
1374 
1375  EndVoteTickBox.Selected = false;
1376 
1377  WaitForNextRoundRespawn = null;
1378 
1379  roundInitStatus = RoundInitStatus.Starting;
1380 
1381  int seed = inc.ReadInt32();
1382  string modeIdentifier = inc.ReadString();
1383 
1384  GameModePreset gameMode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier);
1385  if (gameMode == null)
1386  {
1387  DebugConsole.ThrowError("Game mode \"" + modeIdentifier + "\" not found!");
1388  roundInitStatus = RoundInitStatus.Interrupted;
1389  yield return CoroutineStatus.Failure;
1390  }
1391 
1392  bool respawnAllowed = inc.ReadBoolean();
1401  bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean();
1402  GameMain.LightManager.LosMode = (LosMode)inc.ReadByte();
1404  bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits();
1405  GameMain.LightManager.LightingEnabled = true;
1406 
1408 
1409  Rand.SetSyncedSeed(seed);
1410 
1411  Task loadTask = null;
1412  var roundSummary = (GUIMessageBox.MessageBoxes.Find(c => c?.UserData is RoundSummary)?.UserData) as RoundSummary;
1413 
1414  bool isOutpost = false;
1415 
1416  if (gameMode != GameModePreset.MultiPlayerCampaign)
1417  {
1418  string levelSeed = inc.ReadString();
1419  float levelDifficulty = inc.ReadSingle();
1420  string subName = inc.ReadString();
1421  string subHash = inc.ReadString();
1422  string shuttleName = inc.ReadString();
1423  string shuttleHash = inc.ReadString();
1424  List<UInt32> missionHashes = new List<UInt32>();
1425  int missionCount = inc.ReadByte();
1426  for (int i = 0; i < missionCount; i++)
1427  {
1428  missionHashes.Add(inc.ReadUInt32());
1429  }
1430  if (!GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList))
1431  {
1432  roundInitStatus = RoundInitStatus.Interrupted;
1433  yield return CoroutineStatus.Success;
1434  }
1435 
1436  if (!GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox))
1437  {
1438  roundInitStatus = RoundInitStatus.Interrupted;
1439  yield return CoroutineStatus.Success;
1440  }
1441 
1442  //this shouldn't happen, TrySelectSub should stop the coroutine if the correct sub/shuttle cannot be found
1443  if (GameMain.NetLobbyScreen.SelectedSub == null ||
1444  GameMain.NetLobbyScreen.SelectedSub.Name != subName ||
1445  GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation != subHash)
1446  {
1447  string errorMsg = "Failed to select submarine \"" + subName + "\" (hash: " + subHash + ").";
1448  if (GameMain.NetLobbyScreen.SelectedSub == null)
1449  {
1450  errorMsg += "\n" + "SelectedSub is null";
1451  }
1452  else
1453  {
1454  if (GameMain.NetLobbyScreen.SelectedSub.Name != subName)
1455  {
1456  errorMsg += "\n" + "Name mismatch: " + GameMain.NetLobbyScreen.SelectedSub.Name + " != " + subName;
1457  }
1458  if (GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation != subHash)
1459  {
1460  errorMsg += "\n" + "Hash mismatch: " + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation + " != " + subHash;
1461  }
1462  }
1463  GameStarted = true;
1464  GameMain.NetLobbyScreen.Select();
1465  DebugConsole.ThrowError(errorMsg);
1466  GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectSub" + subName, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1467  roundInitStatus = RoundInitStatus.Interrupted;
1468  yield return CoroutineStatus.Failure;
1469  }
1470  if (GameMain.NetLobbyScreen.SelectedShuttle == null ||
1471  GameMain.NetLobbyScreen.SelectedShuttle.Name != shuttleName ||
1472  GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash?.StringRepresentation != shuttleHash)
1473  {
1474  GameStarted = true;
1475  GameMain.NetLobbyScreen.Select();
1476  string errorMsg = "Failed to select shuttle \"" + shuttleName + "\" (hash: " + shuttleHash + ").";
1477  DebugConsole.ThrowError(errorMsg);
1478  GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectShuttle" + shuttleName, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1479  roundInitStatus = RoundInitStatus.Interrupted;
1480  yield return CoroutineStatus.Failure;
1481  }
1482 
1483  var selectedMissions = missionHashes.Select(i => MissionPrefab.Prefabs.Find(p => p.UintIdentifier == i));
1484 
1485  GameMain.GameSession = new GameSession(GameMain.NetLobbyScreen.SelectedSub, gameMode, missionPrefabs: selectedMissions);
1486  GameMain.GameSession.StartRound(levelSeed, levelDifficulty);
1487  }
1488  else
1489  {
1490  if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign))
1491  {
1492  throw new InvalidOperationException("Attempted to start a campaign round when a campaign was not active.");
1493  }
1494 
1495  if (GameMain.GameSession?.CrewManager != null) { GameMain.GameSession.CrewManager.Reset(); }
1496 
1497  byte campaignID = inc.ReadByte();
1498  UInt16 campaignSaveID = inc.ReadUInt16();
1499  int nextLocationIndex = inc.ReadInt32();
1500  int nextConnectionIndex = inc.ReadInt32();
1501  int selectedLocationIndex = inc.ReadInt32();
1502  bool mirrorLevel = inc.ReadBoolean();
1503 
1504  if (campaign.CampaignID != campaignID)
1505  {
1506  GameStarted = true;
1507  DebugConsole.ThrowError("Failed to start campaign round (campaign ID does not match).");
1508  GameMain.NetLobbyScreen.Select();
1509  roundInitStatus = RoundInitStatus.Interrupted;
1510  yield return CoroutineStatus.Failure;
1511  }
1512 
1513  if (NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID) ||
1514  NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID))
1515  {
1516  campaign.PendingSaveID = campaignSaveID;
1517  DateTime saveFileTimeOut = DateTime.Now + CampaignSaveTransferTimeOut;
1518  while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID))
1519  {
1520  if (DateTime.Now > saveFileTimeOut)
1521  {
1522  GameStarted = true;
1523  new GUIMessageBox(TextManager.Get("error"), TextManager.Get("campaignsavetransfer.timeout"));
1524  GameMain.NetLobbyScreen.Select();
1525  roundInitStatus = RoundInitStatus.Interrupted;
1526  //use success status, even though this is a failure (no need to show a console error because we show it in the message box)
1527  yield return CoroutineStatus.Success;
1528  }
1529  yield return new WaitForSeconds(0.1f);
1530  }
1531  }
1532 
1533  if (campaign.Map == null)
1534  {
1535  GameStarted = true;
1536  DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet).");
1537  GameMain.NetLobbyScreen.Select();
1538  roundInitStatus = RoundInitStatus.Interrupted;
1539  yield return CoroutineStatus.Failure;
1540  }
1541 
1542  campaign.Map.SelectLocation(selectedLocationIndex);
1543 
1544  LevelData levelData = nextLocationIndex > -1 ?
1545  campaign.Map.Locations[nextLocationIndex].LevelData :
1546  campaign.Map.Connections[nextConnectionIndex].LevelData;
1547 
1548  debugStartGameCampaignSaveID = campaign.LastSaveID;
1549 
1550  if (roundSummary != null)
1551  {
1552  loadTask = campaign.SelectSummaryScreen(roundSummary, levelData, mirrorLevel, null);
1553  roundSummary.ContinueButton.Visible = false;
1554  }
1555  else
1556  {
1557  GameMain.GameSession.StartRound(levelData, mirrorLevel, startOutpost: campaign?.GetPredefinedStartOutpost());
1558  }
1559  isOutpost = levelData.Type == LevelData.LevelType.Outpost;
1560  }
1561 
1562  Voting?.ResetVotes(GameMain.Client.ConnectedClients);
1563 
1564  if (loadTask != null)
1565  {
1566  while (!loadTask.IsCompleted && !loadTask.IsFaulted && !loadTask.IsCanceled)
1567  {
1568  yield return CoroutineStatus.Running;
1569  }
1570  }
1571 
1572  if (ClientPeer == null)
1573  {
1574  DebugConsole.ThrowError("There was an error initializing the round (disconnected during the StartGame coroutine.)");
1575  roundInitStatus = RoundInitStatus.Error;
1576  yield return CoroutineStatus.Failure;
1577  }
1578 
1579  roundInitStatus = RoundInitStatus.WaitingForStartGameFinalize;
1580 
1581  //wait for up to 30 seconds for the server to send the STARTGAMEFINALIZE message
1582  TimeSpan timeOutDuration = new TimeSpan(0, 0, seconds: 30);
1583  DateTime timeOut = DateTime.Now + timeOutDuration;
1584  DateTime requestFinalizeTime = DateTime.Now;
1585  TimeSpan requestFinalizeInterval = new TimeSpan(0, 0, 2);
1586  IWriteMessage msg = new WriteOnlyMessage();
1587  msg.WriteByte((byte)ClientPacketHeader.REQUEST_STARTGAMEFINALIZE);
1588  ClientPeer.Send(msg, DeliveryMethod.Unreliable);
1589 
1590  GUIMessageBox interruptPrompt = null;
1591 
1592  if (includesFinalize)
1593  {
1594  ReadStartGameFinalize(inc);
1595  }
1596  else
1597  {
1598  while (true)
1599  {
1600  try
1601  {
1602  if (DateTime.Now > requestFinalizeTime)
1603  {
1604  msg = new WriteOnlyMessage();
1605  msg.WriteByte((byte)ClientPacketHeader.REQUEST_STARTGAMEFINALIZE);
1606  ClientPeer.Send(msg, DeliveryMethod.Unreliable);
1607  requestFinalizeTime = DateTime.Now + requestFinalizeInterval;
1608  }
1609  if (DateTime.Now > timeOut && interruptPrompt == null)
1610  {
1611  interruptPrompt = new GUIMessageBox(string.Empty, TextManager.Get("WaitingForStartGameFinalizeTakingTooLong"),
1612  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") })
1613  {
1614  DisplayInLoadingScreens = true
1615  };
1616  interruptPrompt.Buttons[0].OnClicked += (btn, userData) =>
1617  {
1618  roundInitStatus = RoundInitStatus.Interrupted;
1619  DebugConsole.ThrowError("Error while starting the round (did not receive STARTGAMEFINALIZE message from the server). Returning to the lobby...");
1620  GameStarted = true;
1621  GameMain.NetLobbyScreen.Select();
1622  interruptPrompt.Close();
1623  interruptPrompt = null;
1624  return true;
1625  };
1626  interruptPrompt.Buttons[1].OnClicked += (btn, userData) =>
1627  {
1628  timeOut = DateTime.Now + timeOutDuration;
1629  interruptPrompt.Close();
1630  interruptPrompt = null;
1631  return true;
1632  };
1633  }
1634 
1635  if (!connected)
1636  {
1637  roundInitStatus = RoundInitStatus.Interrupted;
1638  break;
1639  }
1640 
1641  if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; }
1642  }
1643  catch (Exception e)
1644  {
1645  DebugConsole.ThrowError("There was an error initializing the round.", e, createMessageBox: true);
1646  roundInitStatus = RoundInitStatus.Error;
1647  break;
1648  }
1649 
1650  //waiting for a STARTGAMEFINALIZE message
1651  yield return CoroutineStatus.Running;
1652  }
1653  }
1654 
1655  interruptPrompt?.Close();
1656  interruptPrompt = null;
1657 
1658  if (roundInitStatus != RoundInitStatus.Started)
1659  {
1660  if (roundInitStatus != RoundInitStatus.Interrupted)
1661  {
1662  DebugConsole.ThrowError(roundInitStatus.ToString());
1663  CoroutineManager.StartCoroutine(EndGame(""));
1664  yield return CoroutineStatus.Failure;
1665  }
1666  else
1667  {
1668  yield return CoroutineStatus.Success;
1669  }
1670  }
1671 
1672  if (GameMain.GameSession.Submarine.Info.IsFileCorrupted)
1673  {
1674  DebugConsole.ThrowError($"Failed to start a round. Could not load the submarine \"{GameMain.GameSession.Submarine.Info.Name}\".");
1675  yield return CoroutineStatus.Failure;
1676  }
1677 
1678  for (int i = 0; i < Submarine.MainSubs.Length; i++)
1679  {
1680  if (Submarine.MainSubs[i] == null) { break; }
1681 
1682  var teamID = i == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2;
1683  Submarine.MainSubs[i].TeamID = teamID;
1684  foreach (Item item in Item.ItemList)
1685  {
1686  if (item.Submarine == null) { continue; }
1687  if (item.Submarine != Submarine.MainSubs[i] && !Submarine.MainSubs[i].DockedTo.Contains(item.Submarine)) { continue; }
1688  foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
1689  {
1690  wifiComponent.TeamID = Submarine.MainSubs[i].TeamID;
1691  }
1692  }
1693  foreach (Submarine sub in Submarine.MainSubs[i].DockedTo)
1694  {
1695  if (sub.Info.Type == SubmarineType.Outpost) { continue; }
1696  sub.TeamID = teamID;
1697  }
1698  }
1699 
1700  if (respawnAllowed)
1701  {
1702  RespawnManager = new RespawnManager(this, usingShuttle && !isOutpost ? GameMain.NetLobbyScreen.SelectedShuttle : null);
1703  }
1704 
1705  GameStarted = true;
1707 
1708  if (roundSummary != null)
1709  {
1710  roundSummary.ContinueButton.Visible = true;
1711  }
1712 
1713  GameMain.GameScreen.Select();
1714 
1715  string message = "ServerMessage.HowToCommunicate" +
1716  $"~[chatbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ActiveChat)}" +
1717  $"~[pttbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Voice)}" +
1718  $"~[switchbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleChatMode)}";
1719  AddChatMessage(message, ChatMessageType.Server);
1720 
1721  yield return CoroutineStatus.Success;
1722  }
1723 
1724  public IEnumerable<CoroutineStatus> EndGame(string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null)
1725  {
1726  //round starting up, wait for it to finish
1727  DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 60);
1728  while (TaskPool.IsTaskRunning("AsyncCampaignStartRound"))
1729  {
1730  if (DateTime.Now > timeOut)
1731  {
1732  throw new Exception("Failed to end a round (async campaign round start timed out).");
1733  }
1734  yield return new WaitForSeconds(1.0f);
1735  }
1736 
1737  if (!GameStarted)
1738  {
1740  yield return CoroutineStatus.Success;
1741  }
1742 
1743  GameMain.GameSession?.EndRound(endMessage, transitionType, traitorResults);
1744 
1746 
1747  GameStarted = false;
1748  Character.Controlled = null;
1749  WaitForNextRoundRespawn = null;
1750  GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
1751  GameMain.LightManager.LosEnabled = false;
1752  RespawnManager = null;
1753 
1755  {
1756  Submarine refSub = Submarine.MainSub;
1757  if (Submarine.MainSubs[1] != null &&
1760  {
1761  refSub = Submarine.MainSubs[1];
1762  }
1763 
1764  // Enable characters near the main sub for the endCinematic
1765  foreach (Character c in Character.CharacterList)
1766  {
1767  if (Vector2.DistanceSquared(refSub.WorldPosition, c.WorldPosition) < MathUtils.Pow2(c.Params.DisableDistance))
1768  {
1769  c.Enabled = true;
1770  }
1771  }
1772 
1773  EndCinematic = new CameraTransition(refSub, GameMain.GameScreen.Cam, Alignment.CenterLeft, Alignment.CenterRight);
1775  {
1776  yield return CoroutineStatus.Running;
1777  }
1778  EndCinematic = null;
1779  }
1780 
1781  Submarine.Unload();
1782  if (transitionType == CampaignMode.TransitionType.None)
1783  {
1785  }
1786  myCharacter = null;
1787  foreach (Client c in otherClients)
1788  {
1789  c.InGame = false;
1790  c.Character = null;
1791  }
1792 
1793  yield return CoroutineStatus.Success;
1794  }
1795 
1796  private void ReadInitialUpdate(IReadMessage inc)
1797  {
1798  SessionId = inc.ReadByte();
1799 
1800  UInt16 subListCount = inc.ReadUInt16();
1801  ServerSubmarines.Clear();
1802  for (int i = 0; i < subListCount; i++)
1803  {
1804  string subName = inc.ReadString();
1805  string subHash = inc.ReadString();
1806  SubmarineClass subClass = (SubmarineClass)inc.ReadByte();
1807  bool isShuttle = inc.ReadBoolean();
1808  bool requiredContentPackagesInstalled = inc.ReadBoolean();
1809 
1810  var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash);
1811  if (matchingSub == null)
1812  {
1813  matchingSub = new SubmarineInfo(Path.Combine(SaveUtil.SubmarineDownloadFolder, subName) + ".sub", subHash, tryLoad: false)
1814  {
1815  SubmarineClass = subClass
1816  };
1817  if (isShuttle) { matchingSub.AddTag(SubmarineTag.Shuttle); }
1818  }
1819  matchingSub.RequiredContentPackagesInstalled = requiredContentPackagesInstalled;
1820  ServerSubmarines.Add(matchingSub);
1821  }
1822 
1823  GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, ServerSubmarines);
1824  GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.ShuttleList.ListBox, ServerSubmarines.Where(s => s.HasTag(SubmarineTag.Shuttle)));
1825 
1826  GameStarted = inc.ReadBoolean();
1827  bool allowSpectating = inc.ReadBoolean();
1828  bool permadeathMode = inc.ReadBoolean();
1829  bool ironmanMode = inc.ReadBoolean();
1830 
1831  ReadPermissions(inc);
1832 
1833  if (GameStarted)
1834  {
1835  if (Screen.Selected != GameMain.GameScreen)
1836  {
1837  LocalizedString message;
1838  if (permadeathMode)
1839  {
1840  message = TextManager.Get(ironmanMode ? "RoundRunningIronman" : "RoundRunningPermadeath");
1841  }
1842  else
1843  {
1844  message = TextManager.Get(allowSpectating ? "RoundRunningSpectateEnabled" : "RoundRunningSpectateDisabled");
1845  }
1846  new GUIMessageBox(TextManager.Get("PleaseWait"), message);
1847  if (!(Screen.Selected is ModDownloadScreen)) { GameMain.NetLobbyScreen.Select(); }
1848  }
1849  }
1850  }
1851 
1852  private void ReadClientList(IReadMessage inc)
1853  {
1854  bool refreshCampaignUI = false;
1855  UInt16 listId = inc.ReadUInt16();
1856  List<TempClient> tempClients = new List<TempClient>();
1857  int clientCount = inc.ReadByte();
1858  for (int i = 0; i < clientCount; i++)
1859  {
1860  tempClients.Add(INetSerializableStruct.Read<TempClient>(inc));
1861  inc.ReadPadBits();
1862  }
1863 
1864  if (NetIdUtils.IdMoreRecent(listId, LastClientListUpdateID))
1865  {
1866  bool updateClientListId = true;
1867  List<Client> currentClients = new List<Client>();
1868  foreach (TempClient tc in tempClients)
1869  {
1870  //see if the client already exists
1871  var existingClient = ConnectedClients.Find(c => c.SessionId == tc.SessionId && c.Name == tc.Name);
1872  if (existingClient == null) //if not, create it
1873  {
1874  existingClient = new Client(tc.Name, tc.SessionId)
1875  {
1876  AccountInfo = tc.AccountInfo,
1877  Muted = tc.Muted,
1878  InGame = tc.InGame,
1879  IsOwner = tc.IsOwner
1880  };
1881  otherClients.Add(existingClient);
1882  refreshCampaignUI = true;
1883  GameMain.NetLobbyScreen.AddPlayer(existingClient);
1884  }
1885  existingClient.NameId = tc.NameId;
1886  existingClient.PreferredJob = tc.PreferredJob;
1887  existingClient.PreferredTeam = tc.PreferredTeam;
1888  existingClient.Character = null;
1889  existingClient.Karma = tc.Karma;
1890  existingClient.Muted = tc.Muted;
1891  existingClient.InGame = tc.InGame;
1892  existingClient.IsOwner = tc.IsOwner;
1893  existingClient.IsDownloading = tc.IsDownloading;
1894  GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient);
1895  if (Screen.Selected != GameMain.NetLobbyScreen && tc.CharacterId > 0)
1896  {
1897  existingClient.CharacterID = tc.CharacterId;
1898  }
1899  if (existingClient.SessionId == SessionId)
1900  {
1901  existingClient.SetPermissions(permissions, permittedConsoleCommands);
1902  if (!NetIdUtils.IdMoreRecent(nameId, tc.NameId))
1903  {
1904  Name = tc.Name;
1905  nameId = tc.NameId;
1906  }
1907  if (GameMain.NetLobbyScreen.CharacterNameBox != null &&
1908  !GameMain.NetLobbyScreen.CharacterNameBox.Selected)
1909  {
1910  GameMain.NetLobbyScreen.CharacterNameBox.Text = Name;
1911  }
1912  }
1913  currentClients.Add(existingClient);
1914  }
1915  //remove clients that aren't present anymore
1916  for (int i = ConnectedClients.Count - 1; i >= 0; i--)
1917  {
1918  if (!currentClients.Contains(ConnectedClients[i]))
1919  {
1920  GameMain.NetLobbyScreen.RemovePlayer(ConnectedClients[i]);
1921  otherClients[i].Dispose();
1922  otherClients.RemoveAt(i);
1923  refreshCampaignUI = true;
1924  }
1925  }
1926  foreach (Client client in ConnectedClients)
1927  {
1928  int index = previouslyConnectedClients.FindIndex(c => c.SessionId == client.SessionId);
1929  if (index < 0)
1930  {
1931  if (previouslyConnectedClients.Count > 100)
1932  {
1933  previouslyConnectedClients.RemoveRange(0, previouslyConnectedClients.Count - 100);
1934  }
1935  }
1936  else
1937  {
1938  previouslyConnectedClients.RemoveAt(index);
1939  }
1940  previouslyConnectedClients.Add(client);
1941  }
1942  if (updateClientListId) { LastClientListUpdateID = listId; }
1943 
1944  if (ClientPeer is P2POwnerPeer)
1945  {
1946  Eos.EosSessionManager.UpdateOwnedSession(ClientPeer.ServerConnection.Endpoint, ServerSettings);
1947  TaskPool.Add("WaitForPingDataAsync (owner)",
1948  Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) =>
1949  {
1950  Steam.SteamManager.UpdateLobby(ServerSettings);
1951  });
1952 
1953  Steam.SteamManager.UpdateLobby(ServerSettings);
1954  }
1955  }
1956 
1957  if (refreshCampaignUI)
1958  {
1959  if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1960  {
1961  campaign.CampaignUI?.UpgradeStore?.RequestRefresh();
1962  campaign.CampaignUI?.HRManagerUI?.RefreshUI();
1963  }
1964  }
1965  }
1966 
1967  private bool initialUpdateReceived;
1968 
1969  private void ReadLobbyUpdate(IReadMessage inc)
1970  {
1971  SegmentTableReader<ServerNetSegment>.Read(inc, (segment, inc) =>
1972  {
1973  switch (segment)
1974  {
1975  case ServerNetSegment.SyncIds:
1976  bool lobbyUpdated = inc.ReadBoolean();
1977  inc.ReadPadBits();
1978 
1979  if (lobbyUpdated)
1980  {
1981  var prevDispatcher = GUI.KeyboardDispatcher.Subscriber;
1982 
1983  UInt16 updateID = inc.ReadUInt16();
1984 
1985  UInt16 settingsLen = inc.ReadUInt16();
1986  byte[] settingsData = inc.ReadBytes(settingsLen);
1987 
1988  bool isInitialUpdate = inc.ReadBoolean();
1989  if (isInitialUpdate)
1990  {
1991  if (GameSettings.CurrentConfig.VerboseLogging)
1992  {
1993  DebugConsole.NewMessage("Received initial lobby update, ID: " + updateID + ", last ID: " + GameMain.NetLobbyScreen.LastUpdateID, Color.Gray);
1994  }
1995  ReadInitialUpdate(inc);
1996  initialUpdateReceived = true;
1997  }
1998 
1999  string selectSubName = inc.ReadString();
2000  string selectSubHash = inc.ReadString();
2001 
2002  bool usingShuttle = inc.ReadBoolean();
2003  string selectShuttleName = inc.ReadString();
2004  string selectShuttleHash = inc.ReadString();
2005 
2006  bool allowSubVoting = inc.ReadBoolean();
2007  bool allowModeVoting = inc.ReadBoolean();
2008 
2009  bool voiceChatEnabled = inc.ReadBoolean();
2010 
2011  bool allowSpectating = inc.ReadBoolean();
2012 
2013  float traitorProbability = inc.ReadSingle();
2014  int traitorDangerLevel = inc.ReadRangedInteger(TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel);
2015 
2016  MissionType missionType = (MissionType)inc.ReadRangedInteger(0, (int)MissionType.All);
2017  int modeIndex = inc.ReadByte();
2018 
2019  string levelSeed = inc.ReadString();
2020  float levelDifficulty = inc.ReadSingle();
2021 
2022  byte botCount = inc.ReadByte();
2023  BotSpawnMode botSpawnMode = inc.ReadBoolean() ? BotSpawnMode.Fill : BotSpawnMode.Normal;
2024 
2025  bool autoRestartEnabled = inc.ReadBoolean();
2026  float autoRestartTimer = autoRestartEnabled ? inc.ReadSingle() : 0.0f;
2027 
2028  //ignore the message if we already a more up-to-date one
2029  //or if we're still waiting for the initial update
2030  if (NetIdUtils.IdMoreRecent(updateID, GameMain.NetLobbyScreen.LastUpdateID) &&
2031  (isInitialUpdate || initialUpdateReceived))
2032  {
2033  ReadWriteMessage settingsBuf = new ReadWriteMessage();
2034  settingsBuf.WriteBytes(settingsData, 0, settingsLen); settingsBuf.BitPosition = 0;
2035  ServerSettings.ClientRead(settingsBuf);
2036  if (!IsServerOwner)
2037  {
2038  ServerInfo info = CreateServerInfoFromSettings();
2039  GameMain.ServerListScreen.AddToRecentServers(info);
2040  GameMain.NetLobbyScreen.Favorite.Visible = true;
2041  GameMain.NetLobbyScreen.Favorite.Selected = GameMain.ServerListScreen.IsFavorite(info);
2042  }
2043  else
2044  {
2045  GameMain.NetLobbyScreen.Favorite.Visible = false;
2046  }
2047 
2048  GameMain.NetLobbyScreen.LastUpdateID = updateID;
2049 
2051  GameMain.NetLobbyScreen.UsingShuttle = usingShuttle;
2052 
2053  if (!allowSubVoting || GameMain.NetLobbyScreen.SelectedSub == null) { GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, GameMain.NetLobbyScreen.SubList); }
2054  GameMain.NetLobbyScreen.TrySelectSub(selectShuttleName, selectShuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox);
2055 
2056  GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability);
2057  GameMain.NetLobbyScreen.SetTraitorDangerLevel(traitorDangerLevel);
2058  GameMain.NetLobbyScreen.SetMissionType(missionType);
2059  GameMain.NetLobbyScreen.LevelSeed = levelSeed;
2060 
2061  GameMain.NetLobbyScreen.SelectMode(modeIndex);
2062  if (isInitialUpdate && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
2063  {
2064  if (GameMain.Client.IsServerOwner) { RequestSelectMode(modeIndex); }
2065  }
2066 
2067  if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
2068  {
2069  foreach (SubmarineInfo sub in ServerSubmarines.Where(s => !ServerSettings.HiddenSubs.Contains(s.Name)))
2070  {
2071  GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Campaign);
2072  }
2073  }
2074 
2075  GameMain.NetLobbyScreen.SetAllowSpectating(allowSpectating);
2076  GameMain.NetLobbyScreen.SetLevelDifficulty(levelDifficulty);
2077  GameMain.NetLobbyScreen.SetBotSpawnMode(botSpawnMode);
2078  GameMain.NetLobbyScreen.SetBotCount(botCount);
2079  GameMain.NetLobbyScreen.SetAutoRestart(autoRestartEnabled, autoRestartTimer);
2080 
2081  ServerSettings.VoiceChatEnabled = voiceChatEnabled;
2082  ServerSettings.AllowSubVoting = allowSubVoting;
2083  ServerSettings.AllowModeVoting = allowModeVoting;
2084 
2085  if (ClientPeer is P2POwnerPeer)
2086  {
2087  Eos.EosSessionManager.UpdateOwnedSession(ClientPeer.ServerConnection.Endpoint, ServerSettings);
2088  Steam.SteamManager.UpdateLobby(ServerSettings);
2089  }
2090 
2091  GUI.KeyboardDispatcher.Subscriber = prevDispatcher;
2092  }
2093  }
2094 
2095  bool campaignUpdated = inc.ReadBoolean();
2096  inc.ReadPadBits();
2097  if (campaignUpdated)
2098  {
2099  MultiPlayerCampaign.ClientRead(inc);
2100  }
2101  else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
2102  {
2103  GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
2104  }
2105 
2106  lastSentChatMsgID = inc.ReadUInt16();
2107  break;
2108  case ServerNetSegment.ClientList:
2109  ReadClientList(inc);
2110  break;
2111  case ServerNetSegment.ChatMessage:
2112  ChatMessage.ClientRead(inc);
2113  break;
2114  case ServerNetSegment.Vote:
2115  Voting.ClientRead(inc);
2116  break;
2117  }
2118 
2119  return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.No;
2120  });
2121  }
2122 
2123  readonly List<IServerSerializable> debugEntityList = new List<IServerSerializable>();
2124  private void ReadIngameUpdate(IReadMessage inc)
2125  {
2126  debugEntityList.Clear();
2127 
2128  float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset;
2129 
2130  EndRoundTimeRemaining = inc.ReadSingle();
2131 
2133  segmentDataReader: (segment, inc) =>
2134  {
2135  switch (segment)
2136  {
2137  case ServerNetSegment.SyncIds:
2138  lastSentChatMsgID = inc.ReadUInt16();
2139  LastSentEntityEventID = inc.ReadUInt16();
2140 
2141  bool campaignUpdated = inc.ReadBoolean();
2142  inc.ReadPadBits();
2143  if (campaignUpdated)
2144  {
2145  MultiPlayerCampaign.ClientRead(inc);
2146  }
2147  else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
2148  {
2149  GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
2150  }
2151  break;
2152  case ServerNetSegment.EntityPosition:
2153  inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
2154 
2155  uint msgLength = inc.ReadVariableUInt32();
2156  int msgEndPos = (int)(inc.BitPosition + msgLength * 8);
2157 
2158  var header = INetSerializableStruct.Read<EntityPositionHeader>(inc);
2159 
2160  var entity = Entity.FindEntityByID(header.EntityId) as IServerPositionSync;
2161  if (msgEndPos > inc.LengthBits)
2162  {
2163  DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer.");
2164  return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
2165  }
2166 
2167  debugEntityList.Add(entity);
2168  if (entity != null)
2169  {
2170  if (entity is Item != header.IsItem)
2171  {
2172  DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(header.IsItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message...");
2173  }
2174  else if (entity is MapEntity { Prefab.UintIdentifier: var uintIdentifier } me &&
2175  uintIdentifier != header.PrefabUintIdentifier)
2176  {
2177  DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message."
2178  +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == header.PrefabUintIdentifier)?.Identifier.Value ?? "[not found]"}, "
2179  +$"client entity is {me.Prefab.Identifier}). Ignoring the message...");
2180  }
2181  else
2182  {
2183  entity.ClientReadPosition(inc, sendingTime);
2184  }
2185  }
2186  //force to the correct position in case the entity doesn't exist
2187  //or the message wasn't read correctly for whatever reason
2188  inc.BitPosition = msgEndPos;
2189  inc.ReadPadBits();
2190  break;
2191  case ServerNetSegment.ClientList:
2192  ReadClientList(inc);
2193  break;
2194  case ServerNetSegment.EntityEvent:
2195  case ServerNetSegment.EntityEventInitial:
2196  if (!EntityEventManager.Read(segment, inc, sendingTime))
2197  {
2198  return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
2199  }
2200  break;
2201  case ServerNetSegment.ChatMessage:
2202  ChatMessage.ClientRead(inc);
2203  break;
2204  default:
2205  throw new Exception($"Unknown segment \"{segment}\"!)");
2206  }
2207 
2208  return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.No;
2209  },
2210  exceptionHandler: (segment, prevSegments, ex) =>
2211  {
2212  List<string> errorLines = new List<string>
2213  {
2214  ex.Message,
2215  "Message length: " + inc.LengthBits + " (" + inc.LengthBytes + " bytes)",
2216  "Read position: " + inc.BitPosition,
2217  $"Segment with error: {segment}"
2218  };
2219  if (prevSegments.Any())
2220  {
2221  errorLines.Add("Prev segments: " + string.Join(", ", prevSegments));
2222  errorLines.Add(" ");
2223  }
2224  errorLines.Add(ex.StackTrace.CleanupStackTrace());
2225  errorLines.Add(" ");
2226  if (prevSegments.Concat(segment.ToEnumerable()).Any(s => s.Identifier
2227  is ServerNetSegment.EntityPosition
2228  or ServerNetSegment.EntityEvent
2229  or ServerNetSegment.EntityEventInitial))
2230  {
2231  foreach (IServerSerializable ent in debugEntityList)
2232  {
2233  if (ent == null)
2234  {
2235  errorLines.Add(" - NULL");
2236  continue;
2237  }
2238  Entity e = ent as Entity;
2239  errorLines.Add(" - " + e.ToString());
2240  }
2241  }
2242 
2243  errorLines.Add("Last console messages:");
2244  for (int i = DebugConsole.Messages.Count - 1; i > Math.Max(0, DebugConsole.Messages.Count - 20); i--)
2245  {
2246  errorLines.Add("[" + DebugConsole.Messages[i].Time + "] " + DebugConsole.Messages[i].Text);
2247  }
2248  GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsManager.ErrorSeverity.Critical, string.Join("\n", errorLines));
2249 
2250  throw new Exception(
2251  $"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." +
2252  (prevSegments.Any() ? $" Previous segments: {string.Join(", ", prevSegments)}" : ""),
2253  ex);
2254  });
2255  }
2256 
2257  private void SendLobbyUpdate()
2258  {
2259  IWriteMessage outmsg = new WriteOnlyMessage();
2260  outmsg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY);
2261 
2262  using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(outmsg))
2263  {
2264  segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
2265  outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID);
2266  outmsg.WriteUInt16(ChatMessage.LastID);
2267  outmsg.WriteUInt16(LastClientListUpdateID);
2268  outmsg.WriteUInt16(nameId);
2269  outmsg.WriteString(Name);
2270  var jobPreferences = GameMain.NetLobbyScreen.JobPreferences;
2271  if (jobPreferences.Count > 0)
2272  {
2273  outmsg.WriteIdentifier(jobPreferences[0].Prefab.Identifier);
2274  }
2275  else
2276  {
2277  outmsg.WriteIdentifier(Identifier.Empty);
2278  }
2279  outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference);
2280 
2281  if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.LastSaveID == 0)
2282  {
2283  outmsg.WriteUInt16((UInt16)0);
2284  }
2285  else
2286  {
2287  outmsg.WriteUInt16(campaign.LastSaveID);
2288  outmsg.WriteByte(campaign.CampaignID);
2289  foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
2290  {
2291  outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag));
2292  }
2293  outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
2294  }
2295 
2296  chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
2297  for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
2298  {
2299  if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
2300  {
2301  //no more room in this packet
2302  break;
2303  }
2304  chatMsgQueue[i].ClientWrite(segmentTable, outmsg);
2305  }
2306  }
2307  if (outmsg.LengthBytes > MsgConstants.MTU)
2308  {
2309  DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})");
2310  }
2311 
2312  ClientPeer.Send(outmsg, DeliveryMethod.Unreliable);
2313  }
2314 
2315  private void SendIngameUpdate()
2316  {
2317  IWriteMessage outmsg = new WriteOnlyMessage();
2318  outmsg.WriteByte((byte)ClientPacketHeader.UPDATE_INGAME);
2319  outmsg.WriteBoolean(EntityEventManager.MidRoundSyncingDone);
2320  outmsg.WritePadBits();
2321 
2322  using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(outmsg))
2323  {
2324  segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
2325  //outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID);
2326  outmsg.WriteUInt16(ChatMessage.LastID);
2327  outmsg.WriteUInt16(EntityEventManager.LastReceivedID);
2328  outmsg.WriteUInt16(LastClientListUpdateID);
2329 
2330  if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
2331  {
2332  outmsg.WriteUInt16((UInt16)0);
2333  }
2334  else
2335  {
2336  outmsg.WriteUInt16(campaign.LastSaveID);
2337  outmsg.WriteByte(campaign.CampaignID);
2338  foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
2339  {
2340  outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(flag));
2341  }
2342 
2343  outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
2344  }
2345 
2346  Character.Controlled?.ClientWriteInput(segmentTable, outmsg);
2347  GameMain.GameScreen.Cam?.ClientWrite(segmentTable, outmsg);
2348 
2349  EntityEventManager.Write(segmentTable, outmsg, ClientPeer?.ServerConnection);
2350 
2351  chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
2352  for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
2353  {
2354  if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
2355  {
2356  //not enough room in this packet
2357  break;
2358  }
2359 
2360  chatMsgQueue[i].ClientWrite(segmentTable, outmsg);
2361  }
2362  }
2363 
2364  if (outmsg.LengthBytes > MsgConstants.MTU)
2365  {
2366  DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})");
2367  }
2368 
2369  ClientPeer.Send(outmsg, DeliveryMethod.Unreliable);
2370  }
2371 
2372  public void SendChatMessage(ChatMessage msg)
2373  {
2374  if (ClientPeer?.ServerConnection == null) { return; }
2375  lastQueueChatMsgID++;
2376  msg.NetStateID = lastQueueChatMsgID;
2377  chatMsgQueue.Add(msg);
2378  }
2379 
2380  public void SendChatMessage(string message, ChatMessageType type = ChatMessageType.Default)
2381  {
2382  if (ClientPeer?.ServerConnection == null) { return; }
2383 
2384  ChatMessage chatMessage = ChatMessage.Create(
2385  GameStarted && myCharacter != null ? myCharacter.Name : Name,
2386  message,
2387  type,
2388  GameStarted && myCharacter != null ? myCharacter : null);
2389  chatMessage.ChatMode = GameMain.ActiveChatMode;
2390 
2391  lastQueueChatMsgID++;
2392  chatMessage.NetStateID = lastQueueChatMsgID;
2393 
2394  chatMsgQueue.Add(chatMessage);
2395  }
2396 
2397  public void SendRespawnPromptResponse(bool waitForNextRoundRespawn)
2398  {
2399  WaitForNextRoundRespawn = waitForNextRoundRespawn;
2400  IWriteMessage msg = new WriteOnlyMessage();
2401  msg.WriteByte((byte)ClientPacketHeader.READY_TO_SPAWN);
2403  msg.WriteBoolean(waitForNextRoundRespawn);
2404  ClientPeer?.Send(msg, DeliveryMethod.Reliable);
2405  }
2406 
2408  {
2409  IWriteMessage msg = new WriteOnlyMessage();
2410  msg.WriteByte((byte)ClientPacketHeader.TAKEOVERBOT);
2411  msg.WriteUInt16(bot.ID);
2412  ClientPeer?.Send(msg, DeliveryMethod.Reliable);
2413  }
2414 
2415  public void RequestFile(FileTransferType fileType, string file, string fileHash)
2416  {
2417  DebugConsole.Log(
2418  fileType == FileTransferType.CampaignSave ?
2419  $"Sending a campaign file request to the server." :
2420  $"Sending a file request to the server (type: {fileType}, path: {file ?? "null"}");
2421 
2422  IWriteMessage msg = new WriteOnlyMessage();
2423  msg.WriteByte((byte)ClientPacketHeader.FILE_REQUEST);
2424  msg.WriteByte((byte)FileTransferMessageType.Initiate);
2425  msg.WriteByte((byte)fileType);
2426  if (fileType != FileTransferType.CampaignSave)
2427  {
2428  msg.WriteString(file ?? throw new ArgumentNullException(nameof(file)));
2429  msg.WriteString(fileHash ?? throw new ArgumentNullException(nameof(fileHash)));
2430  }
2431  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2432  }
2433 
2435  {
2436  CancelFileTransfer(transfer.ID);
2437  }
2438 
2439  public void UpdateFileTransfer(FileReceiver.FileTransferIn transfer, int expecting, int lastSeen, bool reliable = false)
2440  {
2441  if (!reliable && (DateTime.Now - transfer.LastOffsetAckTime).TotalSeconds < 1)
2442  {
2443  return;
2444  }
2445  transfer.RecordOffsetAckTime();
2446 
2447  IWriteMessage msg = new WriteOnlyMessage();
2448  msg.WriteByte((byte)ClientPacketHeader.FILE_REQUEST);
2449  msg.WriteByte((byte)FileTransferMessageType.Data);
2450  msg.WriteByte((byte)transfer.ID);
2451  msg.WriteInt32(expecting);
2452  msg.WriteInt32(lastSeen);
2453  ClientPeer.Send(msg, reliable ? DeliveryMethod.Reliable : DeliveryMethod.Unreliable);
2454  }
2455 
2456  public void CancelFileTransfer(int id)
2457  {
2458  IWriteMessage msg = new WriteOnlyMessage();
2459  msg.WriteByte((byte)ClientPacketHeader.FILE_REQUEST);
2460  msg.WriteByte((byte)FileTransferMessageType.Cancel);
2461  msg.WriteByte((byte)id);
2462  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2463  }
2464 
2465  private void OnFileReceived(FileReceiver.FileTransferIn transfer)
2466  {
2467  switch (transfer.FileType)
2468  {
2469  case FileTransferType.Submarine:
2470  //new GUIMessageBox(TextManager.Get("ServerDownloadFinished"), TextManager.GetWithVariable("FileDownloadedNotification", "[filename]", transfer.FileName));
2471  var newSub = new SubmarineInfo(transfer.FilePath);
2472  if (newSub.IsFileCorrupted) { return; }
2473 
2474  var existingSubs = SubmarineInfo.SavedSubmarines
2475  .Where(s => s.Name == newSub.Name && s.MD5Hash == newSub.MD5Hash)
2476  .ToList();
2477  foreach (SubmarineInfo existingSub in existingSubs)
2478  {
2479  existingSub.Dispose();
2480  }
2481  SubmarineInfo.AddToSavedSubs(newSub);
2482 
2483  for (int i = 0; i < 2; i++)
2484  {
2485  IEnumerable<GUIComponent> subListChildren = (i == 0) ?
2486  GameMain.NetLobbyScreen.ShuttleList.ListBox.Content.Children :
2487  GameMain.NetLobbyScreen.SubList.Content.Children;
2488 
2489  var subElement = subListChildren.FirstOrDefault(c =>
2490  ((SubmarineInfo)c.UserData).Name == newSub.Name &&
2491  ((SubmarineInfo)c.UserData).MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation);
2492  if (subElement == null) { continue; }
2493 
2494  Color newSubTextColor = new Color(subElement.GetChild<GUITextBlock>().TextColor, 1.0f);
2495  subElement.GetChild<GUITextBlock>().TextColor = newSubTextColor;
2496 
2497  if (subElement.GetChildByUserData("classtext") is GUITextBlock classTextBlock)
2498  {
2499  Color newSubClassTextColor = new Color(classTextBlock.TextColor, 0.8f);
2500  classTextBlock.Text = TextManager.Get($"submarineclass.{newSub.SubmarineClass}");
2501  classTextBlock.TextColor = newSubClassTextColor;
2502  }
2503 
2504  subElement.UserData = newSub;
2505  subElement.ToolTip = newSub.Description;
2506  }
2507 
2508  if (GameMain.NetLobbyScreen.FailedSelectedSub.HasValue &&
2509  GameMain.NetLobbyScreen.FailedSelectedSub.Value.Name == newSub.Name &&
2510  GameMain.NetLobbyScreen.FailedSelectedSub.Value.Hash == newSub.MD5Hash.StringRepresentation)
2511  {
2512  GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, GameMain.NetLobbyScreen.SubList);
2513  }
2514 
2515  if (GameMain.NetLobbyScreen.FailedSelectedShuttle.HasValue &&
2516  GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Name == newSub.Name &&
2517  GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Hash == newSub.MD5Hash.StringRepresentation)
2518  {
2519  GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, GameMain.NetLobbyScreen.ShuttleList.ListBox);
2520  }
2521 
2522  NetLobbyScreen.FailedSubInfo failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation);
2523  if (failedCampaignSub != default)
2524  {
2525  GameMain.NetLobbyScreen.FailedCampaignSubs.Remove(failedCampaignSub);
2526  }
2527 
2528  NetLobbyScreen.FailedSubInfo failedOwnedSub = GameMain.NetLobbyScreen.FailedOwnedSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation);
2529  if (failedOwnedSub != default)
2530  {
2531  GameMain.NetLobbyScreen.FailedOwnedSubs.Remove(failedOwnedSub);
2532  }
2533 
2534  // Replace a submarine dud with the downloaded version
2535  SubmarineInfo existingServerSub = ServerSubmarines.Find(s =>
2536  s.Name == newSub.Name
2537  && s.MD5Hash == newSub.MD5Hash);
2538  if (existingServerSub != null)
2539  {
2540  int existingIndex = ServerSubmarines.IndexOf(existingServerSub);
2541  ServerSubmarines[existingIndex] = newSub;
2542  existingServerSub.Dispose();
2543  }
2544 
2545  break;
2546  case FileTransferType.CampaignSave:
2547  XElement gameSessionDocRoot = SaveUtil.DecompressSaveAndLoadGameSessionDoc(transfer.FilePath)?.Root;
2548  byte campaignID = (byte)MathHelper.Clamp(gameSessionDocRoot.GetAttributeInt("campaignid", 0), 0, 255);
2549  if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.CampaignID != campaignID)
2550  {
2551  string savePath = transfer.FilePath;
2552  GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty);
2553  campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode;
2554  campaign.CampaignID = campaignID;
2555  GameMain.NetLobbyScreen.ToggleCampaignMode(true);
2556  }
2557 
2558  GameMain.GameSession.SavePath = transfer.FilePath;
2559  if (GameMain.GameSession.SubmarineInfo == null || campaign.Map == null)
2560  {
2561  string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDocRoot.GetAttributeString("submarine", "")) + ".sub";
2562  GameMain.GameSession.SubmarineInfo = new SubmarineInfo(subPath, "");
2563  }
2564 
2565  campaign.LoadState(GameMain.GameSession.SavePath);
2566  GameMain.GameSession?.SubmarineInfo?.Reload();
2567  GameMain.GameSession?.SubmarineInfo?.CheckSubsLeftBehind();
2568 
2569  if (GameMain.GameSession?.SubmarineInfo?.Name != null)
2570  {
2571  GameMain.NetLobbyScreen.TryDisplayCampaignSubmarine(GameMain.GameSession.SubmarineInfo);
2572  }
2573  campaign.LastSaveID = campaign.PendingSaveID;
2574 
2575  if (Screen.Selected == GameMain.NetLobbyScreen)
2576  {
2577  //reselect to refresh the state of the lobby screen (enable spectate button, etc)
2578  GameMain.NetLobbyScreen.SaveAppearance();
2579  GameMain.NetLobbyScreen.Select();
2580  }
2581 
2582  DebugConsole.Log("Campaign save received (" + GameMain.GameSession.SavePath + "), save ID " + campaign.LastSaveID);
2583  //decrement campaign update IDs so the server will send us the latest data
2584  //(as there may have been campaign updates after the save file was created)
2585  foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
2586  {
2587  campaign.SetLastUpdateIdForFlag(flag, (ushort)(campaign.GetLastUpdateIdForFlag(flag) - 1));
2588  }
2589  break;
2590  case FileTransferType.Mod:
2591  if (!(Screen.Selected is ModDownloadScreen)) { return; }
2592 
2593  GameMain.ModDownloadScreen.CurrentDownloadFinished(transfer);
2594  break;
2595  }
2596  }
2597 
2598  private void OnTransferFailed(FileReceiver.FileTransferIn transfer)
2599  {
2600  if (transfer.FileType == FileTransferType.CampaignSave)
2601  {
2602  GameMain.Client.RequestFile(FileTransferType.CampaignSave, null, null);
2603  }
2604  }
2605 
2606  public override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null)
2607  {
2608  CreateEntityEvent(entity, extraData, requireControlledCharacter: true);
2609  }
2610 
2611  public void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData, bool requireControlledCharacter)
2612  {
2613  if (entity is not IClientSerializable clientSerializable)
2614  {
2615  throw new InvalidCastException($"Entity is not {nameof(IClientSerializable)}");
2616  }
2617  EntityEventManager.CreateEvent(clientSerializable, extraData, requireControlledCharacter);
2618  }
2619 
2620  public bool HasPermission(ClientPermissions permission)
2621  {
2622  return permissions.HasFlag(permission);
2623  }
2624 
2625  public bool HasConsoleCommandPermission(Identifier commandName)
2626  {
2627  if (!permissions.HasFlag(ClientPermissions.ConsoleCommands)) { return false; }
2628 
2629  if (permittedConsoleCommands.Contains(commandName)) { return true; }
2630 
2631  //check aliases
2632  foreach (DebugConsole.Command command in DebugConsole.Commands)
2633  {
2634  if (command.Names.Contains(commandName))
2635  {
2636  if (command.Names.Intersect(permittedConsoleCommands).Any()) { return true; }
2637  break;
2638  }
2639  }
2640 
2641  return false;
2642  }
2643 
2644  public void Quit()
2645  {
2646  GameMain.LuaCs.Stop();
2647 
2648  ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
2649 
2650  GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary);
2651  }
2652 
2653  public void SendCharacterInfo(string newName = null)
2654  {
2655  IWriteMessage msg = new WriteOnlyMessage();
2656  msg.WriteByte((byte)ClientPacketHeader.UPDATE_CHARACTERINFO);
2657  WriteCharacterInfo(msg, newName);
2658  ClientPeer?.Send(msg, DeliveryMethod.Reliable);
2659  }
2660 
2661  public void WriteCharacterInfo(IWriteMessage msg, string newName = null)
2662  {
2664  msg.WritePadBits();
2665  if (characterInfo == null) { return; }
2666 
2667  var head = characterInfo.Head;
2668 
2669  var netInfo = new NetCharacterInfo(
2670  NewName: newName ?? string.Empty,
2671  Tags: head.Preset.TagSet.ToImmutableArray(),
2672  HairIndex: (byte)head.HairIndex,
2673  BeardIndex: (byte)head.BeardIndex,
2674  MoustacheIndex: (byte)head.MoustacheIndex,
2675  FaceAttachmentIndex: (byte)head.FaceAttachmentIndex,
2676  SkinColor: head.SkinColor,
2677  HairColor: head.HairColor,
2678  FacialHairColor: head.FacialHairColor,
2679  JobVariants: GameMain.NetLobbyScreen.JobPreferences.Select(NetJobVariant.FromJobVariant).ToImmutableArray());
2680 
2681  msg.WriteNetSerializableStruct(netInfo);
2682  }
2683 
2684  public void Vote(VoteType voteType, object data)
2685  {
2686  if (ClientPeer == null) { return; }
2687 
2688  IWriteMessage msg = new WriteOnlyMessage();
2689  msg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY);
2690  using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(msg))
2691  {
2692  segmentTable.StartNewSegment(ClientNetSegment.Vote);
2693  bool succeeded = Voting.ClientWrite(msg, voteType, data);
2694  if (!succeeded)
2695  {
2696  throw new Exception(
2697  $"Failed to write vote of type {voteType}: " +
2698  $"data was of invalid type {data?.GetType().Name ?? "NULL"}");
2699  }
2700  }
2701 
2702  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2703  }
2704 
2705  public void VoteForKick(Client votedClient)
2706  {
2707  if (votedClient == null) { return; }
2708  Vote(VoteType.Kick, votedClient);
2709  }
2710 
2711  #region Submarine Change Voting
2712  public void InitiateSubmarineChange(SubmarineInfo sub, bool transferItems, VoteType voteType)
2713  {
2714  if (sub == null) { return; }
2715  Vote(voteType, (sub, transferItems));
2716  }
2717 
2718  public void ShowSubmarineChangeVoteInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float timeOut)
2719  {
2720  if (info == null) { return; }
2721  if (votingInterface != null && votingInterface.VoteRunning) { return; }
2722  votingInterface?.Remove();
2723  votingInterface = VotingInterface.CreateSubmarineVotingInterface(starter, info, type, transferItems, timeOut);
2724  }
2725  #endregion
2726 
2727  #region Money Transfer Voting
2728  public void ShowMoneyTransferVoteInterface(Client starter, Client from, int amount, Client to, float timeOut)
2729  {
2730  if (votingInterface != null && votingInterface.VoteRunning) { return; }
2731  if (from == null && to == null)
2732  {
2733  DebugConsole.ThrowError("Tried to initiate a vote for transferring from null to null!");
2734  return;
2735  }
2736  votingInterface?.Remove();
2737  votingInterface = VotingInterface.CreateMoneyTransferVotingInterface(starter, from, to, amount, timeOut);
2738  }
2739  #endregion
2740 
2741  public override void AddChatMessage(ChatMessage message)
2742  {
2743  var should = GameMain.LuaCs.Hook.Call<bool?>("chatMessage", message.Text, message.SenderClient, message.Type, message);
2744  if (should != null && should.Value) { return; }
2745 
2746  if (string.IsNullOrEmpty(message.Text)) { return; }
2747  if (message.SenderCharacter is { IsDead: false } sender)
2748  {
2749  if (message.Text.IsNullOrEmpty())
2750  {
2751  sender.ShowTextlessSpeechBubble(2.0f, message.Color);
2752  }
2753  else
2754  {
2755  sender.ShowSpeechBubble(message.Color, message.Text);
2756  if (!sender.IsBot)
2757  {
2758  sender.TextChatVolume = 1f;
2759  }
2760  }
2761  }
2762  GameMain.NetLobbyScreen.NewChatMessage(message);
2763  chatBox.AddMessage(message);
2764  }
2765 
2766  public override void KickPlayer(string kickedName, string reason)
2767  {
2768  IWriteMessage msg = new WriteOnlyMessage();
2769  msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
2770  msg.WriteUInt16((UInt16)ClientPermissions.Kick);
2771  msg.WriteString(kickedName);
2772  msg.WriteString(reason);
2773 
2774  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2775  }
2776 
2777  public override void BanPlayer(string kickedName, string reason, TimeSpan? duration = null)
2778  {
2779  IWriteMessage msg = new WriteOnlyMessage();
2780  msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
2781  msg.WriteUInt16((UInt16)ClientPermissions.Ban);
2782  msg.WriteString(kickedName);
2783  msg.WriteString(reason);
2784  msg.WriteDouble(duration.HasValue ? duration.Value.TotalSeconds : 0.0); //0 = permaban
2785 
2786  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2787  }
2788 
2789  public override void UnbanPlayer(string playerName)
2790  {
2791  IWriteMessage msg = new WriteOnlyMessage();
2792  msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
2793  msg.WriteUInt16((UInt16)ClientPermissions.Unban);
2794  msg.WriteBoolean(true); msg.WritePadBits();
2795  msg.WriteString(playerName);
2796  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2797  }
2798 
2799  public override void UnbanPlayer(Endpoint endpoint)
2800  {
2801  IWriteMessage msg = new WriteOnlyMessage();
2802  msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
2803  msg.WriteUInt16((UInt16)ClientPermissions.Unban);
2804  msg.WriteBoolean(false); msg.WritePadBits();
2805  msg.WriteString(endpoint.StringRepresentation);
2806  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2807  }
2808 
2809  public void UpdateClientPermissions(Client targetClient)
2810  {
2811  IWriteMessage msg = new WriteOnlyMessage();
2812  msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
2813  msg.WriteUInt16((UInt16)ClientPermissions.ManagePermissions);
2814  targetClient.WritePermissions(msg);
2815  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2816  }
2817 
2818  public void SendCampaignState()
2819  {
2820  if (!(GameMain.GameSession.GameMode is MultiPlayerCampaign campaign))
2821  {
2822  DebugConsole.ThrowError("Failed send campaign state to the server (no campaign active).\n" + Environment.StackTrace.CleanupStackTrace());
2823  return;
2824  }
2825  IWriteMessage msg = new WriteOnlyMessage();
2826  msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
2827  msg.WriteUInt16((UInt16)ClientPermissions.ManageCampaign);
2828  campaign.ClientWrite(msg);
2829  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2830  }
2831 
2832  public void SendConsoleCommand(string command)
2833  {
2834  if (string.IsNullOrWhiteSpace(command))
2835  {
2836  DebugConsole.ThrowError("Cannot send an empty console command to the server!\n" + Environment.StackTrace.CleanupStackTrace());
2837  return;
2838  }
2839 
2840  IWriteMessage msg = new WriteOnlyMessage();
2841  msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
2842  msg.WriteUInt16((UInt16)ClientPermissions.ConsoleCommands);
2843  msg.WriteString(command);
2844  Vector2 cursorWorldPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition);
2845  msg.WriteSingle(cursorWorldPos.X);
2846  msg.WriteSingle(cursorWorldPos.Y);
2847 
2848  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2849  }
2850 
2854  public void RequestStartRound(bool continueCampaign = false)
2855  {
2856  IWriteMessage msg = new WriteOnlyMessage();
2857  msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
2858  msg.WriteUInt16((UInt16)ClientPermissions.ManageRound);
2859  msg.WriteBoolean(false); //indicates round start
2860  msg.WriteBoolean(continueCampaign);
2861 
2862  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2863  }
2864 
2868  public void RequestSelectSub(SubmarineInfo sub, bool isShuttle)
2869  {
2870  if (!HasPermission(ClientPermissions.SelectSub) || sub == null) { return; }
2871 
2872  IWriteMessage msg = new WriteOnlyMessage();
2873  msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
2874  msg.WriteUInt16((UInt16)ClientPermissions.SelectSub);
2875  msg.WriteBoolean(isShuttle); msg.WritePadBits();
2877  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2878  }
2879 
2883  public void RequestSelectMode(int modeIndex)
2884  {
2885  if (modeIndex < 0 || modeIndex >= GameMain.NetLobbyScreen.ModeList.Content.CountChildren)
2886  {
2887  DebugConsole.ThrowError("Gamemode index out of bounds (" + modeIndex + ")\n" + Environment.StackTrace.CleanupStackTrace());
2888  return;
2889  }
2890 
2891  IWriteMessage msg = new WriteOnlyMessage();
2892  msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
2893  msg.WriteUInt16((UInt16)ClientPermissions.SelectMode);
2894  msg.WriteUInt16((UInt16)modeIndex);
2895 
2896  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2897  }
2898 
2899  public void SetupNewCampaign(SubmarineInfo sub, string saveName, string mapSeed, CampaignSettings settings)
2900  {
2903 
2904  saveName = Path.GetFileNameWithoutExtension(saveName);
2905 
2906  IWriteMessage msg = new WriteOnlyMessage();
2907  msg.WriteByte((byte)ClientPacketHeader.CAMPAIGN_SETUP_INFO);
2908 
2909  msg.WriteBoolean(true); msg.WritePadBits();
2910  msg.WriteString(saveName);
2911  msg.WriteString(mapSeed);
2912  msg.WriteString(sub.Name);
2914  msg.WriteNetSerializableStruct(settings);
2915 
2916  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2917  }
2918 
2919  public void SetupLoadCampaign(string saveName)
2920  {
2921  if (ClientPeer == null) { return; }
2922 
2925 
2926  IWriteMessage msg = new WriteOnlyMessage();
2927  msg.WriteByte((byte)ClientPacketHeader.CAMPAIGN_SETUP_INFO);
2928 
2929  msg.WriteBoolean(false); msg.WritePadBits();
2930  msg.WriteString(saveName);
2931 
2932  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2933  }
2934 
2938  public void RequestRoundEnd(bool save, bool quitCampaign = false)
2939  {
2940  IWriteMessage msg = new WriteOnlyMessage();
2941  msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
2942  msg.WriteUInt16((UInt16)ClientPermissions.ManageRound);
2943  msg.WriteBoolean(true); //indicates round end
2944  msg.WriteBoolean(save);
2945  msg.WriteBoolean(quitCampaign);
2946 
2947  ClientPeer.Send(msg, DeliveryMethod.Reliable);
2948  }
2949 
2950  public bool JoinOnGoingClicked(GUIButton button, object _)
2951  {
2952  MultiPlayerCampaign campaign =
2955 
2956  if (FileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave) ||
2957  (campaign != null && NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID)))
2958  {
2959  new GUIMessageBox("", TextManager.Get("campaignfiletransferinprogress"));
2960  return false;
2961  }
2962  if (button != null) { button.Enabled = false; }
2963  if (campaign != null) { LateCampaignJoin = true; }
2964 
2965  if (ClientPeer == null) { return false; }
2966 
2967  IWriteMessage readyToStartMsg = new WriteOnlyMessage();
2968  readyToStartMsg.WriteByte((byte)ClientPacketHeader.RESPONSE_STARTGAME);
2969 
2970  //assume we have the required sub files to start the round
2971  //(if not, we'll find out when the server sends the STARTGAME message and can initiate a file transfer)
2972  readyToStartMsg.WriteBoolean(true);
2973 
2974  WriteCharacterInfo(readyToStartMsg);
2975 
2976  ClientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable);
2977 
2978  return false;
2979  }
2980 
2981  public bool SetReadyToStart(GUITickBox tickBox)
2982  {
2983  if (GameStarted)
2984  {
2985  tickBox.Parent.Visible = false;
2986  return false;
2987  }
2988  Vote(VoteType.StartRound, tickBox.Selected);
2989  return true;
2990  }
2991 
2992  public bool ToggleEndRoundVote(GUITickBox tickBox)
2993  {
2994  if (!GameStarted) return false;
2995 
2996  if (!ServerSettings.AllowEndVoting || !HasSpawned)
2997  {
2998  tickBox.Visible = false;
2999  return false;
3000  }
3001 
3002  Vote(VoteType.EndRound, tickBox.Selected);
3003  return false;
3004  }
3005 
3008 
3010  {
3011  get { return characterInfo; }
3012  set { characterInfo = value; }
3013  }
3014 
3016  {
3017  get { return myCharacter; }
3018  set { myCharacter = value; }
3019  }
3020 
3021  protected GUIFrame inGameHUD;
3022  protected ChatBox chatBox;
3023 
3024  public GUIButton ShowLogButton; //TODO: move to NetLobbyScreen
3025  private bool hasPermissionToUseLogButton;
3026 
3028  {
3029  hasPermissionToUseLogButton = GameMain.Client.HasPermission(ClientPermissions.ServerLog);
3030  UpdateLogButtonVisibility();
3031  }
3032 
3033  private void UpdateLogButtonVisibility()
3034  {
3035  if (ShowLogButton != null)
3036  {
3038  {
3039  ShowLogButton.Visible = hasPermissionToUseLogButton;
3040  }
3041  else
3042  {
3043  var campaign = GameMain.GameSession?.Campaign;
3044  ShowLogButton.Visible = hasPermissionToUseLogButton && (campaign == null || !campaign.ShowCampaignUI);
3045  }
3046  }
3047  }
3048 
3049  public GUIFrame InGameHUD
3050  {
3051  get { return inGameHUD; }
3052  }
3053 
3055  {
3056  get { return chatBox; }
3057  }
3058 
3060  {
3061  get { return votingInterface; }
3062  }
3063  private VotingInterface votingInterface;
3064 
3065  public bool TypingChatMessage(GUITextBox textBox, string text)
3066  {
3067  return chatBox.TypingChatMessage(textBox, text);
3068  }
3069 
3070  public bool EnterChatMessage(GUITextBox textBox, string message)
3071  {
3072  textBox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Default];
3073 
3074  if (string.IsNullOrWhiteSpace(message))
3075  {
3076  if (textBox == chatBox.InputBox) textBox.Deselect();
3077  return false;
3078  }
3079  chatBox.ChatManager.Store(message);
3080  SendChatMessage(message);
3081 
3082  if (textBox.DeselectAfterMessage)
3083  {
3084  textBox.Deselect();
3085  }
3086  textBox.Text = "";
3087 
3089  {
3090  ChatBox.ToggleOpen = false;
3092  }
3093 
3094  return true;
3095  }
3096 
3097  public void AddToGUIUpdateList()
3098  {
3099  if (GUI.DisableHUD || GUI.DisableUpperHUD) return;
3100 
3101  if (GameStarted &&
3103  {
3104  inGameHUD.AddToGUIUpdateList();
3106  }
3107 
3110 
3112  }
3113 
3114  public void UpdateHUD(float deltaTime)
3115  {
3116  GUITextBox msgBox = null;
3117 
3119  {
3120  msgBox = chatBox.InputBox;
3121  }
3122  else if (Screen.Selected == GameMain.NetLobbyScreen)
3123  {
3125  }
3126 
3127  UpdateLogButtonVisibility();
3128 
3129  if (GameStarted && Screen.Selected == GameMain.GameScreen)
3130  {
3131  bool disableButtons = Character.Controlled?.SelectedItem?.GetComponent<Controller>() is Controller c1 && c1.HideHUD ||
3133  buttonContainer.Visible = !disableButtons;
3134 
3135  if (!GUI.DisableHUD && !GUI.DisableUpperHUD)
3136  {
3137  inGameHUD.UpdateManually(deltaTime);
3138  chatBox.Update(deltaTime);
3139 
3140  if (votingInterface != null)
3141  {
3142  votingInterface.Update(deltaTime);
3143  if (!votingInterface.VoteRunning || votingInterface.TimedOut)
3144  {
3145  if (votingInterface.TimedOut)
3146  {
3147  DebugConsole.AddWarning($"Voting interface timed out.");
3148  }
3149  votingInterface.Remove();
3150  votingInterface = null;
3151  }
3152  }
3153 
3154  cameraFollowsSub.Visible = Character.Controlled == null;
3155  }
3156  /*if (Character.Controlled == null || Character.Controlled.IsDead)
3157  {
3158  GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
3159  GameMain.LightManager.LosEnabled = false;
3160  }*/
3161  }
3162 
3163  //tab doesn't autoselect the chatbox when debug console is open,
3164  //because tab is used for autocompleting console commands
3165  if (msgBox != null)
3166  {
3167  if (GUI.KeyboardDispatcher.Subscriber == null)
3168  {
3169  var chatKeyStates = ChatBox.ChatKeyStates.GetChatKeyStates();
3170  if (chatKeyStates.AnyHit)
3171  {
3172  if (msgBox.Selected)
3173  {
3174  msgBox.Text = "";
3175  msgBox.Deselect();
3176  }
3177  else
3178  {
3180  {
3181  ChatBox.ApplySelectionInputs(msgBox, false, chatKeyStates);
3182  }
3183  msgBox.Select(msgBox.Text.Length);
3184  }
3185  }
3186  }
3187  }
3188  }
3189 
3190  public void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
3191  {
3192  if (GUI.DisableHUD || GUI.DisableUpperHUD) return;
3193 
3194  if (FileReceiver != null && FileReceiver.ActiveTransfers.Count > 0)
3195  {
3196  var transfer = FileReceiver.ActiveTransfers.First();
3200  ToolBox.LimitString(
3201  TextManager.GetWithVariable("DownloadingFile", "[filename]", transfer.FileName).Value,
3206  MathUtils.GetBytesReadable((long)transfer.Received) + " / " + MathUtils.GetBytesReadable((long)transfer.FileSize);
3207  }
3208  else
3209  {
3211  }
3212 
3213  if (!GameStarted || Screen.Selected != GameMain.GameScreen) { return; }
3214 
3215  inGameHUD.DrawManually(spriteBatch);
3216 
3217  int endVoteCount = Voting.GetVoteCountYes(VoteType.EndRound);
3218  int endVoteMax = Voting.GetVoteCountMax(VoteType.EndRound);
3219  if (endVoteCount > 0)
3220  {
3221  if (EndVoteTickBox.Visible)
3222  {
3223  EndVoteTickBox.Text = $"{endRoundVoteText} {endVoteCount}/{endVoteMax}";
3224  }
3225  else
3226  {
3227  LocalizedString endVoteText = TextManager.GetWithVariables("EndRoundVotes", ("[votes]", endVoteCount.ToString()), ("[max]", endVoteMax.ToString()));
3228  GUI.DrawString(spriteBatch, EndVoteTickBox.Rect.Center.ToVector2() - GUIStyle.SmallFont.MeasureString(endVoteText) / 2,
3229  endVoteText.Value,
3230  Color.White,
3231  font: GUIStyle.SmallFont);
3232  }
3233  }
3234  else
3235  {
3236  EndVoteTickBox.Text = endRoundVoteText;
3237  }
3238 
3239  if (RespawnManager != null)
3240  {
3241  LocalizedString respawnText = string.Empty;
3242  Color textColor = Color.White;
3243  bool hideRespawnButtons = false;
3244 
3245  if (EndRoundTimeRemaining > 0)
3246  {
3247  respawnText = TextManager.GetWithVariable("endinground", "[time]", ToolBox.SecondsToReadableTime(EndRoundTimeRemaining))
3248  .Fallback(ToolBox.SecondsToReadableTime(EndRoundTimeRemaining), useDefaultLanguageIfFound: false);
3249  }
3251  {
3253  {
3254  float timeLeft = (float)(RespawnManager.RespawnTime - DateTime.Now).TotalSeconds;
3255  respawnText = TextManager.GetWithVariable("RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft));
3256  }
3257  else if (RespawnManager.PendingRespawnCount > 0)
3258  {
3259  respawnText = TextManager.GetWithVariables("RespawnWaitingForMoreDeadPlayers",
3260  ("[deadplayers]", RespawnManager.PendingRespawnCount.ToString()),
3261  ("[requireddeadplayers]", RespawnManager.RequiredRespawnCount.ToString()));
3262  }
3263  }
3264  else if (RespawnManager.CurrentState == RespawnManager.State.Transporting &&
3266  {
3267  float timeLeft = (float)(RespawnManager.ReturnTime - DateTime.Now).TotalSeconds;
3268  respawnText = timeLeft <= 0.0f ?
3269  "" :
3270  TextManager.GetWithVariable("RespawnShuttleLeavingIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft));
3271  if (timeLeft < 20.0f)
3272  {
3273  //oscillate between 0-1
3274  float phase = (float)(Math.Sin(timeLeft * MathHelper.Pi) + 1.0f) * 0.5f;
3275  //textScale = 1.0f + phase * 0.5f;
3276  textColor = Color.Lerp(GUIStyle.Red, Color.White, 1.0f - phase);
3277  }
3278  hideRespawnButtons = true;
3279  }
3280 
3282  text: respawnText.Value, textColor: textColor,
3283  waitForNextRoundRespawn: (WaitForNextRoundRespawn ?? true), hideButtons: hideRespawnButtons);
3284  }
3285 
3286  if (!ShowNetStats) { return; }
3287 
3288  NetStats.Draw(spriteBatch, new Rectangle(300, 10, 300, 150));
3289 
3290  /* TODO: reimplement
3291  int width = 200, height = 300;
3292  int x = GameMain.GraphicsWidth - width, y = (int)(GameMain.GraphicsHeight * 0.3f);
3293 
3294  GUI.DrawRectangle(spriteBatch, new Rectangle(x, y, width, height), Color.Black * 0.7f, true);
3295  GUIStyle.Font.DrawString(spriteBatch, "Network statistics:", new Vector2(x + 10, y + 10), Color.White);
3296 
3297  if (client.ServerConnection != null)
3298  {
3299  GUIStyle.Font.DrawString(spriteBatch, "Ping: " + (int)(client.ServerConnection.AverageRoundtripTime * 1000.0f) + " ms", new Vector2(x + 10, y + 25), Color.White);
3300 
3301  y += 15;
3302 
3303  GUIStyle.SmallFont.DrawString(spriteBatch, "Received bytes: " + client.Statistics.ReceivedBytes, new Vector2(x + 10, y + 45), Color.White);
3304  GUIStyle.SmallFont.DrawString(spriteBatch, "Received packets: " + client.Statistics.ReceivedPackets, new Vector2(x + 10, y + 60), Color.White);
3305 
3306  GUIStyle.SmallFont.DrawString(spriteBatch, "Sent bytes: " + client.Statistics.SentBytes, new Vector2(x + 10, y + 75), Color.White);
3307  GUIStyle.SmallFont.DrawString(spriteBatch, "Sent packets: " + client.Statistics.SentPackets, new Vector2(x + 10, y + 90), Color.White);
3308  }
3309  else
3310  {
3311  GUIStyle.Font.DrawString(spriteBatch, "Disconnected", new Vector2(x + 10, y + 25), Color.White);
3312  }*/
3313  }
3314 
3315  public bool SelectCrewCharacter(Character character, GUIComponent frame)
3316  {
3317  if (character == null) { return false; }
3318 
3319  if (character != myCharacter)
3320  {
3321  var client = previouslyConnectedClients.Find(c => c.Character == character);
3322  if (client == null) { return false; }
3323 
3324  CreateSelectionRelatedButtons(client, frame);
3325  }
3326 
3327  return true;
3328  }
3329 
3330  public bool SelectCrewClient(Client client, GUIComponent frame)
3331  {
3332  if (client == null || client.SessionId == SessionId) { return false; }
3333  CreateSelectionRelatedButtons(client, frame);
3334  return true;
3335  }
3336 
3337  private void CreateSelectionRelatedButtons(Client client, GUIComponent frame)
3338  {
3339  var content = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1.0f - frame.RectTransform.RelativeSize.Y), frame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), childAnchor: Anchor.TopCenter);
3340 
3341  var mute = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.2f), content.RectTransform, Anchor.TopCenter),
3342  TextManager.Get("Mute"))
3343  {
3344  Selected = client.MutedLocally,
3345  OnSelected = (tickBox) => { client.MutedLocally = tickBox.Selected; return true; }
3346  };
3347 
3348  var volumeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.45f), content.RectTransform, Anchor.TopCenter), isHorizontal: false);
3349 
3350  var volumeTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
3351  var label = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), volumeTextLayout.RectTransform), TextManager.Get("VoiceChatVolume"));
3352  var percentageText = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), volumeTextLayout.RectTransform), ToolBox.GetFormattedPercentage(client.VoiceVolume), textAlignment: Alignment.Right);
3353 
3354  var volumeSlider = new GUIScrollBar(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), barSize: 0.1f, style: "GUISlider")
3355  {
3356  Range = new Vector2(0f, 1f),
3357  BarScroll = client.VoiceVolume / Client.MaxVoiceChatBoost,
3358  OnMoved = (_, barScroll) =>
3359  {
3360  float newVolume = barScroll * Client.MaxVoiceChatBoost;
3361 
3362  client.VoiceVolume = newVolume;
3363  percentageText.Text = ToolBox.GetFormattedPercentage(newVolume);
3364  return true;
3365  }
3366  };
3367 
3368  var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft)
3369  {
3370  RelativeSpacing = 0.05f,
3371  Stretch = true
3372  };
3373 
3374  if (!GameMain.Client.GameStarted || (GameMain.Client.Character == null || GameMain.Client.Character.IsDead) && (client.Character == null || client.Character.IsDead))
3375  {
3376  var messageButton = new GUIButton(new RectTransform(new Vector2(1f, 0.2f), content.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0f, buttonContainer.RectTransform.RelativeSize.Y) },
3377  TextManager.Get("message"), style: "GUIButtonSmall")
3378  {
3379  UserData = client,
3380  OnClicked = (btn, userdata) =>
3381  {
3382  chatBox.InputBox.Text = $"{client.Name}; ";
3383  CoroutineManager.StartCoroutine(selectCoroutine());
3384  return false;
3385  }
3386  };
3387  }
3388 
3389  // Need a delayed selection due to the inputbox being deselected when a left click occurs outside of it
3390  IEnumerable<CoroutineStatus> selectCoroutine()
3391  {
3392  yield return new WaitForSeconds(0.01f, true);
3393  chatBox.InputBox.Select(chatBox.InputBox.Text.Length);
3394  }
3395 
3396  if (HasPermission(ClientPermissions.Ban) && client.AllowKicking)
3397  {
3398  var banButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform),
3399  TextManager.Get("Ban"), style: "GUIButtonSmall")
3400  {
3401  UserData = client,
3402  OnClicked = (btn, userdata) => { NetLobbyScreen.BanPlayer(client); return false; }
3403  };
3404  }
3405  if (HasPermission(ClientPermissions.Kick) && client.AllowKicking)
3406  {
3407  var kickButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform),
3408  TextManager.Get("Kick"), style: "GUIButtonSmall")
3409  {
3410  UserData = client,
3411  OnClicked = (btn, userdata) => { NetLobbyScreen.KickPlayer(client); return false; }
3412  };
3413  }
3414  else if (ServerSettings.AllowVoteKick && client.AllowKicking)
3415  {
3416  var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform),
3417  TextManager.Get("VoteToKick"), style: "GUIButtonSmall")
3418  {
3419  UserData = client,
3420  OnClicked = (btn, userdata) => { VoteForKick(client); btn.Enabled = false; return true; }
3421  };
3422  }
3423  }
3424 
3425  public void CreateKickReasonPrompt(string clientName, bool ban)
3426  {
3427  var banReasonPrompt = new GUIMessageBox(
3428  TextManager.Get(ban ? "BanReasonPrompt" : "KickReasonPrompt"),
3429  "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.25f), new Point(400, 260));
3430 
3431  var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.6f), banReasonPrompt.InnerFrame.RectTransform, Anchor.Center))
3432  {
3433  AbsoluteSpacing = GUI.IntScale(5)
3434  };
3435  var banReasonBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform))
3436  {
3437  Wrap = true,
3438  MaxTextLength = 100
3439  };
3440 
3441  GUINumberInput durationInputDays = null, durationInputHours = null;
3442  GUITickBox permaBanTickBox = null;
3443 
3444  if (ban)
3445  {
3446  var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), content.RectTransform), isHorizontal: false);
3447  new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), labelContainer.RectTransform), TextManager.Get("BanDuration"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero };
3448  var buttonContent = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), labelContainer.RectTransform), isHorizontal: true);
3449  permaBanTickBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.15f), buttonContent.RectTransform), TextManager.Get("BanPermanent"))
3450  {
3451  Selected = true
3452  };
3453 
3454  var durationContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1f), buttonContent.RectTransform), isHorizontal: true)
3455  {
3456  Visible = false
3457  };
3458 
3459  permaBanTickBox.OnSelected += (tickBox) =>
3460  {
3461  durationContainer.Visible = !tickBox.Selected;
3462  return true;
3463  };
3464 
3465  durationInputDays = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), NumberType.Int)
3466  {
3467  MinValueInt = 0,
3468  MaxValueFloat = 1000
3469  };
3470  new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), TextManager.Get("Days"));
3471  durationInputHours = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), NumberType.Int)
3472  {
3473  MinValueInt = 0,
3474  MaxValueFloat = 24
3475  };
3476  new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), TextManager.Get("Hours"));
3477  }
3478 
3479  banReasonPrompt.Buttons[0].OnClicked += (btn, userData) =>
3480  {
3481  if (ban)
3482  {
3483  if (!permaBanTickBox.Selected)
3484  {
3485  TimeSpan banDuration = new TimeSpan(durationInputDays.IntValue, durationInputHours.IntValue, 0, 0);
3486  BanPlayer(clientName, banReasonBox.Text, banDuration);
3487  }
3488  else
3489  {
3490  BanPlayer(clientName, banReasonBox.Text);
3491  }
3492  }
3493  else
3494  {
3495  KickPlayer(clientName, banReasonBox.Text);
3496  }
3497  return true;
3498  };
3499  banReasonPrompt.Buttons[0].OnClicked += banReasonPrompt.Close;
3500  banReasonPrompt.Buttons[1].OnClicked += banReasonPrompt.Close;
3501  }
3502 
3503  public void ReportError(ClientNetError error, UInt16 expectedId = 0, UInt16 eventId = 0, UInt16 entityId = 0)
3504  {
3505  IWriteMessage outMsg = new WriteOnlyMessage();
3506  outMsg.WriteByte((byte)ClientPacketHeader.ERROR);
3507  outMsg.WriteByte((byte)error);
3508  switch (error)
3509  {
3510  case ClientNetError.MISSING_EVENT:
3511  outMsg.WriteUInt16(expectedId);
3512  outMsg.WriteUInt16(eventId);
3513  break;
3514  case ClientNetError.MISSING_ENTITY:
3515  outMsg.WriteUInt16(eventId);
3516  outMsg.WriteUInt16(entityId);
3517  outMsg.WriteByte((byte)Submarine.Loaded.Count);
3518  foreach (Submarine sub in Submarine.Loaded)
3519  {
3520  outMsg.WriteString(sub.Info.Name);
3521  }
3522  break;
3523  }
3524  ClientPeer.Send(outMsg, DeliveryMethod.Reliable);
3525 
3526  WriteEventErrorData(error, expectedId, eventId, entityId);
3527  }
3528 
3529  private bool eventErrorWritten;
3530  private void WriteEventErrorData(ClientNetError error, UInt16 expectedID, UInt16 eventID, UInt16 entityID)
3531  {
3532  if (eventErrorWritten) { return; }
3533  List<string> errorLines = new List<string>
3534  {
3535  error.ToString(), ""
3536  };
3537 
3538  if (IsServerOwner)
3539  {
3540  errorLines.Add("SERVER OWNER");
3541  }
3542 
3543  if (error == ClientNetError.MISSING_EVENT)
3544  {
3545  errorLines.Add("Expected ID: " + expectedID + ", received " + eventID);
3546  }
3547  else if (error == ClientNetError.MISSING_ENTITY)
3548  {
3549  errorLines.Add("Event ID: " + eventID + ", entity ID " + entityID);
3550  }
3551 
3552  if (GameMain.GameSession?.GameMode != null)
3553  {
3554  errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name.Value);
3555  if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
3556  {
3557  errorLines.Add("Campaign ID: " + campaign.CampaignID);
3558  errorLines.Add("Campaign save ID: " + campaign.LastSaveID + "(pending: " + campaign.PendingSaveID + ")");
3559  }
3560  foreach (Mission mission in GameMain.GameSession.Missions)
3561  {
3562  errorLines.Add("Mission: " + mission.Prefab.Identifier);
3563  }
3564  }
3565  if (GameMain.GameSession?.Submarine != null)
3566  {
3567  errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Info.Name);
3568  }
3569  if (GameMain.NetworkMember?.RespawnManager?.RespawnShuttle != null)
3570  {
3571  errorLines.Add("Respawn shuttle: " + GameMain.NetworkMember.RespawnManager.RespawnShuttle.Info.Name);
3572  }
3573  if (Level.Loaded != null)
3574  {
3575  errorLines.Add("Level: " + Level.Loaded.Seed + ", "
3576  + string.Join("; ", Level.Loaded.EqualityCheckValues.Select(cv
3577  => cv.Key + "=" + cv.Value.ToString("X"))));
3578  errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate);
3579  errorLines.Add("Entities:");
3580  foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex))
3581  {
3582  errorLines.Add(e.ErrorLine);
3583  }
3584  errorLines.Add("Entity count after generating level: " + Level.Loaded.EntityCountAfterGenerate);
3585  }
3586 
3587  errorLines.Add("Entity IDs:");
3588  Entity[] sortedEntities = Entity.GetEntities().OrderBy(e => e.CreationIndex).ToArray();
3589  foreach (Entity e in sortedEntities)
3590  {
3591  errorLines.Add(e.ErrorLine);
3592  }
3593 
3594  if (Entity.Spawner != null)
3595  {
3596  errorLines.Add("");
3597  errorLines.Add("EntitySpawner events:");
3598  foreach ((Entity entity, bool isRemoval) in Entity.Spawner.receivedEvents)
3599  {
3600  errorLines.Add(
3601  (isRemoval ? "Remove " : "Create ") +
3602  entity.ToString() +
3603  " (" + entity.ID + ")");
3604  }
3605  }
3606 
3607  errorLines.Add("");
3608  errorLines.Add("Last debug messages:");
3609  for (int i = DebugConsole.Messages.Count - 1; i > 0 && i > DebugConsole.Messages.Count - 15; i--)
3610  {
3611  errorLines.Add(" " + DebugConsole.Messages[i].Time + " - " + DebugConsole.Messages[i].Text);
3612  }
3613 
3614  string filePath = $"event_error_log_client_{Name}_{DateTime.UtcNow.ToShortTimeString()}.log";
3615  filePath = Path.Combine(ServerLog.SavePath, ToolBox.RemoveInvalidFileNameChars(filePath));
3616 
3617  if (!Directory.Exists(ServerLog.SavePath))
3618  {
3619  Directory.CreateDirectory(ServerLog.SavePath);
3620  }
3621  File.WriteAllLines(filePath, errorLines);
3622 
3623  eventErrorWritten = true;
3624  }
3625 
3626  private static void AppendExceptionInfo(ref string errorMsg, Exception e)
3627  {
3628  if (!errorMsg.EndsWith("\n")) { errorMsg += "\n"; }
3629  errorMsg += e.Message + "\n";
3630  var innermostException = e.GetInnermost();
3631  if (innermostException != e)
3632  {
3633  // If available, only append the stacktrace of the innermost exception,
3634  // because that's the most important one to fix
3635  errorMsg += "Inner exception: " + innermostException.Message + "\n" + innermostException.StackTrace.CleanupStackTrace();
3636  }
3637  else
3638  {
3639  errorMsg += e.StackTrace.CleanupStackTrace();
3640  }
3641  }
3642 
3643 #if DEBUG
3644  public void ForceTimeOut()
3645  {
3646  ClientPeer?.ForceTimeOut();
3647  }
3648 #endif
3649  }
3650 }
Vector2 ScreenToWorld(Vector2 coords)
Definition: Camera.cs:410
static bool FollowSub
Definition: Camera.cs:11
Vector2 TargetPos
Definition: Camera.cs:156
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
Item SelectedSecondaryItem
The secondary selected item. It's an item other than a device (see SelectedItem), e....
Stores information about the Character that is needed between rounds in the menu etc....
ushort ID
Unique ID given to character infos in MP. Non-persistent. Used by clients to identify which infos are...
static readonly Identifier HumanSpeciesName
void ApplySelectionInputs()
GUITextBox.OnEnterHandler OnEnterMessage
Definition: ChatBox.cs:54
void Update(float deltaTime)
Definition: ChatBox.cs:614
GUITextBox InputBox
Definition: ChatBox.cs:61
bool CloseAfterMessageSent
Definition: ChatBox.cs:36
readonly ChatManager ChatManager
Definition: ChatBox.cs:18
bool TypingChatMessage(GUITextBox textBox, string text)
Definition: ChatBox.cs:342
void Store(string message)
Definition: ChatManager.cs:84
static CoroutineStatus Running
static CoroutineStatus Success
virtual Vector2 WorldPosition
Definition: Entity.cs:49
override bool Enabled
Definition: GUIButton.cs:27
GUITextBlock TextBlock
Definition: GUIButton.cs:11
virtual void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
void UpdateManually(float deltaTime, bool alsoChildren=false, bool recursive=true)
By default, all the gui elements are updated automatically in the same order they appear on the updat...
virtual void DrawManually(SpriteBatch spriteBatch, bool alsoChildren=false, bool recursive=true)
By default, all the gui elements are drawn automatically in the same order they appear on the update ...
virtual Rectangle Rect
RectTransform RectTransform
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
static readonly List< GUIComponent > MessageBoxes
List< GUIButton > Buttons
override GUIFont Font
Definition: GUITextBlock.cs:66
bool AutoScaleHorizontal
When enabled, the text is automatically scaled down to fit the textblock horizontally.
OnTextChangedHandler OnTextChanged
Don't set the Text property on delegates that register to this event, because modifying the Text will...
Definition: GUITextBox.cs:38
void Select(int forcedCaretIndex=-1, bool ignoreSelectSound=false)
Definition: GUITextBox.cs:377
override bool Enabled
Definition: GUITextBox.cs:166
LocalizedString Text
Definition: GUITickBox.cs:109
GUITextBlock TextBlock
Definition: GUITickBox.cs:93
OnSelectedHandler OnSelected
Definition: GUITickBox.cs:13
override bool Selected
Definition: GUITickBox.cs:18
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetLobbyScreen NetLobbyScreen
Definition: GameMain.cs:55
static Lights.LightManager LightManager
Definition: GameMain.cs:78
static ChatMode ActiveChatMode
Definition: GameMain.cs:218
static GameScreen GameScreen
Definition: GameMain.cs:52
static MainMenuScreen MainMenuScreen
Definition: GameMain.cs:53
static bool DebugDraw
Definition: GameMain.cs:29
static GameClient Client
Definition: GameMain.cs:188
static bool WindowActive
Definition: GameMain.cs:174
static ServerListScreen ServerListScreen
Definition: GameMain.cs:66
static LuaCsSetup LuaCs
Definition: GameMain.cs:26
static void ResetNetLobbyScreen()
Definition: GameMain.cs:58
void SetRespawnInfo(string text, Color textColor, bool waitForNextRoundRespawn, bool hideButtons=false)
This method controls the content and visibility logic of the respawn-related GUI elements at the top ...
void EndRound(string endMessage, CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults=null)
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
object Call(string name, params object[] args)
void NetMessageReceived(IReadMessage netMessage, ServerPacketHeader header, Client client=null)
readonly string StringRepresentation
Definition: Md5Hash.cs:32
static ChatMessage Create(string senderName, string text, ChatMessageType type, Entity sender, Client client=null, PlayerConnectionChangeType changeType=PlayerConnectionChangeType.None, Color? textColor=null)
void ClearSelf()
Clears events generated by the current client, used when resynchronizing with the server after a time...
static void ReadPermissions(IReadMessage inc, out ClientPermissions permissions, out List< DebugConsole.Command > permittedCommands)
void SetPermissions(ClientPermissions permissions, IEnumerable< Identifier > permittedConsoleCommands)
readonly byte SessionId
An ID for this client for the current session. THIS IS NOT A PERSISTENT VALUE. DO NOT STORE THIS LONG...
abstract string StringRepresentation
Definition: Endpoint.cs:7
TransferInDelegate OnTransferFailed
void ReadMessage(IReadMessage inc)
IReadOnlyList< FileTransferIn > ActiveTransfers
void StopTransfer(FileTransferIn transfer, bool deleteFile=false)
void VoteForKick(Client votedClient)
Definition: GameClient.cs:2705
void Vote(VoteType voteType, object data)
Definition: GameClient.cs:2684
void Update(float deltaTime)
Definition: GameClient.cs:467
void RequestFile(FileTransferType fileType, string file, string fileHash)
Definition: GameClient.cs:2415
readonly ClientEntityEventManager EntityEventManager
Definition: GameClient.cs:163
void UpdateHUD(float deltaTime)
Definition: GameClient.cs:3114
readonly NamedEvent< PermissionChangedEvent > OnPermissionChanged
Definition: GameClient.cs:188
bool TypingChatMessage(GUITextBox textBox, string text)
Definition: GameClient.cs:3065
override IReadOnlyList< Client > ConnectedClients
Definition: GameClient.cs:133
override void BanPlayer(string kickedName, string reason, TimeSpan? duration=null)
Definition: GameClient.cs:2777
GameClient(string newName, ImmutableArray< Endpoint > endpoints, string serverName, Option< int > ownerKey)
Definition: GameClient.cs:193
void SetName(string value)
Definition: GameClient.cs:37
static readonly TimeSpan LevelTransitionTimeOut
Definition: GameClient.cs:20
IEnumerable< Client > PreviouslyConnectedClients
Definition: GameClient.cs:152
bool ToggleEndRoundVote(GUITickBox tickBox)
Definition: GameClient.cs:2992
static void SetLobbyPublic(bool isPublic)
Definition: GameClient.cs:324
TraitorEventPrefab TraitorMission
Definition: GameClient.cs:122
IEnumerable< CoroutineStatus > EndGame(string endMessage, CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults=null)
Definition: GameClient.cs:1724
readonly NetStats NetStats
Definition: GameClient.cs:58
void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
Definition: GameClient.cs:3190
override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData=null)
Definition: GameClient.cs:2606
void UpdateClientPermissions(Client targetClient)
Definition: GameClient.cs:2809
LocalizedString endRoundVoteText
Definition: GameClient.cs:54
bool HasPermission(ClientPermissions permission)
Definition: GameClient.cs:2620
bool HasConsoleCommandPermission(Identifier commandName)
Definition: GameClient.cs:2625
readonly FileReceiver FileReceiver
Definition: GameClient.cs:156
void RequestSelectSub(SubmarineInfo sub, bool isShuttle)
Tell the server to select a submarine (permission required)
Definition: GameClient.cs:2868
void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData, bool requireControlledCharacter)
Definition: GameClient.cs:2611
CameraTransition EndCinematic
Definition: GameClient.cs:67
void SendTakeOverBotRequest(CharacterInfo bot)
Definition: GameClient.cs:2407
void RequestSelectMode(int modeIndex)
Tell the server to select a mode (permission required)
Definition: GameClient.cs:2883
void SendConsoleCommand(string command)
Definition: GameClient.cs:2832
void SendCharacterInfo(string newName=null)
Definition: GameClient.cs:2653
readonly List< SubmarineInfo > ServerSubmarines
Definition: GameClient.cs:94
override void UnbanPlayer(string playerName)
Definition: GameClient.cs:2789
LocalizedString TraitorFirstObjective
Definition: GameClient.cs:121
void SendChatMessage(string message, ChatMessageType type=ChatMessageType.Default)
Definition: GameClient.cs:2380
bool JoinOnGoingClicked(GUIButton button, object _)
Definition: GameClient.cs:2950
void CreateKickReasonPrompt(string clientName, bool ban)
Definition: GameClient.cs:3425
static readonly TimeSpan CampaignSaveTransferTimeOut
Definition: GameClient.cs:18
void SetupNewCampaign(SubmarineInfo sub, string saveName, string mapSeed, CampaignSettings settings)
Definition: GameClient.cs:2899
void SetupLoadCampaign(string saveName)
Definition: GameClient.cs:2919
bool SelectCrewCharacter(Character character, GUIComponent frame)
Definition: GameClient.cs:3315
void SendRespawnPromptResponse(bool waitForNextRoundRespawn)
Definition: GameClient.cs:2397
bool EnterChatMessage(GUITextBox textBox, string message)
Definition: GameClient.cs:3070
bool SetReadyToStart(GUITickBox tickBox)
Definition: GameClient.cs:2981
bool SelectCrewClient(Client client, GUIComponent frame)
Definition: GameClient.cs:3330
void InitiateSubmarineChange(SubmarineInfo sub, bool transferItems, VoteType voteType)
Definition: GameClient.cs:2712
override void AddChatMessage(ChatMessage message)
Definition: GameClient.cs:2741
void RequestRoundEnd(bool save, bool quitCampaign=false)
Tell the server to end the round (permission required)
Definition: GameClient.cs:2938
void CancelFileTransfer(FileReceiver.FileTransferIn transfer)
Definition: GameClient.cs:2434
void SendChatMessage(ChatMessage msg)
Definition: GameClient.cs:2372
void RequestStartRound(bool continueCampaign=false)
Tell the server to start the round (permission required)
Definition: GameClient.cs:2854
GameClient(string newName, Endpoint endpoint, string serverName, Option< int > ownerKey)
Definition: GameClient.cs:190
override void UnbanPlayer(Endpoint endpoint)
Definition: GameClient.cs:2799
void WriteCharacterInfo(IWriteMessage msg, string newName=null)
Definition: GameClient.cs:2661
override void KickPlayer(string kickedName, string reason)
Definition: GameClient.cs:2766
void UpdateFileTransfer(FileReceiver.FileTransferIn transfer, int expecting, int lastSeen, bool reliable=false)
Definition: GameClient.cs:2439
void ShowMoneyTransferVoteInterface(Client starter, Client from, int amount, Client to, float timeOut)
Definition: GameClient.cs:2728
void ShowSubmarineChangeVoteInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float timeOut)
Definition: GameClient.cs:2718
void ReportError(ClientNetError error, UInt16 expectedId=0, UInt16 eventId=0, UInt16 entityId=0)
Definition: GameClient.cs:3503
ServerInfo CreateServerInfoFromSettings()
Definition: GameClient.cs:290
void Update(float deltaTime)
Definition: NetStats.cs:44
void Draw(SpriteBatch spriteBatch, Rectangle rect)
Definition: NetStats.cs:59
DateTime RespawnTime
When will the shuttle be dispatched with respawned characters
DateTime ReturnTime
When will the sub start heading back out of the level
static ServerInfo FromServerEndpoints(ImmutableArray< Endpoint > endpoints, ServerSettings serverSettings)
Definition: ServerInfo.cs:104
bool ServerDetailsChanged
Have some of the properties listed in the server list changed
void Read(IReadMessage msg)
Definition: VoipClient.cs:88
Vector2 RelativeSize
Relative to the parent rect.
void UpdateOrAddServerInfo(ServerInfo serverInfo)
static IEnumerable< SubmarineInfo > SavedSubmarines
void ResetVotes(IEnumerable< Client > connectedClients)
bool ClientWrite(IWriteMessage msg, VoteType voteType, object data)
Returns true if the given data is valid for the given vote type, returns false otherwise....
void Update(float deltaTime)
static VotingInterface CreateMoneyTransferVotingInterface(Client starter, Client from, Client to, int amount, float votingTime)
static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float votingTime)
Interface for entities that the clients can send events to the server
int ReadRangedInteger(int min, int max)
Single ReadRangedSingle(Single min, Single max, int bitCount)
byte[] ReadBytes(int numberOfBytes)
Interface for entities that handle ServerNetObject.ENTITY_POSITION
void ClientReadPosition(IReadMessage msg, float sendingTime)
Interface for entities that the server can send events to the clients
void WriteIdentifier(Identifier val)
NumberType
Definition: Enums.cs:715
static ChatKeyStates GetChatKeyStates()
Definition: ChatBox.cs:822
static void Read(IReadMessage msg, SegmentDataReader segmentDataReader, ExceptionHandler? exceptionHandler=null)
static SegmentTableWriter< T > StartWriting(IWriteMessage msg)
Definition: SegmentTable.cs:94