5 using Microsoft.Xna.Framework;
6 using Microsoft.Xna.Framework.Input;
8 using System.Collections.Generic;
9 using System.Collections.Immutable;
11 using System.Threading.Tasks;
12 using System.Xml.Linq;
26 public float DebugServerVoipAmplitude;
31 private UInt16 nameId = 0;
33 public string Name {
get;
private set; }
39 if (
string.IsNullOrEmpty(value)) {
return; }
72 private List<Identifier> permittedConsoleCommands =
new List<Identifier>();
74 private bool connected;
76 private enum RoundInitStatus
80 WaitingForStartGameFinalize,
86 private UInt16? debugStartGameCampaignSaveID;
88 private RoundInitStatus roundInitStatus = RoundInitStatus.NotStarted;
90 public bool RoundStarting => roundInitStatus == RoundInitStatus.Starting || roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize;
92 private readonly List<Client> otherClients;
98 private bool canStart;
100 private UInt16 lastSentChatMsgID = 0;
101 private UInt16 lastQueueChatMsgID = 0;
102 private readonly List<ChatMessage> chatMsgQueue =
new List<ChatMessage>();
107 public void PrintReceiverTransters()
111 DebugConsole.NewMessage(transfer.FileName +
" " + transfer.Progress.ToString());
145 if (selfClient is
null || selfClient.
Ping == 0) {
return Option<int>.None(); }
146 return Option<int>.Some(selfClient.
Ping);
150 private readonly List<Client> previouslyConnectedClients =
new List<Client>();
153 get {
return previouslyConnectedClients; }
171 private readonly ImmutableArray<Endpoint> serverEndpoints;
172 private readonly Option<int> ownerKey;
176 internal readonly
struct PermissionChangedEvent
179 public readonly ImmutableArray<Identifier> NewPermittedConsoleCommands;
181 public PermissionChangedEvent(
ClientPermissions newPermissions, IReadOnlyList<Identifier> newPermittedConsoleCommands)
183 NewPermissions = newPermissions;
184 NewPermittedConsoleCommands = newPermittedConsoleCommands.ToImmutableArray();
188 public readonly NamedEvent<PermissionChangedEvent>
OnPermissionChanged =
new NamedEvent<PermissionChangedEvent>();
191 : this(newName, endpoint.ToEnumerable().ToImmutableArray(), serverName, ownerKey) { }
193 public GameClient(
string newName, ImmutableArray<Endpoint> endpoints,
string serverName, Option<int> ownerKey)
196 this.ownerKey = ownerKey;
198 roundInitStatus = RoundInitStatus.NotStarted;
212 isHorizontal:
true, childAnchor:
Anchor.CenterRight)
228 TextManager.Get(
"ServerLog"))
230 OnClicked = (
GUIButton button,
object userData) =>
239 GUI.KeyboardDispatcher.Subscriber =
null;
248 MinSize = new Point(150, 0)
249 }, TextManager.Get(
"CamFollowSubmarine"))
252 OnSelected = (tbox) =>
260 Hull.EditFire =
false;
276 otherClients =
new List<Client>();
281 serverEndpoints = endpoints;
282 InitiateServerJoin();
297 private void InitiateServerJoin()
299 LastClientListUpdateID = 0;
306 otherClients.Clear();
317 ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
321 CoroutineManager.StartCoroutine(WaitForStartingInfo(),
"WaitForStartingInfo");
326 SteamManager.SetLobbyPublic(isPublic);
331 Networking.ClientPeer.Callbacks callbacks =
new ClientPeer.Callbacks(
333 OnClientPeerDisconnect,
334 OnConnectionInitializationComplete);
335 return serverEndpoints.First()
switch
338 =>
new LidgrenClientPeer(lidgrenEndpoint, callbacks, ownerKey),
342 =>
new P2PClientPeer(serverEndpoints.Cast<
P2PEndpoint>().ToImmutableArray(), callbacks),
343 _ =>
throw new ArgumentOutOfRangeException()
350 var basicServerCrashMsg = TextManager.Get($
"{nameof(DisconnectReason)}.{nameof(DisconnectReason.ServerCrashed)}");
353 .Where(mb => mb.Text?.Text == basicServerCrashMsg)
355 .ForEach(mb => mb.Close());
359 mb => (mb as
GUIMessageBox)?.Text?.Text != ChildServerRelay.CrashMessage))
361 var msgBox =
new GUIMessageBox(TextManager.Get(
"ConnectionLost"), ChildServerRelay.CrashMessage);
362 msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu;
366 private bool ReturnToPreviousMenu(
GUIButton button,
object obj)
380 GUIMessageBox.MessageBoxes.Clear();
385 private bool connectCancelled;
386 private void CancelConnect()
392 private IEnumerable<CoroutineStatus> WaitForStartingInfo()
394 GUI.SetCursorWaiting();
396 connectCancelled =
false;
400 DateTime timeOut = DateTime.Now +
new TimeSpan(0, 0, 200);
403 LocalizedString connectingText = TextManager.Get(
"Connecting");
404 while (!canStart && !connectCancelled)
406 if (reconnectBox ==
null && waitInServerQueueBox ==
null)
411 if (SteamManager.IsInitialized && steamConnection.AccountInfo.AccountId.TryUnwrap(out var accountId) && accountId is SteamId steamId)
413 serverDisplayName = steamId.ToString();
414 string steamUserName =
new Steamworks.Friend(steamId.Value).Name;
415 if (!
string.IsNullOrEmpty(steamUserName) && steamUserName !=
"[unknown]")
417 serverDisplayName = steamUserName;
421 if (
string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = TextManager.Get(
"Unknown").Value; }
425 TextManager.GetWithVariable(
"ConnectingTo",
"[serverip]", serverDisplayName));
428 if (reconnectBox !=
null)
430 reconnectBox.
Header.
Text = connectingText +
new string(
'.', ((
int)Timing.TotalTime % 3 + 1));
433 yield
return CoroutineStatus.Running;
435 if (DateTime.Now > timeOut)
437 ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
438 var msgBox =
new GUIMessageBox(TextManager.Get(
"ConnectionFailed"), TextManager.Get(
"CouldNotConnectToServer"))
440 DisplayInLoadingScreens =
true
442 msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu;
447 if (
ClientPeer.WaitingForPassword && !canStart && !connectCancelled)
449 GUI.ClearCursorWait();
454 yield
return CoroutineStatus.Running;
461 GUI.ClearCursorWait();
462 if (connectCancelled) { yield
return CoroutineStatus.Success; }
464 yield
return CoroutineStatus.Success;
474 OnPermissionChanged.Invoke(
new PermissionChangedEvent(permissions, permittedConsoleCommands));
506 incomingMessagesToProcess.Clear();
507 incomingMessagesToProcess.AddRange(pendingIncomingMessages);
508 foreach (var inc
in incomingMessagesToProcess)
510 ReadDataMessage(inc);
512 pendingIncomingMessages.Clear();
517 string errorMsg =
"Error while reading a message from server. ";
519 AppendExceptionInfo(ref errorMsg, e);
520 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
521 DebugConsole.ThrowError(errorMsg);
522 new GUIMessageBox(TextManager.Get(
"Error"), TextManager.GetWithVariables(
"MessageReadError", (
"[message]", e.Message), (
"[targetsite]", e.TargetSite.ToString())))
524 DisplayInLoadingScreens =
true
527 GUI.DisableHUD =
false;
532 if (!connected) {
return; }
542 if (updateTimer <= DateTime.Now)
549 if (updateTimer <= DateTime.Now)
564 if (!ChildServerRelay.IsProcessAlive)
572 if (updateTimer <= DateTime.Now)
575 updateTimer = DateTime.Now + UpdateInterval;
579 private readonly List<IReadMessage> pendingIncomingMessages =
new List<IReadMessage>();
580 private readonly List<IReadMessage> incomingMessagesToProcess =
new List<IReadMessage>();
588 if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize
597 pendingIncomingMessages.Add(inc);
633 for (
int i = 0; i < requestLen; i++)
638 ClientPeer.Send(response, DeliveryMethod.Unreliable);
642 for (
int i = 0; i < clientCount; i++)
649 client.
Ping = clientPing;
654 ReadLobbyUpdate(inc);
659 ReadIngameUpdate(inc);
663 string errorMsg =
"Error while reading an ingame update message from server.";
664 AppendExceptionInfo(ref errorMsg, e);
665 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
672 string errorMsg =
"Failed to read a voice packet from the server (VoipClient == null). ";
673 if (GameMain.Client ==
null) { errorMsg +=
"Client disposed. "; }
674 errorMsg +=
"\n" + Environment.StackTrace.CleanupStackTrace();
675 GameAnalyticsManager.AddErrorEventOnce(
676 "GameClient.ReadDataMessage:VoipClientNull",
677 GameMain.Client ==
null ? GameAnalyticsManager.ErrorSeverity.Error : GameAnalyticsManager.ErrorSeverity.Warning,
686 GameMain.Client.DebugServerVoipAmplitude = inc.
ReadRangedSingle(min: 0, max: 1, bitCount: 8);
690 DebugConsole.Log(
"Received QUERY_STARTGAME packet.");
700 Dictionary<MultiPlayerCampaign.NetFlags, UInt16> campaignUpdateIDs =
new Dictionary<MultiPlayerCampaign.NetFlags, ushort>();
701 foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
710 GameMain.NetLobbyScreen.UsingShuttle = usingShuttle;
712 if (campaign ==
null && campaignID == 0)
714 readyToStart = GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList) &&
715 GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox);
727 DebugConsole.Log(readyToStart ?
"Ready to start." :
"Not ready to start.");
731 ClientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable);
733 if (readyToStart && !CoroutineManager.IsCoroutineRunning(
"WaitForStartRound"))
735 CoroutineManager.StartCoroutine(NetLobbyScreen.WaitForStartRound(startButton:
null),
"WaitForStartRound");
739 DebugConsole.Log(
"Received STARTGAME packet.");
740 if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is CampaignMode)
743 CoroutineManager.StartCoroutine(StartGame(inc));
747 GUIMessageBox.CloseAll();
748 GameMain.Instance.ShowLoading(StartGame(inc),
false);
752 DebugConsole.NewMessage(
"Received STARTGAMEFINALIZE packet. Round init status: " + roundInitStatus);
753 if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize)
756 if (campaign !=
null &&
762 ReadStartGameFinalize(inc);
766 CampaignMode.TransitionType transitionType = (CampaignMode.TransitionType)inc.
ReadByte();
768 string endMessage =
string.Empty;
772 for (
int i = 0; i < missionCount; i++)
775 var mission = GameMain.GameSession?.GetMission(i);
778 mission.Completed = missionSuccessful;
784 GameMain.GameSession.WinningTeam = winningTeam;
785 var combatMission = GameMain.GameSession.Missions.FirstOrDefault(m => m is CombatMission);
786 if (combatMission !=
null)
788 combatMission.Completed =
true;
793 TraitorManager.TraitorResults? traitorResults =
null;
794 if (includesTraitorInfo)
796 traitorResults = INetSerializableStruct.Read<TraitorManager.TraitorResults>(inc);
799 roundInitStatus = RoundInitStatus.Interrupted;
800 CoroutineManager.StartCoroutine(
EndGame(endMessage, transitionType, traitorResults),
"EndGame");
801 GUI.SetSavingIndicatorState(save);
805 List<CampaignMode.SaveInfo> saveInfos =
new List<CampaignMode.SaveInfo>();
806 for (
int i = 0; i < saveCount; i++)
808 saveInfos.Add(INetSerializableStruct.Read<CampaignMode.SaveInfo>(inc));
810 MultiPlayerCampaign.StartCampaignSetup(saveInfos);
813 ReadPermissions(inc);
816 ReadAchievement(inc);
819 ReadAchievementStat(inc);
824 if (cheatsEnabled == DebugConsole.CheatsEnabled)
830 DebugConsole.CheatsEnabled = cheatsEnabled;
831 AchievementManager.CheatsEnabled = cheatsEnabled;
834 var cheatMessageBox =
new GUIMessageBox(TextManager.Get(
"CheatsEnabledTitle"), TextManager.Get(
"CheatsEnabledDescription"));
835 cheatMessageBox.Buttons[0].OnClicked += (btn, userdata) =>
837 DebugConsole.TextBox.Select();
850 ReadCircuitBoxMessage(inc);
856 ReadyCheck.ClientRead(inc);
859 TraitorManager.ClientRead(inc);
867 Mission mission = GameMain.GameSession?.GetMission(missionIndex);
868 mission?.ClientRead(inc);
872 GameMain.GameSession?.EventManager.ClientRead(inc);
879 TaskPool.ListTasks(DebugConsole.Log);
880 ushort contentToPreloadCount = inc.
ReadUInt16();
881 List<ContentFile> contentToPreload =
new List<ContentFile>();
882 for (
int i = 0; i < contentToPreloadCount; i++)
885 ContentFile file = ContentPackageManager.EnabledPackages.All
887 p.Files.FirstOrDefault(f => f.Path == filePath))
888 .FirstOrDefault(f => f is not
null);
889 contentToPreload.AddIfNotNull(file);
892 string campaignErrorInfo =
string.Empty;
893 if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign)
895 campaignErrorInfo = $
" Round start save ID: {debugStartGameCampaignSaveID}, last save id: {campaign.LastSaveID}, pending save id: {campaign.PendingSaveID}.";
898 GameMain.GameSession.EventManager.PreloadContent(contentToPreload);
900 int subEqualityCheckValue = inc.
ReadInt32();
901 if (subEqualityCheckValue != (
Submarine.MainSub?.Info?.EqualityCheckVal ?? 0))
904 "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server. " +
905 $
"There may have been an error in receiving the up-to-date submarine file from the server. Round init status: {roundInitStatus}." + campaignErrorInfo;
906 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:SubsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
907 throw new Exception(errorMsg);
911 List<Identifier> serverMissionIdentifiers =
new List<Identifier>();
912 for (
int i = 0; i < missionCount; i++)
916 if (missionCount != GameMain.GameSession.GameMode.Missions.Count())
919 $
"Mission equality check failed. Mission count doesn't match the server. " +
920 $
"Server: {string.Join(",
", serverMissionIdentifiers)}, " +
921 $
"client: {string.Join(",
", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
922 $
"game session: {string.Join(",
", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo;
923 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
924 throw new Exception(errorMsg);
927 if (missionCount > 0)
929 if (!GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier).OrderBy(
id =>
id).SequenceEqual(serverMissionIdentifiers.OrderBy(
id =>
id)))
932 $
"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " +
933 $
"Server: {string.Join(",
", serverMissionIdentifiers)}, " +
934 $
"client: {string.Join(",
", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
935 $
"game session: {string.Join(",
", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo;
936 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
937 throw new Exception(errorMsg);
939 GameMain.GameSession.EnforceMissionOrder(serverMissionIdentifiers);
942 var levelEqualityCheckValues =
new Dictionary<Level.LevelGenStage,
int>();
943 foreach (Level.LevelGenStage stage in Enum.GetValues(typeof(Level.LevelGenStage)).OfType<Level.LevelGenStage>().OrderBy(s => s))
945 levelEqualityCheckValues.Add(stage, inc.
ReadInt32());
948 foreach (var stage
in levelEqualityCheckValues.Keys)
950 if (Level.Loaded.EqualityCheckValues[stage] != levelEqualityCheckValues[stage])
952 string errorMsg =
"Level equality check failed. The level generated at your end doesn't match the level generated by the server" +
953 " (client value " + stage +
": " + Level.Loaded.EqualityCheckValues[stage].ToString(
"X") +
954 ", server value " + stage +
": " + levelEqualityCheckValues[stage].ToString(
"X") +
955 ", level value count: " + levelEqualityCheckValues.Count +
956 ", seed: " + Level.Loaded.Seed +
957 ", sub: " +
Submarine.MainSub.Info.Name +
" (" +
Submarine.MainSub.Info.MD5Hash.ShortRepresentation +
")" +
958 ", mirrored: " + Level.Loaded.Mirrored +
"). Round init status: " + roundInitStatus +
"." + campaignErrorInfo;
959 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
960 throw new Exception(errorMsg);
964 foreach (Mission mission
in GameMain.GameSession.Missions)
966 mission.ClientReadInitial(inc);
971 CrewManager.ClientReadActiveOrders(inc);
974 roundInitStatus = RoundInitStatus.Started;
980 private void OnClientPeerDisconnect(PeerDisconnectPacket disconnectPacket)
982 bool wasConnected = connected;
984 connectCancelled =
true;
986 CoroutineManager.StopCoroutines(
"WaitForStartingInfo");
989 GUI.ClearCursorWait();
991 if (disconnectPacket.ShouldCreateAnalyticsEvent)
993 GameAnalyticsManager.AddErrorEventOnce(
994 "GameClient.HandleDisconnectMessage",
995 GameAnalyticsManager.ErrorSeverity.Debug,
996 $
"Client received a disconnect message. Reason: {disconnectPacket.DisconnectReason}");
999 if (disconnectPacket.DisconnectReason == DisconnectReason.ServerFull)
1003 else if (disconnectPacket.ShouldAttemptReconnect && !
IsServerOwner && wasConnected)
1005 if (disconnectPacket.IsEventSyncError)
1007 GameMain.NetLobbyScreen.Select();
1008 GameMain.GameSession?.EndRound(
"");
1009 GameStarted =
false;
1012 AttemptReconnect(disconnectPacket);
1018 Eos.EosSessionManager.LeaveSession();
1019 SteamManager.LeaveLobby();
1022 GameMain.ModDownloadScreen.Reset();
1023 ContentPackageManager.EnabledPackages.Restore();
1025 GameMain.GameSession?.Campaign?.CancelStartRound();
1033 ChildServerRelay.AttemptGracefulShutDown();
1034 GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary);
1040 GameMain.Client =
null;
1041 GameMain.GameSession =
null;
1043 ReturnToPreviousMenu(
null,
null);
1044 if (disconnectPacket.DisconnectReason != DisconnectReason.Disconnected)
1046 new GUIMessageBox(TextManager.Get(wasConnected ?
"ConnectionLost" :
"CouldNotConnectToServer"), disconnectPacket.PopupMessage)
1048 DisplayInLoadingScreens =
true
1054 private void CreateReconnectBox(LocalizedString headerText, LocalizedString bodyText)
1056 reconnectBox =
new GUIMessageBox(
1059 new LocalizedString[] { TextManager.Get(
"Cancel") })
1061 DisplayInLoadingScreens =
true
1063 reconnectBox.
Buttons[0].OnClicked += (btn, userdata) => { CancelConnect();
return true; };
1064 reconnectBox.
Buttons[0].OnClicked += reconnectBox.
Close;
1067 private void CloseReconnectBox()
1069 reconnectBox?.
Close();
1070 reconnectBox =
null;
1073 private void AskToWaitInQueue()
1075 CoroutineManager.StopCoroutines(
"WaitForStartingInfo");
1078 if (CoroutineManager.IsCoroutineRunning(
"WaitInServerQueue"))
1083 var queueBox =
new GUIMessageBox(
1084 TextManager.Get(
"DisconnectReason.ServerFull"),
1085 TextManager.Get(
"ServerFullQuestionPrompt"),
new LocalizedString[] { TextManager.Get(
"Cancel"), TextManager.Get(
"ServerQueue") });
1087 queueBox.Buttons[0].OnClicked += queueBox.Close;
1088 queueBox.Buttons[1].OnClicked += queueBox.Close;
1089 queueBox.Buttons[1].OnClicked += (btn, userdata) =>
1091 CloseReconnectBox();
1092 CoroutineManager.StartCoroutine(WaitInServerQueue(),
"WaitInServerQueue");
1097 private void AttemptReconnect(PeerDisconnectPacket peerDisconnectPacket)
1099 connectCancelled =
false;
1102 TextManager.Get(
"ConnectionLost"),
1103 peerDisconnectPacket.ReconnectMessage);
1105 var prevContentPackages =
ClientPeer.ServerContentPackages;
1107 GameMain.NetLobbyScreen.LastUpdateID--;
1108 InitiateServerJoin();
1112 ClientPeer.ContentPackageOrderReceived =
true;
1113 ClientPeer.ServerContentPackages = prevContentPackages;
1117 private void UpdatePresence(
string connectCommand)
1119 #warning TODO: use store localization functionality
1120 var desc = TextManager.GetWithVariable(
"FriendPlayingOnServer",
"[servername]",
ServerName);
1122 async Task updateEosPresence()
1124 var epicIds = EosInterface.IdQueries.GetLoggedInEpicIds();
1125 if (!epicIds.FirstOrNone().TryUnwrap(out var epicAccountId)) {
return; }
1127 var setPresenceResult = await EosInterface.Presence.SetJoinCommand(
1128 epicAccountId: epicAccountId,
1131 joinCommand: connectCommand);
1132 DebugConsole.NewMessage($
"Set connect command: {connectCommand}, result: {setPresenceResult}");
1136 "UpdateEosPresence",
1137 updateEosPresence(),
1140 if (SteamManager.IsInitialized)
1142 Steamworks.SteamFriends.ClearRichPresence();
1143 if (!connectCommand.IsNullOrWhiteSpace())
1145 Steamworks.SteamFriends.SetRichPresence(
"servername",
ServerName);
1146 Steamworks.SteamFriends.SetRichPresence(
"status",
1148 Steamworks.SteamFriends.SetRichPresence(
"connect",
1154 private void OnConnectionInitializationComplete()
1156 UpdatePresence($
"-connect \"{ToolBox.EscapeCharacters(ServerName)}\" {string.Join(",
", serverEndpoints.Select(e => e.StringRepresentation))}");
1164 if (Screen.Selected is GameScreen or RoundSummaryScreen or NetLobbyScreen)
1174 GameMain.ModDownloadScreen.Select();
1178 if (GameMain.NetLobbyScreen?.ChatInput !=
null)
1180 GameMain.NetLobbyScreen.ChatInput.Enabled =
true;
1184 private IEnumerable<CoroutineStatus> WaitInServerQueue()
1186 waitInServerQueueBox =
new GUIMessageBox(
1187 TextManager.Get(
"ServerQueuePleaseWait"),
1188 TextManager.Get(
"WaitingInServerQueue"),
new LocalizedString[] { TextManager.Get(
"Cancel") });
1189 waitInServerQueueBox.
Buttons[0].OnClicked += (btn, userdata) =>
1191 CoroutineManager.StopCoroutines(
"WaitInServerQueue");
1192 waitInServerQueueBox?.
Close();
1193 waitInServerQueueBox =
null;
1199 if (!CoroutineManager.IsCoroutineRunning(
"WaitForStartingInfo"))
1201 InitiateServerJoin();
1202 yield
return new WaitForSeconds(5.0f);
1204 yield
return new WaitForSeconds(0.5f);
1207 waitInServerQueueBox?.
Close();
1208 waitInServerQueueBox =
null;
1210 yield
return CoroutineStatus.Success;
1217 AchievementManager.UnlockAchievement(achievementIdentifier);
1220 private static void ReadAchievementStat(
IReadMessage inc)
1222 var netStat = INetSerializableStruct.Read<NetIncrementedStat>(inc);
1223 AchievementManager.IncrementStat(netStat.Stat, netStat.Amount);
1226 private static void ReadCircuitBoxMessage(
IReadMessage inc)
1228 var header = INetSerializableStruct.Read<NetCircuitBoxHeader>(inc);
1230 INetSerializableStruct data = header.Opcode
switch
1232 CircuitBoxOpcode.Cursor => INetSerializableStruct.Read<NetCircuitBoxCursorInfo>(inc),
1233 CircuitBoxOpcode.Error => INetSerializableStruct.Read<CircuitBoxErrorEvent>(inc),
1234 _ =>
throw new ArgumentOutOfRangeException(nameof(header.Opcode), header.Opcode,
"This data cannot be handled using direct network messages.")
1237 if (header.FindTarget().TryUnwrap(out CircuitBox box))
1239 box.ClientRead(data);
1245 List<string> permittedConsoleCommands =
new List<string>();
1249 List<DebugConsole.Command> permittedCommands =
new List<DebugConsole.Command>();
1256 SetMyPermissions(permissions, permittedCommands.Select(command => command.Names[0]));
1260 private void SetMyPermissions(
ClientPermissions newPermissions, IEnumerable<Identifier> permittedConsoleCommands)
1262 if (!(this.permittedConsoleCommands.Any(c => !permittedConsoleCommands.Contains(c)) ||
1263 permittedConsoleCommands.Any(c => !
this.permittedConsoleCommands.Contains(c))))
1265 if (newPermissions == permissions) {
return; }
1271 permissions = newPermissions;
1272 this.permittedConsoleCommands = permittedConsoleCommands.ToList();
1276 GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as
string ==
"permissions");
1277 GUIMessageBox msgBox =
new GUIMessageBox(
"",
"") { UserData =
"permissions" };
1278 msgBox.Content.ClearChildren();
1279 msgBox.Content.RectTransform.RelativeSize =
new Vector2(0.95f, 0.9f);
1281 var header =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), msgBox.Content.RectTransform), TextManager.Get(
"PermissionsChanged"), textAlignment: Alignment.Center, font: GUIStyle.LargeFont);
1282 header.RectTransform.IsFixedSize =
true;
1284 var permissionArea =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 1.0f), msgBox.Content.RectTransform), isHorizontal:
true) { Stretch =
true, RelativeSpacing = 0.05f };
1285 var leftColumn =
new GUILayoutGroup(
new RectTransform(
new Vector2(0.5f, 1.0f), permissionArea.RectTransform)) { Stretch =
true, RelativeSpacing = 0.05f };
1286 var rightColumn =
new GUILayoutGroup(
new RectTransform(
new Vector2(0.5f, 1.0f), permissionArea.RectTransform)) { Stretch =
true, RelativeSpacing = 0.05f };
1288 var permissionsLabel =
new GUITextBlock(
new RectTransform(
new Vector2(newPermissions ==
ClientPermissions.None ? 2.0f : 1.0f, 0.0f), leftColumn.RectTransform),
1289 TextManager.Get(newPermissions ==
ClientPermissions.None ?
"PermissionsRemoved" :
"CurrentPermissions"),
1290 wrap:
true, font: (newPermissions ==
ClientPermissions.None ? GUIStyle.Font : GUIStyle.SubHeadingFont));
1291 permissionsLabel.RectTransform.NonScaledSize =
new Point(permissionsLabel.Rect.Width, permissionsLabel.Rect.Height);
1292 permissionsLabel.RectTransform.IsFixedSize =
true;
1295 LocalizedString permissionList =
"";
1298 if (!newPermissions.HasFlag(permission) || permission ==
ClientPermissions.None) {
continue; }
1299 permissionList +=
" - " + TextManager.Get(
"ClientPermission." + permission) +
"\n";
1301 new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), leftColumn.RectTransform),
1307 var commandsLabel =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), rightColumn.RectTransform),
1308 TextManager.Get(
"PermittedConsoleCommands"), wrap:
true, font: GUIStyle.SubHeadingFont);
1309 var commandList =
new GUIListBox(
new RectTransform(
new Vector2(1.0f, 1.0f), rightColumn.RectTransform));
1310 foreach (Identifier permittedCommand
in permittedConsoleCommands)
1312 new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.05f), commandList.Content.RectTransform, minSize:
new Point(0, 15)),
1313 permittedCommand.Value, font: GUIStyle.SmallFont)
1315 CanBeFocused =
false
1318 permissionsLabel.RectTransform.NonScaledSize = commandsLabel.RectTransform.NonScaledSize =
1319 new Point(permissionsLabel.Rect.Width, Math.Max(permissionsLabel.Rect.Height, commandsLabel.Rect.Height));
1320 commandsLabel.RectTransform.IsFixedSize =
true;
1323 new GUIButton(
new RectTransform(
new Vector2(0.5f, 0.05f), msgBox.Content.RectTransform), TextManager.Get(
"ok"))
1325 OnClicked = msgBox.Close
1328 permissionArea.RectTransform.MinSize =
new Point(0, Math.Max(leftColumn.RectTransform.Children.Sum(c => c.Rect.Height), rightColumn.RectTransform.Children.Sum(c => c.Rect.Height)));
1329 permissionArea.RectTransform.IsFixedSize =
true;
1330 int contentHeight = (int)(msgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height + msgBox.Content.AbsoluteSpacing) * 1.05f);
1331 msgBox.Content.ChildAnchor =
Anchor.TopCenter;
1332 msgBox.Content.Stretch =
true;
1333 msgBox.Content.RectTransform.MinSize =
new Point(0, contentHeight);
1334 msgBox.InnerFrame.RectTransform.MinSize =
new Point(0, (
int)(contentHeight / permissionArea.RectTransform.RelativeSize.Y / msgBox.Content.RectTransform.RelativeSize.Y));
1337 if (refreshCampaignUI)
1339 if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1341 campaign.CampaignUI?.UpgradeStore?.RequestRefresh();
1342 campaign.CampaignUI?.HRManagerUI?.RefreshUI();
1346 GameMain.NetLobbyScreen.RefreshEnabledElements();
1349 OnPermissionChanged.Invoke(
new PermissionChangedEvent(permissions, this.permittedConsoleCommands));
1352 private IEnumerable<CoroutineStatus> StartGame(
IReadMessage inc)
1357 eventErrorWritten =
false;
1358 GameMain.NetLobbyScreen.StopWaitingForStartRound();
1360 debugStartGameCampaignSaveID =
null;
1362 while (CoroutineManager.IsCoroutineRunning(
"EndGame"))
1365 yield
return CoroutineStatus.Running;
1370 GameMain.NetLobbyScreen.ShowSpectateButton();
1379 roundInitStatus = RoundInitStatus.Starting;
1384 GameModePreset gameMode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier);
1385 if (gameMode ==
null)
1387 DebugConsole.ThrowError(
"Game mode \"" + modeIdentifier +
"\" not found!");
1388 roundInitStatus = RoundInitStatus.Interrupted;
1389 yield
return CoroutineStatus.Failure;
1401 bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.
ReadBoolean();
1405 GameMain.LightManager.LightingEnabled =
true;
1409 Rand.SetSyncedSeed(seed);
1411 Task loadTask =
null;
1412 var roundSummary = (GUIMessageBox.MessageBoxes.Find(c => c?.UserData is RoundSummary)?.UserData) as RoundSummary;
1414 bool isOutpost =
false;
1416 if (gameMode != GameModePreset.MultiPlayerCampaign)
1424 List<UInt32> missionHashes =
new List<UInt32>();
1426 for (
int i = 0; i < missionCount; i++)
1430 if (!GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList))
1432 roundInitStatus = RoundInitStatus.Interrupted;
1433 yield
return CoroutineStatus.Success;
1436 if (!GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox))
1438 roundInitStatus = RoundInitStatus.Interrupted;
1439 yield
return CoroutineStatus.Success;
1443 if (GameMain.NetLobbyScreen.SelectedSub ==
null ||
1444 GameMain.NetLobbyScreen.SelectedSub.Name != subName ||
1445 GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation != subHash)
1447 string errorMsg =
"Failed to select submarine \"" + subName +
"\" (hash: " + subHash +
").";
1448 if (GameMain.NetLobbyScreen.SelectedSub ==
null)
1450 errorMsg +=
"\n" +
"SelectedSub is null";
1454 if (GameMain.NetLobbyScreen.SelectedSub.Name != subName)
1456 errorMsg +=
"\n" +
"Name mismatch: " + GameMain.NetLobbyScreen.SelectedSub.Name +
" != " + subName;
1458 if (GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation != subHash)
1460 errorMsg +=
"\n" +
"Hash mismatch: " + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation +
" != " + subHash;
1464 GameMain.NetLobbyScreen.Select();
1465 DebugConsole.ThrowError(errorMsg);
1466 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:FailedToSelectSub" + subName, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1467 roundInitStatus = RoundInitStatus.Interrupted;
1468 yield
return CoroutineStatus.Failure;
1470 if (GameMain.NetLobbyScreen.SelectedShuttle ==
null ||
1471 GameMain.NetLobbyScreen.SelectedShuttle.Name != shuttleName ||
1472 GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash?.StringRepresentation != shuttleHash)
1475 GameMain.NetLobbyScreen.Select();
1476 string errorMsg =
"Failed to select shuttle \"" + shuttleName +
"\" (hash: " + shuttleHash +
").";
1477 DebugConsole.ThrowError(errorMsg);
1478 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.StartGame:FailedToSelectShuttle" + shuttleName, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1479 roundInitStatus = RoundInitStatus.Interrupted;
1480 yield
return CoroutineStatus.Failure;
1483 var selectedMissions = missionHashes.Select(i => MissionPrefab.Prefabs.Find(p => p.UintIdentifier == i));
1485 GameMain.GameSession =
new GameSession(GameMain.NetLobbyScreen.SelectedSub, gameMode, missionPrefabs: selectedMissions);
1486 GameMain.GameSession.StartRound(levelSeed, levelDifficulty);
1490 if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign))
1492 throw new InvalidOperationException(
"Attempted to start a campaign round when a campaign was not active.");
1495 if (GameMain.GameSession?.CrewManager !=
null) { GameMain.GameSession.CrewManager.Reset(); }
1499 int nextLocationIndex = inc.
ReadInt32();
1500 int nextConnectionIndex = inc.
ReadInt32();
1501 int selectedLocationIndex = inc.
ReadInt32();
1504 if (campaign.CampaignID != campaignID)
1507 DebugConsole.ThrowError(
"Failed to start campaign round (campaign ID does not match).");
1508 GameMain.NetLobbyScreen.Select();
1509 roundInitStatus = RoundInitStatus.Interrupted;
1510 yield
return CoroutineStatus.Failure;
1513 if (NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID) ||
1514 NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID))
1516 campaign.PendingSaveID = campaignSaveID;
1518 while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID))
1520 if (DateTime.Now > saveFileTimeOut)
1523 new GUIMessageBox(TextManager.Get(
"error"), TextManager.Get(
"campaignsavetransfer.timeout"));
1524 GameMain.NetLobbyScreen.Select();
1525 roundInitStatus = RoundInitStatus.Interrupted;
1527 yield
return CoroutineStatus.Success;
1529 yield
return new WaitForSeconds(0.1f);
1533 if (campaign.Map ==
null)
1536 DebugConsole.ThrowError(
"Failed to start campaign round (campaign map not loaded yet).");
1537 GameMain.NetLobbyScreen.Select();
1538 roundInitStatus = RoundInitStatus.Interrupted;
1539 yield
return CoroutineStatus.Failure;
1542 campaign.Map.SelectLocation(selectedLocationIndex);
1544 LevelData levelData = nextLocationIndex > -1 ?
1545 campaign.Map.Locations[nextLocationIndex].LevelData :
1546 campaign.Map.Connections[nextConnectionIndex].LevelData;
1548 debugStartGameCampaignSaveID = campaign.LastSaveID;
1550 if (roundSummary !=
null)
1552 loadTask = campaign.SelectSummaryScreen(roundSummary, levelData, mirrorLevel,
null);
1553 roundSummary.ContinueButton.Visible =
false;
1557 GameMain.GameSession.StartRound(levelData, mirrorLevel, startOutpost: campaign?.GetPredefinedStartOutpost());
1559 isOutpost = levelData.Type == LevelData.LevelType.Outpost;
1564 if (loadTask !=
null)
1566 while (!loadTask.IsCompleted && !loadTask.IsFaulted && !loadTask.IsCanceled)
1568 yield
return CoroutineStatus.Running;
1574 DebugConsole.ThrowError(
"There was an error initializing the round (disconnected during the StartGame coroutine.)");
1575 roundInitStatus = RoundInitStatus.Error;
1576 yield
return CoroutineStatus.Failure;
1579 roundInitStatus = RoundInitStatus.WaitingForStartGameFinalize;
1582 TimeSpan timeOutDuration =
new TimeSpan(0, 0, seconds: 30);
1583 DateTime timeOut = DateTime.Now + timeOutDuration;
1584 DateTime requestFinalizeTime = DateTime.Now;
1585 TimeSpan requestFinalizeInterval =
new TimeSpan(0, 0, 2);
1588 ClientPeer.Send(msg, DeliveryMethod.Unreliable);
1590 GUIMessageBox interruptPrompt =
null;
1592 if (includesFinalize)
1594 ReadStartGameFinalize(inc);
1602 if (DateTime.Now > requestFinalizeTime)
1604 msg =
new WriteOnlyMessage();
1606 ClientPeer.Send(msg, DeliveryMethod.Unreliable);
1607 requestFinalizeTime = DateTime.Now + requestFinalizeInterval;
1609 if (DateTime.Now > timeOut && interruptPrompt ==
null)
1611 interruptPrompt =
new GUIMessageBox(
string.Empty, TextManager.Get(
"WaitingForStartGameFinalizeTakingTooLong"),
1612 new LocalizedString[] { TextManager.Get(
"Yes"), TextManager.Get(
"No") })
1614 DisplayInLoadingScreens =
true
1616 interruptPrompt.Buttons[0].OnClicked += (btn, userData) =>
1618 roundInitStatus = RoundInitStatus.Interrupted;
1619 DebugConsole.ThrowError(
"Error while starting the round (did not receive STARTGAMEFINALIZE message from the server). Returning to the lobby...");
1621 GameMain.NetLobbyScreen.Select();
1622 interruptPrompt.Close();
1623 interruptPrompt =
null;
1626 interruptPrompt.Buttons[1].OnClicked += (btn, userData) =>
1628 timeOut = DateTime.Now + timeOutDuration;
1629 interruptPrompt.Close();
1630 interruptPrompt =
null;
1637 roundInitStatus = RoundInitStatus.Interrupted;
1641 if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) {
break; }
1645 DebugConsole.ThrowError(
"There was an error initializing the round.", e, createMessageBox:
true);
1646 roundInitStatus = RoundInitStatus.Error;
1651 yield
return CoroutineStatus.Running;
1655 interruptPrompt?.Close();
1656 interruptPrompt =
null;
1658 if (roundInitStatus != RoundInitStatus.Started)
1660 if (roundInitStatus != RoundInitStatus.Interrupted)
1662 DebugConsole.ThrowError(roundInitStatus.ToString());
1663 CoroutineManager.StartCoroutine(
EndGame(
""));
1664 yield
return CoroutineStatus.Failure;
1668 yield
return CoroutineStatus.Success;
1672 if (GameMain.GameSession.Submarine.Info.IsFileCorrupted)
1674 DebugConsole.ThrowError($
"Failed to start a round. Could not load the submarine \"{GameMain.GameSession.Submarine.Info.Name}\".");
1675 yield
return CoroutineStatus.Failure;
1678 for (
int i = 0; i <
Submarine.MainSubs.Length; i++)
1680 if (
Submarine.MainSubs[i] ==
null) {
break; }
1684 foreach (Item item
in Item.ItemList)
1686 if (item.Submarine ==
null) {
continue; }
1687 if (item.Submarine !=
Submarine.MainSubs[i] && !
Submarine.MainSubs[i].DockedTo.Contains(item.Submarine)) {
continue; }
1693 foreach (Submarine sub
in Submarine.MainSubs[i].DockedTo)
1696 sub.TeamID = teamID;
1708 if (roundSummary !=
null)
1710 roundSummary.ContinueButton.Visible =
true;
1713 GameMain.GameScreen.Select();
1715 string message =
"ServerMessage.HowToCommunicate" +
1716 $
"~[chatbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ActiveChat)}" +
1717 $
"~[pttbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Voice)}" +
1718 $
"~[switchbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleChatMode)}";
1721 yield
return CoroutineStatus.Success;
1727 DateTime timeOut = DateTime.Now +
new TimeSpan(0, 0, 60);
1728 while (TaskPool.IsTaskRunning(
"AsyncCampaignStartRound"))
1730 if (DateTime.Now > timeOut)
1732 throw new Exception(
"Failed to end a round (async campaign round start timed out).");
1747 GameStarted =
false;
1787 foreach (
Client c
in otherClients)
1802 for (
int i = 0; i < subListCount; i++)
1808 bool requiredContentPackagesInstalled = inc.
ReadBoolean();
1811 if (matchingSub ==
null)
1813 matchingSub =
new SubmarineInfo(Path.Combine(SaveUtil.SubmarineDownloadFolder, subName) +
".sub", subHash, tryLoad:
false)
1819 matchingSub.RequiredContentPackagesInstalled = requiredContentPackagesInstalled;
1823 GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList,
ServerSubmarines);
1824 GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.ShuttleList.ListBox,
ServerSubmarines.Where(s => s.HasTag(
SubmarineTag.Shuttle)));
1831 ReadPermissions(inc);
1835 if (Screen.Selected != GameMain.GameScreen)
1837 LocalizedString message;
1840 message = TextManager.Get(ironmanMode ?
"RoundRunningIronman" :
"RoundRunningPermadeath");
1844 message = TextManager.Get(allowSpectating ?
"RoundRunningSpectateEnabled" :
"RoundRunningSpectateDisabled");
1846 new GUIMessageBox(TextManager.Get(
"PleaseWait"), message);
1847 if (!(Screen.Selected is ModDownloadScreen)) { GameMain.NetLobbyScreen.Select(); }
1854 bool refreshCampaignUI =
false;
1856 List<TempClient> tempClients =
new List<TempClient>();
1858 for (
int i = 0; i < clientCount; i++)
1860 tempClients.Add(INetSerializableStruct.Read<
TempClient>(inc));
1864 if (NetIdUtils.IdMoreRecent(listId, LastClientListUpdateID))
1866 bool updateClientListId =
true;
1867 List<Client> currentClients =
new List<Client>();
1872 if (existingClient ==
null)
1881 otherClients.Add(existingClient);
1882 refreshCampaignUI =
true;
1883 GameMain.NetLobbyScreen.AddPlayer(existingClient);
1885 existingClient.NameId = tc.
NameId;
1888 existingClient.Character =
null;
1889 existingClient.Karma = tc.
Karma;
1890 existingClient.Muted = tc.
Muted;
1891 existingClient.InGame = tc.
InGame;
1892 existingClient.IsOwner = tc.
IsOwner;
1894 GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient);
1895 if (Screen.Selected != GameMain.NetLobbyScreen && tc.
CharacterId > 0)
1899 if (existingClient.SessionId ==
SessionId)
1901 existingClient.SetPermissions(permissions, permittedConsoleCommands);
1902 if (!NetIdUtils.IdMoreRecent(nameId, tc.
NameId))
1907 if (GameMain.NetLobbyScreen.CharacterNameBox !=
null &&
1908 !GameMain.NetLobbyScreen.CharacterNameBox.Selected)
1910 GameMain.NetLobbyScreen.CharacterNameBox.Text =
Name;
1913 currentClients.Add(existingClient);
1921 otherClients[i].Dispose();
1922 otherClients.RemoveAt(i);
1923 refreshCampaignUI =
true;
1928 int index = previouslyConnectedClients.FindIndex(c => c.
SessionId == client.
SessionId);
1931 if (previouslyConnectedClients.Count > 100)
1933 previouslyConnectedClients.RemoveRange(0, previouslyConnectedClients.Count - 100);
1938 previouslyConnectedClients.RemoveAt(index);
1940 previouslyConnectedClients.Add(client);
1942 if (updateClientListId) { LastClientListUpdateID = listId; }
1947 TaskPool.Add(
"WaitForPingDataAsync (owner)",
1948 Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) =>
1950 Steam.SteamManager.UpdateLobby(ServerSettings);
1957 if (refreshCampaignUI)
1959 if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1961 campaign.CampaignUI?.UpgradeStore?.RequestRefresh();
1962 campaign.CampaignUI?.HRManagerUI?.RefreshUI();
1967 private bool initialUpdateReceived;
1981 var prevDispatcher = GUI.KeyboardDispatcher.Subscriber;
1986 byte[] settingsData = inc.
ReadBytes(settingsLen);
1989 if (isInitialUpdate)
1991 if (GameSettings.CurrentConfig.VerboseLogging)
1993 DebugConsole.NewMessage(
"Received initial lobby update, ID: " + updateID +
", last ID: " + GameMain.NetLobbyScreen.LastUpdateID, Color.Gray);
1995 ReadInitialUpdate(inc);
1996 initialUpdateReceived =
true;
2014 int traitorDangerLevel = inc.
ReadRangedInteger(TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel);
2026 float autoRestartTimer = autoRestartEnabled ? inc.
ReadSingle() : 0.0f;
2030 if (NetIdUtils.IdMoreRecent(updateID, GameMain.NetLobbyScreen.LastUpdateID) &&
2031 (isInitialUpdate || initialUpdateReceived))
2033 ReadWriteMessage settingsBuf = new ReadWriteMessage();
2034 settingsBuf.WriteBytes(settingsData, 0, settingsLen); settingsBuf.BitPosition = 0;
2035 ServerSettings.ClientRead(settingsBuf);
2038 ServerInfo info = CreateServerInfoFromSettings();
2039 GameMain.ServerListScreen.AddToRecentServers(info);
2040 GameMain.NetLobbyScreen.Favorite.Visible = true;
2041 GameMain.NetLobbyScreen.Favorite.Selected = GameMain.ServerListScreen.IsFavorite(info);
2045 GameMain.NetLobbyScreen.Favorite.Visible = false;
2048 GameMain.NetLobbyScreen.LastUpdateID = updateID;
2051 GameMain.NetLobbyScreen.UsingShuttle = usingShuttle;
2053 if (!allowSubVoting || GameMain.NetLobbyScreen.SelectedSub ==
null) { GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, GameMain.NetLobbyScreen.SubList); }
2054 GameMain.NetLobbyScreen.TrySelectSub(selectShuttleName, selectShuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox);
2056 GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability);
2057 GameMain.NetLobbyScreen.SetTraitorDangerLevel(traitorDangerLevel);
2058 GameMain.NetLobbyScreen.SetMissionType(missionType);
2059 GameMain.NetLobbyScreen.LevelSeed = levelSeed;
2061 GameMain.NetLobbyScreen.SelectMode(modeIndex);
2062 if (isInitialUpdate && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
2064 if (GameMain.Client.IsServerOwner) { RequestSelectMode(modeIndex); }
2067 if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
2069 foreach (SubmarineInfo sub in ServerSubmarines.Where(s => !ServerSettings.HiddenSubs.Contains(s.Name)))
2071 GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Campaign);
2075 GameMain.NetLobbyScreen.SetAllowSpectating(allowSpectating);
2076 GameMain.NetLobbyScreen.SetLevelDifficulty(levelDifficulty);
2077 GameMain.NetLobbyScreen.SetBotSpawnMode(botSpawnMode);
2078 GameMain.NetLobbyScreen.SetBotCount(botCount);
2079 GameMain.NetLobbyScreen.SetAutoRestart(autoRestartEnabled, autoRestartTimer);
2091 GUI.KeyboardDispatcher.Subscriber = prevDispatcher;
2097 if (campaignUpdated)
2099 MultiPlayerCampaign.ClientRead(inc);
2101 else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
2103 GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
2109 ReadClientList(inc);
2123 readonly List<IServerSerializable> debugEntityList =
new List<IServerSerializable>();
2126 debugEntityList.Clear();
2133 segmentDataReader: (segment, inc) =>
2143 if (campaignUpdated)
2145 MultiPlayerCampaign.ClientRead(inc);
2147 else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
2149 GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
2156 int msgEndPos = (
int)(inc.
BitPosition + msgLength * 8);
2158 var header = INetSerializableStruct.Read<EntityPositionHeader>(inc);
2163 DebugConsole.ThrowError($
"Error while reading a position update for the entity \"({entity?.ToString() ?? "null
"})\". Message length exceeds the size of the buffer.");
2164 return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
2167 debugEntityList.Add(entity);
2170 if (entity is Item != header.IsItem)
2172 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...");
2174 else if (entity is MapEntity { Prefab.UintIdentifier: var uintIdentifier } me &&
2175 uintIdentifier != header.PrefabUintIdentifier)
2177 DebugConsole.AddWarning($
"Received a potentially invalid ENTITY_POSITION message."
2178 +$
"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == header.PrefabUintIdentifier)?.Identifier.Value ?? "[not found]
"}, "
2179 +$
"client entity is {me.Prefab.Identifier}). Ignoring the message...");
2192 ReadClientList(inc);
2196 if (!EntityEventManager.Read(segment, inc, sendingTime))
2205 throw new Exception($
"Unknown segment \"{segment}\"!)");
2210 exceptionHandler: (segment, prevSegments, ex) =>
2212 List<string> errorLines =
new List<string>
2217 $
"Segment with error: {segment}"
2219 if (prevSegments.Any())
2221 errorLines.Add(
"Prev segments: " +
string.Join(
", ", prevSegments));
2222 errorLines.Add(
" ");
2224 errorLines.Add(ex.StackTrace.CleanupStackTrace());
2225 errorLines.Add(
" ");
2226 if (prevSegments.Concat(segment.ToEnumerable()).Any(s => s.Identifier
2235 errorLines.Add(
" - NULL");
2238 Entity e = ent as Entity;
2239 errorLines.Add(
" - " + e.ToString());
2243 errorLines.Add(
"Last console messages:");
2244 for (
int i = DebugConsole.Messages.Count - 1; i > Math.Max(0, DebugConsole.Messages.Count - 20); i--)
2246 errorLines.Add(
"[" + DebugConsole.Messages[i].Time +
"] " + DebugConsole.Messages[i].Text);
2248 GameAnalyticsManager.AddErrorEventOnce(
"GameClient.ReadInGameUpdate", GameAnalyticsManager.ErrorSeverity.Critical,
string.Join(
"\n", errorLines));
2250 throw new Exception(
2251 $
"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." +
2252 (prevSegments.Any() ? $
" Previous segments: {string.Join(",
", prevSegments)}" :
""),
2257 private void SendLobbyUpdate()
2265 outmsg.
WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID);
2270 var jobPreferences = GameMain.NetLobbyScreen.JobPreferences;
2271 if (jobPreferences.Count > 0)
2279 outmsg.
WriteByte((
byte)MultiplayerPreferences.Instance.TeamPreference);
2281 if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.LastSaveID == 0)
2289 foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
2291 outmsg.
WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag));
2293 outmsg.
WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
2296 chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
2299 if (outmsg.
LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
2304 chatMsgQueue[i].ClientWrite(segmentTable, outmsg);
2309 DebugConsole.ThrowError($
"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})");
2312 ClientPeer.Send(outmsg, DeliveryMethod.Unreliable);
2315 private void SendIngameUpdate()
2319 outmsg.
WriteBoolean(EntityEventManager.MidRoundSyncingDone);
2327 outmsg.
WriteUInt16(EntityEventManager.LastReceivedID);
2330 if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
2338 foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
2340 outmsg.
WriteUInt16(campaign.GetLastUpdateIdForFlag(flag));
2343 outmsg.
WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
2346 Character.Controlled?.ClientWriteInput(segmentTable, outmsg);
2347 GameMain.GameScreen.Cam?.ClientWrite(segmentTable, outmsg);
2349 EntityEventManager.Write(segmentTable, outmsg, ClientPeer?.ServerConnection);
2351 chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
2354 if (outmsg.
LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
2360 chatMsgQueue[i].ClientWrite(segmentTable, outmsg);
2366 DebugConsole.ThrowError($
"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})");
2369 ClientPeer.Send(outmsg, DeliveryMethod.Unreliable);
2374 if (ClientPeer?.ServerConnection ==
null) {
return; }
2375 lastQueueChatMsgID++;
2377 chatMsgQueue.Add(msg);
2382 if (ClientPeer?.ServerConnection ==
null) {
return; }
2385 GameStarted && myCharacter !=
null ? myCharacter.Name : Name,
2388 GameStarted && myCharacter !=
null ? myCharacter :
null);
2391 lastQueueChatMsgID++;
2394 chatMsgQueue.Add(chatMessage);
2399 WaitForNextRoundRespawn = waitForNextRoundRespawn;
2404 ClientPeer?.Send(msg, DeliveryMethod.Reliable);
2412 ClientPeer?.Send(msg, DeliveryMethod.Reliable);
2419 $
"Sending a campaign file request to the server." :
2420 $
"Sending a file request to the server (type: {fileType}, path: {file ?? "null"}");
2428 msg.
WriteString(file ??
throw new ArgumentNullException(nameof(file)));
2429 msg.
WriteString(fileHash ??
throw new ArgumentNullException(nameof(fileHash)));
2431 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2436 CancelFileTransfer(transfer.ID);
2441 if (!reliable && (DateTime.Now - transfer.LastOffsetAckTime).TotalSeconds < 1)
2445 transfer.RecordOffsetAckTime();
2453 ClientPeer.Send(msg, reliable ? DeliveryMethod.Reliable : DeliveryMethod.Unreliable);
2462 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2467 switch (transfer.FileType)
2472 if (newSub.IsFileCorrupted) {
return; }
2475 .Where(s => s.Name == newSub.Name && s.MD5Hash == newSub.MD5Hash)
2481 SubmarineInfo.AddToSavedSubs(newSub);
2483 for (
int i = 0; i < 2; i++)
2485 IEnumerable<GUIComponent> subListChildren = (i == 0) ?
2486 GameMain.NetLobbyScreen.ShuttleList.ListBox.Content.Children :
2487 GameMain.NetLobbyScreen.SubList.Content.Children;
2489 var subElement = subListChildren.FirstOrDefault(c =>
2490 ((SubmarineInfo)c.UserData).Name == newSub.Name &&
2491 ((SubmarineInfo)c.UserData).MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation);
2492 if (subElement ==
null) {
continue; }
2494 Color newSubTextColor =
new Color(subElement.GetChild<GUITextBlock>().TextColor, 1.0f);
2495 subElement.GetChild<GUITextBlock>().TextColor = newSubTextColor;
2497 if (subElement.GetChildByUserData(
"classtext") is GUITextBlock classTextBlock)
2499 Color newSubClassTextColor =
new Color(classTextBlock.TextColor, 0.8f);
2500 classTextBlock.Text = TextManager.Get($
"submarineclass.{newSub.SubmarineClass}");
2501 classTextBlock.TextColor = newSubClassTextColor;
2504 subElement.UserData = newSub;
2505 subElement.ToolTip = newSub.Description;
2508 if (GameMain.NetLobbyScreen.FailedSelectedSub.HasValue &&
2509 GameMain.NetLobbyScreen.FailedSelectedSub.Value.Name == newSub.Name &&
2510 GameMain.NetLobbyScreen.FailedSelectedSub.Value.Hash == newSub.MD5Hash.StringRepresentation)
2512 GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, GameMain.NetLobbyScreen.SubList);
2515 if (GameMain.NetLobbyScreen.FailedSelectedShuttle.HasValue &&
2516 GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Name == newSub.Name &&
2517 GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Hash == newSub.MD5Hash.StringRepresentation)
2519 GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, GameMain.NetLobbyScreen.ShuttleList.ListBox);
2522 NetLobbyScreen.FailedSubInfo failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation);
2523 if (failedCampaignSub !=
default)
2525 GameMain.NetLobbyScreen.FailedCampaignSubs.Remove(failedCampaignSub);
2528 NetLobbyScreen.FailedSubInfo failedOwnedSub = GameMain.NetLobbyScreen.FailedOwnedSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation);
2529 if (failedOwnedSub !=
default)
2531 GameMain.NetLobbyScreen.FailedOwnedSubs.Remove(failedOwnedSub);
2535 SubmarineInfo existingServerSub = ServerSubmarines.Find(s =>
2536 s.Name == newSub.Name
2537 && s.MD5Hash == newSub.MD5Hash);
2538 if (existingServerSub !=
null)
2540 int existingIndex = ServerSubmarines.IndexOf(existingServerSub);
2541 ServerSubmarines[existingIndex] = newSub;
2542 existingServerSub.Dispose();
2547 XElement gameSessionDocRoot = SaveUtil.DecompressSaveAndLoadGameSessionDoc(transfer.FilePath)?.Root;
2548 byte campaignID = (byte)MathHelper.Clamp(gameSessionDocRoot.GetAttributeInt(
"campaignid", 0), 0, 255);
2549 if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.CampaignID != campaignID)
2551 string savePath = transfer.FilePath;
2552 GameMain.GameSession =
new GameSession(
null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty);
2553 campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode;
2554 campaign.CampaignID = campaignID;
2555 GameMain.NetLobbyScreen.ToggleCampaignMode(
true);
2558 GameMain.GameSession.SavePath = transfer.FilePath;
2559 if (GameMain.GameSession.SubmarineInfo ==
null || campaign.Map ==
null)
2561 string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDocRoot.GetAttributeString(
"submarine",
"")) +
".sub";
2562 GameMain.GameSession.SubmarineInfo =
new SubmarineInfo(subPath,
"");
2565 campaign.LoadState(GameMain.GameSession.SavePath);
2566 GameMain.GameSession?.SubmarineInfo?.Reload();
2567 GameMain.GameSession?.SubmarineInfo?.CheckSubsLeftBehind();
2569 if (GameMain.GameSession?.SubmarineInfo?.Name !=
null)
2571 GameMain.NetLobbyScreen.TryDisplayCampaignSubmarine(GameMain.GameSession.SubmarineInfo);
2573 campaign.LastSaveID = campaign.PendingSaveID;
2575 if (Screen.Selected == GameMain.NetLobbyScreen)
2578 GameMain.NetLobbyScreen.SaveAppearance();
2579 GameMain.NetLobbyScreen.Select();
2582 DebugConsole.Log(
"Campaign save received (" + GameMain.GameSession.SavePath +
"), save ID " + campaign.LastSaveID);
2585 foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
2587 campaign.SetLastUpdateIdForFlag(flag, (ushort)(campaign.GetLastUpdateIdForFlag(flag) - 1));
2591 if (!(Screen.Selected is ModDownloadScreen)) {
return; }
2593 GameMain.ModDownloadScreen.CurrentDownloadFinished(transfer);
2608 CreateEntityEvent(entity, extraData, requireControlledCharacter:
true);
2615 throw new InvalidCastException($
"Entity is not {nameof(IClientSerializable)}");
2617 EntityEventManager.CreateEvent(clientSerializable, extraData, requireControlledCharacter);
2622 return permissions.HasFlag(permission);
2629 if (permittedConsoleCommands.Contains(commandName)) {
return true; }
2632 foreach (DebugConsole.Command command in DebugConsole.Commands)
2634 if (command.Names.Contains(commandName))
2636 if (command.Names.Intersect(permittedConsoleCommands).Any()) {
return true; }
2648 ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
2657 WriteCharacterInfo(msg, newName);
2658 ClientPeer?.Send(msg, DeliveryMethod.Reliable);
2665 if (characterInfo ==
null) {
return; }
2667 var head = characterInfo.Head;
2669 var netInfo =
new NetCharacterInfo(
2670 NewName: newName ??
string.Empty,
2671 Tags: head.Preset.TagSet.ToImmutableArray(),
2672 HairIndex: (
byte)head.HairIndex,
2673 BeardIndex: (
byte)head.BeardIndex,
2674 MoustacheIndex: (
byte)head.MoustacheIndex,
2675 FaceAttachmentIndex: (
byte)head.FaceAttachmentIndex,
2676 SkinColor: head.SkinColor,
2677 HairColor: head.HairColor,
2678 FacialHairColor: head.FacialHairColor,
2681 msg.WriteNetSerializableStruct(netInfo);
2684 public void Vote(VoteType voteType,
object data)
2686 if (ClientPeer ==
null) {
return; }
2696 throw new Exception(
2697 $
"Failed to write vote of type {voteType}: " +
2698 $
"data was of invalid type {data?.GetType().Name ?? "NULL
"}");
2702 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2707 if (votedClient ==
null) {
return; }
2708 Vote(VoteType.Kick, votedClient);
2711 #region Submarine Change Voting
2714 if (sub ==
null) {
return; }
2715 Vote(voteType, (sub, transferItems));
2720 if (info ==
null) {
return; }
2721 if (votingInterface !=
null && votingInterface.VoteRunning) {
return; }
2722 votingInterface?.Remove();
2727 #region Money Transfer Voting
2730 if (votingInterface !=
null && votingInterface.VoteRunning) {
return; }
2731 if (from ==
null && to ==
null)
2733 DebugConsole.ThrowError(
"Tried to initiate a vote for transferring from null to null!");
2736 votingInterface?.Remove();
2744 if (should !=
null && should.Value) {
return; }
2746 if (
string.IsNullOrEmpty(message.
Text)) {
return; }
2749 if (message.Text.IsNullOrEmpty())
2751 sender.ShowTextlessSpeechBubble(2.0f, message.Color);
2755 sender.ShowSpeechBubble(message.Color, message.Text);
2758 sender.TextChatVolume = 1f;
2762 GameMain.NetLobbyScreen.NewChatMessage(message);
2763 chatBox.AddMessage(message);
2766 public override void KickPlayer(
string kickedName,
string reason)
2774 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2777 public override void BanPlayer(
string kickedName,
string reason, TimeSpan? duration =
null)
2784 msg.
WriteDouble(duration.HasValue ? duration.Value.TotalSeconds : 0.0);
2786 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2796 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2806 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2815 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2822 DebugConsole.ThrowError(
"Failed send campaign state to the server (no campaign active).\n" + Environment.StackTrace.CleanupStackTrace());
2828 campaign.ClientWrite(msg);
2829 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2834 if (
string.IsNullOrWhiteSpace(command))
2836 DebugConsole.ThrowError(
"Cannot send an empty console command to the server!\n" + Environment.StackTrace.CleanupStackTrace());
2848 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2862 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2877 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2887 DebugConsole.ThrowError(
"Gamemode index out of bounds (" + modeIndex +
")\n" + Environment.StackTrace.CleanupStackTrace());
2896 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2904 saveName = Path.GetFileNameWithoutExtension(saveName);
2914 msg.WriteNetSerializableStruct(settings);
2916 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2921 if (ClientPeer ==
null) {
return; }
2932 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2947 ClientPeer.Send(msg, DeliveryMethod.Reliable);
2959 new GUIMessageBox(
"", TextManager.Get(
"campaignfiletransferinprogress"));
2962 if (button !=
null) { button.
Enabled =
false; }
2963 if (campaign !=
null) { LateCampaignJoin =
true; }
2965 if (ClientPeer ==
null) {
return false; }
2974 WriteCharacterInfo(readyToStartMsg);
2976 ClientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable);
2994 if (!GameStarted)
return false;
3011 get {
return characterInfo; }
3012 set { characterInfo = value; }
3017 get {
return myCharacter; }
3018 set { myCharacter = value; }
3025 private bool hasPermissionToUseLogButton;
3030 UpdateLogButtonVisibility();
3033 private void UpdateLogButtonVisibility()
3035 if (ShowLogButton !=
null)
3039 ShowLogButton.
Visible = hasPermissionToUseLogButton;
3044 ShowLogButton.
Visible = hasPermissionToUseLogButton && (campaign ==
null || !campaign.ShowCampaignUI);
3051 get {
return inGameHUD; }
3056 get {
return chatBox; }
3061 get {
return votingInterface; }
3074 if (
string.IsNullOrWhiteSpace(message))
3080 SendChatMessage(message);
3099 if (GUI.DisableHUD || GUI.DisableUpperHUD)
return;
3127 UpdateLogButtonVisibility();
3133 buttonContainer.Visible = !disableButtons;
3135 if (!GUI.DisableHUD && !GUI.DisableUpperHUD)
3138 chatBox.
Update(deltaTime);
3140 if (votingInterface !=
null)
3142 votingInterface.
Update(deltaTime);
3147 DebugConsole.AddWarning($
"Voting interface timed out.");
3149 votingInterface.
Remove();
3150 votingInterface =
null;
3167 if (GUI.KeyboardDispatcher.Subscriber ==
null)
3170 if (chatKeyStates.AnyHit)
3190 public void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
3192 if (GUI.DisableHUD || GUI.DisableUpperHUD)
return;
3200 ToolBox.LimitString(
3201 TextManager.GetWithVariable(
"DownloadingFile",
"[filename]", transfer.FileName).Value,
3206 MathUtils.GetBytesReadable((
long)transfer.Received) +
" / " + MathUtils.GetBytesReadable((
long)transfer.FileSize);
3219 if (endVoteCount > 0)
3223 EndVoteTickBox.
Text = $
"{endRoundVoteText} {endVoteCount}/{endVoteMax}";
3227 LocalizedString endVoteText = TextManager.GetWithVariables(
"EndRoundVotes", (
"[votes]", endVoteCount.ToString()), (
"[max]", endVoteMax.ToString()));
3228 GUI.DrawString(spriteBatch, EndVoteTickBox.
Rect.Center.ToVector2() - GUIStyle.SmallFont.MeasureString(endVoteText) / 2,
3231 font: GUIStyle.SmallFont);
3236 EndVoteTickBox.
Text = endRoundVoteText;
3242 Color textColor = Color.White;
3243 bool hideRespawnButtons =
false;
3245 if (EndRoundTimeRemaining > 0)
3247 respawnText = TextManager.GetWithVariable(
"endinground",
"[time]", ToolBox.SecondsToReadableTime(EndRoundTimeRemaining))
3248 .
Fallback(ToolBox.SecondsToReadableTime(EndRoundTimeRemaining), useDefaultLanguageIfFound:
false);
3255 respawnText = TextManager.GetWithVariable(
"RespawningIn",
"[time]", ToolBox.SecondsToReadableTime(timeLeft));
3259 respawnText = TextManager.GetWithVariables(
"RespawnWaitingForMoreDeadPlayers",
3268 respawnText = timeLeft <= 0.0f ?
3270 TextManager.GetWithVariable(
"RespawnShuttleLeavingIn",
"[time]", ToolBox.SecondsToReadableTime(timeLeft));
3271 if (timeLeft < 20.0f)
3274 float phase = (float)(Math.Sin(timeLeft * MathHelper.Pi) + 1.0f) * 0.5f;
3276 textColor = Color.Lerp(GUIStyle.Red, Color.White, 1.0f - phase);
3278 hideRespawnButtons =
true;
3282 text: respawnText.
Value, textColor: textColor,
3283 waitForNextRoundRespawn: (WaitForNextRoundRespawn ??
true), hideButtons: hideRespawnButtons);
3286 if (!ShowNetStats) {
return; }
3317 if (character ==
null) {
return false; }
3319 if (character != myCharacter)
3321 var client = previouslyConnectedClients.Find(c => c.
Character == character);
3322 if (client ==
null) {
return false; }
3324 CreateSelectionRelatedButtons(client, frame);
3332 if (client ==
null || client.
SessionId == SessionId) {
return false; }
3333 CreateSelectionRelatedButtons(client, frame);
3342 TextManager.Get(
"Mute"))
3345 OnSelected = (tickBox) => { client.
MutedLocally = tickBox.Selected;
return true; }
3348 var volumeLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 0.45f), content.RectTransform, Anchor.TopCenter), isHorizontal:
false);
3350 var volumeTextLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(1f, 0.5f), volumeLayout.RectTransform), isHorizontal:
true, childAnchor: Anchor.CenterLeft);
3351 var label =
new GUITextBlock(
new RectTransform(
new Vector2(0.6f, 1f), volumeTextLayout.RectTransform), TextManager.Get(
"VoiceChatVolume"));
3352 var percentageText =
new GUITextBlock(
new RectTransform(
new Vector2(0.4f, 1f), volumeTextLayout.RectTransform), ToolBox.GetFormattedPercentage(client.
VoiceVolume), textAlignment: Alignment.Right);
3354 var volumeSlider =
new GUIScrollBar(
new RectTransform(
new Vector2(1f, 0.5f), volumeLayout.RectTransform), barSize: 0.1f, style:
"GUISlider")
3356 Range =
new Vector2(0f, 1f),
3358 OnMoved = (_, barScroll) =>
3363 percentageText.Text = ToolBox.GetFormattedPercentage(newVolume);
3368 var buttonContainer =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 0.35f), content.RectTransform), isHorizontal:
true, childAnchor:
Anchor.BottomLeft)
3370 RelativeSpacing = 0.05f,
3374 if (!GameMain.Client.GameStarted || (GameMain.Client.Character ==
null || GameMain.Client.Character.IsDead) && (client.
Character ==
null || client.
Character.
IsDead))
3376 var messageButton =
new GUIButton(
new RectTransform(
new Vector2(1f, 0.2f), content.RectTransform,
Anchor.BottomCenter) { RelativeOffset = new Vector2(0f, buttonContainer.RectTransform.RelativeSize.Y) },
3377 TextManager.Get(
"message"), style:
"GUIButtonSmall")
3380 OnClicked = (btn, userdata) =>
3383 CoroutineManager.StartCoroutine(selectCoroutine());
3390 IEnumerable<CoroutineStatus> selectCoroutine()
3392 yield
return new WaitForSeconds(0.01f,
true);
3398 var banButton =
new GUIButton(
new RectTransform(
new Vector2(0.45f, 0.9f), buttonContainer.RectTransform),
3399 TextManager.Get(
"Ban"), style:
"GUIButtonSmall")
3402 OnClicked = (btn, userdata) => { NetLobbyScreen.BanPlayer(client);
return false; }
3407 var kickButton =
new GUIButton(
new RectTransform(
new Vector2(0.45f, 0.9f), buttonContainer.RectTransform),
3408 TextManager.Get(
"Kick"), style:
"GUIButtonSmall")
3411 OnClicked = (btn, userdata) => { NetLobbyScreen.KickPlayer(client);
return false; }
3416 var kickVoteButton =
new GUIButton(
new RectTransform(
new Vector2(0.45f, 0.9f), buttonContainer.RectTransform),
3417 TextManager.Get(
"VoteToKick"), style:
"GUIButtonSmall")
3420 OnClicked = (btn, userdata) => { VoteForKick(client); btn.Enabled =
false;
return true; }
3428 TextManager.Get(ban ?
"BanReasonPrompt" :
"KickReasonPrompt"),
3429 "",
new LocalizedString[] { TextManager.Get(
"OK"), TextManager.Get(
"Cancel") },
new Vector2(0.25f, 0.25f),
new Point(400, 260));
3433 AbsoluteSpacing = GUI.IntScale(5)
3441 GUINumberInput durationInputDays =
null, durationInputHours =
null;
3447 new GUITextBlock(
new RectTransform(
new Vector2(1f, 0.0f), labelContainer.RectTransform), TextManager.Get(
"BanDuration"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero };
3448 var buttonContent =
new GUILayoutGroup(
new RectTransform(
new Vector2(1f, 0.5f), labelContainer.RectTransform), isHorizontal:
true);
3449 permaBanTickBox =
new GUITickBox(
new RectTransform(
new Vector2(0.4f, 0.15f), buttonContent.RectTransform), TextManager.Get(
"BanPermanent"))
3454 var durationContainer =
new GUILayoutGroup(
new RectTransform(
new Vector2(0.8f, 1f), buttonContent.RectTransform), isHorizontal:
true)
3461 durationContainer.Visible = !tickBox.Selected;
3468 MaxValueFloat = 1000
3476 new GUITextBlock(
new RectTransform(
new Vector2(0.2f, 1.0f), durationContainer.RectTransform), TextManager.Get(
"Hours"));
3479 banReasonPrompt.Buttons[0].OnClicked += (btn, userData) =>
3485 TimeSpan banDuration =
new TimeSpan(durationInputDays.
IntValue, durationInputHours.IntValue, 0, 0);
3486 BanPlayer(clientName, banReasonBox.Text, banDuration);
3490 BanPlayer(clientName, banReasonBox.Text);
3495 KickPlayer(clientName, banReasonBox.Text);
3499 banReasonPrompt.Buttons[0].OnClicked += banReasonPrompt.Close;
3500 banReasonPrompt.Buttons[1].OnClicked += banReasonPrompt.Close;
3524 ClientPeer.Send(outMsg, DeliveryMethod.Reliable);
3526 WriteEventErrorData(error, expectedId, eventId, entityId);
3529 private bool eventErrorWritten;
3530 private void WriteEventErrorData(
ClientNetError error, UInt16 expectedID, UInt16 eventID, UInt16 entityID)
3532 if (eventErrorWritten) {
return; }
3533 List<string> errorLines =
new List<string>
3535 error.ToString(),
""
3540 errorLines.Add(
"SERVER OWNER");
3545 errorLines.Add(
"Expected ID: " + expectedID +
", received " + eventID);
3549 errorLines.Add(
"Event ID: " + eventID +
", entity ID " + entityID);
3552 if (GameMain.GameSession?.GameMode !=
null)
3554 errorLines.Add(
"Game mode: " + GameMain.GameSession.GameMode.Name.Value);
3555 if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
3557 errorLines.Add(
"Campaign ID: " + campaign.CampaignID);
3558 errorLines.Add(
"Campaign save ID: " + campaign.LastSaveID +
"(pending: " + campaign.PendingSaveID +
")");
3560 foreach (Mission mission
in GameMain.GameSession.Missions)
3562 errorLines.Add(
"Mission: " + mission.Prefab.Identifier);
3565 if (GameMain.GameSession?.Submarine !=
null)
3567 errorLines.Add(
"Submarine: " + GameMain.GameSession.Submarine.Info.Name);
3569 if (GameMain.NetworkMember?.RespawnManager?.RespawnShuttle !=
null)
3571 errorLines.Add(
"Respawn shuttle: " + GameMain.NetworkMember.RespawnManager.RespawnShuttle.Info.Name);
3573 if (Level.Loaded !=
null)
3575 errorLines.Add(
"Level: " + Level.Loaded.Seed +
", "
3576 +
string.Join(
"; ", Level.Loaded.EqualityCheckValues.Select(cv
3577 => cv.Key +
"=" + cv.Value.ToString(
"X"))));
3578 errorLines.Add(
"Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate);
3579 errorLines.Add(
"Entities:");
3580 foreach (Entity e
in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex))
3582 errorLines.Add(e.ErrorLine);
3584 errorLines.Add(
"Entity count after generating level: " + Level.Loaded.EntityCountAfterGenerate);
3587 errorLines.Add(
"Entity IDs:");
3588 Entity[] sortedEntities = Entity.GetEntities().OrderBy(e => e.CreationIndex).ToArray();
3589 foreach (Entity e
in sortedEntities)
3591 errorLines.Add(e.ErrorLine);
3594 if (Entity.Spawner !=
null)
3597 errorLines.Add(
"EntitySpawner events:");
3598 foreach ((Entity entity,
bool isRemoval) in Entity.Spawner.receivedEvents)
3601 (isRemoval ?
"Remove " :
"Create ") +
3603 " (" + entity.ID +
")");
3608 errorLines.Add(
"Last debug messages:");
3609 for (
int i = DebugConsole.Messages.Count - 1; i > 0 && i > DebugConsole.Messages.Count - 15; i--)
3611 errorLines.Add(
" " + DebugConsole.Messages[i].Time +
" - " + DebugConsole.Messages[i].Text);
3614 string filePath = $
"event_error_log_client_{Name}_{DateTime.UtcNow.ToShortTimeString()}.log";
3615 filePath = Path.Combine(
ServerLog.
SavePath, ToolBox.RemoveInvalidFileNameChars(filePath));
3621 File.WriteAllLines(filePath, errorLines);
3623 eventErrorWritten =
true;
3626 private static void AppendExceptionInfo(ref
string errorMsg, Exception e)
3628 if (!errorMsg.EndsWith(
"\n")) { errorMsg +=
"\n"; }
3629 errorMsg += e.Message +
"\n";
3630 var innermostException = e.GetInnermost();
3631 if (innermostException != e)
3635 errorMsg +=
"Inner exception: " + innermostException.Message +
"\n" + innermostException.StackTrace.CleanupStackTrace();
3639 errorMsg += e.StackTrace.CleanupStackTrace();
3644 public void ForceTimeOut()
3646 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)
GUITextBlock FileTransferProgressText
List< JobVariant >? JobPreferences
GUIFrame CampaignSetupFrame
GUIComponent FileTransferFrame
void SetPlayerSpeaking(Client client)
GameModePreset SelectedMode
GUIProgressBar FileTransferProgressBar
GUITextBlock FileTransferTitle
void RemovePlayer(Client client)
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
void RequestSelectSub(SubmarineInfo sub, bool isShuttle)
Tell the server to select a submarine (permission required)
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 ForceNameAndJobUpdate()
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)
void SetupLoadCampaign(string saveName)
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 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)
DateTime RespawnTime
When will the shuttle be dispatched with respawned characters
bool RespawnCountdownStarted
bool ReturnCountdownStarted
DateTime ReturnTime
When will the sub start heading back out of the level
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
static List< Submarine > Loaded
void AddTag(SubmarineTag tag)
static IEnumerable< SubmarineInfo > SavedSubmarines
void ClientRead(IReadMessage inc)
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)
static ChatKeyStates GetChatKeyStates()
CharacterTeamType PreferredTeam
static void Read(IReadMessage msg, SegmentDataReader segmentDataReader, ExceptionHandler? exceptionHandler=null)
static SegmentTableWriter< T > StartWriting(IWriteMessage msg)