5 using Microsoft.Xna.Framework;
6 using Microsoft.Xna.Framework.Input;
8 using System.Collections.Generic;
9 using System.Collections.Immutable;
10 using System.Globalization;
12 using System.Threading.Tasks;
13 using System.Xml.Linq;
28 public float DebugServerVoipAmplitude;
33 private UInt16 nameId = 0;
35 public string Name {
get;
private set; }
41 if (
string.IsNullOrEmpty(value)) {
return; }
75 private List<Identifier> permittedConsoleCommands =
new List<Identifier>();
77 private bool connected;
79 private enum RoundInitStatus
83 WaitingForStartGameFinalize,
89 private UInt16? debugStartGameCampaignSaveID;
91 private RoundInitStatus roundInitStatus = RoundInitStatus.NotStarted;
93 public bool RoundStarting => roundInitStatus == RoundInitStatus.Starting || roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize;
95 private readonly List<Client> otherClients;
101 private bool canStart;
103 private UInt16 lastSentChatMsgID = 0;
104 private UInt16 lastQueueChatMsgID = 0;
105 private readonly List<ChatMessage> chatMsgQueue =
new List<ChatMessage>();
110 public void PrintReceiverTransters()
114 DebugConsole.NewMessage(transfer.FileName +
" " + transfer.Progress.ToString());
154 private readonly List<Client> previouslyConnectedClients =
new List<Client>();
157 get {
return previouslyConnectedClients; }
175 private readonly ImmutableArray<Endpoint> serverEndpoints;
176 private readonly Option<int> ownerKey;
180 internal readonly
struct PermissionChangedEvent
183 public readonly ImmutableArray<Identifier> NewPermittedConsoleCommands;
185 public PermissionChangedEvent(
ClientPermissions newPermissions, IReadOnlyList<Identifier> newPermittedConsoleCommands)
187 NewPermissions = newPermissions;
188 NewPermittedConsoleCommands = newPermittedConsoleCommands.ToImmutableArray();
192 public readonly NamedEvent<PermissionChangedEvent>
OnPermissionChanged =
new NamedEvent<PermissionChangedEvent>();
195 : this(newName, endpoint.ToEnumerable().ToImmutableArray(), serverName, ownerKey) { }
197 public GameClient(
string newName, ImmutableArray<Endpoint> endpoints,
string serverName, Option<int> ownerKey)
200 this.ownerKey = ownerKey;
202 roundInitStatus = RoundInitStatus.NotStarted;
216 isHorizontal:
true, childAnchor:
Anchor.CenterRight)
232 TextManager.Get(
"ServerLog"))
234 OnClicked = (
GUIButton button,
object userData) =>
243 GUI.KeyboardDispatcher.Subscriber =
null;
252 MinSize = new Point(150, 0)
253 }, TextManager.Get(
"CamFollowSubmarine"))
256 OnSelected = (tbox) =>
264 Hull.EditFire =
false;
280 otherClients =
new List<Client>();
285 serverEndpoints = endpoints;
286 InitiateServerJoin();
301 private void InitiateServerJoin()
303 LastClientListUpdateID = 0;
310 otherClients.Clear();
321 ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
325 CoroutineManager.StartCoroutine(WaitForStartingInfo(),
"WaitForStartingInfo");
330 SteamManager.SetLobbyPublic(isPublic);
335 Networking.ClientPeer.Callbacks callbacks =
new ClientPeer.Callbacks(
337 OnClientPeerDisconnect,
338 OnConnectionInitializationComplete);
339 return serverEndpoints.First()
switch
342 =>
new LidgrenClientPeer(lidgrenEndpoint, callbacks, ownerKey),
346 =>
new P2PClientPeer(serverEndpoints.Cast<
P2PEndpoint>().ToImmutableArray(), callbacks),
347 _ =>
throw new ArgumentOutOfRangeException()
354 var basicServerCrashMsg = TextManager.Get($
"{nameof(DisconnectReason)}.{nameof(DisconnectReason.ServerCrashed)}");
357 .Where(mb => mb.Text?.Text == basicServerCrashMsg)
359 .ForEach(mb => mb.Close());
363 mb => (mb as
GUIMessageBox)?.Text?.Text != ChildServerRelay.CrashMessage))
365 var msgBox =
new GUIMessageBox(TextManager.Get(
"ConnectionLost"), ChildServerRelay.CrashMessage);
366 msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu;
370 private bool ReturnToPreviousMenu(
GUIButton button,
object obj)
384 GUIMessageBox.MessageBoxes.Clear();
389 private bool connectCancelled;
390 private void CancelConnect()
396 private IEnumerable<CoroutineStatus> WaitForStartingInfo()
398 GUI.SetCursorWaiting();
400 connectCancelled =
false;
404 DateTime timeOut = DateTime.Now +
new TimeSpan(0, 0, 200);
407 LocalizedString connectingText = TextManager.Get(
"Connecting");
408 while (!canStart && !connectCancelled)
410 if (reconnectBox ==
null && waitInServerQueueBox ==
null)
415 if (SteamManager.IsInitialized && steamConnection.AccountInfo.AccountId.TryUnwrap(out var accountId) && accountId is SteamId steamId)
417 serverDisplayName = steamId.ToString();
418 string steamUserName =
new Steamworks.Friend(steamId.Value).Name;
419 if (!
string.IsNullOrEmpty(steamUserName) && steamUserName !=
"[unknown]")
421 serverDisplayName = steamUserName;
425 if (
string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = TextManager.Get(
"Unknown").Value; }
429 TextManager.GetWithVariable(
"ConnectingTo",
"[serverip]", serverDisplayName));
432 if (reconnectBox !=
null)
434 reconnectBox.
Header.
Text = connectingText +
new string(
'.', ((
int)Timing.TotalTime % 3 + 1));
437 yield
return CoroutineStatus.Running;
439 if (DateTime.Now > timeOut)
441 ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
442 var msgBox =
new GUIMessageBox(TextManager.Get(
"ConnectionFailed"), TextManager.Get(
"CouldNotConnectToServer"))
444 DisplayInLoadingScreens =
true
446 msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu;
451 if (
ClientPeer.WaitingForPassword && !canStart && !connectCancelled)
453 GUI.ClearCursorWait();
458 yield
return CoroutineStatus.Running;
465 GUI.ClearCursorWait();
466 if (connectCancelled) { yield
return CoroutineStatus.Success; }
468 yield
return CoroutineStatus.Success;
478 OnPermissionChanged.Invoke(
new PermissionChangedEvent(permissions, permittedConsoleCommands));
509 incomingMessagesToProcess.Clear();
510 incomingMessagesToProcess.AddRange(pendingIncomingMessages);
511 foreach (var inc
in incomingMessagesToProcess)
513 ReadDataMessage(inc);
515 pendingIncomingMessages.Clear();
520 string errorMsg =
"Error while reading a message from server. ";
522 AppendExceptionInfo(ref errorMsg, e);
523 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
524 DebugConsole.ThrowError(errorMsg);
525 new GUIMessageBox(TextManager.Get(
"Error"), TextManager.GetWithVariables(
"MessageReadError", (
"[message]", e.Message), (
"[targetsite]", e.TargetSite.ToString())))
527 DisplayInLoadingScreens =
true
530 GUI.DisableHUD =
false;
535 if (!connected) {
return; }
545 if (updateTimer <= DateTime.Now)
552 if (updateTimer <= DateTime.Now)
567 if (!ChildServerRelay.IsProcessAlive)
575 if (updateTimer <= DateTime.Now)
578 updateTimer = DateTime.Now + UpdateInterval;
582 private readonly List<IReadMessage> pendingIncomingMessages =
new List<IReadMessage>();
583 private readonly List<IReadMessage> incomingMessagesToProcess =
new List<IReadMessage>();
591 if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize
600 pendingIncomingMessages.Add(inc);
636 for (
int i = 0; i < requestLen; i++)
641 ClientPeer.Send(response, DeliveryMethod.Unreliable);
645 for (
int i = 0; i < clientCount; i++)
652 client.
Ping = clientPing;
657 ReadLobbyUpdate(inc);
662 ReadIngameUpdate(inc);
666 string errorMsg =
"Error while reading an ingame update message from server.";
667 AppendExceptionInfo(ref errorMsg, e);
668 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
675 string errorMsg =
"Failed to read a voice packet from the server (VoipClient == null). ";
676 if (GameMain.Client ==
null) { errorMsg +=
"Client disposed. "; }
677 errorMsg +=
"\n" + Environment.StackTrace.CleanupStackTrace();
678 GameAnalyticsManager.AddErrorEventOnce(
679 "GameClient.ReadDataMessage:VoipClientNull",
680 GameMain.Client ==
null ? GameAnalyticsManager.ErrorSeverity.Error : GameAnalyticsManager.ErrorSeverity.Warning,
689 GameMain.Client.DebugServerVoipAmplitude = inc.
ReadRangedSingle(min: 0, max: 1, bitCount: 8);
693 DebugConsole.Log(
"Received QUERY_STARTGAME packet.");
699 string enemySubName = subName;
700 string enemySubHash = subHash;
713 Dictionary<MultiPlayerCampaign.NetFlags, UInt16> campaignUpdateIDs =
new Dictionary<MultiPlayerCampaign.NetFlags, ushort>();
714 foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
723 GameMain.NetLobbyScreen.UsingShuttle = usingShuttle;
725 if (campaign ==
null && campaignID == 0)
727 readyToStart = GameMain.NetLobbyScreen.TrySelectSub(subName, subHash,
SelectedSubType.Sub, GameMain.NetLobbyScreen.SubList) &&
728 GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash,
SelectedSubType.Shuttle, GameMain.NetLobbyScreen.ShuttleList.ListBox);
730 if (hasEnemySub && !GameMain.NetLobbyScreen.TrySelectSub(enemySubName, enemySubHash,
SelectedSubType.EnemySub, GameMain.NetLobbyScreen.SubList))
732 readyToStart =
false;
745 DebugConsole.Log(readyToStart ?
"Ready to start." :
"Not ready to start.");
749 ClientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable);
751 if (readyToStart && !CoroutineManager.IsCoroutineRunning(
"WaitForStartRound"))
753 CoroutineManager.StartCoroutine(NetLobbyScreen.WaitForStartRound(startButton:
null),
"WaitForStartRound");
757 DebugConsole.Log(
"Received WARN_STARTGAME packet.");
759 RoundStartWarningData warningData = INetSerializableStruct.Read<RoundStartWarningData>(inc);
760 var team1IncompatiblePerks = ToolBox.UintIdentifierArrayToPrefabCollection(DisembarkPerkPrefab.Prefabs, warningData.Team1IncompatiblePerks);
761 var team2IncompatiblePerks = ToolBox.UintIdentifierArrayToPrefabCollection(DisembarkPerkPrefab.Prefabs, warningData.Team2IncompatiblePerks);
763 GameMain.NetLobbyScreen?.ShowStartRoundWarning(SerializableDateTime.UtcNow + TimeSpan.FromSeconds(warningData.RoundStartsAnywaysTimeInSeconds), warningData.Team1Sub, team1IncompatiblePerks, warningData.Team2Sub, team2IncompatiblePerks);
766 DebugConsole.Log(
"Received CANCEL_STARTGAME packet.");
767 GameMain.NetLobbyScreen?.CloseStartRoundWarning();
770 DebugConsole.Log(
"Received STARTGAME packet.");
771 if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is CampaignMode)
774 CoroutineManager.StartCoroutine(StartGame(inc));
778 GUIMessageBox.CloseAll();
779 GameMain.Instance.ShowLoading(StartGame(inc),
false);
783 DebugConsole.NewMessage(
"Received STARTGAMEFINALIZE packet. Round init status: " + roundInitStatus);
784 if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize)
787 if (campaign !=
null &&
793 ReadStartGameFinalize(inc);
797 CampaignMode.TransitionType transitionType = (CampaignMode.TransitionType)inc.
ReadByte();
799 string endMessage =
string.Empty;
803 for (
int i = 0; i < missionCount; i++)
806 var mission = GameMain.GameSession?.GetMission(i);
809 mission.Completed = missionSuccessful;
815 GameMain.GameSession.WinningTeam = winningTeam;
816 var combatMission = GameMain.GameSession.Missions.FirstOrDefault(m => m is CombatMission);
817 if (combatMission !=
null)
819 combatMission.Completed =
true;
824 TraitorManager.TraitorResults? traitorResults =
null;
825 if (includesTraitorInfo)
827 traitorResults = INetSerializableStruct.Read<TraitorManager.TraitorResults>(inc);
830 roundInitStatus = RoundInitStatus.Interrupted;
831 CoroutineManager.StartCoroutine(
EndGame(endMessage, transitionType, traitorResults),
"EndGame");
832 GUI.SetSavingIndicatorState(save);
836 List<CampaignMode.SaveInfo> saveInfos =
new List<CampaignMode.SaveInfo>();
837 for (
int i = 0; i < saveCount; i++)
839 saveInfos.Add(INetSerializableStruct.Read<CampaignMode.SaveInfo>(inc));
841 MultiPlayerCampaign.StartCampaignSetup(saveInfos);
844 ReadPermissions(inc);
847 ReadAchievement(inc);
850 ReadAchievementStat(inc);
855 if (cheatsEnabled == DebugConsole.CheatsEnabled)
861 DebugConsole.CheatsEnabled = cheatsEnabled;
862 AchievementManager.CheatsEnabled = cheatsEnabled;
865 var cheatMessageBox =
new GUIMessageBox(TextManager.Get(
"CheatsEnabledTitle"), TextManager.Get(
"CheatsEnabledDescription"));
866 cheatMessageBox.Buttons[0].OnClicked += (btn, userdata) =>
868 DebugConsole.TextBox.Select();
881 ReadCircuitBoxMessage(inc);
887 ReadyCheck.ClientRead(inc);
890 TraitorManager.ClientRead(inc);
898 Mission mission = GameMain.GameSession?.GetMission(missionIndex);
899 mission?.ClientRead(inc);
903 GameMain.GameSession?.EventManager.ClientRead(inc);
906 GameMain.NetLobbyScreen?.CampaignSetupUI?.OnBackupIndicesReceived(inc);
913 TaskPool.ListTasks(DebugConsole.Log);
914 ushort contentToPreloadCount = inc.
ReadUInt16();
915 List<ContentFile> contentToPreload =
new List<ContentFile>();
916 for (
int i = 0; i < contentToPreloadCount; i++)
919 ContentFile file = ContentPackageManager.EnabledPackages.All
921 p.Files.FirstOrDefault(f => f.Path == filePath))
922 .FirstOrDefault(f => f is not
null);
923 contentToPreload.AddIfNotNull(file);
926 string campaignErrorInfo =
string.Empty;
927 if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign)
929 campaignErrorInfo = $
" Round start save ID: {debugStartGameCampaignSaveID}, last save id: {campaign.LastSaveID}, pending save id: {campaign.PendingSaveID}.";
932 GameMain.GameSession.EventManager.PreloadContent(contentToPreload);
934 int subEqualityCheckValue = inc.
ReadInt32();
935 if (subEqualityCheckValue != (
Submarine.MainSub?.Info?.EqualityCheckVal ?? 0))
938 "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server. " +
939 $
"There may have been an error in receiving the up-to-date submarine file from the server. Round init status: {roundInitStatus}." + campaignErrorInfo;
940 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:SubsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
941 throw new Exception(errorMsg);
945 List<Identifier> serverMissionIdentifiers =
new List<Identifier>();
946 for (
int i = 0; i < missionCount; i++)
950 if (missionCount != GameMain.GameSession.GameMode.Missions.Count())
953 $
"Mission equality check failed. Mission count doesn't match the server. " +
954 $
"Server: {string.Join(",
", serverMissionIdentifiers)}, " +
955 $
"client: {string.Join(",
", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
956 $
"game session: {string.Join(",
", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo;
957 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
958 throw new Exception(errorMsg);
961 if (missionCount > 0)
963 if (!GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier).OrderBy(
id =>
id).SequenceEqual(serverMissionIdentifiers.OrderBy(
id =>
id)))
966 $
"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " +
967 $
"Server: {string.Join(",
", serverMissionIdentifiers)}, " +
968 $
"client: {string.Join(",
", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
969 $
"game session: {string.Join(",
", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo;
970 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
971 throw new Exception(errorMsg);
973 GameMain.GameSession.EnforceMissionOrder(serverMissionIdentifiers);
976 var levelEqualityCheckValues =
new Dictionary<Level.LevelGenStage,
int>();
977 foreach (Level.LevelGenStage stage in Enum.GetValues(typeof(Level.LevelGenStage)).OfType<Level.LevelGenStage>().OrderBy(s => s))
979 levelEqualityCheckValues.Add(stage, inc.
ReadInt32());
982 foreach (var stage
in levelEqualityCheckValues.Keys)
984 if (Level.Loaded.EqualityCheckValues[stage] != levelEqualityCheckValues[stage])
986 string errorMsg =
"Level equality check failed. The level generated at your end doesn't match the level generated by the server" +
987 " (client value " + stage +
": " + Level.Loaded.EqualityCheckValues[stage].ToString(
"X") +
988 ", server value " + stage +
": " + levelEqualityCheckValues[stage].ToString(
"X") +
989 ", level value count: " + levelEqualityCheckValues.Count +
990 ", seed: " + Level.Loaded.Seed +
991 ", sub: " + (
Submarine.MainSub ==
null ?
"null" : (
Submarine.MainSub.Info.Name +
" (" +
Submarine.MainSub.Info.MD5Hash.ShortRepresentation +
")")) +
992 ", mirrored: " + Level.Loaded.Mirrored +
"). Round init status: " + roundInitStatus +
"." + campaignErrorInfo;
993 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
994 throw new Exception(errorMsg);
998 foreach (Mission mission
in GameMain.GameSession.Missions)
1000 mission.ClientReadInitial(inc);
1005 CrewManager.ClientReadActiveOrders(inc);
1010 ApplyDisembarkPerk();
1013 roundInitStatus = RoundInitStatus.Started;
1016 private void ApplyDisembarkPerk()
1018 var characters = GameSession.GetSessionCrewCharacters(
CharacterType.Both);
1020 ImmutableArray<Character> team1Characters = characters.Where(
static c => c.
TeamID is
CharacterTeamType.Team1).ToImmutableArray(),
1023 GameSession.GetPerks().ApplyAll(team1Characters, team2Characters);
1029 private void OnClientPeerDisconnect(PeerDisconnectPacket disconnectPacket)
1031 bool wasConnected = connected;
1033 connectCancelled =
true;
1035 CoroutineManager.StopCoroutines(
"WaitForStartingInfo");
1036 CloseReconnectBox();
1038 GUI.ClearCursorWait();
1040 if (disconnectPacket.ShouldCreateAnalyticsEvent)
1042 GameAnalyticsManager.AddErrorEventOnce(
1043 "GameClient.HandleDisconnectMessage",
1044 GameAnalyticsManager.ErrorSeverity.Debug,
1045 $
"Client received a disconnect message. Reason: {disconnectPacket.DisconnectReason}");
1048 if (disconnectPacket.DisconnectReason == DisconnectReason.ServerFull)
1052 else if (disconnectPacket.ShouldAttemptReconnect && !
IsServerOwner && wasConnected)
1054 if (disconnectPacket.IsEventSyncError)
1056 GameMain.NetLobbyScreen.Select();
1057 GameMain.GameSession?.EndRound(
"");
1058 GameStarted =
false;
1061 AttemptReconnect(disconnectPacket);
1067 Eos.EosSessionManager.LeaveSession();
1068 SteamManager.LeaveLobby();
1071 GameMain.ModDownloadScreen.Reset();
1072 ContentPackageManager.EnabledPackages.Restore();
1074 GameMain.GameSession?.Campaign?.CancelStartRound();
1082 ChildServerRelay.AttemptGracefulShutDown();
1083 GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary);
1089 GameMain.Client =
null;
1090 GameMain.GameSession =
null;
1092 ReturnToPreviousMenu(
null,
null);
1093 if (disconnectPacket.DisconnectReason != DisconnectReason.Disconnected)
1095 new GUIMessageBox(TextManager.Get(wasConnected ?
"ConnectionLost" :
"CouldNotConnectToServer"), disconnectPacket.PopupMessage)
1097 DisplayInLoadingScreens =
true
1103 private void CreateReconnectBox(LocalizedString headerText, LocalizedString bodyText)
1105 reconnectBox =
new GUIMessageBox(
1108 new LocalizedString[] { TextManager.Get(
"Cancel") })
1110 DisplayInLoadingScreens =
true
1112 reconnectBox.
Buttons[0].OnClicked += (btn, userdata) => { CancelConnect();
return true; };
1113 reconnectBox.
Buttons[0].OnClicked += reconnectBox.
Close;
1116 private void CloseReconnectBox()
1118 reconnectBox?.
Close();
1119 reconnectBox =
null;
1122 private void AskToWaitInQueue()
1124 CoroutineManager.StopCoroutines(
"WaitForStartingInfo");
1127 if (CoroutineManager.IsCoroutineRunning(
"WaitInServerQueue"))
1132 var queueBox =
new GUIMessageBox(
1133 TextManager.Get(
"DisconnectReason.ServerFull"),
1134 TextManager.Get(
"ServerFullQuestionPrompt"),
new LocalizedString[] { TextManager.Get(
"Cancel"), TextManager.Get(
"ServerQueue") });
1136 queueBox.Buttons[0].OnClicked += queueBox.Close;
1137 queueBox.Buttons[1].OnClicked += queueBox.Close;
1138 queueBox.Buttons[1].OnClicked += (btn, userdata) =>
1140 CloseReconnectBox();
1141 CoroutineManager.StartCoroutine(WaitInServerQueue(),
"WaitInServerQueue");
1146 private void AttemptReconnect(PeerDisconnectPacket peerDisconnectPacket)
1148 connectCancelled =
false;
1151 TextManager.Get(
"ConnectionLost"),
1152 peerDisconnectPacket.ReconnectMessage);
1154 var prevContentPackages =
ClientPeer.ServerContentPackages;
1156 GameMain.NetLobbyScreen.LastUpdateID--;
1157 InitiateServerJoin();
1161 ClientPeer.ContentPackageOrderReceived =
true;
1162 ClientPeer.ServerContentPackages = prevContentPackages;
1166 private void UpdatePresence(
string connectCommand)
1168 #warning TODO: use store localization functionality
1169 var desc = TextManager.GetWithVariable(
"FriendPlayingOnServer",
"[servername]",
ServerName);
1171 async Task updateEosPresence()
1173 var epicIds = EosInterface.IdQueries.GetLoggedInEpicIds();
1174 if (!epicIds.FirstOrNone().TryUnwrap(out var epicAccountId)) {
return; }
1176 var setPresenceResult = await EosInterface.Presence.SetJoinCommand(
1177 epicAccountId: epicAccountId,
1180 joinCommand: connectCommand);
1181 DebugConsole.NewMessage($
"Set connect command: {connectCommand}, result: {setPresenceResult}");
1185 "UpdateEosPresence",
1186 updateEosPresence(),
1189 if (SteamManager.IsInitialized)
1191 Steamworks.SteamFriends.ClearRichPresence();
1192 if (!connectCommand.IsNullOrWhiteSpace())
1194 Steamworks.SteamFriends.SetRichPresence(
"servername",
ServerName);
1195 Steamworks.SteamFriends.SetRichPresence(
"status",
1197 Steamworks.SteamFriends.SetRichPresence(
"connect",
1203 private void OnConnectionInitializationComplete()
1205 UpdatePresence($
"-connect \"{ToolBox.EscapeCharacters(ServerName)}\" {string.Join(",
", serverEndpoints.Select(e => e.StringRepresentation))}");
1213 if (Screen.Selected is GameScreen or RoundSummaryScreen or NetLobbyScreen)
1223 GameMain.ModDownloadScreen.Select();
1227 if (GameMain.NetLobbyScreen?.ChatInput !=
null)
1229 GameMain.NetLobbyScreen.ChatInput.Enabled =
true;
1233 private IEnumerable<CoroutineStatus> WaitInServerQueue()
1235 waitInServerQueueBox =
new GUIMessageBox(
1236 TextManager.Get(
"ServerQueuePleaseWait"),
1237 TextManager.Get(
"WaitingInServerQueue"),
new LocalizedString[] { TextManager.Get(
"Cancel") });
1238 waitInServerQueueBox.
Buttons[0].OnClicked += (btn, userdata) =>
1240 CoroutineManager.StopCoroutines(
"WaitInServerQueue");
1241 waitInServerQueueBox?.
Close();
1242 waitInServerQueueBox =
null;
1248 if (!CoroutineManager.IsCoroutineRunning(
"WaitForStartingInfo"))
1250 InitiateServerJoin();
1251 yield
return new WaitForSeconds(5.0f);
1253 yield
return new WaitForSeconds(0.5f);
1256 waitInServerQueueBox?.
Close();
1257 waitInServerQueueBox =
null;
1259 yield
return CoroutineStatus.Success;
1266 AchievementManager.UnlockAchievement(achievementIdentifier);
1269 private static void ReadAchievementStat(
IReadMessage inc)
1271 var netStat = INetSerializableStruct.Read<NetIncrementedStat>(inc);
1272 AchievementManager.IncrementStat(netStat.Stat, netStat.Amount);
1275 private static void ReadCircuitBoxMessage(
IReadMessage inc)
1277 var header = INetSerializableStruct.Read<NetCircuitBoxHeader>(inc);
1279 INetSerializableStruct data = header.Opcode
switch
1281 CircuitBoxOpcode.Cursor => INetSerializableStruct.Read<NetCircuitBoxCursorInfo>(inc),
1282 CircuitBoxOpcode.Error => INetSerializableStruct.Read<CircuitBoxErrorEvent>(inc),
1283 _ =>
throw new ArgumentOutOfRangeException(nameof(header.Opcode), header.Opcode,
"This data cannot be handled using direct network messages.")
1286 if (header.FindTarget().TryUnwrap(out CircuitBox box))
1288 box.ClientRead(data);
1294 List<string> permittedConsoleCommands =
new List<string>();
1298 List<DebugConsole.Command> permittedCommands =
new List<DebugConsole.Command>();
1305 SetMyPermissions(permissions, permittedCommands.Select(command => command.Names[0]));
1309 private void SetMyPermissions(
ClientPermissions newPermissions, IEnumerable<Identifier> permittedConsoleCommands)
1311 if (!(this.permittedConsoleCommands.Any(c => !permittedConsoleCommands.Contains(c)) ||
1312 permittedConsoleCommands.Any(c => !
this.permittedConsoleCommands.Contains(c))))
1314 if (newPermissions == permissions) {
return; }
1320 permissions = newPermissions;
1321 this.permittedConsoleCommands = permittedConsoleCommands.ToList();
1325 GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as
string ==
"permissions");
1326 GUIMessageBox msgBox =
new GUIMessageBox(
"",
"") { UserData =
"permissions" };
1327 msgBox.Content.ClearChildren();
1328 msgBox.Content.RectTransform.RelativeSize =
new Vector2(0.95f, 0.9f);
1330 var header =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), msgBox.Content.RectTransform), TextManager.Get(
"PermissionsChanged"), textAlignment: Alignment.Center, font: GUIStyle.LargeFont);
1331 header.RectTransform.IsFixedSize =
true;
1333 var permissionArea =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 1.0f), msgBox.Content.RectTransform), isHorizontal:
true) { Stretch =
true, RelativeSpacing = 0.05f };
1334 var leftColumn =
new GUILayoutGroup(
new RectTransform(
new Vector2(0.5f, 1.0f), permissionArea.RectTransform)) { Stretch =
true, RelativeSpacing = 0.05f };
1335 var rightColumn =
new GUILayoutGroup(
new RectTransform(
new Vector2(0.5f, 1.0f), permissionArea.RectTransform)) { Stretch =
true, RelativeSpacing = 0.05f };
1337 var permissionsLabel =
new GUITextBlock(
new RectTransform(
new Vector2(newPermissions ==
ClientPermissions.None ? 2.0f : 1.0f, 0.0f), leftColumn.RectTransform),
1338 TextManager.Get(newPermissions ==
ClientPermissions.None ?
"PermissionsRemoved" :
"CurrentPermissions"),
1339 wrap:
true, font: (newPermissions ==
ClientPermissions.None ? GUIStyle.Font : GUIStyle.SubHeadingFont));
1340 permissionsLabel.RectTransform.NonScaledSize =
new Point(permissionsLabel.Rect.Width, permissionsLabel.Rect.Height);
1341 permissionsLabel.RectTransform.IsFixedSize =
true;
1344 LocalizedString permissionList =
"";
1347 if (!newPermissions.HasFlag(permission) || permission ==
ClientPermissions.None) {
continue; }
1348 permissionList +=
" - " + TextManager.Get(
"ClientPermission." + permission) +
"\n";
1350 new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), leftColumn.RectTransform),
1356 var commandsLabel =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), rightColumn.RectTransform),
1357 TextManager.Get(
"PermittedConsoleCommands"), wrap:
true, font: GUIStyle.SubHeadingFont);
1358 var commandList =
new GUIListBox(
new RectTransform(
new Vector2(1.0f, 1.0f), rightColumn.RectTransform));
1359 foreach (Identifier permittedCommand
in permittedConsoleCommands)
1361 new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.05f), commandList.Content.RectTransform, minSize:
new Point(0, 15)),
1362 permittedCommand.Value, font: GUIStyle.SmallFont)
1364 CanBeFocused =
false
1367 permissionsLabel.RectTransform.NonScaledSize = commandsLabel.RectTransform.NonScaledSize =
1368 new Point(permissionsLabel.Rect.Width, Math.Max(permissionsLabel.Rect.Height, commandsLabel.Rect.Height));
1369 commandsLabel.RectTransform.IsFixedSize =
true;
1372 new GUIButton(
new RectTransform(
new Vector2(0.5f, 0.05f), msgBox.Content.RectTransform), TextManager.Get(
"ok"))
1374 OnClicked = msgBox.Close
1377 permissionArea.RectTransform.MinSize =
new Point(0, Math.Max(leftColumn.RectTransform.Children.Sum(c => c.Rect.Height), rightColumn.RectTransform.Children.Sum(c => c.Rect.Height)));
1378 permissionArea.RectTransform.IsFixedSize =
true;
1379 int contentHeight = (int)(msgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height + msgBox.Content.AbsoluteSpacing) * 1.05f);
1380 msgBox.Content.ChildAnchor =
Anchor.TopCenter;
1381 msgBox.Content.Stretch =
true;
1382 msgBox.Content.RectTransform.MinSize =
new Point(0, contentHeight);
1383 msgBox.InnerFrame.RectTransform.MinSize =
new Point(0, (
int)(contentHeight / permissionArea.RectTransform.RelativeSize.Y / msgBox.Content.RectTransform.RelativeSize.Y));
1386 if (refreshCampaignUI)
1388 if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1390 campaign.CampaignUI?.UpgradeStore?.RequestRefresh();
1391 campaign.CampaignUI?.HRManagerUI?.RefreshUI();
1395 GameMain.NetLobbyScreen.RefreshEnabledElements();
1398 OnPermissionChanged.Invoke(
new PermissionChangedEvent(permissions, this.permittedConsoleCommands));
1401 private IEnumerable<CoroutineStatus> StartGame(
IReadMessage inc)
1406 eventErrorWritten =
false;
1407 GameMain.NetLobbyScreen.StopWaitingForStartRound();
1409 debugStartGameCampaignSaveID =
null;
1411 while (CoroutineManager.IsCoroutineRunning(
"EndGame"))
1414 yield
return CoroutineStatus.Running;
1419 GameMain.NetLobbyScreen.ShowSpectateButton();
1428 roundInitStatus = RoundInitStatus.Starting;
1433 GameModePreset gameMode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier);
1434 if (gameMode ==
null)
1436 DebugConsole.ThrowError(
"Game mode \"" + modeIdentifier +
"\" not found!");
1437 roundInitStatus = RoundInitStatus.Interrupted;
1438 yield
return CoroutineStatus.Failure;
1451 bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.
ReadBoolean();
1455 GameMain.LightManager.LightingEnabled =
true;
1459 Rand.SetSyncedSeed(seed);
1461 Task loadTask =
null;
1462 var roundSummary = (GUIMessageBox.MessageBoxes.Find(c => c?.UserData is RoundSummary)?.UserData) as RoundSummary;
1464 bool isOutpost =
false;
1466 if (gameMode != GameModePreset.MultiPlayerCampaign)
1476 string enemySubName = subName;
1477 string enemySubHash = subHash;
1484 List<UInt32> missionHashes =
new List<UInt32>();
1486 for (
int i = 0; i < missionCount; i++)
1490 if (!GameMain.NetLobbyScreen.TrySelectSub(subName, subHash,
SelectedSubType.Sub, GameMain.NetLobbyScreen.SubList))
1492 roundInitStatus = RoundInitStatus.Interrupted;
1493 yield
return CoroutineStatus.Success;
1498 if (!GameMain.NetLobbyScreen.TrySelectSub(enemySubName, enemySubHash,
SelectedSubType.EnemySub, GameMain.NetLobbyScreen.SubList))
1500 roundInitStatus = RoundInitStatus.Interrupted;
1501 yield
return CoroutineStatus.Success;
1505 if (!GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash,
SelectedSubType.Shuttle, GameMain.NetLobbyScreen.ShuttleList.ListBox))
1507 roundInitStatus = RoundInitStatus.Interrupted;
1508 yield
return CoroutineStatus.Success;
1512 if (GameMain.NetLobbyScreen.SelectedSub ==
null ||
1513 GameMain.NetLobbyScreen.SelectedSub.Name != subName ||
1514 GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation != subHash)
1516 string errorMsg =
"Failed to select submarine \"" + subName +
"\" (hash: " + subHash +
").";
1517 if (GameMain.NetLobbyScreen.SelectedSub ==
null)
1519 errorMsg +=
"\n" +
"SelectedSub is null";
1523 if (GameMain.NetLobbyScreen.SelectedSub.Name != subName)
1525 errorMsg +=
"\n" +
"Name mismatch: " + GameMain.NetLobbyScreen.SelectedSub.Name +
" != " + subName;
1527 if (GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation != subHash)
1529 errorMsg +=
"\n" +
"Hash mismatch: " + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation +
" != " + subHash;
1533 GameMain.NetLobbyScreen.Select();
1534 DebugConsole.ThrowError(errorMsg);
1535 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:FailedToSelectSub" + subName, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1536 roundInitStatus = RoundInitStatus.Interrupted;
1537 yield
return CoroutineStatus.Failure;
1539 if (GameMain.NetLobbyScreen.SelectedShuttle ==
null ||
1540 GameMain.NetLobbyScreen.SelectedShuttle.Name != shuttleName ||
1541 GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash?.StringRepresentation != shuttleHash)
1544 GameMain.NetLobbyScreen.Select();
1545 string errorMsg =
"Failed to select shuttle \"" + shuttleName +
"\" (hash: " + shuttleHash +
").";
1546 DebugConsole.ThrowError(errorMsg);
1547 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:FailedToSelectShuttle" + shuttleName, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1548 roundInitStatus = RoundInitStatus.Interrupted;
1549 yield
return CoroutineStatus.Failure;
1552 var selectedMissions = missionHashes.Select(i => MissionPrefab.Prefabs.Find(p => p.UintIdentifier == i));
1554 var selectedEnemySub = hasEnemySub && GameMain.NetLobbyScreen.SelectedEnemySub is { } enemySub ? Option.Some(enemySub) : Option.None;
1556 GameMain.GameSession =
new GameSession(GameMain.NetLobbyScreen.SelectedSub, selectedEnemySub, gameMode, missionPrefabs: selectedMissions);
1557 GameMain.GameSession.StartRound(levelSeed, levelDifficulty, levelGenerationParams:
null, forceBiome:
ServerSettings.
Biome);
1561 if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign))
1563 throw new InvalidOperationException(
"Attempted to start a campaign round when a campaign was not active.");
1566 if (GameMain.GameSession?.CrewManager !=
null) { GameMain.GameSession.CrewManager.Reset(); }
1570 int nextLocationIndex = inc.
ReadInt32();
1571 int nextConnectionIndex = inc.
ReadInt32();
1572 int selectedLocationIndex = inc.
ReadInt32();
1575 if (campaign.CampaignID != campaignID)
1578 DebugConsole.ThrowError(
"Failed to start campaign round (campaign ID does not match).");
1579 GameMain.NetLobbyScreen.Select();
1580 roundInitStatus = RoundInitStatus.Interrupted;
1581 yield
return CoroutineStatus.Failure;
1584 if (NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID) ||
1585 NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID))
1587 campaign.PendingSaveID = campaignSaveID;
1589 while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID))
1591 if (DateTime.Now > saveFileTimeOut)
1594 new GUIMessageBox(TextManager.Get(
"error"), TextManager.Get(
"campaignsavetransfer.timeout"));
1595 GameMain.NetLobbyScreen.Select();
1596 roundInitStatus = RoundInitStatus.Interrupted;
1598 yield
return CoroutineStatus.Success;
1600 yield
return new WaitForSeconds(0.1f);
1604 if (campaign.Map ==
null)
1607 DebugConsole.ThrowError(
"Failed to start campaign round (campaign map not loaded yet).");
1608 GameMain.NetLobbyScreen.Select();
1609 roundInitStatus = RoundInitStatus.Interrupted;
1610 yield
return CoroutineStatus.Failure;
1613 campaign.Map.SelectLocation(selectedLocationIndex);
1615 LevelData levelData = nextLocationIndex > -1 ?
1616 campaign.Map.Locations[nextLocationIndex].LevelData :
1617 campaign.Map.Connections[nextConnectionIndex].LevelData;
1619 debugStartGameCampaignSaveID = campaign.LastSaveID;
1621 if (roundSummary !=
null)
1623 loadTask = campaign.SelectSummaryScreen(roundSummary, levelData, mirrorLevel,
null);
1624 roundSummary.ContinueButton.Visible =
false;
1628 GameMain.GameSession.StartRound(levelData, mirrorLevel, startOutpost: campaign?.GetPredefinedStartOutpost());
1630 isOutpost = levelData.Type == LevelData.LevelType.Outpost;
1635 if (loadTask !=
null)
1637 while (!loadTask.IsCompleted && !loadTask.IsFaulted && !loadTask.IsCanceled)
1639 yield
return CoroutineStatus.Running;
1645 DebugConsole.ThrowError(
"There was an error initializing the round (disconnected during the StartGame coroutine.)");
1646 roundInitStatus = RoundInitStatus.Error;
1647 yield
return CoroutineStatus.Failure;
1650 roundInitStatus = RoundInitStatus.WaitingForStartGameFinalize;
1653 TimeSpan timeOutDuration =
new TimeSpan(0, 0, seconds: 30);
1654 DateTime timeOut = DateTime.Now + timeOutDuration;
1655 DateTime requestFinalizeTime = DateTime.Now;
1656 TimeSpan requestFinalizeInterval =
new TimeSpan(0, 0, 2);
1659 ClientPeer.Send(msg, DeliveryMethod.Unreliable);
1661 GUIMessageBox interruptPrompt =
null;
1663 if (includesFinalize)
1665 ReadStartGameFinalize(inc);
1673 if (DateTime.Now > requestFinalizeTime)
1675 msg =
new WriteOnlyMessage();
1677 ClientPeer.Send(msg, DeliveryMethod.Unreliable);
1678 requestFinalizeTime = DateTime.Now + requestFinalizeInterval;
1680 if (DateTime.Now > timeOut && interruptPrompt ==
null)
1682 interruptPrompt =
new GUIMessageBox(
string.Empty, TextManager.Get(
"WaitingForStartGameFinalizeTakingTooLong"),
1683 new LocalizedString[] { TextManager.Get(
"Yes"), TextManager.Get(
"No") })
1685 DisplayInLoadingScreens =
true
1687 interruptPrompt.Buttons[0].OnClicked += (btn, userData) =>
1689 roundInitStatus = RoundInitStatus.Interrupted;
1690 DebugConsole.ThrowError(
"Error while starting the round (did not receive STARTGAMEFINALIZE message from the server). Returning to the lobby...");
1692 GameMain.NetLobbyScreen.Select();
1693 interruptPrompt.Close();
1694 interruptPrompt =
null;
1697 interruptPrompt.Buttons[1].OnClicked += (btn, userData) =>
1699 timeOut = DateTime.Now + timeOutDuration;
1700 interruptPrompt.Close();
1701 interruptPrompt =
null;
1708 roundInitStatus = RoundInitStatus.Interrupted;
1712 if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) {
break; }
1716 DebugConsole.ThrowError(
"There was an error initializing the round.", e, createMessageBox:
true);
1717 roundInitStatus = RoundInitStatus.Error;
1722 yield
return CoroutineStatus.Running;
1726 interruptPrompt?.Close();
1727 interruptPrompt =
null;
1729 if (roundInitStatus != RoundInitStatus.Started)
1731 if (roundInitStatus != RoundInitStatus.Interrupted)
1733 DebugConsole.ThrowError(roundInitStatus.ToString());
1734 CoroutineManager.StartCoroutine(
EndGame(
""));
1735 yield
return CoroutineStatus.Failure;
1739 yield
return CoroutineStatus.Success;
1743 if (GameMain.GameSession.Submarine !=
null &&
1744 GameMain.GameSession.Submarine.Info.IsFileCorrupted)
1746 DebugConsole.ThrowError($
"Failed to start a round. Could not load the submarine \"{GameMain.GameSession.Submarine.Info.Name}\".");
1747 yield
return CoroutineStatus.Failure;
1750 for (
int i = 0; i <
Submarine.MainSubs.Length; i++)
1752 if (
Submarine.MainSubs[i] ==
null) {
break; }
1756 foreach (Item item
in Item.ItemList)
1758 if (item.Submarine ==
null) {
continue; }
1759 if (item.Submarine !=
Submarine.MainSubs[i] && !
Submarine.MainSubs[i].DockedTo.Contains(item.Submarine)) {
continue; }
1765 foreach (Submarine sub
in Submarine.MainSubs[i].DockedTo)
1768 sub.TeamID = teamID;
1780 if (roundSummary !=
null)
1782 roundSummary.ContinueButton.Visible =
true;
1785 GameMain.GameScreen.Select();
1787 string message =
"ServerMessage.HowToCommunicate" +
1788 $
"~[chatbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ActiveChat)}" +
1789 $
"~[pttbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Voice)}" +
1790 $
"~[switchbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleChatMode)}";
1793 yield
return CoroutineStatus.Success;
1799 DateTime timeOut = DateTime.Now +
new TimeSpan(0, 0, 60);
1800 while (TaskPool.IsTaskRunning(
"AsyncCampaignStartRound"))
1802 if (DateTime.Now > timeOut)
1804 throw new Exception(
"Failed to end a round (async campaign round start timed out).");
1819 GameStarted =
false;
1862 foreach (
Client c
in otherClients)
1877 for (
int i = 0; i < subListCount; i++)
1883 bool requiredContentPackagesInstalled = inc.
ReadBoolean();
1886 if (matchingSub ==
null)
1888 matchingSub =
new SubmarineInfo(Path.Combine(SaveUtil.SubmarineDownloadFolder, subName) +
".sub", subHash, tryLoad:
false)
1894 matchingSub.RequiredContentPackagesInstalled = requiredContentPackagesInstalled;
1898 GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList,
ServerSubmarines);
1899 GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.ShuttleList.ListBox,
ServerSubmarines.Where(s => s.HasTag(
SubmarineTag.Shuttle)));
1906 ReadPermissions(inc);
1910 if (Screen.Selected != GameMain.GameScreen)
1912 LocalizedString message;
1915 message = TextManager.Get(ironmanMode ?
"RoundRunningIronman" :
"RoundRunningPermadeath");
1919 message = TextManager.Get(allowSpectating ?
"RoundRunningSpectateEnabled" :
"RoundRunningSpectateDisabled");
1921 new GUIMessageBox(TextManager.Get(
"PleaseWait"), message);
1922 if (!(Screen.Selected is ModDownloadScreen)) { GameMain.NetLobbyScreen.Select(); }
1929 bool refreshCampaignUI =
false;
1931 GameMain.NetLobbyScreen.Team1Count = inc.
ReadByte();
1932 GameMain.NetLobbyScreen.Team2Count = inc.
ReadByte();
1933 List<TempClient> tempClients =
new List<TempClient>();
1935 for (
int i = 0; i < clientCount; i++)
1937 tempClients.Add(INetSerializableStruct.Read<
TempClient>(inc));
1941 if (NetIdUtils.IdMoreRecent(listId, LastClientListUpdateID))
1943 bool updateClientListId =
true;
1944 List<Client> currentClients =
new List<Client>();
1949 if (existingClient ==
null)
1958 otherClients.Add(existingClient);
1959 refreshCampaignUI =
true;
1960 GameMain.NetLobbyScreen.AddPlayer(existingClient);
1962 existingClient.NameId = tc.
NameId;
1965 existingClient.TeamID = tc.
TeamID;
1966 existingClient.Character =
null;
1967 existingClient.Karma = tc.
Karma;
1968 existingClient.Muted = tc.
Muted;
1969 existingClient.InGame = tc.
InGame;
1970 existingClient.IsOwner = tc.
IsOwner;
1972 GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient);
1973 if (Screen.Selected != GameMain.NetLobbyScreen && tc.
CharacterId > 0)
1977 if (existingClient.SessionId ==
SessionId)
1979 MultiplayerPreferences.Instance.TeamPreference = existingClient.PreferredTeam;
1983 GameMain.NetLobbyScreen.TeamPreferenceListBox?.Select(MultiplayerPreferences.Instance.TeamPreference);
1987 GameMain.NetLobbyScreen.RefreshPvpTeamSelectionButtons();
1989 existingClient.SetPermissions(permissions, permittedConsoleCommands);
1990 if (!NetIdUtils.IdMoreRecent(nameId, tc.
NameId))
1995 if (GameMain.NetLobbyScreen.CharacterNameBox !=
null &&
1996 !GameMain.NetLobbyScreen.CharacterNameBox.Selected)
1998 GameMain.NetLobbyScreen.CharacterNameBox.Text =
Name;
2001 currentClients.Add(existingClient);
2009 otherClients[i].Dispose();
2010 otherClients.RemoveAt(i);
2011 refreshCampaignUI =
true;
2016 int index = previouslyConnectedClients.FindIndex(c => c.
SessionId == client.
SessionId);
2019 if (previouslyConnectedClients.Count > 100)
2021 previouslyConnectedClients.RemoveRange(0, previouslyConnectedClients.Count - 100);
2026 previouslyConnectedClients.RemoveAt(index);
2028 previouslyConnectedClients.Add(client);
2030 if (updateClientListId) { LastClientListUpdateID = listId; }
2035 TaskPool.Add(
"WaitForPingDataAsync (owner)",
2036 Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) =>
2038 Steam.SteamManager.UpdateLobby(ServerSettings);
2043 GameMain.NetLobbyScreen?.UpdateDisembarkPointListFromServerSettings();
2046 if (refreshCampaignUI)
2048 if (GameMain.GameSession?.GameMode is CampaignMode campaign)
2050 campaign.CampaignUI?.UpgradeStore?.RequestRefresh();
2051 campaign.CampaignUI?.HRManagerUI?.RefreshUI();
2057 private bool initialUpdateReceived;
2071 var prevDispatcher = GUI.KeyboardDispatcher.Subscriber;
2076 byte[] settingsData = inc.
ReadBytes(settingsLen);
2079 if (isInitialUpdate)
2081 if (GameSettings.CurrentConfig.VerboseLogging)
2083 DebugConsole.NewMessage(
"Received initial lobby update, ID: " + updateID +
", last ID: " + GameMain.NetLobbyScreen.LastUpdateID, Color.Gray);
2085 ReadInitialUpdate(inc);
2086 initialUpdateReceived =
true;
2093 string selectEnemySubName = selectSubName;
2094 string selectEnemySubHash = selectSubHash;
2113 int traitorDangerLevel = inc.
ReadRangedInteger(TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel);
2115 List<Identifier> missionTypes =
new List<Identifier>();
2117 for (
int i = 0; i < missionTypeCount; i++)
2131 float autoRestartTimer = autoRestartEnabled ? inc.
ReadSingle() : 0.0f;
2135 if (NetIdUtils.IdMoreRecent(updateID, GameMain.NetLobbyScreen.LastUpdateID) &&
2136 (isInitialUpdate || initialUpdateReceived))
2138 ReadWriteMessage settingsBuf = new ReadWriteMessage();
2139 settingsBuf.WriteBytes(settingsData, 0, settingsLen); settingsBuf.BitPosition = 0;
2140 ServerSettings.ClientRead(settingsBuf);
2143 ServerInfo info = CreateServerInfoFromSettings();
2144 GameMain.ServerListScreen.AddToRecentServers(info);
2145 GameMain.NetLobbyScreen.Favorite.Visible = true;
2146 GameMain.NetLobbyScreen.Favorite.Selected = GameMain.ServerListScreen.IsFavorite(info);
2150 GameMain.NetLobbyScreen.Favorite.Visible = false;
2153 GameMain.NetLobbyScreen.LastUpdateID = updateID;
2156 GameMain.NetLobbyScreen.UsingShuttle = usingShuttle;
2158 if (!allowSubVoting || GameMain.NetLobbyScreen.SelectedSub ==
null)
2160 GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, SelectedSubType.Sub, GameMain.NetLobbyScreen.SubList);
2163 GameMain.NetLobbyScreen.TrySelectSub(selectEnemySubName, selectEnemySubHash, SelectedSubType.EnemySub, GameMain.NetLobbyScreen.SubList);
2166 GameMain.NetLobbyScreen.TrySelectSub(selectShuttleName, selectShuttleHash,
SelectedSubType.Shuttle, GameMain.NetLobbyScreen.ShuttleList.ListBox);
2168 GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability);
2169 GameMain.NetLobbyScreen.SetTraitorDangerLevel(traitorDangerLevel);
2170 GameMain.NetLobbyScreen.SetMissionTypes(missionTypes);
2171 GameMain.NetLobbyScreen.LevelSeed = levelSeed;
2173 GameMain.NetLobbyScreen.SelectMode(modeIndex);
2174 if (isInitialUpdate && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
2176 if (GameMain.Client.IsServerOwner) { RequestSelectMode(modeIndex); }
2179 if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
2181 foreach (SubmarineInfo sub in ServerSubmarines.Where(s => !ServerSettings.HiddenSubs.Contains(s.Name)))
2183 GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Campaign);
2187 GameMain.NetLobbyScreen.SetAllowSpectating(allowSpectating);
2188 GameMain.NetLobbyScreen.SetLevelDifficulty(levelDifficulty);
2189 GameMain.NetLobbyScreen.SetBotSpawnMode(botSpawnMode);
2190 GameMain.NetLobbyScreen.SetBotCount(botCount);
2191 GameMain.NetLobbyScreen.SetAutoRestart(autoRestartEnabled, autoRestartTimer);
2203 GUI.KeyboardDispatcher.Subscriber = prevDispatcher;
2209 if (campaignUpdated)
2211 MultiPlayerCampaign.ClientRead(inc);
2213 else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
2215 GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
2221 ReadClientList(inc);
2227 Voting.ClientRead(inc);
2235 readonly List<IServerSerializable> debugEntityList =
new List<IServerSerializable>();
2238 debugEntityList.Clear();
2245 segmentDataReader: (segment, inc) =>
2255 if (campaignUpdated)
2257 MultiPlayerCampaign.ClientRead(inc);
2259 else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
2261 GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
2268 int msgEndPos = (
int)(inc.
BitPosition + msgLength * 8);
2270 var header = INetSerializableStruct.Read<EntityPositionHeader>(inc);
2275 DebugConsole.ThrowError($
"Error while reading a position update for the entity \"({entity?.ToString() ?? "null
"})\". Message length exceeds the size of the buffer.");
2276 return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
2279 debugEntityList.Add(entity);
2282 if (entity is Item != header.IsItem)
2284 DebugConsole.AddWarning($
"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(header.IsItem ? "an item
" : "not an item
")}, client entity is {(entity?.GetType().ToString() ?? "null
")}). Ignoring the message...");
2286 else if (entity is MapEntity { Prefab.UintIdentifier: var uintIdentifier } me &&
2287 uintIdentifier != header.PrefabUintIdentifier)
2289 DebugConsole.AddWarning($
"Received a potentially invalid ENTITY_POSITION message."
2290 +$
"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == header.PrefabUintIdentifier)?.Identifier.Value ?? "[not found]
"}, "
2291 +$
"client entity is {me.Prefab.Identifier}). Ignoring the message...");
2304 ReadClientList(inc);
2308 if (!EntityEventManager.Read(segment, inc, sendingTime))
2317 throw new Exception($
"Unknown segment \"{segment}\"!)");
2322 exceptionHandler: (segment, prevSegments, ex) =>
2324 List<string> errorLines =
new List<string>
2329 $
"Segment with error: {segment}"
2331 if (prevSegments.Any())
2333 errorLines.Add(
"Prev segments: " +
string.Join(
", ", prevSegments));
2334 errorLines.Add(
" ");
2336 errorLines.Add(ex.StackTrace.CleanupStackTrace());
2337 errorLines.Add(
" ");
2338 if (prevSegments.Concat(segment.ToEnumerable()).Any(s => s.Identifier
2347 errorLines.Add(
" - NULL");
2350 Entity e = ent as Entity;
2351 errorLines.Add(
" - " + e.ToString());
2355 errorLines.Add(
"Last console messages:");
2356 for (
int i = DebugConsole.Messages.Count - 1; i > Math.Max(0, DebugConsole.Messages.Count - 20); i--)
2358 errorLines.Add(
"[" + DebugConsole.Messages[i].Time +
"] " + DebugConsole.Messages[i].Text);
2360 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.ReadInGameUpdate", GameAnalyticsManager.ErrorSeverity.Critical,
string.Join(
"\n", errorLines));
2362 throw new Exception(
2363 $
"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." +
2364 (prevSegments.Any() ? $
" Previous segments: {string.Join(",
", prevSegments)}" :
""),
2369 private void SendLobbyUpdate()
2377 outmsg.
WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID);
2382 var jobPreferences = GameMain.NetLobbyScreen.JobPreferences;
2383 if (jobPreferences.Count > 0)
2391 outmsg.
WriteByte((
byte)MultiplayerPreferences.Instance.TeamPreference);
2393 if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.LastSaveID == 0)
2401 foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
2403 outmsg.
WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag));
2405 outmsg.
WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
2408 chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
2411 if (outmsg.
LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
2416 chatMsgQueue[i].ClientWrite(segmentTable, outmsg);
2421 DebugConsole.ThrowError($
"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})");
2424 ClientPeer.Send(outmsg, DeliveryMethod.Unreliable);
2427 private void SendIngameUpdate()
2431 outmsg.
WriteBoolean(EntityEventManager.MidRoundSyncingDone);
2439 outmsg.
WriteUInt16(EntityEventManager.LastReceivedID);
2442 if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
2450 foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
2452 outmsg.
WriteUInt16(campaign.GetLastUpdateIdForFlag(flag));
2455 outmsg.
WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
2458 Character.Controlled?.ClientWriteInput(segmentTable, outmsg);
2459 GameMain.GameScreen.Cam?.ClientWrite(segmentTable, outmsg);
2461 EntityEventManager.Write(segmentTable, outmsg, ClientPeer?.ServerConnection);
2463 chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
2466 if (outmsg.
LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
2472 chatMsgQueue[i].ClientWrite(segmentTable, outmsg);
2478 DebugConsole.ThrowError($
"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})");
2481 ClientPeer.Send(outmsg, DeliveryMethod.Unreliable);
2486 if (ClientPeer?.ServerConnection ==
null) {
return; }
2487 lastQueueChatMsgID++;
2489 chatMsgQueue.Add(msg);
2494 if (ClientPeer?.ServerConnection ==
null) {
return; }
2497 GameStarted && myCharacter !=
null ? myCharacter.Name : Name,
2500 GameStarted && myCharacter !=
null ? myCharacter :
null);
2503 lastQueueChatMsgID++;
2506 chatMsgQueue.Add(chatMessage);
2511 WaitForNextRoundRespawn = waitForNextRoundRespawn;
2516 ClientPeer?.Send(msg, DeliveryMethod.Reliable);
2524 ClientPeer?.Send(msg, DeliveryMethod.Reliable);
2531 $
"Sending a campaign file request to the server." :
2532 $
"Sending a file request to the server (type: {fileType}, path: {file ?? "null"}");
2540 msg.
WriteString(file ??
throw new ArgumentNullException(nameof(file)));
2541 msg.
WriteString(fileHash ??
throw new ArgumentNullException(nameof(fileHash)));
2543 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2548 CancelFileTransfer(transfer.ID);
2553 if (!reliable && (DateTime.Now - transfer.LastOffsetAckTime).TotalSeconds < 1)
2557 transfer.RecordOffsetAckTime();
2565 ClientPeer.Send(msg, reliable ? DeliveryMethod.Reliable : DeliveryMethod.Unreliable);
2574 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2579 switch (transfer.FileType)
2584 if (newSub.IsFileCorrupted) {
return; }
2587 .Where(s => s.Name == newSub.Name && s.MD5Hash == newSub.MD5Hash)
2593 SubmarineInfo.AddToSavedSubs(newSub);
2595 for (
int i = 0; i < 2; i++)
2597 IEnumerable<GUIComponent> subListChildren = (i == 0) ?
2598 GameMain.NetLobbyScreen.ShuttleList.ListBox.Content.Children :
2599 GameMain.NetLobbyScreen.SubList.Content.Children;
2601 var subElement = subListChildren.FirstOrDefault(c =>
2602 ((SubmarineInfo)c.UserData).Name == newSub.Name &&
2603 ((SubmarineInfo)c.UserData).MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation);
2604 if (subElement ==
null) {
continue; }
2607 if (subElement.FindChild(
"nametext", recursive:
true) is GUITextBlock nameTextBlock)
2609 nameTextBlock.TextColor =
new Color(nameTextBlock.TextColor, 1.0f);
2611 if (subElement.FindChild(
"classtext", recursive:
true) is GUITextBlock classTextBlock)
2613 classTextBlock.Text = TextManager.Get($
"submarineclass.{newSub.SubmarineClass}");
2614 classTextBlock.TextColor =
new Color(classTextBlock.TextColor, 0.8f);
2616 if (subElement.FindChild(
"pricetext", recursive:
true) is GUITextBlock priceTextBlock)
2618 priceTextBlock.Text = TextManager.GetWithVariable(
"currencyformat",
"[credits]",
string.Format(CultureInfo.InvariantCulture,
"{0:N0}", newSub.Price));
2619 priceTextBlock.TextColor =
new Color(priceTextBlock.TextColor, 0.8f);
2622 subElement.UserData = newSub;
2623 subElement.ToolTip = newSub.Description;
2626 if (GameMain.NetLobbyScreen.FailedSelectedSub.HasValue &&
2627 GameMain.NetLobbyScreen.FailedSelectedSub.Value.Name == newSub.Name &&
2628 GameMain.NetLobbyScreen.FailedSelectedSub.Value.Hash == newSub.MD5Hash.StringRepresentation)
2630 GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation,
SelectedSubType.Sub, GameMain.NetLobbyScreen.SubList);
2633 if (GameMain.NetLobbyScreen.FailedSelectedShuttle.HasValue &&
2634 GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Name == newSub.Name &&
2635 GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Hash == newSub.MD5Hash.StringRepresentation)
2637 GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation,
SelectedSubType.Shuttle, GameMain.NetLobbyScreen.ShuttleList.ListBox);
2640 if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.PvP &&
2641 GameMain.NetLobbyScreen.FailedSelectedEnemySub.HasValue &&
2642 GameMain.NetLobbyScreen.FailedSelectedEnemySub.Value.Name == newSub.Name &&
2643 GameMain.NetLobbyScreen.FailedSelectedEnemySub.Value.Hash == newSub.MD5Hash.StringRepresentation)
2645 GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation,
SelectedSubType.EnemySub, GameMain.NetLobbyScreen.SubList);
2648 NetLobbyScreen.FailedSubInfo failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation);
2649 if (failedCampaignSub !=
default)
2651 GameMain.NetLobbyScreen.FailedCampaignSubs.Remove(failedCampaignSub);
2654 NetLobbyScreen.FailedSubInfo failedOwnedSub = GameMain.NetLobbyScreen.FailedOwnedSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation);
2655 if (failedOwnedSub !=
default)
2657 GameMain.NetLobbyScreen.FailedOwnedSubs.Remove(failedOwnedSub);
2661 SubmarineInfo existingServerSub = ServerSubmarines.Find(s =>
2662 s.Name == newSub.Name
2663 && s.MD5Hash == newSub.MD5Hash);
2664 if (existingServerSub !=
null)
2666 int existingIndex = ServerSubmarines.IndexOf(existingServerSub);
2667 ServerSubmarines[existingIndex] = newSub;
2668 existingServerSub.Dispose();
2673 XElement gameSessionDocRoot = SaveUtil.DecompressSaveAndLoadGameSessionDoc(transfer.FilePath)?.Root;
2674 byte campaignID = (byte)MathHelper.Clamp(gameSessionDocRoot.GetAttributeInt(
"campaignid", 0), 0, 255);
2675 if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.CampaignID != campaignID)
2677 string savePath = transfer.FilePath;
2678 GameMain.GameSession =
new GameSession(
null, Option.None, CampaignDataPath.CreateRegular(savePath), GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty);
2679 campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode;
2680 campaign.CampaignID = campaignID;
2681 GameMain.NetLobbyScreen.ToggleCampaignMode(
true);
2684 GameMain.GameSession.DataPath = CampaignDataPath.CreateRegular(transfer.FilePath);
2685 if (GameMain.GameSession.SubmarineInfo ==
null || campaign.Map ==
null)
2687 string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDocRoot.GetAttributeString(
"submarine",
"")) +
".sub";
2688 GameMain.GameSession.SubmarineInfo =
new SubmarineInfo(subPath,
"");
2691 campaign.LoadState(GameMain.GameSession.DataPath.LoadPath);
2692 GameMain.GameSession?.SubmarineInfo?.Reload();
2693 GameMain.GameSession?.SubmarineInfo?.CheckSubsLeftBehind();
2695 if (GameMain.GameSession?.SubmarineInfo?.Name !=
null)
2697 GameMain.NetLobbyScreen.TryDisplayCampaignSubmarine(GameMain.GameSession.SubmarineInfo);
2699 campaign.LastSaveID = campaign.PendingSaveID;
2701 if (Screen.Selected == GameMain.NetLobbyScreen)
2704 GameMain.NetLobbyScreen.SaveAppearance();
2705 GameMain.NetLobbyScreen.Select();
2708 DebugConsole.Log(
"Campaign save received (" + GameMain.GameSession.DataPath +
"), save ID " + campaign.LastSaveID);
2711 foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
2713 campaign.SetLastUpdateIdForFlag(flag, (ushort)(campaign.GetLastUpdateIdForFlag(flag) - 1));
2717 if (!(Screen.Selected is ModDownloadScreen)) {
return; }
2719 GameMain.ModDownloadScreen.CurrentDownloadFinished(transfer);
2734 CreateEntityEvent(entity, extraData, requireControlledCharacter:
true);
2741 throw new InvalidCastException($
"Entity is not {nameof(IClientSerializable)}");
2743 EntityEventManager.CreateEvent(clientSerializable, extraData, requireControlledCharacter);
2748 return permissions.HasFlag(permission);
2755 if (permittedConsoleCommands.Contains(commandName)) {
return true; }
2758 foreach (DebugConsole.Command command in DebugConsole.Commands)
2760 if (command.Names.Contains(commandName))
2762 if (command.Names.Intersect(permittedConsoleCommands).Any()) {
return true; }
2774 ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
2783 WriteCharacterInfo(msg, newName);
2784 ClientPeer?.Send(msg, DeliveryMethod.Reliable);
2791 if (characterInfo ==
null) {
return; }
2793 var head = characterInfo.Head;
2795 var netInfo =
new NetCharacterInfo(
2796 NewName: newName ??
string.Empty,
2797 Tags: head.Preset.TagSet.ToImmutableArray(),
2798 HairIndex: (
byte)head.HairIndex,
2799 BeardIndex: (
byte)head.BeardIndex,
2800 MoustacheIndex: (
byte)head.MoustacheIndex,
2801 FaceAttachmentIndex: (
byte)head.FaceAttachmentIndex,
2802 SkinColor: head.SkinColor,
2803 HairColor: head.HairColor,
2804 FacialHairColor: head.FacialHairColor,
2807 msg.WriteNetSerializableStruct(netInfo);
2810 public void Vote(VoteType voteType,
object data)
2812 if (ClientPeer ==
null) {
return; }
2822 throw new Exception(
2823 $
"Failed to write vote of type {voteType}: " +
2824 $
"data was of invalid type {data?.GetType().Name ?? "NULL
"}");
2828 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2833 if (votedClient ==
null) {
return; }
2834 Vote(VoteType.Kick, votedClient);
2837 #region Submarine Change Voting
2840 if (sub ==
null) {
return; }
2841 Vote(voteType, (sub, transferItems));
2846 if (info ==
null) {
return; }
2847 if (votingInterface !=
null && votingInterface.VoteRunning) {
return; }
2848 votingInterface?.Remove();
2853 #region Money Transfer Voting
2856 if (votingInterface !=
null && votingInterface.VoteRunning) {
return; }
2857 if (from ==
null && to ==
null)
2859 DebugConsole.ThrowError(
"Tried to initiate a vote for transferring from null to null!");
2862 votingInterface?.Remove();
2870 if (should !=
null && should.Value) {
return; }
2872 if (
string.IsNullOrEmpty(message.
Text)) {
return; }
2875 if (message.Text.IsNullOrEmpty())
2877 sender.ShowTextlessSpeechBubble(2.0f, message.Color);
2881 sender.ShowSpeechBubble(message.Color, message.Text);
2884 sender.TextChatVolume = 1f;
2888 GameMain.NetLobbyScreen.NewChatMessage(message);
2889 chatBox.AddMessage(message);
2892 public override void KickPlayer(
string kickedName,
string reason)
2900 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2903 public override void BanPlayer(
string kickedName,
string reason, TimeSpan? duration =
null)
2910 msg.
WriteDouble(duration.HasValue ? duration.Value.TotalSeconds : 0.0);
2912 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2922 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2932 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2941 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2948 DebugConsole.ThrowError(
"Failed send campaign state to the server (no campaign active).\n" + Environment.StackTrace.CleanupStackTrace());
2954 campaign.ClientWrite(msg);
2955 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2960 if (
string.IsNullOrWhiteSpace(command))
2962 DebugConsole.ThrowError(
"Cannot send an empty console command to the server!\n" + Environment.StackTrace.CleanupStackTrace());
2974 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2988 ClientPeer.Send(msg, DeliveryMethod.Reliable);
3003 ClientPeer.Send(msg, DeliveryMethod.Reliable);
3013 DebugConsole.ThrowError(
"Gamemode index out of bounds (" + modeIndex +
")\n" + Environment.StackTrace.CleanupStackTrace());
3022 ClientPeer.Send(msg, DeliveryMethod.Reliable);
3030 saveName = Path.GetFileNameWithoutExtension(saveName);
3040 msg.WriteNetSerializableStruct(settings);
3042 ClientPeer.Send(msg, DeliveryMethod.Reliable);
3047 if (ClientPeer ==
null) {
return; }
3058 if (backupIndex.TryUnwrap(out uint index))
3070 ClientPeer.Send(msg, DeliveryMethod.Reliable);
3085 ClientPeer.Send(msg, DeliveryMethod.Reliable);
3097 new GUIMessageBox(
"", TextManager.Get(
"campaignfiletransferinprogress"));
3100 if (button !=
null) { button.
Enabled =
false; }
3101 if (campaign !=
null) { LateCampaignJoin =
true; }
3103 if (ClientPeer ==
null) {
return false; }
3112 WriteCharacterInfo(readyToStartMsg);
3114 ClientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable);
3132 if (!GameStarted)
return false;
3149 get {
return characterInfo; }
3150 set { characterInfo = value; }
3155 get {
return myCharacter; }
3156 set { myCharacter = value; }
3163 private bool hasPermissionToUseLogButton;
3168 UpdateLogButtonVisibility();
3171 private void UpdateLogButtonVisibility()
3173 if (ShowLogButton !=
null)
3177 ShowLogButton.
Visible = hasPermissionToUseLogButton;
3182 ShowLogButton.
Visible = hasPermissionToUseLogButton && (campaign ==
null || !campaign.ShowCampaignUI);
3189 get {
return inGameHUD; }
3194 get {
return chatBox; }
3199 get {
return votingInterface; }
3213 if (
string.IsNullOrWhiteSpace(message))
3219 SendChatMessage(message, type: messageType);
3238 if (GUI.DisableHUD || GUI.DisableUpperHUD)
return;
3266 UpdateLogButtonVisibility();
3272 buttonContainer.Visible = !disableButtons;
3274 if (!GUI.DisableHUD && !GUI.DisableUpperHUD)
3277 chatBox.
Update(deltaTime);
3279 if (votingInterface !=
null)
3281 votingInterface.
Update(deltaTime);
3286 DebugConsole.AddWarning($
"Voting interface timed out.");
3288 votingInterface.
Remove();
3289 votingInterface =
null;
3306 if (GUI.KeyboardDispatcher.Subscriber ==
null)
3309 if (chatKeyStates.AnyHit)
3329 public void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
3331 if (GUI.DisableHUD || GUI.DisableUpperHUD)
return;
3339 ToolBox.LimitString(
3340 TextManager.GetWithVariable(
"DownloadingFile",
"[filename]", transfer.FileName).Value,
3345 MathUtils.GetBytesReadable((
long)transfer.Received) +
" / " + MathUtils.GetBytesReadable((
long)transfer.FileSize);
3358 if (endVoteCount > 0)
3362 EndVoteTickBox.
Text = $
"{endRoundVoteText} {endVoteCount}/{endVoteMax}";
3366 LocalizedString endVoteText = TextManager.GetWithVariables(
"EndRoundVotes", (
"[votes]", endVoteCount.ToString()), (
"[max]", endVoteMax.ToString()));
3367 GUI.DrawString(spriteBatch, EndVoteTickBox.
Rect.Center.ToVector2() - GUIStyle.SmallFont.MeasureString(endVoteText) / 2,
3370 font: GUIStyle.SmallFont);
3375 EndVoteTickBox.
Text = endRoundVoteText;
3381 Color textColor = Color.White;
3382 bool hideRespawnButtons =
false;
3384 if (EndRoundTimeRemaining > 0)
3386 respawnText = TextManager.GetWithVariable(
"endinground",
"[time]", ToolBox.SecondsToReadableTime(EndRoundTimeRemaining))
3387 .
Fallback(ToolBox.SecondsToReadableTime(EndRoundTimeRemaining), useDefaultLanguageIfFound:
false);
3394 respawnText = TextManager.GetWithVariable(
"RespawningIn",
"[time]", ToolBox.SecondsToReadableTime(timeLeft));
3398 respawnText = TextManager.GetWithVariables(
"RespawnWaitingForMoreDeadPlayers",
3407 respawnText = timeLeft <= 0.0f ?
3409 TextManager.GetWithVariable(
"RespawnShuttleLeavingIn",
"[time]", ToolBox.SecondsToReadableTime(timeLeft));
3410 if (timeLeft < 20.0f)
3413 float phase = (float)(Math.Sin(timeLeft * MathHelper.Pi) + 1.0f) * 0.5f;
3415 textColor = Color.Lerp(GUIStyle.Red, Color.White, 1.0f - phase);
3417 hideRespawnButtons =
true;
3421 text: respawnText.
Value, textColor: textColor,
3422 waitForNextRoundRespawn: (WaitForNextRoundRespawn ??
true), hideButtons: hideRespawnButtons);
3425 if (!ShowNetStats) {
return; }
3456 if (character ==
null) {
return false; }
3458 if (character != myCharacter)
3460 var client = previouslyConnectedClients.Find(c => c.
Character == character);
3461 if (client ==
null) {
return false; }
3463 CreateSelectionRelatedButtons(client, frame);
3471 if (client ==
null || client.
SessionId == SessionId) {
return false; }
3472 CreateSelectionRelatedButtons(client, frame);
3481 TextManager.Get(
"Mute"))
3484 OnSelected = (tickBox) => { client.
MutedLocally = tickBox.Selected;
return true; }
3487 var volumeLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 0.45f), content.RectTransform, Anchor.TopCenter), isHorizontal:
false);
3489 var volumeTextLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(1f, 0.5f), volumeLayout.RectTransform), isHorizontal:
true, childAnchor: Anchor.CenterLeft);
3490 var label =
new GUITextBlock(
new RectTransform(
new Vector2(0.6f, 1f), volumeTextLayout.RectTransform), TextManager.Get(
"VoiceChatVolume"));
3491 var percentageText =
new GUITextBlock(
new RectTransform(
new Vector2(0.4f, 1f), volumeTextLayout.RectTransform), ToolBox.GetFormattedPercentage(client.
VoiceVolume), textAlignment: Alignment.Right);
3493 var volumeSlider =
new GUIScrollBar(
new RectTransform(
new Vector2(1f, 0.5f), volumeLayout.RectTransform), barSize: 0.1f, style:
"GUISlider")
3495 Range =
new Vector2(0f, 1f),
3497 OnMoved = (_, barScroll) =>
3502 percentageText.Text = ToolBox.GetFormattedPercentage(newVolume);
3507 var buttonContainer =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 0.35f), content.RectTransform), isHorizontal:
true, childAnchor:
Anchor.BottomLeft)
3509 RelativeSpacing = 0.05f,
3513 if (!GameMain.Client.GameStarted || (GameMain.Client.Character ==
null || GameMain.Client.Character.IsDead) && (client.
Character ==
null || client.
Character.
IsDead))
3515 var messageButton =
new GUIButton(
new RectTransform(
new Vector2(1f, 0.2f), content.RectTransform,
Anchor.BottomCenter) { RelativeOffset = new Vector2(0f, buttonContainer.RectTransform.RelativeSize.Y) },
3516 TextManager.Get(
"message"), style:
"GUIButtonSmall")
3519 OnClicked = (btn, userdata) =>
3522 CoroutineManager.StartCoroutine(selectCoroutine());
3529 IEnumerable<CoroutineStatus> selectCoroutine()
3531 yield
return new WaitForSeconds(0.01f,
true);
3537 var banButton =
new GUIButton(
new RectTransform(
new Vector2(0.45f, 0.9f), buttonContainer.RectTransform),
3538 TextManager.Get(
"Ban"), style:
"GUIButtonSmall")
3541 OnClicked = (btn, userdata) => { NetLobbyScreen.BanPlayer(client);
return false; }
3546 var kickButton =
new GUIButton(
new RectTransform(
new Vector2(0.45f, 0.9f), buttonContainer.RectTransform),
3547 TextManager.Get(
"Kick"), style:
"GUIButtonSmall")
3550 OnClicked = (btn, userdata) => { NetLobbyScreen.KickPlayer(client);
return false; }
3555 var kickVoteButton =
new GUIButton(
new RectTransform(
new Vector2(0.45f, 0.9f), buttonContainer.RectTransform),
3556 TextManager.Get(
"VoteToKick"), style:
"GUIButtonSmall")
3559 OnClicked = (btn, userdata) => { VoteForKick(client); btn.Enabled =
false;
return true; }
3567 TextManager.Get(ban ?
"BanReasonPrompt" :
"KickReasonPrompt"),
3568 "",
new LocalizedString[] { TextManager.Get(
"OK"), TextManager.Get(
"Cancel") },
new Vector2(0.25f, 0.25f),
new Point(400, 260));
3572 AbsoluteSpacing = GUI.IntScale(5)
3580 GUINumberInput durationInputDays =
null, durationInputHours =
null;
3586 new GUITextBlock(
new RectTransform(
new Vector2(1f, 0.0f), labelContainer.RectTransform), TextManager.Get(
"BanDuration"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero };
3587 var buttonContent =
new GUILayoutGroup(
new RectTransform(
new Vector2(1f, 0.5f), labelContainer.RectTransform), isHorizontal:
true);
3588 permaBanTickBox =
new GUITickBox(
new RectTransform(
new Vector2(0.4f, 0.15f), buttonContent.RectTransform), TextManager.Get(
"BanPermanent"))
3593 var durationContainer =
new GUILayoutGroup(
new RectTransform(
new Vector2(0.8f, 1f), buttonContent.RectTransform), isHorizontal:
true)
3600 durationContainer.Visible = !tickBox.Selected;
3607 MaxValueFloat = 1000
3615 new GUITextBlock(
new RectTransform(
new Vector2(0.2f, 1.0f), durationContainer.RectTransform), TextManager.Get(
"Hours"));
3618 banReasonPrompt.Buttons[0].OnClicked += (btn, userData) =>
3624 TimeSpan banDuration =
new TimeSpan(durationInputDays.
IntValue, durationInputHours.IntValue, 0, 0);
3625 BanPlayer(clientName, banReasonBox.Text, banDuration);
3629 BanPlayer(clientName, banReasonBox.Text);
3634 KickPlayer(clientName, banReasonBox.Text);
3638 banReasonPrompt.Buttons[0].OnClicked += banReasonPrompt.Close;
3639 banReasonPrompt.Buttons[1].OnClicked += banReasonPrompt.Close;
3663 ClientPeer.Send(outMsg, DeliveryMethod.Reliable);
3665 WriteEventErrorData(error, expectedId, eventId, entityId);
3668 private bool eventErrorWritten;
3669 private void WriteEventErrorData(
ClientNetError error, UInt16 expectedID, UInt16 eventID, UInt16 entityID)
3671 if (eventErrorWritten) {
return; }
3672 List<string> errorLines =
new List<string>
3674 error.ToString(),
""
3679 errorLines.Add(
"SERVER OWNER");
3684 errorLines.Add(
"Expected ID: " + expectedID +
", received " + eventID);
3688 errorLines.Add(
"Event ID: " + eventID +
", entity ID " + entityID);
3691 if (GameMain.GameSession?.GameMode !=
null)
3693 errorLines.Add(
"Game mode: " + GameMain.GameSession.GameMode.Name.Value);
3694 if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
3696 errorLines.Add(
"Campaign ID: " + campaign.CampaignID);
3697 errorLines.Add(
"Campaign save ID: " + campaign.LastSaveID +
"(pending: " + campaign.PendingSaveID +
")");
3699 foreach (Mission mission
in GameMain.GameSession.Missions)
3701 errorLines.Add(
"Mission: " + mission.Prefab.Identifier);
3704 if (GameMain.GameSession?.Submarine !=
null)
3706 errorLines.Add(
"Submarine: " + GameMain.GameSession.Submarine.Info.Name);
3708 if (GameMain.NetworkMember?.RespawnManager is { } respawnManager)
3710 errorLines.Add(
"Respawn shuttles: " +
string.Join(
", ", respawnManager.RespawnShuttles.Select(s => s.Info.Name)));
3712 if (Level.Loaded !=
null)
3714 errorLines.Add(
"Level: " + Level.Loaded.Seed +
", "
3715 +
string.Join(
"; ", Level.Loaded.EqualityCheckValues.Select(cv
3716 => cv.Key +
"=" + cv.Value.ToString(
"X"))));
3717 errorLines.Add(
"Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate);
3718 errorLines.Add(
"Entities:");
3719 foreach (Entity e
in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex))
3721 errorLines.Add(e.ErrorLine);
3723 errorLines.Add(
"Entity count after generating level: " + Level.Loaded.EntityCountAfterGenerate);
3726 errorLines.Add(
"Entity IDs:");
3727 Entity[] sortedEntities = Entity.GetEntities().OrderBy(e => e.CreationIndex).ToArray();
3728 foreach (Entity e
in sortedEntities)
3730 errorLines.Add(e.ErrorLine);
3733 if (Entity.Spawner !=
null)
3736 errorLines.Add(
"EntitySpawner events:");
3737 foreach ((Entity entity,
bool isRemoval) in Entity.Spawner.receivedEvents)
3740 (isRemoval ?
"Remove " :
"Create ") +
3742 " (" + entity.ID +
")");
3747 errorLines.Add(
"Last debug messages:");
3748 for (
int i = DebugConsole.Messages.Count - 1; i > 0 && i > DebugConsole.Messages.Count - 15; i--)
3750 errorLines.Add(
" " + DebugConsole.Messages[i].Time +
" - " + DebugConsole.Messages[i].Text);
3753 string filePath = $
"event_error_log_client_{Name}_{DateTime.UtcNow.ToShortTimeString()}.log";
3754 filePath = Path.Combine(
ServerLog.
SavePath, ToolBox.RemoveInvalidFileNameChars(filePath));
3760 File.WriteAllLines(filePath, errorLines);
3762 eventErrorWritten =
true;
3765 private static void AppendExceptionInfo(ref
string errorMsg, Exception e)
3767 if (!errorMsg.EndsWith(
"\n")) { errorMsg +=
"\n"; }
3768 errorMsg += e.Message +
"\n";
3769 var innermostException = e.GetInnermost();
3770 if (innermostException != e)
3774 errorMsg +=
"Inner exception: " + innermostException.Message +
"\n" + innermostException.StackTrace.CleanupStackTrace();
3778 errorMsg += e.StackTrace.CleanupStackTrace();
3783 public void ForceTimeOut()
3785 ClientPeer?.ForceTimeOut();
Vector2 ScreenToWorld(Vector2 coords)
MedicalClinic MedicalClinic
SubmarineInfo PendingSubmarineSwitch
readonly CharacterParams Params
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
static readonly List< Character > CharacterList
static Character? Controlled
Item SelectedSecondaryItem
The secondary selected item. It's an item other than a device (see SelectedItem), e....
Stores information about the Character that is needed between rounds in the menu etc....
ushort ID
Unique ID given to character infos in MP. Non-persistent. Used by clients to identify which infos are...
static readonly Identifier HumanSpeciesName
void ApplySelectionInputs()
GUITextBox.OnEnterHandler OnEnterMessage
void Update(float deltaTime)
bool CloseAfterMessageSent
readonly ChatManager ChatManager
bool TypingChatMessage(GUITextBox textBox, string text)
void Store(string message)
static CoroutineStatus Running
static CoroutineStatus Success
void SetClientSpeaking(Client client)
virtual Vector2 WorldPosition
virtual void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
void UpdateManually(float deltaTime, bool alsoChildren=false, bool recursive=true)
By default, all the gui elements are updated automatically in the same order they appear on the updat...
virtual void DrawManually(SpriteBatch spriteBatch, bool alsoChildren=false, bool recursive=true)
By default, all the gui elements are drawn automatically in the same order they appear on the update ...
RectTransform RectTransform
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
static readonly List< GUIComponent > MessageBoxes
List< GUIButton > Buttons
bool AutoScaleHorizontal
When enabled, the text is automatically scaled down to fit the textblock horizontally.
OnTextChangedHandler OnTextChanged
Don't set the Text property on delegates that register to this event, because modifying the Text will...
void Select(int forcedCaretIndex=-1, bool ignoreSelectSound=false)
bool DeselectAfterMessage
OnSelectedHandler OnSelected
static GameSession?? GameSession
static NetLobbyScreen NetLobbyScreen
static Lights.LightManager LightManager
static ChatMode ActiveChatMode
static GameScreen GameScreen
static MainMenuScreen MainMenuScreen
static ServerListScreen ServerListScreen
static void ResetNetLobbyScreen()
CharacterTeamType? WinningTeam
void SetRespawnInfo(string text, Color textColor, bool waitForNextRoundRespawn, bool hideButtons=false)
This method controls the content and visibility logic of the respawn-related GUI elements at the top ...
void EndRound(string endMessage, CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults=null)
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
object Call(string name, params object[] args)
void NetMessageReceived(IReadMessage netMessage, ServerPacketHeader header, Client client=null)
LuaCsNetworking Networking
readonly string StringRepresentation
void ClientReadMoney(IReadMessage inc)
void ClientReadCrew(IReadMessage msg)
UInt16 GetLastUpdateIdForFlag(NetFlags flag)
const int MaxMessagesPerPacket
static void ClientRead(IReadMessage msg)
static Color[] MessageColor
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
void ClearSelf()
Clears events generated by the current client, used when resynchronizing with the server after a time...
const float MaxVoiceChatBoost
void WritePermissions(IWriteMessage msg)
static void ReadPermissions(IReadMessage inc, out ClientPermissions permissions, out List< DebugConsole.Command > permittedCommands)
void SetPermissions(ClientPermissions permissions, IEnumerable< Identifier > permittedConsoleCommands)
readonly byte SessionId
An ID for this client for the current session. THIS IS NOT A PERSISTENT VALUE. DO NOT STORE THIS LONG...
abstract string StringRepresentation
TransferInDelegate OnTransferFailed
void ReadMessage(IReadMessage inc)
IReadOnlyList< FileTransferIn > ActiveTransfers
void StopTransfer(FileTransferIn transfer, bool deleteFile=false)
TransferInDelegate OnFinished
void VoteForKick(Client votedClient)
void Vote(VoteType voteType, object data)
void Update(float deltaTime)
void RequestFile(FileTransferType fileType, string file, string fileHash)
readonly ClientEntityEventManager EntityEventManager
void UpdateHUD(float deltaTime)
void CancelFileTransfer(int id)
readonly NamedEvent< PermissionChangedEvent > OnPermissionChanged
bool TypingChatMessage(GUITextBox textBox, string text)
override IReadOnlyList< Client > ConnectedClients
override void BanPlayer(string kickedName, string reason, TimeSpan? duration=null)
GameClient(string newName, ImmutableArray< Endpoint > endpoints, string serverName, Option< int > ownerKey)
void SetName(string value)
GUITickBox FollowSubTickBox
static readonly TimeSpan LevelTransitionTimeOut
void CreateServerCrashMessage()
UInt16 LastSentEntityEventID
IEnumerable< Client > PreviouslyConnectedClients
CharacterInfo characterInfo
bool ToggleEndRoundVote(GUITickBox tickBox)
static void SetLobbyPublic(bool isPublic)
TraitorEventPrefab TraitorMission
IEnumerable< CoroutineStatus > EndGame(string endMessage, CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults=null)
readonly NetStats NetStats
void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData=null)
void UpdateClientPermissions(Client targetClient)
LocalizedString endRoundVoteText
bool HasPermission(ClientPermissions permission)
bool HasConsoleCommandPermission(Identifier commandName)
bool? WaitForNextRoundRespawn
readonly FileReceiver FileReceiver
float EndRoundTimeRemaining
CharacterInfo CharacterInfo
GUITickBox cameraFollowsSub
void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData, bool requireControlledCharacter)
GUITickBox EndVoteTickBox
CameraTransition EndCinematic
void SendTakeOverBotRequest(CharacterInfo bot)
void RequestSelectMode(int modeIndex)
Tell the server to select a mode (permission required)
void SendConsoleCommand(string command)
void SendCharacterInfo(string newName=null)
void UpdateLogButtonPermissions()
readonly List< SubmarineInfo > ServerSubmarines
void AddToGUIUpdateList()
override void UnbanPlayer(string playerName)
LocalizedString TraitorFirstObjective
void RequestSelectSub(SubmarineInfo sub, SelectedSubType type)
Tell the server to select a submarine (permission required)
void ForceNameJobTeamUpdate()
void SendChatMessage(string message, ChatMessageType type=ChatMessageType.Default)
bool JoinOnGoingClicked(GUIButton button, object _)
void CreateKickReasonPrompt(string clientName, bool ban)
static readonly TimeSpan CampaignSaveTransferTimeOut
void SetupNewCampaign(SubmarineInfo sub, string saveName, string mapSeed, CampaignSettings settings)
bool SelectCrewCharacter(Character character, GUIComponent frame)
void SendRespawnPromptResponse(bool waitForNextRoundRespawn)
bool EnterChatMessage(GUITextBox textBox, string message)
bool SetReadyToStart(GUITickBox tickBox)
bool IsFollowSubTickBoxVisible
bool SelectCrewClient(Client client, GUIComponent frame)
void InitiateSubmarineChange(SubmarineInfo sub, bool transferItems, VoteType voteType)
override void AddChatMessage(ChatMessage message)
void RequestRoundEnd(bool save, bool quitCampaign=false)
Tell the server to end the round (permission required)
void CancelFileTransfer(FileReceiver.FileTransferIn transfer)
void SendChatMessage(ChatMessage msg)
void SetupLoadCampaign(string filePath, Option< uint > backupIndex)
void RequestStartRound(bool continueCampaign=false)
Tell the server to start the round (permission required)
GameClient(string newName, Endpoint endpoint, string serverName, Option< int > ownerKey)
override void UnbanPlayer(Endpoint endpoint)
void WriteCharacterInfo(IWriteMessage msg, string newName=null)
override void KickPlayer(string kickedName, string reason)
void UpdateFileTransfer(FileReceiver.FileTransferIn transfer, int expecting, int lastSeen, bool reliable=false)
void ShowMoneyTransferVoteInterface(Client starter, Client from, int amount, Client to, float timeOut)
void ShowSubmarineChangeVoteInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float timeOut)
void ReportError(ClientNetError error, UInt16 expectedId=0, UInt16 eventId=0, UInt16 entityId=0)
ServerInfo CreateServerInfoFromSettings()
void Update(float deltaTime)
void Draw(SpriteBatch spriteBatch, Rectangle rect)
void Update(float deltaTime)
bool RespawnCountdownStarted
bool ReturnCountdownStarted
static ServerInfo FromServerEndpoints(ImmutableArray< Endpoint > endpoints, ServerSettings serverSettings)
bool AllowImmediateItemDelivery
void AddToGUIUpdateList()
bool ServerDetailsChanged
Have some of the properties listed in the server list changed
int MaximumMoneyTransferRequest
bool AllowLinkingWifiToChat
bool AllowDragAndDropGive
bool ReadMonsterEnabled(IReadMessage inc)
EnemyHealthBarMode ShowEnemyHealthBars
DateTime LastEnqueueAudio
static VoipCapture Instance
void Read(IReadMessage msg)
void UpdateOrAddServerInfo(ServerInfo serverInfo)
static readonly Submarine[] MainSubs
override Vector2? WorldPosition
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
static List< Submarine > Loaded
void AddTag(SubmarineTag tag)
static IEnumerable< SubmarineInfo > SavedSubmarines
int GetVoteCountMax(VoteType voteType)
void ResetVotes(IEnumerable< Client > connectedClients)
bool ClientWrite(IWriteMessage msg, VoteType voteType, object data)
Returns true if the given data is valid for the given vote type, returns false otherwise....
int GetVoteCountYes(VoteType voteType)
void Update(float deltaTime)
static VotingInterface CreateMoneyTransferVotingInterface(Client starter, Client from, Client to, int amount, float votingTime)
static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float votingTime)
Interface for entities that the clients can send events to the server
int ReadRangedInteger(int min, int max)
UInt32 ReadVariableUInt32()
Single ReadRangedSingle(Single min, Single max, int bitCount)
byte[] ReadBytes(int numberOfBytes)
Identifier ReadIdentifier()
Interface for entities that handle ServerNetObject.ENTITY_POSITION
void ClientReadPosition(IReadMessage msg, float sendingTime)
Interface for entities that the server can send events to the clients
void WriteString(string val)
void WriteBoolean(bool val)
void WriteUInt16(UInt16 val)
void WriteDouble(Double val)
void WriteIdentifier(Identifier val)
void WriteInt32(Int32 val)
void WriteSingle(Single val)
void WriteUInt32(UInt32 val)
@ Character
Characters only
static ChatKeyStates GetChatKeyStates()
GUITextBlock FileTransferProgressText
List< JobVariant >? JobPreferences
static bool TeamChatSelected
GUIFrame CampaignSetupFrame
GUIComponent FileTransferFrame
void SetPlayerSpeaking(Client client)
GameModePreset SelectedMode
GUIProgressBar FileTransferProgressBar
GUITextBlock FileTransferTitle
void RemovePlayer(Client client)
CharacterTeamType PreferredTeam
static void Read(IReadMessage msg, SegmentDataReader segmentDataReader, ExceptionHandler? exceptionHandler=null)
static SegmentTableWriter< T > StartWriting(IWriteMessage msg)