Server LuaCsForBarotrauma
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;
14 
15 namespace Barotrauma
16 {
17  internal readonly record struct PerkCollection(
18  ImmutableArray<DisembarkPerkPrefab> Team1Perks,
19  ImmutableArray<DisembarkPerkPrefab> Team2Perks)
20  {
21  public static readonly PerkCollection Empty = new PerkCollection(ImmutableArray<DisembarkPerkPrefab>.Empty, ImmutableArray<DisembarkPerkPrefab>.Empty);
22 
23  public void ApplyAll(IReadOnlyCollection<Character> team1Characters, IReadOnlyCollection<Character> team2Characters)
24  {
25  // Usually there should only be 1 mission active on pvp and mission modes
26  bool anyMissionDoesNotLoadSubs = GameMain.GameSession.Missions.Any(static m => !m.Prefab.LoadSubmarines);
27 
28  foreach (var team1Perk in Team1Perks)
29  {
30  GameAnalyticsManager.AddDesignEvent("DisembarkPerk:" + team1Perk.Identifier);
31  foreach (PerkBase behavior in team1Perk.PerkBehaviors)
32  {
33  if (anyMissionDoesNotLoadSubs && !behavior.CanApplyWithoutSubmarine()) { continue; }
34 #if CLIENT
35  if (behavior.Simulation == PerkSimulation.ServerOnly) { continue; }
36 #endif
37  behavior.ApplyOnRoundStart(team1Characters, Submarine.MainSubs[0]);
38  }
39  }
40 
41  if (Submarine.MainSubs[1] is not null)
42  {
43  foreach (var team2Perk in Team2Perks)
44  {
45  GameAnalyticsManager.AddDesignEvent("DisembarkPerk:" + team2Perk.Identifier);
46  foreach (PerkBase behavior in team2Perk.PerkBehaviors)
47  {
48  if (anyMissionDoesNotLoadSubs && !behavior.CanApplyWithoutSubmarine()) { continue; }
49 #if CLIENT
50  if (behavior.Simulation == PerkSimulation.ServerOnly) { continue; }
51 #endif
52  behavior.ApplyOnRoundStart(team2Characters, Submarine.MainSubs[1]);
53  }
54  }
55  }
56  }
57  }
58 
59  partial class GameSession
60  {
61 #if DEBUG
62  public static float MinimumLoadingTime;
63 #endif
64 
65  public enum InfoFrameTab { Crew, Mission, MyCharacter, Traitor };
66 
67  public Version LastSaveVersion { get; set; } = GameMain.Version;
68 
69  public readonly EventManager EventManager;
70 
71  public GameMode? GameMode;
72 
73  //two locations used as the start and end in the MP mode
74  private Location[]? dummyLocations;
76 
77  public float RoundDuration
78  {
79  get; private set;
80  }
81 
82  public double TimeSpentCleaning, TimeSpentPainting;
83 
84  private readonly List<Mission> missions = new List<Mission>();
85  public IEnumerable<Mission> Missions { get { return missions; } }
86 
87  private readonly HashSet<Character> casualties = new HashSet<Character>();
88  public IEnumerable<Character> Casualties { get { return casualties; } }
89 
95  private Dictionary<Option<AccountId>, int> permadeathsPerAccount = new Dictionary<Option<AccountId>, int>();
96  public void IncrementPermadeath(Option<AccountId> accountId)
97  {
98  permadeathsPerAccount[accountId] = permadeathsPerAccount.GetValueOrDefault(accountId, 0) + 1;
99  }
100  public int PermadeathCountForAccount(Option<AccountId> accountId)
101  {
102  return permadeathsPerAccount.GetValueOrDefault(accountId, 0);
103  }
104 
106 
110  public bool IsRunning { get; private set; }
111 
112  public bool RoundEnding { get; private set; }
113 
114  public Level? Level { get; private set; }
115  public LevelData? LevelData { get; private set; }
116 
117  public bool MirrorLevel { get; private set; }
118 
119  public Map? Map
120  {
121  get
122  {
123  return (GameMode as CampaignMode)?.Map;
124  }
125  }
126 
128  {
129  get
130  {
131  return GameMode as CampaignMode;
132  }
133  }
134 
135 
137  {
138  get
139  {
140  if (Map != null) { return Map.CurrentLocation; }
141  if (dummyLocations == null)
142  {
143  dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData);
144  }
145  if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
146  return dummyLocations[0];
147  }
148  }
149 
151  {
152  get
153  {
154  if (Map != null) { return Map.SelectedLocation; }
155  if (dummyLocations == null)
156  {
157  dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData);
158  }
159  if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
160  return dummyLocations[1];
161  }
162  }
163 
164  public SubmarineInfo SubmarineInfo { get; set; }
165  public SubmarineInfo EnemySubmarineInfo { get; set; }
166 
168 
169  public List<SubmarineInfo> OwnedSubmarines = new List<SubmarineInfo>();
170 
171  public Submarine? Submarine { get; set; }
172 
173  public CampaignDataPath DataPath { get; set; }
174 
175  public bool TraitorsEnabled =>
176  GameMain.NetworkMember?.ServerSettings != null &&
177  GameMain.NetworkMember.ServerSettings.TraitorProbability > 0.0f;
178 
179  partial void InitProjSpecific();
180 
181  private GameSession(SubmarineInfo submarineInfo)
182  {
183  InitProjSpecific();
184  SubmarineInfo = submarineInfo;
186  GameMain.GameSession = this;
187  EventManager = new EventManager();
188  }
189 
190  private GameSession(SubmarineInfo submarineInfo, SubmarineInfo enemySubmarineInfo)
191  : this(submarineInfo)
192  {
193  EnemySubmarineInfo = enemySubmarineInfo;
194  }
195 
199  public GameSession(SubmarineInfo submarineInfo, Option<SubmarineInfo> enemySub, CampaignDataPath dataPath, GameModePreset gameModePreset, CampaignSettings settings, string? seed = null, IEnumerable<Identifier>? missionTypes = null)
200  : this(submarineInfo)
201  {
202  DataPath = dataPath;
203  CrewManager = new CrewManager(gameModePreset.IsSinglePlayer);
204  GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, settings, missionTypes: missionTypes);
205  EnemySubmarineInfo = enemySub.TryUnwrap(out var enemySubmarine) ? enemySubmarine : submarineInfo;
206  InitOwnedSubs(submarineInfo);
207  }
208 
212  public GameSession(SubmarineInfo submarineInfo, Option<SubmarineInfo> enemySub, GameModePreset gameModePreset, string? seed = null, IEnumerable<MissionPrefab>? missionPrefabs = null)
213  : this(submarineInfo)
214  {
215  CrewManager = new CrewManager(gameModePreset.IsSinglePlayer);
216  GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, CampaignSettings.Empty, missionPrefabs: missionPrefabs);
217  EnemySubmarineInfo = enemySub.TryUnwrap(out var enemySubmarine) ? enemySubmarine : submarineInfo;
218  InitOwnedSubs(submarineInfo);
219  }
220 
224  public GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines, XDocument doc, CampaignDataPath campaignData) : this(submarineInfo)
225  {
226  DataPath = campaignData;
227  GameMain.GameSession = this;
228  XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null.");
229 
230  LastSaveVersion = doc.Root.GetAttributeVersion("version", GameMain.Version);
231 
232  foreach (var subElement in rootElement.Elements())
233  {
234  switch (subElement.Name.ToString().ToLowerInvariant())
235  {
236  case "gamemode": //legacy support
237  case "singleplayercampaign":
238 #if CLIENT
239  CrewManager = new CrewManager(true);
240  var campaign = SinglePlayerCampaign.Load(subElement);
241  campaign.LoadNewLevel();
242  GameMode = campaign;
243  InitOwnedSubs(submarineInfo, ownedSubmarines);
244 #else
245  throw new Exception("The server cannot load a single player campaign.");
246 #endif
247  break;
248  case "multiplayercampaign":
249  CrewManager = new CrewManager(false);
250  var mpCampaign = MultiPlayerCampaign.LoadNew(subElement);
251  GameMode = mpCampaign;
252  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
253  {
254  mpCampaign.LoadNewLevel();
255  InitOwnedSubs(submarineInfo, ownedSubmarines);
256  //save to ensure the campaign ID in the save file matches the one that got assigned to this campaign instance
257  SaveUtil.SaveGame(campaignData, isSavingOnLoading: true);
258  }
259  break;
260  case "permadeaths":
261  permadeathsPerAccount = new Dictionary<Option<AccountId>, int>();
262  foreach (XElement accountElement in subElement.Elements("account"))
263  {
264  if (accountElement.Attribute("id") is XAttribute accountIdAttr &&
265  accountElement.Attribute("permadeathcount") is XAttribute permadeathCountAttr)
266  {
267  try
268  {
269  permadeathsPerAccount[AccountId.Parse(accountIdAttr.Value)] = int.Parse(permadeathCountAttr.Value);
270  }
271  catch (Exception e)
272  {
273  DebugConsole.AddWarning($"Exception while trying to load permadeath counts!\n{e}\n id: {accountIdAttr}\n permadeathcount: {permadeathCountAttr}");
274  }
275  }
276  }
277  break;
278  }
279  }
280  }
281 
282  private void InitOwnedSubs(SubmarineInfo submarineInfo, List<SubmarineInfo>? ownedSubmarines = null)
283  {
284  OwnedSubmarines = ownedSubmarines ?? new List<SubmarineInfo>();
285  if (submarineInfo != null && !OwnedSubmarines.Any(s => s.Name == submarineInfo.Name))
286  {
287  OwnedSubmarines.Add(submarineInfo);
288  }
289  }
290 
291  private GameMode InstantiateGameMode(GameModePreset gameModePreset, string? seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable<MissionPrefab>? missionPrefabs = null, IEnumerable<Identifier>? missionTypes = null)
292  {
293  if (gameModePreset.GameModeType == typeof(CoOpMode))
294  {
295  return missionPrefabs != null ?
296  new CoOpMode(gameModePreset, missionPrefabs) :
297  new CoOpMode(gameModePreset, missionTypes, seed ?? ToolBox.RandomSeed(8));
298  }
299  else if (gameModePreset.GameModeType == typeof(PvPMode))
300  {
301  return missionPrefabs != null ?
302  new PvPMode(gameModePreset, missionPrefabs) :
303  new PvPMode(gameModePreset, missionTypes, seed ?? ToolBox.RandomSeed(8));
304  }
305  else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
306  {
307  var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
308  if (selectedSub != null)
309  {
310  campaign.Bank.Deduct(selectedSub.Price);
311  campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0);
312 #if SERVER
313  if (GameMain.Server?.ServerSettings?.NewCampaignDefaultSalary is { } salary)
314  {
315  campaign.Bank.SetRewardDistribution((int)Math.Round(salary, digits: 0));
316  }
317 #endif
318  }
319  return campaign;
320  }
321 #if CLIENT
322  else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
323  {
324  var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
325  if (selectedSub != null)
326  {
327  campaign.Bank.TryDeduct(selectedSub.Price);
328  campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0);
329  }
330  return campaign;
331  }
332  else if (gameModePreset.GameModeType == typeof(TutorialMode))
333  {
334  return new TutorialMode(gameModePreset);
335  }
336  else if (gameModePreset.GameModeType == typeof(TestGameMode))
337  {
338  return new TestGameMode(gameModePreset);
339  }
340 #endif
341  else if (gameModePreset.GameModeType == typeof(GameMode))
342  {
343  return new GameMode(gameModePreset);
344  }
345  else
346  {
347  throw new Exception($"Could not find a game mode of the type \"{gameModePreset.GameModeType}\"");
348  }
349  }
350 
351  public static Location[] CreateDummyLocations(LevelData levelData, LocationType? forceLocationType = null)
352  {
353  MTRandom rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
354  var forceParams = levelData?.ForceOutpostGenerationParams;
355  if (forceLocationType == null &&
356  forceParams != null && forceParams.AllowedLocationTypes.Any() && !forceParams.AllowedLocationTypes.Contains("Any".ToIdentifier()))
357  {
358  forceLocationType =
359  LocationType.Prefabs.Where(lt => forceParams.AllowedLocationTypes.Contains(lt.Identifier)).GetRandom(rand);
360  }
361  var dummyLocations = CreateDummyLocations(rand, forceLocationType);
362  List<Faction> factions = new List<Faction>();
363  foreach (var factionPrefab in FactionPrefab.Prefabs)
364  {
365  factions.Add(new Faction(new CampaignMetadata(), factionPrefab));
366  }
367  foreach (var location in dummyLocations)
368  {
369  if (location.Type.HasOutpost)
370  {
371  location.Faction = CampaignMode.GetRandomFaction(factions, rand, secondary: false);
372  location.SecondaryFaction = CampaignMode.GetRandomFaction(factions, rand, secondary: true);
373  }
374  }
375  return dummyLocations;
376  }
377 
378  public static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType = null)
379  {
380  return CreateDummyLocations(new MTRandom(ToolBox.StringToInt(seed)), forceLocationType);
381  }
382 
383  private static Location[] CreateDummyLocations(Random rand, LocationType? forceLocationType = null)
384  {
385  var dummyLocations = new Location[2];
386  for (int i = 0; i < 2; i++)
387  {
388  dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType);
389  }
390  return dummyLocations;
391  }
392 
393  public static bool ShouldApplyDisembarkPoints(GameModePreset? preset)
394  {
395  if (preset is null) { return true; } // sure I guess?
396 
397  return preset == GameModePreset.Sandbox ||
398  preset == GameModePreset.Mission ||
399  preset == GameModePreset.PvP;
400  }
401 
402  public void LoadPreviousSave()
403  {
404  Submarine.Unload();
405  SaveUtil.LoadGame(DataPath);
406  }
407 
411  public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client = null)
412  {
413  if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
414  {
415  OwnedSubmarines.Add(newSubmarine);
416  }
417  else
418  {
419  // Fetch owned submarine data as the newSubmarine is just the base submarine
420  for (int i = 0; i < OwnedSubmarines.Count; i++)
421  {
422  if (OwnedSubmarines[i].Name == newSubmarine.Name)
423  {
424  newSubmarine = OwnedSubmarines[i];
425  break;
426  }
427  }
428  }
429  Campaign!.PendingSubmarineSwitch = newSubmarine;
430  Campaign!.TransferItemsOnSubSwitch = transferItems;
431  }
432 
433  public bool TryPurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
434  {
435  if (Campaign is null) { return false; }
436  int price = newSubmarine.GetPrice();
437  if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return false; }
438  if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
439  {
440  GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name);
441  OwnedSubmarines.Add(newSubmarine);
442 #if SERVER
443  (Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList);
444 #endif
445  }
446  return true;
447  }
448 
449  public bool IsSubmarineOwned(SubmarineInfo query)
450  {
451  return
452  Submarine.MainSub?.Info.Name == query.Name ||
453  (OwnedSubmarines != null && OwnedSubmarines.Any(os => os.Name == query.Name));
454  }
455 
457  {
458  if (Map?.CurrentLocation == null || Campaign == null) { return false; }
459 
460  bool isRadiated = Map.CurrentLocation.IsRadiated();
461 
462  if (Level.Loaded?.EndLocation is { } endLocation)
463  {
464  isRadiated |= endLocation.IsRadiated();
465  }
466 
467  return isRadiated;
468  }
469 
470  public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams? levelGenerationParams = null, Identifier forceBiome = default)
471  {
472  if (GameMode == null) { return; }
473 
474  LevelData? randomLevel = null;
475  bool pvpOnly = GameMode is PvPMode;
476  foreach (Mission mission in Missions.Union(GameMode.Missions))
477  {
478  MissionPrefab missionPrefab = mission.Prefab;
479  if (missionPrefab != null &&
480  missionPrefab.AllowedLocationTypes.Any() &&
481  !missionPrefab.AllowedConnectionTypes.Any())
482  {
483  Random rand = new MTRandom(ToolBox.StringToInt(levelSeed));
484  LocationType locationType = LocationType.Prefabs
485  .Where(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier))
486  .GetRandom(rand)!;
487  dummyLocations = CreateDummyLocations(levelSeed, locationType);
488 
489  if (!tryCreateFaction(mission.Prefab.RequiredLocationFaction, dummyLocations, static (loc, fac) => loc.Faction = fac))
490  {
491  tryCreateFaction(locationType.Faction, dummyLocations, static (loc, fac) => loc.Faction = fac);
492  tryCreateFaction(locationType.SecondaryFaction, dummyLocations, static (loc, fac) => loc.SecondaryFaction = fac);
493  }
494  static bool tryCreateFaction(Identifier factionIdentifier, Location[] locations, Action<Location, Faction> setter)
495  {
496  if (factionIdentifier.IsEmpty) { return false; }
497  if (!FactionPrefab.Prefabs.TryGet(factionIdentifier, out var prefab)) { return false; }
498  if (locations.Length == 0) { return false; }
499 
500  var newFaction = new Faction(metadata: null, prefab);
501  for (int i = 0; i < locations.Length; i++)
502  {
503  setter(locations[i], newFaction);
504  }
505 
506  return true;
507  }
508 
509  randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true, biomeId: forceBiome, pvpOnly: pvpOnly);
510  break;
511  }
512  }
513  randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, biomeId: forceBiome, pvpOnly: pvpOnly);
514  StartRound(randomLevel);
515  }
516 
517  private bool TryGenerateStationAroundModule(SubmarineInfo? moduleInfo, out Submarine? outpostSub)
518  {
519  outpostSub = null;
520  if (moduleInfo == null) { return false; }
521 
522  var allSuitableOutpostParams = OutpostGenerationParams.OutpostParams
523  .Where(outpostParam => IsOutpostParamsSuitable(outpostParam));
524 
525  // allow for fallback when there are no options with allowed location types defined
526  var suitableOutpostParams =
527  allSuitableOutpostParams.Where(p => p.AllowedLocationTypes.Any()).GetRandomUnsynced() ??
528  allSuitableOutpostParams.GetRandomUnsynced();
529 
530  bool IsOutpostParamsSuitable(OutpostGenerationParams outpostParams)
531  {
532  bool moduleWorksWithOutpostParams = outpostParams.ModuleCounts.Any(moduleCount => moduleInfo.OutpostModuleInfo.ModuleFlags.Contains(moduleCount.Identifier));
533  if (!moduleWorksWithOutpostParams) { return false; }
534 
535  // is there a location that these outpostParams are suitable for, and which this module is suitable for
536  return LocationType.Prefabs.Any(locationType => IsSuitableLocationType(moduleInfo.OutpostModuleInfo.AllowedLocationTypes, locationType.Identifier)
537  && IsSuitableLocationType(outpostParams.AllowedLocationTypes, locationType.Identifier));
538 
539  bool IsSuitableLocationType(IEnumerable<Identifier> allowedLocationTypes, Identifier locationType)
540  {
541  return allowedLocationTypes.None() || allowedLocationTypes.Contains("Any".ToIdentifier()) || allowedLocationTypes.Contains(locationType);
542  }
543  }
544 
545  if (suitableOutpostParams == null)
546  {
547  DebugConsole.AddWarning("No suitable generation parameters found for ForceOutpostModule, skipping outpost generation!");
548  return false;
549  }
550 
551  var suitableLocationType = LocationType.Prefabs.Where(locationType =>
552  suitableOutpostParams.AllowedLocationTypes.Contains(locationType.Identifier)).GetRandomUnsynced();
553 
554  if (suitableLocationType == null)
555  {
556  DebugConsole.AddWarning("No suitable location type found for ForceOutpostModule, skipping outpost generation!");
557  return false;
558  }
559 
560  // try to find a required faction id matching our module
561  var requiredFactionModuleCount = suitableOutpostParams.ModuleCounts.FirstOrDefault(mc => !mc.RequiredFaction.IsEmpty && moduleInfo.OutpostModuleInfo.ModuleFlags.Contains(mc.Identifier));
562  Identifier requiredFactionId = requiredFactionModuleCount?.RequiredFaction ?? Identifier.Empty;
563 
564  if (requiredFactionId.IsEmpty)
565  {
566  // no matching faction requirements, generate normally from location type
567  outpostSub = OutpostGenerator.Generate(suitableOutpostParams, suitableLocationType);
568  return outpostSub != null;
569  }
570 
571  // if there is a faction requirement for the module, create a dummy location and augment its factions to match
572  var dummyLocations = CreateDummyLocations("1337", suitableLocationType);
573  var dummyLocation = dummyLocations[0];
574 
575  if (FactionPrefab.Prefabs.TryGet(requiredFactionId, out FactionPrefab? factionPrefab))
576  {
577  if (factionPrefab.ControlledOutpostPercentage > factionPrefab.SecondaryControlledOutpostPercentage)
578  {
579  dummyLocation.Faction = new Faction(null, factionPrefab);
580  }
581  else
582  {
583  dummyLocation.SecondaryFaction = new Faction(null, factionPrefab);
584  }
585 
586  outpostSub = OutpostGenerator.Generate(suitableOutpostParams, dummyLocation);
587  return outpostSub != null;
588  }
589 
590  return false;
591  }
592 
593  public void StartRound(LevelData? levelData, bool mirrorLevel = false, SubmarineInfo? startOutpost = null, SubmarineInfo? endOutpost = null)
594  {
595 #if DEBUG
596  DateTime startTime = DateTime.Now;
597 #endif
598  RoundDuration = 0.0f;
600 
601  MirrorLevel = mirrorLevel;
602  if (SubmarineInfo == null)
603  {
604  DebugConsole.ThrowError("Couldn't start game session, submarine not selected.");
605  return;
606  }
608  {
609  DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted.");
610  return;
611  }
612  if (SubmarineInfo.SubmarineElement.Elements().Count() == 0)
613  {
614  DebugConsole.ThrowError("Couldn't start game session, saved submarine is empty. The submarine file may be corrupted.");
615  return;
616  }
617 
618  Submarine.LockX = Submarine.LockY = false;
619 
620  LevelData = levelData;
621 
622  Submarine.Unload();
623 
624  bool loadSubmarine = GameMode!.Missions.None(m => !m.Prefab.LoadSubmarines);
625 
626  // attempt to generate an outpost for the main sub, with the forced module inside it
627  if (loadSubmarine)
628  {
629  if (TryGenerateStationAroundModule(ForceOutpostModule, out Submarine? outpostSub))
630  {
631  Submarine = Submarine.MainSub = outpostSub ?? new Submarine(SubmarineInfo);
632  }
633  else
634  {
636  }
637  foreach (Submarine sub in Submarine.GetConnectedSubs())
638  {
639  sub.TeamID = CharacterTeamType.Team1;
640  foreach (Item item in Item.ItemList)
641  {
642  if (item.Submarine != sub) { continue; }
643  foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
644  {
645  wifiComponent.TeamID = sub.TeamID;
646  }
647  }
648  }
649  }
650  else
651  {
652  Submarine = Submarine.MainSub = null;
653  }
654 
656  foreach (Mission mission in GameMode!.Missions)
657  {
658  // setting level for missions that may involve difficulty-related submarine creation
659  mission.SetLevel(levelData);
660  }
661 
662  if (Submarine.MainSubs[1] == null && loadSubmarine)
663  {
664  var enemySubmarineInfo = GameMode is PvPMode ? EnemySubmarineInfo : GameMode.Missions.FirstOrDefault(m => m.EnemySubmarineInfo != null)?.EnemySubmarineInfo;
665  if (enemySubmarineInfo != null)
666  {
667  Submarine.MainSubs[1] = new Submarine(enemySubmarineInfo);
668  }
669  }
670 
671  if (GameMain.NetworkMember?.ServerSettings is { LockAllDefaultWires: true } &&
672  Submarine.MainSubs[0] != null)
673  {
674  List<Item> items = new List<Item>();
675  items.AddRange(Submarine.MainSubs[0].GetItems(alsoFromConnectedSubs: true));
676  if (Submarine.MainSubs[1] != null)
677  {
678  items.AddRange(Submarine.MainSubs[1].GetItems(alsoFromConnectedSubs: true));
679  }
680  foreach (Item item in items)
681  {
682  if (item.GetComponent<CircuitBox>() is { } cb)
683  {
684  cb.TemporarilyLocked = true;
685  }
686 
687  Wire wire = item.GetComponent<Wire>();
688  if (wire != null && !wire.NoAutoLock && wire.Connections.Any(c => c != null)) { wire.Locked = true; }
689  }
690  }
691 
692  Level? level = null;
693  if (levelData != null)
694  {
695  level = Level.Generate(levelData, mirrorLevel, StartLocation, EndLocation, startOutpost, endOutpost);
696  }
697 
698  InitializeLevel(level);
699 
700  //Clear out the cached grids and force update
701  Powered.Grids.Clear();
702 
703  casualties.Clear();
704 
705  GameAnalyticsManager.AddProgressionEvent(
706  GameAnalyticsManager.ProgressionStatus.Start,
707  GameMode?.Preset?.Identifier.Value ?? "none");
708 
709  string eventId = "StartRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
710  GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
711  GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier.Value ?? "none"));
712  GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0));
713  foreach (Mission mission in missions)
714  {
715  GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier);
716  }
717  if (Level.Loaded != null)
718  {
719  Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
721  Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
722  GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + levelId);
723  }
724  GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"));
725 #if CLIENT
726  if (GameMode is TutorialMode tutorialMode)
727  {
728  GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
729  if (GameMain.IsFirstLaunch)
730  {
731  GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
732  }
733  }
734  GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}");
735 #endif
736  var campaignMode = GameMode as CampaignMode;
737  if (campaignMode != null)
738  {
739  GameAnalyticsManager.AddDesignEvent("CampaignSettings:RadiationEnabled:" + campaignMode.Settings.RadiationEnabled);
740  GameAnalyticsManager.AddDesignEvent("CampaignSettings:WorldHostility:" + campaignMode.Settings.WorldHostility);
741  GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShowHuskWarning:" + campaignMode.Settings.ShowHuskWarning);
742  GameAnalyticsManager.AddDesignEvent("CampaignSettings:StartItemSet:" + campaignMode.Settings.StartItemSet);
743  GameAnalyticsManager.AddDesignEvent("CampaignSettings:MaxMissionCount:" + campaignMode.Settings.MaxMissionCount);
744  //log the multipliers as integers to reduce the number of distinct values
745  GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100));
746  GameAnalyticsManager.AddDesignEvent("CampaignSettings:FuelMultiplier:" + (int)(campaignMode.Settings.FuelMultiplier * 100));
747  GameAnalyticsManager.AddDesignEvent("CampaignSettings:MissionRewardMultiplier:" + (int)(campaignMode.Settings.MissionRewardMultiplier * 100));
748  GameAnalyticsManager.AddDesignEvent("CampaignSettings:CrewVitalityMultiplier:" + (int)(campaignMode.Settings.CrewVitalityMultiplier * 100));
749  GameAnalyticsManager.AddDesignEvent("CampaignSettings:NonCrewVitalityMultiplier:" + (int)(campaignMode.Settings.NonCrewVitalityMultiplier * 100));
750  GameAnalyticsManager.AddDesignEvent("CampaignSettings:OxygenMultiplier:" + (int)(campaignMode.Settings.OxygenMultiplier * 100));
751  GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100));
752  GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShipyardPriceMultiplier:" + (int)(campaignMode.Settings.ShipyardPriceMultiplier * 100));
753  GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShopPriceMultiplier:" + (int)(campaignMode.Settings.ShopPriceMultiplier * 100));
754 
755  bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData!.Biome);
756  if (firstTimeInBiome)
757  {
758  GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime);
759  GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels);
760  }
761  if (GameMain.NetworkMember?.ServerSettings is { } serverSettings)
762  {
763  GameAnalyticsManager.AddDesignEvent("ServerSettings:RespawnMode:" + serverSettings.RespawnMode);
764  GameAnalyticsManager.AddDesignEvent("ServerSettings:IronmanMode:" + serverSettings.IronmanModeActive);
765  GameAnalyticsManager.AddDesignEvent("ServerSettings:AllowBotTakeoverOnPermadeath:" + serverSettings.AllowBotTakeoverOnPermadeath);
766  }
767  }
768 
769 #if DEBUG
770  double startDuration = (DateTime.Now - startTime).TotalSeconds;
771  if (startDuration < MinimumLoadingTime)
772  {
773  int sleepTime = (int)((MinimumLoadingTime - startDuration) * 1000);
774  DebugConsole.NewMessage($"Stalling round start by {sleepTime / 1000.0f} s (minimum loading time set to {MinimumLoadingTime})...", Color.Magenta);
775  System.Threading.Thread.Sleep(sleepTime);
776  }
777 #endif
778 #if CLIENT
779  var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary;
780  if (existingRoundSummary?.ContinueButton != null)
781  {
782  existingRoundSummary.ContinueButton.Visible = true;
783  }
784 
785  CharacterHUD.ClearBossProgressBars();
786 
787  RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation);
788 
789  if (GameMode is not TutorialMode && GameMode is not TestGameMode)
790  {
791  GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
792  if (EndLocation != null && levelData != null)
793  {
794  GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false);
795  GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.DisplayName), Color.CadetBlue, playSound: false);
796  var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage);
797  if (missionsToShow.Count() > 1)
798  {
799  string joinedMissionNames = string.Join(", ", missions.Select(m => m.Name));
800  GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false);
801  }
802  else
803  {
804  var mission = missionsToShow.FirstOrDefault();
805  GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), mission?.Name ?? TextManager.Get("None")), Color.CadetBlue, playSound: false);
806  }
807  }
808  else
809  {
810  GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.DisplayName), Color.CadetBlue, playSound: false);
811  }
812  }
813 
814  ReadyCheck.ReadyCheckCooldown = DateTime.MinValue;
815  GUI.PreventPauseMenuToggle = false;
816  HintManager.OnRoundStarted();
817 
818  GameMain.LuaCs.Hook.Call("roundStart");
819  EnableEventLogNotificationIcon(enabled: false);
820 #endif
821  if (campaignMode is { ItemsRelocatedToMainSub: true })
822  {
823 #if SERVER
824  GameMain.Server.SendChatMessage(TextManager.Get("itemrelocated").Value, ChatMessageType.ServerMessageBoxInGame);
825 #else
826  if (campaignMode.IsSinglePlayer)
827  {
828  new GUIMessageBox(string.Empty, TextManager.Get("itemrelocated"));
829  }
830 #endif
831  campaignMode.ItemsRelocatedToMainSub = false;
832  }
833 
835  if (campaignMode is { DivingSuitWarningShown: false } &&
836  Level.Loaded != null && Level.Loaded.GetRealWorldDepth(0) > 4000)
837  {
838 #if CLIENT
839  CoroutineManager.Invoke(() => new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("hint.upgradedivingsuits")), delay: 5.0f);
840 #endif
841  campaignMode.DivingSuitWarningShown = true;
842  }
843  }
844 
845  private void InitializeLevel(Level? level)
846  {
847  //make sure no status effects have been carried on from the next round
848  //(they should be stopped in EndRound, this is a safeguard against cases where the round is ended ungracefully)
849  StatusEffect.StopAll();
850 
851  bool forceDocking = false;
852 #if CLIENT
853  GameMain.LightManager.LosEnabled = (GameMain.Client == null || GameMain.Client.CharacterInfo != null) && !GameMain.DevMode;
854  if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; }
855  if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; }
856  forceDocking = GameMode is TutorialMode;
857 #endif
858  LevelData = level?.LevelData;
859  Level = level;
860 
861  PlaceSubAtInitialPosition(Submarine, Level, placeAtStart: true, forceDocking: forceDocking);
862 
863  foreach (var sub in Submarine.Loaded)
864  {
865  // TODO: Currently there's no need to check these on ruins, but that might change -> Could maybe just check if the body is static?
866  if (sub.Info.IsOutpost || sub.Info.IsBeacon || sub.Info.IsWreck)
867  {
868  sub.DisableObstructedWayPoints();
869  }
870  }
871 
872  Entity.Spawner = new EntitySpawner();
873 
874  if (GameMode != null)
875  {
876  missions.Clear();
877  missions.AddRange(GameMode.Missions);
878  GameMode.Start();
879  foreach (Mission mission in missions)
880  {
881  int prevEntityCount = Entity.GetEntities().Count;
882  mission.Start(Level.Loaded);
883  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count != prevEntityCount)
884  {
885  DebugConsole.ThrowError(
886  $"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " +
887  "The clients should not instantiate entities themselves when starting the mission," +
888  " but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial.");
889  }
890  }
891 
892 #if CLIENT
893  ObjectiveManager.ResetObjectives();
894 #endif
895  EventManager?.StartRound(Level.Loaded);
896  AchievementManager.OnStartRound(Level?.LevelData.Biome);
897 
898  GameMode.ShowStartMessage();
899 
900  if (GameMain.NetworkMember == null)
901  {
902  //only place items and corpses here in single player
903  //the server does this after loading the respawn shuttle
904  if (Level != null)
905  {
906  if (GameMain.GameSession.Missions.None(m => !m.Prefab.AllowOutpostNPCs))
907  {
908  Level.SpawnNPCs();
909  }
910  Level.SpawnCorpses();
911  Level.PrepareBeaconStation();
912  }
913  else
914  {
915  // Spawn npcs in the sub editor test mode.
916  foreach (Submarine sub in Submarine.Loaded)
917  {
918  if (sub?.Info?.OutpostGenerationParams != null)
919  {
920  OutpostGenerator.SpawnNPCs(StartLocation, sub);
921  }
922  }
923  }
924  AutoItemPlacer.SpawnItems(Campaign?.Settings.StartItemSet);
925  }
926  if (GameMode is MultiPlayerCampaign mpCampaign)
927  {
928  mpCampaign.UpgradeManager.ApplyUpgrades();
929  mpCampaign.UpgradeManager.SanityCheckUpgrades();
930  }
931  }
932 
933  CreatureMetrics.RecentlyEncountered.Clear();
934 
935  GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub?.WorldPosition ?? Submarine.Loaded.First().WorldPosition;
936  RoundDuration = 0.0f;
937  GameMain.ResetFrameTime();
938  IsRunning = true;
939  }
940 
941  public static void PlaceSubAtInitialPosition(Submarine? sub, Level? level, bool placeAtStart = true, bool forceDocking = false)
942  {
943  if (level == null || sub == null)
944  {
945  sub?.SetPosition(Vector2.Zero);
946  return;
947  }
948 
949  Submarine outpost = placeAtStart ? level.StartOutpost : level.EndOutpost;
950 
951  var originalSubPos = sub.WorldPosition;
952  var spawnPoint = WayPoint.WayPointList.Find(wp => wp.SpawnType.HasFlag(SpawnType.Submarine) && wp.Submarine == outpost);
953  if (spawnPoint != null)
954  {
955  //pre-determine spawnpoint, just use it directly
956  sub.SetPosition(spawnPoint.WorldPosition);
957  sub.NeutralizeBallast();
959  }
960  else if (outpost != null)
961  {
962  //start by placing the sub below the outpost
963  Rectangle outpostBorders = outpost.GetDockedBorders();
964  Rectangle subBorders = sub.GetDockedBorders();
965 
966  sub.SetPosition(
967  outpost.WorldPosition -
968  new Vector2(0.0f, outpostBorders.Height / 2 + subBorders.Height / 2));
969 
970  //find the port that's the nearest to the outpost and dock if one is found
971  float closestDistance = 0.0f;
972  DockingPort? myPort = null, outPostPort = null;
973  foreach (DockingPort port in DockingPort.List)
974  {
975  if (port.IsHorizontal || port.Docked) { continue; }
976  if (port.Item.Submarine == outpost)
977  {
978  if (port.DockingTarget == null || (outPostPort != null && !outPostPort.MainDockingPort && port.MainDockingPort))
979  {
980  outPostPort = port;
981  }
982  continue;
983  }
984  if (port.Item.Submarine != sub) { continue; }
985 
986  //the submarine port has to be at the top of the sub
987  if (port.Item.WorldPosition.Y < sub.WorldPosition.Y) { continue; }
988 
989  float dist = Vector2.DistanceSquared(port.Item.WorldPosition, outpost.WorldPosition);
990  if ((myPort == null || dist < closestDistance || port.MainDockingPort) && !(myPort?.MainDockingPort ?? false))
991  {
992  myPort = port;
993  closestDistance = dist;
994  }
995  }
996 
997  if (myPort != null && outPostPort != null)
998  {
999  Vector2 portDiff = myPort.Item.WorldPosition - sub.WorldPosition;
1000  Vector2 spawnPos = (outPostPort.Item.WorldPosition - portDiff) - Vector2.UnitY * outPostPort.DockedDistance;
1001 
1002  bool startDocked = level.Type == LevelData.LevelType.Outpost || forceDocking;
1003  if (startDocked)
1004  {
1005  sub.SetPosition(spawnPos);
1006  myPort.Dock(outPostPort);
1007  myPort.Lock(isNetworkMessage: true, applyEffects: false);
1008  }
1009  else
1010  {
1011  sub.SetPosition(spawnPos - Vector2.UnitY * 100.0f);
1012  sub.NeutralizeBallast();
1013  sub.EnableMaintainPosition();
1014  }
1015  }
1016  else
1017  {
1018  sub.NeutralizeBallast();
1019  sub.EnableMaintainPosition();
1020  }
1021 
1022  }
1023  else
1024  {
1025  sub.SetPosition(sub.FindSpawnPos(placeAtStart ? level.StartPosition : level.EndPosition));
1026  sub.NeutralizeBallast();
1027  sub.EnableMaintainPosition();
1028  }
1029 
1030  // Make sure that linked subs which are NOT docked to the main sub
1031  // (but still close enough to NOT be considered as 'left behind')
1032  // are also moved to keep their relative position to the main sub
1033  var linkedSubs = MapEntity.MapEntityList.FindAll(me => me is LinkedSubmarine);
1034  foreach (LinkedSubmarine ls in linkedSubs)
1035  {
1036  if (ls.Sub == null || ls.Submarine != sub) { continue; }
1037  if (!ls.LoadSub || ls.Sub.DockedTo.Contains(sub)) { continue; }
1038  if (sub.Info.LeftBehindDockingPortIDs.Contains(ls.OriginalLinkedToID)) { continue; }
1039  if (ls.Sub.Info.SubmarineElement.Attribute("location") != null) { continue; }
1041  }
1042  }
1043 
1044  public void Update(float deltaTime)
1045  {
1046  RoundDuration += deltaTime;
1047  EventManager?.Update(deltaTime);
1048  GameMode?.Update(deltaTime);
1049  //backwards for loop because the missions may get completed and removed from the list in Update()
1050  for (int i = missions.Count - 1; i >= 0; i--)
1051  {
1052  missions[i].Update(deltaTime);
1053  }
1054  UpdateProjSpecific(deltaTime);
1055  }
1056 
1057  public Mission? GetMission(int index)
1058  {
1059  if (index < 0 || index >= missions.Count) { return null; }
1060  return missions[index];
1061  }
1062 
1063  public int GetMissionIndex(Mission mission)
1064  {
1065  return missions.IndexOf(mission);
1066  }
1067 
1068  public void EnforceMissionOrder(List<Identifier> missionIdentifiers)
1069  {
1070  List<Mission> sortedMissions = new List<Mission>();
1071  foreach (Identifier missionId in missionIdentifiers)
1072  {
1073  var matchingMission = missions.Find(m => m.Prefab.Identifier == missionId);
1074  if (matchingMission == null) { continue; }
1075  sortedMissions.Add(matchingMission);
1076  missions.Remove(matchingMission);
1077  }
1078  missions.AddRange(sortedMissions);
1079  }
1080 
1081  partial void UpdateProjSpecific(float deltaTime);
1082 
1091  public static ImmutableHashSet<Character> GetSessionCrewCharacters(CharacterType type)
1092  {
1093  var result = GameMain.LuaCs.Hook.Call<Character[]?>("getSessionCrewCharacters", type);
1094  if (result != null) return ImmutableHashSet.Create(result);
1095 
1096  if (GameMain.GameSession?.CrewManager is not { } crewManager) { return ImmutableHashSet<Character>.Empty; }
1097 
1098  IEnumerable<Character> players;
1099  IEnumerable<Character> bots;
1100  HashSet<Character> characters = new HashSet<Character>();
1101 
1102 #if SERVER
1103  players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead);
1104  bots = crewManager.GetCharacters().Where(c => !c.IsRemotePlayer);
1105 #elif CLIENT
1106  players = crewManager.GetCharacters().Where(static c => c.IsPlayer);
1107  bots = crewManager.GetCharacters().Where(static c => c.IsBot);
1108 #endif
1109  if (type.HasFlag(CharacterType.Bot))
1110  {
1111  foreach (Character bot in bots) { characters.Add(bot); }
1112  }
1113 
1114  if (type.HasFlag(CharacterType.Player))
1115  {
1116  foreach (Character player in players) { characters.Add(player); }
1117  }
1118 
1119  return characters.ToImmutableHashSet();
1120  }
1121 
1122 #if SERVER
1123  private double LastEndRoundErrorMessageTime;
1124 #endif
1125 
1126  public void EndRound(string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null)
1127  {
1128  RoundEnding = true;
1129 
1130 #if CLIENT
1131  GameMain.LuaCs.Hook.Call("roundEnd");
1132 #endif
1133  //Clear the grids to allow for garbage collection
1134  Powered.Grids.Clear();
1135  Powered.ChangedConnections.Clear();
1136 
1137  try
1138  {
1140 
1141  ImmutableHashSet<Character> crewCharacters = GetSessionCrewCharacters(CharacterType.Both);
1142  int prevMoney = GetAmountOfMoney(crewCharacters);
1143  foreach (Mission mission in missions)
1144  {
1145  mission.End();
1146  }
1147 
1148  foreach (Character character in crewCharacters)
1149  {
1150  character.CheckTalents(AbilityEffectType.OnRoundEnd);
1151  }
1152 
1153  if (missions.Any())
1154  {
1155  if (missions.Any(m => m.Completed))
1156  {
1157  foreach (Character character in crewCharacters)
1158  {
1159  character.CheckTalents(AbilityEffectType.OnAnyMissionCompleted);
1160  }
1161  }
1162 
1163  if (missions.All(m => m.Completed))
1164  {
1165  foreach (Character character in crewCharacters)
1166  {
1167  character.CheckTalents(AbilityEffectType.OnAllMissionsCompleted);
1168  }
1169  }
1170  }
1171 
1172  GameMain.LuaCs.Hook.Call("missionsEnded", missions);
1173 
1174 #if CLIENT
1175  if (GUI.PauseMenuOpen)
1176  {
1177  GUI.TogglePauseMenu();
1178  }
1179  if (IsTabMenuOpen)
1180  {
1181  ToggleTabMenu();
1182  }
1183  DeathPrompt?.Close();
1184  DeathPrompt.CloseBotPanel();
1185 
1186  GUI.PreventPauseMenuToggle = true;
1187 
1188  if (GameMode is not TestGameMode && Screen.Selected == GameMain.GameScreen && RoundSummary != null && transitionType != CampaignMode.TransitionType.End)
1189  {
1190  GUI.ClearMessages();
1191  GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary);
1192  GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, transitionType, traitorResults);
1193  GUIMessageBox.MessageBoxes.Add(summaryFrame);
1194  RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; };
1195  }
1196 
1197  if (GameMain.NetLobbyScreen != null) { GameMain.NetLobbyScreen.OnRoundEnded(); }
1198  TabMenu.OnRoundEnded();
1199  GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb));
1200  ObjectiveManager.ResetUI();
1201  CharacterHUD.ClearBossProgressBars();
1202 #endif
1203  AchievementManager.OnRoundEnded(this);
1204 
1205 #if SERVER
1207 #endif
1208  GameMode?.End(transitionType);
1212  IsRunning = false;
1213 
1214 #if CLIENT
1215  bool success = CrewManager!.GetCharacters().Any(c => !c.IsDead);
1216 #else
1217  bool success =
1218  GameMain.Server != null &&
1219  GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
1220 #endif
1221  GameAnalyticsManager.AddProgressionEvent(
1222  success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail,
1223  GameMode?.Preset.Identifier.Value ?? "none",
1224  RoundDuration);
1225  string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
1226  LogEndRoundStats(eventId, traitorResults);
1227  if (GameMode is CampaignMode campaignMode)
1228  {
1229  GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney);
1230  campaignMode.TotalPlayTime += RoundDuration;
1231  }
1232 #if CLIENT
1233  HintManager.OnRoundEnded();
1234 #endif
1235  missions.Clear();
1236  }
1237  catch (Exception e)
1238  {
1239  string errorMsg = "Unknown error while ending the round.";
1240  DebugConsole.ThrowError(errorMsg, e);
1241  GameAnalyticsManager.AddErrorEventOnce("GameSession.EndRound:UnknownError", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.StackTrace);
1242 #if SERVER
1243  if (Timing.TotalTime > LastEndRoundErrorMessageTime + 1.0)
1244  {
1245  GameMain.Server?.SendChatMessage(errorMsg + "\n" + e.StackTrace, Networking.ChatMessageType.Error);
1246  LastEndRoundErrorMessageTime = Timing.TotalTime;
1247  }
1248 #endif
1249  }
1250  finally
1251  {
1252  RoundEnding = false;
1253  }
1254 
1255  int GetAmountOfMoney(IEnumerable<Character> crew)
1256  {
1257  if (GameMode is not CampaignMode campaign) { return 0; }
1258 
1259  return GameMain.NetworkMember switch
1260  {
1261  null => campaign.Bank.Balance,
1262  _ => crew.Sum(c => c.Wallet.Balance) + campaign.Bank.Balance
1263  };
1264  }
1265  }
1266 
1267  public static PerkCollection GetPerks()
1268  {
1269  if (GameMain.NetworkMember?.ServerSettings is not { } serverSettings)
1270  {
1271  return PerkCollection.Empty;
1272  }
1273 
1274  var team1Builder = ImmutableArray.CreateBuilder<DisembarkPerkPrefab>();
1275  var team2Builder = ImmutableArray.CreateBuilder<DisembarkPerkPrefab>();
1276 
1277  foreach (Identifier coalitionPerk in serverSettings.SelectedCoalitionPerks)
1278  {
1279  if (!DisembarkPerkPrefab.Prefabs.TryGet(coalitionPerk, out DisembarkPerkPrefab? disembarkPerk)) { continue; }
1280  team1Builder.Add(disembarkPerk);
1281  }
1282 
1283  foreach (Identifier separatistsPerk in serverSettings.SelectedSeparatistsPerks)
1284  {
1285  if (!DisembarkPerkPrefab.Prefabs.TryGet(separatistsPerk, out DisembarkPerkPrefab? disembarkPerk)) { continue; }
1286  team2Builder.Add(disembarkPerk);
1287  }
1288 
1289  return new PerkCollection(team1Builder.ToImmutable(), team2Builder.ToImmutable());
1290  }
1291 
1292  public static bool ValidatedDisembarkPoints(GameModePreset preset, IEnumerable<Identifier> missionTypes)
1293  {
1294  if (GameMain.NetworkMember?.ServerSettings is not { } settings) { return false; }
1295 
1296  bool checkBothTeams = preset == GameModePreset.PvP;
1297 
1298  PerkCollection perks = GetPerks();
1299 
1300  int team1TotalCost = GetTotalCost(perks.Team1Perks);
1301  if (team1TotalCost > settings.DisembarkPointAllowance)
1302  {
1303  return false;
1304  }
1305 
1306  if (checkBothTeams)
1307  {
1308  int team2TotalCost = GetTotalCost(perks.Team2Perks);
1309  if (team2TotalCost > settings.DisembarkPointAllowance)
1310  {
1311  return false;
1312  }
1313  }
1314 
1315  return true;
1316 
1317  int GetTotalCost(ImmutableArray<DisembarkPerkPrefab> perksToCheck)
1318  {
1319  if (preset == GameModePreset.Mission || preset == GameModePreset.PvP)
1320  {
1321  if (ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(preset, missionTypes))
1322  {
1323  perksToCheck = perksToCheck.Where(static p => p.PerkBehaviors.All(static b => b.CanApplyWithoutSubmarine())).ToImmutableArray();
1324  }
1325  }
1326  return perksToCheck.Sum(static p => p.Cost);
1327  }
1328  }
1329 
1330  public static bool ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(GameModePreset preset, IEnumerable<Identifier> missionTypes)
1331  {
1332  if (preset == GameModePreset.Mission || preset == GameModePreset.PvP)
1333  {
1335  foreach (var missionType in missionTypesToCheck)
1336  {
1337  foreach (var missionPrefab in MissionPrefab.Prefabs.Where(mp => mp.Type == missionType))
1338  {
1339  if (missionPrefab.LoadSubmarines)
1340  {
1341  return false;
1342  }
1343  }
1344  }
1345  }
1346  return true;
1347  }
1348 
1349  public void LogEndRoundStats(string eventId, TraitorManager.TraitorResults? traitorResults = null)
1350  {
1351  if (Submarine.MainSub?.Info?.IsVanillaSubmarine() ?? false)
1352  {
1353  //don't log modded subs, that's a ton of extra data to collect
1354  GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration);
1355  }
1356  GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration);
1357  GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0), RoundDuration);
1358  foreach (Mission mission in missions)
1359  {
1360  GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), RoundDuration);
1361  }
1362  if (!ContentPackageManager.ModsEnabled)
1363  {
1364  if (Level.Loaded != null)
1365  {
1366  Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
1368  Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
1369  GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), RoundDuration);
1370  GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration);
1371  }
1372 
1373  //disabled for now, we're collecting too many events and this is information we don't need atm
1374  /*if (Submarine.MainSub != null)
1375  {
1376  Dictionary<ItemPrefab, int> submarineInventory = new Dictionary<ItemPrefab, int>();
1377  foreach (Item item in Item.ItemList)
1378  {
1379  var rootContainer = item.RootContainer ?? item;
1380  if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; }
1381  if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; }
1382 
1383  var holdable = item.GetComponent<Holdable>();
1384  if (holdable == null || holdable.Attached) { continue; }
1385  var wire = item.GetComponent<Wire>();
1386  if (wire != null && wire.Connections.Any(c => c != null)) { continue; }
1387 
1388  if (!submarineInventory.ContainsKey(item.Prefab))
1389  {
1390  submarineInventory.Add(item.Prefab, 0);
1391  }
1392  submarineInventory[item.Prefab]++;
1393  }
1394  foreach (var subItem in submarineInventory)
1395  {
1396  GameAnalyticsManager.AddDesignEvent(eventId + "SubmarineInventory:" + subItem.Key.Identifier, subItem.Value);
1397  }
1398  }*/
1399  }
1400 
1401  if (traitorResults.HasValue)
1402  {
1403  GameAnalyticsManager.AddDesignEvent($"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{traitorResults.Value.ObjectiveSuccessful}");
1404  GameAnalyticsManager.AddDesignEvent($"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{(traitorResults.Value.VotedCorrectTraitor ? "TraitorIdentifier" : "TraitorUnidentified")}");
1405  }
1406 
1407  foreach (Character c in GetSessionCrewCharacters(CharacterType.Both))
1408  {
1409  foreach (var itemSelectedDuration in c.ItemSelectedDurations)
1410  {
1411  string characterType = "Unknown";
1412  if (c.IsBot)
1413  {
1414  characterType = "Bot";
1415  }
1416  else if (c.IsPlayer)
1417  {
1418  characterType = "Player";
1419  }
1420  GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier.Value ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value);
1421  }
1422  }
1423 #if CLIENT
1424  if (GameMode is TutorialMode tutorialMode)
1425  {
1426  GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
1427  if (GameMain.IsFirstLaunch)
1428  {
1429  GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
1430  }
1431  }
1432  GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentCleaning", TimeSpentCleaning);
1433  GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentPainting", TimeSpentPainting);
1434  TimeSpentCleaning = TimeSpentPainting = 0.0;
1435 #endif
1436  }
1437 
1438  public void KillCharacter(Character character)
1439  {
1440  if (CrewManager != null && CrewManager.GetCharacters().Contains(character))
1441  {
1442  casualties.Add(character);
1443  }
1444 #if CLIENT
1445  CrewManager?.KillCharacter(character);
1446 #endif
1447  }
1448 
1449  public void ReviveCharacter(Character character)
1450  {
1451  casualties.Remove(character);
1452 #if CLIENT
1453  CrewManager?.ReviveCharacter(character);
1454 #endif
1455  }
1456 
1457  public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackageNames, out LocalizedString errorMsg)
1458  {
1459  errorMsg = "";
1460  //no known content packages, must be an older save file
1461  if (!contentPackageNames.Any()) { return true; }
1462 
1463  List<string> missingPackages = new List<string>();
1464  foreach (string packageName in contentPackageNames)
1465  {
1466  if (!ContentPackageManager.EnabledPackages.All.Any(cp => cp.NameMatches(packageName)))
1467  {
1468  missingPackages.Add(packageName);
1469  }
1470  }
1471  List<string> excessPackages = new List<string>();
1472  foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All)
1473  {
1474  if (!cp.HasMultiplayerSyncedContent) { continue; }
1475  if (!contentPackageNames.Any(p => cp.NameMatches(p)))
1476  {
1477  excessPackages.Add(cp.Name);
1478  }
1479  }
1480 
1481  bool orderMismatch = false;
1482  if (missingPackages.Count == 0 && missingPackages.Count == 0)
1483  {
1484  var enabledPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToImmutableArray();
1485  for (int i = 0; i < contentPackageNames.Count && i < enabledPackages.Length; i++)
1486  {
1487  if (!enabledPackages[i].NameMatches(contentPackageNames[i]))
1488  {
1489  orderMismatch = true;
1490  break;
1491  }
1492  }
1493  }
1494 
1495  if (!orderMismatch && missingPackages.Count == 0 && excessPackages.Count == 0) { return true; }
1496 
1497  if (missingPackages.Count == 1)
1498  {
1499  errorMsg = TextManager.GetWithVariable("campaignmode.missingcontentpackage", "[missingcontentpackage]", missingPackages[0]);
1500  }
1501  else if (missingPackages.Count > 1)
1502  {
1503  errorMsg = TextManager.GetWithVariable("campaignmode.missingcontentpackages", "[missingcontentpackages]", string.Join(", ", missingPackages));
1504  }
1505  if (excessPackages.Count == 1)
1506  {
1507  if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
1508  errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackage", "[incompatiblecontentpackage]", excessPackages[0]);
1509  }
1510  else if (excessPackages.Count > 1)
1511  {
1512  if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
1513  errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackages", "[incompatiblecontentpackages]", string.Join(", ", excessPackages));
1514  }
1515  if (orderMismatch)
1516  {
1517  if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
1518  errorMsg += TextManager.GetWithVariable("campaignmode.contentpackageordermismatch", "[loadorder]", string.Join(", ", contentPackageNames));
1519  }
1520 
1521  return false;
1522  }
1523 
1524  public void Save(string filePath, bool isSavingOnLoading)
1525  {
1526  if (GameMode is not CampaignMode campaign)
1527  {
1528  throw new NotSupportedException("GameSessions can only be saved when playing in a campaign mode.");
1529  }
1530 
1531  XDocument doc = new XDocument(new XElement("Gamesession"));
1532  XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null.");
1533 
1534  rootElement.Add(new XAttribute("savetime", SerializableDateTime.UtcNow.ToUnixTime()));
1535  #warning TODO: after this gets on main, replace savetime with the commented line
1536  //rootElement.Add(new XAttribute("savetime", SerializableDateTime.LocalNow));
1537 
1538  rootElement.Add(new XAttribute("currentlocation", Map?.CurrentLocation?.NameIdentifier.Value ?? string.Empty));
1539  rootElement.Add(new XAttribute("currentlocationnameformatindex", Map?.CurrentLocation?.NameFormatIndex ?? -1));
1540  rootElement.Add(new XAttribute("locationtype", Map?.CurrentLocation?.Type?.Identifier ?? Identifier.Empty));
1541 
1542  rootElement.Add(new XAttribute("nextleveltype", campaign.NextLevel?.Type ?? LevelData?.Type ?? LevelData.LevelType.Outpost));
1543 
1544  LastSaveVersion = GameMain.Version;
1545  rootElement.Add(new XAttribute("version", GameMain.Version));
1546  if (Submarine?.Info != null && !Submarine.Removed && Campaign != null)
1547  {
1548  bool hasNewPendingSub = Campaign.PendingSubmarineSwitch != null &&
1549  Campaign.PendingSubmarineSwitch.MD5Hash.StringRepresentation != Submarine.Info.MD5Hash.StringRepresentation;
1550  if (hasNewPendingSub)
1551  {
1552  Campaign.SwitchSubs();
1553  }
1554  }
1555  rootElement.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name));
1556  if (OwnedSubmarines != null)
1557  {
1558  List<string> ownedSubmarineNames = new List<string>();
1559  var ownedSubsElement = new XElement("ownedsubmarines");
1560  rootElement.Add(ownedSubsElement);
1561  foreach (var ownedSub in OwnedSubmarines)
1562  {
1563  ownedSubsElement.Add(new XElement("sub", new XAttribute("name", ownedSub.Name)));
1564  }
1565  }
1566  if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); }
1567  rootElement.Add(new XAttribute("selectedcontentpackagenames",
1568  string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Name.Replace("|", @"\|")))));
1569 
1570  XElement permadeathsElement = new XElement("permadeaths");
1571  foreach (var kvp in permadeathsPerAccount)
1572  {
1573  if (kvp.Key.TryUnwrap(out AccountId? accountId))
1574  {
1575  permadeathsElement.Add(
1576  new XElement("account"),
1577  new XAttribute("id", accountId.StringRepresentation),
1578  new XAttribute("permadeathcount", kvp.Value));
1579  }
1580  }
1581  rootElement.Add(permadeathsElement);
1582 
1583  ((CampaignMode)GameMode).Save(doc.Root, isSavingOnLoading);
1584 
1585  doc.SaveSafe(filePath, throwExceptions: true);
1586  }
1587 
1588  /*public void Load(XElement saveElement)
1589  {
1590  foreach (var subElement in saveElement.Elements())
1591  {
1592  switch (subElement.Name.ToString().ToLowerInvariant())
1593  {
1594 #if CLIENT
1595  case "gamemode": //legacy support
1596  case "singleplayercampaign":
1597  GameMode = SinglePlayerCampaign.Load(subElement);
1598  break;
1599 #endif
1600  case "multiplayercampaign":
1601  if (!(GameMode is MultiPlayerCampaign mpCampaign))
1602  {
1603  DebugConsole.ThrowError("Error while loading a save file: the save file is for a multiplayer campaign but the current gamemode is " + GameMode.GetType().ToString());
1604  break;
1605  }
1606 
1607  mpCampaign.Load(subElement);
1608  break;
1609  }
1610  }
1611  }*/
1612 
1613  }
1614 }
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,...
Submarine Submarine
Definition: Entity.cs:53
static NetLobbyScreen NetLobbyScreen
Definition: GameMain.cs:57
static readonly Version Version
Definition: GameMain.cs:21
static GameScreen GameScreen
Definition: GameMain.cs:56
static GameServer Server
Definition: GameMain.cs:39
static NetworkMember NetworkMember
Definition: GameMain.cs:41
static LuaCsSetup LuaCs
Definition: GameMain.cs:37
static GameSession GameSession
Definition: GameMain.cs:45
virtual void ShowStartMessage()
Definition: GameMode.cs:57
virtual IEnumerable< Mission > Missions
Definition: GameMode.cs:21
virtual void Start()
Definition: GameMode.cs:52
virtual void AddExtraMissions(LevelData levelData)
Definition: GameMode.cs:59
GameModePreset Preset
Definition: GameMode.cs:43
virtual void End(CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None)
Definition: GameMode.cs:73
virtual void Update(float deltaTime)
Definition: GameMode.cs:68
static GameModePreset Mission
static GameModePreset PvP
readonly bool IsSinglePlayer
static GameModePreset Sandbox
readonly Identifier Identifier
Mission? GetMission(int index)
void StartRound(LevelData? levelData, bool mirrorLevel=false, SubmarineInfo? startOutpost=null, SubmarineInfo? endOutpost=null)
Definition: GameSession.cs:593
List< SubmarineInfo > OwnedSubmarines
Definition: GameSession.cs:169
CharacterTeamType? WinningTeam
Definition: GameSession.cs:105
static void PlaceSubAtInitialPosition(Submarine? sub, Level? level, bool placeAtStart=true, bool forceDocking=false)
Definition: GameSession.cs:941
bool IsRunning
Is a round currently running?
Definition: GameSession.cs:110
static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType=null)
Definition: GameSession.cs:378
void LogEndRoundStats(string eventId, TraitorManager.TraitorResults? traitorResults=null)
GameSession(SubmarineInfo submarineInfo, Option< SubmarineInfo > enemySub, GameModePreset gameModePreset, string? seed=null, IEnumerable< MissionPrefab >? missionPrefabs=null)
Start a new GameSession with a specific pre-selected mission.
Definition: GameSession.cs:212
GameSession(SubmarineInfo submarineInfo, Option< SubmarineInfo > enemySub, CampaignDataPath dataPath, GameModePreset gameModePreset, CampaignSettings settings, string? seed=null, IEnumerable< Identifier >? missionTypes=null)
Start a new GameSession. Will be saved to the specified save path (if playing a game mode that can be...
Definition: GameSession.cs:199
static bool ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(GameModePreset preset, IEnumerable< Identifier > missionTypes)
static PerkCollection GetPerks()
bool IsSubmarineOwned(SubmarineInfo query)
Definition: GameSession.cs:449
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.
CampaignDataPath DataPath
Definition: GameSession.cs:173
void Save(string filePath, bool isSavingOnLoading)
IEnumerable< Mission > Missions
Definition: GameSession.cs:85
CampaignMode? Campaign
Definition: GameSession.cs:128
static bool IsCompatibleWithEnabledContentPackages(IList< string > contentPackageNames, out LocalizedString errorMsg)
bool IsCurrentLocationRadiated()
Definition: GameSession.cs:456
bool TryPurchaseSubmarine(SubmarineInfo newSubmarine, Client? client=null)
Definition: GameSession.cs:433
static bool ShouldApplyDisembarkPoints(GameModePreset? preset)
Definition: GameSession.cs:393
SubmarineInfo SubmarineInfo
Definition: GameSession.cs:164
int PermadeathCountForAccount(Option< AccountId > accountId)
Definition: GameSession.cs:100
void ReviveCharacter(Character character)
void KillCharacter(Character character)
IEnumerable< Character > Casualties
Definition: GameSession.cs:88
SubmarineInfo EnemySubmarineInfo
Definition: GameSession.cs:165
static Location[] CreateDummyLocations(LevelData levelData, LocationType? forceLocationType=null)
Definition: GameSession.cs:351
static bool ValidatedDisembarkPoints(GameModePreset preset, IEnumerable< Identifier > missionTypes)
readonly EventManager EventManager
Definition: GameSession.cs:69
int GetMissionIndex(Mission mission)
void StartRound(string levelSeed, float? difficulty=null, LevelGenerationParams? levelGenerationParams=null, Identifier forceBiome=default)
Definition: GameSession.cs:470
void EndRound(string endMessage, CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults=null)
SubmarineInfo? ForceOutpostModule
Definition: GameSession.cs:167
void Update(float deltaTime)
CrewManager? CrewManager
Definition: GameSession.cs:75
GameSession(SubmarineInfo submarineInfo, List< SubmarineInfo > ownedSubmarines, XDocument doc, CampaignDataPath campaignData)
Load a game session from the specified XML document. The session will be saved to the specified path.
Definition: GameSession.cs:224
void IncrementPermadeath(Option< AccountId > accountId)
Definition: GameSession.cs:96
void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client=null)
Switch to another submarine. The sub is loaded when the next round starts.
Definition: GameSession.cs:411
static readonly List< Item > ItemList
static readonly Dictionary< int, GridInfo > Grids
Definition: Powered.cs:74
static readonly HashSet< Connection > ChangedConnections
Definition: Powered.cs:72
JobPrefab Prefab
Definition: Job.cs:18
static LevelData CreateRandom(string seed="", float? difficulty=null, LevelGenerationParams generationParams=null, Identifier biomeId=default, bool requireOutpost=false, bool pvpOnly=false)
Definition: LevelData.cs:282
OutpostGenerationParams ForceOutpostGenerationParams
Definition: LevelData.cs:45
readonly LevelType Type
Definition: LevelData.cs:20
readonly string Seed
Definition: LevelData.cs:22
readonly Biome Biome
Definition: LevelData.cs:26
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:742
LocalizedString DisplayName
Definition: Location.cs:59
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:97
Identifier SecondaryFaction
If set, forces the location to be assigned to this secondary faction. Set to "None" if you don't want...
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
Definition: MapEntity.cs:15
Map(CampaignSettings settings)
Definition: Map.cs:81
Location CurrentLocation
Definition: Map.cs:47
string Seed
Definition: Map.cs:68
Location SelectedLocation
Definition: Map.cs:54
List< LocationConnection > Connections
Definition: Map.cs:75
readonly string StringRepresentation
Definition: Md5Hash.cs:32
void End()
End the mission and give a reward if it was completed successfully
static IEnumerable< Identifier > ValidateMissionTypes(IEnumerable< Identifier > missionTypes, Dictionary< Identifier, Type > missionClasses)
Returns the mission types that are valid for the given mission classes (e.g. all mission types suitab...
static readonly Dictionary< Identifier, Type > CoOpMissionClasses
The keys here are for backwards compatibility, tying the old mission types to the appropriate class....
static readonly PrefabCollection< MissionPrefab > Prefabs
static readonly Dictionary< Identifier, Type > PvPMissionClasses
The keys here are for backwards compatibility, tying the old mission types to the appropriate class....
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 Type
override IReadOnlyList< Client > ConnectedClients
Definition: GameServer.cs:117
void SendChatMessage(string message, ChatMessageType? type=null, Client senderClient=null, Character senderCharacter=null, PlayerConnectionChangeType changeType=PlayerConnectionChangeType.None, ChatMode chatMode=ChatMode.None)
Add the message to the chatbox and pass it to all clients who can receive it
Definition: GameServer.cs:3686
IEnumerable< Identifier > AllowedLocationTypes
IEnumerable< Identifier > ModuleFlags
readonly Identifier Identifier
Definition: Prefab.cs:34
static Screen Selected
Definition: Screen.cs:5
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
Definition: StatusEffect.cs:72
List< Item > GetItems(bool alsoFromConnectedSubs)
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
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...
OutpostGenerationParams OutpostGenerationParams
int GetPrice(Location location=null, ImmutableHashSet< Character > characterList=null)
OutpostModuleInfo OutpostModuleInfo
static List< WayPoint > WayPointList
Definition: WayPoint.cs:18
AbilityEffectType
Definition: Enums.cs:140
CharacterType
Definition: Enums.cs:711
@ Character
Characters only
DateTime wrapper that tries to offer a reliable string representation that's also human-friendly
static SerializableDateTime UtcNow