Server LuaCsForBarotrauma
GameServer.cs
2 using Barotrauma.IO;
4 using Barotrauma.Steam;
5 using Lidgren.Network;
6 using Microsoft.Xna.Framework;
7 using System;
8 using System.Collections.Generic;
9 using System.Collections.Immutable;
10 using System.Diagnostics;
11 using System.Linq;
12 using System.Threading;
13 using System.Xml.Linq;
14 using MoonSharp.Interpreter;
15 using System.Net;
17 
18 namespace Barotrauma.Networking
19 {
20  sealed class GameServer : NetworkMember
21  {
22  public override bool IsServer => true;
23  public override bool IsClient => false;
24 
25  public override Voting Voting { get; }
26 
27  public string ServerName
28  {
29  get { return ServerSettings.ServerName; }
30  set
31  {
32  if (string.IsNullOrEmpty(value)) { return; }
33  ServerSettings.ServerName = value;
34  }
35  }
36 
37  public bool SubmarineSwitchLoad = false;
38 
39  private readonly List<Client> connectedClients = new List<Client>();
40 
44  private readonly List<Client> clientsAttemptingToReconnectSoon = new List<Client>();
45 
46  //keeps track of players who've previously been playing on the server
47  //so kick votes persist during the session and the server can let the clients know what name this client used previously
48  private readonly List<PreviousPlayer> previousPlayers = new List<PreviousPlayer>();
49 
50  private int roundStartSeed;
51 
52  //is the server running
53  private bool started;
54 
55  private ServerPeer serverPeer;
56  public ServerPeer ServerPeer { get { return serverPeer; } }
57 
58  private DateTime refreshMasterTimer;
59  private readonly TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 60);
60  private bool registeredToSteamMaster;
61 
62  private DateTime roundStartTime;
63 
64  private bool wasReadyToStartAutomatically;
65  private bool autoRestartTimerRunning;
66  public float EndRoundTimer { get; private set; }
67  public float EndRoundDelay { get; private set; }
68 
70 
71  private const int PvpAutoBalanceCountdown = 10;
72  private static float pvpAutoBalanceCountdownRemaining = -1;
73  private int Team1Count => GetPlayingClients().Count(static c => c.TeamID == CharacterTeamType.Team1);
74  private int Team2Count => GetPlayingClients().Count(static c => c.TeamID == CharacterTeamType.Team2);
75 
79  private static readonly Queue<ChatMessage> pendingMessagesToOwner = new Queue<ChatMessage>();
80 
82  {
83  get;
84  private set;
85  }
86 
87  private bool initiatedStartGame;
88  private CoroutineHandle startGameCoroutine;
89 
90  private readonly ServerEntityEventManager entityEventManager;
91 
92  public FileSender FileSender { get; private set; }
93 
94  public ModSender ModSender { get; private set; }
95 
96  private TraitorManager traitorManager;
98  {
99  get
100  {
101  traitorManager ??= new TraitorManager(this);
102  return traitorManager;
103  }
104  }
105 
106 #if DEBUG
107  public void PrintSenderTransters()
108  {
109  foreach (var transfer in FileSender.ActiveTransfers)
110  {
111  DebugConsole.NewMessage(transfer.FileName + " " + transfer.Progress.ToString());
112  }
113  }
114 #endif
115 
116  public override IReadOnlyList<Client> ConnectedClients
117  {
118  get
119  {
120  return connectedClients;
121  }
122  }
123 
124 
126  {
127  get { return entityEventManager; }
128  }
129 
130  public int Port => ServerSettings?.Port ?? 0;
131 
132  //only used when connected to steam
133  public int QueryPort => ServerSettings?.QueryPort ?? 0;
134 
135  public NetworkConnection OwnerConnection { get; private set; }
136  private readonly Option<int> ownerKey;
137  private readonly Option<P2PEndpoint> ownerEndpoint;
138 
140  {
141  lock (clientsAttemptingToReconnectSoon)
142  {
143  clientsAttemptingToReconnectSoon.Clear();
144  }
145  }
146 
148  {
149  lock (clientsAttemptingToReconnectSoon)
150  {
151  Client found = null;
152  foreach (var client in clientsAttemptingToReconnectSoon)
153  {
154  if (conn.AddressMatches(client.Connection))
155  {
156  found = client;
157  break;
158  }
159  }
160 
161  if (found is not null)
162  {
163  clientsAttemptingToReconnectSoon.Remove(found);
164  return true;
165  }
166  }
167 
168  return false;
169  }
170 
171  public GameServer(
172  string name,
173  IPAddress listenIp,
174  int port,
175  int queryPort,
176  bool isPublic,
177  string password,
178  bool attemptUPnP,
179  int maxPlayers,
180  Option<int> ownerKey,
181  Option<P2PEndpoint> ownerEndpoint)
182  {
183  if (name.Length > NetConfig.ServerNameMaxLength)
184  {
185  name = name.Substring(0, NetConfig.ServerNameMaxLength);
186  }
187 
188  LastClientListUpdateID = 0;
189 
190  ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP, listenIp);
192  ServerSettings.SetPassword(password);
194 
195  Voting = new Voting();
196 
197  this.ownerKey = ownerKey;
198 
199  this.ownerEndpoint = ownerEndpoint;
200 
201  entityEventManager = new ServerEntityEventManager(this);
202  }
203 
204  public void StartServer(bool registerToServerList)
205  {
206  Log("Starting the server...", ServerLog.MessageType.ServerMessage);
207 
208  var callbacks = new ServerPeer.Callbacks(
209  ReadDataMessage,
210  OnClientDisconnect,
211  OnInitializationComplete,
213  OnOwnerDetermined);
214 
215  if (ownerEndpoint.TryUnwrap(out var endpoint))
216  {
217  Log("Using P2P networking.", ServerLog.MessageType.ServerMessage);
218  serverPeer = new P2PServerPeer(endpoint, ownerKey.Fallback(0), ServerSettings, callbacks);
219  }
220  else
221  {
222  Log("Using Lidgren networking. Manual port forwarding may be required. If players cannot connect to the server, you may want to use the in-game hosting menu (which uses Steamworks and EOS networking and does not require port forwarding).", ServerLog.MessageType.ServerMessage);
223  serverPeer = new LidgrenServerPeer(ownerKey, ServerSettings, callbacks);
224  if (registerToServerList)
225  {
226  registeredToSteamMaster = SteamManager.CreateServer(this, ServerSettings.IsPublic);
227  Eos.EosSessionManager.UpdateOwnedSession(Option.None, ServerSettings);
228  }
229  }
230 
231  FileSender = new FileSender(serverPeer, MsgConstants.MTU);
232  FileSender.OnEnded += FileTransferChanged;
233  FileSender.OnStarted += FileTransferChanged;
234 
236 
237  serverPeer.Start();
238 
239  VoipServer = new VoipServer(serverPeer);
240 
242  Log("Server started", ServerLog.MessageType.ServerMessage);
243 
246  if (!string.IsNullOrEmpty(ServerSettings.SelectedSubmarine))
247  {
248  SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == ServerSettings.SelectedSubmarine);
249  if (sub != null) { GameMain.NetLobbyScreen.SelectedSub = sub; }
250  }
251  if (!string.IsNullOrEmpty(ServerSettings.SelectedShuttle))
252  {
253  SubmarineInfo shuttle = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == ServerSettings.SelectedShuttle);
254  if (shuttle != null) { GameMain.NetLobbyScreen.SelectedShuttle = shuttle; }
255  }
256 
257  started = true;
258 
259  GameAnalyticsManager.AddDesignEvent("GameServer:Start");
260  }
261 
262 
266  public static void AddPendingMessageToOwner(string message, ChatMessageType messageType)
267  {
268  pendingMessagesToOwner.Enqueue(ChatMessage.Create(string.Empty, message, messageType, sender: null));
269  }
270 
271  private void OnOwnerDetermined(NetworkConnection connection)
272  {
273  OwnerConnection = connection;
274 
275  var ownerClient = ConnectedClients.Find(c => c.Connection == connection);
276  if (ownerClient == null)
277  {
278  DebugConsole.ThrowError("Owner client not found! Can't set permissions");
279  return;
280  }
281  ownerClient.SetPermissions(ClientPermissions.All, DebugConsole.Commands);
282  UpdateClientPermissions(ownerClient);
283  }
284 
285  public void NotifyCrash()
286  {
287  var tempList = ConnectedClients.Where(c => c.Connection != OwnerConnection).ToList();
288  foreach (var c in tempList)
289  {
290  DisconnectClient(c.Connection, PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
291  }
292  if (OwnerConnection != null)
293  {
294  var conn = OwnerConnection; OwnerConnection = null;
295  DisconnectClient(conn, PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
296  }
297  Thread.Sleep(500);
298  }
299 
300  private void OnInitializationComplete(NetworkConnection connection, string clientName)
301  {
302  clientName = Client.SanitizeName(clientName);
303  Client newClient = new Client(clientName, GetNewClientSessionId());
304  newClient.InitClientSync();
305  newClient.Connection = connection;
306  newClient.Connection.Status = NetworkConnectionStatus.Connected;
307  newClient.AccountInfo = connection.AccountInfo;
308  newClient.Language = connection.Language;
309  connectedClients.Add(newClient);
310 
311  var previousPlayer = previousPlayers.Find(p => p.MatchesClient(newClient));
312  if (previousPlayer != null)
313  {
314  newClient.Karma = previousPlayer.Karma;
315  newClient.KarmaKickCount = previousPlayer.KarmaKickCount;
316  foreach (Client c in previousPlayer.KickVoters)
317  {
318  if (!connectedClients.Contains(c)) { continue; }
319  newClient.AddKickVote(c);
320  }
321  }
322 
323  LastClientListUpdateID++;
324 
325  if (newClient.Connection == OwnerConnection && OwnerConnection != null)
326  {
327  newClient.GivePermission(ClientPermissions.All);
328  foreach (var command in DebugConsole.Commands)
329  {
330  newClient.PermittedConsoleCommands.Add(command);
331  }
332  SendConsoleMessage("Granted all permissions to " + newClient.Name + ".", newClient);
333  }
334 
335  GameMain.LuaCs.Hook.Call("client.connected", newClient);
336 
337  SendChatMessage($"ServerMessage.JoinedServer~[client]={ClientLogName(newClient)}", ChatMessageType.Server, changeType: PlayerConnectionChangeType.Joined);
339 
340  if (previousPlayer != null && previousPlayer.Name != newClient.Name)
341  {
342  string prevNameSanitized = previousPlayer.Name.Replace("‖", "");
343  SendChatMessage($"ServerMessage.PreviousClientName~[client]={ClientLogName(newClient)}~[previousname]={prevNameSanitized}", ChatMessageType.Server);
344  previousPlayer.Name = newClient.Name;
345  }
346  if (!ServerSettings.ServerMessageText.IsNullOrEmpty())
347  {
348  SendDirectChatMessage((TextManager.Get("servermotd") + '\n' + ServerSettings.ServerMessageText).Value, newClient, ChatMessageType.Server);
349  }
350 
351  var savedPermissions = ServerSettings.ClientPermissions.Find(scp =>
352  scp.AddressOrAccountId.TryGet(out AccountId accountId)
353  ? newClient.AccountId.ValueEquals(accountId)
354  : newClient.Connection.Endpoint.Address == scp.AddressOrAccountId);
355 
356  if (savedPermissions != null)
357  {
358  newClient.SetPermissions(savedPermissions.Permissions, savedPermissions.PermittedCommands);
359  }
360  else
361  {
362  var defaultPerms = PermissionPreset.List.Find(p => p.Identifier == "None");
363  if (defaultPerms != null)
364  {
365  newClient.SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands);
366  }
367  else
368  {
369  newClient.SetPermissions(ClientPermissions.None, Enumerable.Empty<DebugConsole.Command>());
370  }
371  }
372 
373  UpdateClientPermissions(newClient);
374  //notify the client of everyone else's permissions
375  foreach (Client otherClient in connectedClients)
376  {
377  if (otherClient == newClient) { continue; }
378  CoroutineManager.StartCoroutine(SendClientPermissionsAfterClientListSynced(newClient, otherClient));
379  }
380  }
381 
382  private void OnClientDisconnect(NetworkConnection connection, PeerDisconnectPacket peerDisconnectPacket)
383  {
384  Client connectedClient = connectedClients.Find(c => c.Connection == connection);
385 
386  DisconnectClient(connectedClient, peerDisconnectPacket);
387  }
388 
389  public void Update(float deltaTime)
390  {
391  dosProtection.Update(deltaTime);
392 
393  if (!started) { return; }
394 
395  if (ChildServerRelay.HasShutDown)
396  {
398  return;
399  }
400 
401  FileSender.Update(deltaTime);
403 
404  UpdatePing();
405 
407  {
408  VoipServer.SendToClients(connectedClients);
409  foreach (var c in connectedClients)
410  {
411  c.VoipServerDecoder.DebugUpdate(deltaTime);
412  }
413  }
414 
415  if (GameStarted)
416  {
417  RespawnManager?.Update(deltaTime);
418 
419  entityEventManager.Update(connectedClients);
420  bool permadeathMode = ServerSettings.RespawnMode == RespawnMode.Permadeath;
421 
422  //go through the characters backwards to give rejoining clients control of the latest created character
423  for (int i = Character.CharacterList.Count - 1; i >= 0; i--)
424  {
425  Character character = Character.CharacterList[i];
426  if (!character.ClientDisconnected) { continue; }
427 
428  Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c));
429  bool canOwnerTakeControl =
430  owner != null && owner.InGame && !owner.NeedsMidRoundSync &&
432  (permadeathMode && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)));
433  if (!character.IsDead)
434  {
436  {
437  character.KillDisconnectedTimer += deltaTime;
438  character.SetStun(1.0f);
439  }
440 
441  if ((OwnerConnection == null || owner?.Connection != OwnerConnection) &&
443  {
444  character.Kill(CauseOfDeathType.Disconnected, null);
445  continue;
446  }
447  if (canOwnerTakeControl)
448  {
449  SetClientCharacter(owner, character);
450  }
451  }
452  else if (canOwnerTakeControl &&
453  character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected &&
455  {
456  //create network event immediately to ensure the character is revived client-side
457  //before the client gains control of it (normally status events are created periodically)
458  character.Revive(removeAfflictions: false, createNetworkEvent: true);
459  SetClientCharacter(owner, character);
460  }
461  }
462 
463  TraitorManager?.Update(deltaTime);
464 
465  Voting.Update(deltaTime);
466 
467  bool isCrewDown =
468  connectedClients.All(c => !c.UsingFreeCam && (c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated));
469  bool isSomeoneIncapacitatedNotDead =
470  connectedClients.Any(c => !c.UsingFreeCam && c.Character is { IsDead: false, IsIncapacitated: true });
471 
472  bool subAtLevelEnd = false;
473  if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode)
474  {
475  if (Level.Loaded?.EndOutpost != null)
476  {
477  int charactersInsideOutpost = connectedClients.Count(c =>
478  c.Character != null &&
479  !c.Character.IsDead && !c.Character.IsUnconscious &&
480  c.Character.Submarine == Level.Loaded.EndOutpost);
481  int charactersOutsideOutpost = connectedClients.Count(c =>
482  c.Character != null &&
483  !c.Character.IsDead && !c.Character.IsUnconscious &&
484  c.Character.Submarine != Level.Loaded.EndOutpost);
485 
486  //level finished if the sub is docked to the outpost
487  //or very close and someone from the crew made it inside the outpost
488  subAtLevelEnd =
490  (Submarine.MainSub.AtEndExit && charactersInsideOutpost > 0) ||
491  (charactersInsideOutpost > charactersOutsideOutpost);
492  }
493  else
494  {
495  subAtLevelEnd = Submarine.MainSub.AtEndExit;
496  }
497  }
498 
499  EndRoundDelay = 1.0f;
500  if (permadeathMode && isCrewDown)
501  {
502  if (EndRoundTimer <= 0.0f)
503  {
505  }
506  EndRoundDelay = 120.0f;
507  EndRoundTimer += deltaTime;
508  }
509  else if (ServerSettings.AutoRestart && isCrewDown)
510  {
511  EndRoundDelay = isSomeoneIncapacitatedNotDead ? 120.0f : 5.0f;
512  EndRoundTimer += deltaTime;
513  }
514  else if (subAtLevelEnd && GameMain.GameSession?.GameMode is not CampaignMode)
515  {
516  EndRoundDelay = 5.0f;
517  EndRoundTimer += deltaTime;
518  }
519  else if (isCrewDown &&
521  {
522 #if !DEBUG
523  if (EndRoundTimer <= 0.0f)
524  {
525  SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "120").Value, ChatMessageType.Server);
526  }
527  EndRoundDelay = 120.0f;
528  EndRoundTimer += deltaTime;
529 #endif
530  }
531  else if (isCrewDown && (GameMain.GameSession?.GameMode is CampaignMode))
532  {
533 #if !DEBUG
534  EndRoundDelay = isSomeoneIncapacitatedNotDead ? 120.0f : 2.0f;
535  EndRoundTimer += deltaTime;
536 #endif
537  }
538  else
539  {
540  EndRoundTimer = 0.0f;
541  }
542 
544  {
545  if (permadeathMode && isCrewDown)
546  {
547  Log("Ending round (entire crew dead or down and did not acquire new characters in time)", ServerLog.MessageType.ServerMessage);
548  }
549  else if (ServerSettings.AutoRestart && isCrewDown)
550  {
551  Log("Ending round (entire crew down)", ServerLog.MessageType.ServerMessage);
552  }
553  else if (subAtLevelEnd)
554  {
555  Log("Ending round (submarine reached the end of the level)", ServerLog.MessageType.ServerMessage);
556  }
557  else if (RespawnManager == null)
558  {
559  Log("Ending round (no players left standing and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage);
560  }
561  else
562  {
563  Log("Ending round (no players left standing)", ServerLog.MessageType.ServerMessage);
564  }
565  EndGame(wasSaved: false);
566  return;
567  }
568  }
569  else if (initiatedStartGame)
570  {
571  //tried to start up the game and StartGame coroutine is not running anymore
572  // -> something wen't wrong during startup, re-enable start button and reset AutoRestartTimer
573  if (startGameCoroutine != null && !CoroutineManager.IsCoroutineRunning(startGameCoroutine))
574  {
576 
577  if (startGameCoroutine.Exception != null && OwnerConnection != null)
578  {
580  startGameCoroutine.Exception.Message + '\n' +
581  (startGameCoroutine.Exception.StackTrace?.CleanupStackTrace() ?? "null"),
582  connectedClients.Find(c => c.Connection == OwnerConnection),
583  Color.Red);
584  }
585 
586  EndGame();
588 
589  startGameCoroutine = null;
590  initiatedStartGame = false;
591  }
592  }
593  else if (Screen.Selected == GameMain.NetLobbyScreen && !GameStarted && !initiatedStartGame)
594  {
596  {
597  //autorestart if there are any non-spectators on the server (ignoring the server owner)
598  bool shouldAutoRestart = connectedClients.Any(c =>
599  c.Connection != OwnerConnection &&
600  (!c.SpectateOnly || !ServerSettings.AllowSpectating));
601 
602  if (shouldAutoRestart != autoRestartTimerRunning)
603  {
604  autoRestartTimerRunning = shouldAutoRestart;
606  }
607 
608  if (autoRestartTimerRunning)
609  {
610  ServerSettings.AutoRestartTimer -= deltaTime;
611  }
612  }
613 
614  bool readyToStartAutomatically = false;
615  if (ServerSettings.AutoRestart && autoRestartTimerRunning && ServerSettings.AutoRestartTimer < 0.0f)
616  {
617  readyToStartAutomatically = true;
618  }
620  {
621  int clientsReady = connectedClients.Count(c => c.GetVote<bool>(VoteType.StartRound));
622  if (clientsReady / (float)connectedClients.Count >= ServerSettings.StartWhenClientsReadyRatio)
623  {
624  readyToStartAutomatically = true;
625  }
626  }
627  if (readyToStartAutomatically)
628  {
629  if (!wasReadyToStartAutomatically) { GameMain.NetLobbyScreen.LastUpdateID++; }
630  TryStartGame();
631  }
632  wasReadyToStartAutomatically = readyToStartAutomatically;
633  }
634 
635  lock (clientsAttemptingToReconnectSoon)
636  {
637  foreach (var client in clientsAttemptingToReconnectSoon)
638  {
639  client.DeleteDisconnectedTimer -= deltaTime;
640  }
641 
642  clientsAttemptingToReconnectSoon.RemoveAll(static c => c.DeleteDisconnectedTimer < 0f);
643  }
644 
645  foreach (Client c in connectedClients)
646  {
647  //slowly reset spam timers
648  c.ChatSpamTimer = Math.Max(0.0f, c.ChatSpamTimer - deltaTime);
649  c.ChatSpamSpeed = Math.Max(0.0f, c.ChatSpamSpeed - deltaTime);
650 
651  //constantly increase AFK timer if the client is controlling a character (gets reset to zero every time an input is received)
652  if (GameStarted && c.Character != null && !c.Character.IsDead && !c.Character.IsIncapacitated)
653  {
654  if (c.Connection != OwnerConnection && c.Permissions != ClientPermissions.All) { c.KickAFKTimer += deltaTime; }
655  }
656  }
657 
658  if (pvpAutoBalanceCountdownRemaining > 0)
659  {
660  if (GameStarted || initiatedStartGame || Screen.Selected != GameMain.NetLobbyScreen ||
662  {
663  StopAutoBalanceCountdown();
664  }
665  else
666  {
667  float prevTimeRemaining = pvpAutoBalanceCountdownRemaining;
668  pvpAutoBalanceCountdownRemaining -= deltaTime;
669  if (pvpAutoBalanceCountdownRemaining <= 0)
670  {
671  pvpAutoBalanceCountdownRemaining = -1;
672  RefreshPvpTeamAssignments(autoBalanceNow: true);
673  }
674  else
675  {
676  // Send a chat message about the countdown every 5 seconds the countdown is running, but not when
677  // it (=its integer part, which gets printed out) is still at the starting value, or zero
678  int currentTimeRemainingInteger = (int)Math.Ceiling(pvpAutoBalanceCountdownRemaining);
679  if (Math.Ceiling(prevTimeRemaining) > currentTimeRemainingInteger && currentTimeRemainingInteger % 5 == 0)
680  {
682  TextManager.GetWithVariable("AutoBalance.CountdownRemaining", "[number]", currentTimeRemainingInteger.ToString()).Value,
683  ChatMessageType.Server);
684  }
685  }
686  }
687  }
688 
689  if (connectedClients.Any(c => c.KickAFKTimer >= ServerSettings.KickAFKTime))
690  {
691  IEnumerable<Client> kickAFK = connectedClients.FindAll(c =>
693  (OwnerConnection == null || c.Connection != OwnerConnection));
694  foreach (Client c in kickAFK)
695  {
696  KickClient(c, "DisconnectMessage.AFK");
697  }
698  }
699 
700  serverPeer.Update(deltaTime);
701 
702  //don't run the rest of the method if something in serverPeer.Update causes the server to shutdown
703  if (!started) { return; }
704 
705  // if update interval has passed
706  if (updateTimer < DateTime.Now)
707  {
708  if (ConnectedClients.Count > 0)
709  {
710  foreach (Client c in ConnectedClients)
711  {
712  try
713  {
714  ClientWrite(c);
715  }
716  catch (Exception e)
717  {
718  DebugConsole.ThrowError("Failed to write a network message for the client \"" + c.Name + "\"!", e);
719 
720  string errorMsg = "Failed to write a network message for a client! (MidRoundSyncing: " + c.NeedsMidRoundSync + ")\n"
721  + e.Message + "\n" + e.StackTrace.CleanupStackTrace();
722  if (e.InnerException != null)
723  {
724  errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
725  }
726 
727  GameAnalyticsManager.AddErrorEventOnce(
728  "GameServer.Update:ClientWriteFailed" + e.StackTrace.CleanupStackTrace(),
729  GameAnalyticsManager.ErrorSeverity.Error,
730  errorMsg);
731  }
732  }
733 
734  foreach (Character character in Character.CharacterList)
735  {
736  if (character.healthUpdateTimer <= 0.0f)
737  {
738  if (!character.HealthUpdatePending)
739  {
740  character.healthUpdateTimer = character.HealthUpdateInterval;
741  }
742  character.HealthUpdatePending = true;
743  }
744  else
745  {
746  character.healthUpdateTimer -= (float)UpdateInterval.TotalSeconds;
747  }
748  character.HealthUpdateInterval += (float)UpdateInterval.TotalSeconds;
749  }
750  }
751 
752  updateTimer = DateTime.Now + UpdateInterval;
753  }
754 
755  if (DateTime.Now > refreshMasterTimer || ServerSettings.ServerDetailsChanged)
756  {
757  if (registeredToSteamMaster)
758  {
759  bool refreshSuccessful = SteamManager.RefreshServerDetails(this);
760  if (GameSettings.CurrentConfig.VerboseLogging)
761  {
762  Log(refreshSuccessful ?
763  "Refreshed server info on the Steam server list." :
764  "Refreshing server info on the Steam server list failed.", ServerLog.MessageType.ServerMessage);
765  }
766  }
767 
768  Eos.EosSessionManager.UpdateOwnedSession(Option.None, ServerSettings);
769 
771  refreshMasterTimer = DateTime.Now + refreshMasterInterval;
772  }
773  }
774 
775 
776  private double lastPingTime;
777  private byte[] lastPingData;
778  private void UpdatePing()
779  {
780  if (Timing.TotalTime > lastPingTime + 1.0)
781  {
782  lastPingData ??= new byte[64];
783  for (int i = 0; i < lastPingData.Length; i++)
784  {
785  lastPingData[i] = (byte)Rand.Range(33, 126);
786  }
787  lastPingTime = Timing.TotalTime;
788 
789  ConnectedClients.ForEach(c =>
790  {
791  IWriteMessage pingReq = new WriteOnlyMessage();
792  pingReq.WriteByte((byte)ServerPacketHeader.PING_REQUEST);
793  pingReq.WriteByte((byte)lastPingData.Length);
794  pingReq.WriteBytes(lastPingData, 0, lastPingData.Length);
795  serverPeer.Send(pingReq, c.Connection, DeliveryMethod.Unreliable);
796 
797  IWriteMessage pingInf = new WriteOnlyMessage();
798  pingInf.WriteByte((byte)ServerPacketHeader.CLIENT_PINGS);
799  pingInf.WriteByte((byte)ConnectedClients.Count);
800  ConnectedClients.ForEach(c2 =>
801  {
802  pingInf.WriteByte(c2.SessionId);
803  pingInf.WriteUInt16(c2.Ping);
804  });
805  serverPeer.Send(pingInf, c.Connection, DeliveryMethod.Unreliable);
806  });
807  }
808  }
809 
810  private readonly DoSProtection dosProtection = new();
811 
812  private void ReadDataMessage(NetworkConnection sender, IReadMessage inc)
813  {
814  var connectedClient = connectedClients.Find(c => c.Connection == sender);
815 
816  using var _ = dosProtection.Start(connectedClient);
817 
819 
820  GameMain.LuaCs.Networking.NetMessageReceived(inc, header, connectedClient);
821 
822  switch (header)
823  {
824  case ClientPacketHeader.PING_RESPONSE:
825  byte responseLen = inc.ReadByte();
826  if (responseLen != lastPingData.Length) { return; }
827  for (int i = 0; i < responseLen; i++)
828  {
829  byte b = inc.ReadByte();
830  if (b != lastPingData[i]) { return; }
831  }
832  connectedClient.Ping = (UInt16)((Timing.TotalTime - lastPingTime) * 1000);
833  break;
834  case ClientPacketHeader.RESPONSE_STARTGAME:
835  if (connectedClient != null)
836  {
837  connectedClient.ReadyToStart = inc.ReadBoolean();
838  UpdateCharacterInfo(inc, connectedClient);
839 
840  //game already started -> send start message immediately
841  if (GameStarted)
842  {
843  SendStartMessage(roundStartSeed, GameMain.GameSession.Level.Seed, GameMain.GameSession, connectedClient, true);
844  }
845  }
846  break;
847  case ClientPacketHeader.RESPONSE_CANCEL_STARTGAME:
848  if (isRoundStartWarningActive)
849  {
850  foreach (Client c in connectedClients)
851  {
852  IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.CANCEL_STARTGAME);
853  serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable);
854  }
855  }
856 
857  AbortStartGameIfWarningActive();
858  break;
859  case ClientPacketHeader.REQUEST_STARTGAMEFINALIZE:
860  if (connectedClient == null)
861  {
862  DebugConsole.AddWarning("Received a REQUEST_STARTGAMEFINALIZE message. Client not connected, ignoring the message.");
863  }
864  else if (!GameStarted)
865  {
866  DebugConsole.AddWarning("Received a REQUEST_STARTGAMEFINALIZE message. Game not started, ignoring the message.");
867  }
868  else
869  {
870  SendRoundStartFinalize(connectedClient);
871  }
872  break;
873  case ClientPacketHeader.UPDATE_LOBBY:
874  ClientReadLobby(inc);
875  break;
876  case ClientPacketHeader.UPDATE_INGAME:
877  if (!GameStarted) { return; }
878  ClientReadIngame(inc);
879  break;
880  case ClientPacketHeader.CAMPAIGN_SETUP_INFO:
881  bool isNew = inc.ReadBoolean(); inc.ReadPadBits();
882  if (isNew)
883  {
884  string saveName = inc.ReadString();
885  string seed = inc.ReadString();
886  string subName = inc.ReadString();
887  string subHash = inc.ReadString();
888  CampaignSettings settings = INetSerializableStruct.Read<CampaignSettings>(inc);
889 
890  var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash);
891 
892  if (GameStarted)
893  {
894  SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox);
895  return;
896  }
897 
898  if (matchingSub == null)
899  {
901  TextManager.GetWithVariable("CampaignStartFailedSubNotFound", "[subname]", subName).Value,
902  connectedClient, ChatMessageType.MessageBox);
903  }
904  else
905  {
906  string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName);
907  if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
908  {
909  using (dosProtection.Pause(connectedClient))
910  {
911  ServerSettings.CampaignSettings = settings;
913  MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings);
914  }
915  }
916  }
917  }
918  else
919  {
920  string savePath = inc.ReadString();
921  bool isBackup = inc.ReadBoolean();
922  inc.ReadPadBits();
923  uint backupIndex = isBackup ? inc.ReadUInt32() : uint.MinValue;
924 
925  if (GameStarted)
926  {
927  SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox);
928  break;
929  }
930  if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
931  {
932  using (dosProtection.Pause(connectedClient))
933  {
934  CampaignDataPath dataPath;
935  if (isBackup)
936  {
937  string backupPath = SaveUtil.GetBackupPath(savePath, backupIndex);
938  dataPath = new CampaignDataPath(loadPath: backupPath, savePath: savePath);
939  }
940  else
941  {
942  dataPath = CampaignDataPath.CreateRegular(savePath);
943  }
944 
945  MultiPlayerCampaign.LoadCampaign(dataPath, connectedClient);
946  }
947  }
948  }
949  break;
950  case ClientPacketHeader.VOICE:
951  if (ServerSettings.VoiceChatEnabled && !connectedClient.Muted)
952  {
953  byte id = inc.ReadByte();
954  if (connectedClient.SessionId != id)
955  {
956 #if DEBUG
957  DebugConsole.ThrowError(
958  "Client \"" + connectedClient.Name + "\" sent a VOIP update that didn't match its ID (" + id.ToString() + "!=" + connectedClient.SessionId.ToString() + ")");
959 #endif
960  return;
961  }
962  VoipServer.Read(inc, connectedClient);
963  }
964  break;
965  case ClientPacketHeader.SERVER_SETTINGS:
966  ServerSettings.ServerRead(inc, connectedClient);
967  break;
968  case ClientPacketHeader.SERVER_SETTINGS_PERKS:
969  ServerSettings.ReadPerks(inc, connectedClient);
970  break;
971  case ClientPacketHeader.SERVER_COMMAND:
972  ClientReadServerCommand(inc);
973  break;
974  case ClientPacketHeader.CREW:
975  ReadCrewMessage(inc, connectedClient);
976  break;
977  case ClientPacketHeader.TRANSFER_MONEY:
978  ReadMoneyMessage(inc, connectedClient);
979  break;
980  case ClientPacketHeader.REWARD_DISTRIBUTION:
981  ReadRewardDistributionMessage(inc, connectedClient);
982  break;
983  case ClientPacketHeader.RESET_REWARD_DISTRIBUTION:
984  ResetRewardDistribution(connectedClient);
985  break;
986  case ClientPacketHeader.MEDICAL:
987  ReadMedicalMessage(inc, connectedClient);
988  break;
989  case ClientPacketHeader.CIRCUITBOX:
990  ReadCircuitBoxMessage(inc, connectedClient);
991  break;
992  case ClientPacketHeader.READY_CHECK:
993  ReadyCheck.ServerRead(inc, connectedClient);
994  break;
995  case ClientPacketHeader.READY_TO_SPAWN:
996  ReadReadyToSpawnMessage(inc, connectedClient);
997  break;
998  case ClientPacketHeader.TAKEOVERBOT:
999  ReadTakeOverBotMessage(inc, connectedClient);
1000  break;
1001  case ClientPacketHeader.FILE_REQUEST:
1003  {
1004  FileSender.ReadFileRequest(inc, connectedClient);
1005  }
1006  break;
1007  case ClientPacketHeader.EVENTMANAGER_RESPONSE:
1008  GameMain.GameSession?.EventManager.ServerRead(inc, connectedClient);
1009  break;
1010  case ClientPacketHeader.UPDATE_CHARACTERINFO:
1011  UpdateCharacterInfo(inc, connectedClient);
1012  break;
1013  case ClientPacketHeader.REQUEST_BACKUP_INDICES:
1014  SendBackupIndices(inc, connectedClient);
1015  break;
1016  case ClientPacketHeader.ERROR:
1017  HandleClientError(inc, connectedClient);
1018  break;
1019  }
1020  }
1021 
1022  private void SendBackupIndices(IReadMessage inc, Client connectedClient)
1023  {
1024  string savePath = inc.ReadString();
1025 
1026  var indexData = SaveUtil.GetIndexData(savePath);
1027 
1028  IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.SEND_BACKUP_INDICES);
1029  msg.WriteString(savePath);
1030  msg.WriteNetSerializableStruct(indexData.ToNetCollection());
1031  serverPeer?.Send(msg, connectedClient.Connection, DeliveryMethod.Reliable);
1032  }
1033 
1034  private void HandleClientError(IReadMessage inc, Client c)
1035  {
1036  string errorStr = "Unhandled error report";
1037  string errorStrNoName = errorStr;
1038 
1039  ClientNetError error = (ClientNetError)inc.ReadByte();
1040  switch (error)
1041  {
1042  case ClientNetError.MISSING_EVENT:
1043  UInt16 expectedID = inc.ReadUInt16();
1044  UInt16 receivedID = inc.ReadUInt16();
1045  errorStr = errorStrNoName = "Expecting event id " + expectedID.ToString() + ", received " + receivedID.ToString();
1046  break;
1047  case ClientNetError.MISSING_ENTITY:
1048  UInt16 eventID = inc.ReadUInt16();
1049  UInt16 entityID = inc.ReadUInt16();
1050  byte subCount = inc.ReadByte();
1051  List<string> subNames = new List<string>();
1052  for (int i = 0; i < subCount; i++)
1053  {
1054  subNames.Add(inc.ReadString());
1055  }
1056  Entity entity = Entity.FindEntityByID(entityID);
1057  if (entity == null)
1058  {
1059  errorStr = errorStrNoName = "Received an update for an entity that doesn't exist (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ").";
1060  }
1061  else if (entity is Character character)
1062  {
1063  errorStr = $"Missing character {character.Name} (event id {eventID}, entity id {entityID}).";
1064  errorStrNoName = $"Missing character {character.SpeciesName} (event id {eventID}, entity id {entityID}).";
1065  }
1066  else if (entity is Item item)
1067  {
1068  errorStr = errorStrNoName = $"Missing item {item.Name}, sub: {item.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID}).";
1069  }
1070  else
1071  {
1072  errorStr = errorStrNoName = $"Missing entity {entity}, sub: {entity.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID}).";
1073  }
1074  if (GameStarted)
1075  {
1076  var serverSubNames = Submarine.Loaded.Select(s => s.Info.Name);
1077  if (subCount != Submarine.Loaded.Count || !subNames.SequenceEqual(serverSubNames))
1078  {
1079  string subErrorStr = $" Loaded submarines don't match (client: {string.Join(", ", subNames)}, server: {string.Join(", ", serverSubNames)}).";
1080  errorStr += subErrorStr;
1081  errorStrNoName += subErrorStr;
1082  }
1083  }
1084  break;
1085  }
1086 
1087  Log(ClientLogName(c) + " has reported an error: " + errorStr, ServerLog.MessageType.Error);
1088  GameAnalyticsManager.AddErrorEventOnce("GameServer.HandleClientError:" + errorStrNoName, GameAnalyticsManager.ErrorSeverity.Error, errorStr);
1089 
1090  try
1091  {
1092  WriteEventErrorData(c, errorStr);
1093  }
1094  catch (Exception e)
1095  {
1096  DebugConsole.ThrowError("Failed to write event error data", e);
1097  }
1098 
1099  if (c.Connection == OwnerConnection)
1100  {
1101  SendDirectChatMessage(errorStr, c, ChatMessageType.MessageBox);
1102  EndGame(wasSaved: false);
1103  }
1104  else
1105  {
1106  KickClient(c, errorStr);
1107  }
1108 
1109  }
1110 
1111  private void WriteEventErrorData(Client client, string errorStr)
1112  {
1113  if (!Directory.Exists(ServerLog.SavePath))
1114  {
1115  Directory.CreateDirectory(ServerLog.SavePath);
1116  }
1117 
1118  string filePath = $"event_error_log_server_{client.Name}_{DateTime.UtcNow.ToShortTimeString()}.log";
1119  filePath = Path.Combine(ServerLog.SavePath, ToolBox.RemoveInvalidFileNameChars(filePath));
1120  if (File.Exists(filePath)) { return; }
1121 
1122  List<string> errorLines = new List<string>
1123  {
1124  errorStr, ""
1125  };
1126 
1127  if (GameMain.GameSession?.GameMode != null)
1128  {
1129  errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name.Value);
1130  if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
1131  {
1132  errorLines.Add("Campaign ID: " + campaign.CampaignID);
1133  errorLines.Add("Campaign save ID: " + campaign.LastSaveID);
1134  }
1135  foreach (Mission mission in GameMain.GameSession.Missions)
1136  {
1137  errorLines.Add("Mission: " + mission.Prefab.Identifier);
1138  }
1139  }
1140  if (GameMain.GameSession?.Submarine != null)
1141  {
1142  errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Info.Name);
1143  }
1144  if (GameMain.NetworkMember?.RespawnManager is { } respawnManager)
1145  {
1146  errorLines.Add("Respawn shuttles: " + string.Join(", ", respawnManager.RespawnShuttles.Select(s => s.Info.Name)));
1147  }
1148  if (Level.Loaded != null)
1149  {
1150  errorLines.Add("Level: " + Level.Loaded.Seed + ", "
1151  + string.Join("; ", Level.Loaded.EqualityCheckValues.Select(cv
1152  => cv.Key + "=" + cv.Value.ToString("X"))));
1153  errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate);
1154  errorLines.Add("Entities:");
1155  foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex))
1156  {
1157  errorLines.Add(e.ErrorLine);
1158  }
1159  errorLines.Add("Entity count after generating level: " + Level.Loaded.EntityCountAfterGenerate);
1160  }
1161 
1162  errorLines.Add("Entity IDs:");
1163  Entity[] sortedEntities = Entity.GetEntities().OrderBy(e => e.CreationIndex).ToArray();
1164  foreach (Entity e in sortedEntities)
1165  {
1166  errorLines.Add(e.ErrorLine);
1167  }
1168 
1169  errorLines.Add("");
1170  errorLines.Add("EntitySpawner events:");
1171  foreach (var entityEvent in entityEventManager.UniqueEvents)
1172  {
1173  if (entityEvent.Entity is EntitySpawner)
1174  {
1175  var spawnData = entityEvent.Data as EntitySpawner.SpawnOrRemove;
1176  errorLines.Add(
1177  entityEvent.ID + ": " +
1178  (spawnData is EntitySpawner.RemoveEntity ? "Remove " : "Create ") +
1179  spawnData.Entity.ToString() +
1180  " (" + spawnData.ID + ", " + spawnData.Entity.ID + ")");
1181  }
1182  }
1183 
1184  errorLines.Add("");
1185  errorLines.Add("Last debug messages:");
1186  for (int i = DebugConsole.Messages.Count - 1; i > 0 && i > DebugConsole.Messages.Count - 15; i--)
1187  {
1188  errorLines.Add(" " + DebugConsole.Messages[i].Time + " - " + DebugConsole.Messages[i].Text);
1189  }
1190 
1191  File.WriteAllLines(filePath, errorLines);
1192  }
1193 
1194  public override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null)
1195  {
1196  if (!(entity is IServerSerializable serverSerializable))
1197  {
1198  throw new InvalidCastException($"Entity is not {nameof(IServerSerializable)}");
1199  }
1200  entityEventManager.CreateEvent(serverSerializable, extraData);
1201  }
1202 
1203  private byte GetNewClientSessionId()
1204  {
1205  byte userId = 1;
1206  while (connectedClients.Any(c => c.SessionId == userId))
1207  {
1208  userId++;
1209  }
1210  return userId;
1211  }
1212 
1213  private void ClientReadLobby(IReadMessage inc)
1214  {
1215  Client c = ConnectedClients.Find(x => x.Connection == inc.Sender);
1216  if (c == null)
1217  {
1218  //TODO: remove?
1219  //inc.Sender.Disconnect("You're not a connected client.");
1220  return;
1221  }
1222 
1223  SegmentTableReader<ClientNetSegment>.Read(inc, (segment, inc) =>
1224  {
1225  switch (segment)
1226  {
1227  case ClientNetSegment.SyncIds:
1228  //TODO: might want to use a clever class for this
1229  c.LastRecvLobbyUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvLobbyUpdate, GameMain.NetLobbyScreen.LastUpdateID);
1230  if (c.HasPermission(ClientPermissions.ManageSettings) &&
1231  NetIdUtils.IdMoreRecentOrMatches(c.LastRecvLobbyUpdate, c.LastSentServerSettingsUpdate))
1232  {
1233  c.LastRecvServerSettingsUpdate = c.LastSentServerSettingsUpdate;
1234  }
1235  c.LastRecvChatMsgID = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvChatMsgID, c.LastChatMsgQueueID);
1236  c.LastRecvClientListUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvClientListUpdate, LastClientListUpdateID);
1237 
1238  ReadClientNameChange(c, inc);
1239 
1240  c.LastRecvCampaignSave = inc.ReadUInt16();
1241  if (c.LastRecvCampaignSave > 0)
1242  {
1243  byte campaignID = inc.ReadByte();
1244  foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
1245  {
1246  c.LastRecvCampaignUpdate[netFlag] = inc.ReadUInt16();
1247  }
1248  bool characterDiscarded = inc.ReadBoolean();
1249  if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
1250  {
1251  if (characterDiscarded) { campaign.DiscardClientCharacterData(c); }
1252  //the client has a campaign save for another campaign
1253  //(the server started a new campaign and the client isn't aware of it yet?)
1254  if (campaign.CampaignID != campaignID)
1255  {
1256  c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1);
1257  foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
1258  {
1259  c.LastRecvCampaignUpdate[netFlag] =
1260  (UInt16)(campaign.GetLastUpdateIdForFlag(netFlag) - 1);
1261  }
1262  }
1263  }
1264  }
1265  break;
1266  case ClientNetSegment.ChatMessage:
1267  ChatMessage.ServerRead(inc, c);
1268  break;
1269  case ClientNetSegment.Vote:
1270  Voting.ServerRead(inc, c, dosProtection);
1271  break;
1272  default:
1273  return SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
1274  }
1275 
1276  //don't read further messages if the client has been disconnected (kicked due to spam for example)
1277  return connectedClients.Contains(c)
1278  ? SegmentTableReader<ClientNetSegment>.BreakSegmentReading.No
1279  : SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
1280  });
1281  }
1282 
1283  private void ClientReadIngame(IReadMessage inc)
1284  {
1285  Client c = ConnectedClients.Find(x => x.Connection == inc.Sender);
1286  if (c == null)
1287  {
1288  //TODO: remove?
1289  //inc.SenderConnection.Disconnect("You're not a connected client.");
1290  return;
1291  }
1292 
1293  bool midroundSyncingDone = inc.ReadBoolean();
1294  inc.ReadPadBits();
1295  if (GameStarted)
1296  {
1297  if (!c.InGame)
1298  {
1299  //check if midround syncing is needed due to missed unique events
1300  if (!midroundSyncingDone) { entityEventManager.InitClientMidRoundSync(c); }
1301  MissionAction.NotifyMissionsUnlockedThisRound(c);
1302  if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign)
1303  {
1304  mpCampaign.SendCrewState();
1305  }
1306  else if (GameMain.GameSession.GameMode is PvPMode && c.TeamID == CharacterTeamType.None)
1307  {
1308  AssignClientToPvpTeamMidgame(c);
1309  }
1310  c.InGame = true;
1311  }
1312  }
1313 
1314  SegmentTableReader<ClientNetSegment>.Read(inc, (segment, inc) =>
1315  {
1316  switch (segment)
1317  {
1318  case ClientNetSegment.SyncIds:
1319  //TODO: switch this to INetSerializableStruct
1320 
1321  UInt16 lastRecvChatMsgID = inc.ReadUInt16();
1322  UInt16 lastRecvEntityEventID = inc.ReadUInt16();
1323  UInt16 lastRecvClientListUpdate = inc.ReadUInt16();
1324 
1325  //last msgs we've created/sent, the client IDs should never be higher than these
1326  UInt16 lastEntityEventID = entityEventManager.Events.Count == 0 ? (UInt16)0 : entityEventManager.Events.Last().ID;
1327 
1328  c.LastRecvCampaignSave = inc.ReadUInt16();
1329  if (c.LastRecvCampaignSave > 0)
1330  {
1331  byte campaignID = inc.ReadByte();
1332  foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
1333  {
1334  c.LastRecvCampaignUpdate[netFlag] = inc.ReadUInt16();
1335  }
1336  bool characterDiscarded = inc.ReadBoolean();
1337  if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
1338  {
1339  if (characterDiscarded) { campaign.DiscardClientCharacterData(c); }
1340  //the client has a campaign save for another campaign
1341  //(the server started a new campaign and the client isn't aware of it yet?)
1342  if (campaign.CampaignID != campaignID)
1343  {
1344  c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1);
1345  foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
1346  {
1347  c.LastRecvCampaignUpdate[netFlag] =
1348  (UInt16)(campaign.GetLastUpdateIdForFlag(netFlag) - 1);
1349  }
1350  }
1351  }
1352  }
1353 
1354  if (c.NeedsMidRoundSync)
1355  {
1356  //received all the old events -> client in sync, we can switch to normal behavior
1357  if (lastRecvEntityEventID >= c.UnreceivedEntityEventCount - 1 ||
1359  {
1360  ushort prevID = lastRecvEntityEventID;
1361  c.NeedsMidRoundSync = false;
1362  lastRecvEntityEventID = (UInt16)(c.FirstNewEventID - 1);
1363  c.LastRecvEntityEventID = lastRecvEntityEventID;
1364  DebugConsole.Log("Finished midround syncing " + c.Name + " - switching from ID " + prevID + " to " + c.LastRecvEntityEventID);
1365  //notify the client of the state of the respawn manager (so they show the respawn prompt if needed)
1366  if (RespawnManager != null) { CreateEntityEvent(RespawnManager); }
1367  if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
1368  {
1369  //notify the client of the current bank balance and purchased repairs
1370  campaign.Bank.ForceUpdate();
1371  campaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.Misc);
1372  }
1373  }
1374  else
1375  {
1376  lastEntityEventID = (UInt16)(c.UnreceivedEntityEventCount - 1);
1377  }
1378  }
1379 
1380  if (NetIdUtils.IsValidId(lastRecvChatMsgID, c.LastRecvChatMsgID, c.LastChatMsgQueueID))
1381  {
1382  c.LastRecvChatMsgID = lastRecvChatMsgID;
1383  }
1384  else if (lastRecvChatMsgID != c.LastRecvChatMsgID && GameSettings.CurrentConfig.VerboseLogging)
1385  {
1386  DebugConsole.ThrowError(
1387  "Invalid lastRecvChatMsgID " + lastRecvChatMsgID +
1388  " (previous: " + c.LastChatMsgQueueID + ", latest: " + c.LastChatMsgQueueID + ")");
1389  }
1390 
1391  if (NetIdUtils.IsValidId(lastRecvEntityEventID, c.LastRecvEntityEventID, lastEntityEventID))
1392  {
1393  if (c.NeedsMidRoundSync)
1394  {
1395  //give midround-joining clients a bit more time to get in sync if they keep receiving messages
1396  int receivedEventCount = lastRecvEntityEventID - c.LastRecvEntityEventID;
1397  if (receivedEventCount < 0) { receivedEventCount += ushort.MaxValue; }
1398  c.MidRoundSyncTimeOut += receivedEventCount * 0.01f;
1399  DebugConsole.Log("Midround sync timeout " + c.MidRoundSyncTimeOut.ToString("0.##") + "/" + Timing.TotalTime.ToString("0.##"));
1400  }
1401 
1402  c.LastRecvEntityEventID = lastRecvEntityEventID;
1403  }
1404  else if (lastRecvEntityEventID != c.LastRecvEntityEventID && GameSettings.CurrentConfig.VerboseLogging)
1405  {
1406  DebugConsole.ThrowError(
1407  "Invalid lastRecvEntityEventID " + lastRecvEntityEventID +
1408  " (previous: " + c.LastRecvEntityEventID + ", latest: " + lastEntityEventID + ")");
1409  }
1410 
1411  if (NetIdUtils.IdMoreRecent(lastRecvClientListUpdate, c.LastRecvClientListUpdate))
1412  {
1413  c.LastRecvClientListUpdate = lastRecvClientListUpdate;
1414  }
1415 
1416  break;
1417  case ClientNetSegment.ChatMessage:
1418  ChatMessage.ServerRead(inc, c);
1419  break;
1420  case ClientNetSegment.CharacterInput:
1421  if (c.Character != null)
1422  {
1423  c.Character.ServerReadInput(inc, c);
1424  }
1425  else
1426  {
1427  DebugConsole.AddWarning($"Received character inputs from a client who's not controlling a character ({c.Name}).");
1428  }
1429  break;
1430  case ClientNetSegment.EntityState:
1431  entityEventManager.Read(inc, c);
1432  break;
1433  case ClientNetSegment.Vote:
1434  Voting.ServerRead(inc, c, dosProtection);
1435  break;
1436  case ClientNetSegment.SpectatingPos:
1437  c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle());
1438  break;
1439  default:
1440  return SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
1441  }
1442 
1443  //don't read further messages if the client has been disconnected (kicked due to spam for example)
1444  return connectedClients.Contains(c)
1445  ? SegmentTableReader<ClientNetSegment>.BreakSegmentReading.No
1446  : SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
1447  });
1448  }
1449 
1450  private void ReadCrewMessage(IReadMessage inc, Client sender)
1451  {
1452  if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1453  {
1454  mpCampaign.ServerReadCrew(inc, sender);
1455  }
1456  }
1457 
1458  private void ReadMoneyMessage(IReadMessage inc, Client sender)
1459  {
1460  if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1461  {
1462  mpCampaign.ServerReadMoney(inc, sender);
1463  }
1464  }
1465 
1466  private void ReadRewardDistributionMessage(IReadMessage inc, Client sender)
1467  {
1468  if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1469  {
1470  mpCampaign.ServerReadRewardDistribution(inc, sender);
1471  }
1472  }
1473 
1474  private void ResetRewardDistribution(Client client)
1475  {
1476  if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1477  {
1478  mpCampaign.ResetSalaries(client);
1479  }
1480  }
1481 
1482  private void ReadMedicalMessage(IReadMessage inc, Client sender)
1483  {
1484  if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1485  {
1486  mpCampaign.MedicalClinic.ServerRead(inc, sender);
1487  }
1488  }
1489 
1490  private static void ReadCircuitBoxMessage(IReadMessage inc, Client sender)
1491  {
1492  var header = INetSerializableStruct.Read<NetCircuitBoxHeader>(inc);
1493 
1494  INetSerializableStruct data = header.Opcode switch
1495  {
1496  CircuitBoxOpcode.Cursor => INetSerializableStruct.Read<NetCircuitBoxCursorInfo>(inc),
1497  _ => throw new ArgumentOutOfRangeException(nameof(header.Opcode), header.Opcode, "This data cannot be handled using direct network messages.")
1498  };
1499 
1500  if (header.FindTarget().TryUnwrap(out var box))
1501  {
1502  box.ServerRead(data, sender);
1503  }
1504  }
1505 
1506  private void ReadReadyToSpawnMessage(IReadMessage inc, Client sender)
1507  {
1508  sender.SpectateOnly = inc.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection);
1509  sender.WaitForNextRoundRespawn = inc.ReadBoolean();
1510  if (!(GameMain.GameSession?.GameMode is CampaignMode))
1511  {
1512  sender.WaitForNextRoundRespawn = null;
1513  }
1514  }
1515 
1516  private void ReadTakeOverBotMessage(IReadMessage inc, Client sender)
1517  {
1518  UInt16 botId = inc.ReadUInt16();
1519  if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign) { return; }
1520 
1522  {
1523  DebugConsole.ThrowError($"Client {sender.Name} has requested to take over a bot in Ironman mode!");
1524  return;
1525  }
1526 
1527  if (campaign.CurrentLocation.GetHireableCharacters().FirstOrDefault(c => c.ID == botId) is CharacterInfo hireableCharacter)
1528  {
1530  CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMoney) ||
1531  CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageHires))
1532  {
1533  if (campaign.TryHireCharacter(campaign.CurrentLocation, hireableCharacter, takeMoney: true, sender, buyingNewCharacter: true))
1534  {
1535  campaign.CurrentLocation.RemoveHireableCharacter(hireableCharacter);
1536  SpawnAndTakeOverBot(campaign, hireableCharacter, sender);
1537  campaign.SendCrewState(createNotification: false);
1538  }
1539  else
1540  {
1541  SendConsoleMessage($"Could not hire the bot {hireableCharacter.Name}.", sender, Color.Red);
1542  DebugConsole.ThrowError($"Client {sender.Name} failed to hire the bot {hireableCharacter.Name}.");
1543  }
1544  }
1545  else
1546  {
1547  SendConsoleMessage($"Could not hire the bot {hireableCharacter.Name}. No permission to manage money or hires.", sender, Color.Red);
1548  DebugConsole.ThrowError($"Client {sender.Name} failed to hire the bot {hireableCharacter.Name}. No permission to manage money or hires.");
1549  }
1550  }
1551  else
1552  {
1553  CharacterInfo botInfo = GameMain.GameSession.CrewManager?.GetCharacterInfos()?.FirstOrDefault(i => i.ID == botId);
1554 
1555  if (botInfo is { IsNewHire: true, Character: null })
1556  {
1557  SpawnAndTakeOverBot(campaign, botInfo, sender);
1558  }
1559  else if (botInfo?.Character == null || !botInfo.Character.IsBot)
1560  {
1561  SendConsoleMessage($"Could not find a bot with the id {botId}.", sender, Color.Red);
1562  DebugConsole.ThrowError($"Client {sender.Name} failed to take over a bot (Could not find a bot with the id {botId}).");
1563  return;
1564  }
1566  {
1567  sender.TryTakeOverBot(botInfo.Character);
1568  }
1569  else
1570  {
1571  SendConsoleMessage($"Failed to take over a bot (taking control of bots is disallowed).", sender, Color.Red);
1572  DebugConsole.ThrowError($"Client {sender.Name} failed to take over a bot (taking control of bots is disallowed).");
1573  }
1574  }
1575  }
1576 
1577  private static void SpawnAndTakeOverBot(CampaignMode campaign, CharacterInfo botInfo, Client client)
1578  {
1579  var mainSubSpawnpoint = WayPoint.SelectCrewSpawnPoints(botInfo.ToEnumerable().ToList(), Submarine.MainSub).FirstOrDefault();
1580  var spawnWaypoint = campaign.CrewManager.GetOutpostSpawnpoints()?.FirstOrDefault() ?? mainSubSpawnpoint;
1581  if (spawnWaypoint == null)
1582  {
1583  DebugConsole.ThrowError("SpawnAndTakeOverBot: Unable to find any spawn waypoints inside the sub");
1584  return;
1585  }
1586  Entity.Spawner.AddCharacterToSpawnQueue(botInfo.SpeciesName, spawnWaypoint.WorldPosition, botInfo, onSpawn: newCharacter =>
1587  {
1588  if (newCharacter == null)
1589  {
1590  DebugConsole.ThrowError("SpawnAndTakeOverBot: newCharacter is null somehow");
1591  return;
1592  }
1593  // No longer show the hired character in the HR list of current hires
1594  campaign.CrewManager.RemoveCharacterInfo(botInfo);
1595  newCharacter.TeamID = CharacterTeamType.Team1;
1596  campaign.CrewManager.InitializeCharacter(newCharacter, mainSubSpawnpoint, spawnWaypoint);
1597  client.TryTakeOverBot(newCharacter);
1598  });
1599  }
1600 
1601  private void ClientReadServerCommand(IReadMessage inc)
1602  {
1603  Client sender = ConnectedClients.Find(x => x.Connection == inc.Sender);
1604  if (sender == null)
1605  {
1606  //TODO: remove?
1607  //inc.SenderConnection.Disconnect("You're not a connected client.");
1608  return;
1609  }
1610 
1611  ClientPermissions command = ClientPermissions.None;
1612  try
1613  {
1614  command = (ClientPermissions)inc.ReadUInt16();
1615  }
1616  catch
1617  {
1618  return;
1619  }
1620 
1621  var mpCampaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign;
1622  if (command == ClientPermissions.ManageRound && mpCampaign != null)
1623  {
1624  //do nothing, ending campaign rounds is checked in more detail below
1625  }
1626  else if (command == ClientPermissions.ManageCampaign && mpCampaign != null)
1627  {
1628  //do nothing, campaign permissions are checked in more detail in MultiplayerCampaign.ServerRead
1629  }
1630  else if (!sender.HasPermission(command))
1631  {
1632  Log("Client \"" + GameServer.ClientLogName(sender) + "\" sent a server command \"" + command + "\". Permission denied.", ServerLog.MessageType.ServerMessage);
1633  return;
1634  }
1635 
1636  switch (command)
1637  {
1638  case ClientPermissions.Kick:
1639  string kickedName = inc.ReadString().ToLowerInvariant();
1640  string kickReason = inc.ReadString();
1641  var kickedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(kickedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection);
1642  if (kickedClient != null)
1643  {
1644  Log("Client \"" + GameServer.ClientLogName(sender) + "\" kicked \"" + GameServer.ClientLogName(kickedClient) + "\".", ServerLog.MessageType.ServerMessage);
1645  KickClient(kickedClient, string.IsNullOrEmpty(kickReason) ? $"ServerMessage.KickedBy~[initiator]={sender.Name}" : kickReason);
1646  }
1647  else
1648  {
1649  SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={kickedName}").Value, sender, ChatMessageType.Console);
1650  }
1651  break;
1652  case ClientPermissions.Ban:
1653  string bannedName = inc.ReadString().ToLowerInvariant();
1654  string banReason = inc.ReadString();
1655  double durationSeconds = inc.ReadDouble();
1656 
1657  TimeSpan? banDuration = null;
1658  if (durationSeconds > 0) { banDuration = TimeSpan.FromSeconds(durationSeconds); }
1659 
1660  var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(bannedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection);
1661  if (bannedClient != null)
1662  {
1663  Log("Client \"" + ClientLogName(sender) + "\" banned \"" + ClientLogName(bannedClient) + "\".", ServerLog.MessageType.ServerMessage);
1664  BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, banDuration);
1665  }
1666  else
1667  {
1668  var bannedPreviousClient = previousPlayers.Find(p => p.Name.Equals(bannedName, StringComparison.OrdinalIgnoreCase));
1669  if (bannedPreviousClient != null)
1670  {
1671  Log("Client \"" + ClientLogName(sender) + "\" banned \"" + bannedPreviousClient.Name + "\".", ServerLog.MessageType.ServerMessage);
1672  BanPreviousPlayer(bannedPreviousClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, banDuration);
1673  }
1674  else
1675  {
1676  SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={bannedName}").Value, sender, ChatMessageType.Console);
1677  }
1678  }
1679  break;
1680  case ClientPermissions.Unban:
1681  bool isPlayerName = inc.ReadBoolean(); inc.ReadPadBits();
1682  string str = inc.ReadString();
1683  if (isPlayerName)
1684  {
1685  UnbanPlayer(playerName: str);
1686  }
1687  else if (Endpoint.Parse(str).TryUnwrap(out var endpoint))
1688  {
1689  UnbanPlayer(endpoint);
1690  }
1691  break;
1692  case ClientPermissions.ManageRound:
1693  bool end = inc.ReadBoolean();
1694  if (end)
1695  {
1696  if (mpCampaign == null ||
1697  CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageRound))
1698  {
1699  bool save = inc.ReadBoolean();
1700  bool quitCampaign = inc.ReadBoolean();
1701  if (GameStarted)
1702  {
1703  using (dosProtection.Pause(sender))
1704  {
1705  Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage);
1706  if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save)
1707  {
1708  mpCampaign.SavePlayers();
1709  mpCampaign.HandleSaveAndQuit();
1710  GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
1711  SaveUtil.SaveGame(GameMain.GameSession.DataPath);
1712  }
1713  else
1714  {
1715  save = false;
1716  }
1717  EndGame(wasSaved: save);
1718  }
1719  }
1720  else if (mpCampaign != null)
1721  {
1722  Log($"Client \"{ClientLogName(sender)}\" quit the currently active campaign.", ServerLog.MessageType.ServerMessage);
1723  GameMain.GameSession = null;
1724  GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModePreset.Sandbox.Identifier;
1725  GameMain.NetLobbyScreen.LastUpdateID++;
1726 
1727  }
1728  }
1729  }
1730  else
1731  {
1732  bool continueCampaign = inc.ReadBoolean();
1733  if (mpCampaign != null && mpCampaign.GameOver || continueCampaign)
1734  {
1735  if (GameStarted)
1736  {
1737  SendDirectChatMessage("Cannot continue the campaign from the previous save (round already running).", sender, ChatMessageType.Error);
1738  break;
1739  }
1740  else if (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))
1741  {
1742  using (dosProtection.Pause(sender))
1743  {
1744  MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.DataPath, sender);
1745  }
1746  }
1747  }
1748  else if (!GameStarted && !initiatedStartGame)
1749  {
1750  using (dosProtection.Pause(sender))
1751  {
1752  Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage);
1753  var result = TryStartGame();
1754  if (result != TryStartGameResult.Success)
1755  {
1756  SendDirectChatMessage(TextManager.Get($"TryStartGameError.{result}").Value, sender, ChatMessageType.Error);
1757  }
1758  }
1759  }
1760  else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)))
1761  {
1762  using (dosProtection.Pause(sender))
1763  {
1764  var availableTransition = mpCampaign.GetAvailableTransition(out _, out _);
1765  //don't force location if we've teleported
1766  bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation;
1767  switch (availableTransition)
1768  {
1769  case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation:
1770  if (forceLocation)
1771  {
1772  mpCampaign.Map.SelectLocation(
1773  mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation));
1774  }
1775  mpCampaign.LoadNewLevel();
1776  break;
1777  case CampaignMode.TransitionType.ProgressToNextEmptyLocation:
1778  if (forceLocation)
1779  {
1780  mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation));
1781  }
1782  mpCampaign.LoadNewLevel();
1783  break;
1784  case CampaignMode.TransitionType.None:
1785  #if DEBUG || UNSTABLE
1786  DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available.");
1787  #endif
1788  break;
1789  default:
1790  Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage);
1791  mpCampaign.LoadNewLevel();
1792  break;
1793  }
1794  }
1795  }
1796  }
1797  break;
1798  case ClientPermissions.SelectSub:
1799  SelectedSubType subType = (SelectedSubType)inc.ReadByte();
1800  string subHash = inc.ReadString();
1801  var subList = GameMain.NetLobbyScreen.GetSubList();
1802  var sub = subList.FirstOrDefault(s => s.MD5Hash.StringRepresentation == subHash);
1803  if (sub == null)
1804  {
1805  DebugConsole.NewMessage($"Client \"{ClientLogName(sender)}\" attempted to select a sub, could not find a sub with the MD5 hash \"{subHash}\".", Color.Red);
1806  }
1807  else
1808  {
1809  switch (subType)
1810  {
1811  case SelectedSubType.Shuttle:
1812  GameMain.NetLobbyScreen.SelectedShuttle = sub;
1813  break;
1814  case SelectedSubType.Sub:
1815  GameMain.NetLobbyScreen.SelectedSub = sub;
1816  break;
1817  case SelectedSubType.EnemySub:
1818  GameMain.NetLobbyScreen.SelectedEnemySub = sub;
1819  break;
1820  }
1821  }
1822  break;
1823  case ClientPermissions.SelectMode:
1824  UInt16 modeIndex = inc.ReadUInt16();
1825  GameMain.NetLobbyScreen.SelectedModeIndex = modeIndex;
1826  Log("Gamemode changed to " + (GameMain.NetLobbyScreen.SelectedMode?.Name.Value ?? "none"), ServerLog.MessageType.ServerMessage);
1827  if (GameMain.NetLobbyScreen.GameModes[modeIndex] == GameModePreset.MultiPlayerCampaign)
1828  {
1829  TrySendCampaignSetupInfo(sender);
1830  }
1831  break;
1832  case ClientPermissions.ManageCampaign:
1833  mpCampaign?.ServerRead(inc, sender);
1834  break;
1835  case ClientPermissions.ConsoleCommands:
1836  DebugConsole.ServerRead(inc, sender);
1837  break;
1838  case ClientPermissions.ManagePermissions:
1839  byte targetClientID = inc.ReadByte();
1840  Client targetClient = connectedClients.Find(c => c.SessionId == targetClientID);
1841  if (targetClient == null || targetClient == sender || targetClient.Connection == OwnerConnection) { return; }
1842 
1843  targetClient.ReadPermissions(inc);
1844 
1845  List<string> permissionNames = new List<string>();
1846  foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions)))
1847  {
1848  if (permission == ClientPermissions.None || permission == ClientPermissions.All)
1849  {
1850  continue;
1851  }
1852  if (targetClient.Permissions.HasFlag(permission)) { permissionNames.Add(permission.ToString()); }
1853  }
1854 
1855  string logMsg;
1856  if (permissionNames.Any())
1857  {
1858  logMsg = "Client \"" + GameServer.ClientLogName(sender) + "\" set the permissions of the client \"" + GameServer.ClientLogName(targetClient) + "\" to "
1859  + string.Join(", ", permissionNames);
1860  }
1861  else
1862  {
1863  logMsg = "Client \"" + GameServer.ClientLogName(sender) + "\" removed all permissions from the client \"" + GameServer.ClientLogName(targetClient) + ".";
1864  }
1865  Log(logMsg, ServerLog.MessageType.ServerMessage);
1866 
1867  UpdateClientPermissions(targetClient);
1868 
1869  break;
1870  }
1871 
1872  inc.ReadPadBits();
1873  }
1874 
1875  private void ClientWrite(Client c)
1876  {
1877  if (GameStarted && c.InGame)
1878  {
1879  ClientWriteIngame(c);
1880  }
1881  else
1882  {
1883  //if 30 seconds have passed since the round started and the client isn't ingame yet,
1884  //consider the client's character disconnected (causing it to die if the client does not join soon)
1885  if (GameStarted && c.Character != null && (DateTime.Now - roundStartTime).Seconds > 30.0f)
1886  {
1887  c.Character.ClientDisconnected = true;
1888  }
1889 
1890  ClientWriteLobby(c);
1891 
1892  }
1893 
1894  if (c.Connection == OwnerConnection)
1895  {
1896  while (pendingMessagesToOwner.Any())
1897  {
1898  SendDirectChatMessage(pendingMessagesToOwner.Dequeue(), c);
1899  }
1900  }
1901 
1902  if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign &&
1903  GameMain.NetLobbyScreen.SelectedMode == campaign.Preset &&
1904  NetIdUtils.IdMoreRecent(campaign.LastSaveID, c.LastRecvCampaignSave))
1905  {
1906  //already sent an up-to-date campaign save
1907  if (c.LastCampaignSaveSendTime != default && campaign.LastSaveID == c.LastCampaignSaveSendTime.saveId)
1908  {
1909  //the save was sent less than 5 second ago, don't attempt to resend yet
1910  //(the client may have received it but hasn't acked us yet)
1911  if (c.LastCampaignSaveSendTime.time > NetTime.Now - 5.0f)
1912  {
1913  return;
1914  }
1915  }
1916 
1917  if (!FileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave))
1918  {
1919  FileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.DataPath.SavePath);
1920  c.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)NetTime.Now);
1921  }
1922  }
1923  }
1924 
1928  private void ClientWriteInitial(Client c, IWriteMessage outmsg)
1929  {
1930  if (GameSettings.CurrentConfig.VerboseLogging)
1931  {
1932  DebugConsole.NewMessage($"Sending initial lobby update to {c.Name}", Color.Gray);
1933  }
1934 
1935  outmsg.WriteByte(c.SessionId);
1936 
1937  var subList = GameMain.NetLobbyScreen.GetSubList();
1938  outmsg.WriteUInt16((UInt16)subList.Count);
1939  for (int i = 0; i < subList.Count; i++)
1940  {
1941  var sub = subList[i];
1942  outmsg.WriteString(sub.Name);
1943  outmsg.WriteString(sub.MD5Hash.ToString());
1944  outmsg.WriteByte((byte)sub.SubmarineClass);
1945  outmsg.WriteBoolean(sub.HasTag(SubmarineTag.Shuttle));
1946  outmsg.WriteBoolean(sub.RequiredContentPackagesInstalled);
1947  }
1948 
1949  outmsg.WriteBoolean(GameStarted);
1951  outmsg.WriteBoolean(ServerSettings.RespawnMode == RespawnMode.Permadeath);
1953 
1954  c.WritePermissions(outmsg);
1955  }
1956 
1957  private void ClientWriteIngame(Client c)
1958  {
1959  //don't send position updates to characters who are still midround syncing
1960  //characters or items spawned mid-round don't necessarily exist at the client's end yet
1961  if (!c.NeedsMidRoundSync)
1962  {
1963  Character clientCharacter = c.Character;
1964  foreach (Character otherCharacter in Character.CharacterList)
1965  {
1966  if (!otherCharacter.Enabled) { continue; }
1967  if (c.SpectatePos == null)
1968  {
1969  //not spectating ->
1970  // check if the client's character, or the entity they're viewing,
1971  // is close enough to the other character or the entity the other character is viewing
1972  float distSqr = GetShortestDistance(clientCharacter.WorldPosition, otherCharacter);
1973  if (clientCharacter.ViewTarget != null && clientCharacter.ViewTarget != clientCharacter)
1974  {
1975  distSqr = Math.Min(distSqr, GetShortestDistance(clientCharacter.ViewTarget.WorldPosition, otherCharacter));
1976  }
1977  if (distSqr >= MathUtils.Pow2(otherCharacter.Params.DisableDistance)) { continue; }
1978  }
1979  else if (otherCharacter != clientCharacter)
1980  {
1981  //spectating ->
1982  // check if the position the client is viewing
1983  // is close enough to the other character or the entity the other character is viewing
1984  if (GetShortestDistance(c.SpectatePos.Value, otherCharacter) >= MathUtils.Pow2(otherCharacter.Params.DisableDistance)) { continue; }
1985  }
1986 
1987  static float GetShortestDistance(Vector2 viewPos, Character targetCharacter)
1988  {
1989  float distSqr = Vector2.DistanceSquared(viewPos, targetCharacter.WorldPosition);
1990  if (targetCharacter.ViewTarget != null && targetCharacter.ViewTarget != targetCharacter)
1991  {
1992  //if the character is viewing something (far-away turret?),
1993  //we might want to send updates about it to the spectating client even though they're far away from the actual character
1994  distSqr = Math.Min(distSqr, Vector2.DistanceSquared(viewPos, targetCharacter.ViewTarget.WorldPosition));
1995  }
1996  return distSqr;
1997  }
1998 
1999  float updateInterval = otherCharacter.GetPositionUpdateInterval(c);
2000  c.PositionUpdateLastSent.TryGetValue(otherCharacter, out float lastSent);
2001  if (lastSent > NetTime.Now)
2002  {
2003  //sent in the future -> can't be right, remove
2004  c.PositionUpdateLastSent.Remove(otherCharacter);
2005  }
2006  else
2007  {
2008  if (lastSent > NetTime.Now - updateInterval) { continue; }
2009  }
2010  if (!c.PendingPositionUpdates.Contains(otherCharacter)) { c.PendingPositionUpdates.Enqueue(otherCharacter); }
2011  }
2012 
2013  foreach (Submarine sub in Submarine.Loaded)
2014  {
2015  //if docked to a sub with a smaller ID, don't send an update
2016  // (= update is only sent for the docked sub that has the smallest ID, doesn't matter if it's the main sub or a shuttle)
2017  if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) { continue; }
2018  if (sub.PhysicsBody == null || sub.PhysicsBody.BodyType == FarseerPhysics.BodyType.Static) { continue; }
2019  if (!c.PendingPositionUpdates.Contains(sub)) { c.PendingPositionUpdates.Enqueue(sub); }
2020  }
2021 
2022  foreach (Item item in Item.ItemList)
2023  {
2024  if (item.PositionUpdateInterval == float.PositiveInfinity) { continue; }
2025  float updateInterval = item.GetPositionUpdateInterval(c);
2026  c.PositionUpdateLastSent.TryGetValue(item, out float lastSent);
2027  if (lastSent > NetTime.Now)
2028  {
2029  //sent in the future -> can't be right, remove
2030  c.PositionUpdateLastSent.Remove(item);
2031  }
2032  else
2033  {
2034  if (lastSent > NetTime.Now - updateInterval) { continue; }
2035  }
2036  if (!c.PendingPositionUpdates.Contains(item)) { c.PendingPositionUpdates.Enqueue(item); }
2037  }
2038  }
2039 
2040  IWriteMessage outmsg = new WriteOnlyMessage();
2041  outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME);
2042  outmsg.WriteSingle((float)NetTime.Now);
2043  outmsg.WriteSingle(EndRoundTimeRemaining);
2044 
2045  using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
2046  {
2047  segmentTable.StartNewSegment(ServerNetSegment.SyncIds);
2048  outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server
2050 
2051  if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode)
2052  {
2053  outmsg.WriteBoolean(true);
2054  outmsg.WritePadBits();
2055  campaign.ServerWrite(outmsg, c);
2056  }
2057  else
2058  {
2059  outmsg.WriteBoolean(false);
2060  outmsg.WritePadBits();
2061  }
2062 
2063  int clientListBytes = outmsg.LengthBytes;
2064  WriteClientList(segmentTable, c, outmsg);
2065  clientListBytes = outmsg.LengthBytes - clientListBytes;
2066 
2067  int chatMessageBytes = outmsg.LengthBytes;
2068  WriteChatMessages(segmentTable, outmsg, c);
2069  chatMessageBytes = outmsg.LengthBytes - chatMessageBytes;
2070 
2071  //write as many position updates as the message can fit (only after midround syncing is done)
2072  int positionUpdateBytes = outmsg.LengthBytes;
2073  while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0)
2074  {
2075  var entity = c.PendingPositionUpdates.Peek();
2076  if (entity is not IServerPositionSync entityPositionSync ||
2077  entity.Removed ||
2078  (entity is Item item && float.IsInfinity(item.PositionUpdateInterval)))
2079  {
2080  c.PendingPositionUpdates.Dequeue();
2081  continue;
2082  }
2083 
2084  var tempBuffer = new ReadWriteMessage();
2085  var entityPositionHeader = EntityPositionHeader.FromEntity(entity);
2086  tempBuffer.WriteNetSerializableStruct(entityPositionHeader);
2087  entityPositionSync.ServerWritePosition(tempBuffer, c);
2088 
2089  //no more room in this packet
2090  if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100)
2091  {
2092  break;
2093  }
2094 
2095  segmentTable.StartNewSegment(ServerNetSegment.EntityPosition);
2096  outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
2097  outmsg.WriteVariableUInt32((uint)tempBuffer.LengthBytes);
2098  outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes);
2099  outmsg.WritePadBits();
2100 
2101  c.PositionUpdateLastSent[entity] = (float)NetTime.Now;
2102  c.PendingPositionUpdates.Dequeue();
2103  }
2104  positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes;
2105 
2106  if (outmsg.LengthBytes > MsgConstants.MTU)
2107  {
2108  string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n";
2109  errorMsg +=
2110  " Client list size: " + clientListBytes + " bytes\n" +
2111  " Chat message size: " + chatMessageBytes + " bytes\n" +
2112  " Position update size: " + positionUpdateBytes + " bytes\n\n";
2113  DebugConsole.ThrowError(errorMsg);
2114  GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
2115  }
2116  }
2117 
2118  serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable);
2119 
2120  //---------------------------------------------------------------------------
2121 
2122  for (int i = 0; i < NetConfig.MaxEventPacketsPerUpdate; i++)
2123  {
2124  outmsg = new WriteOnlyMessage();
2125  outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME);
2126  outmsg.WriteSingle((float)NetTime.Now);
2127  outmsg.WriteSingle(EndRoundTimeRemaining);
2128 
2129  using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
2130  {
2131  int eventManagerBytes = outmsg.LengthBytes;
2132  entityEventManager.Write(segmentTable, c, outmsg, out List<NetEntityEvent> sentEvents);
2133  eventManagerBytes = outmsg.LengthBytes - eventManagerBytes;
2134 
2135  if (sentEvents.Count == 0)
2136  {
2137  break;
2138  }
2139 
2140  if (outmsg.LengthBytes > MsgConstants.MTU)
2141  {
2142  string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " +
2143  MsgConstants.MTU + ")\n";
2144  errorMsg +=
2145  " Event size: " + eventManagerBytes + " bytes\n";
2146 
2147  if (sentEvents != null && sentEvents.Count > 0)
2148  {
2149  errorMsg += "Sent events: \n";
2150  foreach (var entityEvent in sentEvents)
2151  {
2152  errorMsg += " - " + (entityEvent.Entity?.ToString() ?? "null") + "\n";
2153  }
2154  }
2155 
2156  DebugConsole.ThrowError(errorMsg);
2157  GameAnalyticsManager.AddErrorEventOnce(
2158  "GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes,
2159  GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
2160  }
2161  }
2162 
2163  serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable);
2164  }
2165  }
2166 
2167  private void WriteClientList(in SegmentTableWriter<ServerNetSegment> segmentTable, Client c, IWriteMessage outmsg)
2168  {
2169  bool hasChanged = NetIdUtils.IdMoreRecent(LastClientListUpdateID, c.LastRecvClientListUpdate);
2170  if (!hasChanged) { return; }
2171 
2172  segmentTable.StartNewSegment(ServerNetSegment.ClientList);
2173  outmsg.WriteUInt16(LastClientListUpdateID);
2174 
2175  GameMain.LuaCs.Hook.Call("writeClientList", c, outmsg);
2176  outmsg.WriteByte((byte)Team1Count);
2177  outmsg.WriteByte((byte)Team2Count);
2178 
2179  outmsg.WriteByte((byte)connectedClients.Count);
2180  foreach (Client client in connectedClients)
2181  {
2182  var tempClientData = new TempClient
2183  {
2184  SessionId = client.SessionId,
2185  AccountInfo = client.AccountInfo,
2186  NameId = client.NameId,
2187  Name = client.Name,
2188  PreferredJob = client.Character?.Info?.Job != null && GameStarted
2190  : client.PreferredJob,
2191  PreferredTeam = client.PreferredTeam,
2192  TeamID = client.TeamID,
2193  CharacterId = client.Character == null || !GameStarted ? (ushort)0 : client.Character.ID,
2194  Karma = c.HasPermission(ClientPermissions.ServerLog) ? client.Karma : 100.0f,
2195  Muted = client.Muted,
2196  InGame = client.InGame,
2197  HasPermissions = client.Permissions != ClientPermissions.None,
2198  IsOwner = client.Connection == OwnerConnection,
2199  IsDownloading = FileSender.ActiveTransfers.Any(t => t.Connection == client.Connection)
2200  };
2201 
2202  var result = GameMain.LuaCs.Hook.Call<TempClient?>("writeClientList.modifyTempClientData", c, client, tempClientData, outmsg);
2203 
2204  if (result != null)
2205  {
2206  tempClientData = result.Value;
2207  }
2208 
2209  outmsg.WriteNetSerializableStruct(tempClientData);
2210  outmsg.WritePadBits();
2211  }
2212  }
2213 
2214  public void ClientWriteLobby(Client c)
2215  {
2216  bool isInitialUpdate = false;
2217 
2218  IWriteMessage outmsg = new WriteOnlyMessage();
2219  outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY);
2220 
2221  bool messageTooLarge;
2222  using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
2223  {
2224  segmentTable.StartNewSegment(ServerNetSegment.SyncIds);
2225 
2226  int settingsBytes = outmsg.LengthBytes;
2227  int initialUpdateBytes = 0;
2228 
2230  {
2232  }
2233 
2234  IWriteMessage settingsBuf = null;
2235  if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate))
2236  {
2237  outmsg.WriteBoolean(true);
2238  outmsg.WritePadBits();
2239 
2241 
2242  settingsBuf = new ReadWriteMessage();
2243  ServerSettings.ServerWrite(settingsBuf, c);
2244  outmsg.WriteUInt16((UInt16)settingsBuf.LengthBytes);
2245  outmsg.WriteBytes(settingsBuf.Buffer, 0, settingsBuf.LengthBytes);
2246 
2247  outmsg.WriteBoolean(c.LastRecvLobbyUpdate < 1);
2248  if (c.LastRecvLobbyUpdate < 1)
2249  {
2250  isInitialUpdate = true;
2251  initialUpdateBytes = outmsg.LengthBytes;
2252  ClientWriteInitial(c, outmsg);
2253  initialUpdateBytes = outmsg.LengthBytes - initialUpdateBytes;
2254  }
2257 
2258  if (GameMain.NetLobbyScreen.SelectedEnemySub is { } enemySub)
2259  {
2260  outmsg.WriteBoolean(true);
2261  outmsg.WriteString(enemySub.Name);
2262  outmsg.WriteString(enemySub.MD5Hash.ToString());
2263  }
2264  else
2265  {
2266  outmsg.WriteBoolean(false);
2267  }
2268 
2269  outmsg.WriteBoolean(IsUsingRespawnShuttle());
2270  var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ?
2271  RespawnManager.RespawnShuttles.First().Info :
2273  outmsg.WriteString(selectedShuttle.Name);
2274  outmsg.WriteString(selectedShuttle.MD5Hash.ToString());
2275 
2278 
2280 
2282 
2285 
2287  foreach (var missionType in GameMain.NetLobbyScreen.MissionTypes)
2288  {
2289  outmsg.WriteIdentifier(missionType);
2290  }
2291 
2295 
2296  outmsg.WriteByte((byte)ServerSettings.BotCount);
2298 
2301  {
2302  outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f);
2303  }
2304 
2306  connectedClients.None(c => c.Connection == OwnerConnection || c.HasPermission(ClientPermissions.ManageRound) || c.HasPermission(ClientPermissions.ManageCampaign)))
2307  {
2308  //if no-one has permissions to manage the campaign, show the setup UI to everyone
2309  TrySendCampaignSetupInfo(c);
2310  }
2311  }
2312  else
2313  {
2314  outmsg.WriteBoolean(false);
2315  outmsg.WritePadBits();
2316  }
2317  settingsBytes = outmsg.LengthBytes - settingsBytes;
2318 
2319  int campaignBytes = outmsg.LengthBytes;
2320  if (outmsg.LengthBytes < MsgConstants.MTU - 500 &&
2322  {
2323  outmsg.WriteBoolean(true);
2324  outmsg.WritePadBits();
2325  campaign.ServerWrite(outmsg, c);
2326  }
2327  else
2328  {
2329  outmsg.WriteBoolean(false);
2330  outmsg.WritePadBits();
2331  }
2332  campaignBytes = outmsg.LengthBytes - campaignBytes;
2333 
2334  outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server
2335 
2336  int clientListBytes = outmsg.LengthBytes;
2337  if (outmsg.LengthBytes < MsgConstants.MTU - 500)
2338  {
2339  WriteClientList(segmentTable, c, outmsg);
2340  }
2341  clientListBytes = outmsg.LengthBytes - clientListBytes;
2342 
2343  int chatMessageBytes = outmsg.LengthBytes;
2344  WriteChatMessages(segmentTable, outmsg, c);
2345  chatMessageBytes = outmsg.LengthBytes - chatMessageBytes;
2346 
2347  messageTooLarge = outmsg.LengthBytes > MsgConstants.MTU;
2348  if (messageTooLarge && !isInitialUpdate)
2349  {
2350  string warningMsg = "Maximum packet size exceeded, will send using reliable mode (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n";
2351  warningMsg +=
2352  " Client list size: " + clientListBytes + " bytes\n" +
2353  " Chat message size: " + chatMessageBytes + " bytes\n" +
2354  " Campaign size: " + campaignBytes + " bytes\n" +
2355  " Settings size: " + settingsBytes + " bytes\n";
2356  if (initialUpdateBytes > 0)
2357  {
2358  warningMsg +=
2359  " Initial update size: " + initialUpdateBytes + " bytes\n";
2360  }
2361  if (settingsBuf != null)
2362  {
2363  warningMsg +=
2364  " Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n";
2365  }
2366 #if DEBUG || UNSTABLE
2367  DebugConsole.ThrowError(warningMsg);
2368 #else
2369  if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); }
2370 #endif
2371  GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
2372  }
2373  }
2374 
2375  if (isInitialUpdate || messageTooLarge)
2376  {
2377  //the initial update may be very large if the host has a large number
2378  //of submarine files, so the message may have to be fragmented
2379 
2380  //unreliable messages don't play nicely with fragmenting, so we'll send the message reliably
2381  serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Reliable);
2382 
2383  //and assume the message was received, so we don't have to keep resending
2384  //these large initial messages until the client acknowledges receiving them
2386 
2387  }
2388  else
2389  {
2390  serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable);
2391  }
2392 
2393  if (isInitialUpdate)
2394  {
2395  SendVoteStatus(new List<Client>() { c });
2396  }
2397  }
2398 
2399  private static void WriteChatMessages(in SegmentTableWriter<ServerNetSegment> segmentTable, IWriteMessage outmsg, Client c)
2400  {
2401  c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID));
2402  for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
2403  {
2404  if (outmsg.LengthBytes + c.ChatMsgQueue[i].EstimateLengthBytesServer(c) > MsgConstants.MTU - 5 && i > 0)
2405  {
2406  //not enough room in this packet
2407  return;
2408  }
2409  c.ChatMsgQueue[i].ServerWrite(segmentTable, outmsg, c);
2410  }
2411  }
2412 
2414  {
2415  Success,
2416  GameAlreadyStarted,
2417  PerksExceedAllowance,
2418  SubmarineNotFound,
2419  GameModeNotSelected,
2420  CannotStartMultiplayerCampaign,
2421  }
2422 
2424  {
2425  if (initiatedStartGame || GameStarted) { return TryStartGameResult.GameAlreadyStarted; }
2426 
2427  GameModePreset selectedMode =
2428  Voting.HighestVoted<GameModePreset>(VoteType.Mode, connectedClients) ?? GameMain.NetLobbyScreen.SelectedMode;
2429  if (selectedMode == null)
2430  {
2431  return TryStartGameResult.GameModeNotSelected;
2432  }
2434  {
2435  //DebugConsole.ThrowError($"{nameof(TryStartGame)} failed. Cannot start a multiplayer campaign via {nameof(TryStartGame)} - use {nameof(MultiPlayerCampaign.StartNewCampaign)} or {nameof(MultiPlayerCampaign.LoadCampaign)} instead.");
2437  {
2439  }
2440  return TryStartGameResult.CannotStartMultiplayerCampaign;
2441  }
2442 
2443  bool applyPerks = GameSession.ShouldApplyDisembarkPoints(selectedMode);
2444  if (applyPerks)
2445  {
2447  {
2448  return TryStartGameResult.PerksExceedAllowance;
2449  }
2450  }
2451 
2452  Log("Starting a new round...", ServerLog.MessageType.ServerMessage);
2454 
2455  SubmarineInfo selectedSub;
2456  Option<SubmarineInfo> selectedEnemySub = Option.None;
2457 
2459  {
2460  if (selectedMode == GameModePreset.PvP)
2461  {
2462  var team1Voters = connectedClients.Where(static c => c.PreferredTeam == CharacterTeamType.Team1);
2463  var team2Voters = connectedClients.Where(static c => c.PreferredTeam == CharacterTeamType.Team2);
2464 
2465  SubmarineInfo team1Sub = Voting.HighestVoted<SubmarineInfo>(VoteType.Sub, team1Voters, out int team1VoteCount);
2466  SubmarineInfo team2Sub = Voting.HighestVoted<SubmarineInfo>(VoteType.Sub, team2Voters, out int team2VoteCount);
2467 
2468  // check if anyone on coalition voted for a sub
2469  if (team1VoteCount > 0)
2470  {
2471  // use the most voted one
2472  selectedSub = team1Sub;
2473  }
2474  else
2475  {
2476  selectedSub = team2VoteCount > 0
2477  ? team2Sub // only separatists voted for a sub, use theirs
2478  : GameMain.NetLobbyScreen.SelectedSub; // nobody voted for a sub so use the default one
2479  }
2480 
2481  // check if separatists voted for a sub
2482  if (team2VoteCount > 0 && team2Sub != null)
2483  {
2484  selectedEnemySub = Option.Some(team2Sub);
2485  }
2486  // no reason to fall back to coalition sub,
2487  // since not selecting an enemy submarine automatically selects the coalition sub
2488  // deeper in the code
2489  }
2490  else
2491  {
2492  selectedSub = Voting.HighestVoted<SubmarineInfo>(VoteType.Sub, connectedClients) ?? GameMain.NetLobbyScreen.SelectedSub;
2493  }
2494  }
2495  else
2496  {
2497  selectedSub = GameMain.NetLobbyScreen.SelectedSub;
2499 
2500  // Option throws an exception if the value is null, prevent that
2501  if (enemySub != null)
2502  {
2503  selectedEnemySub = Option.Some(enemySub);
2504  }
2505  }
2506 
2507  if (selectedSub == null || selectedShuttle == null)
2508  {
2509  return TryStartGameResult.SubmarineNotFound;
2510  }
2511 
2512  if (applyPerks && CheckIfAnyPerksAreIncompatible(selectedSub, selectedEnemySub.Fallback(selectedSub), selectedMode, out var incompatiblePerks))
2513  {
2514  CoroutineManager.StartCoroutine(WarnAndDelayStartGame(incompatiblePerks, selectedSub, selectedEnemySub, selectedShuttle, selectedMode), nameof(WarnAndDelayStartGame));
2515  return TryStartGameResult.Success;
2516  }
2517 
2518  initiatedStartGame = true;
2519  startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedEnemySub, selectedShuttle, selectedMode), "InitiateStartGame");
2520 
2521  return TryStartGameResult.Success;
2522  }
2523 
2524  private bool CheckIfAnyPerksAreIncompatible(SubmarineInfo team1Sub, SubmarineInfo team2Sub, GameModePreset preset, out PerkCollection incompatiblePerks)
2525  {
2526  var incompatibleTeam1Perks = ImmutableArray.CreateBuilder<DisembarkPerkPrefab>();
2527  var incompatibleTeam2Perks = ImmutableArray.CreateBuilder<DisembarkPerkPrefab>();
2528  bool hasIncompatiblePerks = false;
2529  PerkCollection perks = GameSession.GetPerks();
2530 
2531  bool ignorePerksThatCanNotApplyWithoutSubmarine = GameSession.ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(preset, GameMain.NetLobbyScreen.MissionTypes);
2532 
2533  foreach (DisembarkPerkPrefab perk in perks.Team1Perks)
2534  {
2535  if (ignorePerksThatCanNotApplyWithoutSubmarine && perk.PerkBehaviors.Any(static p => !p.CanApplyWithoutSubmarine())) { continue; }
2536  bool anyCanNotApply = perk.PerkBehaviors.Any(p => !p.CanApply(team1Sub));
2537 
2538  if (anyCanNotApply)
2539  {
2540  incompatibleTeam1Perks.Add(perk);
2541  hasIncompatiblePerks = true;
2542  }
2543  }
2544 
2545  if (preset == GameModePreset.PvP)
2546  {
2547  foreach (DisembarkPerkPrefab perk in perks.Team2Perks)
2548  {
2549  if (ignorePerksThatCanNotApplyWithoutSubmarine && perk.PerkBehaviors.Any(static p => !p.CanApplyWithoutSubmarine())) { continue; }
2550 
2551  bool anyCanNotApply = perk.PerkBehaviors.Any(p => !p.CanApply(team2Sub));
2552 
2553  if (anyCanNotApply)
2554  {
2555  incompatibleTeam2Perks.Add(perk);
2556  hasIncompatiblePerks = true;
2557  }
2558  }
2559  }
2560 
2561  incompatiblePerks = new PerkCollection(incompatibleTeam1Perks.ToImmutable(), incompatibleTeam2Perks.ToImmutable());
2562  return hasIncompatiblePerks;
2563  }
2564 
2565  private bool isRoundStartWarningActive;
2566 
2567  private void AbortStartGameIfWarningActive()
2568  {
2569  isRoundStartWarningActive = false;
2570  CoroutineManager.StopCoroutines(nameof(WarnAndDelayStartGame));
2571  }
2572 
2573  private IEnumerable<CoroutineStatus> WarnAndDelayStartGame(PerkCollection incompatiblePerks, SubmarineInfo selectedSub, Option<SubmarineInfo> selectedEnemySub, SubmarineInfo selectedShuttle, GameModePreset selectedMode)
2574  {
2575  isRoundStartWarningActive = true;
2576  const float warningDuration = 15.0f;
2577 
2578  SerializableDateTime waitUntilTime = SerializableDateTime.UtcNow + TimeSpan.FromSeconds(warningDuration);
2579  if (connectedClients.Any())
2580  {
2581  IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.WARN_STARTGAME);
2582  INetSerializableStruct warnData = new RoundStartWarningData(
2583  RoundStartsAnywaysTimeInSeconds: warningDuration,
2584  Team1Sub: selectedSub.Name,
2585  Team1IncompatiblePerks: ToolBox.PrefabCollectionToUintIdentifierArray(incompatiblePerks.Team1Perks),
2586  Team2Sub: selectedEnemySub.Fallback(selectedSub).Name,
2587  Team2IncompatiblePerks: ToolBox.PrefabCollectionToUintIdentifierArray(incompatiblePerks.Team2Perks));
2588  msg.WriteNetSerializableStruct(warnData);
2589 
2590  foreach (Client c in connectedClients)
2591  {
2592  serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable);
2593  }
2594  }
2595 
2596  while (waitUntilTime > SerializableDateTime.UtcNow)
2597  {
2598  yield return CoroutineStatus.Running;
2599  }
2600 
2601  CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedEnemySub, selectedShuttle, selectedMode), "InitiateStartGame");
2602  yield return CoroutineStatus.Success;
2603  }
2604 
2605  private IEnumerable<CoroutineStatus> InitiateStartGame(SubmarineInfo selectedSub, Option<SubmarineInfo> selectedEnemySub, SubmarineInfo selectedShuttle, GameModePreset selectedMode)
2606  {
2607  isRoundStartWarningActive = false;
2608  initiatedStartGame = true;
2609 
2610  if (connectedClients.Any())
2611  {
2612  IWriteMessage msg = new WriteOnlyMessage();
2613  msg.WriteByte((byte)ServerPacketHeader.QUERY_STARTGAME);
2614 
2615  msg.WriteString(selectedSub.Name);
2616  msg.WriteString(selectedSub.MD5Hash.StringRepresentation);
2617 
2618  if (selectedEnemySub.TryUnwrap(out var enemySub))
2619  {
2620  msg.WriteBoolean(true);
2621  msg.WriteString(enemySub.Name);
2622  msg.WriteString(enemySub.MD5Hash.StringRepresentation);
2623  }
2624  else
2625  {
2626  msg.WriteBoolean(false);
2627  }
2628 
2629  msg.WriteBoolean(IsUsingRespawnShuttle());
2630  msg.WriteString(selectedShuttle.Name);
2631  msg.WriteString(selectedShuttle.MD5Hash.StringRepresentation);
2632 
2633  var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign;
2634  msg.WriteByte(campaign == null ? (byte)0 : campaign.CampaignID);
2635  msg.WriteUInt16(campaign == null ? (UInt16)0 : campaign.LastSaveID);
2636  foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
2637  {
2638  msg.WriteUInt16(campaign == null ? (UInt16)0 : campaign.GetLastUpdateIdForFlag(flag));
2639  }
2640 
2641  connectedClients.ForEach(c => c.ReadyToStart = false);
2642 
2643  foreach (NetworkConnection conn in connectedClients.Select(c => c.Connection))
2644  {
2645  serverPeer.Send(msg, conn, DeliveryMethod.Reliable);
2646  }
2647 
2648  //give the clients a few seconds to request missing sub/shuttle files before starting the round
2649  float waitForResponseTimer = 5.0f;
2650  while (connectedClients.Any(c => !c.ReadyToStart) && waitForResponseTimer > 0.0f)
2651  {
2652  waitForResponseTimer -= CoroutineManager.DeltaTime;
2653  yield return CoroutineStatus.Running;
2654  }
2655 
2656  if (FileSender.ActiveTransfers.Count > 0)
2657  {
2658  float waitForTransfersTimer = 20.0f;
2659  while (FileSender.ActiveTransfers.Count > 0 && waitForTransfersTimer > 0.0f)
2660  {
2661  waitForTransfersTimer -= CoroutineManager.DeltaTime;
2662  yield return CoroutineStatus.Running;
2663  }
2664  }
2665  }
2666 
2667  startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedEnemySub, selectedMode, CampaignSettings.Empty), false);
2668 
2669  yield return CoroutineStatus.Success;
2670  }
2671 
2672  private IEnumerable<CoroutineStatus> StartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, Option<SubmarineInfo> selectedEnemySub, GameModePreset selectedMode, CampaignSettings settings)
2673  {
2674  PerkCollection perkCollection = PerkCollection.Empty;
2675 
2676  if (GameSession.ShouldApplyDisembarkPoints(selectedMode))
2677  {
2678  perkCollection = GameSession.GetPerks();
2679  }
2680 
2681  entityEventManager.Clear();
2682 
2683  roundStartSeed = DateTime.Now.Millisecond;
2684  Rand.SetSyncedSeed(roundStartSeed);
2685 
2686  int teamCount = 1;
2687  bool isPvP = selectedMode == GameModePreset.PvP;
2688  MultiPlayerCampaign campaign = selectedMode == GameMain.GameSession?.GameMode.Preset ?
2689  GameMain.GameSession?.GameMode as MultiPlayerCampaign : null;
2690 
2691  if (campaign != null && campaign.Map == null)
2692  {
2693  initiatedStartGame = false;
2694  startGameCoroutine = null;
2695  string errorMsg = "Starting the round failed. Campaign was still active, but the map has been disposed. Try selecting another game mode.";
2696  DebugConsole.ThrowError(errorMsg);
2697  GameAnalyticsManager.AddErrorEventOnce("GameServer.StartGame:InvalidCampaignState", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
2698  if (OwnerConnection != null)
2699  {
2700  SendDirectChatMessage(errorMsg, connectedClients.Find(c => c.Connection == OwnerConnection), ChatMessageType.Error);
2701  }
2702  yield return CoroutineStatus.Failure;
2703  }
2704 
2705  bool initialSuppliesSpawned = false;
2706  //don't instantiate a new gamesession if we're playing a campaign
2707  if (campaign == null || GameMain.GameSession == null)
2708  {
2709  traitorManager = new TraitorManager(this);
2710  GameMain.GameSession = new GameSession(selectedSub, selectedEnemySub, CampaignDataPath.Empty, selectedMode, settings, GameMain.NetLobbyScreen.LevelSeed, missionTypes: GameMain.NetLobbyScreen.MissionTypes);
2711  }
2712  else
2713  {
2714  initialSuppliesSpawned = GameMain.GameSession.SubmarineInfo is { InitialSuppliesSpawned: true };
2715  }
2716 
2717  if (GameMain.GameSession.GameMode is PvPMode pvpMode)
2718  {
2719  teamCount = 2;
2720 
2721  // In Player Preference mode, team assignments are handled only at this point, and in Player Choice mode,
2722  // everyone should already have chosen a team, ie. players can no longer make choices now and we should
2723  // finalize all the team assignments without further delay.
2724  RefreshPvpTeamAssignments(assignUnassignedNow: true, autoBalanceNow: true);
2725  }
2726  else
2727  {
2728  connectedClients.ForEach(c => c.TeamID = CharacterTeamType.Team1);
2729  }
2730 
2731  bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || missionMode.Missions.All(m => m.AllowRespawning);
2732  foreach (var mission in GameMain.GameSession.GameMode.Missions)
2733  {
2734  if (mission.Prefab.ForceRespawnMode.HasValue)
2735  {
2736  ServerSettings.RespawnMode = mission.Prefab.ForceRespawnMode.Value;
2737  }
2738  }
2739 
2740 
2741  List<Client> playingClients = GetPlayingClients();
2742  if (campaign != null)
2743  {
2744  if (campaign.Map == null)
2745  {
2746  throw new Exception("Campaign map was null.");
2747  }
2748  if (campaign.NextLevel == null)
2749  {
2750  string errorMsg = "Failed to start a campaign round (next level not set).";
2751  DebugConsole.ThrowError(errorMsg);
2752  GameAnalyticsManager.AddErrorEventOnce("GameServer.StartGame:InvalidCampaignState", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
2753  if (OwnerConnection != null)
2754  {
2755  SendDirectChatMessage(errorMsg, connectedClients.Find(c => c.Connection == OwnerConnection), ChatMessageType.Error);
2756  }
2757  yield return CoroutineStatus.Failure;
2758  }
2759 
2760  SendStartMessage(roundStartSeed, campaign.NextLevel.Seed, GameMain.GameSession, connectedClients, includesFinalize: false);
2761  GameMain.GameSession.StartRound(campaign.NextLevel, startOutpost: campaign.GetPredefinedStartOutpost(), mirrorLevel: campaign.MirrorLevel);
2762  SubmarineSwitchLoad = false;
2763  campaign.AssignClientCharacterInfos(connectedClients);
2764  Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage);
2765  Log("Submarine: " + GameMain.GameSession.SubmarineInfo.Name, ServerLog.MessageType.ServerMessage);
2766  Log("Level seed: " + campaign.NextLevel.Seed, ServerLog.MessageType.ServerMessage);
2767  }
2768  else
2769  {
2770  SendStartMessage(roundStartSeed, GameMain.NetLobbyScreen.LevelSeed, GameMain.GameSession, connectedClients, false);
2771  GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, ServerSettings.SelectedLevelDifficulty, forceBiome: ServerSettings.Biome);
2772  Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage);
2773  Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage);
2774  Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage);
2775  }
2776 
2777  foreach (Mission mission in GameMain.GameSession.Missions)
2778  {
2779  Log("Mission: " + mission.Prefab.Name.Value, ServerLog.MessageType.ServerMessage);
2780  }
2781 
2782  if (GameMain.GameSession.SubmarineInfo.IsFileCorrupted)
2783  {
2784  CoroutineManager.StopCoroutines(startGameCoroutine);
2785  initiatedStartGame = false;
2786  SendChatMessage(TextManager.FormatServerMessage($"SubLoadError~[subname]={GameMain.GameSession.SubmarineInfo.Name}"), ChatMessageType.Error);
2787  yield return CoroutineStatus.Failure;
2788  }
2789 
2790  bool isOutpost = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost;
2791  if (ServerSettings.RespawnMode != RespawnMode.BetweenRounds && missionAllowRespawn)
2792  {
2793  RespawnManager = new RespawnManager(this, ServerSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null);
2794  }
2795  if (campaign != null)
2796  {
2797  campaign.CargoManager.CreatePurchasedItems();
2798  //midround-joining clients need to be informed of pending/new hires at outposts
2799  if (isOutpost) { campaign.SendCrewState(); }
2800  }
2801 
2802  if (GameMain.GameSession.Missions.None(m => !m.Prefab.AllowOutpostNPCs))
2803  {
2804  Level.Loaded?.SpawnNPCs();
2805  }
2806  Level.Loaded?.SpawnCorpses();
2807  Level.Loaded?.PrepareBeaconStation();
2808  AutoItemPlacer.SpawnItems(campaign?.Settings.StartItemSet);
2809 
2810  CrewManager crewManager = GameMain.GameSession.CrewManager;
2811 
2812  bool hadBots = true;
2813 
2814  List<Character> team1Characters = new(),
2815  team2Characters = new();
2816 
2817  //assign jobs and spawnpoints separately for each team
2818  for (int n = 0; n < teamCount; n++)
2819  {
2820  var teamID = n == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2;
2821 
2822  Submarine teamSub = Submarine.MainSubs[n];
2823  if (teamSub != null)
2824  {
2825  teamSub.TeamID = teamID;
2826  foreach (Item item in Item.ItemList)
2827  {
2828  if (item.Submarine == null) { continue; }
2829  if (item.Submarine != teamSub && !teamSub.DockedTo.Contains(item.Submarine)) { continue; }
2830  foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
2831  {
2832  wifiComponent.TeamID = teamSub.TeamID;
2833  }
2834  }
2835  foreach (Submarine sub in teamSub.DockedTo)
2836  {
2837  if (sub.Info.Type != SubmarineType.Player) { continue; }
2838  sub.TeamID = teamID;
2839  }
2840  }
2841 
2842  //find the clients in this team
2843  List<Client> teamClients = teamCount == 1 ? new List<Client>(playingClients) : playingClients.FindAll(c => c.TeamID == teamID);
2845  {
2846  teamClients.RemoveAll(c => c.SpectateOnly);
2847  }
2848  //always allow the server owner to spectate even if it's disallowed in server settings
2849  teamClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly);
2850  // Clients with last character permanently dead spectate regardless of server settings
2851  teamClients.RemoveAll(c => c.CharacterInfo != null && c.CharacterInfo.PermanentlyDead);
2852 
2853  //if (!teamClients.Any() && n > 0) { continue; }
2854 
2855  AssignJobs(teamClients);
2856 
2857  List<CharacterInfo> characterInfos = new List<CharacterInfo>();
2858  foreach (Client client in teamClients)
2859  {
2860  client.NeedsMidRoundSync = false;
2861 
2862  client.PendingPositionUpdates.Clear();
2863  client.EntityEventLastSent.Clear();
2864  client.LastSentEntityEventID = 0;
2865  client.LastRecvEntityEventID = 0;
2866  client.UnreceivedEntityEventCount = 0;
2867 
2868  if (client.CharacterInfo == null)
2869  {
2870  client.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, client.Name);
2871  }
2872  characterInfos.Add(client.CharacterInfo);
2873  if (client.CharacterInfo.Job == null ||
2874  client.CharacterInfo.Job.Prefab != client.AssignedJob.Prefab ||
2875  //always recreate the job to reset the skills in non-campaign modes
2876  campaign == null)
2877  {
2878  client.CharacterInfo.Job = new Job(client.AssignedJob.Prefab, isPvP, Rand.RandSync.Unsynced, client.AssignedJob.Variant);
2879  }
2880  }
2881 
2882  List<CharacterInfo> bots = new List<CharacterInfo>();
2883  // do not load new bots if we already have them
2884  if (!crewManager.HasBots || campaign == null)
2885  {
2886  int botsToSpawn = ServerSettings.BotSpawnMode == BotSpawnMode.Fill ? ServerSettings.BotCount - characterInfos.Count : ServerSettings.BotCount;
2887  for (int i = 0; i < botsToSpawn; i++)
2888  {
2889  var botInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName)
2890  {
2891  TeamID = teamID
2892  };
2893  characterInfos.Add(botInfo);
2894  bots.Add(botInfo);
2895  }
2896 
2897  AssignBotJobs(bots, teamID, isPvP);
2898  foreach (CharacterInfo bot in bots)
2899  {
2900  crewManager.AddCharacterInfo(bot);
2901  }
2902 
2903  crewManager.HasBots = true;
2904  hadBots = false;
2905  }
2906 
2907  List<WayPoint> spawnWaypoints = null;
2908  List<WayPoint> mainSubWaypoints = teamSub != null ? WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[n]).ToList() : null;
2909  if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost())
2910  {
2911  spawnWaypoints = WayPoint.GetOutpostSpawnPoints(teamID);
2912  while (spawnWaypoints.Count > characterInfos.Count)
2913  {
2914  spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count));
2915  }
2916  while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count)
2917  {
2918  spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]);
2919  }
2920  }
2921  if (teamSub != null)
2922  {
2923  if (spawnWaypoints == null || !spawnWaypoints.Any())
2924  {
2925  spawnWaypoints = mainSubWaypoints;
2926  }
2927  Debug.Assert(spawnWaypoints.Count == mainSubWaypoints.Count);
2928  }
2929 
2930  for (int i = 0; i < teamClients.Count; i++)
2931  {
2932  //if there's a main sub waypoint available (= the spawnpoint the character would've spawned at, if they'd spawned in the main sub instead of the outpost),
2933  //give the job items based on that spawnpoint
2934  WayPoint jobItemSpawnPoint = mainSubWaypoints != null ? mainSubWaypoints[i] : spawnWaypoints[i];
2935 
2936  Character spawnedCharacter = Character.Create(teamClients[i].CharacterInfo, spawnWaypoints[i].WorldPosition, teamClients[i].CharacterInfo.Name, isRemotePlayer: true, hasAi: false);
2937  spawnedCharacter.AnimController.Frozen = true;
2938  spawnedCharacter.TeamID = teamID;
2939  teamClients[i].Character = spawnedCharacter;
2940  var characterData = campaign?.GetClientCharacterData(teamClients[i]);
2941  if (characterData == null)
2942  {
2943  spawnedCharacter.GiveJobItems(GameMain.GameSession.GameMode is PvPMode, jobItemSpawnPoint);
2944  if (campaign != null)
2945  {
2946  characterData = campaign.SetClientCharacterData(teamClients[i]);
2947  characterData.HasSpawned = true;
2948  }
2949  }
2950  else
2951  {
2952  if (!characterData.HasItemData && !characterData.CharacterInfo.StartItemsGiven)
2953  {
2954  //clients who've chosen to spawn with the respawn penalty can have CharacterData without inventory data
2955  spawnedCharacter.GiveJobItems(GameMain.GameSession.GameMode is PvPMode, jobItemSpawnPoint);
2956  }
2957  else
2958  {
2959  characterData.SpawnInventoryItems(spawnedCharacter, spawnedCharacter.Inventory);
2960  }
2961  characterData.ApplyHealthData(spawnedCharacter);
2962  characterData.ApplyOrderData(spawnedCharacter);
2963  characterData.ApplyWalletData(spawnedCharacter);
2964  spawnedCharacter.GiveIdCardTags(jobItemSpawnPoint);
2965  spawnedCharacter.LoadTalents();
2966  characterData.HasSpawned = true;
2967  }
2968  if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && spawnedCharacter.Info != null)
2969  {
2970  spawnedCharacter.Info.SetExperience(Math.Max(spawnedCharacter.Info.ExperiencePoints, mpCampaign.GetSavedExperiencePoints(teamClients[i])));
2971  mpCampaign.ClearSavedExperiencePoints(teamClients[i]);
2972 
2973  if (spawnedCharacter.Info.LastRewardDistribution.TryUnwrap(out int salary))
2974  {
2975  spawnedCharacter.Wallet.SetRewardDistribution(salary);
2976  }
2977  }
2978 
2979  spawnedCharacter.SetOwnerClient(teamClients[i]);
2980  AddCharacterToList(teamID, spawnedCharacter);
2981  }
2982 
2983  for (int i = teamClients.Count; i < teamClients.Count + bots.Count; i++)
2984  {
2985  WayPoint jobItemSpawnPoint = mainSubWaypoints != null ? mainSubWaypoints[i] : spawnWaypoints[i];
2986  Character spawnedCharacter = Character.Create(characterInfos[i], spawnWaypoints[i].WorldPosition, characterInfos[i].Name, isRemotePlayer: false, hasAi: true);
2987  spawnedCharacter.TeamID = teamID;
2988  spawnedCharacter.GiveJobItems(GameMain.GameSession.GameMode is PvPMode, jobItemSpawnPoint);
2989  spawnedCharacter.GiveIdCardTags(jobItemSpawnPoint);
2990  spawnedCharacter.Info.InventoryData = new XElement("inventory");
2991  spawnedCharacter.Info.StartItemsGiven = true;
2992  spawnedCharacter.SaveInventory();
2993  spawnedCharacter.LoadTalents();
2994  AddCharacterToList(teamID, spawnedCharacter);
2995  }
2996 
2997  void AddCharacterToList(CharacterTeamType team, Character character)
2998  {
2999  switch (team)
3000  {
3001  case CharacterTeamType.Team1:
3002  team1Characters.Add(character);
3003  break;
3004  case CharacterTeamType.Team2:
3005  team2Characters.Add(character);
3006  break;
3007  }
3008  }
3009  }
3010 
3011  if (campaign != null && crewManager.HasBots)
3012  {
3013  if (hadBots)
3014  {
3015  //loaded existing bots -> init them
3016  crewManager.InitRound();
3017  }
3018  else
3019  {
3020  //created new bots -> save them
3021  SaveUtil.SaveGame(GameMain.GameSession.DataPath);
3022  }
3023  }
3024 
3025  campaign?.LoadPets();
3026  campaign?.LoadActiveOrders();
3027 
3028  campaign?.CargoManager.InitPurchasedIDCards();
3029 
3030  if (campaign == null || !initialSuppliesSpawned)
3031  {
3032  foreach (Submarine sub in Submarine.MainSubs)
3033  {
3034  if (sub == null) { continue; }
3035  List<PurchasedItem> spawnList = new List<PurchasedItem>();
3036  foreach (KeyValuePair<ItemPrefab, int> kvp in ServerSettings.ExtraCargo)
3037  {
3038  spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value, buyer: null));
3039  }
3040  CargoManager.DeliverItemsToSub(spawnList, sub, cargoManager: null);
3041  }
3042  }
3043 
3044  TraitorManager.Initialize(GameMain.GameSession.EventManager, Level.Loaded);
3045  if (GameMain.LuaCs.Game.overrideTraitors)
3046  {
3047  TraitorManager.Enabled = false;
3048  }
3049  else
3050  {
3051  TraitorManager.Enabled = Rand.Range(0.0f, 1.0f) < ServerSettings.TraitorProbability;
3052  }
3053 
3054  GameAnalyticsManager.AddDesignEvent("Traitors:" + (TraitorManager == null ? "Disabled" : "Enabled"));
3055 
3056  perkCollection.ApplyAll(team1Characters, team2Characters);
3057 
3058  yield return CoroutineStatus.Running;
3059 
3060  Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false);
3061 
3062  GameMain.GameScreen.Select();
3063 
3064  Log("Round started.", ServerLog.MessageType.ServerMessage);
3065 
3066  GameStarted = true;
3067  initiatedStartGame = false;
3068  GameMain.ResetFrameTime();
3069 
3070  LastClientListUpdateID++;
3071 
3072  roundStartTime = DateTime.Now;
3073 
3074  GameMain.LuaCs.Hook.Call("roundStart");
3075 
3076  startGameCoroutine = null;
3077  yield return CoroutineStatus.Success;
3078  }
3079 
3080  private void SendStartMessage(int seed, string levelSeed, GameSession gameSession, List<Client> clients, bool includesFinalize)
3081  {
3082  foreach (Client client in clients)
3083  {
3084  SendStartMessage(seed, levelSeed, gameSession, client, includesFinalize);
3085  }
3086  }
3087 
3088  private void SendStartMessage(int seed, string levelSeed, GameSession gameSession, Client client, bool includesFinalize)
3089  {
3090  IWriteMessage msg = new WriteOnlyMessage();
3091  msg.WriteByte((byte)ServerPacketHeader.STARTGAME);
3092  msg.WriteInt32(seed);
3093  msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier);
3094  bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawning);
3095  msg.WriteBoolean(ServerSettings.RespawnMode != RespawnMode.BetweenRounds && missionAllowRespawn);
3105  msg.WriteBoolean(IsUsingRespawnShuttle());
3106  msg.WriteByte((byte)ServerSettings.LosMode);
3108  msg.WriteBoolean(includesFinalize); msg.WritePadBits();
3109 
3111 
3112  if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign)
3113  {
3114  msg.WriteString(levelSeed);
3116  msg.WriteString(gameSession.SubmarineInfo.Name);
3117  msg.WriteString(gameSession.SubmarineInfo.MD5Hash.StringRepresentation);
3118  var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ?
3119  RespawnManager.RespawnShuttles.First().Info : GameMain.NetLobbyScreen.SelectedShuttle;
3120  msg.WriteString(selectedShuttle.Name);
3121  msg.WriteString(selectedShuttle.MD5Hash.StringRepresentation);
3122 
3123  if (gameSession.EnemySubmarineInfo is { } enemySub)
3124  {
3125  msg.WriteBoolean(true);
3126  msg.WriteString(enemySub.Name);
3127  msg.WriteString(enemySub.MD5Hash.StringRepresentation);
3128  }
3129  else
3130  {
3131  msg.WriteBoolean(false);
3132  }
3133 
3134  msg.WriteByte((byte)GameMain.GameSession.GameMode.Missions.Count());
3135  foreach (Mission mission in GameMain.GameSession.GameMode.Missions)
3136  {
3137  msg.WriteUInt32(mission.Prefab.UintIdentifier);
3138  }
3139  }
3140  else
3141  {
3142  int nextLocationIndex = campaign.Map.Locations.FindIndex(l => l.LevelData == campaign.NextLevel);
3143  int nextConnectionIndex = campaign.Map.Connections.FindIndex(c => c.LevelData == campaign.NextLevel);
3144  msg.WriteByte(campaign.CampaignID);
3145  msg.WriteUInt16(campaign.LastSaveID);
3146  msg.WriteInt32(nextLocationIndex);
3147  msg.WriteInt32(nextConnectionIndex);
3148  msg.WriteInt32(campaign.Map.SelectedLocationIndex);
3149  msg.WriteBoolean(campaign.MirrorLevel);
3150  }
3151 
3152  if (includesFinalize)
3153  {
3154  WriteRoundStartFinalize(msg, client);
3155  }
3156 
3157  serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
3158  }
3159 
3160  private bool TrySendCampaignSetupInfo(Client client)
3161  {
3162  if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; }
3163 
3164  using (dosProtection.Pause(client))
3165  {
3166  const int MaxSaves = 255;
3167  var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false);
3168  IWriteMessage msg = new WriteOnlyMessage();
3169  msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO);
3170  msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves));
3171  for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++)
3172  {
3173  msg.WriteNetSerializableStruct(saveInfos[i]);
3174  }
3175  serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
3176  }
3177 
3178  return true;
3179  }
3180 
3181  private bool IsUsingRespawnShuttle()
3182  {
3183  return ServerSettings.UseRespawnShuttle || (GameStarted && RespawnManager != null && RespawnManager.UsingShuttle);
3184  }
3185 
3186  private void SendRoundStartFinalize(Client client)
3187  {
3188  IWriteMessage msg = new WriteOnlyMessage();
3189  msg.WriteByte((byte)ServerPacketHeader.STARTGAMEFINALIZE);
3190  WriteRoundStartFinalize(msg, client);
3191  serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
3192  }
3193 
3194  private void WriteRoundStartFinalize(IWriteMessage msg, Client client)
3195  {
3196  //tell the client what content files they should preload
3197  var contentToPreload = GameMain.GameSession.EventManager.GetFilesToPreload();
3198  msg.WriteUInt16((ushort)contentToPreload.Count());
3199  foreach (ContentFile contentFile in contentToPreload)
3200  {
3201  msg.WriteString(contentFile.Path.Value);
3202  }
3203  msg.WriteInt32(Submarine.MainSub?.Info.EqualityCheckVal ?? 0);
3204  msg.WriteByte((byte)GameMain.GameSession.Missions.Count());
3205  foreach (Mission mission in GameMain.GameSession.Missions)
3206  {
3207  msg.WriteIdentifier(mission.Prefab.Identifier);
3208  }
3209  foreach (Level.LevelGenStage stage in Enum.GetValues(typeof(Level.LevelGenStage)).OfType<Level.LevelGenStage>().OrderBy(s => s))
3210  {
3211  msg.WriteInt32(GameMain.GameSession.Level.EqualityCheckValues[stage]);
3212  }
3213  foreach (Mission mission in GameMain.GameSession.Missions)
3214  {
3215  mission.ServerWriteInitial(msg, client);
3216  }
3217  msg.WriteBoolean(GameMain.GameSession.CrewManager != null);
3218  GameMain.GameSession.CrewManager?.ServerWriteActiveOrders(msg);
3219 
3220  msg.WriteBoolean(GameSession.ShouldApplyDisembarkPoints(GameMain.GameSession.GameMode?.Preset));
3221  }
3222 
3223  public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false, IEnumerable<Mission> missions = null)
3224  {
3225  if (GameStarted)
3226  {
3227  if (GameSettings.CurrentConfig.VerboseLogging)
3228  {
3229  Log("Ending the round...\n" + Environment.StackTrace.CleanupStackTrace(), ServerLog.MessageType.ServerMessage);
3230 
3231  }
3232  else
3233  {
3234  Log("Ending the round...", ServerLog.MessageType.ServerMessage);
3235  }
3236  }
3237 
3238  string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded");
3239  missions ??= GameMain.GameSession.Missions.ToList();
3240  if (GameMain.GameSession is { IsRunning: true })
3241  {
3242  GameMain.GameSession.EndRound(endMessage);
3243  }
3244  TraitorManager.TraitorResults? traitorResults = traitorManager?.GetEndResults() ?? null;
3245  var result = GameMain.LuaCs.Hook.Call<List<object>>("roundEnd");
3246  if (result != null)
3247  {
3248  foreach (var data in result)
3249  {
3250  if (data is TraitorManager.TraitorResults traitorResultData) { traitorResults = traitorResultData; }
3251  if (data is string endMessageData) { endMessage = endMessageData; }
3252  }
3253  }
3254 
3255  EndRoundTimer = 0.0f;
3256 
3258  {
3260  //send a netlobby update to get the clients' autorestart timers up to date
3262  }
3263 
3265 
3266  GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
3267 
3268  entityEventManager.Clear();
3269  foreach (Client c in connectedClients)
3270  {
3271  c.EntityEventLastSent.Clear();
3272  c.PendingPositionUpdates.Clear();
3273  c.PositionUpdateLastSent.Clear();
3274  }
3275 
3276  if (GameStarted)
3277  {
3279  }
3280 
3281  RespawnManager = null;
3282  GameStarted = false;
3283 
3284  if (connectedClients.Count > 0)
3285  {
3286  IWriteMessage msg = new WriteOnlyMessage();
3287  msg.WriteByte((byte)ServerPacketHeader.ENDGAME);
3288  msg.WriteByte((byte)transitionType);
3289  msg.WriteBoolean(wasSaved);
3290  msg.WriteString(endMessage);
3291  msg.WriteByte((byte)missions.Count());
3292  foreach (Mission mission in missions)
3293  {
3294  msg.WriteBoolean(mission.Completed);
3295  }
3296  msg.WriteByte(GameMain.GameSession?.WinningTeam == null ? (byte)0 : (byte)GameMain.GameSession.WinningTeam);
3297 
3298  msg.WriteBoolean(traitorResults.HasValue);
3299  if (traitorResults.HasValue)
3300  {
3301  msg.WriteNetSerializableStruct(traitorResults.Value);
3302  }
3303 
3304  foreach (Client client in connectedClients)
3305  {
3306  serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
3307  client.Character?.Info?.ClearCurrentOrders();
3308  client.Character = null;
3309  client.HasSpawned = false;
3310  client.InGame = false;
3311  client.WaitForNextRoundRespawn = null;
3312  }
3313  }
3314 
3315  entityEventManager.Clear();
3316  Submarine.Unload();
3318  Log("Round ended.", ServerLog.MessageType.ServerMessage);
3319 
3321  }
3322 
3323  public override void AddChatMessage(ChatMessage message)
3324  {
3325  if (string.IsNullOrEmpty(message.Text)) { return; }
3326  string logMsg;
3327  if (message.SenderClient != null)
3328  {
3329  logMsg = GameServer.ClientLogName(message.SenderClient) + ": " + message.TranslatedText;
3330  }
3331  else
3332  {
3333  logMsg = message.TextWithSender;
3334  }
3335 
3336  if (message.Sender is Character sender)
3337  {
3338  sender.TextChatVolume = 1f;
3339  }
3340 
3341  Log(logMsg, ServerLog.MessageType.Chat);
3342  }
3343 
3344  private bool ReadClientNameChange(Client c, IReadMessage inc)
3345  {
3346  UInt16 nameId = inc.ReadUInt16();
3347  string newName = inc.ReadString();
3348  Identifier newJob = inc.ReadIdentifier();
3349  CharacterTeamType newTeam = (CharacterTeamType)inc.ReadByte();
3350 
3351  if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameId)) { return false; }
3352 
3353  var timeSinceNameChange = DateTime.Now - c.LastNameChangeTime;
3354  if (timeSinceNameChange < Client.NameChangeCoolDown && newName != c.Name)
3355  {
3356  //only send once per second at most to prevent using this for spamming
3357  if (timeSinceNameChange.TotalSeconds > 1)
3358  {
3359  var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange;
3360  SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c);
3361  LastClientListUpdateID++;
3362  }
3363  c.NameId = nameId;
3364  c.RejectedName = newName;
3365  return false;
3366  }
3367 
3368  if (!newJob.IsEmpty)
3369  {
3370  if (!JobPrefab.Prefabs.TryGet(newJob, out JobPrefab newJobPrefab) || newJobPrefab.HiddenJob)
3371  {
3372  newJob = Identifier.Empty;
3373  }
3374  }
3375  c.NameId = nameId;
3376  if (newName == c.Name && newJob == c.PreferredJob && newTeam == c.PreferredTeam) { return false; }
3377 
3378  var result = GameMain.LuaCs.Hook.Call<bool?>("tryChangeClientName", c, newName, newJob, newTeam);
3379 
3380  if (result != null)
3381  {
3382  LastClientListUpdateID++;
3383  return result.Value;
3384  }
3385 
3386  c.PreferredJob = newJob;
3387 
3388  if (newTeam != c.PreferredTeam)
3389  {
3390  c.PreferredTeam = newTeam;
3391  RefreshPvpTeamAssignments();
3392  }
3393 
3394  return TryChangeClientName(c, newName);
3395  }
3396 
3397  public bool TryChangeClientName(Client c, string newName)
3398  {
3399  newName = Client.SanitizeName(newName);
3400  if (newName != c.Name && !string.IsNullOrEmpty(newName) && IsNameValid(c, newName))
3401  {
3402  c.LastNameChangeTime = DateTime.Now;
3403  string oldName = c.Name;
3404  c.Name = newName;
3405  c.RejectedName = string.Empty;
3406  SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server);
3407  LastClientListUpdateID++;
3408  return true;
3409  }
3410  else
3411  {
3412  //update client list even if the name cannot be changed to the one sent by the client,
3413  //so the client will be informed what their actual name is
3414  LastClientListUpdateID++;
3415  return false;
3416  }
3417  }
3418 
3419  public bool IsNameValid(Client c, string newName)
3420  {
3421  if (c.Connection != OwnerConnection)
3422  {
3423  if (!Client.IsValidName(newName, ServerSettings))
3424  {
3425  SendDirectChatMessage($"ServerMessage.NameChangeFailedSymbols~[newname]={newName}", c, ChatMessageType.ServerMessageBox);
3426  return false;
3427  }
3428  if (Homoglyphs.Compare(newName.ToLower(), ServerName.ToLower()))
3429  {
3430  SendDirectChatMessage($"ServerMessage.NameChangeFailedServerTooSimilar~[newname]={newName}", c, ChatMessageType.ServerMessageBox);
3431  return false;
3432  }
3433 
3434  if (c.KickVoteCount > 0)
3435  {
3436  SendDirectChatMessage($"ServerMessage.NameChangeFailedVoteKick~[newname]={newName}", c, ChatMessageType.ServerMessageBox);
3437  return false;
3438  }
3439  }
3440 
3441  Client nameTakenByClient = ConnectedClients.Find(c2 => c != c2 && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower()));
3442  if (nameTakenByClient != null)
3443  {
3444  SendDirectChatMessage($"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTakenByClient.Name}", c, ChatMessageType.ServerMessageBox);
3445  return false;
3446  }
3447 
3448  Character nameTakenByCharacter =
3449  GameSession.GetSessionCrewCharacters(CharacterType.Both).FirstOrDefault(c2 => c2 != c.Character && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower()));
3450  if (nameTakenByCharacter != null)
3451  {
3452  SendDirectChatMessage($"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTakenByCharacter.Name}", c, ChatMessageType.ServerMessageBox);
3453  return false;
3454  }
3455  return true;
3456  }
3457 
3458  public override void KickPlayer(string playerName, string reason)
3459  {
3460  Client client = connectedClients.Find(c =>
3461  c.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase) ||
3462  (c.Character != null && c.Character.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)));
3463 
3464  KickClient(client, reason);
3465  }
3466 
3467  public void KickClient(NetworkConnection conn, string reason)
3468  {
3469  if (conn == OwnerConnection) return;
3470 
3471  Client client = connectedClients.Find(c => c.Connection == conn);
3472  KickClient(client, reason);
3473  }
3474 
3475  public void KickClient(Client client, string reason, bool resetKarma = false)
3476  {
3477  if (client == null || client.Connection == OwnerConnection) { return; }
3478 
3479  if (resetKarma)
3480  {
3481  var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client));
3482  if (previousPlayer != null)
3483  {
3484  previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f);
3485  }
3486  client.Karma = Math.Max(client.Karma, 50.0f);
3487  }
3488 
3489  DisconnectClient(client, PeerDisconnectPacket.Kicked(reason));
3490  }
3491 
3492  public override void BanPlayer(string playerName, string reason, TimeSpan? duration = null)
3493  {
3494  Client client = connectedClients.Find(c =>
3495  c.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase) ||
3496  (c.Character != null && c.Character.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)));
3497 
3498  if (client == null)
3499  {
3500  DebugConsole.ThrowError("Client \"" + playerName + "\" not found.");
3501  return;
3502  }
3503 
3504  BanClient(client, reason, duration);
3505  }
3506 
3507  public void BanClient(Client client, string reason, TimeSpan? duration = null)
3508  {
3509  if (client == null || client.Connection == OwnerConnection) { return; }
3510 
3511  var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client));
3512  if (previousPlayer != null)
3513  {
3514  //reset karma to a neutral value, so if/when the ban is revoked the client wont get immediately punished by low karma again
3515  previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f);
3516  }
3517  client.Karma = Math.Max(client.Karma, 50.0f);
3518 
3519  DisconnectClient(client, PeerDisconnectPacket.Banned(reason));
3520 
3521  if (client.AccountInfo.AccountId.TryUnwrap(out var accountId))
3522  {
3523  ServerSettings.BanList.BanPlayer(client.Name, accountId, reason, duration);
3524  }
3525  else
3526  {
3527  ServerSettings.BanList.BanPlayer(client.Name, client.Connection.Endpoint, reason, duration);
3528  }
3529  foreach (var relatedId in client.AccountInfo.OtherMatchingIds)
3530  {
3531  ServerSettings.BanList.BanPlayer(client.Name, relatedId, reason, duration);
3532  }
3533  }
3534 
3535  public void BanPreviousPlayer(PreviousPlayer previousPlayer, string reason, TimeSpan? duration = null)
3536  {
3537  if (previousPlayer == null) { return; }
3538 
3539  //reset karma to a neutral value, so if/when the ban is revoked the client wont get immediately punished by low karma again
3540  previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f);
3541 
3542  ServerSettings.BanList.BanPlayer(previousPlayer.Name, previousPlayer.Address, reason, duration);
3543  if (previousPlayer.AccountInfo.AccountId.TryUnwrap(out var accountId))
3544  {
3545  ServerSettings.BanList.BanPlayer(previousPlayer.Name, accountId, reason, duration);
3546  }
3547  foreach (var relatedId in previousPlayer.AccountInfo.OtherMatchingIds)
3548  {
3549  ServerSettings.BanList.BanPlayer(previousPlayer.Name, relatedId, reason, duration);
3550  }
3551 
3552  string msg = $"ServerMessage.BannedFromServer~[client]={previousPlayer.Name}";
3553  if (!string.IsNullOrWhiteSpace(reason))
3554  {
3555  msg += $"/ /ServerMessage.Reason/: /{reason}";
3556  }
3557  SendChatMessage(msg, ChatMessageType.Server, changeType: PlayerConnectionChangeType.Banned);
3558  }
3559 
3560  public override void UnbanPlayer(string playerName)
3561  {
3562  BannedPlayer bannedPlayer
3563  = ServerSettings.BanList.BannedPlayers.FirstOrDefault(bp => bp.Name == playerName);
3564  if (bannedPlayer is null) { return; }
3566  }
3567 
3568  public override void UnbanPlayer(Endpoint endpoint)
3569  {
3571  }
3572 
3573  public void DisconnectClient(NetworkConnection senderConnection, PeerDisconnectPacket peerDisconnectPacket)
3574  {
3575  Client client = connectedClients.Find(x => x.Connection == senderConnection);
3576  if (client == null) { return; }
3577 
3578  DisconnectClient(client, peerDisconnectPacket);
3579  }
3580 
3581  public void DisconnectClient(Client client, PeerDisconnectPacket peerDisconnectPacket)
3582  {
3583  if (client == null) return;
3584 
3585  GameMain.LuaCs.Hook.Call("client.disconnected", client);
3586 
3587  if (client.Character != null)
3588  {
3589  client.Character.ClientDisconnected = true;
3590  client.Character.ClearInputs();
3591  }
3592 
3593  client.Character = null;
3594  client.HasSpawned = false;
3595  client.WaitForNextRoundRespawn = null;
3596  client.InGame = false;
3597 
3598  var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client));
3599  if (previousPlayer == null)
3600  {
3601  previousPlayer = new PreviousPlayer(client);
3602  previousPlayers.Add(previousPlayer);
3603  }
3604 
3605  if (peerDisconnectPacket.ShouldAttemptReconnect)
3606  {
3607  lock (clientsAttemptingToReconnectSoon)
3608  {
3610  clientsAttemptingToReconnectSoon.Add(client);
3611  }
3612  }
3613 
3614  previousPlayer.Name = client.Name;
3615  previousPlayer.Karma = client.Karma;
3616  previousPlayer.KarmaKickCount = client.KarmaKickCount;
3617  previousPlayer.KickVoters.Clear();
3618  foreach (Client c in connectedClients)
3619  {
3620  if (client.HasKickVoteFrom(c)) { previousPlayer.KickVoters.Add(c); }
3621  }
3622 
3623  client.Dispose();
3624  connectedClients.Remove(client);
3625  serverPeer.Disconnect(client.Connection, peerDisconnectPacket);
3626 
3628 
3629  // A player disconnecting might impact PvP team assignments if still in the lobby
3630  if (!GameStarted)
3631  {
3632  RefreshPvpTeamAssignments();
3633  }
3634 
3635  UpdateVoteStatus();
3636 
3637  SendChatMessage(peerDisconnectPacket.ChatMessage(client).Value, ChatMessageType.Server, changeType: peerDisconnectPacket.ConnectionChangeType);
3638 
3639  UpdateCrewFrame();
3640 
3642  refreshMasterTimer = DateTime.Now;
3643  }
3644 
3645  private void UpdateCrewFrame()
3646  {
3647  foreach (Client c in connectedClients)
3648  {
3649  if (c.Character == null || !c.InGame) continue;
3650  }
3651  }
3652 
3653  public void SendDirectChatMessage(string txt, Client recipient, ChatMessageType messageType = ChatMessageType.Server)
3654  {
3655  ChatMessage msg = ChatMessage.Create("", txt, messageType, null);
3656  SendDirectChatMessage(msg, recipient);
3657  }
3658 
3659  public void SendConsoleMessage(string txt, Client recipient, Color? color = null)
3660  {
3661  ChatMessage msg = ChatMessage.Create("", txt, ChatMessageType.Console, sender: null, textColor: color);
3662  SendDirectChatMessage(msg, recipient);
3663  }
3664 
3665  public void SendDirectChatMessage(ChatMessage msg, Client recipient)
3666  {
3667  if (recipient == null)
3668  {
3669  string errorMsg = "Attempted to send a chat message to a null client.\n" + Environment.StackTrace.CleanupStackTrace();
3670  DebugConsole.ThrowError(errorMsg);
3671  GameAnalyticsManager.AddErrorEventOnce("GameServer.SendDirectChatMessage:ClientNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
3672  return;
3673  }
3674 
3675  msg.NetStateID = recipient.ChatMsgQueue.Count > 0 ?
3676  (ushort)(recipient.ChatMsgQueue.Last().NetStateID + 1) :
3677  (ushort)(recipient.LastRecvChatMsgID + 1);
3678 
3679  recipient.ChatMsgQueue.Add(msg);
3680  recipient.LastChatMsgQueueID = msg.NetStateID;
3681  }
3682 
3686  public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, ChatMode chatMode = ChatMode.None)
3687  {
3688  string senderName = "";
3689 
3690  Client targetClient = null;
3691 
3692  if (type == null)
3693  {
3694  string command = ChatMessage.GetChatMessageCommand(message, out string tempStr);
3695  switch (command.ToLowerInvariant())
3696  {
3697  case "r":
3698  case "radio":
3699  type = ChatMessageType.Radio;
3700  break;
3701  case "d":
3702  case "dead":
3703  type = ChatMessageType.Dead;
3704  break;
3705  default:
3706  if (command != "")
3707  {
3708  if (command.ToLower() == ServerName.ToLower())
3709  {
3710  //a private message to the host
3711  if (OwnerConnection != null)
3712  {
3713  targetClient = connectedClients.Find(c => c.Connection == OwnerConnection);
3714  }
3715  }
3716  else
3717  {
3718  targetClient = connectedClients.Find(c =>
3719  command.ToLower() == c.Name.ToLower() ||
3720  command.ToLower() == c.Character?.Name?.ToLower());
3721 
3722  if (targetClient == null)
3723  {
3724  if (senderClient != null)
3725  {
3726  var chatMsg = ChatMessage.Create(
3727  "", $"ServerMessage.PlayerNotFound~[player]={command}",
3728  ChatMessageType.Error, null);
3729  SendDirectChatMessage(chatMsg, senderClient);
3730  }
3731  else
3732  {
3733  AddChatMessage($"ServerMessage.PlayerNotFound~[player]={command}", ChatMessageType.Error);
3734  }
3735 
3736  return;
3737  }
3738  }
3739 
3740  type = ChatMessageType.Private;
3741  }
3742  else if (chatMode == ChatMode.Radio)
3743  {
3744  type = ChatMessageType.Radio;
3745  }
3746  else
3747  {
3748  type = ChatMessageType.Default;
3749  }
3750  break;
3751  }
3752 
3753  message = tempStr;
3754  }
3755 
3756  if (GameStarted)
3757  {
3758  if (senderClient == null)
3759  {
3760  //msg sent by the server
3761  if (senderCharacter == null)
3762  {
3763  senderName = ServerName;
3764  }
3765  else //msg sent by an AI character
3766  {
3767  senderName = senderCharacter.Name;
3768  }
3769  }
3770  else //msg sent by a client
3771  {
3772  senderCharacter = senderClient.Character;
3773  senderName = senderCharacter == null ? senderClient.Name : senderCharacter.Name;
3774  if (type == ChatMessageType.Private)
3775  {
3776  if (senderCharacter != null && !senderCharacter.IsDead || targetClient.Character != null && !targetClient.Character.IsDead)
3777  {
3778  //sender or target has an alive character, sending private messages not allowed
3779  SendDirectChatMessage(ChatMessage.Create("", $"ServerMessage.PrivateMessagesNotAllowed", ChatMessageType.Error, null), senderClient);
3780  return;
3781  }
3782  }
3783  //sender doesn't have a character or the character can't speak -> only ChatMessageType.Dead allowed
3784  else if (senderCharacter == null || senderCharacter.IsDead || senderCharacter.SpeechImpediment >= 100.0f)
3785  {
3786  type = ChatMessageType.Dead;
3787  }
3788  }
3789  }
3790  else
3791  {
3792  if (senderClient == null)
3793  {
3794  //msg sent by the server
3795  if (senderCharacter == null)
3796  {
3797  senderName = ServerName;
3798  }
3799  else //sent by an AI character, not allowed when the game is not running
3800  {
3801  return;
3802  }
3803  }
3804  else //msg sent by a client
3805  {
3806  //game not started -> clients can only send normal, private, and team chatmessages
3807  if (type != ChatMessageType.Private && type != ChatMessageType.Team) type = ChatMessageType.Default;
3808  senderName = senderClient.Name;
3809  }
3810  }
3811 
3812  //check if the client is allowed to send the message
3813  WifiComponent senderRadio = null;
3814  switch (type)
3815  {
3816  case ChatMessageType.Radio:
3817  case ChatMessageType.Order:
3818  if (senderCharacter == null) { return; }
3819  if (!ChatMessage.CanUseRadio(senderCharacter, out senderRadio)) { return; }
3820  break;
3821  case ChatMessageType.Dead:
3822  //character still alive and capable of speaking -> dead chat not allowed
3823  if (senderClient != null && senderCharacter != null && !senderCharacter.IsDead && senderCharacter.SpeechImpediment < 100.0f)
3824  {
3825  return;
3826  }
3827  break;
3828  }
3829 
3830  if (type == ChatMessageType.Server || type == ChatMessageType.Error)
3831  {
3832  senderName = null;
3833  senderCharacter = null;
3834  }
3835  else if (type == ChatMessageType.Radio && !GameMain.LuaCs.Game.overrideSignalRadio)
3836  {
3837  //send to chat-linked wifi components
3838  Signal s = new Signal(message, sender: senderCharacter, source: senderRadio.Item);
3839  senderRadio.TransmitSignal(s, sentFromChat: true);
3840  }
3841 
3842  var hookChatMsg = ChatMessage.Create(senderName, message, (ChatMessageType)type, senderCharacter, senderClient, changeType);
3843 
3844  var should = GameMain.LuaCs.Hook.Call<bool?>("modifyChatMessage", hookChatMsg, senderRadio);
3845 
3846  if (should != null && should.Value)
3847  return;
3848 
3849 
3850  //check which clients can receive the message and apply distance effects
3851  foreach (Client client in ConnectedClients)
3852  {
3853  string modifiedMessage = message;
3854 
3855  switch (type)
3856  {
3857  case ChatMessageType.Default:
3858  case ChatMessageType.Radio:
3859  case ChatMessageType.Order:
3860  if (senderCharacter != null &&
3861  client.Character != null && !client.Character.IsDead)
3862  {
3863  if (senderCharacter != client.Character)
3864  {
3865  modifiedMessage = ChatMessage.ApplyDistanceEffect(message, (ChatMessageType)type, senderCharacter, client.Character);
3866  }
3867 
3868  //too far to hear the msg -> don't send
3869  if (string.IsNullOrWhiteSpace(modifiedMessage)) { continue; }
3870  }
3871  break;
3872  case ChatMessageType.Dead:
3873  //character still alive -> don't send
3874  if (client != senderClient && client.Character != null && !client.Character.IsDead) { continue; }
3875  break;
3876  case ChatMessageType.Private:
3877  //private msg sent to someone else than this client -> don't send
3878  if (client != targetClient && client != senderClient) { continue; }
3879  break;
3880  case ChatMessageType.Team:
3881  // No need to relay team messages at all to clients in opposing teams (or without a team)
3882  if (client.TeamID == CharacterTeamType.None || client.TeamID != senderClient.TeamID) { continue; }
3883  break;
3884  }
3885 
3886 
3887  var chatMsg = ChatMessage.Create(
3888  senderName,
3889  modifiedMessage,
3890  (ChatMessageType)type,
3891  senderCharacter,
3892  senderClient,
3893  changeType);
3894 
3895  SendDirectChatMessage(chatMsg, client);
3896  }
3897 
3898  if (type.Value != ChatMessageType.MessageBox)
3899  {
3900  string myReceivedMessage = type == ChatMessageType.Server || type == ChatMessageType.Error ? TextManager.GetServerMessage(message).Value : message;
3901  if (!string.IsNullOrWhiteSpace(myReceivedMessage))
3902  {
3903  AddChatMessage(myReceivedMessage, (ChatMessageType)type, senderName, senderClient, senderCharacter);
3904  }
3905  }
3906  }
3907 
3909  {
3910  if (message.SenderCharacter == null || message.SenderCharacter.SpeechImpediment >= 100.0f) { return; }
3911  //check which clients can receive the message and apply distance effects
3912  foreach (Client client in ConnectedClients)
3913  {
3914  if (message.SenderCharacter != null && client.Character != null && !client.Character.IsDead)
3915  {
3916  //too far to hear the msg -> don't send
3917  if (!client.Character.CanHearCharacter(message.SenderCharacter)) { continue; }
3918  }
3919  SendDirectChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client);
3920  }
3921  if (!string.IsNullOrWhiteSpace(message.Text))
3922  {
3923  AddChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder));
3924  if (ChatMessage.CanUseRadio(message.SenderCharacter, out var senderRadio))
3925  {
3926  //send to chat-linked wifi components
3927  Signal s = new Signal(message.Text, sender: message.SenderCharacter, source: senderRadio.Item);
3928  senderRadio.TransmitSignal(s, sentFromChat: true);
3929  }
3930  }
3931  }
3932 
3933  private void FileTransferChanged(FileSender.FileTransferOut transfer)
3934  {
3935  Client recipient = connectedClients.Find(c => c.Connection == transfer.Connection);
3936  if (transfer.FileType == FileTransferType.CampaignSave &&
3937  (transfer.Status == FileTransferStatus.Sending || transfer.Status == FileTransferStatus.Finished) &&
3938  recipient.LastCampaignSaveSendTime != default)
3939  {
3940  recipient.LastCampaignSaveSendTime.time = (float)NetTime.Now;
3941  }
3942  }
3943 
3945  {
3946  IWriteMessage msg = new WriteOnlyMessage();
3947  msg.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER);
3948  msg.WriteByte((byte)FileTransferMessageType.Cancel);
3949  msg.WriteByte((byte)transfer.ID);
3950  serverPeer.Send(msg, transfer.Connection, DeliveryMethod.Reliable);
3951  }
3952 
3953  public void UpdateVoteStatus(bool checkActiveVote = true)
3954  {
3955  if (connectedClients.Count == 0) { return; }
3956 
3957  if (checkActiveVote && Voting.ActiveVote != null)
3958  {
3959 #warning TODO: this is mostly the same as Voting.Update, deduplicate (if/when refactoring the Voting class?)
3960  var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame);
3961  if (inGameClients.Count() == 1 && inGameClients.First() == Voting.ActiveVote.VoteStarter)
3962  {
3963  Voting.ActiveVote.Finish(Voting, passed: true);
3964  }
3965  else if (inGameClients.Any())
3966  {
3967  var eligibleClients = inGameClients.Where(c => c != Voting.ActiveVote.VoteStarter);
3968  int yes = eligibleClients.Count(c => c.GetVote<int>(Voting.ActiveVote.VoteType) == 2);
3969  int no = eligibleClients.Count(c => c.GetVote<int>(Voting.ActiveVote.VoteType) == 1);
3970  int max = eligibleClients.Count();
3971  // Required ratio cannot be met
3972  if (no / (float)max > 1f - ServerSettings.VoteRequiredRatio)
3973  {
3974  Voting.ActiveVote.Finish(Voting, passed: false);
3975  }
3976  else if (yes / (float)max >= ServerSettings.VoteRequiredRatio)
3977  {
3978  Voting.ActiveVote.Finish(Voting, passed: true);
3979  }
3980  }
3981  }
3982 
3983  Client.UpdateKickVotes(connectedClients);
3984 
3985  var kickVoteEligibleClients = connectedClients.Where(c => (DateTime.Now - c.JoinTime).TotalSeconds > ServerSettings.DisallowKickVoteTime);
3986  float minimumKickVotes = Math.Max(2.0f, kickVoteEligibleClients.Count() * ServerSettings.KickVoteRequiredRatio);
3987  var clientsToKick = connectedClients.FindAll(c =>
3988  c.Connection != OwnerConnection &&
3989  !c.HasPermission(ClientPermissions.Kick) &&
3990  !c.HasPermission(ClientPermissions.Ban) &&
3991  !c.HasPermission(ClientPermissions.Unban) &&
3992  c.KickVoteCount >= minimumKickVotes);
3993  foreach (Client c in clientsToKick)
3994  {
3995  //reset the client's kick votes (they can rejoin after their ban expires)
3996  c.ResetVotes(resetKickVotes: true);
3997  previousPlayers.Where(p => p.MatchesClient(c)).ForEach(p => p.KickVoters.Clear());
3998  BanClient(c, "ServerMessage.KickedByVoteAutoBan", duration: TimeSpan.FromSeconds(ServerSettings.AutoBanTime));
3999  }
4000 
4001  //GameMain.NetLobbyScreen.LastUpdateID++;
4002 
4003  SendVoteStatus(connectedClients);
4004 
4005  int endVoteCount = ConnectedClients.Count(c => c.HasSpawned && c.GetVote<bool>(VoteType.EndRound));
4006  int endVoteMax = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned);
4007  if (ServerSettings.AllowEndVoting && endVoteMax > 0 &&
4008  ((float)endVoteCount / (float)endVoteMax) >= ServerSettings.EndVoteRequiredRatio)
4009  {
4010  Log("Ending round by votes (" + endVoteCount + "/" + (endVoteMax - endVoteCount) + ")", ServerLog.MessageType.ServerMessage);
4011  EndGame(wasSaved: false);
4012  }
4013  }
4014 
4015  public void SendVoteStatus(List<Client> recipients)
4016  {
4017  if (!recipients.Any()) { return; }
4018 
4019  IWriteMessage msg = new WriteOnlyMessage();
4020  msg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY);
4021  using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(msg))
4022  {
4023  segmentTable.StartNewSegment(ServerNetSegment.Vote);
4024  Voting.ServerWrite(msg);
4025  }
4026 
4027  foreach (var c in recipients)
4028  {
4029  serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable);
4030  }
4031  }
4032 
4033  public bool TrySwitchSubmarine()
4034  {
4035  if (Voting.ActiveVote is not Voting.SubmarineVote subVote) { return false; }
4036 
4037  SubmarineInfo targetSubmarine = subVote.Sub;
4038  VoteType voteType = Voting.ActiveVote.VoteType;
4039  Client starter = Voting.ActiveVote.VoteStarter;
4040 
4041  bool purchaseFailed = false;
4042  switch (voteType)
4043  {
4044  case VoteType.PurchaseAndSwitchSub:
4045  case VoteType.PurchaseSub:
4046  // Pay for submarine
4047  purchaseFailed = !GameMain.GameSession.TryPurchaseSubmarine(targetSubmarine, starter);
4048  break;
4049  case VoteType.SwitchSub:
4050  break;
4051  default:
4052  return false;
4053  }
4054 
4055  if (voteType != VoteType.PurchaseSub && !purchaseFailed)
4056  {
4057  GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, starter);
4058  }
4059 
4060  Voting.StopSubmarineVote(passed: !purchaseFailed);
4061  return !purchaseFailed;
4062  }
4063 
4064  public void UpdateClientPermissions(Client client)
4065  {
4066  if (client.AccountId.TryUnwrap(out var accountId))
4067  {
4068  ServerSettings.ClientPermissions.RemoveAll(scp => scp.AddressOrAccountId == accountId);
4069  if (client.Permissions != ClientPermissions.None)
4070  {
4072  client.Name,
4073  accountId,
4074  client.Permissions,
4075  client.PermittedConsoleCommands));
4076  }
4077  }
4078  else
4079  {
4080  ServerSettings.ClientPermissions.RemoveAll(scp => client.Connection.Endpoint.Address == scp.AddressOrAccountId);
4081  if (client.Permissions != ClientPermissions.None)
4082  {
4084  client.Name,
4085  client.Connection.Endpoint.Address,
4086  client.Permissions,
4087  client.PermittedConsoleCommands));
4088  }
4089  }
4090 
4091  foreach (Client recipient in connectedClients)
4092  {
4093  CoroutineManager.StartCoroutine(SendClientPermissionsAfterClientListSynced(recipient, client));
4094  }
4096  }
4097 
4098  private IEnumerable<CoroutineStatus> SendClientPermissionsAfterClientListSynced(Client recipient, Client client)
4099  {
4100  DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10);
4101  while (NetIdUtils.IdMoreRecent(LastClientListUpdateID, recipient.LastRecvClientListUpdate))
4102  {
4103  if (DateTime.Now > timeOut || GameMain.Server == null || !connectedClients.Contains(recipient))
4104  {
4105  yield return CoroutineStatus.Success;
4106  }
4107  yield return null;
4108  }
4109 
4110  SendClientPermissions(recipient, client);
4111  yield return CoroutineStatus.Success;
4112  }
4113 
4114  private void SendClientPermissions(Client recipient, Client client)
4115  {
4116  if (recipient?.Connection == null) { return; }
4117 
4118  IWriteMessage msg = new WriteOnlyMessage();
4119  msg.WriteByte((byte)ServerPacketHeader.PERMISSIONS);
4120  client.WritePermissions(msg);
4121  serverPeer.Send(msg, recipient.Connection, DeliveryMethod.Reliable);
4122  }
4123 
4124  public void GiveAchievement(Character character, Identifier achievementIdentifier)
4125  {
4126  foreach (Client client in connectedClients)
4127  {
4128  if (client.Character == character)
4129  {
4130  GiveAchievement(client, achievementIdentifier);
4131  return;
4132  }
4133  }
4134  }
4135 
4136  public void IncrementStat(Character character, AchievementStat stat, int amount)
4137  {
4138  foreach (Client client in connectedClients)
4139  {
4140  if (client.Character == character)
4141  {
4142  IncrementStat(client, stat, amount);
4143  return;
4144  }
4145  }
4146  }
4147 
4148  public void GiveAchievement(Client client, Identifier achievementIdentifier)
4149  {
4150  if (client.GivenAchievements.Contains(achievementIdentifier)) { return; }
4151  client.GivenAchievements.Add(achievementIdentifier);
4152 
4153  IWriteMessage msg = new WriteOnlyMessage();
4154  msg.WriteByte((byte)ServerPacketHeader.ACHIEVEMENT);
4155  msg.WriteIdentifier(achievementIdentifier);
4156 
4157  serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
4158  }
4159 
4160  public void IncrementStat(Client client, AchievementStat stat, int amount)
4161  {
4162  IWriteMessage msg = new WriteOnlyMessage();
4163  msg.WriteByte((byte)ServerPacketHeader.ACHIEVEMENT_STAT);
4164 
4165  INetSerializableStruct incrementedStat = new NetIncrementedStat(stat, amount);
4166  incrementedStat.Write(msg);
4167 
4168  serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
4169  }
4170 
4171  public void SendTraitorMessage(WriteOnlyMessage msg, Client client)
4172  {
4173  if (client == null) { return; };
4174  serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
4175  }
4176 
4177  public void UpdateCheatsEnabled()
4178  {
4179  if (!connectedClients.Any()) { return; }
4180 
4181  IWriteMessage msg = new WriteOnlyMessage();
4182  msg.WriteByte((byte)ServerPacketHeader.CHEATS_ENABLED);
4183  msg.WriteBoolean(DebugConsole.CheatsEnabled);
4184  msg.WritePadBits();
4185 
4186  foreach (Client c in connectedClients)
4187  {
4188  serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable);
4189  }
4190  }
4191 
4192  public void SetClientCharacter(Client client, Character newCharacter)
4193  {
4194  if (client == null) return;
4195 
4196  //the client's previous character is no longer a remote player
4197  if (client.Character != null)
4198  {
4199  client.Character.SetOwnerClient(null);
4200  }
4201 
4202  if (newCharacter == null)
4203  {
4204  if (client.Character != null) //removing control of the current character
4205  {
4206  CreateEntityEvent(client.Character, new Character.ControlEventData(null));
4207  client.Character = null;
4208  }
4209  }
4210  else //taking control of a new character
4211  {
4212  newCharacter.ClientDisconnected = false;
4213  newCharacter.KillDisconnectedTimer = 0.0f;
4214  newCharacter.ResetNetState();
4215  if (client.Character != null)
4216  {
4217  newCharacter.LastNetworkUpdateID = client.Character.LastNetworkUpdateID;
4218  }
4219 
4220  if (newCharacter.Info != null && newCharacter.Info.Character == null)
4221  {
4222  newCharacter.Info.Character = newCharacter;
4223  }
4224 
4225  newCharacter.SetOwnerClient(client);
4226  newCharacter.Enabled = true;
4227  client.Character = newCharacter;
4228  client.CharacterInfo = newCharacter.Info;
4229  CreateEntityEvent(newCharacter, new Character.ControlEventData(client));
4230  }
4231  }
4232 
4233  private readonly RateLimiter charInfoRateLimiter = new(
4234  maxRequests: 5,
4235  expiryInSeconds: 10,
4236  punishmentRules: new[]
4237  {
4238  (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce),
4239  (RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick)
4240  });
4241 
4242  private void UpdateCharacterInfo(IReadMessage message, Client sender)
4243  {
4244  bool spectateOnly = message.ReadBoolean();
4245  message.ReadPadBits();
4246 
4247  sender.SpectateOnly = spectateOnly && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection);
4248  if (sender.SpectateOnly) { return; }
4249 
4250  var netInfo = INetSerializableStruct.Read<NetCharacterInfo>(message);
4251 
4252  if (charInfoRateLimiter.IsLimitReached(sender)) { return; }
4253 
4254  string newName = netInfo.NewName;
4255  if (string.IsNullOrEmpty(newName))
4256  {
4257  newName = sender.Name;
4258  }
4259  else
4260  {
4261  newName = Client.SanitizeName(newName);
4262  if (!IsNameValid(sender, newName))
4263  {
4264  newName = sender.Name;
4265  }
4266  else
4267  {
4268  sender.PendingName = newName;
4269  }
4270  }
4271 
4272  // If a CharacterInfo for this Client already exists on the server, make sure it is used, and prevent the Client from replacing it
4273  var existingCampaignData = (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.GetClientCharacterData(sender);
4274  if (existingCampaignData != null)
4275  {
4276  sender.CharacterInfo = existingCampaignData.CharacterInfo;
4277  return;
4278  }
4279 
4280  sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName);
4281 
4282  sender.CharacterInfo.RecreateHead(
4283  tags: netInfo.Tags.ToImmutableHashSet(),
4284  hairIndex: netInfo.HairIndex,
4285  beardIndex: netInfo.BeardIndex,
4286  moustacheIndex: netInfo.MoustacheIndex,
4287  faceAttachmentIndex: netInfo.FaceAttachmentIndex);
4288 
4289  sender.CharacterInfo.Head.SkinColor = netInfo.SkinColor;
4290  sender.CharacterInfo.Head.HairColor = netInfo.HairColor;
4291  sender.CharacterInfo.Head.FacialHairColor = netInfo.FacialHairColor;
4292 
4293  if (netInfo.JobVariants.Length > 0)
4294  {
4295  List<JobVariant> variants = new List<JobVariant>();
4296  foreach (NetJobVariant jv in netInfo.JobVariants)
4297  {
4298  if (jv.ToJobVariant() is { } variant)
4299  {
4300  variants.Add(variant);
4301  }
4302  }
4303 
4304  sender.JobPreferences = variants;
4305  }
4306  }
4307 
4308  public readonly List<string> JobAssignmentDebugLog = new List<string>();
4309 
4310  public void AssignJobs(List<Client> unassigned)
4311  {
4312  JobAssignmentDebugLog.Clear();
4313 
4314  var jobList = JobPrefab.Prefabs.ToList();
4315  unassigned = new List<Client>(unassigned);
4316  unassigned = unassigned.OrderBy(sp => Rand.Int(int.MaxValue)).ToList();
4317 
4318  Dictionary<JobPrefab, int> assignedClientCount = new Dictionary<JobPrefab, int>();
4319  foreach (JobPrefab jp in jobList)
4320  {
4321  assignedClientCount.Add(jp, 0);
4322  }
4323 
4324  CharacterTeamType teamID = CharacterTeamType.None;
4325  if (unassigned.Count > 0) { teamID = unassigned[0].TeamID; }
4326 
4327  //if we're playing a multiplayer campaign, check which clients already have a character and a job
4328  //(characters are persistent in campaigns)
4329  if (GameMain.GameSession.GameMode is MultiPlayerCampaign multiplayerCampaign)
4330  {
4331  var campaignAssigned = multiplayerCampaign.GetAssignedJobs(connectedClients);
4332  //remove already assigned clients from unassigned
4333  unassigned.RemoveAll(u => campaignAssigned.ContainsKey(u));
4334  //add up to assigned client count
4335  foreach ((Client client, Job job) in campaignAssigned)
4336  {
4337  assignedClientCount[job.Prefab]++;
4338  client.AssignedJob = new JobVariant(job.Prefab, job.Variant);
4339  JobAssignmentDebugLog.Add($"Client {client.Name} has an existing campaign character, keeping the job {job.Name}.");
4340  }
4341  }
4342 
4343  //count the clients who already have characters with an assigned job
4344  foreach (Client c in connectedClients)
4345  {
4346  if (c.TeamID != teamID || unassigned.Contains(c)) { continue; }
4347  if (c.Character?.Info?.Job != null && !c.Character.IsDead)
4348  {
4349  assignedClientCount[c.Character.Info.Job.Prefab]++;
4350  }
4351  }
4352 
4353  //if any of the players has chosen a job that is Always Allowed, give them that job
4354  for (int i = unassigned.Count - 1; i >= 0; i--)
4355  {
4356  if (unassigned[i].JobPreferences.Count == 0) { continue; }
4357  if (!unassigned[i].JobPreferences.Any() || !unassigned[i].JobPreferences[0].Prefab.AllowAlways) { continue; }
4358  JobAssignmentDebugLog.Add($"Client {unassigned[i].Name} has {unassigned[i].JobPreferences[0].Prefab.Name} as their first preference, assigning it because the job is always allowed.");
4359  unassigned[i].AssignedJob = unassigned[i].JobPreferences[0];
4360  unassigned.RemoveAt(i);
4361  }
4362 
4363  // Assign the necessary jobs that are always required at least one, in vanilla this means in practice the captain
4364  bool unassignedJobsFound = true;
4365  while (unassignedJobsFound && unassigned.Any())
4366  {
4367  unassignedJobsFound = false;
4368 
4369  foreach (JobPrefab jobPrefab in jobList)
4370  {
4371  if (unassigned.Count == 0) { break; }
4372  if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) { continue; }
4373  // Find the client that wants the job the most, don't force any jobs yet, because it might be that we can meet the preference for other jobs.
4374  Client client = FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: false);
4375  if (client != null)
4376  {
4377  JobAssignmentDebugLog.Add($"At least {jobPrefab.MinNumber} {jobPrefab.Name} required. Assigning {client.Name} as a {jobPrefab.Name} (has the job in their preferences).");
4378  AssignJob(client, jobPrefab);
4379  }
4380  }
4381 
4382  if (unassigned.Any())
4383  {
4384  // Another pass, force required jobs that are not yet filled.
4385  foreach (JobPrefab jobPrefab in jobList)
4386  {
4387  if (unassigned.Count == 0) { break; }
4388  if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) { continue; }
4389  var client = FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: true);
4390  JobAssignmentDebugLog.Add(
4391  $"At least {jobPrefab.MinNumber} {jobPrefab.Name} required. "+
4392  $"A random client needs to be assigned because no one has the job in their preferences. Assigning {client.Name} as a {jobPrefab.Name}.");
4393  AssignJob(client, jobPrefab);
4394  }
4395  }
4396 
4397  void AssignJob(Client client, JobPrefab jobPrefab)
4398  {
4399  client.AssignedJob =
4400  client.JobPreferences.FirstOrDefault(jp => jp.Prefab == jobPrefab) ??
4401  new JobVariant(jobPrefab, Rand.Int(jobPrefab.Variants));
4402 
4403  assignedClientCount[jobPrefab]++;
4404  unassigned.Remove(client);
4405 
4406  //the job still needs more crew members, set unassignedJobsFound to true to keep the while loop running
4407  if (assignedClientCount[jobPrefab] < jobPrefab.MinNumber) { unassignedJobsFound = true; }
4408  }
4409  }
4410 
4411  // Attempt to give the clients a job they have in their job preferences.
4412  // First evaluate all the primary preferences, then all the secondary etc.
4413  for (int preferenceIndex = 0; preferenceIndex < 3; preferenceIndex++)
4414  {
4415  for (int i = unassigned.Count - 1; i >= 0; i--)
4416  {
4417  Client client = unassigned[i];
4418  if (preferenceIndex >= client.JobPreferences.Count) { continue; }
4419  var preferredJob = client.JobPreferences[preferenceIndex];
4420  JobPrefab jobPrefab = preferredJob.Prefab;
4421  if (assignedClientCount[jobPrefab] >= jobPrefab.MaxNumber)
4422  {
4423  JobAssignmentDebugLog.Add($"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Cannot assign, maximum number of the job has been reached.");
4424  continue;
4425  }
4426  if (client.Karma < jobPrefab.MinKarma)
4427  {
4428  JobAssignmentDebugLog.Add($"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Cannot assign, karma too low ({client.Karma} < {jobPrefab.MinKarma}).");
4429  continue;
4430  }
4431  JobAssignmentDebugLog.Add($"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Assigning {client.Name} as a {jobPrefab.Name}.");
4432  client.AssignedJob = preferredJob;
4433  assignedClientCount[jobPrefab]++;
4434  unassigned.RemoveAt(i);
4435  }
4436  }
4437 
4438  //give random jobs to rest of the clients
4439  foreach (Client c in unassigned)
4440  {
4441  //find all jobs that are still available
4442  var remainingJobs = jobList.FindAll(jp => !jp.HiddenJob && assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma);
4443 
4444  //all jobs taken, give a random job
4445  if (remainingJobs.Count == 0)
4446  {
4447  string errorMsg = $"Failed to assign a suitable job for \"{c.Name}\" (all jobs already have the maximum numbers of players). Assigning a random job...";
4448  DebugConsole.ThrowError(errorMsg);
4449  JobAssignmentDebugLog.Add(errorMsg);
4450  int jobIndex = Rand.Range(0, jobList.Count);
4451  int skips = 0;
4452  while (c.Karma < jobList[jobIndex].MinKarma)
4453  {
4454  jobIndex++;
4455  skips++;
4456  if (jobIndex >= jobList.Count) { jobIndex -= jobList.Count; }
4457  if (skips >= jobList.Count) { break; }
4458  }
4459  c.AssignedJob =
4460  c.JobPreferences.FirstOrDefault(jp => jp.Prefab == jobList[jobIndex]) ??
4461  new JobVariant(jobList[jobIndex], 0);
4462  assignedClientCount[c.AssignedJob.Prefab]++;
4463  }
4464  //if one of the client's preferences is still available, give them that job
4465  else if (c.JobPreferences.FirstOrDefault(jp => remainingJobs.Contains(jp.Prefab)) is { } remainingJob)
4466  {
4467  JobAssignmentDebugLog.Add(
4468  $"{c.Name} has {remainingJob.Prefab.Name} as their {c.JobPreferences.IndexOf(remainingJob) + 1}. preference, and it is still available."+
4469  $" Assigning {c.Name} as a {remainingJob.Prefab.Name}.");
4470  c.AssignedJob = remainingJob;
4471  assignedClientCount[remainingJob.Prefab]++;
4472  }
4473  else //none of the client's preferred jobs available, choose a random job
4474  {
4475  c.AssignedJob = new JobVariant(remainingJobs[Rand.Range(0, remainingJobs.Count)], 0);
4476  assignedClientCount[c.AssignedJob.Prefab]++;
4477  JobAssignmentDebugLog.Add(
4478  $"No suitable jobs available for {c.Name} (karma {c.Karma}). Assigning a random job: {c.AssignedJob.Prefab.Name}.");
4479  }
4480  }
4481 
4482  GameMain.LuaCs.Hook.Call("jobsAssigned", unassigned);
4483  }
4484 
4485  public void AssignBotJobs(List<CharacterInfo> bots, CharacterTeamType teamID, bool isPvP)
4486  {
4487  //shuffle first so the parts where we go through the prefabs
4488  //and find ones there's too few of don't always pick the same job
4489  List<JobPrefab> shuffledPrefabs = JobPrefab.Prefabs.Where(static jp => !jp.HiddenJob).ToList();
4490  shuffledPrefabs.Shuffle(Rand.RandSync.Unsynced);
4491 
4492  Dictionary<JobPrefab, int> assignedPlayerCount = new Dictionary<JobPrefab, int>();
4493  foreach (JobPrefab jp in shuffledPrefabs)
4494  {
4495  if (jp.HiddenJob) { continue; }
4496  assignedPlayerCount.Add(jp, 0);
4497  }
4498 
4499  //count the clients who already have characters with an assigned job
4500  foreach (Client c in connectedClients)
4501  {
4502  if (c.TeamID != teamID) continue;
4503  if (c.Character?.Info?.Job != null && !c.Character.IsDead)
4504  {
4505  assignedPlayerCount[c.Character.Info.Job.Prefab]++;
4506  }
4507  else if (c.CharacterInfo?.Job != null)
4508  {
4509  assignedPlayerCount[c.CharacterInfo?.Job.Prefab]++;
4510  }
4511  }
4512 
4513  List<CharacterInfo> unassignedBots = new List<CharacterInfo>(bots);
4514  while (unassignedBots.Count > 0)
4515  {
4516  //if there's any jobs left that must be included in the crew, assign those
4517  var jobsBelowMinNumber = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.MinNumber);
4518  if (jobsBelowMinNumber.Any())
4519  {
4520  AssignJob(unassignedBots[0], jobsBelowMinNumber.GetRandomUnsynced());
4521  }
4522  else
4523  {
4524  //if there's any jobs left that are below the normal number of bots initially in the crew, assign those
4525  var jobsBelowInitialCount = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.InitialCount);
4526  if (jobsBelowInitialCount.Any())
4527  {
4528  AssignJob(unassignedBots[0], jobsBelowInitialCount.GetRandomUnsynced());
4529  }
4530  else
4531  {
4532  //no "must-have-jobs" left, break and start assigning randomly
4533  break;
4534  }
4535  }
4536  }
4537 
4538  foreach (CharacterInfo c in unassignedBots.ToList())
4539  {
4540  //find all jobs that are still available
4541  var remainingJobs = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.MaxNumber);
4542  //all jobs taken, give a random job
4543  if (remainingJobs.None())
4544  {
4545  DebugConsole.ThrowError("Failed to assign a suitable job for bot \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job...");
4546  AssignJob(c, shuffledPrefabs.GetRandomUnsynced());
4547  }
4548  else
4549  {
4550  //some jobs still left, choose one of them by random (preferring ones there's the least of in the crew)
4551  var selectedJob = remainingJobs.GetRandomByWeight(jp => 1.0f / Math.Max(assignedPlayerCount[jp], 0.01f), Rand.RandSync.Unsynced);
4552  AssignJob(c, selectedJob);
4553  }
4554  }
4555 
4556  void AssignJob(CharacterInfo bot, JobPrefab job)
4557  {
4558  int variant = Rand.Range(0, job.Variants);
4559  bot.Job = new Job(job, isPvP, Rand.RandSync.Unsynced, variant);
4560  assignedPlayerCount[bot.Job.Prefab]++;
4561  unassignedBots.Remove(bot);
4562  }
4563  }
4564 
4565  private Client FindClientWithJobPreference(List<Client> clients, JobPrefab job, bool forceAssign = false)
4566  {
4567  int bestPreference = int.MaxValue;
4568  Client preferredClient = null;
4569  foreach (Client c in clients)
4570  {
4571  if (ServerSettings.KarmaEnabled && c.Karma < job.MinKarma) { continue; }
4572  int index = c.JobPreferences.IndexOf(c.JobPreferences.Find(j => j.Prefab == job));
4573  if (index > -1 && index < bestPreference)
4574  {
4575  bestPreference = index;
4576  preferredClient = c;
4577  }
4578  }
4579 
4580  //none of the clients wants the job, assign it to random client
4581  if (forceAssign && preferredClient == null)
4582  {
4583  preferredClient = clients[Rand.Int(clients.Count)];
4584  }
4585 
4586  return preferredClient;
4587  }
4588 
4589  public void UpdateMissionState(Mission mission)
4590  {
4591  foreach (var client in connectedClients)
4592  {
4593  IWriteMessage msg = new WriteOnlyMessage();
4594  msg.WriteByte((byte)ServerPacketHeader.MISSION);
4595  int missionIndex = GameMain.GameSession.GetMissionIndex(mission);
4596  msg.WriteByte((byte)(missionIndex == -1 ? 255: missionIndex));
4597  mission?.ServerWrite(msg);
4598  serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
4599  }
4600  }
4601 
4602  public static string CharacterLogName(Character character)
4603  {
4604  if (character == null) { return "[NULL]"; }
4605  Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
4606  return ClientLogName(client, character.LogName);
4607  }
4608 
4609  public static void Log(string line, ServerLog.MessageType messageType)
4610  {
4611  if (GameMain.Server == null || !GameMain.Server.ServerSettings.SaveServerLogs) { return; }
4612 
4613  GameMain.LuaCs?.Hook?.Call("serverLog", line, messageType);
4614 
4615  GameMain.Server.ServerSettings.ServerLog.WriteLine(line, messageType);
4616 
4617  foreach (Client client in GameMain.Server.ConnectedClients)
4618  {
4619  if (!client.HasPermission(ClientPermissions.ServerLog)) continue;
4620  //use sendername as the message type
4622  ChatMessage.Create(messageType.ToString(), line, ChatMessageType.ServerLog, null),
4623  client);
4624  }
4625  }
4626 
4627  public void Quit()
4628  {
4629  if (started)
4630  {
4631  started = false;
4632 
4634 
4637 
4639 
4640  ModSender?.Dispose();
4641 
4643  {
4644  Log("Shutting down the server...", ServerLog.MessageType.ServerMessage);
4646  }
4647 
4648  GameAnalyticsManager.AddDesignEvent("GameServer:ShutDown");
4649  serverPeer?.Close();
4650 
4651  SteamManager.CloseServer();
4652  }
4653  }
4654 
4655  private void UpdateClientLobbies()
4656  {
4657  // Triggers a call to WriteClientList(), which causes clients to call GameClient.ReadClientList()
4658  LastClientListUpdateID++;
4659  }
4660 
4661  private List<Client> GetPlayingClients()
4662  {
4663  List<Client> playingClients = new List<Client>(connectedClients);
4665  {
4666  playingClients.RemoveAll(static c => c.SpectateOnly);
4667  }
4668  // Always allow the server owner to spectate even if it's disallowed in server settings
4669  playingClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly);
4670  return playingClients;
4671  }
4672 
4678  public void RefreshPvpTeamAssignments(bool assignUnassignedNow = false, bool autoBalanceNow = false)
4679  {
4680  List<Client> team1 = new List<Client>();
4681  List<Client> team2 = new List<Client>();
4682  List<Client> playingClients = GetPlayingClients();
4683 
4684  // First assign clients with a team preference/choice into the teams they want (applies in both team selection modes)
4685  List<Client> unassignedClients = new List<Client>(playingClients);
4686  for (int i = 0; i < unassignedClients.Count; i++)
4687  {
4688  if (unassignedClients[i].PreferredTeam == CharacterTeamType.Team1 ||
4689  unassignedClients[i].PreferredTeam == CharacterTeamType.Team2)
4690  {
4691  assignTeam(unassignedClients[i], unassignedClients[i].PreferredTeam);
4692  i--;
4693  }
4694  }
4695 
4696  // Should unassigned players be forced into teams now? (eg. at round start when the time to make choices is over)
4697  if (assignUnassignedNow)
4698  {
4699  if (unassignedClients.Any())
4700  {
4701  SendChatMessage(TextManager.Get("PvP.WithoutTeamWillBeRandomlyAssigned").Value, ChatMessageType.Server);
4702  }
4703 
4704  // Assign to the team that has the least players
4705  while (unassignedClients.Any())
4706  {
4707  var randomClient = unassignedClients.GetRandom(Rand.RandSync.Unsynced);
4708  assignTeam(randomClient, team1.Count < team2.Count ? CharacterTeamType.Team1 : CharacterTeamType.Team2);
4709  }
4710  }
4711 
4713  {
4714  // Deal with team size balance as necessary
4715  int sizeDifference = Math.Abs(team1.Count - team2.Count);
4716  if (sizeDifference > ServerSettings.PvpAutoBalanceThreshold)
4717  {
4718  if (autoBalanceNow)
4719  {
4720  SendChatMessage(TextManager.Get("AutoBalance.Activating").Value, ChatMessageType.Server);
4721 
4722  // Assign a random player from the bigger team into the smaller team until the teams are no longer too imbalanced
4723  while (Math.Abs(team1.Count - team2.Count) > ServerSettings.PvpAutoBalanceThreshold)
4724  {
4725  // Note: team size difference never 0 at this point
4726  var biggerTeam = GetPlayingClients().Where(
4727  c => team1.Count > team2.Count ?
4728  c.TeamID == CharacterTeamType.Team1 :
4729  c.TeamID == CharacterTeamType.Team2)
4730  .ToList();
4731  switchTeam(biggerTeam.GetRandom(Rand.RandSync.Unsynced), team1.Count < team2.Count ? CharacterTeamType.Team1 : CharacterTeamType.Team2);
4732  }
4733  }
4734  else if (ServerSettings.PvpTeamSelectionMode != PvpTeamSelectionMode.PlayerPreference)
4735  {
4736  // Start a countdown (if not already running) to auto-balancing, so players have a chance to manually rebalance the team before that
4737  if (pvpAutoBalanceCountdownRemaining == -1)
4738  {
4739  SendChatMessage(TextManager.GetWithVariables(
4740  "AutoBalance.CountdownStarted",
4741  ("[teamname]", TextManager.Get(team1.Count > team2.Count ? "teampreference.team1" : "teampreference.team2")),
4742  ("[numberplayers]", (sizeDifference - ServerSettings.PvpAutoBalanceThreshold).ToString()),
4743  ("[numberseconds]", PvpAutoBalanceCountdown.ToString())
4744  ).Value, ChatMessageType.Server);
4745  pvpAutoBalanceCountdownRemaining = PvpAutoBalanceCountdown;
4746  }
4747  }
4748  }
4749  else
4750  {
4751  // Stop countdown if there was one
4752  StopAutoBalanceCountdown();
4753  }
4754  }
4755  else
4756  {
4757  // Stop countdown if there was one (eg. if the settings were changed during countdown)
4758  StopAutoBalanceCountdown();
4759  }
4760 
4761  // Finally, push the assignments to the clients
4762  UpdateClientLobbies();
4763 
4764  void assignTeam(Client client, CharacterTeamType newTeam)
4765  {
4766  client.TeamID = newTeam;
4767  unassignedClients.Remove(client);
4768  if (newTeam == CharacterTeamType.Team1)
4769  {
4770  team1.Add(client);
4771  }
4772  else if (newTeam == CharacterTeamType.Team2)
4773  {
4774  team2.Add(client);
4775  }
4776  }
4777 
4778  void switchTeam(Client client, CharacterTeamType newTeam)
4779  {
4780  string teamNameVariable = "";
4781  if (newTeam == CharacterTeamType.Team1)
4782  {
4783  team2.Remove(client);
4784  team1.Add(client);
4785  teamNameVariable = "teampreference.team1";
4786  }
4787  else if (newTeam == CharacterTeamType.Team2)
4788  {
4789  team1.Remove(client);
4790  team2.Add(client);
4791  teamNameVariable = "teampreference.team2";
4792  }
4793  SendChatMessage(TextManager.GetWithVariables(
4794  "AutoBalance.PlayerMoved",
4795  ("[clientname]", client.Name),
4796  ("[teamname]", TextManager.Get(teamNameVariable))
4797  ).Value, ChatMessageType.Server);
4798  client.TeamID = newTeam;
4799  client.PreferredTeam = newTeam;
4800  }
4801  }
4802 
4807  {
4808  if (client.PreferredTeam == CharacterTeamType.None)
4809  {
4810  // If teams are currently even, assign the preference-less new player into a random team
4811  if (Team1Count == Team2Count)
4812  {
4813  client.TeamID = Rand.Value() > 0.5f ? CharacterTeamType.Team1 : CharacterTeamType.Team2;
4814  }
4815  else // Otherwise, just assign them to the smaller team
4816  {
4817  client.TeamID = Team1Count < Team2Count ? CharacterTeamType.Team1 : CharacterTeamType.Team2;
4818  }
4819  }
4820  else if (ServerSettings.PvpAutoBalanceThreshold > 0) // Check if the player can be put into their preferred team
4821  {
4822  int newTeam1Count = Team1Count + (client.PreferredTeam == CharacterTeamType.Team1 ? 1 : 0);
4823  int newTeam2Count = Team2Count + (client.PreferredTeam == CharacterTeamType.Team2 ? 1 : 0);
4824 
4825  // Threshold won't be crossed by assigning the player to their preferred team, so do it
4826  if (Math.Abs(newTeam1Count - newTeam2Count) <= ServerSettings.PvpAutoBalanceThreshold)
4827  {
4828  client.TeamID = client.PreferredTeam;
4829  }
4830  else // Preferred team would go against balance threshold, assing the player to the smaller team
4831  {
4832  client.TeamID = Team1Count < Team2Count ? CharacterTeamType.Team1 : CharacterTeamType.Team2;
4833  }
4834  }
4835  else // Nothing stopping us from assigning the player into their preferred team
4836  {
4837  client.TeamID = client.PreferredTeam;
4838  }
4839  }
4840 
4841  private void StopAutoBalanceCountdown()
4842  {
4843  if (pvpAutoBalanceCountdownRemaining != -1)
4844  {
4845  SendChatMessage(TextManager.Get("AutoBalance.CountdownCancelled").Value, ChatMessageType.Server);
4846  }
4847  pvpAutoBalanceCountdownRemaining = -1;
4848  }
4849  }
4850 
4852  {
4853  public string Name;
4854  public Address Address;
4856  public float Karma;
4857  public int KarmaKickCount;
4858  public readonly List<Client> KickVoters = new List<Client>();
4859 
4861  {
4862  Name = c.Name;
4863  Address = c.Connection.Endpoint.Address;
4865  }
4866 
4867  public bool MatchesClient(Client c)
4868  {
4869  if (c.AccountInfo.AccountId.IsSome() && AccountInfo.AccountId.IsSome()) { return c.AccountInfo.AccountId == AccountInfo.AccountId; }
4870  return c.AddressMatches(Address);
4871  }
4872  }
4873 }
Vector2 TargetPos
Definition: Camera.cs:121
readonly CauseOfDeathType Type
Definition: CauseOfDeath.cs:12
float VitalityDisregardingDeath
How much vitality the character would have if it was alive? E.g. a character killed by disconnection ...
void SetStun(float newStun, bool allowStunDecrease=false, bool isNetworkMessage=false)
void Revive(bool removeAfflictions=true, bool createNetworkEvent=false)
void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage=false, bool log=true)
Stores information about the Character that is needed between rounds in the menu etc....
CharacterInfo(Identifier speciesName, string name="", string originalName="", Either< Job, JobPrefab > jobOrJobPrefab=null, int variant=0, Rand.RandSync randSync=Rand.RandSync.Unsynced, Identifier npcIdentifier=default)
void RecreateHead(ImmutableHashSet< Identifier > tags, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex)
static CoroutineStatus Success
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
static NetLobbyScreen NetLobbyScreen
Definition: GameMain.cs:57
static GameScreen GameScreen
Definition: GameMain.cs:56
static GameServer Server
Definition: GameMain.cs:39
static GameMain Instance
Definition: GameMain.cs:48
static LuaCsSetup LuaCs
Definition: GameMain.cs:37
static GameSession GameSession
Definition: GameMain.cs:45
GameModePreset Preset
Definition: GameMode.cs:43
static GameModePreset PvP
static GameModePreset MultiPlayerCampaign
readonly Identifier Identifier
override Camera Cam
Definition: GameScreen.cs:27
CharacterTeamType? WinningTeam
Definition: GameSession.cs:105
static bool ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(GameModePreset preset, IEnumerable< Identifier > missionTypes)
static PerkCollection GetPerks()
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
IEnumerable< Mission > Missions
Definition: GameSession.cs:85
bool TryPurchaseSubmarine(SubmarineInfo newSubmarine, Client? client=null)
Definition: GameSession.cs:433
static bool ShouldApplyDisembarkPoints(GameModePreset? preset)
Definition: GameSession.cs:393
static bool ValidatedDisembarkPoints(GameModePreset preset, IEnumerable< Identifier > missionTypes)
int GetMissionIndex(Mission mission)
void EndRound(string endMessage, CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults=null)
void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client=null)
Switch to another submarine. The sub is loaded when the next round starts.
Definition: GameSession.cs:411
static bool Compare(string a, string b)
Definition: Homoglyphs.cs:1865
int Variant
Definition: Job.cs:20
JobPrefab Prefab
Definition: Job.cs:18
static readonly PrefabCollection< JobPrefab > Prefabs
Definition: JobPrefab.cs:75
void UpdateClients(IEnumerable< Client > clients, float deltaTime)
object Call(string name, params object[] args)
void Initialize(bool forceEnableCs=false)
Definition: LuaCsSetup.cs:372
bool disableDisconnectCharacter
Definition: LuaGame.cs:147
bool overrideSignalRadio
Definition: LuaGame.cs:145
override string ToString()
Definition: Md5Hash.cs:142
void BanPlayer(string name, Endpoint endpoint, string reason, TimeSpan? duration)
void UnbanPlayer(Endpoint endpoint)
static string GetChatMessageCommand(string message, out string messageWithoutCommand)
static ChatMessage Create(string senderName, string text, ChatMessageType type, Entity sender, Client client=null, PlayerConnectionChangeType changeType=PlayerConnectionChangeType.None, Color? textColor=null)
static bool CanUseRadio(Character sender, bool ignoreJamming=false)
void ResetVotes(bool resetKickVotes)
Reset what this client has voted for and the kick votes given to this client
readonly HashSet< DebugConsole.Command > PermittedConsoleCommands
static void ReadPermissions(IReadMessage inc, out ClientPermissions permissions, out List< DebugConsole.Command > permittedCommands)
void SetPermissions(ClientPermissions permissions, IEnumerable< DebugConsole.Command > permittedConsoleCommands)
static bool IsValidName(string name, ServerSettings serverSettings)
readonly Dictionary< UInt16, double > EntityEventLastSent
Option< AccountId > AccountId
The ID of the account used to authenticate this session. This value can be used as a persistent value...
static void UpdateKickVotes(IReadOnlyList< Client > connectedClients)
readonly byte SessionId
An ID for this client for the current session. THIS IS NOT A PERSISTENT VALUE. DO NOT STORE THIS LONG...
readonly Dictionary< MultiPlayerCampaign.NetFlags, UInt16 > LastRecvCampaignUpdate
readonly Dictionary< Entity, float > PositionUpdateLastSent
static Option< Endpoint > Parse(string str)
IReadOnlyList< FileTransferOut > ActiveTransfers
Definition: FileSender.cs:127
FileTransferOut StartTransfer(NetworkConnection recipient, FileTransferType fileType, string filePath)
Definition: FileSender.cs:137
void ReadFileRequest(IReadMessage inc, Client client)
Definition: FileSender.cs:336
FileTransferDelegate OnStarted
Definition: FileSender.cs:113
void Update(float deltaTime)
Definition: FileSender.cs:182
FileTransferDelegate OnEnded
Definition: FileSender.cs:114
void KickClient(Client client, string reason, bool resetKarma=false)
Definition: GameServer.cs:3475
GameServer(string name, IPAddress listenIp, int port, int queryPort, bool isPublic, string password, bool attemptUPnP, int maxPlayers, Option< int > ownerKey, Option< P2PEndpoint > ownerEndpoint)
Definition: GameServer.cs:171
void SetClientCharacter(Client client, Character newCharacter)
Definition: GameServer.cs:4192
override void UnbanPlayer(string playerName)
Definition: GameServer.cs:3560
void UpdateVoteStatus(bool checkActiveVote=true)
Definition: GameServer.cs:3953
static string CharacterLogName(Character character)
Definition: GameServer.cs:4602
void DisconnectClient(Client client, PeerDisconnectPacket peerDisconnectPacket)
Definition: GameServer.cs:3581
NetworkConnection OwnerConnection
Definition: GameServer.cs:135
void IncrementStat(Client client, AchievementStat stat, int amount)
Definition: GameServer.cs:4160
void GiveAchievement(Character character, Identifier achievementIdentifier)
Definition: GameServer.cs:4124
void SendOrderChatMessage(OrderChatMessage message)
Definition: GameServer.cs:3908
TryStartGameResult TryStartGame()
Definition: GameServer.cs:2423
override IReadOnlyList< Client > ConnectedClients
Definition: GameServer.cs:117
void BanPreviousPlayer(PreviousPlayer previousPlayer, string reason, TimeSpan? duration=null)
Definition: GameServer.cs:3535
void SendDirectChatMessage(string txt, Client recipient, ChatMessageType messageType=ChatMessageType.Server)
Definition: GameServer.cs:3653
void Update(float deltaTime)
Definition: GameServer.cs:389
void GiveAchievement(Client client, Identifier achievementIdentifier)
Definition: GameServer.cs:4148
void EndGame(CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, bool wasSaved=false, IEnumerable< Mission > missions=null)
Definition: GameServer.cs:3223
override void KickPlayer(string playerName, string reason)
Definition: GameServer.cs:3458
void UpdateMissionState(Mission mission)
Definition: GameServer.cs:4589
void RefreshPvpTeamAssignments(bool assignUnassignedNow=false, bool autoBalanceNow=false)
Assigns currently playing clients into PvP teams according to current server settings.
Definition: GameServer.cs:4678
void DisconnectClient(NetworkConnection senderConnection, PeerDisconnectPacket peerDisconnectPacket)
Definition: GameServer.cs:3573
void BanClient(Client client, string reason, TimeSpan? duration=null)
Definition: GameServer.cs:3507
bool TryChangeClientName(Client c, string newName)
Definition: GameServer.cs:3397
void StartServer(bool registerToServerList)
Definition: GameServer.cs:204
ServerEntityEventManager EntityEventManager
Definition: GameServer.cs:126
void KickClient(NetworkConnection conn, string reason)
Definition: GameServer.cs:3467
void SendConsoleMessage(string txt, Client recipient, Color? color=null)
Definition: GameServer.cs:3659
bool IsNameValid(Client c, string newName)
Definition: GameServer.cs:3419
void SendTraitorMessage(WriteOnlyMessage msg, Client client)
Definition: GameServer.cs:4171
void IncrementStat(Character character, AchievementStat stat, int amount)
Definition: GameServer.cs:4136
bool FindAndRemoveRecentlyDisconnectedConnection(NetworkConnection conn)
Definition: GameServer.cs:147
void SendDirectChatMessage(ChatMessage msg, Client recipient)
Definition: GameServer.cs:3665
static void AddPendingMessageToOwner(string message, ChatMessageType messageType)
Creates a message that gets sent to the server owner once the connection is initialized....
Definition: GameServer.cs:266
static void Log(string line, ServerLog.MessageType messageType)
Definition: GameServer.cs:4609
void SendVoteStatus(List< Client > recipients)
Definition: GameServer.cs:4015
void AssignBotJobs(List< CharacterInfo > bots, CharacterTeamType teamID, bool isPvP)
Definition: GameServer.cs:4485
void AssignClientToPvpTeamMidgame(Client client)
Assign a team for single clients who join the server when a round is already running.
Definition: GameServer.cs:4806
void AssignJobs(List< Client > unassigned)
Definition: GameServer.cs:4310
override void UnbanPlayer(Endpoint endpoint)
Definition: GameServer.cs:3568
override void BanPlayer(string playerName, string reason, TimeSpan? duration=null)
Definition: GameServer.cs:3492
void UpdateClientPermissions(Client client)
Definition: GameServer.cs:4064
override void AddChatMessage(ChatMessage message)
Definition: GameServer.cs:3323
void SendChatMessage(string message, ChatMessageType? type=null, Client senderClient=null, Character senderCharacter=null, PlayerConnectionChangeType changeType=PlayerConnectionChangeType.None, ChatMode chatMode=ChatMode.None)
Add the message to the chatbox and pass it to all clients who can receive it
Definition: GameServer.cs:3686
override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData=null)
Definition: GameServer.cs:1194
void SendCancelTransferMsg(FileSender.FileTransferOut transfer)
Definition: GameServer.cs:3944
abstract bool AddressMatches(NetworkConnection other)
Similar to EndpointMatches but ignores port on LidgrenEndpoint
static readonly List< PermissionPreset > List
float ReplaceCostPercentage
Percentage modifier for the cost of hiring a new character to replace a permanently killed one.
float DespawnDisconnectedPermadeathTime
The number of seconds a disconnected player's Character remains in the world until despawned,...
bool IronmanMode
This is an optional setting for permadeath mode. When it's enabled, a player client whose character d...
bool ServerDetailsChanged
Have some of the properties listed in the server list changed
void WriteMonsterEnabled(IWriteMessage msg, Dictionary< Identifier, bool > monsterEnabled=null)
bool AllowBotTakeoverOnPermadeath
Are players allowed to take over bots when permadeath is enabled?
float KillDisconnectedTime
The number of seconds a disconnected player's Character remains in the world until despawned (via "br...
static void Read(IReadMessage inc, Client connectedClient)
Definition: VoipServer.cs:136
void SendToClients(List< Client > clients)
Definition: VoipServer.cs:31
Prefab(ContentFile file, Identifier identifier)
Definition: Prefab.cs:40
readonly Identifier Identifier
Definition: Prefab.cs:34
static Screen Selected
Definition: Screen.cs:5
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 ServerRead(IReadMessage inc, Client sender, DoSProtection dosProtection)
Interface for entities that handle ServerNetObject.ENTITY_POSITION
Interface for entities that the server can send events to the clients
void WriteBytes(byte[] val, int startIndex, int length)
void WriteRangedInteger(int val, int min, int max)
void WriteIdentifier(Identifier val)
void Finish(Voting voting, bool passed)
PvpTeamSelectionMode
Definition: Enums.cs:754
SelectedSubType
Definition: Enums.cs:16
CharacterType
Definition: Enums.cs:711
@ Character
Characters only
readonly ImmutableArray< AccountId > OtherMatchingIds
Other user IDs that this user might be closely tied to, such as the owner of the current copy of Baro...
Definition: AccountInfo.cs:21
readonly Option< AccountId > AccountId
The primary ID for a given user
Definition: AccountInfo.cs:15
static void Read(IReadMessage msg, SegmentDataReader segmentDataReader, ExceptionHandler? exceptionHandler=null)
static SegmentTableWriter< T > StartWriting(IWriteMessage msg)
Definition: SegmentTable.cs:94