#define ALLOW_BOT_TRAITORS using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; using Barotrauma.Steam; using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Xml.Linq; namespace Barotrauma.Networking { partial class GameServer : NetworkMember { public override bool IsServer { get { return true; } } private string serverName; public string ServerName { get { return serverName; } set { if (string.IsNullOrEmpty(value)) { return; } serverName = value.Replace(":", "").Replace(";", ""); } } public bool SubmarineSwitchLoad = false; private readonly List connectedClients = new List(); //for keeping track of disconnected clients in case the reconnect shortly after private readonly List disconnectedClients = new List(); //keeps track of players who've previously been playing on the server //so kick votes persist during the session and the server can let the clients know what name this client used previously private readonly List previousPlayers = new List(); private int roundStartSeed; //is the server running private bool started; private ServerPeer serverPeer; public ServerPeer ServerPeer { get { return serverPeer; } } private DateTime refreshMasterTimer; private TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 60); private bool registeredToMaster; private DateTime roundStartTime; private bool autoRestartTimerRunning; private float endRoundTimer; public VoipServer VoipServer { get; private set; } private bool initiatedStartGame; private CoroutineHandle startGameCoroutine; public TraitorManager TraitorManager; private readonly ServerEntityEventManager entityEventManager; private FileSender fileSender; #if DEBUG public void PrintSenderTransters() { foreach (var transfer in fileSender.ActiveTransfers) { DebugConsole.NewMessage(transfer.FileName + " " + transfer.Progress.ToString()); } } #endif public override List ConnectedClients { get { return connectedClients; } } public ServerEntityEventManager EntityEventManager { get { return entityEventManager; } } public TimeSpan UpdateInterval { get { return updateInterval; } } public int Port => serverSettings?.Port ?? 0; //only used when connected to steam public int QueryPort => serverSettings?.QueryPort ?? 0; public NetworkConnection OwnerConnection { get; private set; } private readonly int? ownerKey; private readonly UInt64? ownerSteamId; public GameServer(string name, int port, int queryPort = 0, bool isPublic = false, string password = "", bool attemptUPnP = false, int maxPlayers = 10, int? ownKey = null, UInt64? steamId = null) { name = name.Replace(":", ""); name = name.Replace(";", ""); if (name.Length > NetConfig.ServerNameMaxLength) { name = name.Substring(0, NetConfig.ServerNameMaxLength); } this.serverName = name; LastClientListUpdateID = 0; serverSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP); KarmaManager.SelectPreset(serverSettings.KarmaPreset); serverSettings.SetPassword(password); ownerKey = ownKey; ownerSteamId = steamId; entityEventManager = new ServerEntityEventManager(this); CoroutineManager.StartCoroutine(StartServer(isPublic)); } private IEnumerable StartServer(bool isPublic) { bool error = false; try { Log("Starting the server...", ServerLog.MessageType.ServerMessage); if (!ownerSteamId.HasValue || ownerSteamId.Value == 0) { 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 SteamP2P networking and does not require port forwarding).", ServerLog.MessageType.ServerMessage); serverPeer = new LidgrenServerPeer(ownerKey, serverSettings); } else { Log("Using SteamP2P networking.", ServerLog.MessageType.ServerMessage); serverPeer = new SteamP2PServerPeer(ownerSteamId.Value, serverSettings); } serverPeer.OnInitializationComplete = OnInitializationComplete; serverPeer.OnMessageReceived = ReadDataMessage; serverPeer.OnDisconnect = OnClientDisconnect; serverPeer.OnShutdown = GameMain.Instance.CloseServer; serverPeer.OnOwnerDetermined = OnOwnerDetermined; fileSender = new FileSender(serverPeer, MsgConstants.MTU); fileSender.OnEnded += FileTransferChanged; fileSender.OnStarted += FileTransferChanged; serverPeer.Start(); VoipServer = new VoipServer(serverPeer); } catch (Exception e) { Log("Error while starting the server (" + e.Message + ")", ServerLog.MessageType.Error); System.Net.Sockets.SocketException socketException = e as System.Net.Sockets.SocketException; error = true; } if (error) { if (serverPeer != null) serverPeer.Close("Error while starting the server"); Environment.Exit(-1); yield return CoroutineStatus.Success; } if (serverPeer is LidgrenServerPeer) { #if USE_STEAM registeredToMaster = SteamManager.CreateServer(this, isPublic); #endif } TickRate = serverSettings.TickRate; Log("Server started", ServerLog.MessageType.ServerMessage); GameMain.NetLobbyScreen.Select(); GameMain.NetLobbyScreen.RandomizeSettings(); if (!string.IsNullOrEmpty(serverSettings.SelectedSubmarine)) { SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedSubmarine); if (sub != null) { GameMain.NetLobbyScreen.SelectedSub = sub; } } if (!string.IsNullOrEmpty(serverSettings.SelectedShuttle)) { SubmarineInfo shuttle = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedShuttle); if (shuttle != null) { GameMain.NetLobbyScreen.SelectedShuttle = shuttle; } } List campaignSubs = new List(); if (serverSettings.CampaignSubmarines != null && serverSettings.CampaignSubmarines.Length > 0) { string[] submarines = serverSettings.CampaignSubmarines.Split(ServerSettings.SubmarineSeparatorChar); for (int i = 0; i < submarines.Length; i++) { SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == submarines[i]); if (subInfo != null && subInfo.IsCampaignCompatible) { campaignSubs.Add(subInfo); } } } else { // Add vanilla submarines by default for (int i = 0; i < SubmarineInfo.SavedSubmarines.Count(); i++) { SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.ElementAt(i); if (subInfo.IsVanillaSubmarine() && subInfo.IsCampaignCompatible) { campaignSubs.Add(SubmarineInfo.SavedSubmarines.ElementAt(i)); } } } GameMain.NetLobbyScreen.CampaignSubmarines = campaignSubs; started = true; GameAnalyticsManager.AddDesignEvent("GameServer:Start"); yield return CoroutineStatus.Success; } private void OnOwnerDetermined(NetworkConnection connection) { OwnerConnection = connection; var ownerClient = ConnectedClients.Find(c => c.Connection == connection); if (ownerClient == null) { DebugConsole.ThrowError("Owner client not found! Can't set permissions"); return; } ownerClient.SetPermissions(ClientPermissions.All, DebugConsole.Commands); UpdateClientPermissions(ownerClient); } public void NotifyCrash() { var tempList = ConnectedClients.Where(c => c.Connection != OwnerConnection).ToList(); foreach (var c in tempList) { DisconnectClient(c.Connection, DisconnectReason.ServerCrashed.ToString(), DisconnectReason.ServerCrashed.ToString()); } if (OwnerConnection != null) { var conn = OwnerConnection; OwnerConnection = null; DisconnectClient(conn, DisconnectReason.ServerCrashed.ToString(), DisconnectReason.ServerCrashed.ToString()); } Thread.Sleep(500); } private void OnInitializationComplete(NetworkConnection connection) { string clName = connection.Name; Client newClient = new Client(clName, GetNewClientID()); newClient.InitClientSync(); newClient.Connection = connection; newClient.Connection.Status = NetworkConnectionStatus.Connected; newClient.SteamID = connection.SteamID; newClient.OwnerSteamID = connection.OwnerSteamID; newClient.Language = connection.Language; ConnectedClients.Add(newClient); var previousPlayer = previousPlayers.Find(p => p.MatchesClient(newClient)); if (previousPlayer != null) { newClient.Karma = previousPlayer.Karma; newClient.KarmaKickCount = previousPlayer.KarmaKickCount; foreach (Client c in previousPlayer.KickVoters) { if (!connectedClients.Contains(c)) { continue; } newClient.AddKickVote(c); } } LastClientListUpdateID++; if (newClient.Connection == OwnerConnection && OwnerConnection != null) { newClient.GivePermission(ClientPermissions.All); newClient.PermittedConsoleCommands.AddRange(DebugConsole.Commands); SendConsoleMessage("Granted all permissions to " + newClient.Name + ".", newClient); } GameMain.Lua.hook.Call("clientConnected", new MoonSharp.Interpreter.DynValue[] { MoonSharp.Interpreter.UserData.Create(newClient) }); SendChatMessage($"ServerMessage.JoinedServer~[client]={clName}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Joined); serverSettings.ServerDetailsChanged = true; if (previousPlayer != null && previousPlayer.Name != newClient.Name) { string prevNameSanitized = previousPlayer.Name.Replace("‖", ""); SendChatMessage($"ServerMessage.PreviousClientName~[client]={ClientLogName(newClient)}~[previousname]={prevNameSanitized}", ChatMessageType.Server, null); previousPlayer.Name = newClient.Name; } var savedPermissions = serverSettings.ClientPermissions.Find(cp => cp.SteamID > 0 ? cp.SteamID == newClient.SteamID : newClient.EndpointMatches(cp.EndPoint)); if (savedPermissions != null) { newClient.SetPermissions(savedPermissions.Permissions, savedPermissions.PermittedCommands); } else { var defaultPerms = PermissionPreset.List.Find(p => p.Name == "None"); if (defaultPerms != null) { newClient.SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands); } else { newClient.SetPermissions(ClientPermissions.None, new List()); } } UpdateClientPermissions(newClient); } private void OnClientDisconnect(NetworkConnection connection, string disconnectMsg) { Client connectedClient = connectedClients.Find(c => c.Connection == connection); DisconnectClient(connectedClient, reason: disconnectMsg); } public override void Update(float deltaTime) { #if CLIENT if (ShowNetStats) { netStats.Update(deltaTime); } #endif if (!started) { return; } if (OwnerConnection != null && ChildServerRelay.HasShutDown) { Disconnect(); return; } base.Update(deltaTime); fileSender.Update(deltaTime); KarmaManager.UpdateClients(ConnectedClients, deltaTime); UpdatePing(); if (serverSettings.VoiceChatEnabled) { VoipServer.SendToClients(connectedClients); } if (gameStarted) { respawnManager?.Update(deltaTime); entityEventManager.Update(connectedClients); //go through the characters backwards to give rejoining clients control of the latest created character for (int i = Character.CharacterList.Count - 1; i >= 0; i--) { Character character = Character.CharacterList[i]; if (character.IsDead || !character.ClientDisconnected) { continue; } character.KillDisconnectedTimer += deltaTime; character.SetStun(1.0f); Client owner = connectedClients.Find(c => c.EndpointMatches(character.OwnerClientEndPoint)); if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > serverSettings.KillDisconnectedTime) { character.Kill(CauseOfDeathType.Disconnected, null); continue; } if (owner != null && owner.InGame && !owner.NeedsMidRoundSync && (!serverSettings.AllowSpectating || !owner.SpectateOnly)) { SetClientCharacter(owner, character); } } TraitorManager?.Update(deltaTime); if (serverSettings.Voting.VoteRunning) { Voting.SubVote.Timer += deltaTime; if (Voting.SubVote.Timer >= serverSettings.SubmarineVoteTimeout) { // Do not take unanswered into account for total if (SubmarineVoteYesCount / (float)(SubmarineVoteYesCount + SubmarineVoteNoCount) >= serverSettings.SubmarineVoteRequiredRatio) { SwitchSubmarine(); } else { serverSettings.Voting.StopSubmarineVote(false); } } } bool isCrewDead = connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated); bool subAtLevelEnd = false; if (Submarine.MainSub != null && !(GameMain.GameSession.GameMode is PvPMode)) { if (Level.Loaded?.EndOutpost != null) { int charactersInsideOutpost = connectedClients.Count(c => c.Character != null && !c.Character.IsDead && !c.Character.IsUnconscious && c.Character.Submarine == Level.Loaded.EndOutpost); int charactersOutsideOutpost = connectedClients.Count(c => c.Character != null && !c.Character.IsDead && !c.Character.IsUnconscious && c.Character.Submarine != Level.Loaded.EndOutpost); //level finished if the sub is docked to the outpost //or very close and someone from the crew made it inside the outpost subAtLevelEnd = Submarine.MainSub.DockedTo.Contains(Level.Loaded.EndOutpost) || (Submarine.MainSub.AtEndExit && charactersInsideOutpost > 0) || (charactersInsideOutpost > charactersOutsideOutpost); } else { subAtLevelEnd = Submarine.MainSub.AtEndExit; } } float endRoundDelay = 1.0f; if (TraitorManager?.ShouldEndRound ?? false) { endRoundDelay = 5.0f; endRoundTimer += deltaTime; } else if (serverSettings.AutoRestart && isCrewDead) { endRoundDelay = 5.0f; endRoundTimer += deltaTime; } else if (subAtLevelEnd && !(GameMain.GameSession?.GameMode is CampaignMode)) { endRoundDelay = 5.0f; endRoundTimer += deltaTime; } else if (isCrewDead && respawnManager == null) { #if !DEBUG if (endRoundTimer <= 0.0f) { SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "60"), ChatMessageType.Server); } endRoundDelay = 60.0f; endRoundTimer += deltaTime; #endif } else if (isCrewDead && (GameMain.GameSession?.GameMode is CampaignMode)) { #if !DEBUG endRoundDelay = 1.0f; endRoundTimer += deltaTime; #endif } else { endRoundTimer = 0.0f; } if (endRoundTimer >= endRoundDelay) { if (TraitorManager?.ShouldEndRound ?? false) { Log("Ending round (a traitor completed their mission)", ServerLog.MessageType.ServerMessage); } else if (serverSettings.AutoRestart && isCrewDead) { Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage); } else if (subAtLevelEnd) { Log("Ending round (submarine reached the end of the level)", ServerLog.MessageType.ServerMessage); } else if (respawnManager == null) { Log("Ending round (no living players left and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage); } else { Log("Ending round (no living players left)", ServerLog.MessageType.ServerMessage); } EndGame(); return; } } else if (initiatedStartGame) { //tried to start up the game and StartGame coroutine is not running anymore // -> something wen't wrong during startup, re-enable start button and reset AutoRestartTimer if (startGameCoroutine != null && !CoroutineManager.IsCoroutineRunning(startGameCoroutine)) { if (serverSettings.AutoRestart) serverSettings.AutoRestartTimer = Math.Max(serverSettings.AutoRestartInterval, 5.0f); //GameMain.NetLobbyScreen.StartButtonEnabled = true; GameMain.NetLobbyScreen.LastUpdateID++; startGameCoroutine = null; initiatedStartGame = false; } } else if (Screen.Selected == GameMain.NetLobbyScreen && !gameStarted && !initiatedStartGame && (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign || GameMain.GameSession?.GameMode is MultiPlayerCampaign)) { if (serverSettings.AutoRestart) { //autorestart if there are any non-spectators on the server (ignoring the server owner) bool shouldAutoRestart = connectedClients.Any(c => c.Connection != OwnerConnection && (!c.SpectateOnly || !serverSettings.AllowSpectating)); if (shouldAutoRestart != autoRestartTimerRunning) { autoRestartTimerRunning = shouldAutoRestart; GameMain.NetLobbyScreen.LastUpdateID++; } if (autoRestartTimerRunning) { serverSettings.AutoRestartTimer -= deltaTime; } } if (serverSettings.AutoRestart && autoRestartTimerRunning && serverSettings.AutoRestartTimer < 0.0f) { StartGame(); } else if (serverSettings.StartWhenClientsReady) { int clientsReady = connectedClients.Count(c => c.GetVote(VoteType.StartRound)); if (clientsReady / (float)connectedClients.Count >= serverSettings.StartWhenClientsReadyRatio) { StartGame(); } } } for (int i = disconnectedClients.Count - 1; i >= 0; i--) { disconnectedClients[i].DeleteDisconnectedTimer -= deltaTime; if (disconnectedClients[i].DeleteDisconnectedTimer > 0.0f) continue; if (gameStarted && disconnectedClients[i].Character != null) { disconnectedClients[i].Character.Kill(CauseOfDeathType.Disconnected, null); disconnectedClients[i].Character = null; } disconnectedClients.RemoveAt(i); } foreach (Client c in connectedClients) { //slowly reset spam timers c.ChatSpamTimer = Math.Max(0.0f, c.ChatSpamTimer - deltaTime); c.ChatSpamSpeed = Math.Max(0.0f, c.ChatSpamSpeed - deltaTime); //constantly increase AFK timer if the client is controlling a character (gets reset to zero every time an input is received) if (gameStarted && c.Character != null && !c.Character.IsDead && !c.Character.IsIncapacitated) { if (c.Connection != OwnerConnection) c.KickAFKTimer += deltaTime; } } IEnumerable kickAFK = connectedClients.FindAll(c => c.KickAFKTimer >= serverSettings.KickAFKTime && (OwnerConnection == null || c.Connection != OwnerConnection)); foreach (Client c in kickAFK) { KickClient(c, "DisconnectMessage.AFK"); } serverPeer.Update(deltaTime); //don't run the rest of the method if something in serverPeer.Update causes the server to shutdown if (!started) { return; } // if update interval has passed if (updateTimer < DateTime.Now) { if (ConnectedClients.Count > 0) { foreach (Client c in ConnectedClients) { try { ClientWrite(c); } catch (Exception e) { DebugConsole.ThrowError("Failed to write a network message for the client \"" + c.Name + "\"!", e); string errorMsg = "Failed to write a network message for the client \"" + c.Name + "\"! (MidRoundSyncing: " + c.NeedsMidRoundSync + ")\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace(); if (e.InnerException != null) { errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); } GameAnalyticsManager.AddErrorEventOnce( "GameServer.Update:ClientWriteFailed" + e.StackTrace.CleanupStackTrace(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } } foreach (Character character in Character.CharacterList) { if (character.healthUpdateTimer <= 0.0f) { if (!character.HealthUpdatePending) { character.healthUpdateTimer = character.HealthUpdateInterval; } character.HealthUpdatePending = true; } else { character.healthUpdateTimer -= (float)UpdateInterval.TotalSeconds; } character.HealthUpdateInterval += (float)UpdateInterval.TotalSeconds; } } updateTimer = DateTime.Now + updateInterval; } if (registeredToMaster && (DateTime.Now > refreshMasterTimer || serverSettings.ServerDetailsChanged)) { if (GameMain.Config.UseSteamMatchmaking) { bool refreshSuccessful = SteamManager.RefreshServerDetails(this); if (GameSettings.VerboseLogging) { Log(refreshSuccessful ? "Refreshed server info on the server list." : "Refreshing server info on the server list failed.", ServerLog.MessageType.ServerMessage); } } refreshMasterTimer = DateTime.Now + refreshMasterInterval; serverSettings.ServerDetailsChanged = false; } } private double lastPingTime; private byte[] lastPingData; private void UpdatePing() { if (Timing.TotalTime > lastPingTime + 1.0) { lastPingData ??= new byte[64]; for (int i=0;i { IWriteMessage pingReq = new WriteOnlyMessage(); pingReq.Write((byte)ServerPacketHeader.PING_REQUEST); pingReq.Write((byte)lastPingData.Length); pingReq.Write(lastPingData, 0, lastPingData.Length); serverPeer.Send(pingReq, c.Connection, DeliveryMethod.Unreliable); IWriteMessage pingInf = new WriteOnlyMessage(); pingInf.Write((byte)ServerPacketHeader.CLIENT_PINGS); pingInf.Write((byte)ConnectedClients.Count); ConnectedClients.ForEach(c2 => { pingInf.Write(c2.ID); pingInf.Write(c2.Ping); }); serverPeer.Send(pingInf, c.Connection, DeliveryMethod.Unreliable); }); } } private void ReadDataMessage(NetworkConnection sender, IReadMessage inc) { var connectedClient = connectedClients.Find(c => c.Connection == sender); ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte(); switch (header) { case ClientPacketHeader.PING_RESPONSE: byte responseLen = inc.ReadByte(); if (responseLen != lastPingData.Length) { return; } for (int i = 0; i < responseLen; i++) { byte b = inc.ReadByte(); if (b != lastPingData[i]) { return; } } connectedClient.Ping = (UInt16)((Timing.TotalTime - lastPingTime) * 1000); break; case ClientPacketHeader.RESPONSE_STARTGAME: if (connectedClient != null) { connectedClient.ReadyToStart = inc.ReadBoolean(); UpdateCharacterInfo(inc, connectedClient); //game already started -> send start message immediately if (gameStarted) { SendStartMessage(roundStartSeed, GameMain.GameSession.Level.Seed, GameMain.GameSession, connectedClient, true); } } break; case ClientPacketHeader.REQUEST_STARTGAMEFINALIZE: if (gameStarted && connectedClient != null) { SendRoundStartFinalize(connectedClient); } break; case ClientPacketHeader.UPDATE_LOBBY: ClientReadLobby(inc); break; case ClientPacketHeader.UPDATE_INGAME: if (!gameStarted) { return; } ClientReadIngame(inc); break; case ClientPacketHeader.CAMPAIGN_SETUP_INFO: bool isNew = inc.ReadBoolean(); inc.ReadPadBits(); if (isNew) { string saveName = inc.ReadString(); string seed = inc.ReadString(); string subName = inc.ReadString(); string subHash = inc.ReadString(); CampaignSettings settings = new CampaignSettings(inc); var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); if (gameStarted) { SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning"), connectedClient, ChatMessageType.MessageBox); return; } if (matchingSub == null) { SendDirectChatMessage( TextManager.GetWithVariable("CampaignStartFailedSubNotFound", "[subname]", subName), connectedClient, ChatMessageType.MessageBox); } else { string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); } } } else { string saveName = inc.ReadString(); if (gameStarted) { SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning"), connectedClient, ChatMessageType.MessageBox); return; } if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { MultiPlayerCampaign.LoadCampaign(saveName); } } break; case ClientPacketHeader.VOICE: if (serverSettings.VoiceChatEnabled && !connectedClient.Muted) { byte id = inc.ReadByte(); if (connectedClient.ID != id) { #if DEBUG DebugConsole.ThrowError( "Client \"" + connectedClient.Name + "\" sent a VOIP update that didn't match its ID (" + id.ToString() + "!=" + connectedClient.ID.ToString() + ")"); #endif return; } connectedClient.VoipQueue.Read(inc); } break; case ClientPacketHeader.SERVER_SETTINGS: serverSettings.ServerRead(inc, connectedClient); break; case ClientPacketHeader.SERVER_COMMAND: ClientReadServerCommand(inc); break; case ClientPacketHeader.CREW: ReadCrewMessage(inc, connectedClient); break; case ClientPacketHeader.READY_CHECK: ReadyCheck.ServerRead(inc, connectedClient); break; case ClientPacketHeader.READY_TO_SPAWN: ReadReadyToSpawnMessage(inc, connectedClient); break; case ClientPacketHeader.FILE_REQUEST: if (serverSettings.AllowFileTransfers) { fileSender.ReadFileRequest(inc, connectedClient); } break; case ClientPacketHeader.EVENTMANAGER_RESPONSE: GameMain.GameSession?.EventManager.ServerRead(inc, connectedClient); break; case ClientPacketHeader.ERROR: HandleClientError(inc, connectedClient); break; } } private void HandleClientError(IReadMessage inc, Client c) { string errorStr = "Unhandled error report"; ClientNetError error = (ClientNetError)inc.ReadByte(); switch (error) { case ClientNetError.MISSING_EVENT: UInt16 expectedID = inc.ReadUInt16(); UInt16 receivedID = inc.ReadUInt16(); errorStr = "Expecting event id " + expectedID.ToString() + ", received " + receivedID.ToString(); break; case ClientNetError.MISSING_ENTITY: UInt16 eventID = inc.ReadUInt16(); UInt16 entityID = inc.ReadUInt16(); Entity entity = Entity.FindEntityByID(entityID); if (entity == null) { errorStr = "Received an update for an entity that doesn't exist (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; } else if (entity is Character character) { errorStr = "Missing character " + character.Name + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; } else if (entity is Item item) { errorStr = "Missing item " + item.Name + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; } else { errorStr = "Missing entity " + entity.ToString() + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; } break; } Log(GameServer.ClientLogName(c) + " has reported an error: " + errorStr, ServerLog.MessageType.Error); GameAnalyticsManager.AddErrorEventOnce("GameServer.HandleClientError:" + errorStr, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorStr); try { WriteEventErrorData(c, errorStr); } catch (Exception e) { DebugConsole.ThrowError("Failed to write event error data", e); } if (c.Connection == OwnerConnection) { SendDirectChatMessage(errorStr, c, ChatMessageType.MessageBox); EndGame(); } else { KickClient(c, errorStr); } } private void WriteEventErrorData(Client client, string errorStr) { if (!Directory.Exists(ServerLog.SavePath)) { Directory.CreateDirectory(ServerLog.SavePath); } string filePath = "event_error_log_server_" + client.Name + "_" + DateTime.UtcNow.ToShortTimeString() + ".log"; filePath = Path.Combine(ServerLog.SavePath, ToolBox.RemoveInvalidFileNameChars(filePath)); if (File.Exists(filePath)) { return; } List errorLines = new List { errorStr, "" }; if (GameMain.GameSession?.GameMode != null) { errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { errorLines.Add("Campaign ID: " + campaign.CampaignID); errorLines.Add("Campaign save ID: " + campaign.LastSaveID); } foreach (Mission mission in GameMain.GameSession.Missions) { errorLines.Add("Mission: " + mission.Prefab.Identifier); } } if (GameMain.GameSession?.Submarine != null) { errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Info.Name); } if (GameMain.NetworkMember?.RespawnManager?.RespawnShuttle != null) { errorLines.Add("Respawn shuttle: " + GameMain.NetworkMember.RespawnManager.RespawnShuttle.Info.Name); } if (Level.Loaded != null) { errorLines.Add("Level: " + Level.Loaded.Seed + ", " + string.Join(", ", Level.Loaded.EqualityCheckValues.Select(cv => cv.ToString("X")))); errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate); errorLines.Add("Entities:"); foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate) { errorLines.Add(" " + e.ID + ": " + e.ToString()); } errorLines.Add("Entity count after generating level: " + Level.Loaded.EntityCountAfterGenerate); } errorLines.Add("Entity IDs:"); List sortedEntities = Entity.GetEntities().ToList(); sortedEntities.Sort((e1, e2) => e1.ID.CompareTo(e2.ID)); foreach (Entity e in sortedEntities) { errorLines.Add(e.ID + ": " + e.ToString()); } errorLines.Add(""); errorLines.Add("EntitySpawner events:"); foreach (var entityEvent in entityEventManager.UniqueEvents) { if (entityEvent.Entity is EntitySpawner) { var spawnData = entityEvent.Data[0] as EntitySpawner.SpawnOrRemove; errorLines.Add( entityEvent.ID + ": " + (spawnData.Remove ? "Remove " : "Create ") + spawnData.Entity.ToString() + " (" + spawnData.OriginalID + ", " + spawnData.Entity.ID + ")"); } } errorLines.Add(""); errorLines.Add("Last debug messages:"); for (int i = DebugConsole.Messages.Count - 1; i > 0 && i > DebugConsole.Messages.Count - 15; i--) { errorLines.Add(" " + DebugConsole.Messages[i].Time + " - " + DebugConsole.Messages[i].Text); } File.WriteAllLines(filePath, errorLines); } public override void CreateEntityEvent(INetSerializable entity, object[] extraData = null) { if (!(entity is IServerSerializable)) throw new InvalidCastException("entity is not IServerSerializable"); entityEventManager.CreateEvent(entity as IServerSerializable, extraData); } private byte GetNewClientID() { byte userID = 1; while (connectedClients.Any(c => c.ID == userID)) { userID++; } return userID; } private void ClientReadLobby(IReadMessage inc) { Client c = ConnectedClients.Find(x => x.Connection == inc.Sender); if (c == null) { //TODO: remove? //inc.Sender.Disconnect("You're not a connected client."); return; } ClientNetObject objHeader; while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE) { switch (objHeader) { case ClientNetObject.SYNC_IDS: //TODO: might want to use a clever class for this c.LastRecvLobbyUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvLobbyUpdate, GameMain.NetLobbyScreen.LastUpdateID); c.LastRecvChatMsgID = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvChatMsgID, c.LastChatMsgQueueID); c.LastRecvClientListUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvClientListUpdate, LastClientListUpdateID); TryChangeClientName(c, inc); c.LastRecvCampaignSave = inc.ReadUInt16(); if (c.LastRecvCampaignSave > 0) { byte campaignID = inc.ReadByte(); c.LastRecvCampaignUpdate = inc.ReadUInt16(); bool characterDiscarded = inc.ReadBoolean(); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { if (characterDiscarded) { campaign.DiscardClientCharacterData(c); } //the client has a campaign save for another campaign //(the server started a new campaign and the client isn't aware of it yet?) if (campaign.CampaignID != campaignID) { c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1); c.LastRecvCampaignUpdate = (ushort)(campaign.LastUpdateID - 1); } } } break; case ClientNetObject.CHAT_MESSAGE: ChatMessage.ServerRead(inc, c); break; case ClientNetObject.VOTE: serverSettings.Voting.ServerRead(inc, c); break; default: return; } //don't read further messages if the client has been disconnected (kicked due to spam for example) if (!connectedClients.Contains(c)) break; } } private void ClientReadIngame(IReadMessage inc) { Client c = ConnectedClients.Find(x => x.Connection == inc.Sender); if (c == null) { //TODO: remove? //inc.SenderConnection.Disconnect("You're not a connected client."); return; } bool midroundSyncingDone = inc.ReadBoolean(); inc.ReadPadBits(); if (gameStarted) { if (!c.InGame) { //check if midround syncing is needed due to missed unique events if (!midroundSyncingDone) { entityEventManager.InitClientMidRoundSync(c); } c.InGame = true; } } ClientNetObject objHeader; while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE) { switch (objHeader) { case ClientNetObject.SYNC_IDS: //TODO: might want to use a clever class for this UInt16 lastRecvChatMsgID = inc.ReadUInt16(); UInt16 lastRecvEntityEventID = inc.ReadUInt16(); UInt16 lastRecvClientListUpdate = inc.ReadUInt16(); //last msgs we've created/sent, the client IDs should never be higher than these UInt16 lastEntityEventID = entityEventManager.Events.Count == 0 ? (UInt16)0 : entityEventManager.Events.Last().ID; c.LastRecvCampaignSave = inc.ReadUInt16(); if (c.LastRecvCampaignSave > 0) { byte campaignID = inc.ReadByte(); c.LastRecvCampaignUpdate = inc.ReadUInt16(); bool characterDiscarded = inc.ReadBoolean(); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { if (characterDiscarded) { campaign.DiscardClientCharacterData(c); } //the client has a campaign save for another campaign //(the server started a new campaign and the client isn't aware of it yet?) if (campaign.CampaignID != campaignID) { c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1); c.LastRecvCampaignUpdate = (ushort)(campaign.LastUpdateID - 1); } } } if (c.NeedsMidRoundSync) { //received all the old events -> client in sync, we can switch to normal behavior if (lastRecvEntityEventID >= c.UnreceivedEntityEventCount - 1 || c.UnreceivedEntityEventCount == 0) { ushort prevID = lastRecvEntityEventID; c.NeedsMidRoundSync = false; lastRecvEntityEventID = (UInt16)(c.FirstNewEventID - 1); c.LastRecvEntityEventID = lastRecvEntityEventID; DebugConsole.Log("Finished midround syncing " + c.Name + " - switching from ID " + prevID + " to " + c.LastRecvEntityEventID); } else { lastEntityEventID = (UInt16)(c.UnreceivedEntityEventCount - 1); } } if (NetIdUtils.IsValidId(lastRecvChatMsgID, c.LastRecvChatMsgID, c.LastChatMsgQueueID)) { c.LastRecvChatMsgID = lastRecvChatMsgID; } else if (lastRecvChatMsgID != c.LastRecvChatMsgID && GameSettings.VerboseLogging) { DebugConsole.ThrowError( "Invalid lastRecvChatMsgID " + lastRecvChatMsgID + " (previous: " + c.LastChatMsgQueueID + ", latest: " + c.LastChatMsgQueueID + ")"); } if (NetIdUtils.IsValidId(lastRecvEntityEventID, c.LastRecvEntityEventID, lastEntityEventID)) { if (c.NeedsMidRoundSync) { //give midround-joining clients a bit more time to get in sync if they keep receiving messages int receivedEventCount = lastRecvEntityEventID - c.LastRecvEntityEventID; if (receivedEventCount < 0) receivedEventCount += ushort.MaxValue; c.MidRoundSyncTimeOut += receivedEventCount * 0.01f; DebugConsole.Log("Midround sync timeout " + c.MidRoundSyncTimeOut.ToString("0.##") + "/" + Timing.TotalTime.ToString("0.##")); } c.LastRecvEntityEventID = lastRecvEntityEventID; } else if (lastRecvEntityEventID != c.LastRecvEntityEventID && GameSettings.VerboseLogging) { DebugConsole.ThrowError( "Invalid lastRecvEntityEventID " + lastRecvEntityEventID + " (previous: " + c.LastRecvEntityEventID + ", latest: " + lastEntityEventID + ")"); } if (NetIdUtils.IdMoreRecent(lastRecvClientListUpdate, c.LastRecvClientListUpdate)) { c.LastRecvClientListUpdate = lastRecvClientListUpdate; } break; case ClientNetObject.CHAT_MESSAGE: ChatMessage.ServerRead(inc, c); break; case ClientNetObject.CHARACTER_INPUT: if (c.Character != null) { c.Character.ServerRead(objHeader, inc, c); } break; case ClientNetObject.ENTITY_STATE: entityEventManager.Read(inc, c); break; case ClientNetObject.VOTE: serverSettings.Voting.ServerRead(inc, c); break; case ClientNetObject.SPECTATING_POS: c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); break; default: return; } //don't read further messages if the client has been disconnected (kicked due to spam for example) if (!connectedClients.Contains(c)) break; } } private void ReadCrewMessage(IReadMessage inc, Client sender) { if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) { mpCampaign.ServerReadCrew(inc, sender); } } private void ReadReadyToSpawnMessage(IReadMessage inc, Client sender) { sender.SpectateOnly = inc.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); sender.WaitForNextRoundRespawn = inc.ReadBoolean(); if (!(GameMain.GameSession?.GameMode is CampaignMode)) { sender.WaitForNextRoundRespawn = null; } } private void ClientReadServerCommand(IReadMessage inc) { Client sender = ConnectedClients.Find(x => x.Connection == inc.Sender); if (sender == null) { //TODO: remove? //inc.SenderConnection.Disconnect("You're not a connected client."); return; } ClientPermissions command = ClientPermissions.None; try { command = (ClientPermissions)inc.ReadUInt16(); } catch { return; } var mpCampaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; if (command == ClientPermissions.ManageRound && mpCampaign != null) { if (!mpCampaign.AllowedToEndRound(sender)) { return; } } else if (command == ClientPermissions.ManageCampaign && mpCampaign != null) { if (!mpCampaign.AllowedToManageCampaign(sender)) { return; } } else if (!sender.HasPermission(command)) { Log("Client \"" + GameServer.ClientLogName(sender) + "\" sent a server command \"" + command + "\". Permission denied.", ServerLog.MessageType.ServerMessage); return; } switch (command) { case ClientPermissions.Kick: string kickedName = inc.ReadString().ToLowerInvariant(); string kickReason = inc.ReadString(); var kickedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(kickedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection); if (kickedClient != null) { Log("Client \"" + GameServer.ClientLogName(sender) + "\" kicked \"" + GameServer.ClientLogName(kickedClient) + "\".", ServerLog.MessageType.ServerMessage); KickClient(kickedClient, string.IsNullOrEmpty(kickReason) ? $"ServerMessage.KickedBy~[initiator]={sender.Name}" : kickReason); } else { SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={kickedName}"), sender, ChatMessageType.Console); } break; case ClientPermissions.Ban: string bannedName = inc.ReadString().ToLowerInvariant(); string banReason = inc.ReadString(); bool range = inc.ReadBoolean(); double durationSeconds = inc.ReadDouble(); TimeSpan? banDuration = null; if (durationSeconds > 0) { banDuration = TimeSpan.FromSeconds(durationSeconds); } var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(bannedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection); if (bannedClient != null) { Log("Client \"" + ClientLogName(sender) + "\" banned \"" + ClientLogName(bannedClient) + "\".", ServerLog.MessageType.ServerMessage); BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, range, banDuration); } else { var bannedPreviousClient = previousPlayers.Find(p => p.Name.Equals(bannedName, StringComparison.OrdinalIgnoreCase)); if (bannedPreviousClient != null) { Log("Client \"" + ClientLogName(sender) + "\" banned \"" + bannedPreviousClient.Name + "\".", ServerLog.MessageType.ServerMessage); BanPreviousPlayer(bannedPreviousClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, range, banDuration); } else { SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={bannedName}"), sender, ChatMessageType.Console); } } break; case ClientPermissions.Unban: string unbannedName = inc.ReadString(); string unbannedIP = inc.ReadString(); UnbanPlayer(unbannedName, unbannedIP); break; case ClientPermissions.ManageRound: bool end = inc.ReadBoolean(); if (end) { if (gameStarted) { Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); if (mpCampaign != null && Level.IsLoadedOutpost) { mpCampaign.SaveInventories(); GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } EndGame(); } } else { bool continueCampaign = inc.ReadBoolean(); if (mpCampaign != null && mpCampaign.GameOver || continueCampaign) { MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); } else if (!gameStarted && !initiatedStartGame) { Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); StartGame(); } else if (mpCampaign != null) { var availableTransition = mpCampaign.GetAvailableTransition(out _, out _); //don't force location if we've teleported bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation; switch (availableTransition) { case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: if (forceLocation) { mpCampaign.Map.SelectLocation( mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation)); } mpCampaign.LoadNewLevel(); break; case CampaignMode.TransitionType.ProgressToNextEmptyLocation: if (forceLocation) { mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation)); } mpCampaign.LoadNewLevel(); break; case CampaignMode.TransitionType.None: #if DEBUG || UNSTABLE DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available."); #endif return; default: Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); mpCampaign.LoadNewLevel(); break; } } } break; case ClientPermissions.SelectSub: bool isCampaign = inc.ReadBoolean(); if (!isCampaign) { bool isShuttle = inc.ReadBoolean(); inc.ReadPadBits(); UInt16 subIndex = inc.ReadUInt16(); var subList = GameMain.NetLobbyScreen.GetSubList(); if (subIndex >= subList.Count) { DebugConsole.NewMessage("Client \"" + GameServer.ClientLogName(sender) + "\" attempted to select a sub, index out of bounds (" + subIndex + ")", Color.Red); } else { if (isShuttle) { GameMain.NetLobbyScreen.SelectedShuttle = subList[subIndex]; } else { GameMain.NetLobbyScreen.SelectedSub = subList[subIndex]; } } } else { int subEqualityCheckVal = inc.ReadInt32(); bool add = inc.ReadBoolean(); SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.EqualityCheckVal == subEqualityCheckVal); if (sub == null) { DebugConsole.NewMessage("Client \"" + GameServer.ClientLogName(sender) + "\" attempted to select a sub that does not exist on the server!", Color.Red); } else { if (add) { GameMain.NetLobbyScreen.AddCampaignSubmarine(sub); } else { GameMain.NetLobbyScreen.RemoveCampaignSubmarine(sub); } } } break; case ClientPermissions.SelectMode: UInt16 modeIndex = inc.ReadUInt16(); GameMain.NetLobbyScreen.SelectedModeIndex = modeIndex; Log("Gamemode changed to " + GameMain.NetLobbyScreen.GameModes[GameMain.NetLobbyScreen.SelectedModeIndex].Name, ServerLog.MessageType.ServerMessage); if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase)) { string[] saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false).ToArray(); for (int i = 0; i < saveFiles.Length; i++) { XDocument doc = SaveUtil.LoadGameSessionDoc(saveFiles[i]); if (doc?.Root != null) { saveFiles[i] = string.Join(";", saveFiles[i].Replace(';', ' '), doc.Root.GetAttributeString("submarine", ""), doc.Root.GetAttributeString("savetime", ""), doc.Root.GetAttributeString("selectedcontentpackages", "")); } } IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); msg.Write((UInt16)saveFiles.Count()); foreach (string saveFile in saveFiles) { msg.Write(saveFile); } serverPeer.Send(msg, sender.Connection, DeliveryMethod.Reliable); } break; case ClientPermissions.ManageCampaign: (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.ServerRead(inc, sender); break; case ClientPermissions.ConsoleCommands: { string consoleCommand = inc.ReadString(); Vector2 clientCursorPos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); DebugConsole.ExecuteClientCommand(sender, clientCursorPos, consoleCommand); } break; case ClientPermissions.ManagePermissions: byte targetClientID = inc.ReadByte(); Client targetClient = connectedClients.Find(c => c.ID == targetClientID); if (targetClient == null || targetClient == sender || targetClient.Connection == OwnerConnection) { return; } targetClient.ReadPermissions(inc); List permissionNames = new List(); foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { if (permission == ClientPermissions.None || permission == ClientPermissions.All) { continue; } if (targetClient.Permissions.HasFlag(permission)) { permissionNames.Add(permission.ToString()); } } string logMsg; if (permissionNames.Any()) { logMsg = "Client \"" + GameServer.ClientLogName(sender) + "\" set the permissions of the client \"" + GameServer.ClientLogName(targetClient) + "\" to " + string.Join(", ", permissionNames); } else { logMsg = "Client \"" + GameServer.ClientLogName(sender) + "\" removed all permissions from the client \"" + GameServer.ClientLogName(targetClient) + "."; } Log(logMsg, ServerLog.MessageType.ServerMessage); UpdateClientPermissions(targetClient); break; } inc.ReadPadBits(); } private void ClientWrite(Client c) { if (gameStarted && c.InGame) { ClientWriteIngame(c); } else { //if 30 seconds have passed since the round started and the client isn't ingame yet, //consider the client's character disconnected (causing it to die if the client does not join soon) if (gameStarted && c.Character != null && (DateTime.Now - roundStartTime).Seconds > 30.0f) { c.Character.ClientDisconnected = true; } ClientWriteLobby(c); } if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && GameMain.NetLobbyScreen.SelectedMode == campaign.Preset && NetIdUtils.IdMoreRecent(campaign.LastSaveID, c.LastRecvCampaignSave)) { //already sent an up-to-date campaign save if (c.LastCampaignSaveSendTime != null && campaign.LastSaveID == c.LastCampaignSaveSendTime.First) { //the save was sent less than 5 second ago, don't attempt to resend yet //(the client may have received it but hasn't acked us yet) if (c.LastCampaignSaveSendTime.Second > NetTime.Now - 5.0f) { return; } } if (!fileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave)) { fileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); c.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)NetTime.Now); } } } /// /// Write info that the client needs when joining the server /// private void ClientWriteInitial(Client c, IWriteMessage outmsg) { if (GameSettings.VerboseLogging) { DebugConsole.NewMessage("Sending initial lobby update", Color.Gray); } outmsg.Write(c.ID); var subList = GameMain.NetLobbyScreen.GetSubList(); outmsg.Write((UInt16)subList.Count); for (int i = 0; i < subList.Count; i++) { outmsg.Write(subList[i].Name); outmsg.Write(subList[i].MD5Hash.ToString()); outmsg.Write((byte)subList[i].SubmarineClass); outmsg.Write(subList[i].RequiredContentPackagesInstalled); } outmsg.Write(GameStarted); outmsg.Write(serverSettings.AllowSpectating); c.WritePermissions(outmsg); if (gameStarted) { string ownedSubmarineIndexes = string.Empty; for (int i = 0; i < subList.Count; i++) { if (GameMain.GameSession.OwnedSubmarines.Contains(subList[i])) { ownedSubmarineIndexes += i.ToString(); ownedSubmarineIndexes += ";"; } } if (ownedSubmarineIndexes.Length > 0) { ownedSubmarineIndexes.Trim(';'); } outmsg.Write(ownedSubmarineIndexes); } } private void ClientWriteIngame(Client c) { //don't send position updates to characters who are still midround syncing //characters or items spawned mid-round don't necessarily exist at the client's end yet if (!c.NeedsMidRoundSync) { foreach (Character character in Character.CharacterList) { if (!character.Enabled) { continue; } if (c.SpectatePos == null) { float distSqr = Vector2.DistanceSquared(character.WorldPosition, c.Character.WorldPosition); if (c.Character.ViewTarget != null) { distSqr = Math.Min(distSqr, Vector2.DistanceSquared(character.WorldPosition, c.Character.ViewTarget.WorldPosition)); } if (distSqr >= MathUtils.Pow2(character.Params.DisableDistance)) { continue; } } else { if (character != c.Character && Vector2.DistanceSquared(character.WorldPosition, c.SpectatePos.Value) >= MathUtils.Pow2(character.Params.DisableDistance)) { continue; } } float updateInterval = character.GetPositionUpdateInterval(c); c.PositionUpdateLastSent.TryGetValue(character, out float lastSent); if (lastSent > NetTime.Now) { //sent in the future -> can't be right, remove c.PositionUpdateLastSent.Remove(character); } else { if (lastSent > NetTime.Now - updateInterval) { continue; } } if (!c.PendingPositionUpdates.Contains(character)) { c.PendingPositionUpdates.Enqueue(character); } } foreach (Submarine sub in Submarine.Loaded) { //if docked to a sub with a smaller ID, don't send an update // (= 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) if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) { continue; } if (sub.PhysicsBody == null || sub.PhysicsBody.BodyType == FarseerPhysics.BodyType.Static) { continue; } if (!c.PendingPositionUpdates.Contains(sub)) { c.PendingPositionUpdates.Enqueue(sub); } } foreach (Item item in Item.ItemList) { if (item.PositionUpdateInterval == float.PositiveInfinity) { continue; } float updateInterval = item.GetPositionUpdateInterval(c); c.PositionUpdateLastSent.TryGetValue(item, out float lastSent); if (lastSent > NetTime.Now) { //sent in the future -> can't be right, remove c.PositionUpdateLastSent.Remove(item); } else { if (lastSent > NetTime.Now - updateInterval) { continue; } } if (!c.PendingPositionUpdates.Contains(item)) { c.PendingPositionUpdates.Enqueue(item); } } } IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.Write((byte)ServerPacketHeader.UPDATE_INGAME); outmsg.Write((float)NetTime.Now); outmsg.Write((byte)ServerNetObject.SYNC_IDS); outmsg.Write(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server outmsg.Write(c.LastSentEntityEventID); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode && NetIdUtils.IdMoreRecent(campaign.LastUpdateID, c.LastRecvCampaignUpdate)) { outmsg.Write(true); outmsg.WritePadBits(); campaign.ServerWrite(outmsg, c); } else { outmsg.Write(false); outmsg.WritePadBits(); } int clientListBytes = outmsg.LengthBytes; WriteClientList(c, outmsg); clientListBytes = outmsg.LengthBytes - clientListBytes; int chatMessageBytes = outmsg.LengthBytes; WriteChatMessages(outmsg, c); chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; //write as many position updates as the message can fit (only after midround syncing is done) int positionUpdateBytes = outmsg.LengthBytes; while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0) { var entity = c.PendingPositionUpdates.Peek(); if (entity == null || entity.Removed || (entity is Item item && item.PositionUpdateInterval == float.PositiveInfinity)) { c.PendingPositionUpdates.Dequeue(); continue; } IWriteMessage tempBuffer = new ReadWriteMessage(); tempBuffer.Write((byte)ServerNetObject.ENTITY_POSITION); tempBuffer.Write(entity is Item); if (entity is Item) { ((Item)entity).ServerWritePosition(tempBuffer, c); } else { ((IServerSerializable)entity).ServerWrite(tempBuffer, c); } //no more room in this packet if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100) { break; } outmsg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); outmsg.WritePadBits(); c.PositionUpdateLastSent[entity] = (float)NetTime.Now; c.PendingPositionUpdates.Dequeue(); } positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes; outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); if (outmsg.LengthBytes > MsgConstants.MTU) { string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; errorMsg += " Client list size: " + clientListBytes + " bytes\n" + " Chat message size: " + chatMessageBytes + " bytes\n" + " Position update size: " + positionUpdateBytes + " bytes\n\n"; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); //--------------------------------------------------------------------------- for (int i = 0; i < NetConfig.MaxEventPacketsPerUpdate; i++) { outmsg = new WriteOnlyMessage(); outmsg.Write((byte)ServerPacketHeader.UPDATE_INGAME); outmsg.Write((float)Lidgren.Network.NetTime.Now); int eventManagerBytes = outmsg.LengthBytes; entityEventManager.Write(c, outmsg, out List sentEvents); eventManagerBytes = outmsg.LengthBytes - eventManagerBytes; if (sentEvents.Count == 0) { break; } outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); if (outmsg.LengthBytes > MsgConstants.MTU) { string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; errorMsg += " Event size: " + eventManagerBytes + " bytes\n"; if (sentEvents != null && sentEvents.Count > 0) { errorMsg += "Sent events: \n"; foreach (var entityEvent in sentEvents) { errorMsg += " - " + (entityEvent.Entity?.ToString() ?? "null") + "\n"; } } DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); } } private void WriteClientList(Client c, IWriteMessage outmsg) { bool hasChanged = NetIdUtils.IdMoreRecent(LastClientListUpdateID, c.LastRecvClientListUpdate); if (!hasChanged) { return; } outmsg.Write((byte)ServerNetObject.CLIENT_LIST); outmsg.Write(LastClientListUpdateID); outmsg.Write((byte)connectedClients.Count); foreach (Client client in connectedClients) { outmsg.Write(client.ID); outmsg.Write(client.SteamID); outmsg.Write(client.NameID); outmsg.Write(client.Name); outmsg.Write(client.Character?.Info?.Job != null && gameStarted ? client.Character.Info.Job.Prefab.Identifier : (client.PreferredJob ?? "")); outmsg.Write((byte)client.PreferredTeam); outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); if (c.HasPermission(ClientPermissions.ServerLog)) { outmsg.Write(client.Karma); } else { outmsg.Write(100.0f); } outmsg.Write(client.Muted); outmsg.Write(client.InGame); outmsg.Write(client.Permissions != ClientPermissions.None); outmsg.Write(client.Connection == OwnerConnection); outmsg.Write(client.Connection != OwnerConnection && !client.HasPermission(ClientPermissions.Ban) && !client.HasPermission(ClientPermissions.Kick) && !client.HasPermission(ClientPermissions.Unban)); //is kicking the player allowed outmsg.WritePadBits(); } } private void ClientWriteLobby(Client c) { bool isInitialUpdate = false; IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.Write((byte)ServerPacketHeader.UPDATE_LOBBY); outmsg.Write((byte)ServerNetObject.SYNC_IDS); int settingsBytes = outmsg.LengthBytes; int initialUpdateBytes = 0; IWriteMessage settingsBuf = null; if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate)) { outmsg.Write(true); outmsg.WritePadBits(); outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); settingsBuf = new ReadWriteMessage(); serverSettings.ServerWrite(settingsBuf, c); outmsg.Write((UInt16)settingsBuf.LengthBytes); outmsg.Write(settingsBuf.Buffer, 0, settingsBuf.LengthBytes); outmsg.Write(c.LastRecvLobbyUpdate < 1); if (c.LastRecvLobbyUpdate < 1) { isInitialUpdate = true; initialUpdateBytes = outmsg.LengthBytes; ClientWriteInitial(c, outmsg); initialUpdateBytes = outmsg.LengthBytes - initialUpdateBytes; } outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.Name); outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); outmsg.Write(serverSettings.UseRespawnShuttle); outmsg.Write(GameMain.NetLobbyScreen.SelectedShuttle.Name); outmsg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.ToString()); List campaignSubIndices = new List(); if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) { List subList = GameMain.NetLobbyScreen.GetSubList(); for (int i = 0; i < subList.Count; i++) { if (GameMain.NetLobbyScreen.CampaignSubmarines.Contains(subList[i])) { campaignSubIndices.Add(i); } } } outmsg.Write((UInt16)campaignSubIndices.Count); foreach (int campaignSubIndex in campaignSubIndices) { outmsg.Write((UInt16)campaignSubIndex); } outmsg.Write(serverSettings.Voting.AllowSubVoting); outmsg.Write(serverSettings.Voting.AllowModeVoting); outmsg.Write(serverSettings.VoiceChatEnabled); outmsg.Write(serverSettings.AllowSpectating); outmsg.WriteRangedInteger((int)serverSettings.TraitorsEnabled, 0, 2); outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All); outmsg.Write((byte)GameMain.NetLobbyScreen.SelectedModeIndex); outmsg.Write(GameMain.NetLobbyScreen.LevelSeed); outmsg.Write(serverSettings.SelectedLevelDifficulty); outmsg.Write((byte)serverSettings.BotCount); outmsg.Write(serverSettings.BotSpawnMode == BotSpawnMode.Fill); outmsg.Write(serverSettings.AutoRestart); if (serverSettings.AutoRestart) { outmsg.Write(autoRestartTimerRunning ? serverSettings.AutoRestartTimer : 0.0f); } outmsg.Write(serverSettings.RadiationEnabled); outmsg.Write((byte)serverSettings.MaxMissionCount); } else { outmsg.Write(false); outmsg.WritePadBits(); } settingsBytes = outmsg.LengthBytes - settingsBytes; int campaignBytes = outmsg.LengthBytes; var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; if (outmsg.LengthBytes < MsgConstants.MTU - 500 && campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode && NetIdUtils.IdMoreRecent(campaign.LastUpdateID, c.LastRecvCampaignUpdate)) { outmsg.Write(true); outmsg.WritePadBits(); campaign.ServerWrite(outmsg, c); } else { outmsg.Write(false); outmsg.WritePadBits(); } campaignBytes = outmsg.LengthBytes - campaignBytes; outmsg.Write(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server int clientListBytes = outmsg.LengthBytes; if (outmsg.LengthBytes < MsgConstants.MTU - 500) { WriteClientList(c, outmsg); } clientListBytes = outmsg.LengthBytes - clientListBytes; int chatMessageBytes = outmsg.LengthBytes; WriteChatMessages(outmsg, c); chatMessageBytes = outmsg.LengthBytes - outmsg.LengthBytes; outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); if (isInitialUpdate) { //the initial update may be very large if the host has a large number //of submarine files, so the message may have to be fragmented //unreliable messages don't play nicely with fragmenting, so we'll send the message reliably serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Reliable); //and assume the message was received, so we don't have to keep resending //these large initial messages until the client acknowledges receiving them c.LastRecvLobbyUpdate++; SendVoteStatus(new List() { c }); } else { if (outmsg.LengthBytes > MsgConstants.MTU) { string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; errorMsg += " Client list size: " + clientListBytes + " bytes\n" + " Chat message size: " + chatMessageBytes + " bytes\n" + " Campaign size: " + campaignBytes + " bytes\n" + " Settings size: " + settingsBytes + " bytes\n"; if (initialUpdateBytes > 0) { errorMsg += " Initial update size: " + settingsBuf.LengthBytes + " bytes\n"; } if (settingsBuf != null) { errorMsg += " Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n"; } DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); } } private void WriteChatMessages(IWriteMessage outmsg, Client c) { c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID)); for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) { if (outmsg.LengthBytes + c.ChatMsgQueue[i].EstimateLengthBytesServer(c) > MsgConstants.MTU - 5) { //not enough room in this packet return; } c.ChatMsgQueue[i].ServerWrite(outmsg, c); } } public bool StartGame() { if (initiatedStartGame || gameStarted) { return false; } Log("Starting a new round...", ServerLog.MessageType.ServerMessage); SubmarineInfo selectedSub = null; SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; if (serverSettings.Voting.AllowSubVoting) { selectedSub = serverSettings.Voting.HighestVoted(VoteType.Sub, connectedClients); if (selectedSub == null) { selectedSub = GameMain.NetLobbyScreen.SelectedSub; } } else { selectedSub = GameMain.NetLobbyScreen.SelectedSub; } if (selectedSub == null || selectedShuttle == null) { return false; } GameModePreset selectedMode = serverSettings.Voting.HighestVoted(VoteType.Mode, connectedClients); if (selectedMode == null) { selectedMode = GameMain.NetLobbyScreen.SelectedMode; } if (selectedMode == null) { return false; } initiatedStartGame = true; startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedShuttle, selectedMode), "InitiateStartGame"); return true; } private IEnumerable InitiateStartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode) { initiatedStartGame = true; if (connectedClients.Any()) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.QUERY_STARTGAME); msg.Write(selectedSub.Name); msg.Write(selectedSub.MD5Hash.Hash); msg.Write(serverSettings.UseRespawnShuttle); msg.Write(selectedShuttle.Name); msg.Write(selectedShuttle.MD5Hash.Hash); var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; msg.Write(campaign == null ? (byte)0 : campaign.CampaignID); msg.Write(campaign == null ? (UInt16)0 : campaign.LastSaveID); msg.Write(campaign == null ? (UInt16)0 : campaign.LastUpdateID); connectedClients.ForEach(c => c.ReadyToStart = false); foreach (NetworkConnection conn in connectedClients.Select(c => c.Connection)) { serverPeer.Send(msg, conn, DeliveryMethod.Reliable); } //give the clients a few seconds to request missing sub/shuttle files before starting the round float waitForResponseTimer = 5.0f; while (connectedClients.Any(c => !c.ReadyToStart) && waitForResponseTimer > 0.0f) { waitForResponseTimer -= CoroutineManager.UnscaledDeltaTime; yield return CoroutineStatus.Running; } if (fileSender.ActiveTransfers.Count > 0) { float waitForTransfersTimer = 20.0f; while (fileSender.ActiveTransfers.Count > 0 && waitForTransfersTimer > 0.0f) { waitForTransfersTimer -= CoroutineManager.UnscaledDeltaTime; yield return CoroutineStatus.Running; } } } startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedMode, CampaignSettings.Unsure), false); yield return CoroutineStatus.Success; } private IEnumerable StartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode, CampaignSettings settings) { entityEventManager.Clear(); roundStartSeed = DateTime.Now.Millisecond; Rand.SetSyncedSeed(roundStartSeed); int teamCount = 1; MultiPlayerCampaign campaign = selectedMode == GameMain.GameSession?.GameMode.Preset ? GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; if (campaign != null && campaign.Map == null) { initiatedStartGame = false; startGameCoroutine = null; string errorMsg = "Starting the round failed. Campaign was still active, but the map has been disposed. Try selecting another game mode."; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.StartGame:InvalidCampaignState", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); if (OwnerConnection != null) { SendDirectChatMessage(errorMsg, connectedClients.Find(c => c.Connection == OwnerConnection), ChatMessageType.Error); } yield return CoroutineStatus.Failure; } //don't instantiate a new gamesession if we're playing a campaign if (campaign == null || GameMain.GameSession == null) { GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, settings, GameMain.NetLobbyScreen.LevelSeed, missionType: GameMain.NetLobbyScreen.MissionType); } List playingClients = new List(connectedClients); if (serverSettings.AllowSpectating) { playingClients.RemoveAll(c => c.SpectateOnly); } //always allow the server owner to spectate even if it's disallowed in server settings playingClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); if (GameMain.GameSession.GameMode is PvPMode pvpMode) { pvpMode.AssignTeamIDs(playingClients); teamCount = 2; } else { connectedClients.ForEach(c => c.TeamID = CharacterTeamType.Team1); } if (campaign != null) { if (campaign.Map == null) { throw new Exception("Campaign map was null."); } if (campaign.NextLevel == null) { string errorMsg = "Failed to start a campaign round (next level not set)."; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.StartGame:InvalidCampaignState", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); if (OwnerConnection != null) { SendDirectChatMessage(errorMsg, connectedClients.Find(c => c.Connection == OwnerConnection), ChatMessageType.Error); } yield return CoroutineStatus.Failure; } SendStartMessage(roundStartSeed, campaign.NextLevel.Seed, GameMain.GameSession, connectedClients, includesFinalize: false); GameMain.GameSession.StartRound(campaign.NextLevel, mirrorLevel: campaign.MirrorLevel); SubmarineSwitchLoad = false; campaign.AssignClientCharacterInfos(connectedClients); Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); Log("Submarine: " + GameMain.GameSession.SubmarineInfo.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + campaign.NextLevel.Seed, ServerLog.MessageType.ServerMessage); } else { SendStartMessage(roundStartSeed, GameMain.NetLobbyScreen.LevelSeed, GameMain.GameSession, connectedClients, false); GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, serverSettings.SelectedLevelDifficulty); Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); } foreach (Mission mission in GameMain.GameSession.Missions) { Log("Mission: " + mission.Prefab.Name, ServerLog.MessageType.ServerMessage); } if (GameMain.GameSession.SubmarineInfo.IsFileCorrupted) { CoroutineManager.StopCoroutines(startGameCoroutine); initiatedStartGame = false; SendChatMessage(TextManager.FormatServerMessage($"SubLoadError~[subname]={GameMain.GameSession.SubmarineInfo.Name}"), ChatMessageType.Error); yield return CoroutineStatus.Failure; } MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); bool isOutpost = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; if (serverSettings.AllowRespawn && missionAllowRespawn) { respawnManager = new RespawnManager(this, serverSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null); } if (campaign != null) { campaign.CargoManager.CreatePurchasedItems(); campaign.SendCrewState(null, default, null); } Level.Loaded?.SpawnNPCs(); Level.Loaded?.SpawnCorpses(); Level.Loaded?.PrepareBeaconStation(); AutoItemPlacer.PlaceIfNeeded(); CrewManager crewManager = campaign?.CrewManager; entityEventManager.RefreshEntityIDs(); bool hadBots = true; //assign jobs and spawnpoints separately for each team for (int n = 0; n < teamCount; n++) { var teamID = n == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2; Submarine.MainSubs[n].TeamID = teamID; foreach (Item item in Item.ItemList) { if (item.Submarine == null) { continue; } if (item.Submarine != Submarine.MainSubs[n] && !Submarine.MainSubs[n].DockedTo.Contains(item.Submarine)) { continue; } foreach (WifiComponent wifiComponent in item.GetComponents()) { wifiComponent.TeamID = Submarine.MainSubs[n].TeamID; } } foreach (Submarine sub in Submarine.MainSubs[n].DockedTo) { if (sub.Info.Type != SubmarineType.Player) { continue; } sub.TeamID = teamID; } //find the clients in this team List teamClients = teamCount == 1 ? new List(playingClients) : playingClients.FindAll(c => c.TeamID == teamID); if (serverSettings.AllowSpectating) { teamClients.RemoveAll(c => c.SpectateOnly); } //always allow the server owner to spectate even if it's disallowed in server settings teamClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); //if (!teamClients.Any() && n > 0) { continue; } AssignJobs(teamClients); List characterInfos = new List(); foreach (Client client in teamClients) { client.NeedsMidRoundSync = false; client.PendingPositionUpdates.Clear(); client.EntityEventLastSent.Clear(); client.LastSentEntityEventID = 0; client.LastRecvEntityEventID = 0; client.UnreceivedEntityEventCount = 0; if (client.CharacterInfo == null) { client.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, client.Name); } characterInfos.Add(client.CharacterInfo); if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.First) { client.CharacterInfo.Job = new Job(client.AssignedJob.First, client.AssignedJob.Second); } } List bots = new List(); // do not load new bots if we already have them if (crewManager == null || !crewManager.HasBots) { int botsToSpawn = serverSettings.BotSpawnMode == BotSpawnMode.Fill ? serverSettings.BotCount - characterInfos.Count : serverSettings.BotCount; for (int i = 0; i < botsToSpawn; i++) { var botInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName) { TeamID = teamID }; characterInfos.Add(botInfo); bots.Add(botInfo); } AssignBotJobs(bots, teamID); if (campaign != null) { foreach (CharacterInfo bot in bots) { crewManager?.AddCharacterInfo(bot); } } if (crewManager != null) { crewManager.HasBots = true; hadBots = false; } } List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[n]).ToList(); if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost && (Level.Loaded.StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) && Level.Loaded.StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) { spawnWaypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Level.Loaded.StartOutpost && wp.CurrentHull?.OutpostModuleTags != null && wp.CurrentHull.OutpostModuleTags.Contains("airlock")); while (spawnWaypoints.Count > characterInfos.Count) { spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count)); } while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count) { spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]); } } if (spawnWaypoints == null || !spawnWaypoints.Any()) { spawnWaypoints = mainSubWaypoints; } Debug.Assert(spawnWaypoints.Count == mainSubWaypoints.Count); for (int i = 0; i < teamClients.Count; i++) { Character spawnedCharacter = Character.Create(teamClients[i].CharacterInfo, spawnWaypoints[i].WorldPosition, teamClients[i].CharacterInfo.Name, isRemotePlayer: true, hasAi: false); spawnedCharacter.AnimController.Frozen = true; spawnedCharacter.TeamID = teamID; teamClients[i].Character = spawnedCharacter; var characterData = campaign?.GetClientCharacterData(teamClients[i]); if (characterData == null) { spawnedCharacter.GiveJobItems(mainSubWaypoints[i]); if (campaign != null) { characterData = campaign.SetClientCharacterData(teamClients[i]); characterData.HasSpawned = true; } } else { characterData.SpawnInventoryItems(spawnedCharacter, spawnedCharacter.Inventory); characterData.ApplyHealthData(spawnedCharacter); characterData.ApplyOrderData(spawnedCharacter); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); characterData.HasSpawned = true; } spawnedCharacter.OwnerClientEndPoint = teamClients[i].Connection.EndPointString; spawnedCharacter.OwnerClientName = teamClients[i].Name; } for (int i = teamClients.Count; i < teamClients.Count + bots.Count; i++) { Character spawnedCharacter = Character.Create(characterInfos[i], spawnWaypoints[i].WorldPosition, characterInfos[i].Name, isRemotePlayer: false, hasAi: true); spawnedCharacter.TeamID = teamID; spawnedCharacter.GiveJobItems(mainSubWaypoints[i]); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); } } if (crewManager != null && crewManager.HasBots) { if (hadBots) { //loaded existing bots -> init them crewManager.InitRound(); } else { //created new bots -> save them SaveUtil.SaveGame(GameMain.GameSession.SavePath); } } campaign?.LoadPets(); crewManager?.LoadActiveOrders(); foreach (Submarine sub in Submarine.MainSubs) { if (sub == null) { continue; } List spawnList = new List(); foreach (KeyValuePair kvp in serverSettings.ExtraCargo) { spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value)); } CargoManager.CreateItems(spawnList, sub); } TraitorManager = null; if (serverSettings.TraitorsEnabled == YesNoMaybe.Yes || (serverSettings.TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f)) { if (!(GameMain.GameSession?.GameMode is CampaignMode)) { if (!GameMain.Lua.game.overrideTraitors) { TraitorManager = new TraitorManager(); TraitorManager.Start(this); } } } GameAnalyticsManager.AddDesignEvent("Traitors:" + (TraitorManager == null ? "Disabled" : "Enabled")); yield return CoroutineStatus.Running; if (GameMain.Server?.ServerSettings?.Voting != null) { GameMain.Server.ServerSettings.Voting.ResetVotes(GameMain.Server.ConnectedClients); } GameMain.GameScreen.Select(); Log("Round started.", ServerLog.MessageType.ServerMessage); gameStarted = true; initiatedStartGame = false; GameMain.ResetFrameTime(); LastClientListUpdateID++; roundStartTime = DateTime.Now; GameMain.Lua.hook.Call("roundStart", new MoonSharp.Interpreter.DynValue[] { }); yield return CoroutineStatus.Success; } private void SendStartMessage(int seed, string levelSeed, GameSession gameSession, List clients, bool includesFinalize) { foreach (Client client in clients) { SendStartMessage(seed, levelSeed, gameSession, client, includesFinalize); } } private void SendStartMessage(int seed, string levelSeed, GameSession gameSession, Client client, bool includesFinalize) { MultiPlayerCampaign campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.STARTGAME); msg.Write(seed); msg.Write(gameSession.GameMode.Preset.Identifier); bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); msg.Write(serverSettings.AllowRespawn && missionAllowRespawn); msg.Write(serverSettings.AllowDisguises); msg.Write(serverSettings.AllowRewiring); msg.Write(serverSettings.LockAllDefaultWires); msg.Write(serverSettings.AllowRagdollButton); msg.Write(serverSettings.UseRespawnShuttle); msg.Write((byte)GameMain.Config.LosMode); msg.Write(includesFinalize); msg.WritePadBits(); serverSettings.WriteMonsterEnabled(msg); if (campaign == null) { msg.Write(levelSeed); msg.Write(serverSettings.SelectedLevelDifficulty); msg.Write(gameSession.SubmarineInfo.Name); msg.Write(gameSession.SubmarineInfo.MD5Hash.Hash); msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.Name); msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.Hash); msg.Write((byte)GameMain.GameSession.GameMode.Missions.Count()); foreach (Mission mission in GameMain.GameSession.GameMode.Missions) { msg.Write((short)MissionPrefab.List.IndexOf(mission.Prefab)); } } else { int nextLocationIndex = campaign.Map.Locations.FindIndex(l => l.LevelData == campaign.NextLevel); int nextConnectionIndex = campaign.Map.Connections.FindIndex(c => c.LevelData == campaign.NextLevel); msg.Write(campaign.CampaignID); msg.Write(nextLocationIndex); msg.Write(nextConnectionIndex); msg.Write(campaign.Map.SelectedLocationIndex); msg.Write(campaign.MirrorLevel); } if (includesFinalize) { WriteRoundStartFinalize(msg, client); } serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } private void SendRoundStartFinalize(Client client) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.STARTGAMEFINALIZE); WriteRoundStartFinalize(msg, client); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } private void WriteRoundStartFinalize(IWriteMessage msg, Client client) { //tell the client what content files they should preload var contentToPreload = GameMain.GameSession.EventManager.GetFilesToPreload(); msg.Write((ushort)contentToPreload.Count()); foreach (ContentFile contentFile in contentToPreload) { msg.Write((byte)contentFile.Type); msg.Write(contentFile.Path); } msg.Write(Submarine.MainSub?.Info.EqualityCheckVal ?? 0); msg.Write((byte)GameMain.GameSession.Missions.Count()); foreach (Mission mission in GameMain.GameSession.Missions) { msg.Write(mission.Prefab.Identifier); } msg.Write((byte)GameMain.GameSession.Level.EqualityCheckValues.Count); foreach (int equalityCheckValue in GameMain.GameSession.Level.EqualityCheckValues) { msg.Write(equalityCheckValue); } foreach (Mission mission in GameMain.GameSession.Missions) { mission.ServerWriteInitial(msg, client); } msg.Write(GameMain.GameSession.CrewManager != null); GameMain.GameSession.CrewManager?.ServerWriteActiveOrders(msg); } public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { if (!gameStarted) { return; } if (GameSettings.VerboseLogging) { Log("Ending the round...\n" + Environment.StackTrace.CleanupStackTrace(), ServerLog.MessageType.ServerMessage); } else { Log("Ending the round...", ServerLog.MessageType.ServerMessage); } GameMain.Lua.hook.Call("roundEnd", new MoonSharp.Interpreter.DynValue[] { }); string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded"); var traitorResults = TraitorManager?.GetEndResults() ?? new List(); List missions = GameMain.GameSession.Missions.ToList(); if (GameMain.GameSession.IsRunning) { GameMain.GameSession.EndRound(endMessage, traitorResults); } endRoundTimer = 0.0f; if (serverSettings.AutoRestart) { serverSettings.AutoRestartTimer = serverSettings.AutoRestartInterval; //send a netlobby update to get the clients' autorestart timers up to date GameMain.NetLobbyScreen.LastUpdateID++; } if (serverSettings.SaveServerLogs) { serverSettings.ServerLog.Save(); } GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; entityEventManager.Clear(); foreach (Client c in connectedClients) { c.EntityEventLastSent.Clear(); c.PendingPositionUpdates.Clear(); c.PositionUpdateLastSent.Clear(); } KarmaManager.OnRoundEnded(); #if DEBUG messageCount.Clear(); #endif respawnManager = null; gameStarted = false; if (connectedClients.Count > 0) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.ENDGAME); msg.Write((byte)transitionType); msg.Write(endMessage); msg.Write((byte)missions.Count); foreach (Mission mission in missions) { msg.Write(mission.Completed); } msg.Write(GameMain.GameSession?.WinningTeam == null ? (byte)0 : (byte)GameMain.GameSession.WinningTeam); msg.Write((byte)traitorResults.Count); foreach (var traitorResult in traitorResults) { traitorResult.ServerWrite(msg); } foreach (Client client in connectedClients) { serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); client.Character?.Info?.ClearCurrentOrders(); client.Character = null; client.HasSpawned = false; client.InGame = false; client.WaitForNextRoundRespawn = null; } } Submarine.Unload(); entityEventManager.Clear(); GameMain.NetLobbyScreen.Select(); Log("Round ended.", ServerLog.MessageType.ServerMessage); GameMain.NetLobbyScreen.RandomizeSettings(); } public override void AddChatMessage(ChatMessage message) { if (string.IsNullOrEmpty(message.Text)) { return; } string logMsg; if (message.SenderClient != null) { logMsg = GameServer.ClientLogName(message.SenderClient) + ": " + message.TranslatedText; } else { logMsg = message.TextWithSender; } Log(logMsg, ServerLog.MessageType.Chat); base.AddChatMessage(message); } private bool TryChangeClientName(Client c, IReadMessage inc) { UInt16 nameId = inc.ReadUInt16(); string newName = inc.ReadString(); string newJob = inc.ReadString(); CharacterTeamType newTeam = (CharacterTeamType)inc.ReadByte(); if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameID)) { return false; } c.NameID = nameId; newName = Client.SanitizeName(newName); if (newName == c.Name && newJob == c.PreferredJob && newTeam == c.PreferredTeam) { return false; } c.PreferredJob = newJob; c.PreferredTeam = newTeam; //update client list even if the name cannot be changed to the one sent by the client, //so the client will be informed what their actual name is LastClientListUpdateID++; if (newName == c.Name) { return false; } if (c.Connection != OwnerConnection) { if (!Client.IsValidName(newName, serverSettings)) { SendDirectChatMessage($"ServerMessage.NameChangeFailedSymbols~[newname]={newName}", c, ChatMessageType.ServerMessageBox); return false; } if (Homoglyphs.Compare(newName.ToLower(), ServerName.ToLower())) { SendDirectChatMessage($"ServerMessage.NameChangeFailedServerTooSimilar~[newname]={newName}", c, ChatMessageType.ServerMessageBox); return false; } if (c.KickVoteCount > 0) { SendDirectChatMessage($"ServerMessage.NameChangeFailedVoteKick~[newname]={newName}", c, ChatMessageType.ServerMessageBox); return false; } } Client nameTaken = ConnectedClients.Find(c2 => c != c2 && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower())); if (nameTaken != null) { SendDirectChatMessage($"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTaken.Name}", c, ChatMessageType.ServerMessageBox); return false; } SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={c.Name}~[newname]={newName}", ChatMessageType.Server); c.Name = newName; c.Connection.Name = newName; return true; } public override void KickPlayer(string playerName, string reason) { Client client = connectedClients.Find(c => c.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase) || (c.Character != null && c.Character.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase))); KickClient(client, reason); } public void KickClient(NetworkConnection conn, string reason) { if (conn == OwnerConnection) return; Client client = connectedClients.Find(c => c.Connection == conn); KickClient(client, reason); } public void KickClient(Client client, string reason, bool resetKarma = false) { if (client == null || client.Connection == OwnerConnection) return; if (resetKarma) { var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); if (previousPlayer != null) { previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f); } client.Karma = Math.Max(client.Karma, 50.0f); } string msg = DisconnectReason.Kicked.ToString(); string logMsg = $"ServerMessage.KickedFromServer~[client]={client.Name}"; DisconnectClient(client, logMsg, msg, reason, PlayerConnectionChangeType.Kicked); } public override void BanPlayer(string playerName, string reason, bool range = false, TimeSpan? duration = null) { Client client = connectedClients.Find(c => c.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase) || (c.Character != null && c.Character.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase))); if (client == null) { DebugConsole.ThrowError("Client \"" + playerName + "\" not found."); return; } BanClient(client, reason, range, duration); } public void BanClient(Client client, string reason, bool range = false, TimeSpan? duration = null) { if (client == null || client.Connection == OwnerConnection) { return; } var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); if (previousPlayer != null) { //reset karma to a neutral value, so if/when the ban is revoked the client wont get immediately punished by low karma again previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f); } client.Karma = Math.Max(client.Karma, 50.0f); string targetMsg = DisconnectReason.Banned.ToString(); DisconnectClient(client, $"ServerMessage.BannedFromServer~[client]={client.Name}", targetMsg, reason, PlayerConnectionChangeType.Banned); if (client.Connection is LidgrenConnection lidgrenConn && (client.SteamID == 0 || range)) { string ip = ""; ip = lidgrenConn.IPEndPoint.Address.IsIPv4MappedToIPv6 ? lidgrenConn.IPEndPoint.Address.MapToIPv4NoThrow().ToString() : lidgrenConn.IPEndPoint.Address.ToString(); if (range) { ip = BanList.ToRange(ip); } serverSettings.BanList.BanPlayer(client.Name, ip, reason, duration); } if (client.SteamID > 0) { serverSettings.BanList.BanPlayer(client.Name, client.SteamID, reason, duration); } if (client.OwnerSteamID > 0) { serverSettings.BanList.BanPlayer(client.Name, client.OwnerSteamID, reason, duration); } } public void BanPreviousPlayer(PreviousPlayer previousPlayer, string reason, bool range = false, TimeSpan? duration = null) { if (previousPlayer == null) { return; } //reset karma to a neutral value, so if/when the ban is revoked the client wont get immediately punished by low karma again previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f); if (!string.IsNullOrEmpty(previousPlayer.EndPoint) && (previousPlayer.SteamID == 0 || range)) { string ip = previousPlayer.EndPoint; if (range) { ip = BanList.ToRange(ip); } serverSettings.BanList.BanPlayer(previousPlayer.Name, ip, reason, duration); } if (previousPlayer.SteamID > 0) { serverSettings.BanList.BanPlayer(previousPlayer.Name, previousPlayer.SteamID, reason, duration); } if (previousPlayer.OwnerSteamID > 0) { serverSettings.BanList.BanPlayer(previousPlayer.Name, previousPlayer.OwnerSteamID, reason, duration); } string msg = $"ServerMessage.BannedFromServer~[client]={previousPlayer.Name}"; if (!string.IsNullOrWhiteSpace(reason)) { msg += $"/ /ServerMessage.Reason/: /{reason}"; } SendChatMessage(msg, ChatMessageType.Server, changeType: PlayerConnectionChangeType.Banned); } public override void UnbanPlayer(string playerName, string playerEndPoint) { if (!string.IsNullOrEmpty(playerEndPoint)) { serverSettings.BanList.UnbanEndPoint(playerEndPoint); } else if (!string.IsNullOrEmpty(playerName)) { serverSettings.BanList.UnbanPlayer(playerName); } } public void DisconnectClient(NetworkConnection senderConnection, string msg = "", string targetmsg = "") { if (senderConnection == OwnerConnection) { DebugConsole.NewMessage("Owner disconnected: closing the server...", Color.Yellow); Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage); GameMain.ShouldRun = false; } Client client = connectedClients.Find(x => x.Connection == senderConnection); if (client == null) return; DisconnectClient(client, msg, targetmsg, string.Empty, PlayerConnectionChangeType.Disconnected); } public void DisconnectClient(Client client, string msg = "", string targetmsg = "", string reason = "", PlayerConnectionChangeType changeType = PlayerConnectionChangeType.Disconnected) { if (client == null) return; GameMain.Lua.hook.Call("clientDisconnected", new MoonSharp.Interpreter.DynValue[] { MoonSharp.Interpreter.UserData.Create(client) }); if (gameStarted && client.Character != null) { client.Character.ClientDisconnected = true; client.Character.ClearInputs(); } client.Character = null; client.HasSpawned = false; client.WaitForNextRoundRespawn = null; client.InGame = false; if (string.IsNullOrWhiteSpace(msg)) { msg = $"ServerMessage.ClientLeftServer~[client]={ClientLogName(client)}"; } if (string.IsNullOrWhiteSpace(targetmsg)) { targetmsg = "ServerMessage.YouLeftServer"; } if (!string.IsNullOrWhiteSpace(reason)) { msg += $"/ /ServerMessage.Reason/: /{reason}"; targetmsg += $"/\n/ServerMessage.Reason/: /{reason}"; } if (client.SteamID != 0) { SteamManager.StopAuthSession(client.SteamID); } var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); if (previousPlayer == null) { previousPlayer = new PreviousPlayer(client); previousPlayers.Add(previousPlayer); } previousPlayer.Name = client.Name; previousPlayer.Karma = client.Karma; previousPlayer.KarmaKickCount = client.KarmaKickCount; previousPlayer.KickVoters.Clear(); foreach (Client c in connectedClients) { if (client.HasKickVoteFrom(c)) { previousPlayer.KickVoters.Add(c); } } serverPeer.Disconnect(client.Connection, targetmsg); client.Dispose(); connectedClients.Remove(client); KarmaManager.OnClientDisconnected(client); UpdateVoteStatus(); SendChatMessage(msg, ChatMessageType.Server, changeType: changeType); UpdateCrewFrame(); serverSettings.ServerDetailsChanged = true; refreshMasterTimer = DateTime.Now; } private void UpdateCrewFrame() { foreach (Client c in connectedClients) { if (c.Character == null || !c.InGame) continue; } } public void SendDirectChatMessage(string txt, Client recipient, ChatMessageType messageType = ChatMessageType.Server) { ChatMessage msg = ChatMessage.Create("", txt, messageType, null); SendDirectChatMessage(msg, recipient); } public void SendConsoleMessage(string txt, Client recipient) { ChatMessage msg = ChatMessage.Create("", txt, ChatMessageType.Console, null); SendDirectChatMessage(msg, recipient); } public void SendDirectChatMessage(ChatMessage msg, Client recipient) { if (recipient == null) { string errorMsg = "Attempted to send a chat message to a null client.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.SendDirectChatMessage:ClientNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } msg.NetStateID = recipient.ChatMsgQueue.Count > 0 ? (ushort)(recipient.ChatMsgQueue.Last().NetStateID + 1) : (ushort)(recipient.LastRecvChatMsgID + 1); recipient.ChatMsgQueue.Add(msg); recipient.LastChatMsgQueueID = msg.NetStateID; } /// /// Add the message to the chatbox and pass it to all clients who can receive it /// public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) { string senderName = ""; Client targetClient = null; if (type == null) { string command = ChatMessage.GetChatMessageCommand(message, out string tempStr); switch (command.ToLowerInvariant()) { case "r": case "radio": type = ChatMessageType.Radio; break; case "d": case "dead": type = ChatMessageType.Dead; break; default: if (command != "") { if (command.ToLower() == serverName.ToLower()) { //a private message to the host if (OwnerConnection != null) { targetClient = connectedClients.Find(c => c.Connection == OwnerConnection); } } else { targetClient = connectedClients.Find(c => command.ToLower() == c.Name.ToLower() || command.ToLower() == c.Character?.Name?.ToLower()); if (targetClient == null) { if (senderClient != null) { var chatMsg = ChatMessage.Create( "", $"ServerMessage.PlayerNotFound~[player]={command}", ChatMessageType.Error, null); SendDirectChatMessage(chatMsg, senderClient); } else { AddChatMessage($"ServerMessage.PlayerNotFound~[player]={command}", ChatMessageType.Error); } return; } } type = ChatMessageType.Private; } else { type = ChatMessageType.Default; } break; } message = tempStr; } if (gameStarted) { if (senderClient == null) { //msg sent by the server if (senderCharacter == null) { senderName = serverName; } else //msg sent by an AI character { senderName = senderCharacter.Name; } } else //msg sent by a client { senderCharacter = senderClient.Character; senderName = senderCharacter == null ? senderClient.Name : senderCharacter.Name; if (type == ChatMessageType.Private) { if (senderCharacter != null && !senderCharacter.IsDead || targetClient.Character != null && !targetClient.Character.IsDead) { //sender or target has an alive character, sending private messages not allowed SendDirectChatMessage(ChatMessage.Create("", $"ServerMessage.PrivateMessagesNotAllowed", ChatMessageType.Error, null), senderClient); return; } } //sender doesn't have a character or the character can't speak -> only ChatMessageType.Dead allowed else if (senderCharacter == null || senderCharacter.IsDead || senderCharacter.SpeechImpediment >= 100.0f) { type = ChatMessageType.Dead; } } } else { if (senderClient == null) { //msg sent by the server if (senderCharacter == null) { senderName = serverName; } else //sent by an AI character, not allowed when the game is not running { return; } } else //msg sent by a client { //game not started -> clients can only send normal and private chatmessages if (type != ChatMessageType.Private) type = ChatMessageType.Default; senderName = senderClient.Name; } } //check if the client is allowed to send the message WifiComponent senderRadio = null; switch (type) { case ChatMessageType.Radio: case ChatMessageType.Order: if (senderCharacter == null) { return; } //return if senderCharacter doesn't have a working radio var radio = senderCharacter.Inventory?.AllItems.FirstOrDefault(i => i.GetComponent() != null); if (radio == null || !senderCharacter.HasEquippedItem(radio)) { return; } senderRadio = radio.GetComponent(); if (!senderRadio.CanTransmit()) { return; } break; case ChatMessageType.Dead: //character still alive and capable of speaking -> dead chat not allowed if (senderClient != null && senderCharacter != null && !senderCharacter.IsDead && senderCharacter.SpeechImpediment < 100.0f) { return; } break; } if (type == ChatMessageType.Server || type == ChatMessageType.Error) { senderName = null; senderCharacter = null; } else if (type == ChatMessageType.Radio) { //send to chat-linked wifi components Signal s = new Signal(message, sender: senderCharacter, source: senderRadio.Item); senderRadio.TransmitSignal(s, sentFromChat: true); } //check which clients can receive the message and apply distance effects foreach (Client client in ConnectedClients) { string modifiedMessage = message; switch (type) { case ChatMessageType.Default: case ChatMessageType.Radio: case ChatMessageType.Order: if (senderCharacter != null && client.Character != null && !client.Character.IsDead) { if (senderCharacter != client.Character) { modifiedMessage = ChatMessage.ApplyDistanceEffect(message, (ChatMessageType)type, senderCharacter, client.Character); } //too far to hear the msg -> don't send if (string.IsNullOrWhiteSpace(modifiedMessage)) continue; } break; case ChatMessageType.Dead: //character still alive -> don't send if (client != senderClient && client.Character != null && !client.Character.IsDead) continue; break; case ChatMessageType.Private: //private msg sent to someone else than this client -> don't send if (client != targetClient && client != senderClient) continue; break; } var chatMsg = ChatMessage.Create( senderName, modifiedMessage, (ChatMessageType)type, senderCharacter, senderClient, changeType); SendDirectChatMessage(chatMsg, client); } if (type.Value != ChatMessageType.MessageBox) { string myReceivedMessage = type == ChatMessageType.Server || type == ChatMessageType.Error ? TextManager.GetServerMessage(message) : message; if (!string.IsNullOrWhiteSpace(myReceivedMessage)) { AddChatMessage(myReceivedMessage, (ChatMessageType)type, senderName, senderClient, senderCharacter); } } } public void SendOrderChatMessage(OrderChatMessage message) { if (message.Sender == null || message.Sender.SpeechImpediment >= 100.0f) { return; } //ChatMessageType messageType = ChatMessage.CanUseRadio(message.Sender) ? ChatMessageType.Radio : ChatMessageType.Default; //check which clients can receive the message and apply distance effects foreach (Client client in ConnectedClients) { string modifiedMessage = message.Text; if (message.Sender != null && client.Character != null && !client.Character.IsDead) { //too far to hear the msg -> don't send if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.TargetEntity, message.TargetCharacter, message.Sender), client); } string myReceivedMessage = message.Text; if (!string.IsNullOrWhiteSpace(myReceivedMessage)) { AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, myReceivedMessage, message.TargetEntity, message.TargetCharacter, message.Sender)); } } private void FileTransferChanged(FileSender.FileTransferOut transfer) { Client recipient = connectedClients.Find(c => c.Connection == transfer.Connection); if (transfer.FileType == FileTransferType.CampaignSave && (transfer.Status == FileTransferStatus.Sending || transfer.Status == FileTransferStatus.Finished) && recipient.LastCampaignSaveSendTime != null) { recipient.LastCampaignSaveSendTime.Second = (float)Lidgren.Network.NetTime.Now; } } public void SendCancelTransferMsg(FileSender.FileTransferOut transfer) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.FILE_TRANSFER); msg.Write((byte)FileTransferMessageType.Cancel); msg.Write((byte)transfer.ID); serverPeer.Send(msg, transfer.Connection, DeliveryMethod.ReliableOrdered); } public void UpdateVoteStatus() { if (connectedClients.Count == 0) return; if (serverSettings.Voting.VoteRunning) { // Required ratio cannot be met if (SubmarineVoteNoCount / (float)SubmarineVoteMax > 1f - serverSettings.SubmarineVoteRequiredRatio) { serverSettings.Voting.StopSubmarineVote(false); return; // Update will be re-sent via StopSubmarineVote } else if (SubmarineVoteYesCount / (float)SubmarineVoteMax >= serverSettings.SubmarineVoteRequiredRatio) { SwitchSubmarine(); return; // Update will be re-sent via StopSubmarineVote } } Client.UpdateKickVotes(connectedClients); int minimumKickVotes = Math.Max(1, (int)(connectedClients.Count * serverSettings.KickVoteRequiredRatio)); var clientsToKick = connectedClients.FindAll(c => c.Connection != OwnerConnection && !c.HasPermission(ClientPermissions.Kick) && !c.HasPermission(ClientPermissions.Ban) && !c.HasPermission(ClientPermissions.Unban) && c.KickVoteCount >= minimumKickVotes); foreach (Client c in clientsToKick) { var previousPlayer = previousPlayers.Find(p => p.MatchesClient(c)); if (previousPlayer != null) { //reset the client's kick votes (they can rejoin after their ban expires) previousPlayer.KickVoters.Clear(); } SendChatMessage($"ServerMessage.KickedFromServer~[client]={c.Name}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Kicked); KickClient(c, "ServerMessage.KickedByVote"); BanClient(c, "ServerMessage.KickedByVoteAutoBan", duration: TimeSpan.FromSeconds(serverSettings.AutoBanTime)); } GameMain.NetLobbyScreen.LastUpdateID++; SendVoteStatus(connectedClients); if (serverSettings.Voting.AllowEndVoting && EndVoteMax > 0 && ((float)EndVoteCount / (float)EndVoteMax) >= serverSettings.EndVoteRequiredRatio) { Log("Ending round by votes (" + EndVoteCount + "/" + (EndVoteMax - EndVoteCount) + ")", ServerLog.MessageType.ServerMessage); EndGame(); } } public void SendVoteStatus(List recipients) { if (!recipients.Any()) { return; } IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.UPDATE_LOBBY); msg.Write((byte)ServerNetObject.VOTE); serverSettings.Voting.ServerWrite(msg); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); foreach (var c in recipients) { serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable); } } private void SwitchSubmarine() { SubmarineInfo targetSubmarine = Voting.SubVote.Sub; VoteType voteType = Voting.SubVote.VoteType; int deliveryFee = 0; switch (voteType) { case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: // Pay for submarine GameMain.GameSession.PurchaseSubmarine(targetSubmarine); break; case VoteType.SwitchSub: deliveryFee = Voting.SubVote.DeliveryFee; break; default: return; } if (voteType != VoteType.PurchaseSub) { SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee); } serverSettings.Voting.StopSubmarineVote(true); } public void UpdateClientPermissions(Client client) { if (client.SteamID > 0) { serverSettings.ClientPermissions.RemoveAll(cp => cp.SteamID == client.SteamID); if (client.Permissions != ClientPermissions.None) { serverSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( client.Name, client.SteamID, client.Permissions, client.PermittedConsoleCommands)); } } else { serverSettings.ClientPermissions.RemoveAll(cp => client.EndpointMatches(cp.EndPoint)); if (client.Permissions != ClientPermissions.None) { serverSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( client.Name, client.Connection.EndPointString, client.Permissions, client.PermittedConsoleCommands)); } } foreach (Client recipient in connectedClients) { CoroutineManager.StartCoroutine(SendClientPermissionsAfterClientListSynced(recipient, client)); } serverSettings.SaveClientPermissions(); } private IEnumerable SendClientPermissionsAfterClientListSynced(Client recipient, Client client) { DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); while (recipient.LastRecvClientListUpdate < LastClientListUpdateID) { if (DateTime.Now > timeOut || GameMain.Server == null || !connectedClients.Contains(recipient)) { yield return CoroutineStatus.Success; } yield return null; } SendClientPermissions(recipient, client); yield return CoroutineStatus.Success; } private void SendClientPermissions(Client recipient, Client client) { if (recipient?.Connection == null) { return; } IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.PERMISSIONS); client.WritePermissions(msg); serverPeer.Send(msg, recipient.Connection, DeliveryMethod.Reliable); } public void GiveAchievement(Character character, string achievementIdentifier) { achievementIdentifier = achievementIdentifier.ToLowerInvariant(); foreach (Client client in connectedClients) { if (client.Character == character) { GiveAchievement(client, achievementIdentifier); return; } } } public void GiveAchievement(Client client, string achievementIdentifier) { if (client.GivenAchievements.Contains(achievementIdentifier)) return; client.GivenAchievements.Add(achievementIdentifier); IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.ACHIEVEMENT); msg.Write(achievementIdentifier); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } public void SendTraitorMessage(Client client, string message, string missionIdentifier, TraitorMessageType messageType) { if (client == null) { return; } var msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.TRAITOR_MESSAGE); msg.Write((byte)messageType); msg.Write(missionIdentifier ?? ""); msg.Write(message); serverPeer.Send(msg, client.Connection, DeliveryMethod.ReliableOrdered); } public void UpdateCheatsEnabled() { if (!connectedClients.Any()) { return; } IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.CHEATS_ENABLED); msg.Write(DebugConsole.CheatsEnabled); msg.WritePadBits(); foreach (Client c in connectedClients) { serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable); } } public void SetClientCharacter(Client client, Character newCharacter) { if (client == null) return; //the client's previous character is no longer a remote player if (client.Character != null) { client.Character.IsRemotePlayer = false; client.Character.OwnerClientEndPoint = null; client.Character.OwnerClientName = null; } if (newCharacter == null) { if (client.Character != null) //removing control of the current character { CreateEntityEvent(client.Character, new object[] { NetEntityEvent.Type.Control, null }); client.Character = null; } } else //taking control of a new character { newCharacter.ClientDisconnected = false; newCharacter.KillDisconnectedTimer = 0.0f; newCharacter.ResetNetState(); if (client.Character != null) { newCharacter.LastNetworkUpdateID = client.Character.LastNetworkUpdateID; } if (newCharacter.Info != null && newCharacter.Info.Character == null) { newCharacter.Info.Character = newCharacter; } newCharacter.OwnerClientEndPoint = client.Connection.EndPointString; newCharacter.OwnerClientName = client.Name; newCharacter.IsRemotePlayer = true; newCharacter.Enabled = true; client.Character = newCharacter; CreateEntityEvent(newCharacter, new object[] { NetEntityEvent.Type.Control, client }); } } private void UpdateCharacterInfo(IReadMessage message, Client sender) { sender.SpectateOnly = message.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); if (sender.SpectateOnly) { return; } Gender gender = Gender.Male; Race race = Race.White; int headSpriteId = 0; try { gender = (Gender)message.ReadByte(); race = (Race)message.ReadByte(); headSpriteId = message.ReadByte(); } catch (Exception e) { //gender = Gender.Male; //race = Race.White; //headSpriteId = 0; DebugConsole.Log("Received invalid characterinfo from \"" + sender.Name + "\"! { " + e.Message + " }"); } int hairIndex = message.ReadByte(); int beardIndex = message.ReadByte(); int moustacheIndex = message.ReadByte(); int faceAttachmentIndex = message.ReadByte(); List> jobPreferences = new List>(); int count = message.ReadByte(); // TODO: modding support? for (int i = 0; i < Math.Min(count, 3); i++) { string jobIdentifier = message.ReadString(); int variant = message.ReadByte(); if (JobPrefab.Prefabs.ContainsKey(jobIdentifier)) { jobPreferences.Add(new Pair(JobPrefab.Prefabs[jobIdentifier], variant)); } } sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, sender.Name); sender.CharacterInfo.RecreateHead(headSpriteId, race, gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); if (jobPreferences.Count > 0) { sender.JobPreferences = jobPreferences; } } public void AssignJobs(List unassigned) { var jobList = JobPrefab.Prefabs.ToList(); unassigned = new List(unassigned); unassigned = unassigned.OrderBy(sp => Rand.Int(int.MaxValue)).ToList(); Dictionary assignedClientCount = new Dictionary(); foreach (JobPrefab jp in jobList) { assignedClientCount.Add(jp, 0); } CharacterTeamType teamID = CharacterTeamType.None; if (unassigned.Count > 0) { teamID = unassigned[0].TeamID; } //if we're playing a multiplayer campaign, check which clients already have a character and a job //(characters are persistent in campaigns) if (GameMain.GameSession.GameMode is MultiPlayerCampaign multiplayerCampaign) { var campaignAssigned = multiplayerCampaign.GetAssignedJobs(connectedClients); //remove already assigned clients from unassigned unassigned.RemoveAll(u => campaignAssigned.ContainsKey(u)); //add up to assigned client count foreach (KeyValuePair clientJob in campaignAssigned) { assignedClientCount[clientJob.Value.Prefab]++; clientJob.Key.AssignedJob = new Pair(clientJob.Value.Prefab, clientJob.Value.Variant); } } //count the clients who already have characters with an assigned job foreach (Client c in connectedClients) { if (c.TeamID != teamID || unassigned.Contains(c)) { continue; } if (c.Character?.Info?.Job != null && !c.Character.IsDead) { assignedClientCount[c.Character.Info.Job.Prefab]++; } } //if any of the players has chosen a job that is Always Allowed, give them that job for (int i = unassigned.Count - 1; i >= 0; i--) { if (unassigned[i].JobPreferences.Count == 0) { continue; } if (!unassigned[i].JobPreferences.Any() || !unassigned[i].JobPreferences[0].First.AllowAlways) { continue; } unassigned[i].AssignedJob = unassigned[i].JobPreferences[0]; unassigned.RemoveAt(i); } // Assign the necessary jobs that are always required at least one, in vanilla this means in practice the captain bool unassignedJobsFound = true; while (unassignedJobsFound && unassigned.Any()) { unassignedJobsFound = false; foreach (JobPrefab jobPrefab in jobList) { if (unassigned.Count == 0) { break; } if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) { continue; } // 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. Client client = FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: false); if (client != null) { AssignJob(client, jobPrefab); } } if (unassigned.Any()) { // Another pass, force required jobs that are not yet filled. foreach (JobPrefab jobPrefab in jobList) { if (unassigned.Count == 0) { break; } if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) { continue; } AssignJob(FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: true), jobPrefab); } } void AssignJob(Client client, JobPrefab jobPrefab) { client.AssignedJob = client.JobPreferences.FirstOrDefault(jp => jp.First == jobPrefab) ?? new Pair(jobPrefab, Rand.Int(jobPrefab.Variants)); assignedClientCount[jobPrefab]++; unassigned.Remove(client); //the job still needs more crew members, set unassignedJobsFound to true to keep the while loop running if (assignedClientCount[jobPrefab] < jobPrefab.MinNumber) { unassignedJobsFound = true; } } } List availableSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && wp.Submarine != null && wp.Submarine.TeamID == teamID); List unassignedSpawnPoints = new List(availableSpawnPoints); /*bool canAssign = false; do { canAssign = false; foreach (WayPoint spawnPoint in unassignedSpawnPoints) { if (unassigned.Count == 0) { break; } JobPrefab job = spawnPoint.AssignedJob ?? JobPrefab.List.Values.GetRandom(); if (assignedClientCount[job] >= job.MaxNumber) { continue; } Client assignedClient = FindClientWithJobPreference(unassigned, job, true); if (assignedClient != null) { assignedClient.AssignedJob = job; assignedClientCount[job]++; unassigned.Remove(assignedClient); canAssign = true; } } } while (unassigned.Count > 0 && canAssign);*/ // Attempt to give the clients a job they have in their job preferences. // First evaluate all the primary preferences, then all the secondary etc. for (int preferenceIndex = 0; preferenceIndex < 3; preferenceIndex++) { if (unassignedSpawnPoints.None()) { break; } for (int i = unassigned.Count - 1; i >= 0; i--) { if (unassignedSpawnPoints.None()) { break; } Client client = unassigned[i]; if (preferenceIndex >= client.JobPreferences.Count) { continue; } var preferredJob = client.JobPreferences[preferenceIndex]; JobPrefab jobPrefab = preferredJob.First; if (assignedClientCount[jobPrefab] >= jobPrefab.MaxNumber || client.Karma < jobPrefab.MinKarma) { //can't assign this job if maximum number has reached or the clien't karma is too low continue; } //give the client their preferred job if there's a spawnpoint available for that job var matchingSpawnPoint = unassignedSpawnPoints.Find(s => s.AssignedJob == jobPrefab); if (matchingSpawnPoint == null && !availableSpawnPoints.Any(s => s.AssignedJob == jobPrefab)) { //if the job is not available in any spawnpoint (custom job?), treat empty spawnpoints //as a matching ones matchingSpawnPoint = unassignedSpawnPoints.Find(s => s.AssignedJob == null); } if (matchingSpawnPoint != null) { unassignedSpawnPoints.Remove(matchingSpawnPoint); client.AssignedJob = preferredJob; assignedClientCount[jobPrefab]++; unassigned.RemoveAt(i); } } } //give random jobs to rest of the clients foreach (Client c in unassigned) { //find all jobs that are still available var remainingJobs = jobList.FindAll(jp => assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma); //all jobs taken, give a random job if (remainingJobs.Count == 0) { DebugConsole.ThrowError("Failed to assign a suitable job for \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); int jobIndex = Rand.Range(0, jobList.Count); int skips = 0; while (c.Karma < jobList[jobIndex].MinKarma) { jobIndex++; skips++; if (jobIndex >= jobList.Count) { jobIndex -= jobList.Count; } if (skips >= jobList.Count) { break; } } c.AssignedJob = c.JobPreferences.FirstOrDefault(jp => jp.First == jobList[jobIndex]) ?? new Pair(jobList[jobIndex], 0); assignedClientCount[c.AssignedJob.First]++; } //if one of the client's preferences is still available, give them that job else if (c.JobPreferences.Any(jp => remainingJobs.Contains(jp.First))) { foreach (Pair preferredJob in c.JobPreferences) { c.AssignedJob = preferredJob; assignedClientCount[preferredJob.First]++; break; } } else //none of the client's preferred jobs available, choose a random job { c.AssignedJob = new Pair(remainingJobs[Rand.Range(0, remainingJobs.Count)], 0); assignedClientCount[c.AssignedJob.First]++; } } } public void AssignBotJobs(List bots, CharacterTeamType teamID) { Dictionary assignedPlayerCount = new Dictionary(); foreach (JobPrefab jp in JobPrefab.Prefabs) { assignedPlayerCount.Add(jp, 0); } //count the clients who already have characters with an assigned job foreach (Client c in connectedClients) { if (c.TeamID != teamID) continue; if (c.Character?.Info?.Job != null && !c.Character.IsDead) { assignedPlayerCount[c.Character.Info.Job.Prefab]++; } else if (c.CharacterInfo?.Job != null) { assignedPlayerCount[c.CharacterInfo?.Job.Prefab]++; } } List unassignedBots = new List(bots); List spawnPoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && wp.Submarine != null && wp.Submarine.TeamID == teamID) .OrderBy(sp => Rand.Int(int.MaxValue)) .OrderBy(sp => sp.AssignedJob == null ? 0 : 1) .ToList(); bool canAssign = false; do { canAssign = false; foreach (WayPoint spawnPoint in spawnPoints) { if (unassignedBots.Count == 0) { break; } JobPrefab jobPrefab = spawnPoint.AssignedJob ?? JobPrefab.Prefabs.GetRandom(); if (assignedPlayerCount[jobPrefab] >= jobPrefab.MaxNumber) { continue; } var variant = Rand.Range(0, jobPrefab.Variants, Rand.RandSync.Server); unassignedBots[0].Job = new Job(jobPrefab, variant); assignedPlayerCount[jobPrefab]++; unassignedBots.Remove(unassignedBots[0]); canAssign = true; } } while (unassignedBots.Count > 0 && canAssign); //find a suitable job for the rest of the bots foreach (CharacterInfo c in unassignedBots) { //find all jobs that are still available var remainingJobs = JobPrefab.Prefabs.Where(jp => assignedPlayerCount[jp] < jp.MaxNumber); //all jobs taken, give a random job if (remainingJobs.Count() == 0) { 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..."); c.Job = Job.Random(); assignedPlayerCount[c.Job.Prefab]++; } else //some jobs still left, choose one of them by random { var job = remainingJobs.GetRandom(); var variant = Rand.Range(0, job.Variants); c.Job = new Job(job, variant); assignedPlayerCount[c.Job.Prefab]++; } } } private Client FindClientWithJobPreference(List clients, JobPrefab job, bool forceAssign = false) { int bestPreference = int.MaxValue; Client preferredClient = null; foreach (Client c in clients) { if (ServerSettings.KarmaEnabled && c.Karma < job.MinKarma) { continue; } int index = c.JobPreferences.IndexOf(c.JobPreferences.Find(j => j.First == job)); if (index > -1 && index < bestPreference) { bestPreference = index; preferredClient = c; } } //none of the clients wants the job, assign it to random client if (forceAssign && preferredClient == null) { preferredClient = clients[Rand.Int(clients.Count)]; } return preferredClient; } public void UpdateMissionState(Mission mission, int state) { foreach (var client in connectedClients) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.MISSION); int missionIndex = GameMain.GameSession.GetMissionIndex(mission); msg.Write((byte)(missionIndex == -1 ? 255: missionIndex)); msg.Write((ushort)state); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } } public static string CharacterLogName(Character character) { if (character == null) { return "[NULL]"; } Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character); return ClientLogName(client, character.LogName); } public static void Log(string line, ServerLog.MessageType messageType) { if (GameMain.Server == null || !GameMain.Server.ServerSettings.SaveServerLogs) { return; } GameMain.Server.ServerSettings.ServerLog.WriteLine(line, messageType); foreach (Client client in GameMain.Server.ConnectedClients) { if (!client.HasPermission(ClientPermissions.ServerLog)) continue; //use sendername as the message type GameMain.Server.SendDirectChatMessage( ChatMessage.Create(messageType.ToString(), line, ChatMessageType.ServerLog, null), client); } } public Tuple FindPreviousClientData(Client client) { var player = previousPlayers.Find(p => p.MatchesClient(client)); if (player != null) { return Tuple.Create(player.SteamID, player.EndPoint); } return null; } public override void Disconnect() { if (started) { started = false; serverSettings.BanList.Save(); if (GameMain.NetLobbyScreen.SelectedSub != null) { serverSettings.SelectedSubmarine = GameMain.NetLobbyScreen.SelectedSub.Name; } if (GameMain.NetLobbyScreen.SelectedShuttle != null) { serverSettings.SelectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle.Name; } if (GameMain.NetLobbyScreen.CampaignSubmarines != null) { string submarinesString = string.Empty; for (int i = 0; i < GameMain.NetLobbyScreen.CampaignSubmarines.Count; i++) { submarinesString += GameMain.NetLobbyScreen.CampaignSubmarines[i].Name + ServerSettings.SubmarineSeparatorChar; } submarinesString.Trim(ServerSettings.SubmarineSeparatorChar); serverSettings.CampaignSubmarines = submarinesString; } serverSettings.SaveSettings(); if (serverSettings.SaveServerLogs) { Log("Shutting down the server...", ServerLog.MessageType.ServerMessage); serverSettings.ServerLog.Save(); } GameAnalyticsManager.AddDesignEvent("GameServer:ShutDown"); serverPeer?.Close(DisconnectReason.ServerShutdown.ToString()); SteamManager.CloseServer(); } } } partial class PreviousPlayer { public string Name; public string EndPoint; public UInt64 SteamID; public UInt64 OwnerSteamID; public float Karma; public int KarmaKickCount; public readonly List KickVoters = new List(); public PreviousPlayer(Client c) { Name = c.Name; EndPoint = c.Connection?.EndPointString ?? ""; SteamID = c.SteamID; OwnerSteamID = c.OwnerSteamID; } public bool MatchesClient(Client c) { if (c.SteamID > 0 && SteamID > 0) { return c.SteamID == SteamID; } return c.EndpointMatches(EndPoint); } } }