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