6 using Microsoft.Xna.Framework;
8 using System.Collections.Generic;
9 using System.Collections.Immutable;
10 using System.Diagnostics;
12 using System.Threading;
13 using System.Xml.Linq;
14 using MoonSharp.Interpreter;
32 if (
string.IsNullOrEmpty(value)) {
return; }
39 private readonly List<Client> connectedClients =
new List<Client>();
44 private readonly List<Client> clientsAttemptingToReconnectSoon =
new List<Client>();
48 private readonly List<PreviousPlayer> previousPlayers =
new List<PreviousPlayer>();
50 private int roundStartSeed;
58 private DateTime refreshMasterTimer;
59 private readonly TimeSpan refreshMasterInterval =
new TimeSpan(0, 0, 60);
60 private bool registeredToSteamMaster;
62 private DateTime roundStartTime;
64 private bool wasReadyToStartAutomatically;
65 private bool autoRestartTimerRunning;
71 private const int PvpAutoBalanceCountdown = 10;
72 private static float pvpAutoBalanceCountdownRemaining = -1;
73 private int Team1Count => GetPlayingClients().Count(
static c => c.TeamID ==
CharacterTeamType.Team1);
74 private int Team2Count => GetPlayingClients().Count(
static c => c.TeamID ==
CharacterTeamType.Team2);
79 private static readonly Queue<ChatMessage> pendingMessagesToOwner =
new Queue<ChatMessage>();
87 private bool initiatedStartGame;
102 return traitorManager;
107 public void PrintSenderTransters()
111 DebugConsole.NewMessage(transfer.FileName +
" " + transfer.Progress.ToString());
120 return connectedClients;
127 get {
return entityEventManager; }
136 private readonly Option<int> ownerKey;
137 private readonly Option<P2PEndpoint> ownerEndpoint;
141 lock (clientsAttemptingToReconnectSoon)
143 clientsAttemptingToReconnectSoon.Clear();
149 lock (clientsAttemptingToReconnectSoon)
152 foreach (var client
in clientsAttemptingToReconnectSoon)
161 if (found is not
null)
163 clientsAttemptingToReconnectSoon.Remove(found);
180 Option<int> ownerKey,
181 Option<P2PEndpoint> ownerEndpoint)
183 if (name.Length > NetConfig.ServerNameMaxLength)
185 name = name.Substring(0, NetConfig.ServerNameMaxLength);
188 LastClientListUpdateID = 0;
197 this.ownerKey = ownerKey;
199 this.ownerEndpoint = ownerEndpoint;
211 OnInitializationComplete,
215 if (ownerEndpoint.TryUnwrap(out var endpoint))
218 serverPeer =
new P2PServerPeer(endpoint, ownerKey.Fallback(0),
ServerSettings, callbacks);
222 Log(
"Using Lidgren networking. Manual port forwarding may be required. If players cannot connect to the server, you may want to use the in-game hosting menu (which uses Steamworks and EOS networking and does not require port forwarding).",
ServerLog.
MessageType.ServerMessage);
223 serverPeer =
new LidgrenServerPeer(ownerKey,
ServerSettings, callbacks);
224 if (registerToServerList)
227 Eos.EosSessionManager.UpdateOwnedSession(Option.None,
ServerSettings);
259 GameAnalyticsManager.AddDesignEvent(
"GameServer:Start");
268 pendingMessagesToOwner.Enqueue(
ChatMessage.
Create(
string.Empty, message, messageType, sender:
null));
276 if (ownerClient ==
null)
278 DebugConsole.ThrowError(
"Owner client not found! Can't set permissions");
288 foreach (var c
in tempList)
290 DisconnectClient(c.Connection, PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
295 DisconnectClient(conn, PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
300 private void OnInitializationComplete(
NetworkConnection connection,
string clientName)
303 Client newClient =
new Client(clientName, GetNewClientSessionId());
309 connectedClients.Add(newClient);
311 var previousPlayer = previousPlayers.Find(p => p.MatchesClient(newClient));
312 if (previousPlayer !=
null)
314 newClient.
Karma = previousPlayer.Karma;
316 foreach (
Client c
in previousPlayer.KickVoters)
318 if (!connectedClients.Contains(c)) {
continue; }
323 LastClientListUpdateID++;
328 foreach (var command
in DebugConsole.Commands)
335 GameMain.LuaCs.Hook.Call(
"client.connected", newClient);
340 if (previousPlayer !=
null && previousPlayer.Name != newClient.
Name)
342 string prevNameSanitized = previousPlayer.Name.Replace(
"‖",
"");
343 SendChatMessage($
"ServerMessage.PreviousClientName~[client]={ClientLogName(newClient)}~[previousname]={prevNameSanitized}",
ChatMessageType.Server);
344 previousPlayer.Name = newClient.
Name;
352 scp.AddressOrAccountId.TryGet(out AccountId accountId)
353 ? newClient.
AccountId.ValueEquals(accountId)
356 if (savedPermissions !=
null)
358 newClient.
SetPermissions(savedPermissions.Permissions, savedPermissions.PermittedCommands);
363 if (defaultPerms !=
null)
365 newClient.
SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands);
375 foreach (
Client otherClient
in connectedClients)
377 if (otherClient == newClient) {
continue; }
378 CoroutineManager.StartCoroutine(SendClientPermissionsAfterClientListSynced(newClient, otherClient));
382 private void OnClientDisconnect(
NetworkConnection connection, PeerDisconnectPacket peerDisconnectPacket)
384 Client connectedClient = connectedClients.Find(c => c.
Connection == connection);
391 dosProtection.Update(deltaTime);
393 if (!started) {
return; }
395 if (ChildServerRelay.HasShutDown)
409 foreach (var c
in connectedClients)
411 c.VoipServerDecoder.DebugUpdate(deltaTime);
419 entityEventManager.Update(connectedClients);
428 Client owner = connectedClients.Find(c => (c.Character ==
null || c.Character == character) && character.
IsClientOwner(c));
429 bool canOwnerTakeControl =
447 if (canOwnerTakeControl)
452 else if (canOwnerTakeControl &&
458 character.
Revive(removeAfflictions:
false, createNetworkEvent:
true);
468 connectedClients.All(c => !c.UsingFreeCam && (c.Character ==
null || c.Character.IsDead || c.Character.IsIncapacitated));
469 bool isSomeoneIncapacitatedNotDead =
470 connectedClients.Any(c => !c.UsingFreeCam && c.Character is { IsDead: false, IsIncapacitated: true });
472 bool subAtLevelEnd =
false;
477 int charactersInsideOutpost = connectedClients.Count(c =>
478 c.Character !=
null &&
479 !c.Character.IsDead && !c.Character.IsUnconscious &&
481 int charactersOutsideOutpost = connectedClients.Count(c =>
482 c.Character !=
null &&
483 !c.Character.IsDead && !c.Character.IsUnconscious &&
491 (charactersInsideOutpost > charactersOutsideOutpost);
500 if (permadeathMode && isCrewDown)
511 EndRoundDelay = isSomeoneIncapacitatedNotDead ? 120.0f : 5.0f;
519 else if (isCrewDown &&
534 EndRoundDelay = isSomeoneIncapacitatedNotDead ? 120.0f : 2.0f;
545 if (permadeathMode && isCrewDown)
547 Log(
"Ending round (entire crew dead or down and did not acquire new characters in time)",
ServerLog.
MessageType.ServerMessage);
553 else if (subAtLevelEnd)
559 Log(
"Ending round (no players left standing and respawning is not enabled during this round)",
ServerLog.
MessageType.ServerMessage);
569 else if (initiatedStartGame)
573 if (startGameCoroutine !=
null && !CoroutineManager.IsCoroutineRunning(startGameCoroutine))
580 startGameCoroutine.
Exception.Message +
'\n' +
581 (startGameCoroutine.
Exception.StackTrace?.CleanupStackTrace() ??
"null"),
589 startGameCoroutine =
null;
590 initiatedStartGame =
false;
598 bool shouldAutoRestart = connectedClients.Any(c =>
602 if (shouldAutoRestart != autoRestartTimerRunning)
604 autoRestartTimerRunning = shouldAutoRestart;
608 if (autoRestartTimerRunning)
614 bool readyToStartAutomatically =
false;
617 readyToStartAutomatically =
true;
621 int clientsReady = connectedClients.Count(c => c.GetVote<
bool>(VoteType.StartRound));
624 readyToStartAutomatically =
true;
627 if (readyToStartAutomatically)
632 wasReadyToStartAutomatically = readyToStartAutomatically;
635 lock (clientsAttemptingToReconnectSoon)
637 foreach (var client
in clientsAttemptingToReconnectSoon)
639 client.DeleteDisconnectedTimer -= deltaTime;
642 clientsAttemptingToReconnectSoon.RemoveAll(
static c => c.DeleteDisconnectedTimer < 0f);
645 foreach (
Client c
in connectedClients)
658 if (pvpAutoBalanceCountdownRemaining > 0)
663 StopAutoBalanceCountdown();
667 float prevTimeRemaining = pvpAutoBalanceCountdownRemaining;
668 pvpAutoBalanceCountdownRemaining -= deltaTime;
669 if (pvpAutoBalanceCountdownRemaining <= 0)
671 pvpAutoBalanceCountdownRemaining = -1;
678 int currentTimeRemainingInteger = (int)Math.Ceiling(pvpAutoBalanceCountdownRemaining);
679 if (Math.Ceiling(prevTimeRemaining) > currentTimeRemainingInteger && currentTimeRemainingInteger % 5 == 0)
682 TextManager.GetWithVariable(
"AutoBalance.CountdownRemaining",
"[number]", currentTimeRemainingInteger.ToString()).Value,
691 IEnumerable<Client> kickAFK = connectedClients.FindAll(c =>
694 foreach (
Client c
in kickAFK)
700 serverPeer.Update(deltaTime);
703 if (!started) {
return; }
706 if (updateTimer < DateTime.Now)
718 DebugConsole.ThrowError(
"Failed to write a network message for the client \"" + c.
Name +
"\"!", e);
720 string errorMsg =
"Failed to write a network message for a client! (MidRoundSyncing: " + c.
NeedsMidRoundSync +
")\n"
721 + e.Message +
"\n" + e.StackTrace.CleanupStackTrace();
722 if (e.InnerException !=
null)
724 errorMsg +=
"\nInner exception: " + e.InnerException.Message +
"\n" + e.InnerException.StackTrace.CleanupStackTrace();
727 GameAnalyticsManager.AddErrorEventOnce(
728 "GameServer.Update:ClientWriteFailed" + e.StackTrace.CleanupStackTrace(),
729 GameAnalyticsManager.ErrorSeverity.Error,
752 updateTimer = DateTime.Now + UpdateInterval;
757 if (registeredToSteamMaster)
759 bool refreshSuccessful = SteamManager.RefreshServerDetails(
this);
760 if (GameSettings.CurrentConfig.VerboseLogging)
762 Log(refreshSuccessful ?
763 "Refreshed server info on the Steam server list." :
764 "Refreshing server info on the Steam server list failed.",
ServerLog.
MessageType.ServerMessage);
768 Eos.EosSessionManager.UpdateOwnedSession(Option.None,
ServerSettings);
771 refreshMasterTimer = DateTime.Now + refreshMasterInterval;
776 private double lastPingTime;
777 private byte[] lastPingData;
778 private void UpdatePing()
780 if (Timing.TotalTime > lastPingTime + 1.0)
782 lastPingData ??=
new byte[64];
783 for (
int i = 0; i < lastPingData.Length; i++)
785 lastPingData[i] = (byte)Rand.Range(33, 126);
787 lastPingTime = Timing.TotalTime;
793 pingReq.
WriteByte((
byte)lastPingData.Length);
794 pingReq.
WriteBytes(lastPingData, 0, lastPingData.Length);
795 serverPeer.Send(pingReq, c.
Connection, DeliveryMethod.Unreliable);
805 serverPeer.Send(pingInf, c.
Connection, DeliveryMethod.Unreliable);
810 private readonly DoSProtection dosProtection =
new();
814 var connectedClient = connectedClients.Find(c => c.
Connection == sender);
816 using var _ = dosProtection.Start(connectedClient);
820 GameMain.LuaCs.Networking.NetMessageReceived(inc, header, connectedClient);
826 if (responseLen != lastPingData.Length) {
return; }
827 for (
int i = 0; i < responseLen; i++)
830 if (b != lastPingData[i]) {
return; }
832 connectedClient.Ping = (UInt16)((Timing.TotalTime - lastPingTime) * 1000);
835 if (connectedClient !=
null)
838 UpdateCharacterInfo(inc, connectedClient);
843 SendStartMessage(roundStartSeed, GameMain.GameSession.Level.Seed, GameMain.GameSession, connectedClient,
true);
848 if (isRoundStartWarningActive)
850 foreach (
Client c
in connectedClients)
853 serverPeer.Send(msg, c.
Connection, DeliveryMethod.Reliable);
857 AbortStartGameIfWarningActive();
860 if (connectedClient ==
null)
862 DebugConsole.AddWarning(
"Received a REQUEST_STARTGAMEFINALIZE message. Client not connected, ignoring the message.");
864 else if (!GameStarted)
866 DebugConsole.AddWarning(
"Received a REQUEST_STARTGAMEFINALIZE message. Game not started, ignoring the message.");
870 SendRoundStartFinalize(connectedClient);
874 ClientReadLobby(inc);
877 if (!GameStarted) {
return; }
878 ClientReadIngame(inc);
888 CampaignSettings settings = INetSerializableStruct.Read<CampaignSettings>(inc);
890 var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash);
898 if (matchingSub ==
null)
901 TextManager.GetWithVariable(
"CampaignStartFailedSubNotFound",
"[subname]", subName).Value,
906 string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName);
907 if (CampaignMode.AllowedToManageCampaign(connectedClient,
ClientPermissions.ManageRound))
909 using (dosProtection.Pause(connectedClient))
913 MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings);
923 uint backupIndex = isBackup ? inc.
ReadUInt32() : uint.MinValue;
930 if (CampaignMode.AllowedToManageCampaign(connectedClient,
ClientPermissions.ManageRound))
932 using (dosProtection.Pause(connectedClient))
934 CampaignDataPath dataPath;
937 string backupPath = SaveUtil.GetBackupPath(savePath, backupIndex);
938 dataPath =
new CampaignDataPath(loadPath: backupPath, savePath: savePath);
942 dataPath = CampaignDataPath.CreateRegular(savePath);
945 MultiPlayerCampaign.LoadCampaign(dataPath, connectedClient);
954 if (connectedClient.SessionId !=
id)
957 DebugConsole.ThrowError(
958 "Client \"" + connectedClient.Name +
"\" sent a VOIP update that didn't match its ID (" +
id.ToString() +
"!=" + connectedClient.SessionId.ToString() +
")");
972 ClientReadServerCommand(inc);
975 ReadCrewMessage(inc, connectedClient);
978 ReadMoneyMessage(inc, connectedClient);
981 ReadRewardDistributionMessage(inc, connectedClient);
984 ResetRewardDistribution(connectedClient);
987 ReadMedicalMessage(inc, connectedClient);
990 ReadCircuitBoxMessage(inc, connectedClient);
993 ReadyCheck.ServerRead(inc, connectedClient);
996 ReadReadyToSpawnMessage(inc, connectedClient);
999 ReadTakeOverBotMessage(inc, connectedClient);
1008 GameMain.GameSession?.EventManager.ServerRead(inc, connectedClient);
1011 UpdateCharacterInfo(inc, connectedClient);
1014 SendBackupIndices(inc, connectedClient);
1017 HandleClientError(inc, connectedClient);
1026 var indexData = SaveUtil.GetIndexData(savePath);
1030 msg.WriteNetSerializableStruct(indexData.ToNetCollection());
1031 serverPeer?.Send(msg, connectedClient.
Connection, DeliveryMethod.Reliable);
1036 string errorStr =
"Unhandled error report";
1037 string errorStrNoName = errorStr;
1045 errorStr = errorStrNoName =
"Expecting event id " + expectedID.ToString() +
", received " + receivedID.ToString();
1051 List<string> subNames =
new List<string>();
1052 for (
int i = 0; i < subCount; i++)
1056 Entity entity = Entity.FindEntityByID(entityID);
1059 errorStr = errorStrNoName =
"Received an update for an entity that doesn't exist (event id " + eventID.ToString() +
", entity id " + entityID.ToString() +
").";
1063 errorStr = $
"Missing character {character.Name} (event id {eventID}, entity id {entityID}).";
1064 errorStrNoName = $
"Missing character {character.SpeciesName} (event id {eventID}, entity id {entityID}).";
1066 else if (entity is
Item item)
1068 errorStr = errorStrNoName = $
"Missing item {item.Name}, sub: {item.Submarine?.Info?.Name ?? "none
"} (event id {eventID}, entity id {entityID}).";
1072 errorStr = errorStrNoName = $
"Missing entity {entity}, sub: {entity.Submarine?.Info?.Name ?? "none
"} (event id {eventID}, entity id {entityID}).";
1076 var serverSubNames =
Submarine.Loaded.Select(s => s.Info.Name);
1077 if (subCount !=
Submarine.Loaded.Count || !subNames.SequenceEqual(serverSubNames))
1079 string subErrorStr = $
" Loaded submarines don't match (client: {string.Join(",
", subNames)}, server: {string.Join(",
", serverSubNames)}).";
1080 errorStr += subErrorStr;
1081 errorStrNoName += subErrorStr;
1088 GameAnalyticsManager.AddErrorEventOnce(
"GameServer.HandleClientError:" + errorStrNoName, GameAnalyticsManager.ErrorSeverity.Error, errorStr);
1092 WriteEventErrorData(c, errorStr);
1096 DebugConsole.ThrowError(
"Failed to write event error data", e);
1111 private void WriteEventErrorData(
Client client,
string errorStr)
1118 string filePath = $
"event_error_log_server_{client.Name}_{DateTime.UtcNow.ToShortTimeString()}.log";
1119 filePath = Path.Combine(
ServerLog.
SavePath, ToolBox.RemoveInvalidFileNameChars(filePath));
1120 if (File.Exists(filePath)) {
return; }
1122 List<string> errorLines =
new List<string>
1127 if (GameMain.GameSession?.GameMode !=
null)
1129 errorLines.Add(
"Game mode: " + GameMain.GameSession.GameMode.Name.Value);
1130 if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
1132 errorLines.Add(
"Campaign ID: " + campaign.CampaignID);
1133 errorLines.Add(
"Campaign save ID: " + campaign.LastSaveID);
1135 foreach (Mission mission
in GameMain.GameSession.Missions)
1137 errorLines.Add(
"Mission: " + mission.Prefab.Identifier);
1140 if (GameMain.GameSession?.Submarine !=
null)
1142 errorLines.Add(
"Submarine: " + GameMain.GameSession.Submarine.Info.Name);
1144 if (GameMain.NetworkMember?.RespawnManager is { } respawnManager)
1146 errorLines.Add(
"Respawn shuttles: " +
string.Join(
", ", respawnManager.RespawnShuttles.Select(s => s.Info.Name)));
1148 if (Level.Loaded !=
null)
1150 errorLines.Add(
"Level: " + Level.Loaded.Seed +
", "
1151 +
string.Join(
"; ", Level.Loaded.EqualityCheckValues.Select(cv
1152 => cv.Key +
"=" + cv.Value.ToString(
"X"))));
1153 errorLines.Add(
"Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate);
1154 errorLines.Add(
"Entities:");
1155 foreach (Entity e
in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex))
1157 errorLines.Add(e.ErrorLine);
1159 errorLines.Add(
"Entity count after generating level: " + Level.Loaded.EntityCountAfterGenerate);
1162 errorLines.Add(
"Entity IDs:");
1163 Entity[] sortedEntities = Entity.GetEntities().OrderBy(e => e.CreationIndex).ToArray();
1164 foreach (Entity e
in sortedEntities)
1166 errorLines.Add(e.ErrorLine);
1170 errorLines.Add(
"EntitySpawner events:");
1171 foreach (var entityEvent
in entityEventManager.UniqueEvents)
1173 if (entityEvent.Entity is EntitySpawner)
1175 var spawnData = entityEvent.Data as EntitySpawner.SpawnOrRemove;
1177 entityEvent.ID +
": " +
1178 (spawnData is EntitySpawner.RemoveEntity ?
"Remove " :
"Create ") +
1179 spawnData.Entity.ToString() +
1180 " (" + spawnData.ID +
", " + spawnData.Entity.ID +
")");
1185 errorLines.Add(
"Last debug messages:");
1186 for (
int i = DebugConsole.Messages.Count - 1; i > 0 && i > DebugConsole.Messages.Count - 15; i--)
1188 errorLines.Add(
" " + DebugConsole.Messages[i].Time +
" - " + DebugConsole.Messages[i].Text);
1191 File.WriteAllLines(filePath, errorLines);
1198 throw new InvalidCastException($
"Entity is not {nameof(IServerSerializable)}");
1200 entityEventManager.CreateEvent(serverSerializable, extraData);
1203 private byte GetNewClientSessionId()
1206 while (connectedClients.Any(c => c.
SessionId == userId))
1233 c.LastRecvServerSettingsUpdate = c.LastSentServerSettingsUpdate;
1238 ReadClientNameChange(c, inc);
1243 byte campaignID = inc.ReadByte();
1244 foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
1246 c.LastRecvCampaignUpdate[netFlag] = inc.ReadUInt16();
1249 if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
1251 if (characterDiscarded) { campaign.DiscardClientCharacterData(c); }
1254 if (campaign.CampaignID != campaignID)
1256 c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1);
1257 foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
1259 c.LastRecvCampaignUpdate[netFlag] =
1260 (UInt16)(campaign.GetLastUpdateIdForFlag(netFlag) - 1);
1277 return connectedClients.Contains(c)
1285 Client c = ConnectedClients.Find(x => x.Connection == inc.
Sender);
1300 if (!midroundSyncingDone) { entityEventManager.InitClientMidRoundSync(c); }
1301 MissionAction.NotifyMissionsUnlockedThisRound(c);
1302 if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign)
1304 mpCampaign.SendCrewState();
1308 AssignClientToPvpTeamMidgame(c);
1322 UInt16 lastRecvEntityEventID = inc.
ReadUInt16();
1323 UInt16 lastRecvClientListUpdate = inc.
ReadUInt16();
1326 UInt16 lastEntityEventID = entityEventManager.Events.Count == 0 ? (UInt16)0 : entityEventManager.Events.Last().ID;
1332 foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
1337 if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
1339 if (characterDiscarded) { campaign.DiscardClientCharacterData(c); }
1342 if (campaign.CampaignID != campaignID)
1345 foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
1348 (UInt16)(campaign.GetLastUpdateIdForFlag(netFlag) - 1);
1360 ushort prevID = lastRecvEntityEventID;
1364 DebugConsole.Log(
"Finished midround syncing " + c.
Name +
" - switching from ID " + prevID +
" to " + c.
LastRecvEntityEventID);
1367 if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
1370 campaign.Bank.ForceUpdate();
1371 campaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.Misc);
1384 else if (lastRecvChatMsgID != c.
LastRecvChatMsgID && GameSettings.CurrentConfig.VerboseLogging)
1386 DebugConsole.ThrowError(
1387 "Invalid lastRecvChatMsgID " + lastRecvChatMsgID +
1397 if (receivedEventCount < 0) { receivedEventCount += ushort.MaxValue; }
1399 DebugConsole.Log(
"Midround sync timeout " + c.
MidRoundSyncTimeOut.ToString(
"0.##") +
"/" + Timing.TotalTime.ToString(
"0.##"));
1404 else if (lastRecvEntityEventID != c.
LastRecvEntityEventID && GameSettings.CurrentConfig.VerboseLogging)
1406 DebugConsole.ThrowError(
1407 "Invalid lastRecvEntityEventID " + lastRecvEntityEventID +
1427 DebugConsole.AddWarning($
"Received character inputs from a client who's not controlling a character ({c.Name}).");
1431 entityEventManager.Read(inc, c);
1434 Voting.ServerRead(inc, c, dosProtection);
1444 return connectedClients.Contains(c)
1452 if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1454 mpCampaign.ServerReadCrew(inc, sender);
1460 if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1462 mpCampaign.ServerReadMoney(inc, sender);
1468 if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1470 mpCampaign.ServerReadRewardDistribution(inc, sender);
1474 private void ResetRewardDistribution(
Client client)
1476 if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1478 mpCampaign.ResetSalaries(client);
1484 if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1486 mpCampaign.MedicalClinic.ServerRead(inc, sender);
1492 var header = INetSerializableStruct.Read<NetCircuitBoxHeader>(inc);
1494 INetSerializableStruct data = header.Opcode
switch
1496 CircuitBoxOpcode.Cursor => INetSerializableStruct.Read<NetCircuitBoxCursorInfo>(inc),
1497 _ =>
throw new ArgumentOutOfRangeException(nameof(header.Opcode), header.Opcode,
"This data cannot be handled using direct network messages.")
1500 if (header.FindTarget().TryUnwrap(out var box))
1502 box.ServerRead(data, sender);
1510 if (!(GameMain.GameSession?.GameMode is CampaignMode))
1519 if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign) {
return; }
1523 DebugConsole.ThrowError($
"Client {sender.Name} has requested to take over a bot in Ironman mode!");
1527 if (campaign.CurrentLocation.GetHireableCharacters().FirstOrDefault(c => c.ID == botId) is CharacterInfo hireableCharacter)
1533 if (campaign.TryHireCharacter(campaign.CurrentLocation, hireableCharacter, takeMoney:
true, sender, buyingNewCharacter:
true))
1535 campaign.CurrentLocation.RemoveHireableCharacter(hireableCharacter);
1536 SpawnAndTakeOverBot(campaign, hireableCharacter, sender);
1537 campaign.SendCrewState(createNotification:
false);
1541 SendConsoleMessage($
"Could not hire the bot {hireableCharacter.Name}.", sender, Color.Red);
1542 DebugConsole.ThrowError($
"Client {sender.Name} failed to hire the bot {hireableCharacter.Name}.");
1547 SendConsoleMessage($
"Could not hire the bot {hireableCharacter.Name}. No permission to manage money or hires.", sender, Color.Red);
1548 DebugConsole.ThrowError($
"Client {sender.Name} failed to hire the bot {hireableCharacter.Name}. No permission to manage money or hires.");
1553 CharacterInfo botInfo = GameMain.GameSession.CrewManager?.GetCharacterInfos()?.FirstOrDefault(i => i.ID == botId);
1555 if (botInfo is { IsNewHire:
true,
Character:
null })
1557 SpawnAndTakeOverBot(campaign, botInfo, sender);
1559 else if (botInfo?.Character ==
null || !botInfo.Character.IsBot)
1561 SendConsoleMessage($
"Could not find a bot with the id {botId}.", sender, Color.Red);
1562 DebugConsole.ThrowError($
"Client {sender.Name} failed to take over a bot (Could not find a bot with the id {botId}).");
1571 SendConsoleMessage($
"Failed to take over a bot (taking control of bots is disallowed).", sender, Color.Red);
1572 DebugConsole.ThrowError($
"Client {sender.Name} failed to take over a bot (taking control of bots is disallowed).");
1577 private static void SpawnAndTakeOverBot(CampaignMode campaign, CharacterInfo botInfo,
Client client)
1579 var mainSubSpawnpoint = WayPoint.SelectCrewSpawnPoints(botInfo.ToEnumerable().ToList(),
Submarine.MainSub).FirstOrDefault();
1580 var spawnWaypoint = campaign.CrewManager.GetOutpostSpawnpoints()?.FirstOrDefault() ?? mainSubSpawnpoint;
1581 if (spawnWaypoint ==
null)
1583 DebugConsole.ThrowError(
"SpawnAndTakeOverBot: Unable to find any spawn waypoints inside the sub");
1586 Entity.Spawner.AddCharacterToSpawnQueue(botInfo.SpeciesName, spawnWaypoint.WorldPosition, botInfo, onSpawn: newCharacter =>
1588 if (newCharacter == null)
1590 DebugConsole.ThrowError(
"SpawnAndTakeOverBot: newCharacter is null somehow");
1594 campaign.CrewManager.RemoveCharacterInfo(botInfo);
1596 campaign.CrewManager.InitializeCharacter(newCharacter, mainSubSpawnpoint, spawnWaypoint);
1603 Client sender = ConnectedClients.Find(x => x.Connection == inc.
Sender);
1621 var mpCampaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign;
1632 Log(
"Client \"" +
GameServer.ClientLogName(sender) +
"\" sent a server command \"" + command +
"\". Permission denied.",
ServerLog.
MessageType.ServerMessage);
1639 string kickedName = inc.
ReadString().ToLowerInvariant();
1641 var kickedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(kickedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection);
1642 if (kickedClient !=
null)
1645 KickClient(kickedClient,
string.IsNullOrEmpty(kickReason) ? $
"ServerMessage.KickedBy~[initiator]={sender.Name}" : kickReason);
1649 SendDirectChatMessage(TextManager.GetServerMessage($
"ServerMessage.PlayerNotFound~[player]={kickedName}").Value, sender,
ChatMessageType.Console);
1653 string bannedName = inc.
ReadString().ToLowerInvariant();
1657 TimeSpan? banDuration =
null;
1658 if (durationSeconds > 0) { banDuration = TimeSpan.FromSeconds(durationSeconds); }
1660 var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(bannedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection);
1661 if (bannedClient !=
null)
1663 Log(
"Client \"" + ClientLogName(sender) +
"\" banned \"" + ClientLogName(bannedClient) +
"\".",
ServerLog.
MessageType.ServerMessage);
1664 BanClient(bannedClient,
string.IsNullOrEmpty(banReason) ? $
"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, banDuration);
1668 var bannedPreviousClient = previousPlayers.Find(p => p.Name.Equals(bannedName, StringComparison.OrdinalIgnoreCase));
1669 if (bannedPreviousClient !=
null)
1671 Log(
"Client \"" + ClientLogName(sender) +
"\" banned \"" + bannedPreviousClient.Name +
"\".",
ServerLog.
MessageType.ServerMessage);
1672 BanPreviousPlayer(bannedPreviousClient,
string.IsNullOrEmpty(banReason) ? $
"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, banDuration);
1676 SendDirectChatMessage(TextManager.GetServerMessage($
"ServerMessage.PlayerNotFound~[player]={bannedName}").Value, sender,
ChatMessageType.Console);
1685 UnbanPlayer(playerName: str);
1689 UnbanPlayer(endpoint);
1696 if (mpCampaign ==
null ||
1703 using (dosProtection.Pause(sender))
1705 Log($
"Client \"{ClientLogName(sender)}\" ended the round.",
ServerLog.
MessageType.ServerMessage);
1706 if (mpCampaign !=
null && Level.IsLoadedFriendlyOutpost && save)
1708 mpCampaign.SavePlayers();
1709 mpCampaign.HandleSaveAndQuit();
1710 GameMain.GameSession.SubmarineInfo =
new SubmarineInfo(GameMain.GameSession.Submarine);
1711 SaveUtil.SaveGame(GameMain.GameSession.DataPath);
1717 EndGame(wasSaved: save);
1720 else if (mpCampaign !=
null)
1722 Log($
"Client \"{ClientLogName(sender)}\" quit the currently active campaign.",
ServerLog.
MessageType.ServerMessage);
1723 GameMain.GameSession =
null;
1724 GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModePreset.Sandbox.Identifier;
1725 GameMain.NetLobbyScreen.LastUpdateID++;
1733 if (mpCampaign !=
null && mpCampaign.GameOver || continueCampaign)
1737 SendDirectChatMessage(
"Cannot continue the campaign from the previous save (round already running).", sender,
ChatMessageType.Error);
1740 else if (CampaignMode.AllowedToManageCampaign(sender,
ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender,
ClientPermissions.ManageMap))
1742 using (dosProtection.Pause(sender))
1744 MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.DataPath, sender);
1748 else if (!GameStarted && !initiatedStartGame)
1750 using (dosProtection.Pause(sender))
1752 Log(
"Client \"" + ClientLogName(sender) +
"\" started the round.",
ServerLog.
MessageType.ServerMessage);
1753 var result = TryStartGame();
1754 if (result != TryStartGameResult.Success)
1756 SendDirectChatMessage(TextManager.Get($
"TryStartGameError.{result}").Value, sender,
ChatMessageType.Error);
1760 else if (mpCampaign !=
null && (CampaignMode.AllowedToManageCampaign(sender,
ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender,
ClientPermissions.ManageMap)))
1762 using (dosProtection.Pause(sender))
1764 var availableTransition = mpCampaign.GetAvailableTransition(out _, out _);
1766 bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation;
1767 switch (availableTransition)
1769 case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation:
1772 mpCampaign.Map.SelectLocation(
1773 mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation));
1775 mpCampaign.LoadNewLevel();
1777 case CampaignMode.TransitionType.ProgressToNextEmptyLocation:
1780 mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation));
1782 mpCampaign.LoadNewLevel();
1784 case CampaignMode.TransitionType.None:
1785 #if DEBUG || UNSTABLE
1786 DebugConsole.ThrowError($
"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available.");
1790 Log(
"Client \"" + ClientLogName(sender) +
"\" ended the round.",
ServerLog.
MessageType.ServerMessage);
1791 mpCampaign.LoadNewLevel();
1801 var subList = GameMain.NetLobbyScreen.GetSubList();
1802 var sub = subList.FirstOrDefault(s => s.MD5Hash.StringRepresentation == subHash);
1805 DebugConsole.NewMessage($
"Client \"{ClientLogName(sender)}\" attempted to select a sub, could not find a sub with the MD5 hash \"{subHash}\".", Color.Red);
1812 GameMain.NetLobbyScreen.SelectedShuttle = sub;
1815 GameMain.NetLobbyScreen.SelectedSub = sub;
1818 GameMain.NetLobbyScreen.SelectedEnemySub = sub;
1825 GameMain.NetLobbyScreen.SelectedModeIndex = modeIndex;
1826 Log(
"Gamemode changed to " + (GameMain.NetLobbyScreen.SelectedMode?.Name.Value ??
"none"),
ServerLog.
MessageType.ServerMessage);
1827 if (GameMain.NetLobbyScreen.GameModes[modeIndex] == GameModePreset.MultiPlayerCampaign)
1829 TrySendCampaignSetupInfo(sender);
1833 mpCampaign?.ServerRead(inc, sender);
1836 DebugConsole.ServerRead(inc, sender);
1839 byte targetClientID = inc.
ReadByte();
1840 Client targetClient = connectedClients.Find(c => c.
SessionId == targetClientID);
1841 if (targetClient ==
null || targetClient == sender || targetClient.
Connection == OwnerConnection) {
return; }
1845 List<string> permissionNames =
new List<string>();
1852 if (targetClient.
Permissions.HasFlag(permission)) { permissionNames.Add(permission.ToString()); }
1856 if (permissionNames.Any())
1858 logMsg =
"Client \"" +
GameServer.ClientLogName(sender) +
"\" set the permissions of the client \"" +
GameServer.ClientLogName(targetClient) +
"\" to "
1859 +
string.Join(
", ", permissionNames);
1863 logMsg =
"Client \"" +
GameServer.ClientLogName(sender) +
"\" removed all permissions from the client \"" +
GameServer.ClientLogName(targetClient) +
".";
1867 UpdateClientPermissions(targetClient);
1875 private void ClientWrite(
Client c)
1877 if (GameStarted && c.
InGame)
1879 ClientWriteIngame(c);
1885 if (GameStarted && c.
Character !=
null && (DateTime.Now - roundStartTime).Seconds > 30.0f)
1890 ClientWriteLobby(c);
1896 while (pendingMessagesToOwner.Any())
1898 SendDirectChatMessage(pendingMessagesToOwner.Dequeue(), c);
1902 if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign &&
1903 GameMain.NetLobbyScreen.SelectedMode == campaign.Preset &&
1907 if (c.LastCampaignSaveSendTime !=
default && campaign.LastSaveID == c.LastCampaignSaveSendTime.saveId)
1911 if (c.LastCampaignSaveSendTime.time > NetTime.Now - 5.0f)
1920 c.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)NetTime.Now);
1930 if (GameSettings.CurrentConfig.VerboseLogging)
1932 DebugConsole.NewMessage($
"Sending initial lobby update to {c.Name}", Color.Gray);
1937 var subList = GameMain.NetLobbyScreen.GetSubList();
1939 for (
int i = 0; i < subList.Count; i++)
1941 var sub = subList[i];
1944 outmsg.
WriteByte((
byte)sub.SubmarineClass);
1946 outmsg.
WriteBoolean(sub.RequiredContentPackagesInstalled);
1957 private void ClientWriteIngame(
Client c)
1964 foreach (Character otherCharacter
in Character.CharacterList)
1966 if (!otherCharacter.Enabled) {
continue; }
1972 float distSqr = GetShortestDistance(clientCharacter.WorldPosition, otherCharacter);
1973 if (clientCharacter.ViewTarget !=
null && clientCharacter.ViewTarget != clientCharacter)
1975 distSqr = Math.Min(distSqr, GetShortestDistance(clientCharacter.ViewTarget.WorldPosition, otherCharacter));
1977 if (distSqr >= MathUtils.Pow2(otherCharacter.Params.DisableDistance)) {
continue; }
1979 else if (otherCharacter != clientCharacter)
1984 if (GetShortestDistance(c.
SpectatePos.Value, otherCharacter) >= MathUtils.Pow2(otherCharacter.Params.DisableDistance)) {
continue; }
1987 static float GetShortestDistance(Vector2 viewPos, Character targetCharacter)
1989 float distSqr = Vector2.DistanceSquared(viewPos, targetCharacter.WorldPosition);
1990 if (targetCharacter.ViewTarget !=
null && targetCharacter.ViewTarget != targetCharacter)
1994 distSqr = Math.Min(distSqr, Vector2.DistanceSquared(viewPos, targetCharacter.ViewTarget.WorldPosition));
1999 float updateInterval = otherCharacter.GetPositionUpdateInterval(c);
2001 if (lastSent > NetTime.Now)
2008 if (lastSent > NetTime.Now - updateInterval) {
continue; }
2013 foreach (Submarine sub
in Submarine.Loaded)
2017 if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) {
continue; }
2018 if (sub.PhysicsBody ==
null || sub.PhysicsBody.BodyType == FarseerPhysics.BodyType.Static) {
continue; }
2022 foreach (
Item item
in Item.ItemList)
2024 if (item.PositionUpdateInterval ==
float.PositiveInfinity) {
continue; }
2025 float updateInterval = item.GetPositionUpdateInterval(c);
2027 if (lastSent > NetTime.Now)
2034 if (lastSent > NetTime.Now - updateInterval) {
continue; }
2051 if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode)
2055 campaign.ServerWrite(outmsg, c);
2064 WriteClientList(segmentTable, c, outmsg);
2065 clientListBytes = outmsg.
LengthBytes - clientListBytes;
2068 WriteChatMessages(segmentTable, outmsg, c);
2069 chatMessageBytes = outmsg.
LengthBytes - chatMessageBytes;
2078 (entity is
Item item &&
float.IsInfinity(item.PositionUpdateInterval)))
2084 var tempBuffer =
new ReadWriteMessage();
2085 var entityPositionHeader = EntityPositionHeader.FromEntity(entity);
2086 tempBuffer.WriteNetSerializableStruct(entityPositionHeader);
2087 entityPositionSync.ServerWritePosition(tempBuffer, c);
2090 if (outmsg.
LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100)
2098 outmsg.
WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes);
2104 positionUpdateBytes = outmsg.
LengthBytes - positionUpdateBytes;
2108 string errorMsg =
"Maximum packet size exceeded (" + outmsg.
LengthBytes +
" > " + MsgConstants.MTU +
")\n";
2110 " Client list size: " + clientListBytes +
" bytes\n" +
2111 " Chat message size: " + chatMessageBytes +
" bytes\n" +
2112 " Position update size: " + positionUpdateBytes +
" bytes\n\n";
2113 DebugConsole.ThrowError(errorMsg);
2114 GameAnalyticsManager.AddErrorEventOnce(
"GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.
LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
2118 serverPeer.Send(outmsg, c.
Connection, DeliveryMethod.Unreliable);
2122 for (
int i = 0; i < NetConfig.MaxEventPacketsPerUpdate; i++)
2124 outmsg =
new WriteOnlyMessage();
2132 entityEventManager.Write(segmentTable, c, outmsg, out List<NetEntityEvent> sentEvents);
2133 eventManagerBytes = outmsg.
LengthBytes - eventManagerBytes;
2135 if (sentEvents.Count == 0)
2142 string errorMsg =
"Maximum packet size exceeded (" + outmsg.
LengthBytes +
" > " +
2143 MsgConstants.MTU +
")\n";
2145 " Event size: " + eventManagerBytes +
" bytes\n";
2147 if (sentEvents !=
null && sentEvents.Count > 0)
2149 errorMsg +=
"Sent events: \n";
2150 foreach (var entityEvent
in sentEvents)
2152 errorMsg +=
" - " + (entityEvent.Entity?.ToString() ??
"null") +
"\n";
2156 DebugConsole.ThrowError(errorMsg);
2157 GameAnalyticsManager.AddErrorEventOnce(
2158 "GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.
LengthBytes,
2159 GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
2163 serverPeer.Send(outmsg, c.
Connection, DeliveryMethod.Unreliable);
2170 if (!hasChanged) {
return; }
2175 GameMain.LuaCs.Hook.Call(
"writeClientList", c, outmsg);
2179 outmsg.
WriteByte((
byte)connectedClients.Count);
2180 foreach (
Client client
in connectedClients)
2195 Muted = client.
Muted,
2198 IsOwner = client.
Connection == OwnerConnection,
2202 var result = GameMain.LuaCs.Hook.Call<
TempClient?>(
"writeClientList.modifyTempClientData", c, client, tempClientData, outmsg);
2206 tempClientData = result.Value;
2209 outmsg.WriteNetSerializableStruct(tempClientData);
2216 bool isInitialUpdate =
false;
2221 bool messageTooLarge;
2227 int initialUpdateBytes = 0;
2242 settingsBuf =
new ReadWriteMessage();
2250 isInitialUpdate =
true;
2252 ClientWriteInitial(c, outmsg);
2253 initialUpdateBytes = outmsg.
LengthBytes - initialUpdateBytes;
2274 outmsg.
WriteString(selectedShuttle.MD5Hash.ToString());
2309 TrySendCampaignSetupInfo(c);
2317 settingsBytes = outmsg.
LengthBytes - settingsBytes;
2320 if (outmsg.
LengthBytes < MsgConstants.MTU - 500 &&
2325 campaign.ServerWrite(outmsg, c);
2332 campaignBytes = outmsg.
LengthBytes - campaignBytes;
2339 WriteClientList(segmentTable, c, outmsg);
2341 clientListBytes = outmsg.
LengthBytes - clientListBytes;
2344 WriteChatMessages(segmentTable, outmsg, c);
2345 chatMessageBytes = outmsg.
LengthBytes - chatMessageBytes;
2347 messageTooLarge = outmsg.
LengthBytes > MsgConstants.MTU;
2348 if (messageTooLarge && !isInitialUpdate)
2350 string warningMsg =
"Maximum packet size exceeded, will send using reliable mode (" + outmsg.
LengthBytes +
" > " + MsgConstants.MTU +
")\n";
2352 " Client list size: " + clientListBytes +
" bytes\n" +
2353 " Chat message size: " + chatMessageBytes +
" bytes\n" +
2354 " Campaign size: " + campaignBytes +
" bytes\n" +
2355 " Settings size: " + settingsBytes +
" bytes\n";
2356 if (initialUpdateBytes > 0)
2359 " Initial update size: " + initialUpdateBytes +
" bytes\n";
2361 if (settingsBuf !=
null)
2364 " Settings buffer size: " + settingsBuf.
LengthBytes +
" bytes\n";
2366 #if DEBUG || UNSTABLE
2367 DebugConsole.ThrowError(warningMsg);
2369 if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); }
2371 GameAnalyticsManager.AddErrorEventOnce(
"GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.
LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
2375 if (isInitialUpdate || messageTooLarge)
2381 serverPeer.Send(outmsg, c.
Connection, DeliveryMethod.Reliable);
2390 serverPeer.Send(outmsg, c.
Connection, DeliveryMethod.Unreliable);
2393 if (isInitialUpdate)
2395 SendVoteStatus(
new List<Client>() { c });
2409 c.
ChatMsgQueue[i].ServerWrite(segmentTable, outmsg, c);
2417 PerksExceedAllowance,
2419 GameModeNotSelected,
2420 CannotStartMultiplayerCampaign,
2425 if (initiatedStartGame || GameStarted) {
return TryStartGameResult.GameAlreadyStarted; }
2429 if (selectedMode ==
null)
2456 Option<SubmarineInfo> selectedEnemySub = Option.None;
2469 if (team1VoteCount > 0)
2472 selectedSub = team1Sub;
2476 selectedSub = team2VoteCount > 0
2482 if (team2VoteCount > 0 && team2Sub !=
null)
2484 selectedEnemySub = Option.Some(team2Sub);
2501 if (enemySub !=
null)
2503 selectedEnemySub = Option.Some(enemySub);
2507 if (selectedSub ==
null || selectedShuttle ==
null)
2512 if (applyPerks && CheckIfAnyPerksAreIncompatible(selectedSub, selectedEnemySub.Fallback(selectedSub), selectedMode, out var incompatiblePerks))
2514 CoroutineManager.StartCoroutine(WarnAndDelayStartGame(incompatiblePerks, selectedSub, selectedEnemySub, selectedShuttle, selectedMode), nameof(WarnAndDelayStartGame));
2518 initiatedStartGame =
true;
2519 startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedEnemySub, selectedShuttle, selectedMode),
"InitiateStartGame");
2526 var incompatibleTeam1Perks = ImmutableArray.CreateBuilder<DisembarkPerkPrefab>();
2527 var incompatibleTeam2Perks = ImmutableArray.CreateBuilder<DisembarkPerkPrefab>();
2528 bool hasIncompatiblePerks =
false;
2533 foreach (DisembarkPerkPrefab perk
in perks.Team1Perks)
2535 if (ignorePerksThatCanNotApplyWithoutSubmarine && perk.PerkBehaviors.Any(
static p => !p.CanApplyWithoutSubmarine())) {
continue; }
2536 bool anyCanNotApply = perk.PerkBehaviors.Any(p => !p.CanApply(team1Sub));
2540 incompatibleTeam1Perks.Add(perk);
2541 hasIncompatiblePerks =
true;
2545 if (preset == GameModePreset.PvP)
2547 foreach (DisembarkPerkPrefab perk
in perks.Team2Perks)
2549 if (ignorePerksThatCanNotApplyWithoutSubmarine && perk.PerkBehaviors.Any(
static p => !p.CanApplyWithoutSubmarine())) {
continue; }
2551 bool anyCanNotApply = perk.PerkBehaviors.Any(p => !p.CanApply(team2Sub));
2555 incompatibleTeam2Perks.Add(perk);
2556 hasIncompatiblePerks =
true;
2561 incompatiblePerks =
new PerkCollection(incompatibleTeam1Perks.ToImmutable(), incompatibleTeam2Perks.ToImmutable());
2562 return hasIncompatiblePerks;
2565 private bool isRoundStartWarningActive;
2567 private void AbortStartGameIfWarningActive()
2569 isRoundStartWarningActive =
false;
2570 CoroutineManager.StopCoroutines(nameof(WarnAndDelayStartGame));
2573 private IEnumerable<CoroutineStatus> WarnAndDelayStartGame(PerkCollection incompatiblePerks, SubmarineInfo selectedSub, Option<SubmarineInfo> selectedEnemySub, SubmarineInfo selectedShuttle, GameModePreset selectedMode)
2575 isRoundStartWarningActive =
true;
2576 const float warningDuration = 15.0f;
2578 SerializableDateTime waitUntilTime = SerializableDateTime.UtcNow + TimeSpan.FromSeconds(warningDuration);
2579 if (connectedClients.Any())
2582 INetSerializableStruct warnData =
new RoundStartWarningData(
2583 RoundStartsAnywaysTimeInSeconds: warningDuration,
2584 Team1Sub: selectedSub.Name,
2585 Team1IncompatiblePerks: ToolBox.PrefabCollectionToUintIdentifierArray(incompatiblePerks.Team1Perks),
2586 Team2Sub: selectedEnemySub.Fallback(selectedSub).Name,
2587 Team2IncompatiblePerks: ToolBox.PrefabCollectionToUintIdentifierArray(incompatiblePerks.Team2Perks));
2588 msg.WriteNetSerializableStruct(warnData);
2590 foreach (
Client c
in connectedClients)
2592 serverPeer.Send(msg, c.
Connection, DeliveryMethod.Reliable);
2596 while (waitUntilTime > SerializableDateTime.UtcNow)
2598 yield
return CoroutineStatus.Running;
2601 CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedEnemySub, selectedShuttle, selectedMode),
"InitiateStartGame");
2602 yield
return CoroutineStatus.Success;
2605 private IEnumerable<CoroutineStatus> InitiateStartGame(SubmarineInfo selectedSub, Option<SubmarineInfo> selectedEnemySub, SubmarineInfo selectedShuttle, GameModePreset selectedMode)
2607 isRoundStartWarningActive =
false;
2608 initiatedStartGame =
true;
2610 if (connectedClients.Any())
2616 msg.
WriteString(selectedSub.MD5Hash.StringRepresentation);
2618 if (selectedEnemySub.TryUnwrap(out var enemySub))
2622 msg.
WriteString(enemySub.MD5Hash.StringRepresentation);
2631 msg.
WriteString(selectedShuttle.MD5Hash.StringRepresentation);
2633 var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign;
2634 msg.
WriteByte(campaign ==
null ? (
byte)0 : campaign.CampaignID);
2635 msg.
WriteUInt16(campaign ==
null ? (UInt16)0 : campaign.LastSaveID);
2636 foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
2638 msg.
WriteUInt16(campaign ==
null ? (UInt16)0 : campaign.GetLastUpdateIdForFlag(flag));
2645 serverPeer.Send(msg, conn, DeliveryMethod.Reliable);
2649 float waitForResponseTimer = 5.0f;
2650 while (connectedClients.Any(c => !c.
ReadyToStart) && waitForResponseTimer > 0.0f)
2652 waitForResponseTimer -= CoroutineManager.DeltaTime;
2653 yield
return CoroutineStatus.Running;
2658 float waitForTransfersTimer = 20.0f;
2661 waitForTransfersTimer -= CoroutineManager.DeltaTime;
2662 yield
return CoroutineStatus.Running;
2667 startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedEnemySub, selectedMode, CampaignSettings.Empty),
false);
2669 yield
return CoroutineStatus.Success;
2672 private IEnumerable<CoroutineStatus> StartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, Option<SubmarineInfo> selectedEnemySub, GameModePreset selectedMode, CampaignSettings settings)
2674 PerkCollection perkCollection = PerkCollection.Empty;
2676 if (GameSession.ShouldApplyDisembarkPoints(selectedMode))
2678 perkCollection = GameSession.GetPerks();
2681 entityEventManager.Clear();
2683 roundStartSeed = DateTime.Now.Millisecond;
2684 Rand.SetSyncedSeed(roundStartSeed);
2687 bool isPvP = selectedMode == GameModePreset.PvP;
2688 MultiPlayerCampaign campaign = selectedMode == GameMain.GameSession?.GameMode.Preset ?
2689 GameMain.GameSession?.GameMode as MultiPlayerCampaign :
null;
2691 if (campaign !=
null && campaign.Map ==
null)
2693 initiatedStartGame =
false;
2694 startGameCoroutine =
null;
2695 string errorMsg =
"Starting the round failed. Campaign was still active, but the map has been disposed. Try selecting another game mode.";
2696 DebugConsole.ThrowError(errorMsg);
2697 GameAnalyticsManager.AddErrorEventOnce(
"GameServer.StartGame:InvalidCampaignState", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
2698 if (OwnerConnection !=
null)
2702 yield
return CoroutineStatus.Failure;
2705 bool initialSuppliesSpawned =
false;
2707 if (campaign ==
null || GameMain.GameSession ==
null)
2709 traitorManager =
new TraitorManager(
this);
2710 GameMain.GameSession =
new GameSession(selectedSub, selectedEnemySub, CampaignDataPath.Empty, selectedMode, settings, GameMain.NetLobbyScreen.LevelSeed, missionTypes: GameMain.NetLobbyScreen.MissionTypes);
2714 initialSuppliesSpawned = GameMain.GameSession.SubmarineInfo is { InitialSuppliesSpawned:
true };
2717 if (GameMain.GameSession.GameMode is PvPMode pvpMode)
2724 RefreshPvpTeamAssignments(assignUnassignedNow:
true, autoBalanceNow:
true);
2731 bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || missionMode.Missions.All(m => m.AllowRespawning);
2732 foreach (var mission
in GameMain.GameSession.GameMode.Missions)
2734 if (mission.Prefab.ForceRespawnMode.HasValue)
2741 List<Client> playingClients = GetPlayingClients();
2742 if (campaign !=
null)
2744 if (campaign.Map ==
null)
2746 throw new Exception(
"Campaign map was null.");
2748 if (campaign.NextLevel ==
null)
2750 string errorMsg =
"Failed to start a campaign round (next level not set).";
2751 DebugConsole.ThrowError(errorMsg);
2752 GameAnalyticsManager.AddErrorEventOnce(
"GameServer.StartGame:InvalidCampaignState", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
2753 if (OwnerConnection !=
null)
2757 yield
return CoroutineStatus.Failure;
2760 SendStartMessage(roundStartSeed, campaign.NextLevel.Seed, GameMain.GameSession, connectedClients, includesFinalize:
false);
2761 GameMain.GameSession.StartRound(campaign.NextLevel, startOutpost: campaign.GetPredefinedStartOutpost(), mirrorLevel: campaign.MirrorLevel);
2762 SubmarineSwitchLoad =
false;
2763 campaign.AssignClientCharacterInfos(connectedClients);
2765 Log(
"Submarine: " + GameMain.GameSession.SubmarineInfo.Name,
ServerLog.
MessageType.ServerMessage);
2770 SendStartMessage(roundStartSeed, GameMain.NetLobbyScreen.LevelSeed, GameMain.GameSession, connectedClients,
false);
2777 foreach (Mission mission
in GameMain.GameSession.Missions)
2782 if (GameMain.GameSession.SubmarineInfo.IsFileCorrupted)
2784 CoroutineManager.StopCoroutines(startGameCoroutine);
2785 initiatedStartGame =
false;
2786 SendChatMessage(TextManager.FormatServerMessage($
"SubLoadError~[subname]={GameMain.GameSession.SubmarineInfo.Name}"),
ChatMessageType.Error);
2787 yield
return CoroutineStatus.Failure;
2790 bool isOutpost = campaign !=
null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost;
2795 if (campaign !=
null)
2797 campaign.CargoManager.CreatePurchasedItems();
2799 if (isOutpost) { campaign.SendCrewState(); }
2802 if (GameMain.GameSession.Missions.None(m => !m.Prefab.AllowOutpostNPCs))
2804 Level.Loaded?.SpawnNPCs();
2806 Level.Loaded?.SpawnCorpses();
2807 Level.Loaded?.PrepareBeaconStation();
2808 AutoItemPlacer.SpawnItems(campaign?.Settings.StartItemSet);
2810 CrewManager crewManager = GameMain.GameSession.CrewManager;
2812 bool hadBots =
true;
2814 List<Character> team1Characters =
new(),
2815 team2Characters =
new();
2818 for (
int n = 0; n < teamCount; n++)
2823 if (teamSub !=
null)
2825 teamSub.TeamID = teamID;
2826 foreach (
Item item
in Item.ItemList)
2828 if (item.Submarine ==
null) {
continue; }
2829 if (item.Submarine != teamSub && !teamSub.DockedTo.Contains(item.Submarine)) {
continue; }
2832 wifiComponent.
TeamID = teamSub.TeamID;
2835 foreach (Submarine sub
in teamSub.DockedTo)
2838 sub.TeamID = teamID;
2843 List<Client> teamClients = teamCount == 1 ?
new List<Client>(playingClients) : playingClients.FindAll(c => c.TeamID == teamID);
2855 AssignJobs(teamClients);
2857 List<CharacterInfo> characterInfos =
new List<CharacterInfo>();
2858 foreach (
Client client
in teamClients)
2870 client.
CharacterInfo =
new CharacterInfo(CharacterPrefab.HumanSpeciesName, client.
Name);
2882 List<CharacterInfo> bots =
new List<CharacterInfo>();
2884 if (!crewManager.HasBots || campaign ==
null)
2887 for (
int i = 0; i < botsToSpawn; i++)
2889 var botInfo =
new CharacterInfo(CharacterPrefab.HumanSpeciesName)
2893 characterInfos.Add(botInfo);
2897 AssignBotJobs(bots, teamID, isPvP);
2898 foreach (CharacterInfo bot
in bots)
2900 crewManager.AddCharacterInfo(bot);
2903 crewManager.HasBots =
true;
2907 List<WayPoint> spawnWaypoints =
null;
2908 List<WayPoint> mainSubWaypoints = teamSub !=
null ? WayPoint.SelectCrewSpawnPoints(characterInfos,
Submarine.MainSubs[n]).ToList() :
null;
2909 if (Level.Loaded !=
null && Level.Loaded.ShouldSpawnCrewInsideOutpost())
2911 spawnWaypoints = WayPoint.GetOutpostSpawnPoints(teamID);
2912 while (spawnWaypoints.Count > characterInfos.Count)
2914 spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count));
2916 while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count)
2918 spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]);
2921 if (teamSub !=
null)
2923 if (spawnWaypoints ==
null || !spawnWaypoints.Any())
2925 spawnWaypoints = mainSubWaypoints;
2927 Debug.Assert(spawnWaypoints.Count == mainSubWaypoints.Count);
2930 for (
int i = 0; i < teamClients.Count; i++)
2934 WayPoint jobItemSpawnPoint = mainSubWaypoints !=
null ? mainSubWaypoints[i] : spawnWaypoints[i];
2936 Character spawnedCharacter =
Character.Create(teamClients[i].CharacterInfo, spawnWaypoints[i].WorldPosition, teamClients[i].CharacterInfo.Name, isRemotePlayer:
true, hasAi:
false);
2937 spawnedCharacter.AnimController.Frozen =
true;
2938 spawnedCharacter.TeamID = teamID;
2939 teamClients[i].Character = spawnedCharacter;
2940 var characterData = campaign?.GetClientCharacterData(teamClients[i]);
2941 if (characterData ==
null)
2943 spawnedCharacter.GiveJobItems(GameMain.GameSession.GameMode is PvPMode, jobItemSpawnPoint);
2944 if (campaign !=
null)
2946 characterData = campaign.SetClientCharacterData(teamClients[i]);
2947 characterData.HasSpawned =
true;
2952 if (!characterData.HasItemData && !characterData.CharacterInfo.StartItemsGiven)
2955 spawnedCharacter.GiveJobItems(GameMain.GameSession.GameMode is PvPMode, jobItemSpawnPoint);
2959 characterData.SpawnInventoryItems(spawnedCharacter, spawnedCharacter.Inventory);
2961 characterData.ApplyHealthData(spawnedCharacter);
2962 characterData.ApplyOrderData(spawnedCharacter);
2963 characterData.ApplyWalletData(spawnedCharacter);
2964 spawnedCharacter.GiveIdCardTags(jobItemSpawnPoint);
2965 spawnedCharacter.LoadTalents();
2966 characterData.HasSpawned =
true;
2968 if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && spawnedCharacter.Info !=
null)
2970 spawnedCharacter.Info.SetExperience(Math.Max(spawnedCharacter.Info.ExperiencePoints, mpCampaign.GetSavedExperiencePoints(teamClients[i])));
2971 mpCampaign.ClearSavedExperiencePoints(teamClients[i]);
2973 if (spawnedCharacter.Info.LastRewardDistribution.TryUnwrap(out
int salary))
2975 spawnedCharacter.Wallet.SetRewardDistribution(salary);
2979 spawnedCharacter.SetOwnerClient(teamClients[i]);
2980 AddCharacterToList(teamID, spawnedCharacter);
2983 for (
int i = teamClients.Count; i < teamClients.Count + bots.Count; i++)
2985 WayPoint jobItemSpawnPoint = mainSubWaypoints !=
null ? mainSubWaypoints[i] : spawnWaypoints[i];
2986 Character spawnedCharacter =
Character.Create(characterInfos[i], spawnWaypoints[i].WorldPosition, characterInfos[i].Name, isRemotePlayer:
false, hasAi:
true);
2987 spawnedCharacter.TeamID = teamID;
2988 spawnedCharacter.GiveJobItems(GameMain.GameSession.GameMode is PvPMode, jobItemSpawnPoint);
2989 spawnedCharacter.GiveIdCardTags(jobItemSpawnPoint);
2990 spawnedCharacter.Info.InventoryData =
new XElement(
"inventory");
2991 spawnedCharacter.Info.StartItemsGiven =
true;
2992 spawnedCharacter.SaveInventory();
2993 spawnedCharacter.LoadTalents();
2994 AddCharacterToList(teamID, spawnedCharacter);
2997 void AddCharacterToList(CharacterTeamType team, Character character)
3002 team1Characters.Add(character);
3005 team2Characters.Add(character);
3011 if (campaign !=
null && crewManager.HasBots)
3016 crewManager.InitRound();
3021 SaveUtil.SaveGame(GameMain.GameSession.DataPath);
3025 campaign?.LoadPets();
3026 campaign?.LoadActiveOrders();
3028 campaign?.CargoManager.InitPurchasedIDCards();
3030 if (campaign ==
null || !initialSuppliesSpawned)
3032 foreach (Submarine sub
in Submarine.MainSubs)
3034 if (sub ==
null) {
continue; }
3035 List<PurchasedItem> spawnList =
new List<PurchasedItem>();
3038 spawnList.Add(
new PurchasedItem(kvp.Key, kvp.Value, buyer:
null));
3040 CargoManager.DeliverItemsToSub(spawnList, sub, cargoManager:
null);
3044 TraitorManager.Initialize(GameMain.GameSession.EventManager, Level.Loaded);
3045 if (GameMain.LuaCs.Game.overrideTraitors)
3047 TraitorManager.Enabled =
false;
3054 GameAnalyticsManager.AddDesignEvent(
"Traitors:" + (TraitorManager ==
null ?
"Disabled" :
"Enabled"));
3056 perkCollection.ApplyAll(team1Characters, team2Characters);
3058 yield
return CoroutineStatus.Running;
3060 Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes:
false);
3062 GameMain.GameScreen.Select();
3067 initiatedStartGame =
false;
3068 GameMain.ResetFrameTime();
3070 LastClientListUpdateID++;
3072 roundStartTime = DateTime.Now;
3074 GameMain.LuaCs.Hook.Call(
"roundStart");
3076 startGameCoroutine =
null;
3077 yield
return CoroutineStatus.Success;
3080 private void SendStartMessage(
int seed,
string levelSeed, GameSession gameSession, List<Client> clients,
bool includesFinalize)
3082 foreach (
Client client
in clients)
3084 SendStartMessage(seed, levelSeed, gameSession, client, includesFinalize);
3088 private void SendStartMessage(
int seed,
string levelSeed, GameSession gameSession,
Client client,
bool includesFinalize)
3094 bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawning);
3112 if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign)
3117 msg.
WriteString(gameSession.SubmarineInfo.MD5Hash.StringRepresentation);
3121 msg.
WriteString(selectedShuttle.MD5Hash.StringRepresentation);
3123 if (gameSession.EnemySubmarineInfo is { } enemySub)
3127 msg.
WriteString(enemySub.MD5Hash.StringRepresentation);
3134 msg.
WriteByte((
byte)GameMain.GameSession.GameMode.Missions.Count());
3135 foreach (Mission mission
in GameMain.GameSession.GameMode.Missions)
3142 int nextLocationIndex = campaign.Map.Locations.FindIndex(l => l.LevelData == campaign.NextLevel);
3143 int nextConnectionIndex = campaign.Map.Connections.FindIndex(c => c.LevelData == campaign.NextLevel);
3148 msg.
WriteInt32(campaign.Map.SelectedLocationIndex);
3152 if (includesFinalize)
3154 WriteRoundStartFinalize(msg, client);
3157 serverPeer.Send(msg, client.
Connection, DeliveryMethod.Reliable);
3160 private bool TrySendCampaignSetupInfo(
Client client)
3162 if (!CampaignMode.AllowedToManageCampaign(client,
ClientPermissions.ManageRound)) {
return false; }
3164 using (dosProtection.Pause(client))
3166 const int MaxSaves = 255;
3167 var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible:
false);
3170 msg.
WriteByte((
byte)Math.Min(saveInfos.Count, MaxSaves));
3171 for (
int i = 0; i < saveInfos.Count && i < MaxSaves; i++)
3173 msg.WriteNetSerializableStruct(saveInfos[i]);
3175 serverPeer.Send(msg, client.
Connection, DeliveryMethod.Reliable);
3181 private bool IsUsingRespawnShuttle()
3186 private void SendRoundStartFinalize(
Client client)
3190 WriteRoundStartFinalize(msg, client);
3191 serverPeer.Send(msg, client.
Connection, DeliveryMethod.Reliable);
3197 var contentToPreload = GameMain.GameSession.EventManager.GetFilesToPreload();
3198 msg.
WriteUInt16((ushort)contentToPreload.Count());
3199 foreach (ContentFile contentFile
in contentToPreload)
3204 msg.
WriteByte((
byte)GameMain.GameSession.Missions.Count());
3205 foreach (Mission mission
in GameMain.GameSession.Missions)
3209 foreach (Level.LevelGenStage stage in Enum.GetValues(typeof(Level.LevelGenStage)).OfType<Level.LevelGenStage>().OrderBy(s => s))
3211 msg.
WriteInt32(GameMain.GameSession.Level.EqualityCheckValues[stage]);
3213 foreach (Mission mission
in GameMain.GameSession.Missions)
3215 mission.ServerWriteInitial(msg, client);
3217 msg.
WriteBoolean(GameMain.GameSession.CrewManager !=
null);
3218 GameMain.GameSession.CrewManager?.ServerWriteActiveOrders(msg);
3220 msg.
WriteBoolean(GameSession.ShouldApplyDisembarkPoints(GameMain.GameSession.GameMode?.Preset));
3227 if (GameSettings.CurrentConfig.VerboseLogging)
3229 Log(
"Ending the round...\n" + Environment.StackTrace.CleanupStackTrace(),
ServerLog.
MessageType.ServerMessage);
3238 string endMessage = TextManager.FormatServerMessage(
"RoundSummaryRoundHasEnded");
3248 foreach (var data
in result)
3251 if (data is
string endMessageData) { endMessage = endMessageData; }
3255 EndRoundTimer = 0.0f;
3268 entityEventManager.Clear();
3269 foreach (
Client c
in connectedClients)
3282 GameStarted =
false;
3284 if (connectedClients.Count > 0)
3292 foreach (
Mission mission
in missions)
3299 if (traitorResults.HasValue)
3301 msg.WriteNetSerializableStruct(traitorResults.Value);
3304 foreach (
Client client
in connectedClients)
3306 serverPeer.Send(msg, client.
Connection, DeliveryMethod.Reliable);
3315 entityEventManager.Clear();
3325 if (
string.IsNullOrEmpty(message.
Text)) {
return; }
3338 sender.TextChatVolume = 1f;
3351 if (c ==
null ||
string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.
NameId)) {
return false; }
3357 if (timeSinceNameChange.TotalSeconds > 1)
3360 SendDirectChatMessage($
"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c);
3361 LastClientListUpdateID++;
3368 if (!newJob.IsEmpty)
3370 if (!JobPrefab.Prefabs.TryGet(newJob, out JobPrefab newJobPrefab) || newJobPrefab.HiddenJob)
3372 newJob = Identifier.Empty;
3378 var result = GameMain.LuaCs.Hook.Call<
bool?>(
"tryChangeClientName", c, newName, newJob, newTeam);
3382 LastClientListUpdateID++;
3383 return result.Value;
3386 c.PreferredJob = newJob;
3388 if (newTeam != c.PreferredTeam)
3390 c.PreferredTeam = newTeam;
3391 RefreshPvpTeamAssignments();
3394 return TryChangeClientName(c, newName);
3400 if (newName != c.
Name && !
string.IsNullOrEmpty(newName) && IsNameValid(c, newName))
3403 string oldName = c.
Name;
3406 SendChatMessage($
"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}",
ChatMessageType.Server);
3407 LastClientListUpdateID++;
3414 LastClientListUpdateID++;
3425 SendDirectChatMessage($
"ServerMessage.NameChangeFailedSymbols~[newname]={newName}", c,
ChatMessageType.ServerMessageBox);
3430 SendDirectChatMessage($
"ServerMessage.NameChangeFailedServerTooSimilar~[newname]={newName}", c,
ChatMessageType.ServerMessageBox);
3436 SendDirectChatMessage($
"ServerMessage.NameChangeFailedVoteKick~[newname]={newName}", c,
ChatMessageType.ServerMessageBox);
3441 Client nameTakenByClient = ConnectedClients.Find(c2 => c != c2 &&
Homoglyphs.
Compare(c2.Name.ToLower(), newName.ToLower()));
3442 if (nameTakenByClient !=
null)
3444 SendDirectChatMessage($
"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTakenByClient.Name}", c,
ChatMessageType.ServerMessageBox);
3450 if (nameTakenByCharacter !=
null)
3452 SendDirectChatMessage($
"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTakenByCharacter.Name}", c,
ChatMessageType.ServerMessageBox);
3458 public override void KickPlayer(
string playerName,
string reason)
3460 Client client = connectedClients.Find(c =>
3461 c.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase) ||
3462 (c.Character !=
null && c.Character.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)));
3464 KickClient(client, reason);
3469 if (conn == OwnerConnection)
return;
3471 Client client = connectedClients.Find(c => c.Connection == conn);
3472 KickClient(client, reason);
3477 if (client ==
null || client.
Connection == OwnerConnection) {
return; }
3481 var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client));
3482 if (previousPlayer !=
null)
3484 previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f);
3486 client.
Karma = Math.Max(client.
Karma, 50.0f);
3489 DisconnectClient(client, PeerDisconnectPacket.Kicked(reason));
3492 public override void BanPlayer(
string playerName,
string reason, TimeSpan? duration =
null)
3494 Client client = connectedClients.Find(c =>
3495 c.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase) ||
3496 (c.Character !=
null && c.Character.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)));
3500 DebugConsole.ThrowError(
"Client \"" + playerName +
"\" not found.");
3504 BanClient(client, reason, duration);
3509 if (client ==
null || client.
Connection == OwnerConnection) {
return; }
3511 var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client));
3512 if (previousPlayer !=
null)
3515 previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f);
3517 client.
Karma = Math.Max(client.
Karma, 50.0f);
3519 DisconnectClient(client, PeerDisconnectPacket.Banned(reason));
3537 if (previousPlayer ==
null) {
return; }
3540 previousPlayer.
Karma = Math.Max(previousPlayer.
Karma, 50.0f);
3547 foreach (var relatedId
in previousPlayer.AccountInfo.OtherMatchingIds)
3552 string msg = $
"ServerMessage.BannedFromServer~[client]={previousPlayer.Name}";
3553 if (!
string.IsNullOrWhiteSpace(reason))
3555 msg += $
"/ /ServerMessage.Reason/: /{reason}";
3564 if (bannedPlayer is
null) {
return; }
3575 Client client = connectedClients.Find(x => x.Connection == senderConnection);
3576 if (client ==
null) {
return; }
3578 DisconnectClient(client, peerDisconnectPacket);
3583 if (client ==
null)
return;
3598 var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client));
3599 if (previousPlayer ==
null)
3602 previousPlayers.Add(previousPlayer);
3605 if (peerDisconnectPacket.ShouldAttemptReconnect)
3607 lock (clientsAttemptingToReconnectSoon)
3610 clientsAttemptingToReconnectSoon.Add(client);
3614 previousPlayer.Name = client.
Name;
3615 previousPlayer.Karma = client.
Karma;
3617 previousPlayer.KickVoters.Clear();
3618 foreach (
Client c
in connectedClients)
3624 connectedClients.Remove(client);
3625 serverPeer.Disconnect(client.
Connection, peerDisconnectPacket);
3632 RefreshPvpTeamAssignments();
3637 SendChatMessage(peerDisconnectPacket.ChatMessage(client).Value,
ChatMessageType.Server, changeType: peerDisconnectPacket.ConnectionChangeType);
3642 refreshMasterTimer = DateTime.Now;
3645 private void UpdateCrewFrame()
3647 foreach (
Client c
in connectedClients)
3656 SendDirectChatMessage(msg, recipient);
3662 SendDirectChatMessage(msg, recipient);
3667 if (recipient ==
null)
3669 string errorMsg =
"Attempted to send a chat message to a null client.\n" + Environment.StackTrace.CleanupStackTrace();
3670 DebugConsole.ThrowError(errorMsg);
3671 GameAnalyticsManager.AddErrorEventOnce(
"GameServer.SendDirectChatMessage:ClientNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
3676 (ushort)(recipient.
ChatMsgQueue.Last().NetStateID + 1) :
3688 string senderName =
"";
3690 Client targetClient =
null;
3695 switch (command.ToLowerInvariant())
3708 if (command.ToLower() == ServerName.ToLower())
3711 if (OwnerConnection !=
null)
3713 targetClient = connectedClients.Find(c => c.
Connection == OwnerConnection);
3718 targetClient = connectedClients.Find(c =>
3719 command.ToLower() == c.
Name.ToLower() ||
3722 if (targetClient ==
null)
3724 if (senderClient !=
null)
3727 "", $
"ServerMessage.PlayerNotFound~[player]={command}",
3729 SendDirectChatMessage(chatMsg, senderClient);
3733 AddChatMessage($
"ServerMessage.PlayerNotFound~[player]={command}",
ChatMessageType.Error);
3742 else if (chatMode ==
ChatMode.Radio)
3758 if (senderClient ==
null)
3761 if (senderCharacter ==
null)
3763 senderName = ServerName;
3767 senderName = senderCharacter.Name;
3772 senderCharacter = senderClient.Character;
3773 senderName = senderCharacter ==
null ? senderClient.Name : senderCharacter.Name;
3776 if (senderCharacter !=
null && !senderCharacter.IsDead || targetClient.
Character !=
null && !targetClient.
Character.
IsDead)
3784 else if (senderCharacter ==
null || senderCharacter.IsDead || senderCharacter.SpeechImpediment >= 100.0f)
3792 if (senderClient ==
null)
3795 if (senderCharacter ==
null)
3797 senderName = ServerName;
3808 senderName = senderClient.Name;
3818 if (senderCharacter ==
null) {
return; }
3823 if (senderClient !=
null && senderCharacter !=
null && !senderCharacter.IsDead && senderCharacter.SpeechImpediment < 100.0f)
3833 senderCharacter =
null;
3838 Signal s =
new Signal(message, sender: senderCharacter, source: senderRadio.
Item);
3846 if (should !=
null && should.Value)
3851 foreach (
Client client
in ConnectedClients)
3853 string modifiedMessage = message;
3860 if (senderCharacter !=
null &&
3863 if (senderCharacter != client.
Character)
3869 if (
string.IsNullOrWhiteSpace(modifiedMessage)) {
continue; }
3878 if (client != targetClient && client != senderClient) {
continue; }
3895 SendDirectChatMessage(chatMsg, client);
3900 string myReceivedMessage = type ==
ChatMessageType.Server || type ==
ChatMessageType.Error ? TextManager.GetServerMessage(message).Value : message;
3901 if (!
string.IsNullOrWhiteSpace(myReceivedMessage))
3903 AddChatMessage(myReceivedMessage, (
ChatMessageType)type, senderName, senderClient, senderCharacter);
3912 foreach (
Client client
in ConnectedClients)
3919 SendDirectChatMessage(
new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client);
3921 if (!
string.IsNullOrWhiteSpace(message.Text))
3923 AddChatMessage(
new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder));
3927 Signal s =
new Signal(message.Text, sender: message.SenderCharacter, source: senderRadio.Item);
3928 senderRadio.TransmitSignal(s, sentFromChat:
true);
3935 Client recipient = connectedClients.Find(c => c.
Connection == transfer.Connection);
3938 recipient.LastCampaignSaveSendTime !=
default)
3940 recipient.LastCampaignSaveSendTime.time = (float)NetTime.Now;
3950 serverPeer.Send(msg, transfer.Connection, DeliveryMethod.Reliable);
3955 if (connectedClients.Count == 0) {
return; }
3959 #warning TODO: this is mostly the same as Voting.Update, deduplicate (if/when refactoring the Voting class?)
3965 else if (inGameClients.Any())
3970 int max = eligibleClients.Count();
3987 var clientsToKick = connectedClients.FindAll(c =>
3993 foreach (
Client c
in clientsToKick)
3997 previousPlayers.Where(p => p.MatchesClient(c)).ForEach(p => p.KickVoters.Clear());
4003 SendVoteStatus(connectedClients);
4005 int endVoteCount = ConnectedClients.Count(c => c.
HasSpawned && c.GetVote<
bool>(VoteType.EndRound));
4010 Log(
"Ending round by votes (" + endVoteCount +
"/" + (endVoteMax - endVoteCount) +
")",
ServerLog.
MessageType.ServerMessage);
4011 EndGame(wasSaved:
false);
4017 if (!recipients.Any()) {
return; }
4027 foreach (var c
in recipients)
4029 serverPeer.Send(msg, c.
Connection, DeliveryMethod.Reliable);
4041 bool purchaseFailed =
false;
4044 case VoteType.PurchaseAndSwitchSub:
4045 case VoteType.PurchaseSub:
4049 case VoteType.SwitchSub:
4055 if (voteType != VoteType.PurchaseSub && !purchaseFailed)
4061 return !purchaseFailed;
4066 if (client.
AccountId.TryUnwrap(out var accountId))
4091 foreach (
Client recipient
in connectedClients)
4093 CoroutineManager.StartCoroutine(SendClientPermissionsAfterClientListSynced(recipient, client));
4098 private IEnumerable<CoroutineStatus> SendClientPermissionsAfterClientListSynced(
Client recipient,
Client client)
4100 DateTime timeOut = DateTime.Now +
new TimeSpan(0, 0, 10);
4103 if (DateTime.Now > timeOut ||
GameMain.
Server ==
null || !connectedClients.Contains(recipient))
4110 SendClientPermissions(recipient, client);
4111 yield
return CoroutineStatus.Success;
4114 private void SendClientPermissions(
Client recipient,
Client client)
4116 if (recipient?.
Connection ==
null) {
return; }
4121 serverPeer.Send(msg, recipient.
Connection, DeliveryMethod.Reliable);
4126 foreach (
Client client
in connectedClients)
4130 GiveAchievement(client, achievementIdentifier);
4138 foreach (
Client client
in connectedClients)
4142 IncrementStat(client, stat, amount);
4157 serverPeer.Send(msg, client.
Connection, DeliveryMethod.Reliable);
4165 INetSerializableStruct incrementedStat =
new NetIncrementedStat(stat, amount);
4166 incrementedStat.Write(msg);
4168 serverPeer.Send(msg, client.
Connection, DeliveryMethod.Reliable);
4173 if (client ==
null) {
return; };
4174 serverPeer.Send(msg, client.
Connection, DeliveryMethod.Reliable);
4179 if (!connectedClients.Any()) {
return; }
4186 foreach (
Client c
in connectedClients)
4188 serverPeer.Send(msg, c.
Connection, DeliveryMethod.Reliable);
4194 if (client ==
null)
return;
4202 if (newCharacter ==
null)
4233 private readonly RateLimiter charInfoRateLimiter =
new(
4235 expiryInSeconds: 10,
4236 punishmentRules:
new[]
4250 var netInfo = INetSerializableStruct.Read<NetCharacterInfo>(message);
4252 if (charInfoRateLimiter.IsLimitReached(sender)) {
return; }
4254 string newName = netInfo.NewName;
4255 if (
string.IsNullOrEmpty(newName))
4257 newName = sender.
Name;
4262 if (!IsNameValid(sender, newName))
4264 newName = sender.
Name;
4273 var existingCampaignData = (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.GetClientCharacterData(sender);
4274 if (existingCampaignData !=
null)
4280 sender.
CharacterInfo =
new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName);
4283 tags: netInfo.Tags.ToImmutableHashSet(),
4284 hairIndex: netInfo.HairIndex,
4285 beardIndex: netInfo.BeardIndex,
4286 moustacheIndex: netInfo.MoustacheIndex,
4287 faceAttachmentIndex: netInfo.FaceAttachmentIndex);
4293 if (netInfo.JobVariants.Length > 0)
4295 List<JobVariant> variants =
new List<JobVariant>();
4296 foreach (NetJobVariant jv
in netInfo.JobVariants)
4298 if (jv.ToJobVariant() is { } variant)
4300 variants.Add(variant);
4308 public readonly List<string> JobAssignmentDebugLog =
new List<string>();
4312 JobAssignmentDebugLog.Clear();
4315 unassigned =
new List<Client>(unassigned);
4316 unassigned = unassigned.OrderBy(sp => Rand.Int(
int.MaxValue)).ToList();
4318 Dictionary<JobPrefab, int> assignedClientCount =
new Dictionary<JobPrefab, int>();
4321 assignedClientCount.Add(jp, 0);
4325 if (unassigned.Count > 0) { teamID = unassigned[0].TeamID; }
4331 var campaignAssigned = multiplayerCampaign.GetAssignedJobs(connectedClients);
4333 unassigned.RemoveAll(u => campaignAssigned.ContainsKey(u));
4335 foreach ((
Client client,
Job job) in campaignAssigned)
4337 assignedClientCount[job.
Prefab]++;
4339 JobAssignmentDebugLog.Add($
"Client {client.Name} has an existing campaign character, keeping the job {job.Name}.");
4344 foreach (
Client c
in connectedClients)
4346 if (c.
TeamID != teamID || unassigned.Contains(c)) {
continue; }
4354 for (
int i = unassigned.Count - 1; i >= 0; i--)
4356 if (unassigned[i].JobPreferences.Count == 0) {
continue; }
4357 if (!unassigned[i].JobPreferences.Any() || !unassigned[i].JobPreferences[0].Prefab.AllowAlways) {
continue; }
4358 JobAssignmentDebugLog.Add($
"Client {unassigned[i].Name} has {unassigned[i].JobPreferences[0].Prefab.Name} as their first preference, assigning it because the job is always allowed.");
4359 unassigned[i].AssignedJob = unassigned[i].JobPreferences[0];
4360 unassigned.RemoveAt(i);
4364 bool unassignedJobsFound =
true;
4365 while (unassignedJobsFound && unassigned.Any())
4367 unassignedJobsFound =
false;
4369 foreach (
JobPrefab jobPrefab
in jobList)
4371 if (unassigned.Count == 0) {
break; }
4372 if (jobPrefab.
MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.
MinNumber) {
continue; }
4374 Client client = FindClientWithJobPreference(unassigned, jobPrefab, forceAssign:
false);
4377 JobAssignmentDebugLog.Add($
"At least {jobPrefab.MinNumber} {jobPrefab.Name} required. Assigning {client.Name} as a {jobPrefab.Name} (has the job in their preferences).");
4378 AssignJob(client, jobPrefab);
4382 if (unassigned.Any())
4385 foreach (
JobPrefab jobPrefab
in jobList)
4387 if (unassigned.Count == 0) {
break; }
4388 if (jobPrefab.
MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.
MinNumber) {
continue; }
4389 var client = FindClientWithJobPreference(unassigned, jobPrefab, forceAssign:
true);
4390 JobAssignmentDebugLog.Add(
4391 $
"At least {jobPrefab.MinNumber} {jobPrefab.Name} required. "+
4392 $
"A random client needs to be assigned because no one has the job in their preferences. Assigning {client.Name} as a {jobPrefab.Name}.");
4393 AssignJob(client, jobPrefab);
4401 new JobVariant(jobPrefab, Rand.Int(jobPrefab.
Variants));
4403 assignedClientCount[jobPrefab]++;
4404 unassigned.Remove(client);
4407 if (assignedClientCount[jobPrefab] < jobPrefab.
MinNumber) { unassignedJobsFound =
true; }
4413 for (
int preferenceIndex = 0; preferenceIndex < 3; preferenceIndex++)
4415 for (
int i = unassigned.Count - 1; i >= 0; i--)
4417 Client client = unassigned[i];
4418 if (preferenceIndex >= client.
JobPreferences.Count) {
continue; }
4421 if (assignedClientCount[jobPrefab] >= jobPrefab.
MaxNumber)
4423 JobAssignmentDebugLog.Add($
"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Cannot assign, maximum number of the job has been reached.");
4428 JobAssignmentDebugLog.Add($
"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Cannot assign, karma too low ({client.Karma} < {jobPrefab.MinKarma}).");
4431 JobAssignmentDebugLog.Add($
"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Assigning {client.Name} as a {jobPrefab.Name}.");
4433 assignedClientCount[jobPrefab]++;
4434 unassigned.RemoveAt(i);
4439 foreach (
Client c
in unassigned)
4445 if (remainingJobs.Count == 0)
4447 string errorMsg = $
"Failed to assign a suitable job for \"{c.Name}\" (all jobs already have the maximum numbers of players). Assigning a random job...";
4448 DebugConsole.ThrowError(errorMsg);
4449 JobAssignmentDebugLog.Add(errorMsg);
4450 int jobIndex = Rand.Range(0, jobList.Count);
4452 while (c.
Karma < jobList[jobIndex].MinKarma)
4456 if (jobIndex >= jobList.Count) { jobIndex -= jobList.Count; }
4457 if (skips >= jobList.Count) {
break; }
4461 new JobVariant(jobList[jobIndex], 0);
4465 else if (c.
JobPreferences.FirstOrDefault(jp => remainingJobs.Contains(jp.
Prefab)) is { } remainingJob)
4467 JobAssignmentDebugLog.Add(
4468 $
"{c.Name} has {remainingJob.Prefab.Name} as their {c.JobPreferences.IndexOf(remainingJob) + 1}. preference, and it is still available."+
4469 $
" Assigning {c.Name} as a {remainingJob.Prefab.Name}.");
4471 assignedClientCount[remainingJob.Prefab]++;
4475 c.
AssignedJob =
new JobVariant(remainingJobs[Rand.Range(0, remainingJobs.Count)], 0);
4477 JobAssignmentDebugLog.Add(
4478 $
"No suitable jobs available for {c.Name} (karma {c.Karma}). Assigning a random job: {c.AssignedJob.Prefab.Name}.");
4490 shuffledPrefabs.Shuffle(Rand.RandSync.Unsynced);
4492 Dictionary<JobPrefab, int> assignedPlayerCount =
new Dictionary<JobPrefab, int>();
4493 foreach (
JobPrefab jp
in shuffledPrefabs)
4496 assignedPlayerCount.Add(jp, 0);
4500 foreach (
Client c
in connectedClients)
4502 if (c.
TeamID != teamID)
continue;
4513 List<CharacterInfo> unassignedBots =
new List<CharacterInfo>(bots);
4514 while (unassignedBots.Count > 0)
4517 var jobsBelowMinNumber = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.
MinNumber);
4518 if (jobsBelowMinNumber.Any())
4520 AssignJob(unassignedBots[0], jobsBelowMinNumber.GetRandomUnsynced());
4525 var jobsBelowInitialCount = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.
InitialCount);
4526 if (jobsBelowInitialCount.Any())
4528 AssignJob(unassignedBots[0], jobsBelowInitialCount.GetRandomUnsynced());
4541 var remainingJobs = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.
MaxNumber);
4543 if (remainingJobs.None())
4545 DebugConsole.ThrowError(
"Failed to assign a suitable job for bot \"" + c.
Name +
"\" (all jobs already have the maximum numbers of players). Assigning a random job...");
4546 AssignJob(c, shuffledPrefabs.GetRandomUnsynced());
4551 var selectedJob = remainingJobs.GetRandomByWeight(jp => 1.0f / Math.Max(assignedPlayerCount[jp], 0.01f), Rand.RandSync.Unsynced);
4552 AssignJob(c, selectedJob);
4558 int variant = Rand.Range(0, job.
Variants);
4559 bot.
Job =
new Job(job, isPvP, Rand.RandSync.Unsynced, variant);
4561 unassignedBots.Remove(bot);
4565 private Client FindClientWithJobPreference(List<Client> clients,
JobPrefab job,
bool forceAssign =
false)
4567 int bestPreference =
int.MaxValue;
4568 Client preferredClient =
null;
4569 foreach (
Client c
in clients)
4573 if (index > -1 && index < bestPreference)
4575 bestPreference = index;
4576 preferredClient = c;
4581 if (forceAssign && preferredClient ==
null)
4583 preferredClient = clients[Rand.Int(clients.Count)];
4586 return preferredClient;
4591 foreach (var client
in connectedClients)
4596 msg.
WriteByte((
byte)(missionIndex == -1 ? 255: missionIndex));
4597 mission?.ServerWrite(msg);
4598 serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
4604 if (character ==
null) {
return "[NULL]"; }
4606 return ClientLogName(client, character.
LogName);
4615 GameMain.
Server.ServerSettings.ServerLog.WriteLine(line, messageType);
4648 GameAnalyticsManager.AddDesignEvent(
"GameServer:ShutDown");
4649 serverPeer?.Close();
4651 SteamManager.CloseServer();
4655 private void UpdateClientLobbies()
4658 LastClientListUpdateID++;
4661 private List<Client> GetPlayingClients()
4663 List<Client> playingClients =
new List<Client>(connectedClients);
4670 return playingClients;
4680 List<Client> team1 =
new List<Client>();
4681 List<Client> team2 =
new List<Client>();
4682 List<Client> playingClients = GetPlayingClients();
4685 List<Client> unassignedClients =
new List<Client>(playingClients);
4686 for (
int i = 0; i < unassignedClients.Count; i++)
4691 assignTeam(unassignedClients[i], unassignedClients[i].PreferredTeam);
4697 if (assignUnassignedNow)
4699 if (unassignedClients.Any())
4701 SendChatMessage(TextManager.Get(
"PvP.WithoutTeamWillBeRandomlyAssigned").Value,
ChatMessageType.Server);
4705 while (unassignedClients.Any())
4707 var randomClient = unassignedClients.GetRandom(Rand.RandSync.Unsynced);
4715 int sizeDifference = Math.Abs(team1.Count - team2.Count);
4720 SendChatMessage(TextManager.Get(
"AutoBalance.Activating").Value,
ChatMessageType.Server);
4726 var biggerTeam = GetPlayingClients().Where(
4727 c => team1.Count > team2.Count ?
4737 if (pvpAutoBalanceCountdownRemaining == -1)
4739 SendChatMessage(TextManager.GetWithVariables(
4740 "AutoBalance.CountdownStarted",
4741 (
"[teamname]", TextManager.Get(team1.Count > team2.Count ?
"teampreference.team1" :
"teampreference.team2")),
4743 (
"[numberseconds]", PvpAutoBalanceCountdown.ToString())
4745 pvpAutoBalanceCountdownRemaining = PvpAutoBalanceCountdown;
4752 StopAutoBalanceCountdown();
4758 StopAutoBalanceCountdown();
4762 UpdateClientLobbies();
4767 unassignedClients.Remove(client);
4780 string teamNameVariable =
"";
4783 team2.Remove(client);
4785 teamNameVariable =
"teampreference.team1";
4789 team1.Remove(client);
4791 teamNameVariable =
"teampreference.team2";
4793 SendChatMessage(TextManager.GetWithVariables(
4794 "AutoBalance.PlayerMoved",
4795 (
"[clientname]", client.
Name),
4796 (
"[teamname]", TextManager.Get(teamNameVariable))
4811 if (Team1Count == Team2Count)
4841 private void StopAutoBalanceCountdown()
4843 if (pvpAutoBalanceCountdownRemaining != -1)
4845 SendChatMessage(TextManager.Get(
"AutoBalance.CountdownCancelled").Value,
ChatMessageType.Server);
4847 pvpAutoBalanceCountdownRemaining = -1;
4858 public readonly List<Client> KickVoters =
new List<Client>();
readonly CauseOfDeathType Type
float VitalityDisregardingDeath
How much vitality the character would have if it was alive? E.g. a character killed by disconnection ...
void ServerReadInput(IReadMessage msg, Client c)
CauseOfDeath CauseOfDeath
CharacterHealth CharacterHealth
void SetStun(float newStun, bool allowStunDecrease=false, bool isNetworkMessage=false)
void Revive(bool removeAfflictions=true, bool createNetworkEvent=false)
static readonly List< Character > CharacterList
void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage=false, bool log=true)
float KillDisconnectedTimer
bool IsClientOwner(Client client)
void SetOwnerClient(Client client)
UInt16 LastNetworkUpdateID
bool CanHearCharacter(Character speaker)
float? HealthUpdateInterval
Stores information about the Character that is needed between rounds in the menu etc....
CharacterInfo(Identifier speciesName, string name="", string originalName="", Either< Job, JobPrefab > jobOrJobPrefab=null, int variant=0, Rand.RandSync randSync=Rand.RandSync.Unsynced, Identifier npcIdentifier=default)
void ClearCurrentOrders()
void RecreateHead(ImmutableHashSet< Identifier > tags, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex)
Character Character
Note: Can be null.
static CoroutineStatus Success
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
static NetLobbyScreen NetLobbyScreen
static GameScreen GameScreen
static GameSession GameSession
static GameModePreset PvP
static GameModePreset MultiPlayerCampaign
readonly Identifier Identifier
CharacterTeamType? WinningTeam
static bool ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(GameModePreset preset, IEnumerable< Identifier > missionTypes)
static PerkCollection GetPerks()
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
IEnumerable< Mission > Missions
bool TryPurchaseSubmarine(SubmarineInfo newSubmarine, Client? client=null)
static bool ShouldApplyDisembarkPoints(GameModePreset? preset)
static bool ValidatedDisembarkPoints(GameModePreset preset, IEnumerable< Identifier > missionTypes)
int GetMissionIndex(Mission mission)
void EndRound(string endMessage, CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults=null)
void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client=null)
Switch to another submarine. The sub is loaded when the next round starts.
static bool Compare(string a, string b)
void TransmitSignal(Signal signal, bool sentFromChat)
static readonly PrefabCollection< JobPrefab > Prefabs
void OnClientDisconnected(Client client)
void SelectPreset(string presetName)
void UpdateClients(IEnumerable< Client > clients, float deltaTime)
object Call(string name, params object[] args)
void Initialize(bool forceEnableCs=false)
bool disableDisconnectCharacter
override string ToString()
void BanPlayer(string name, Endpoint endpoint, string reason, TimeSpan? duration)
IReadOnlyList< BannedPlayer > BannedPlayers
void UnbanPlayer(Endpoint endpoint)
readonly Either< Address, AccountId > AddressOrAccountId
string ApplyDistanceEffect(Character listener)
static void ServerRead(IReadMessage msg, Client c)
static string GetChatMessageCommand(string message, out string messageWithoutCommand)
const int MaxMessagesPerPacket
static ChatMessage Create(string senderName, string text, ChatMessageType type, Entity sender, Client client=null, PlayerConnectionChangeType changeType=PlayerConnectionChangeType.None, Color? textColor=null)
readonly Client SenderClient
Character SenderCharacter
static bool CanUseRadio(Character sender, bool ignoreJamming=false)
UInt16 LastChatMsgQueueID
UInt16 UnreceivedEntityEventCount
bool? WaitForNextRoundRespawn
UInt16 LastRecvCampaignSave
void ResetVotes(bool resetKickVotes)
Reset what this client has voted for and the kick votes given to this client
static string SanitizeName(string name)
readonly List< ChatMessage > ChatMsgQueue
readonly HashSet< DebugConsole.Command > PermittedConsoleCommands
UInt16 LastRecvEntityEventID
bool AddressMatches(Address address)
HashSet< Identifier > GivenAchievements
static readonly TimeSpan NameChangeCoolDown
UInt16 LastSentServerSettingsUpdate
bool HasPermission(ClientPermissions permission)
void WritePermissions(IWriteMessage msg)
CharacterTeamType PreferredTeam
static void ReadPermissions(IReadMessage inc, out ClientPermissions permissions, out List< DebugConsole.Command > permittedCommands)
void SetPermissions(ClientPermissions permissions, IEnumerable< DebugConsole.Command > permittedConsoleCommands)
static bool IsValidName(string name, ServerSettings serverSettings)
readonly Dictionary< UInt16, double > EntityEventLastSent
void GivePermission(ClientPermissions permission)
float DeleteDisconnectedTimer
Option< AccountId > AccountId
The ID of the account used to authenticate this session. This value can be used as a persistent value...
DateTime LastNameChangeTime
static void UpdateKickVotes(IReadOnlyList< Client > connectedClients)
UInt16 LastRecvClientListUpdate
CharacterInfo? CharacterInfo
readonly byte SessionId
An ID for this client for the current session. THIS IS NOT A PERSISTENT VALUE. DO NOT STORE THIS LONG...
ClientPermissions Permissions
void AddKickVote(Client voter)
bool TryTakeOverBot(Character botCharacter)
readonly Dictionary< MultiPlayerCampaign.NetFlags, UInt16 > LastRecvCampaignUpdate
UInt16 LastSentEntityEventID
readonly Queue< Entity > PendingPositionUpdates
bool HasKickVoteFrom(Client voter)
readonly Dictionary< Entity, float > PositionUpdateLastSent
LanguageIdentifier Language
List< JobVariant > JobPreferences
UInt16 LastRecvLobbyUpdate
NetworkConnection Connection
double MidRoundSyncTimeOut
static Option< Endpoint > Parse(string str)
IReadOnlyList< FileTransferOut > ActiveTransfers
FileTransferOut StartTransfer(NetworkConnection recipient, FileTransferType fileType, string filePath)
void ReadFileRequest(IReadMessage inc, Client client)
FileTransferDelegate OnStarted
void Update(float deltaTime)
FileTransferDelegate OnEnded
void KickClient(Client client, string reason, bool resetKarma=false)
GameServer(string name, IPAddress listenIp, int port, int queryPort, bool isPublic, string password, bool attemptUPnP, int maxPlayers, Option< int > ownerKey, Option< P2PEndpoint > ownerEndpoint)
void SetClientCharacter(Client client, Character newCharacter)
override void UnbanPlayer(string playerName)
void UpdateVoteStatus(bool checkActiveVote=true)
static string CharacterLogName(Character character)
void DisconnectClient(Client client, PeerDisconnectPacket peerDisconnectPacket)
NetworkConnection OwnerConnection
void IncrementStat(Client client, AchievementStat stat, int amount)
void GiveAchievement(Character character, Identifier achievementIdentifier)
void SendOrderChatMessage(OrderChatMessage message)
TryStartGameResult TryStartGame()
override IReadOnlyList< Client > ConnectedClients
void BanPreviousPlayer(PreviousPlayer previousPlayer, string reason, TimeSpan? duration=null)
void SendDirectChatMessage(string txt, Client recipient, ChatMessageType messageType=ChatMessageType.Server)
void Update(float deltaTime)
void GiveAchievement(Client client, Identifier achievementIdentifier)
void EndGame(CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, bool wasSaved=false, IEnumerable< Mission > missions=null)
override void KickPlayer(string playerName, string reason)
void UpdateMissionState(Mission mission)
void UpdateCheatsEnabled()
void RefreshPvpTeamAssignments(bool assignUnassignedNow=false, bool autoBalanceNow=false)
Assigns currently playing clients into PvP teams according to current server settings.
void ClearRecentlyDisconnectedClients()
void DisconnectClient(NetworkConnection senderConnection, PeerDisconnectPacket peerDisconnectPacket)
void BanClient(Client client, string reason, TimeSpan? duration=null)
bool TryChangeClientName(Client c, string newName)
void StartServer(bool registerToServerList)
ServerEntityEventManager EntityEventManager
void KickClient(NetworkConnection conn, string reason)
void SendConsoleMessage(string txt, Client recipient, Color? color=null)
bool IsNameValid(Client c, string newName)
bool TrySwitchSubmarine()
void SendTraitorMessage(WriteOnlyMessage msg, Client client)
void IncrementStat(Character character, AchievementStat stat, int amount)
bool FindAndRemoveRecentlyDisconnectedConnection(NetworkConnection conn)
TraitorManager TraitorManager
void SendDirectChatMessage(ChatMessage msg, Client recipient)
float EndRoundTimeRemaining
void ClientWriteLobby(Client c)
static void AddPendingMessageToOwner(string message, ChatMessageType messageType)
Creates a message that gets sent to the server owner once the connection is initialized....
static void Log(string line, ServerLog.MessageType messageType)
void SendVoteStatus(List< Client > recipients)
void AssignBotJobs(List< CharacterInfo > bots, CharacterTeamType teamID, bool isPvP)
void AssignClientToPvpTeamMidgame(Client client)
Assign a team for single clients who join the server when a round is already running.
void AssignJobs(List< Client > unassigned)
override void UnbanPlayer(Endpoint endpoint)
override void BanPlayer(string playerName, string reason, TimeSpan? duration=null)
void UpdateClientPermissions(Client client)
override void AddChatMessage(ChatMessage message)
void SendChatMessage(string message, ChatMessageType? type=null, Client senderClient=null, Character senderCharacter=null, PlayerConnectionChangeType changeType=PlayerConnectionChangeType.None, ChatMode chatMode=ChatMode.None)
Add the message to the chatbox and pass it to all clients who can receive it
override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData=null)
void SendCancelTransferMsg(FileSender.FileTransferOut transfer)
abstract bool AddressMatches(NetworkConnection other)
Similar to EndpointMatches but ignores port on LidgrenEndpoint
LanguageIdentifier Language
NetworkConnectionStatus Status
static readonly List< PermissionPreset > List
bool MatchesClient(Client c)
void Update(float deltaTime)
bool CanRespawnAgain(CharacterTeamType team)
IEnumerable< Submarine > RespawnShuttles
bool StartWhenClientsReady
float ReplaceCostPercentage
Percentage modifier for the cost of hiring a new character to replace a permanently killed one.
bool AllowImmediateItemDelivery
string? ServerMessageText
void SetPassword(string password)
float DespawnDisconnectedPermadeathTime
The number of seconds a disconnected player's Character remains in the world until despawned,...
float SelectedLevelDifficulty
int PvpAutoBalanceThreshold
bool IronmanMode
This is an optional setting for permadeath mode. When it's enabled, a player client whose character d...
CampaignSettings CampaignSettings
float StartWhenClientsReadyRatio
List< SavedClientPermission > ClientPermissions
float DisallowKickVoteTime
bool ReadPerks(IReadMessage msg)
bool ServerDetailsChanged
Have some of the properties listed in the server list changed
int MaximumMoneyTransferRequest
BotSpawnMode BotSpawnMode
void WriteMonsterEnabled(IWriteMessage msg, Dictionary< Identifier, bool > monsterEnabled=null)
bool AllowBotTakeoverOnPermadeath
Are players allowed to take over bots when permadeath is enabled?
void ServerWrite(IWriteMessage outMsg, Client c)
bool AllowLinkingWifiToChat
float AutoRestartInterval
void ServerRead(IReadMessage incMsg, Client c)
float EndVoteRequiredRatio
bool AllowDragAndDropGive
void SaveClientPermissions()
PvpTeamSelectionMode PvpTeamSelectionMode
Dictionary< ItemPrefab, int > ExtraCargo
EnemyHealthBarMode ShowEnemyHealthBars
float KillDisconnectedTime
The number of seconds a disconnected player's Character remains in the world until despawned (via "br...
float KickVoteRequiredRatio
static void Read(IReadMessage inc, Client connectedClient)
void SendToClients(List< Client > clients)
Prefab(ContentFile file, Identifier identifier)
readonly Identifier Identifier
IEnumerable< Submarine > DockedTo
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
static IEnumerable< SubmarineInfo > SavedSubmarines
void Update(float deltaTime)
TraitorResults? GetEndResults()
void ServerWrite(IWriteMessage msg)
void ServerRead(IReadMessage inc, Client sender, DoSProtection dosProtection)
void StopSubmarineVote(bool passed)
void Update(float deltaTime)
Identifier ReadIdentifier()
Interface for entities that handle ServerNetObject.ENTITY_POSITION
Interface for entities that the server can send events to the clients
void WriteBytes(byte[] val, int startIndex, int length)
void WriteRangedInteger(int val, int min, int max)
void WriteString(string val)
void WriteBoolean(bool val)
void WriteUInt16(UInt16 val)
void WriteIdentifier(Identifier val)
void WriteVariableUInt32(UInt32 val)
void WriteInt32(Int32 val)
void WriteSingle(Single val)
void WriteUInt32(UInt32 val)
void Finish(Voting voting, bool passed)
PlayerConnectionChangeType
@ Character
Characters only
SubmarineInfo? SelectedSub
SubmarineInfo SelectedShuttle
GameModePreset SelectedMode
IEnumerable< Identifier >? MissionTypes
Identifier? SelectedModeIdentifier
SubmarineInfo SelectedEnemySub
readonly ImmutableArray< AccountId > OtherMatchingIds
Other user IDs that this user might be closely tied to, such as the owner of the current copy of Baro...
readonly Option< AccountId > AccountId
The primary ID for a given user
static void Read(IReadMessage msg, SegmentDataReader segmentDataReader, ExceptionHandler? exceptionHandler=null)
static SegmentTableWriter< T > StartWriting(IWriteMessage msg)