Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/GameSession/GameSession.cs
1 #nullable enable
2 
3 using Barotrauma.IO;
5 using Microsoft.Xna.Framework;
6 using System;
7 using System.Collections.Generic;
8 using System.Collections.Immutable;
9 using System.Linq;
10 using System.Xml.Linq;
13 
14 namespace Barotrauma
15 {
16  partial class GameSession
17  {
18 #if DEBUG
19  public static float MinimumLoadingTime;
20 #endif
21 
22  public enum InfoFrameTab { Crew, Mission, MyCharacter, Traitor };
23 
24  public Version LastSaveVersion { get; set; } = GameMain.Version;
25 
26  public readonly EventManager EventManager;
27 
28  public GameMode? GameMode;
29 
30  //two locations used as the start and end in the MP mode
31  private Location[]? dummyLocations;
33 
34  public float RoundDuration
35  {
36  get; private set;
37  }
38 
39  public double TimeSpentCleaning, TimeSpentPainting;
40 
41  private readonly List<Mission> missions = new List<Mission>();
42  public IEnumerable<Mission> Missions { get { return missions; } }
43 
44  private readonly HashSet<Character> casualties = new HashSet<Character>();
45  public IEnumerable<Character> Casualties { get { return casualties; } }
46 
47 
49 
50  public bool IsRunning { get; private set; }
51 
52  public bool RoundEnding { get; private set; }
53 
54  public Level? Level { get; private set; }
55  public LevelData? LevelData { get; private set; }
56 
57  public bool MirrorLevel { get; private set; }
58 
59  public Map? Map
60  {
61  get
62  {
63  return (GameMode as CampaignMode)?.Map;
64  }
65  }
66 
68  {
69  get
70  {
71  return GameMode as CampaignMode;
72  }
73  }
74 
75 
77  {
78  get
79  {
80  if (Map != null) { return Map.CurrentLocation; }
81  if (dummyLocations == null)
82  {
83  dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData);
84  }
85  if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
86  return dummyLocations[0];
87  }
88  }
89 
91  {
92  get
93  {
94  if (Map != null) { return Map.SelectedLocation; }
95  if (dummyLocations == null)
96  {
97  dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData);
98  }
99  if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
100  return dummyLocations[1];
101  }
102  }
103 
104  public SubmarineInfo SubmarineInfo { get; set; }
105 
106  public List<SubmarineInfo> OwnedSubmarines = new List<SubmarineInfo>();
107 
108  public Submarine? Submarine { get; set; }
109 
110  public string? SavePath { get; set; }
111 
112  public bool TraitorsEnabled =>
113  GameMain.NetworkMember?.ServerSettings != null &&
114  GameMain.NetworkMember.ServerSettings.TraitorProbability > 0.0f;
115 
116  partial void InitProjSpecific();
117 
118  private GameSession(SubmarineInfo submarineInfo)
119  {
120  InitProjSpecific();
121  SubmarineInfo = submarineInfo;
122  GameMain.GameSession = this;
123  EventManager = new EventManager();
124  }
125 
129  public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, CampaignSettings settings, string? seed = null, MissionType missionType = MissionType.None)
130  : this(submarineInfo)
131  {
132  this.SavePath = savePath;
133  CrewManager = new CrewManager(gameModePreset.IsSinglePlayer);
134  GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, settings, missionType: missionType);
135  InitOwnedSubs(submarineInfo);
136  }
137 
141  public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string? seed = null, IEnumerable<MissionPrefab>? missionPrefabs = null)
142  : this(submarineInfo)
143  {
144  CrewManager = new CrewManager(gameModePreset.IsSinglePlayer);
145  GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, CampaignSettings.Empty, missionPrefabs: missionPrefabs);
146  InitOwnedSubs(submarineInfo);
147  }
148 
152  public GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines, XDocument doc, string saveFile) : this(submarineInfo)
153  {
154  this.SavePath = saveFile;
155  GameMain.GameSession = this;
156  XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null.");
157 
158  LastSaveVersion = doc.Root.GetAttributeVersion("version", GameMain.Version);
159 
160  foreach (var subElement in rootElement.Elements())
161  {
162  switch (subElement.Name.ToString().ToLowerInvariant())
163  {
164  case "gamemode": //legacy support
165  case "singleplayercampaign":
166 #if CLIENT
167  CrewManager = new CrewManager(true);
168  var campaign = SinglePlayerCampaign.Load(subElement);
169  campaign.LoadNewLevel();
170  GameMode = campaign;
171  InitOwnedSubs(submarineInfo, ownedSubmarines);
172 #else
173  throw new Exception("The server cannot load a single player campaign.");
174 #endif
175  break;
176  case "multiplayercampaign":
177  CrewManager = new CrewManager(false);
178  var mpCampaign = MultiPlayerCampaign.LoadNew(subElement);
179  GameMode = mpCampaign;
180  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
181  {
182  mpCampaign.LoadNewLevel();
183  InitOwnedSubs(submarineInfo, ownedSubmarines);
184  //save to ensure the campaign ID in the save file matches the one that got assigned to this campaign instance
185  SaveUtil.SaveGame(saveFile);
186  }
187  break;
188  }
189  }
190  }
191 
192  private void InitOwnedSubs(SubmarineInfo submarineInfo, List<SubmarineInfo>? ownedSubmarines = null)
193  {
194  OwnedSubmarines = ownedSubmarines ?? new List<SubmarineInfo>();
195  if (submarineInfo != null && !OwnedSubmarines.Any(s => s.Name == submarineInfo.Name))
196  {
197  OwnedSubmarines.Add(submarineInfo);
198  }
199  }
200 
201  private GameMode InstantiateGameMode(GameModePreset gameModePreset, string? seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable<MissionPrefab>? missionPrefabs = null, MissionType missionType = MissionType.None)
202  {
203  if (gameModePreset.GameModeType == typeof(CoOpMode) || gameModePreset.GameModeType == typeof(PvPMode))
204  {
205  //don't allow hidden mission types (e.g. GoTo) in single mission modes
206  var missionTypes = (MissionType[])Enum.GetValues(typeof(MissionType));
207  for (int i = 0; i < missionTypes.Length; i++)
208  {
209  if (MissionPrefab.HiddenMissionClasses.Contains(missionTypes[i]))
210  {
211  missionType &= ~missionTypes[i];
212  }
213  }
214  }
215  if (gameModePreset.GameModeType == typeof(CoOpMode))
216  {
217  return missionPrefabs != null ?
218  new CoOpMode(gameModePreset, missionPrefabs) :
219  new CoOpMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8));
220  }
221  else if (gameModePreset.GameModeType == typeof(PvPMode))
222  {
223  return missionPrefabs != null ?
224  new PvPMode(gameModePreset, missionPrefabs) :
225  new PvPMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8));
226  }
227  else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
228  {
229  var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
230  if (selectedSub != null)
231  {
232  campaign.Bank.Deduct(selectedSub.Price);
233  campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0);
234 #if SERVER
235  if (GameMain.Server?.ServerSettings?.NewCampaignDefaultSalary is { } salary)
236  {
237  campaign.Bank.SetRewardDistribution((int)Math.Round(salary, digits: 0));
238  }
239 #endif
240  }
241  return campaign;
242  }
243 #if CLIENT
244  else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
245  {
246  var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
247  if (selectedSub != null)
248  {
249  campaign.Bank.TryDeduct(selectedSub.Price);
250  campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0);
251  }
252  return campaign;
253  }
254  else if (gameModePreset.GameModeType == typeof(TutorialMode))
255  {
256  return new TutorialMode(gameModePreset);
257  }
258  else if (gameModePreset.GameModeType == typeof(TestGameMode))
259  {
260  return new TestGameMode(gameModePreset);
261  }
262 #endif
263  else if (gameModePreset.GameModeType == typeof(GameMode))
264  {
265  return new GameMode(gameModePreset);
266  }
267  else
268  {
269  throw new Exception($"Could not find a game mode of the type \"{gameModePreset.GameModeType}\"");
270  }
271  }
272 
273  public static Location[] CreateDummyLocations(LevelData levelData, LocationType? forceLocationType = null)
274  {
275  MTRandom rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
276  var forceParams = levelData?.ForceOutpostGenerationParams;
277  if (forceLocationType == null &&
278  forceParams != null && forceParams.AllowedLocationTypes.Any() && !forceParams.AllowedLocationTypes.Contains("Any".ToIdentifier()))
279  {
280  forceLocationType =
281  LocationType.Prefabs.Where(lt => forceParams.AllowedLocationTypes.Contains(lt.Identifier)).GetRandom(rand);
282  }
283  var dummyLocations = CreateDummyLocations(rand, forceLocationType);
284  List<Faction> factions = new List<Faction>();
285  foreach (var factionPrefab in FactionPrefab.Prefabs)
286  {
287  factions.Add(new Faction(new CampaignMetadata(), factionPrefab));
288  }
289  foreach (var location in dummyLocations)
290  {
291  if (location.Type.HasOutpost)
292  {
293  location.Faction = CampaignMode.GetRandomFaction(factions, rand, secondary: false);
294  location.SecondaryFaction = CampaignMode.GetRandomFaction(factions, rand, secondary: true);
295  }
296  }
297  return dummyLocations;
298  }
299 
300  public static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType = null)
301  {
302  return CreateDummyLocations(new MTRandom(ToolBox.StringToInt(seed)), forceLocationType);
303  }
304 
305  private static Location[] CreateDummyLocations(Random rand, LocationType? forceLocationType = null)
306  {
307  var dummyLocations = new Location[2];
308  for (int i = 0; i < 2; i++)
309  {
310  dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType);
311  }
312  return dummyLocations;
313  }
314 
315  public void LoadPreviousSave()
316  {
317  Submarine.Unload();
318  SaveUtil.LoadGame(SavePath ?? "");
319  }
320 
324  public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client = null)
325  {
326  if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
327  {
328  OwnedSubmarines.Add(newSubmarine);
329  }
330  else
331  {
332  // Fetch owned submarine data as the newSubmarine is just the base submarine
333  for (int i = 0; i < OwnedSubmarines.Count; i++)
334  {
335  if (OwnedSubmarines[i].Name == newSubmarine.Name)
336  {
337  newSubmarine = OwnedSubmarines[i];
338  break;
339  }
340  }
341  }
342  Campaign!.PendingSubmarineSwitch = newSubmarine;
343  Campaign!.TransferItemsOnSubSwitch = transferItems;
344  }
345 
346  public bool TryPurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
347  {
348  if (Campaign is null) { return false; }
349  int price = newSubmarine.GetPrice();
350  if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return false; }
351  if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
352  {
353  GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name);
354  OwnedSubmarines.Add(newSubmarine);
355 #if SERVER
356  (Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList);
357 #endif
358  }
359  return true;
360  }
361 
362  public bool IsSubmarineOwned(SubmarineInfo query)
363  {
364  return
365  Submarine.MainSub.Info.Name == query.Name ||
366  (OwnedSubmarines != null && OwnedSubmarines.Any(os => os.Name == query.Name));
367  }
368 
370  {
371  if (Map?.CurrentLocation == null || Campaign == null) { return false; }
372 
373  bool isRadiated = Map.CurrentLocation.IsRadiated();
374 
375  if (Level.Loaded?.EndLocation is { } endLocation)
376  {
377  isRadiated |= endLocation.IsRadiated();
378  }
379 
380  return isRadiated;
381  }
382 
383  public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams? levelGenerationParams = null)
384  {
385  if (GameMode == null) { return; }
386  LevelData? randomLevel = null;
387  foreach (Mission mission in Missions.Union(GameMode.Missions))
388  {
389  MissionPrefab missionPrefab = mission.Prefab;
390  if (missionPrefab != null &&
391  missionPrefab.AllowedLocationTypes.Any() &&
392  !missionPrefab.AllowedConnectionTypes.Any())
393  {
394  Random rand = new MTRandom(ToolBox.StringToInt(levelSeed));
395  LocationType? locationType = LocationType.Prefabs
396  .Where(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier))
397  .GetRandom(rand);
398  dummyLocations = CreateDummyLocations(levelSeed, locationType);
399 
400  if (!tryCreateFaction(mission.Prefab.RequiredLocationFaction, dummyLocations, static (loc, fac) => loc.Faction = fac))
401  {
402  tryCreateFaction(locationType.Faction, dummyLocations, static (loc, fac) => loc.Faction = fac);
403  tryCreateFaction(locationType.SecondaryFaction, dummyLocations, static (loc, fac) => loc.SecondaryFaction = fac);
404  }
405  static bool tryCreateFaction(Identifier factionIdentifier, Location[] locations, Action<Location, Faction> setter)
406  {
407  if (factionIdentifier.IsEmpty) { return false; }
408  if (!FactionPrefab.Prefabs.TryGet(factionIdentifier, out var prefab)) { return false; }
409  if (locations.Length == 0) { return false; }
410 
411  var newFaction = new Faction(metadata: null, prefab);
412  for (int i = 0; i < locations.Length; i++)
413  {
414  setter(locations[i], newFaction);
415  }
416 
417  return true;
418  }
419 
420  randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true);
421  break;
422  }
423  }
424  randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams);
425  StartRound(randomLevel);
426  }
427 
428  public void StartRound(LevelData? levelData, bool mirrorLevel = false, SubmarineInfo? startOutpost = null, SubmarineInfo? endOutpost = null)
429  {
430 #if DEBUG
431  DateTime startTime = DateTime.Now;
432 #endif
433  RoundDuration = 0.0f;
435 
436  MirrorLevel = mirrorLevel;
437  if (SubmarineInfo == null)
438  {
439  DebugConsole.ThrowError("Couldn't start game session, submarine not selected.");
440  return;
441  }
443  {
444  DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted.");
445  return;
446  }
447  if (SubmarineInfo.SubmarineElement.Elements().Count() == 0)
448  {
449  DebugConsole.ThrowError("Couldn't start game session, saved submarine is empty. The submarine file may be corrupted.");
450  return;
451  }
452 
453  Submarine.LockX = Submarine.LockY = false;
454 
455  LevelData = levelData;
456 
457  Submarine.Unload();
459  foreach (Submarine sub in Submarine.GetConnectedSubs())
460  {
461  sub.TeamID = CharacterTeamType.Team1;
462  foreach (Item item in Item.ItemList)
463  {
464  if (item.Submarine != sub) { continue; }
465  foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
466  {
467  wifiComponent.TeamID = sub.TeamID;
468  }
469  }
470  }
471 
473  foreach (Mission mission in GameMode!.Missions)
474  {
475  // setting level for missions that may involve difficulty-related submarine creation
476  mission.SetLevel(levelData);
477  }
478 
479  if (Submarine.MainSubs[1] == null)
480  {
481  var enemySubmarineInfo = GameMode is PvPMode ? SubmarineInfo : GameMode.Missions.FirstOrDefault(m => m.EnemySubmarineInfo != null)?.EnemySubmarineInfo;
482  if (enemySubmarineInfo != null)
483  {
484  Submarine.MainSubs[1] = new Submarine(enemySubmarineInfo, true);
485  }
486  }
487 
488  if (GameMain.NetworkMember?.ServerSettings?.LockAllDefaultWires ?? false)
489  {
490  List<Item> items = new List<Item>();
491  items.AddRange(Submarine.MainSubs[0].GetItems(alsoFromConnectedSubs: true));
492  if (Submarine.MainSubs[1] != null)
493  {
494  items.AddRange(Submarine.MainSubs[1].GetItems(alsoFromConnectedSubs: true));
495  }
496  foreach (Item item in items)
497  {
498  if (item.GetComponent<CircuitBox>() is { } cb)
499  {
500  cb.Locked = true;
501  }
502 
503  Wire wire = item.GetComponent<Wire>();
504  if (wire != null && !wire.NoAutoLock && wire.Connections.Any(c => c != null)) { wire.Locked = true; }
505  }
506  }
507 
508  Level? level = null;
509  if (levelData != null)
510  {
511  level = Level.Generate(levelData, mirrorLevel, StartLocation, EndLocation, startOutpost, endOutpost);
512  }
513 
514  InitializeLevel(level);
515 
516  //Clear out the cached grids and force update
517  Powered.Grids.Clear();
518 
519  casualties.Clear();
520 
521  GameAnalyticsManager.AddProgressionEvent(
522  GameAnalyticsManager.ProgressionStatus.Start,
523  GameMode?.Preset?.Identifier.Value ?? "none");
524 
525  string eventId = "StartRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
526  GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
527  GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier.Value ?? "none"));
528  GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0));
529  foreach (Mission mission in missions)
530  {
531  GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier);
532  }
533  if (Level.Loaded != null)
534  {
535  Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
537  Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
538  GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + levelId);
539  }
540  GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"));
541 #if CLIENT
542  if (GameMode is TutorialMode tutorialMode)
543  {
544  GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
546  {
547  GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
548  }
549  }
550  GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}");
551 #endif
552  var campaignMode = GameMode as CampaignMode;
553  if (campaignMode != null)
554  {
555  GameAnalyticsManager.AddDesignEvent("CampaignSettings:RadiationEnabled:" + campaignMode.Settings.RadiationEnabled);
556  GameAnalyticsManager.AddDesignEvent("CampaignSettings:WorldHostility:" + campaignMode.Settings.WorldHostility);
557  GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShowHuskWarning:" + campaignMode.Settings.ShowHuskWarning);
558  GameAnalyticsManager.AddDesignEvent("CampaignSettings:StartItemSet:" + campaignMode.Settings.StartItemSet);
559  GameAnalyticsManager.AddDesignEvent("CampaignSettings:MaxMissionCount:" + campaignMode.Settings.MaxMissionCount);
560  //log the multipliers as integers to reduce the number of distinct values
561  GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100));
562  GameAnalyticsManager.AddDesignEvent("CampaignSettings:FuelMultiplier:" + (int)(campaignMode.Settings.FuelMultiplier * 100));
563  GameAnalyticsManager.AddDesignEvent("CampaignSettings:MissionRewardMultiplier:" + (int)(campaignMode.Settings.MissionRewardMultiplier * 100));
564  GameAnalyticsManager.AddDesignEvent("CampaignSettings:CrewVitalityMultiplier:" + (int)(campaignMode.Settings.CrewVitalityMultiplier * 100));
565  GameAnalyticsManager.AddDesignEvent("CampaignSettings:NonCrewVitalityMultiplier:" + (int)(campaignMode.Settings.NonCrewVitalityMultiplier * 100));
566  GameAnalyticsManager.AddDesignEvent("CampaignSettings:OxygenMultiplier:" + (int)(campaignMode.Settings.OxygenMultiplier * 100));
567  GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100));
568  GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShipyardPriceMultiplier:" + (int)(campaignMode.Settings.ShipyardPriceMultiplier * 100));
569  GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShopPriceMultiplier:" + (int)(campaignMode.Settings.ShopPriceMultiplier * 100));
570 
571  bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData!.Biome);
572  if (firstTimeInBiome)
573  {
574  GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime);
575  GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels);
576  }
577  if (GameMain.NetworkMember?.ServerSettings is { } serverSettings)
578  {
579  GameAnalyticsManager.AddDesignEvent("ServerSettings:RespawnMode:" + serverSettings.RespawnMode);
580  GameAnalyticsManager.AddDesignEvent("ServerSettings:IronmanMode:" + serverSettings.IronmanMode);
581  GameAnalyticsManager.AddDesignEvent("ServerSettings:AllowBotTakeoverOnPermadeath:" + serverSettings.AllowBotTakeoverOnPermadeath);
582  }
583  }
584 
585 #if DEBUG
586  double startDuration = (DateTime.Now - startTime).TotalSeconds;
587  if (startDuration < MinimumLoadingTime)
588  {
589  int sleepTime = (int)((MinimumLoadingTime - startDuration) * 1000);
590  DebugConsole.NewMessage($"Stalling round start by {sleepTime / 1000.0f} s (minimum loading time set to {MinimumLoadingTime})...", Color.Magenta);
591  System.Threading.Thread.Sleep(sleepTime);
592  }
593 #endif
594 #if CLIENT
595  if (campaignMode != null && levelData != null) { AchievementManager.OnBiomeDiscovered(levelData.Biome); }
596 
597  var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary;
598  if (existingRoundSummary?.ContinueButton != null)
599  {
600  existingRoundSummary.ContinueButton.Visible = true;
601  }
602 
603  CharacterHUD.ClearBossProgressBars();
604 
606 
607  if (GameMode is not TutorialMode && GameMode is not TestGameMode)
608  {
609  GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
610  if (EndLocation != null && levelData != null)
611  {
612  GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false);
613  GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.DisplayName), Color.CadetBlue, playSound: false);
614  var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage);
615  if (missionsToShow.Count() > 1)
616  {
617  string joinedMissionNames = string.Join(", ", missions.Select(m => m.Name));
618  GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false);
619  }
620  else
621  {
622  var mission = missionsToShow.FirstOrDefault();
623  GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), mission?.Name ?? TextManager.Get("None")), Color.CadetBlue, playSound: false);
624  }
625  }
626  else
627  {
628  GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.DisplayName), Color.CadetBlue, playSound: false);
629  }
630  }
631 
632  ReadyCheck.ReadyCheckCooldown = DateTime.MinValue;
633  GUI.PreventPauseMenuToggle = false;
634  HintManager.OnRoundStarted();
635 
636  GameMain.LuaCs.Hook.Call("roundStart");
637  EnableEventLogNotificationIcon(enabled: false);
638 #endif
639  if (campaignMode is { ItemsRelocatedToMainSub: true })
640  {
641 #if SERVER
642  GameMain.Server.SendChatMessage(TextManager.Get("itemrelocated").Value, ChatMessageType.ServerMessageBoxInGame);
643 #else
644  if (campaignMode.IsSinglePlayer)
645  {
646  new GUIMessageBox(string.Empty, TextManager.Get("itemrelocated"));
647  }
648 #endif
649  campaignMode.ItemsRelocatedToMainSub = false;
650  }
651 
653  if (campaignMode is { DivingSuitWarningShown: false } &&
654  Level.Loaded != null && Level.Loaded.GetRealWorldDepth(0) > 4000)
655  {
656 #if CLIENT
657  CoroutineManager.Invoke(() => new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("hint.upgradedivingsuits")), delay: 5.0f);
658 #endif
659  campaignMode.DivingSuitWarningShown = true;
660  }
661  }
662 
663  private void InitializeLevel(Level? level)
664  {
665  //make sure no status effects have been carried on from the next round
666  //(they should be stopped in EndRound, this is a safeguard against cases where the round is ended ungracefully)
667  StatusEffect.StopAll();
668 
669 #if CLIENT
670  GameMain.LightManager.LosEnabled = (GameMain.Client == null || GameMain.Client.CharacterInfo != null) && !GameMain.DevMode;
671  if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; }
672  if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; }
673 #endif
674  LevelData = level?.LevelData;
675  Level = level;
676 
677  PlaceSubAtStart(Level);
678 
679  foreach (var sub in Submarine.Loaded)
680  {
681  // TODO: Currently there's no need to check these on ruins, but that might change -> Could maybe just check if the body is static?
682  if (sub.Info.IsOutpost || sub.Info.IsBeacon || sub.Info.IsWreck)
683  {
684  sub.DisableObstructedWayPoints();
685  }
686  }
687 
688  Entity.Spawner = new EntitySpawner();
689 
690  if (GameMode != null && Submarine != null)
691  {
692  missions.Clear();
693  missions.AddRange(GameMode.Missions);
694  GameMode.Start();
695  foreach (Mission mission in missions)
696  {
697  int prevEntityCount = Entity.GetEntities().Count;
698  mission.Start(Level.Loaded);
699  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count != prevEntityCount)
700  {
701  DebugConsole.ThrowError(
702  $"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " +
703  "The clients should not instantiate entities themselves when starting the mission," +
704  " but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial.");
705  }
706  }
707 
708 #if CLIENT
709  ObjectiveManager.ResetObjectives();
710 #endif
711  EventManager?.StartRound(Level.Loaded);
712  AchievementManager.OnStartRound();
713 
714  GameMode.ShowStartMessage();
715 
716  if (GameMain.NetworkMember == null)
717  {
718  //only place items and corpses here in single player
719  //the server does this after loading the respawn shuttle
720  if (Level != null)
721  {
722  Level.SpawnNPCs();
723  Level.SpawnCorpses();
724  Level.PrepareBeaconStation();
725  }
726  AutoItemPlacer.SpawnItems(Campaign?.Settings.StartItemSet);
727  }
728  if (GameMode is MultiPlayerCampaign mpCampaign)
729  {
730  mpCampaign.UpgradeManager.ApplyUpgrades();
731  mpCampaign.UpgradeManager.SanityCheckUpgrades();
732  }
733  }
734 
735  CreatureMetrics.RecentlyEncountered.Clear();
736 
737  GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition;
738  RoundDuration = 0.0f;
739  GameMain.ResetFrameTime();
740  IsRunning = true;
741  }
742 
743  public void PlaceSubAtStart(Level? level)
744  {
745  if (level == null || Submarine == null)
746  {
747  Submarine?.SetPosition(Vector2.Zero);
748  return;
749  }
750 
751  var originalSubPos = Submarine.WorldPosition;
752  var spawnPoint = WayPoint.WayPointList.Find(wp => wp.SpawnType.HasFlag(SpawnType.Submarine) && wp.Submarine == level.StartOutpost);
753  if (spawnPoint != null)
754  {
755  //pre-determine spawnpoint, just use it directly
756  Submarine.SetPosition(spawnPoint.WorldPosition);
759  }
760  else if (level.StartOutpost != null)
761  {
762  //start by placing the sub below the outpost
764  Rectangle subBorders = Submarine.GetDockedBorders();
765 
768  new Vector2(0.0f, outpostBorders.Height / 2 + subBorders.Height / 2));
769 
770  //find the port that's the nearest to the outpost and dock if one is found
771  float closestDistance = 0.0f;
772  DockingPort? myPort = null, outPostPort = null;
773  foreach (DockingPort port in DockingPort.List)
774  {
775  if (port.IsHorizontal || port.Docked) { continue; }
776  if (port.Item.Submarine == level.StartOutpost)
777  {
778  if (port.DockingTarget == null || (outPostPort != null && !outPostPort.MainDockingPort && port.MainDockingPort))
779  {
780  outPostPort = port;
781  }
782  continue;
783  }
784  if (port.Item.Submarine != Submarine) { continue; }
785 
786  //the submarine port has to be at the top of the sub
787  if (port.Item.WorldPosition.Y < Submarine.WorldPosition.Y) { continue; }
788 
789  float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition);
790  if ((myPort == null || dist < closestDistance || port.MainDockingPort) && !(myPort?.MainDockingPort ?? false))
791  {
792  myPort = port;
793  closestDistance = dist;
794  }
795  }
796 
797  if (myPort != null && outPostPort != null)
798  {
799  Vector2 portDiff = myPort.Item.WorldPosition - Submarine.WorldPosition;
800  Vector2 spawnPos = (outPostPort.Item.WorldPosition - portDiff) - Vector2.UnitY * outPostPort.DockedDistance;
801 
802  bool startDocked = level.Type == LevelData.LevelType.Outpost;
803 #if CLIENT
804  startDocked |= GameMode is TutorialMode;
805 #endif
806  if (startDocked)
807  {
808  Submarine.SetPosition(spawnPos);
809  myPort.Dock(outPostPort);
810  myPort.Lock(isNetworkMessage: true, applyEffects: false);
811  }
812  else
813  {
814  Submarine.SetPosition(spawnPos - Vector2.UnitY * 100.0f);
817  }
818  }
819  else
820  {
823  }
824 
825  }
826  else
827  {
831  }
832 
833  // Make sure that linked subs which are NOT docked to the main sub
834  // (but still close enough to NOT be considered as 'left behind')
835  // are also moved to keep their relative position to the main sub
836  var linkedSubs = MapEntity.MapEntityList.FindAll(me => me is LinkedSubmarine);
837  foreach (LinkedSubmarine ls in linkedSubs)
838  {
839  if (ls.Sub == null || ls.Submarine != Submarine) { continue; }
840  if (!ls.LoadSub || ls.Sub.DockedTo.Contains(Submarine)) { continue; }
841  if (Submarine.Info.LeftBehindDockingPortIDs.Contains(ls.OriginalLinkedToID)) { continue; }
842  if (ls.Sub.Info.SubmarineElement.Attribute("location") != null) { continue; }
844  }
845  }
846 
847  public void Update(float deltaTime)
848  {
849  RoundDuration += deltaTime;
850  EventManager?.Update(deltaTime);
851  GameMode?.Update(deltaTime);
852  //backwards for loop because the missions may get completed and removed from the list in Update()
853  for (int i = missions.Count - 1; i >= 0; i--)
854  {
855  missions[i].Update(deltaTime);
856  }
857  UpdateProjSpecific(deltaTime);
858  }
859 
860  public Mission? GetMission(int index)
861  {
862  if (index < 0 || index >= missions.Count) { return null; }
863  return missions[index];
864  }
865 
866  public int GetMissionIndex(Mission mission)
867  {
868  return missions.IndexOf(mission);
869  }
870 
871  public void EnforceMissionOrder(List<Identifier> missionIdentifiers)
872  {
873  List<Mission> sortedMissions = new List<Mission>();
874  foreach (Identifier missionId in missionIdentifiers)
875  {
876  var matchingMission = missions.Find(m => m.Prefab.Identifier == missionId);
877  if (matchingMission == null) { continue; }
878  sortedMissions.Add(matchingMission);
879  missions.Remove(matchingMission);
880  }
881  missions.AddRange(sortedMissions);
882  }
883 
884  partial void UpdateProjSpecific(float deltaTime);
885 
894  public static ImmutableHashSet<Character> GetSessionCrewCharacters(CharacterType type)
895  {
896  var result = GameMain.LuaCs.Hook.Call<Character[]?>("getSessionCrewCharacters", type);
897  if (result != null) return ImmutableHashSet.Create(result);
898 
899  if (GameMain.GameSession?.CrewManager is not { } crewManager) { return ImmutableHashSet<Character>.Empty; }
900 
901  IEnumerable<Character> players;
902  IEnumerable<Character> bots;
903  HashSet<Character> characters = new HashSet<Character>();
904 
905 #if SERVER
906  players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead);
907  bots = crewManager.GetCharacters().Where(c => !c.IsRemotePlayer);
908 #elif CLIENT
909  players = crewManager.GetCharacters().Where(static c => c.IsPlayer);
910  bots = crewManager.GetCharacters().Where(static c => c.IsBot);
911 #endif
912  if (type.HasFlag(CharacterType.Bot))
913  {
914  foreach (Character bot in bots) { characters.Add(bot); }
915  }
916 
917  if (type.HasFlag(CharacterType.Player))
918  {
919  foreach (Character player in players) { characters.Add(player); }
920  }
921 
922  return characters.ToImmutableHashSet();
923  }
924 
925 #if SERVER
926  private double LastEndRoundErrorMessageTime;
927 #endif
928 
929  public void EndRound(string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null)
930  {
931  RoundEnding = true;
932 
933 #if CLIENT
934  GameMain.LuaCs.Hook.Call("roundEnd");
935 #endif
936  //Clear the grids to allow for garbage collection
937  Powered.Grids.Clear();
938  Powered.ChangedConnections.Clear();
939 
940  try
941  {
943 
944  ImmutableHashSet<Character> crewCharacters = GetSessionCrewCharacters(CharacterType.Both);
945  int prevMoney = GetAmountOfMoney(crewCharacters);
946  foreach (Mission mission in missions)
947  {
948  mission.End();
949  }
950 
951  foreach (Character character in crewCharacters)
952  {
953  character.CheckTalents(AbilityEffectType.OnRoundEnd);
954  }
955 
956  if (missions.Any())
957  {
958  if (missions.Any(m => m.Completed))
959  {
960  foreach (Character character in crewCharacters)
961  {
962  character.CheckTalents(AbilityEffectType.OnAnyMissionCompleted);
963  }
964  }
965  if (missions.All(m => m.Completed))
966  {
967  foreach (Character character in crewCharacters)
968  {
969  character.CheckTalents(AbilityEffectType.OnAllMissionsCompleted);
970  }
971  }
972  }
973 
974  GameMain.LuaCs.Hook.Call("missionsEnded", missions);
975 
976 #if CLIENT
977  if (GUI.PauseMenuOpen)
978  {
979  GUI.TogglePauseMenu();
980  }
981  if (IsTabMenuOpen)
982  {
983  ToggleTabMenu();
984  }
985  DeathPrompt?.Close();
986  DeathPrompt.CloseBotPanel();
987 
988  GUI.PreventPauseMenuToggle = true;
989 
990  if (GameMode is not TestGameMode && Screen.Selected == GameMain.GameScreen && RoundSummary != null && transitionType != CampaignMode.TransitionType.End)
991  {
992  GUI.ClearMessages();
993  GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary);
994  GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, transitionType, traitorResults);
995  GUIMessageBox.MessageBoxes.Add(summaryFrame);
996  RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; };
997  }
998 
1001  GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb));
1002  ObjectiveManager.ResetUI();
1004 #endif
1005  AchievementManager.OnRoundEnded(this);
1006 
1007 #if SERVER
1008  GameMain.Server?.TraitorManager?.EndRound();
1009 #endif
1010  GameMode?.End(transitionType);
1014  IsRunning = false;
1015 
1016 #if CLIENT
1017  bool success = CrewManager!.GetCharacters().Any(c => !c.IsDead);
1018 #else
1019  bool success =
1020  GameMain.Server != null &&
1021  GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
1022 #endif
1023  GameAnalyticsManager.AddProgressionEvent(
1024  success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail,
1025  GameMode?.Preset.Identifier.Value ?? "none",
1026  RoundDuration);
1027  string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
1028  LogEndRoundStats(eventId, traitorResults);
1029  if (GameMode is CampaignMode campaignMode)
1030  {
1031  GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney);
1032  campaignMode.TotalPlayTime += RoundDuration;
1033  }
1034 #if CLIENT
1035  HintManager.OnRoundEnded();
1036 #endif
1037  missions.Clear();
1038  }
1039  catch (Exception e)
1040  {
1041  string errorMsg = "Unknown error while ending the round.";
1042  DebugConsole.ThrowError(errorMsg, e);
1043  GameAnalyticsManager.AddErrorEventOnce("GameSession.EndRound:UnknownError", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.StackTrace);
1044 #if SERVER
1045  if (Timing.TotalTime > LastEndRoundErrorMessageTime + 1.0)
1046  {
1047  GameMain.Server?.SendChatMessage(errorMsg + "\n" + e.StackTrace, Networking.ChatMessageType.Error);
1048  LastEndRoundErrorMessageTime = Timing.TotalTime;
1049  }
1050 #endif
1051  }
1052  finally
1053  {
1054  RoundEnding = false;
1055  }
1056 
1057  int GetAmountOfMoney(IEnumerable<Character> crew)
1058  {
1059  if (GameMode is not CampaignMode campaign) { return 0; }
1060 
1061  return GameMain.NetworkMember switch
1062  {
1063  null => campaign.Bank.Balance,
1064  _ => crew.Sum(c => c.Wallet.Balance) + campaign.Bank.Balance
1065  };
1066  }
1067  }
1068 
1069  public void LogEndRoundStats(string eventId, TraitorManager.TraitorResults? traitorResults = null)
1070  {
1071  if (Submarine.MainSub?.Info?.IsVanillaSubmarine() ?? false)
1072  {
1073  //don't log modded subs, that's a ton of extra data to collect
1074  GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration);
1075  }
1076  GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration);
1077  GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0), RoundDuration);
1078  foreach (Mission mission in missions)
1079  {
1080  GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), RoundDuration);
1081  }
1082  if (!ContentPackageManager.ModsEnabled)
1083  {
1084  if (Level.Loaded != null)
1085  {
1086  Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
1088  Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
1089  GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), RoundDuration);
1090  GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration);
1091  }
1092 
1093  //disabled for now, we're collecting too many events and this is information we don't need atm
1094  /*if (Submarine.MainSub != null)
1095  {
1096  Dictionary<ItemPrefab, int> submarineInventory = new Dictionary<ItemPrefab, int>();
1097  foreach (Item item in Item.ItemList)
1098  {
1099  var rootContainer = item.RootContainer ?? item;
1100  if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; }
1101  if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; }
1102 
1103  var holdable = item.GetComponent<Holdable>();
1104  if (holdable == null || holdable.Attached) { continue; }
1105  var wire = item.GetComponent<Wire>();
1106  if (wire != null && wire.Connections.Any(c => c != null)) { continue; }
1107 
1108  if (!submarineInventory.ContainsKey(item.Prefab))
1109  {
1110  submarineInventory.Add(item.Prefab, 0);
1111  }
1112  submarineInventory[item.Prefab]++;
1113  }
1114  foreach (var subItem in submarineInventory)
1115  {
1116  GameAnalyticsManager.AddDesignEvent(eventId + "SubmarineInventory:" + subItem.Key.Identifier, subItem.Value);
1117  }
1118  }*/
1119  }
1120 
1121  if (traitorResults.HasValue)
1122  {
1123  GameAnalyticsManager.AddDesignEvent($"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{traitorResults.Value.ObjectiveSuccessful}");
1124  GameAnalyticsManager.AddDesignEvent($"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{(traitorResults.Value.VotedCorrectTraitor ? "TraitorIdentifier" : "TraitorUnidentified")}");
1125  }
1126 
1127  foreach (Character c in GetSessionCrewCharacters(CharacterType.Both))
1128  {
1129  foreach (var itemSelectedDuration in c.ItemSelectedDurations)
1130  {
1131  string characterType = "Unknown";
1132  if (c.IsBot)
1133  {
1134  characterType = "Bot";
1135  }
1136  else if (c.IsPlayer)
1137  {
1138  characterType = "Player";
1139  }
1140  GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier.Value ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value);
1141  }
1142  }
1143 #if CLIENT
1144  if (GameMode is TutorialMode tutorialMode)
1145  {
1146  GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
1147  if (GameMain.IsFirstLaunch)
1148  {
1149  GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
1150  }
1151  }
1152  GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentCleaning", TimeSpentCleaning);
1153  GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentPainting", TimeSpentPainting);
1154  TimeSpentCleaning = TimeSpentPainting = 0.0;
1155 #endif
1156  }
1157 
1158  public void KillCharacter(Character character)
1159  {
1160  if (CrewManager != null && CrewManager.GetCharacters().Contains(character))
1161  {
1162  casualties.Add(character);
1163  }
1164 #if CLIENT
1165  CrewManager?.KillCharacter(character);
1166 #endif
1167  }
1168 
1169  public void ReviveCharacter(Character character)
1170  {
1171  casualties.Remove(character);
1172 #if CLIENT
1173  CrewManager?.ReviveCharacter(character);
1174 #endif
1175  }
1176 
1177  public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackageNames, out LocalizedString errorMsg)
1178  {
1179  errorMsg = "";
1180  //no known content packages, must be an older save file
1181  if (!contentPackageNames.Any()) { return true; }
1182 
1183  List<string> missingPackages = new List<string>();
1184  foreach (string packageName in contentPackageNames)
1185  {
1186  if (!ContentPackageManager.EnabledPackages.All.Any(cp => cp.NameMatches(packageName)))
1187  {
1188  missingPackages.Add(packageName);
1189  }
1190  }
1191  List<string> excessPackages = new List<string>();
1192  foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All)
1193  {
1194  if (!cp.HasMultiplayerSyncedContent) { continue; }
1195  if (!contentPackageNames.Any(p => cp.NameMatches(p)))
1196  {
1197  excessPackages.Add(cp.Name);
1198  }
1199  }
1200 
1201  bool orderMismatch = false;
1202  if (missingPackages.Count == 0 && missingPackages.Count == 0)
1203  {
1204  var enabledPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToImmutableArray();
1205  for (int i = 0; i < contentPackageNames.Count && i < enabledPackages.Length; i++)
1206  {
1207  if (!enabledPackages[i].NameMatches(contentPackageNames[i]))
1208  {
1209  orderMismatch = true;
1210  break;
1211  }
1212  }
1213  }
1214 
1215  if (!orderMismatch && missingPackages.Count == 0 && excessPackages.Count == 0) { return true; }
1216 
1217  if (missingPackages.Count == 1)
1218  {
1219  errorMsg = TextManager.GetWithVariable("campaignmode.missingcontentpackage", "[missingcontentpackage]", missingPackages[0]);
1220  }
1221  else if (missingPackages.Count > 1)
1222  {
1223  errorMsg = TextManager.GetWithVariable("campaignmode.missingcontentpackages", "[missingcontentpackages]", string.Join(", ", missingPackages));
1224  }
1225  if (excessPackages.Count == 1)
1226  {
1227  if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
1228  errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackage", "[incompatiblecontentpackage]", excessPackages[0]);
1229  }
1230  else if (excessPackages.Count > 1)
1231  {
1232  if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
1233  errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackages", "[incompatiblecontentpackages]", string.Join(", ", excessPackages));
1234  }
1235  if (orderMismatch)
1236  {
1237  if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
1238  errorMsg += TextManager.GetWithVariable("campaignmode.contentpackageordermismatch", "[loadorder]", string.Join(", ", contentPackageNames));
1239  }
1240 
1241  return false;
1242  }
1243 
1244  public void Save(string filePath)
1245  {
1246  if (!(GameMode is CampaignMode))
1247  {
1248  throw new NotSupportedException("GameSessions can only be saved when playing in a campaign mode.");
1249  }
1250 
1251  XDocument doc = new XDocument(new XElement("Gamesession"));
1252  XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null.");
1253 
1254  rootElement.Add(new XAttribute("savetime", SerializableDateTime.UtcNow.ToUnixTime()));
1255  #warning TODO: after this gets on main, replace savetime with the commented line
1256  //rootElement.Add(new XAttribute("savetime", SerializableDateTime.LocalNow));
1257 
1258  LastSaveVersion = GameMain.Version;
1259  rootElement.Add(new XAttribute("version", GameMain.Version));
1260  if (Submarine?.Info != null && !Submarine.Removed && Campaign != null)
1261  {
1262  bool hasNewPendingSub = Campaign.PendingSubmarineSwitch != null &&
1263  Campaign.PendingSubmarineSwitch.MD5Hash.StringRepresentation != Submarine.Info.MD5Hash.StringRepresentation;
1264  if (hasNewPendingSub)
1265  {
1266  Campaign.SwitchSubs();
1267  }
1268  }
1269  rootElement.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name));
1270  if (OwnedSubmarines != null)
1271  {
1272  List<string> ownedSubmarineNames = new List<string>();
1273  var ownedSubsElement = new XElement("ownedsubmarines");
1274  rootElement.Add(ownedSubsElement);
1275  foreach (var ownedSub in OwnedSubmarines)
1276  {
1277  ownedSubsElement.Add(new XElement("sub", new XAttribute("name", ownedSub.Name)));
1278  }
1279  }
1280  if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); }
1281  rootElement.Add(new XAttribute("selectedcontentpackagenames",
1282  string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Name.Replace("|", @"\|")))));
1283 
1284  ((CampaignMode)GameMode).Save(doc.Root);
1285 
1286  doc.SaveSafe(filePath, throwExceptions: true);
1287  }
1288 
1289  /*public void Load(XElement saveElement)
1290  {
1291  foreach (var subElement in saveElement.Elements())
1292  {
1293  switch (subElement.Name.ToString().ToLowerInvariant())
1294  {
1295 #if CLIENT
1296  case "gamemode": //legacy support
1297  case "singleplayercampaign":
1298  GameMode = SinglePlayerCampaign.Load(subElement);
1299  break;
1300 #endif
1301  case "multiplayercampaign":
1302  if (!(GameMode is MultiPlayerCampaign mpCampaign))
1303  {
1304  DebugConsole.ThrowError("Error while loading a save file: the save file is for a multiplayer campaign but the current gamemode is " + GameMode.GetType().ToString());
1305  break;
1306  }
1307 
1308  mpCampaign.Load(subElement);
1309  break;
1310  }
1311  }
1312  }*/
1313 
1314  }
1315 }
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
static void ClearAllEffects()
Removes all the effects of the prefab (including the sounds and other assets defined in them)....
static void LoadAllEffectsAndTreatmentSuitabilities()
Should be called before each round: loads all StatusEffects and refreshes treatment suitabilities.
Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their ControlledOutpostPercentage
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, RagdollParams ragdoll=null, bool spawnInitialItems=true)
Create a new character
Dictionary< ItemPrefab, double > ItemSelectedDurations
bool NameMatches(Identifier name)
bool HasMultiplayerSyncedContent
Does the content package include some content that needs to match between all players in multiplayer.
Responsible for keeping track of the characters in the player crew, saving and loading their orders,...
void KillCharacter(Character killedCharacter, bool resetCrewListIndex=true)
Submarine Submarine
Definition: Entity.cs:53
OnClickedHandler OnClicked
Definition: GUIButton.cs:16
static readonly List< GUIComponent > MessageBoxes
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetLobbyScreen NetLobbyScreen
Definition: GameMain.cs:55
static bool IsFirstLaunch
Definition: GameMain.cs:138
static readonly Version Version
Definition: GameMain.cs:46
static GameScreen GameScreen
Definition: GameMain.cs:52
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static LuaCsSetup LuaCs
Definition: GameMain.cs:26
virtual void End(CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None)
readonly bool IsSinglePlayer
readonly Identifier Identifier
void StartRound(LevelData? levelData, bool mirrorLevel=false, SubmarineInfo? startOutpost=null, SubmarineInfo? endOutpost=null)
static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType=null)
void LogEndRoundStats(string eventId, TraitorManager.TraitorResults? traitorResults=null)
GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, CampaignSettings settings, string? seed=null, MissionType missionType=MissionType.None)
Start a new GameSession. Will be saved to the specified save path (if playing a game mode that can be...
void StartRound(string levelSeed, float? difficulty=null, LevelGenerationParams? levelGenerationParams=null)
void EnforceMissionOrder(List< Identifier > missionIdentifiers)
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
static bool IsCompatibleWithEnabledContentPackages(IList< string > contentPackageNames, out LocalizedString errorMsg)
bool TryPurchaseSubmarine(SubmarineInfo newSubmarine, Client? client=null)
GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string? seed=null, IEnumerable< MissionPrefab >? missionPrefabs=null)
Start a new GameSession with a specific pre-selected mission.
static Location[] CreateDummyLocations(LevelData levelData, LocationType? forceLocationType=null)
GameSession(SubmarineInfo submarineInfo, List< SubmarineInfo > ownedSubmarines, XDocument doc, string saveFile)
Load a game session from the specified XML document. The session will be saved to the specified path.
void EndRound(string endMessage, CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults=null)
void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client=null)
Switch to another submarine. The sub is loaded when the next round starts.
static readonly List< Item > ItemList
JobPrefab Prefab
Definition: Job.cs:18
static LevelData CreateRandom(string seed="", float? difficulty=null, LevelGenerationParams generationParams=null, bool requireOutpost=false)
Definition: LevelData.cs:267
OutpostGenerationParams ForceOutpostGenerationParams
Definition: LevelData.cs:44
readonly string Seed
Definition: LevelData.cs:21
readonly Biome Biome
Definition: LevelData.cs:25
float GetRealWorldDepth(float worldPositionY)
Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav termina...
static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost=null, SubmarineInfo endOutpost=null)
static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType=null, IEnumerable< Location > existingLocations=null)
Definition: Location.cs:739
LocalizedString DisplayName
Definition: Location.cs:58
Identifier Faction
If set, forces the location to be assigned to this faction. Set to "None" if you don't want the locat...
Definition: LocationType.cs:92
Identifier SecondaryFaction
If set, forces the location to be assigned to this secondary faction. Set to "None" if you don't want...
Definition: LocationType.cs:97
static readonly PrefabCollection< LocationType > Prefabs
Definition: LocationType.cs:15
object Call(string name, params object[] args)
Mersenne Twister based random
Definition: MTRandom.cs:9
static readonly List< MapEntity > MapEntityList
List< LocationConnection > Connections
readonly string StringRepresentation
Definition: Md5Hash.cs:32
void End()
End the mission and give a reward if it was completed successfully
readonly List<(Identifier from, Identifier to)> AllowedConnectionTypes
The mission can only be received when travelling from a location of the first type to a location of t...
readonly Identifier RequiredLocationFaction
The mission can only happen in locations owned by this faction. In the mission mode,...
readonly List< Identifier > AllowedLocationTypes
The mission can only be received in these location types
readonly Identifier Identifier
Definition: Prefab.cs:34
GUIFrame CreateSummaryFrame(GameSession gameSession, string endMessage, CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults=null)
Definition: RoundSummary.cs:50
static SinglePlayerCampaign Load(XElement element)
Load a previously saved single player campaign from xml
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
List< Item > GetItems(bool alsoFromConnectedSubs)
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
Rectangle GetDockedBorders(bool allowDifferentTeam=true)
Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
void SetPosition(Vector2 position, List< Submarine > checkd=null, bool forceUndockFromStaticSubmarines=true)
Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize=null, float subDockingPortOffset=0.0f, int verticalMoveDir=0)
Attempt to find a spawn position close to the specified position where the sub doesn't collide with w...
int GetPrice(Location location=null, ImmutableHashSet< Character > characterList=null)
static void OnRoundEnded()
Definition: TabMenu.cs:255
AbilityEffectType
Definition: Enums.cs:125
CharacterType
Definition: Enums.cs:685
DateTime wrapper that tries to offer a reliable string representation that's also human-friendly
static SerializableDateTime UtcNow