Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs
4 using FarseerPhysics;
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;
11 
12 namespace Barotrauma
13 {
14  abstract partial class CampaignMode : GameMode
15  {
17  public readonly record struct SaveInfo(
18  string FilePath,
19  Option<SerializableDateTime> SaveTime,
20  string SubmarineName,
21  ImmutableArray<string> EnabledContentPackageNames) : INetSerializableStruct;
22 
23  public const int MaxMoney = int.MaxValue / 2; //about 1 billion
24  public const int InitialMoney = 8500;
25 
26  //duration of the camera transition at the end of a round
27  protected const float EndTransitionDuration = 5.0f;
28  //there can be no events before this time has passed during the 1st campaign round
29  const float FirstRoundEventDelay = 0.0f;
30 
31  public double TotalPlayTime;
32  public int TotalPassedLevels;
33 
34  public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Upgrade, PurchaseSub, MedicalClinic, Cargo }
35 
36  public static bool BlocksInteraction(InteractionType interactionType)
37  {
38  return interactionType != InteractionType.None && interactionType != InteractionType.Cargo;
39  }
40 
41  public readonly CargoManager CargoManager;
44 
45  private List<Faction> factions;
46  public IReadOnlyList<Faction> Factions => factions;
47 
49 
50  protected XElement petsElement;
51 
52  protected XElement ActiveOrdersElement { get; set; }
53 
54  public CampaignSettings Settings;
55 
56  private readonly List<Mission> extraMissions = new List<Mission>();
57 
58  public readonly NamedEvent<WalletChangedEvent> OnMoneyChanged = new NamedEvent<WalletChangedEvent>();
59 
60  public enum TransitionType
61  {
62  None,
63  //leaving a location level
64  LeaveLocation,
65  //progressing to next location level
66  ProgressToNextLocation,
67  //returning to previous location level
68  ReturnToPreviousLocation,
69  //returning to previous location (one with no level/outpost, the player is taken to the map screen and must choose their next destination)
70  ReturnToPreviousEmptyLocation,
71  //progressing to an empty location (one with no level/outpost, the player is taken to the map screen and must choose their next destination)
72  ProgressToNextEmptyLocation,
73  //end of campaign (reached end location)
74  End
75  }
76 
77  public bool IsFirstRound { get; protected set; } = true;
78 
79  public bool DisableEvents
80  {
81  get { return IsFirstRound && GameMain.GameSession.RoundDuration < FirstRoundEventDelay; }
82  }
83 
84  public bool CheatsEnabled;
85 
86  public const float HullRepairCostPerDamage = 0.1f, ItemRepairCostPerRepairDuration = 1.0f;
87  public const int ShuttleReplaceCost = 1000;
88  public const int MaxHullRepairCost = 600, MaxItemRepairCost = 2000;
89 
90  protected bool wasDocked;
91 
92  //key = dialog flag, double = Timing.TotalTime when the line was last said
93  private readonly Dictionary<string, double> dialogLastSpoken = new Dictionary<string, double>();
94 
96  public bool TransferItemsOnSubSwitch { get; set; }
97 
98  public bool SwitchedSubsThisRound { get; private set; }
99 
100  protected Map map;
101  public Map Map
102  {
103  get { return map; }
104  }
105 
106  public override IEnumerable<Mission> Missions
107  {
108  get
109  {
110  //map can be null if we're in the process of loading the save
111  if (Map != null)
112  {
113  if (Map.CurrentLocation != null)
114  {
115  foreach (Mission mission in map.CurrentLocation.SelectedMissions)
116  {
117  if (mission.Locations[0] == mission.Locations[1] ||
118  mission.Locations.Contains(Map.SelectedLocation))
119  {
120  yield return mission;
121  }
122  }
123  }
124  foreach (Mission mission in extraMissions)
125  {
126  yield return mission;
127  }
128  }
129  }
130  }
131 
133 
134  public Wallet Bank;
135 
137  {
138  get;
139  protected set;
140  }
141 
142  public bool PurchasedLostShuttlesInLatestSave, PurchasedHullRepairsInLatestSave, PurchasedItemRepairsInLatestSave;
143 
144  public virtual bool PurchasedHullRepairs { get; set; }
145  public virtual bool PurchasedLostShuttles { get; set; }
146  public virtual bool PurchasedItemRepairs { get; set; }
147 
149 
151 
152  private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions)
153  {
154  if (GameMain.NetworkMember == null) { return true; }
155  if (GameMain.NetworkMember.ConnectedClients.Count == 1) { return true; }
156 
157  if (GameMain.NetworkMember.GameStarted)
158  {
159  //allow managing if no-one with permissions is alive and in-game
160  return GameMain.NetworkMember.ConnectedClients.None(c =>
161  c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } &&
162  (IsOwner(c) || c.HasPermission(permissions)));
163  }
164  else
165  {
166  return GameMain.NetworkMember.ConnectedClients.None(c => IsOwner(c) || c.HasPermission(permissions));
167  }
168  }
169 
170  protected CampaignMode(GameModePreset preset, CampaignSettings settings)
171  : base(preset)
172  {
173  Bank = new Wallet(Option<Character>.None())
174  {
175  Balance = settings.InitialMoney
176  };
177 
178  CargoManager = new CargoManager(this);
179  MedicalClinic = new MedicalClinic(this);
181  Identifier messageIdentifier = new Identifier("money");
182 
183 #if CLIENT
184  OnMoneyChanged.RegisterOverwriteExisting(new Identifier("CampaignMoneyChangeNotification"), e =>
185  {
186  if (!e.ChangedData.BalanceChanged.TryUnwrap(out var changed)) { return; }
187 
188  if (changed == 0) { return; }
189 
190  bool isGain = changed > 0;
191  Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red;
192 
193  if (e.Owner.TryUnwrap(out var owner))
194  {
195  owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed);
196  }
197  else if (IsSinglePlayer)
198  {
199  Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed);
200  }
201 
202  string FormatMessage() => TextManager.GetWithVariable(isGain ? "moneygainformat" : "moneyloseformat", "[money]", TextManager.FormatCurrency(Math.Abs(changed))).ToString();
203  });
204 #endif
205  }
206 
207  public virtual Wallet GetWallet(Client client = null)
208  {
209  return Bank;
210  }
211 
212  public virtual bool TryPurchase(Client client, int price)
213  {
214  return price == 0 || GetWallet(client).TryDeduct(price);
215  }
216 
217  public virtual int GetBalance(Client client = null)
218  {
219  return GetWallet(client).Balance;
220  }
221 
222  public bool CanAfford(int cost, Client client = null)
223  {
224  return GetBalance(client) >= cost;
225  }
226 
232  {
233  if (Level.Loaded?.EndLocation != null && !Level.Loaded.Generating &&
234  Level.Loaded.Type == LevelData.LevelType.LocationConnection &&
235  GetAvailableTransition(out _, out _) == TransitionType.ProgressToNextEmptyLocation)
236  {
237  return Level.Loaded.EndLocation;
238  }
240  }
241 
242  public static List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
243  {
244  //leave subs behind if they're not docked to the leaving sub and not at the same exit
245  return Submarine.Loaded.FindAll(sub =>
246  sub != leavingSub &&
247  !leavingSub.DockedTo.Contains(sub) &&
248  sub.Info.Type == SubmarineType.Player && sub.TeamID == CharacterTeamType.Team1 && // pirate subs are currently tagged as player subs as well
249  sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle &&
250  (sub.AtEndExit != leavingSub.AtEndExit || sub.AtStartExit != leavingSub.AtStartExit));
251  }
252 
254  {
255  if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters &&
256  !parameters.OutpostFilePath.IsNullOrEmpty())
257  {
258  return new SubmarineInfo(parameters.OutpostFilePath.Value)
259  {
260  OutpostGenerationParams = parameters
261  };
262  }
263  return null;
264  }
265 
266  public override void Start()
267  {
268  base.Start();
269  dialogLastSpoken.Clear();
270  characterOutOfBoundsTimer.Clear();
271 #if CLIENT
272  prevCampaignUIAutoOpenType = TransitionType.None;
273 #endif
274 
275  foreach (var faction in factions)
276  {
277  faction.Reputation.ReputationAtRoundStart = faction.Reputation.Value;
278  }
279 
280  if (PurchasedHullRepairsInLatestSave)
281  {
282  foreach (Structure wall in Structure.WallList)
283  {
284  if (wall.Submarine == null || wall.Submarine.Info.Type != SubmarineType.Player) { continue; }
285  if (wall.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(wall.Submarine))
286  {
287  for (int i = 0; i < wall.SectionCount; i++)
288  {
289  wall.SetDamage(i, 0, createNetworkEvent: false, createExplosionEffect: false);
290  }
291  }
292  }
293  PurchasedHullRepairsInLatestSave = PurchasedHullRepairs = false;
294  }
295  if (PurchasedItemRepairsInLatestSave)
296  {
297  foreach (Item item in Item.ItemList)
298  {
299  if (item.Submarine == null || item.Submarine.Info.Type != SubmarineType.Player) { continue; }
300  if (item.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(item.Submarine))
301  {
302  if (item.GetComponent<Items.Components.Repairable>() != null)
303  {
304  item.Condition = item.MaxCondition;
305  }
306  }
307  }
308  PurchasedItemRepairsInLatestSave = PurchasedItemRepairs = false;
309  }
311  var connectedSubs = Submarine.MainSub.GetConnectedSubs();
312  wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost);
313  SwitchedSubsThisRound = false;
314  }
315 
316  public static int GetHullRepairCost()
317  {
318  float totalDamage = 0;
319  foreach (Structure wall in Structure.WallList)
320  {
321  if (wall.Submarine == null || wall.Submarine.Info.Type != SubmarineType.Player) { continue; }
322  if (wall.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(wall.Submarine))
323  {
324  for (int i = 0; i < wall.SectionCount; i++)
325  {
326  totalDamage += wall.SectionDamage(i);
327  }
328  }
329  }
330  return (int)Math.Min(totalDamage * HullRepairCostPerDamage, MaxHullRepairCost);
331  }
332 
333  public static int GetItemRepairCost()
334  {
335  float totalRepairDuration = 0.0f;
336  foreach (Item item in Item.ItemList)
337  {
338  if (item.Submarine == null || item.Submarine.Info.Type != SubmarineType.Player) { continue; }
339  if (item.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(item.Submarine))
340  {
341  var repairable = item.GetComponent<Repairable>();
342  if (repairable == null) { continue; }
343  totalRepairDuration += repairable.FixDurationHighSkill * (1.0f - item.Condition / item.MaxCondition);
344  }
345  }
346  return (int)Math.Min(totalRepairDuration * ItemRepairCostPerRepairDuration, MaxItemRepairCost);
347  }
348 
349  public void InitFactions()
350  {
351  factions = new List<Faction>();
352  foreach (FactionPrefab factionPrefab in FactionPrefab.Prefabs)
353  {
354  factions.Add(new Faction(CampaignMetadata, factionPrefab));
355  }
356  }
357 
361  public event Action BeforeLevelLoading;
362 
366  public event Action OnSaveAndQuit;
367 
368  public override void AddExtraMissions(LevelData levelData)
369  {
370  if (levelData == null)
371  {
372  throw new ArgumentException("Level data was null.");
373  }
374 
375  extraMissions.Clear();
376 
377  var currentLocation = Map.CurrentLocation;
378  if (currentLocation == null)
379  {
380  throw new InvalidOperationException("Current location was null.");
381  }
382  if (levelData.Type == LevelData.LevelType.Outpost)
383  {
384  //if there's an available mission that takes place in the outpost, select it
385  foreach (var availableMission in currentLocation.AvailableMissions)
386  {
387  if (availableMission.Locations[0] == currentLocation && availableMission.Locations[1] == currentLocation)
388  {
389  currentLocation.SelectMission(availableMission);
390  }
391  }
392  }
393  else
394  {
395  foreach (Mission mission in currentLocation.SelectedMissions.ToList())
396  {
397  //if we had selected a mission that takes place in the outpost, deselect it when leaving the outpost
398  if (mission.Locations[0] == currentLocation &&
399  mission.Locations[1] == currentLocation)
400  {
401  currentLocation.DeselectMission(mission);
402  }
403  }
404  if (levelData.HasBeaconStation && !levelData.IsBeaconActive && Missions.None(m => m.Prefab.Type == MissionType.Beacon))
405  {
406  var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.IsSideObjective && m.Type == MissionType.Beacon);
407  if (beaconMissionPrefabs.Any())
408  {
409  var filteredMissions = beaconMissionPrefabs.Where(m => levelData.Difficulty >= m.MinLevelDifficulty && levelData.Difficulty <= m.MaxLevelDifficulty);
410  if (filteredMissions.None())
411  {
412  DebugConsole.AddWarning($"No suitable beacon mission found matching the level difficulty {levelData.Difficulty}. Ignoring the restriction.");
413  }
414  else
415  {
416  beaconMissionPrefabs = filteredMissions;
417  }
418  Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
419  var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, p => p.Commonness, rand);
420  extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub));
421  }
422  }
423  if (levelData.HasHuntingGrounds)
424  {
425  var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.IsSideObjective && m.Tags.Contains("huntinggrounds")).OrderBy(m => m.UintIdentifier);
426  if (!huntingGroundsMissionPrefabs.Any())
427  {
428  DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found.");
429  }
430  else
431  {
432  Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
433  // Adjust the prefab commonness based on the difficulty tag
434  var prefabs = huntingGroundsMissionPrefabs.ToList();
435  var weights = prefabs.Select(p => (float)Math.Max(p.Commonness, 1)).ToList();
436  for (int i = 0; i < prefabs.Count; i++)
437  {
438  var prefab = prefabs[i];
439  var weight = weights[i];
440  if (prefab.Tags.Contains("easy"))
441  {
442  weight *= MathHelper.Lerp(0.2f, 2f, MathUtils.InverseLerp(80, LevelData.HuntingGroundsDifficultyThreshold, levelData.Difficulty));
443  }
444  else if (prefab.Tags.Contains("hard"))
445  {
446  weight *= MathHelper.Lerp(0.5f, 1.5f, MathUtils.InverseLerp(LevelData.HuntingGroundsDifficultyThreshold + 10, 80, levelData.Difficulty));
447  }
448  weights[i] = weight;
449  }
450  var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(prefabs, weights, rand);
451  if (!Missions.Any(m => m.Prefab.Tags.Contains("huntinggrounds")))
452  {
453  extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub));
454  }
455  }
456  }
457  foreach (Faction faction in factions.OrderBy(f => f.Prefab.MenuOrder))
458  {
459  foreach (var automaticMission in faction.Prefab.AutomaticMissions)
460  {
461  if (faction.Reputation.Value < automaticMission.MinReputation || faction.Reputation.Value > automaticMission.MaxReputation) { continue; }
462 
463  if (automaticMission.DisallowBetweenOtherFactionOutposts && levelData.Type == LevelData.LevelType.LocationConnection)
464  {
465  if (Map.SelectedConnection.Locations.All(l => l.Faction != null && l.Faction != faction))
466  {
467  continue;
468  }
469  }
470  if (automaticMission.MaxDistanceFromFactionOutpost < int.MaxValue)
471  {
473  currentLocation,
474  automaticMission.MaxDistanceFromFactionOutpost,
475  loc => loc.Faction == faction))
476  {
477  continue;
478  }
479  }
480  Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed + TotalPassedLevels));
481  if (levelData.Type != automaticMission.LevelType) { continue; }
482  float probability =
483  MathHelper.Lerp(
484  automaticMission.MinProbability,
485  automaticMission.MaxProbability,
486  MathUtils.InverseLerp(automaticMission.MinReputation, automaticMission.MaxReputation, faction.Reputation.Value));
487  if (rand.NextDouble() < probability)
488  {
489  var missionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t == automaticMission.MissionTag)).OrderBy(m => m.UintIdentifier);
490  if (missionPrefabs.Any())
491  {
492  var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => p.Commonness, rand);
493  if (missionPrefab.Type == MissionType.Pirate && Missions.Any(m => m.Prefab.Type == MissionType.Pirate))
494  {
495  continue;
496  }
497  if (automaticMission.LevelType == LevelData.LevelType.Outpost)
498  {
499  extraMissions.Add(missionPrefab.Instantiate(new Location[] { currentLocation, currentLocation }, Submarine.MainSub));
500  }
501  else
502  {
503  extraMissions.Add(missionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub));
504  }
505  }
506  }
507  }
508  }
509  }
510  if (levelData.Biome.IsEndBiome)
511  {
512  Identifier endMissionTag = Identifier.Empty;
513  if (levelData.Type == LevelData.LevelType.LocationConnection)
514  {
515  int locationIndex = map.EndLocations.IndexOf(map.SelectedLocation);
516  if (locationIndex > -1)
517  {
518  endMissionTag = ("endlevel_locationconnection_" + locationIndex).ToIdentifier();
519  }
520  }
521  else
522  {
523  int locationIndex = map.EndLocations.IndexOf(map.CurrentLocation);
524  if (locationIndex > -1)
525  {
526  endMissionTag = ("endlevel_location_" + locationIndex).ToIdentifier();
527  }
528  }
529  if (!endMissionTag.IsEmpty)
530  {
531  var endLevelMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains(endMissionTag)).OrderBy(m => m.UintIdentifier);
532  if (endLevelMissionPrefabs.Any())
533  {
534  Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
535  var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => p.Commonness, rand);
536  if (Missions.All(m => m.Prefab.Type != endLevelMissionPrefab.Type))
537  {
538  if (levelData.Type == LevelData.LevelType.LocationConnection)
539  {
540  extraMissions.Add(endLevelMissionPrefab.Instantiate(map.SelectedConnection.Locations, Submarine.MainSub));
541  }
542  else
543  {
544  extraMissions.Add(endLevelMissionPrefab.Instantiate(new Location[] { map.CurrentLocation, map.CurrentLocation }, Submarine.MainSub));
545  }
546  }
547  }
548  }
549  }
550  }
551 
552 
553  public void LoadNewLevel()
554  {
555  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
556  {
557  return;
558  }
559 
560  if (CoroutineManager.IsCoroutineRunning("LevelTransition"))
561  {
562  DebugConsole.ThrowError("Level transition already running.\n" + Environment.StackTrace.CleanupStackTrace());
563  return;
564  }
565 
566  BeforeLevelLoading?.Invoke();
567  BeforeLevelLoading = null;
568 
569  if (Level.Loaded == null || Submarine.MainSub == null)
570  {
572  return;
573  }
574 
575  var availableTransition = GetAvailableTransition(out LevelData nextLevel, out Submarine leavingSub);
576 
577  if (availableTransition == TransitionType.None)
578  {
579  DebugConsole.ThrowErrorLocalized("Failed to load a new campaign level. No available level transitions " +
580  "(current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " +
581  "selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " +
582  "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
583  "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
584  "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" +
585  Environment.StackTrace.CleanupStackTrace());
586  return;
587  }
588  if (nextLevel == null)
589  {
590  DebugConsole.ThrowErrorLocalized("Failed to load a new campaign level. No available level transitions " +
591  "(transition type: " + availableTransition + ", " +
592  "current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " +
593  "selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " +
594  "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
595  "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
596  "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" +
597  Environment.StackTrace.CleanupStackTrace());
598  return;
599  }
600 #if CLIENT
601  ShowCampaignUI = ForceMapUI = false;
602 #endif
603  DebugConsole.NewMessage("Transitioning to " + (nextLevel?.Seed ?? "null") +
604  " (current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " +
605  "selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " +
606  "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
607  "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
608  "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ", " +
609  "transition type: " + availableTransition + ")");
610 
611  IsFirstRound = false;
613  CoroutineManager.StartCoroutine(DoLevelTransition(availableTransition, nextLevel, leavingSub, mirror), "LevelTransition");
614  }
615 
619  protected abstract void LoadInitialLevel();
620 
621  protected abstract IEnumerable<CoroutineStatus> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror);
622 
626  public TransitionType GetAvailableTransition(out LevelData nextLevel, out Submarine leavingSub)
627  {
628  if (Level.Loaded == null || Submarine.MainSub == null)
629  {
630  nextLevel = null;
631  leavingSub = null;
632  return TransitionType.None;
633  }
634 
635  leavingSub = GetLeavingSub();
636  if (leavingSub == null)
637  {
638  nextLevel = null;
639  return TransitionType.None;
640  }
641 
642  //currently travelling from location to another
643  if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
644  {
645  if (leavingSub.AtEndExit)
646  {
648  {
649  nextLevel = Level.Loaded.EndLocation.LevelData;
650  return TransitionType.ProgressToNextLocation;
651  }
652  else if (map.SelectedConnection != null)
653  {
654  nextLevel = map.SelectedConnection.LevelData;
655  return TransitionType.ProgressToNextEmptyLocation;
656  }
657  else
658  {
659  nextLevel = null;
660  return TransitionType.ProgressToNextEmptyLocation;
661  }
662  }
663  else if (leavingSub.AtStartExit)
664  {
666  {
667  nextLevel = map.CurrentLocation.LevelData;
668  return TransitionType.ReturnToPreviousLocation;
669  }
672  {
673  nextLevel = map.SelectedConnection.LevelData;
674  return TransitionType.LeaveLocation;
675  }
676  else
677  {
678  nextLevel = map.SelectedConnection?.LevelData;
679  return TransitionType.ReturnToPreviousEmptyLocation;
680  }
681  }
682  else
683  {
684  nextLevel = null;
685  return TransitionType.None;
686  }
687  }
688  else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
689  {
690  int currentEndLocationIndex = map.EndLocations.IndexOf(map.CurrentLocation);
691  if (currentEndLocationIndex > -1)
692  {
693  if (currentEndLocationIndex == map.EndLocations.Count - 1)
694  {
695  //at the last end location, end of campaign
696  nextLevel = map.StartLocation?.LevelData;
697  return TransitionType.End;
698  }
699  else if (leavingSub.AtEndExit && currentEndLocationIndex < map.EndLocations.Count - 1)
700  {
701  //more end locations to go, progress to the next one
702  nextLevel = map.EndLocations[currentEndLocationIndex + 1]?.LevelData;
703  return TransitionType.ProgressToNextLocation;
704  }
705  else
706  {
707  nextLevel = null;
708  return TransitionType.None;
709  }
710  }
711  else
712  {
713  nextLevel = map.SelectedLocation == null ? null : map.SelectedConnection?.LevelData;
714  return nextLevel == null ? TransitionType.None : TransitionType.LeaveLocation;
715  }
716  }
717  else
718  {
719  throw new NotImplementedException();
720  }
721  }
722 
724 
728  private static Submarine GetLeavingSub()
729  {
731  {
732  return Submarine.MainSub;
733  }
734  //in single player, only the sub the controlled character is inside can transition between levels
735  //in multiplayer, if there's subs at both ends of the level, only the one with more players inside can transition
736  //TODO: ignore players who don't have the permission to trigger a transition between levels?
737  var leavingPlayers = Character.CharacterList.Where(c => !c.IsDead && (c == Character.Controlled || c.IsRemotePlayer));
738 
739  CharacterTeamType submarineTeam = leavingPlayers.FirstOrDefault()?.TeamID ?? CharacterTeamType.Team1;
740 
741  //allow leaving if inside an outpost, and the submarine is either docked to it or close enough
742  Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers, submarineTeam);
743  Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers, submarineTeam);
744 
745  int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 :
746  leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost));
747  int playersInSubAtEnd = leavingSubAtEnd == null || !leavingSubAtEnd.AtEndExit ? 0 :
748  leavingPlayers.Count(c => c.Submarine == leavingSubAtEnd || leavingSubAtEnd.DockedTo.Contains(c.Submarine) || (Level.Loaded.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost));
749 
750  if (playersInSubAtStart == 0 && playersInSubAtEnd == 0)
751  {
752  return null;
753  }
754 
755  return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd;
756 
757  static Submarine GetLeavingSubAtStart(IEnumerable<Character> leavingPlayers, CharacterTeamType submarineTeam)
758  {
759  if (Level.Loaded.StartOutpost == null)
760  {
761  Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
762  if (closestSub == null) { return null; }
763  return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
764  }
765  else
766  {
767  //if there's a sub docked to the outpost, we can leave the level
768  if (Level.Loaded.StartOutpost.DockedTo.Any())
769  {
770  foreach (var dockedSub in Level.Loaded.StartOutpost.DockedTo)
771  {
772  if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { continue; }
773  return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
774  }
775  }
776 
777  //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
778  if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; }
779  Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
780  if (closestSub == null || !closestSub.AtStartExit) { return null; }
781  return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
782  }
783  }
784 
785  static Submarine GetLeavingSubAtEnd(IEnumerable<Character> leavingPlayers, CharacterTeamType submarineTeam)
786  {
787  if (Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.ExitPoints.Any())
788  {
789  Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
790  if (closestSub == null || !closestSub.AtEndExit) { return null; }
791  return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
792  }
793  //no "end" in outpost levels
794  if (Level.Loaded.Type == LevelData.LevelType.Outpost)
795  {
796  return null;
797  }
798 
799  if (Level.Loaded.EndOutpost == null)
800  {
801  Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
802  if (closestSub == null) { return null; }
803  return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
804  }
805  else
806  {
807  //if there's a sub docked to the outpost, we can leave the level
808  if (Level.Loaded.EndOutpost.DockedTo.Any())
809  {
810  foreach (var dockedSub in Level.Loaded.EndOutpost.DockedTo)
811  {
812  if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { continue; }
813  return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
814  }
815  }
816 
817  //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
818  if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; }
819  Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
820  if (closestSub == null || !closestSub.AtEndExit) { return null; }
821  return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
822  }
823  }
824  }
825 
826  public override void End(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
827  {
828  List<Item> takenItems = new List<Item>();
829  if (Level.Loaded?.Type == LevelData.LevelType.Outpost)
830  {
831  foreach (Item item in Item.ItemList)
832  {
833  if (!item.SpawnedInCurrentOutpost || item.OriginalModuleIndex < 0) { continue; }
834  var owner = item.GetRootInventoryOwner();
835  if ((!(owner?.Submarine?.Info?.IsOutpost ?? false)) || (owner is Character character && character.TeamID == CharacterTeamType.Team1) || item.Submarine == null || !item.Submarine.Info.IsOutpost)
836  {
837  takenItems.Add(item);
838  }
839  }
840  }
841  if (map != null && CargoManager != null)
842  {
844  if (transitionType != TransitionType.None)
845  {
847  }
848  }
849  if (GameMain.NetworkMember == null)
850  {
854  }
855  else
856  {
857  if (GameMain.NetworkMember.IsServer)
858  {
861  }
862  else if (GameMain.NetworkMember.IsClient)
863  {
865  }
866  }
867 
868  if (Level.Loaded?.StartOutpost != null)
869  {
870  List<Character> killedCharacters = new List<Character>();
871  foreach (Character c in Level.Loaded.StartOutpost.Info.OutpostNPCs.SelectMany(kpv => kpv.Value))
872  {
873  if (!c.IsDead && !c.Removed) { continue; }
874  killedCharacters.Add(c);
875  }
876  map.CurrentLocation.RegisterKilledCharacters(killedCharacters);
878  }
879 
880  List<Character> deadCharacters = Character.CharacterList.FindAll(c => c.IsDead);
881  foreach (Character c in deadCharacters)
882  {
883  if (c.IsDead)
884  {
886  c.DespawnNow(createNetworkEvents: false);
887  }
888  }
889 
890  //remove ID cards left in duffel bags
891  foreach (var item in Item.ItemList.ToList())
892  {
893  if (item.HasTag(Tags.IdCardTag) &&
894  (item.Container?.HasTag(Tags.DespawnContainer) ?? false))
895  {
896  item.Remove();
897  }
898  }
899 
900  foreach (CharacterInfo ci in CrewManager.GetCharacterInfos().ToList())
901  {
902  if (ci.CauseOfDeath != null)
903  {
905  }
906  }
907 
908  foreach (DockingPort port in DockingPort.List)
909  {
910  if (port.Door != null &
911  port.Item.Submarine.Info.Type == SubmarineType.Player &&
912  port.DockingTarget?.Item?.Submarine != null &&
914  {
915  port.Door.IsOpen = false;
916  }
917  }
918 
919  foreach (Item item in Item.ItemList)
920  {
921  if (item.Submarine is not Submarine sub) { continue; }
922  if (!sub.Info.IsPlayer) { continue; }
923  if (sub.TeamID != CharacterTeamType.Team1 && sub.TeamID != CharacterTeamType.Team2) { continue; }
924  if (item.GetComponent<Reactor>() is Reactor reactor && reactor.LastAIUser != null && reactor.LastUser == reactor.LastAIUser)
925  {
926  // Reactor managed by an AI crew ->
927  // Turn auto temp on, so that the reactor won't be unmanaged at beginning of the next round.
928  reactor.AutoTemp = true;
929  }
930  }
931  }
932 
936  public void HandleSaveAndQuit()
937  {
938  OnSaveAndQuit?.Invoke();
939  OnSaveAndQuit = null;
941  {
943  }
944  GameMain.GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
945  }
946 
950  public void UpdateStoreStock()
951  {
955  }
956 
957  public void EndCampaign()
958  {
959  foreach (Character c in Character.CharacterList)
960  {
961  if (c.IsOnPlayerTeam)
962  {
964  }
965  }
966  foreach (LocationConnection connection in Map.Connections)
967  {
968  connection.Difficulty = connection.Biome.AdjustedMaxDifficulty;
969  connection.LevelData = new LevelData(connection)
970  {
971  IsBeaconActive = false,
972  ForceOutpostGenerationParams = connection.LevelData.ForceOutpostGenerationParams
973  };
974  connection.LevelData.HasHuntingGrounds = connection.LevelData.OriginallyHadHuntingGrounds;
975  }
976  foreach (Location location in Map.Locations)
977  {
978  location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty)
979  {
980  ForceOutpostGenerationParams = location.LevelData.ForceOutpostGenerationParams
981  };
982  location.Reset(this);
983  }
986  Map.SelectLocation(-1);
987  if (Map.Radiation != null)
988  {
989  Map.Radiation.Amount = Map.Radiation.Params.StartingRadiation;
990  }
991  foreach (Location location in Map.Locations)
992  {
993  location.TurnsInRadiation = 0;
994  }
995  foreach (var faction in Factions)
996  {
997  faction.Reputation.SetReputation(faction.Prefab.InitialReputation);
998  }
1000 
1001  if (CampaignMetadata != null)
1002  {
1003  int loops = CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0);
1004  CampaignMetadata.SetValue("campaign.endings".ToIdentifier(), loops + 1);
1005  }
1006 
1007  //no tutorials after finishing the campaign once
1008  Settings.TutorialEnabled = false;
1009 
1010  GameAnalyticsManager.AddProgressionEvent(
1011  GameAnalyticsManager.ProgressionStatus.Complete,
1012  Preset?.Identifier.Value ?? "none");
1013  string eventId = "FinishCampaign:";
1014  GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
1015  GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0));
1016  GameAnalyticsManager.AddDesignEvent(eventId + "Money", Bank.Balance);
1017  GameAnalyticsManager.AddDesignEvent(eventId + "Playtime", TotalPlayTime);
1018  GameAnalyticsManager.AddDesignEvent(eventId + "PassedLevels", TotalPassedLevels);
1019  }
1020 
1021  protected virtual void EndCampaignProjSpecific() { }
1022 
1027  public Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty = true)
1028  {
1029  return GetRandomFaction(Factions, randSync, secondary: false, allowEmpty);
1030  }
1031 
1036  public Faction GetRandomSecondaryFaction(Rand.RandSync randSync, bool allowEmpty = true)
1037  {
1038  return GetRandomFaction(Factions, randSync, secondary: true, allowEmpty);
1039  }
1040 
1041  public static Faction GetRandomFaction(IEnumerable<Faction> factions, Rand.RandSync randSync, bool secondary = false, bool allowEmpty = true)
1042  {
1043  return GetRandomFaction(factions, Rand.GetRNG(randSync), secondary, allowEmpty);
1044  }
1045 
1046  public static Faction GetRandomFaction(IEnumerable<Faction> factions, Random random, bool secondary = false, bool allowEmpty = true)
1047  {
1048  List<Faction> factionsList = factions.OrderBy(f => f.Prefab.Identifier).ToList();
1049  List<float> weights = factionsList.Select(f => secondary ? f.Prefab.SecondaryControlledOutpostPercentage : f.Prefab.ControlledOutpostPercentage).ToList();
1050  float percentageSum = weights.Sum();
1051  if (percentageSum < 100.0f && allowEmpty)
1052  {
1053  //chance of non-faction-specific outposts if percentage of controlled outposts is <100
1054  factionsList.Add(null);
1055  weights.Add(100.0f - percentageSum);
1056  }
1057  return ToolBox.SelectWeightedRandom(factionsList, weights, random);
1058  }
1059 
1060  public bool TryHireCharacter(Location location, CharacterInfo characterInfo, bool takeMoney = true, Client client = null, bool buyingNewCharacter = false)
1061  {
1062  if (characterInfo == null) { return false; }
1063  if (characterInfo.MinReputationToHire.factionId != Identifier.Empty)
1064  {
1065  if (MathF.Round(GetReputation(characterInfo.MinReputationToHire.factionId)) < characterInfo.MinReputationToHire.reputation)
1066  {
1067  return false;
1068  }
1069  }
1070  var price = buyingNewCharacter ? NewCharacterCost(characterInfo) : HireManager.GetSalaryFor(characterInfo);
1071  if (takeMoney && !TryPurchase(client, price)) { return false; }
1072 
1073  characterInfo.IsNewHire = true;
1074  characterInfo.Title = null;
1075  location.RemoveHireableCharacter(characterInfo);
1076  CrewManager.AddCharacterInfo(characterInfo);
1077  GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown");
1078  return true;
1079  }
1080 
1081  public int NewCharacterCost(CharacterInfo characterInfo)
1082  {
1083  float characterCostPercentage = GameMain.NetworkMember?.ServerSettings.ReplaceCostPercentage ?? 100f;
1084  return (int)MathF.Round(HireManager.GetSalaryFor(characterInfo) * (characterCostPercentage/100f));
1085  }
1086 
1087  public bool CanAffordNewCharacter(CharacterInfo characterInfo)
1088  {
1089  return CanAfford(NewCharacterCost(characterInfo));
1090  }
1091 
1092  private void NPCInteract(Character npc, Character interactor)
1093  {
1094  if (!npc.AllowCustomInteract) { return; }
1095  NPCInteractProjSpecific(npc, interactor);
1096  string coroutineName = "DoCharacterWait." + (npc?.ID ?? Entity.NullEntityID);
1097  if (!CoroutineManager.IsCoroutineRunning(coroutineName))
1098  {
1099  CoroutineManager.StartCoroutine(DoCharacterWait(npc, interactor), coroutineName);
1100  }
1101  }
1102 
1103  private IEnumerable<CoroutineStatus> DoCharacterWait(Character npc, Character interactor)
1104  {
1105  if (npc == null || interactor == null) { yield return CoroutineStatus.Failure; }
1106 
1107  HumanAIController humanAI = npc.AIController as HumanAIController;
1108  if (humanAI == null) { yield return CoroutineStatus.Success; }
1109 
1110  var waitOrder = OrderPrefab.Prefabs["wait"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
1111  humanAI.SetForcedOrder(waitOrder);
1112  var waitObjective = humanAI.ObjectiveManager.ForcedOrder;
1113  humanAI.FaceTarget(interactor);
1114 
1115  while (!npc.Removed && !interactor.Removed &&
1116  Vector2.DistanceSquared(npc.WorldPosition, interactor.WorldPosition) < 300.0f * 300.0f &&
1117  humanAI.ObjectiveManager.ForcedOrder == waitObjective &&
1118  humanAI.AllowCampaignInteraction() &&
1119  !interactor.IsIncapacitated)
1120  {
1121  yield return CoroutineStatus.Running;
1122  }
1123 
1124 #if CLIENT
1125  ShowCampaignUI = false;
1126 #endif
1127  if (!npc.Removed)
1128  {
1129  humanAI.ClearForcedOrder();
1130  }
1131  yield return CoroutineStatus.Success;
1132  }
1133 
1134  partial void NPCInteractProjSpecific(Character npc, Character interactor);
1135 
1136  public void AssignNPCMenuInteraction(Character character, InteractionType interactionType)
1137  {
1138  character.CampaignInteractionType = interactionType;
1139 
1140  if (character.CampaignInteractionType == InteractionType.Store &&
1141  character.HumanPrefab is { Identifier: var merchantId })
1142  {
1143  character.MerchantIdentifier = merchantId;
1144  map.CurrentLocation?.GetStore(merchantId)?.SetMerchantFaction(character.Faction);
1145  }
1146 
1147  character.DisableHealthWindow =
1148  interactionType != InteractionType.None &&
1149  interactionType != InteractionType.Examine &&
1150  interactionType != InteractionType.Talk;
1151 
1152  if (interactionType == InteractionType.None)
1153  {
1154  character.SetCustomInteract(null, null);
1155  return;
1156  }
1157  character.SetCustomInteract(
1158  NPCInteract,
1159 #if CLIENT
1160  hudText: TextManager.GetWithVariable("CampaignInteraction." + interactionType, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)));
1161 #else
1162  hudText: TextManager.Get("CampaignInteraction." + interactionType));
1163 #endif
1164  }
1165 
1166  private readonly Dictionary<Character, float> characterOutOfBoundsTimer = new Dictionary<Character, float>();
1167 
1168  protected void KeepCharactersCloseToOutpost(float deltaTime)
1169  {
1170  const float MaxDist = 3000.0f;
1171  const float MinDist = 2500.0f;
1172 
1173  if (!Level.IsLoadedFriendlyOutpost) { return; }
1174 
1175  Rectangle worldBorders = Submarine.MainSub.GetDockedBorders();
1176  worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint();
1177 
1178  foreach (Character c in Character.CharacterList)
1179  {
1180  if ((c != Character.Controlled && !c.IsRemotePlayer) ||
1181  c.Removed || c.IsDead || c.IsIncapacitated || c.Submarine != null)
1182  {
1183  if (characterOutOfBoundsTimer.ContainsKey(c))
1184  {
1185  c.OverrideMovement = null;
1186  characterOutOfBoundsTimer.Remove(c);
1187  }
1188  continue;
1189  }
1190 
1191  if (c.WorldPosition.Y < worldBorders.Y - worldBorders.Height - MaxDist)
1192  {
1193  if (!characterOutOfBoundsTimer.ContainsKey(c))
1194  {
1195  characterOutOfBoundsTimer.Add(c, 0.0f);
1196  }
1197  else
1198  {
1199  characterOutOfBoundsTimer[c] += deltaTime;
1200  }
1201  }
1202  else if (c.WorldPosition.Y > worldBorders.Y - worldBorders.Height - MinDist)
1203  {
1204  if (characterOutOfBoundsTimer.ContainsKey(c))
1205  {
1206  c.OverrideMovement = null;
1207  characterOutOfBoundsTimer.Remove(c);
1208  }
1209  }
1210  }
1211 
1212  foreach (KeyValuePair<Character, float> character in characterOutOfBoundsTimer)
1213  {
1214  if (character.Value <= 0.0f)
1215  {
1216  if (IsSinglePlayer)
1217  {
1218 #if CLIENT
1220  TextManager.Get("RadioAnnouncerName"),
1221  TextManager.Get("TooFarFromOutpostWarning"),
1222  Networking.ChatMessageType.Default,
1223  sender: null);
1224 #endif
1225  }
1226  else
1227  {
1228 #if SERVER
1229  foreach (Networking.Client c in GameMain.Server.ConnectedClients)
1230  {
1231 
1232  GameMain.Server.SendDirectChatMessage(Networking.ChatMessage.Create(
1233  TextManager.Get("RadioAnnouncerName").Value,
1234  TextManager.Get("TooFarFromOutpostWarning").Value, Networking.ChatMessageType.Default, null), c);
1235  }
1236 #endif
1237  }
1238  }
1239  character.Key.OverrideMovement = Vector2.UnitY * 10.0f;
1240 #if CLIENT
1241  Character.DisableControls = true;
1242 #endif
1243  //if the character doesn't get back up in 10 seconds (something blocking the way?), teleport it closer
1244  if (character.Value > 10.0f)
1245  {
1246  Vector2 teleportPos = character.Key.WorldPosition;
1247  teleportPos += Vector2.Normalize(Submarine.MainSub.WorldPosition - character.Key.WorldPosition) * 100.0f;
1248  character.Key.AnimController.SetPosition(ConvertUnits.ToSimUnits(teleportPos));
1249  }
1250  }
1251  }
1252 
1253  public void OutpostNPCAttacked(Character npc, Character attacker, AttackResult attackResult)
1254  {
1255  if (npc == null || attacker == null || npc.IsDead || npc.IsInstigator) { return; }
1256  if (npc.TeamID != CharacterTeamType.FriendlyNPC) { return; }
1257  if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; }
1258 
1259  if (npc.Faction != null && Factions.FirstOrDefault(f => f.Prefab.Identifier == npc.Faction) is Faction faction)
1260  {
1261  faction.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage, Reputation.MaxReputationLossFromNPCDamage);
1262  }
1263  else
1264  {
1265  Location location = Map?.CurrentLocation;
1267  }
1268  }
1269 
1270  public Faction GetFaction(Identifier identifier)
1271  {
1272  return factions.Find(f => f.Prefab.Identifier == identifier);
1273  }
1274 
1275  public float GetReputation(Identifier factionIdentifier)
1276  {
1277  var faction =
1278  factionIdentifier == "location".ToIdentifier() ?
1279  factions.Find(f => f == Map?.CurrentLocation?.Faction) :
1280  factions.Find(f => f.Prefab.Identifier == factionIdentifier);
1281  return faction?.Reputation?.Value ?? 0.0f;
1282  }
1283 
1284  public FactionAffiliation GetFactionAffiliation(Identifier factionIdentifier)
1285  {
1286  var faction = GetFaction(factionIdentifier);
1287  return Faction.GetPlayerAffiliationStatus(faction);
1288  }
1289 
1290  public abstract void Save(XElement element);
1291 
1292  protected void LoadStats(XElement element)
1293  {
1294  TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0);
1295  TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0);
1296  DivingSuitWarningShown = element.GetAttributeBool(nameof(DivingSuitWarningShown).ToLowerInvariant(), false);
1297  }
1298 
1299  protected XElement SaveStats()
1300  {
1301  return new XElement("stats",
1302  new XAttribute(nameof(TotalPlayTime).ToLowerInvariant(), TotalPlayTime),
1303  new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels),
1304  new XAttribute(nameof(DivingSuitWarningShown).ToLowerInvariant(), DivingSuitWarningShown));
1305  }
1306 
1307  public void LogState()
1308  {
1309  DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White);
1310  DebugConsole.NewMessage(" Money: " + Bank.Balance, Color.White);
1311  DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.DisplayName, Color.White);
1312 
1313  DebugConsole.NewMessage(" Available destinations: ", Color.White);
1314  for (int i = 0; i < map.CurrentLocation.Connections.Count; i++)
1315  {
1316  Location destination = map.CurrentLocation.Connections[i].OtherLocation(map.CurrentLocation);
1317  if (destination == map.SelectedLocation)
1318  {
1319  DebugConsole.NewMessage(" " + i + ". " + destination.DisplayName + " [SELECTED]", Color.White);
1320  }
1321  else
1322  {
1323  DebugConsole.NewMessage(" " + i + ". " + destination.DisplayName, Color.White);
1324  }
1325  }
1326 
1327  if (map.CurrentLocation != null)
1328  {
1329  foreach (Mission mission in map.CurrentLocation.SelectedMissions)
1330  {
1331  DebugConsole.NewMessage(" Selected mission: " + mission.Name, Color.White);
1332  DebugConsole.NewMessage("\n" + mission.Description, Color.White);
1333  }
1334  }
1335  }
1336 
1337  public override void Remove()
1338  {
1339  base.Remove();
1340  map?.Remove();
1341  map = null;
1342  }
1343 
1345  {
1346  return Map?.CurrentLocation?.SelectedMissions?.Count(m => m.Locations.Contains(location)) ?? 0;
1347  }
1348 
1349  public void CheckTooManyMissions(Location currentLocation, Client sender)
1350  {
1351  foreach (Location location in currentLocation.Connections.Select(c => c.OtherLocation(currentLocation)))
1352  {
1353  if (NumberOfMissionsAtLocation(location) > Settings.TotalMaxMissionCount)
1354  {
1355  DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.DisplayName}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions.");
1356  foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.TotalMaxMissionCount).ToList())
1357  {
1358  currentLocation.DeselectMission(mission);
1359  }
1360  }
1361  }
1362  }
1363 
1364  protected static void LeaveUnconnectedSubs(Submarine leavingSub)
1365  {
1366  if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub))
1367  {
1368  Submarine.MainSub = leavingSub;
1369  GameMain.GameSession.Submarine = leavingSub;
1370  GameMain.GameSession.SubmarineInfo = leavingSub.Info;
1371  leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub");
1372  var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub);
1373  GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info);
1374  foreach (Submarine sub in subsToLeaveBehind)
1375  {
1376  GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name);
1377  MapEntity.MapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine);
1378  LinkedSubmarine.CreateDummy(leavingSub, sub);
1379  }
1380  }
1381  }
1382 
1383  public void SwitchSubs()
1384  {
1386  {
1388  }
1390  SwitchedSubsThisRound = true;
1391  PendingSubmarineSwitch = null;
1392  }
1393 
1397  protected void TransferItemsBetweenSubs()
1398  {
1399  Submarine currentSub = GameMain.GameSession.Submarine;
1400  if (currentSub == null || currentSub.Removed)
1401  {
1402  DebugConsole.ThrowError("Cannot transfer items between subs, because the current sub is null or removed!");
1403  return;
1404  }
1405  var itemsToTransfer = new List<(Item item, Item container)>();
1406  if (PendingSubmarineSwitch != null)
1407  {
1408  var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet();
1409  // Remove items from the old sub
1410  foreach (Item item in Item.ItemList)
1411  {
1412  if (item.Removed) { continue; }
1413  if (item.NonInteractable || item.NonPlayerTeamInteractable) { continue; }
1414  if (item.IsHidden) { continue; }
1415  if (!connectedSubs.Contains(item.Submarine)) { continue; }
1416  if (item.Prefab.DontTransferBetweenSubs) { continue; }
1417  if (AnyParentInventoryDisableTransfer(item)) { continue; }
1418  var rootOwner = item.GetRootInventoryOwner();
1419  if (rootOwner is Character) { continue; }
1420  if (rootOwner is Item ownerItem && (ownerItem.NonInteractable || item.NonPlayerTeamInteractable || ownerItem.IsHidden)) { continue; }
1421  if (item.GetComponent<Door>() != null) { continue; }
1422  if (item.Components.None(c => c is Pickable)) { continue; }
1423  if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; }
1424  if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; }
1425  itemsToTransfer.Add((item, item.Container));
1426  item.Submarine = null;
1427 
1428  static bool AnyParentInventoryDisableTransfer(Item item)
1429  {
1430  if (item.ParentInventory?.Owner is not Item parentOwner) { return false; }
1431  return HasProblematicComponent(parentOwner) || AnyParentInventoryDisableTransfer(parentOwner);
1432 
1433  static bool HasProblematicComponent(Item it)
1434  => it.Components.Any(static c => c.DontTransferInventoryBetweenSubs);
1435  }
1436  }
1437  foreach (var (item, container) in itemsToTransfer)
1438  {
1439  if (container?.Submarine != null)
1440  {
1441  // Drop the item if it's not inside another item set to be transferred.
1442  item.Drop(null, createNetworkEvent: false, setTransform: false);
1443  //dropping items sets the sub, set it back to null
1444  item.Submarine = null;
1445  foreach (var itemContainer in item.GetComponents<ItemContainer>())
1446  {
1447  itemContainer.Inventory.FindAllItems((_) => true, recursive: true).ForEach(it => it.Submarine = null);
1448  }
1449  }
1450  }
1451  System.Diagnostics.Debug.Assert(itemsToTransfer.None(it => it.item.Submarine != null), "Item that was set to be transferred was not removed from the sub!");
1452  currentSub.Info.NoItems = true;
1453  }
1454  // Serialize the current sub
1455  GameMain.GameSession.SubmarineInfo = new SubmarineInfo(currentSub);
1456  if (PendingSubmarineSwitch != null && itemsToTransfer.Any())
1457  {
1458  // Load the new sub
1459  var newSub = new Submarine(PendingSubmarineSwitch);
1460  var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player);
1461  WayPoint wp = WayPoint.WayPointList.FirstOrDefault(wp => wp.SpawnType == SpawnType.Cargo && connectedSubs.Contains(wp.Submarine));
1462  Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.FirstOrDefault(h => connectedSubs.Contains(h.Submarine) && !h.IsWetRoom);
1463  if (spawnHull == null)
1464  {
1465  DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub.");
1466  return;
1467  }
1468  // First move the cargo containers, so that we can reuse them
1469  var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag(Tags.Crate)).ToHashSet();
1470  foreach (var (item, _) in cargoContainers)
1471  {
1472  Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab));
1473  item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false);
1474  item.CurrentHull = spawnHull;
1475  item.Submarine = spawnHull.Submarine;
1476  }
1477  // Then move the other items
1478  List<ItemContainer> availableContainers = CargoManager.FindReusableCargoContainers(connectedSubs).ToList();
1479  foreach (var (item, oldContainer) in itemsToTransfer)
1480  {
1481  if (cargoContainers.Contains((item, oldContainer))) { continue; }
1482  Item newContainer = null;
1483  item.Submarine = newSub;
1484  if (item.Container == null)
1485  {
1486  newContainer = newSub.FindContainerFor(item, onlyPrimary: true, checkTransferConditions: true, allowConnectedSubs: true);
1487  }
1488  string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})";
1489  if (item.Container == null && (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null, createNetworkEvent: false)))
1490  {
1491  var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers);
1492  if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false))
1493  {
1494  Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab));
1495  item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false);
1496  }
1497  else
1498  {
1499  if (cargoContainer.Item.Submarine is Submarine containerSub)
1500  {
1501  // Use the item's sub in case the sub consists of multiple linked subs.
1502  item.Submarine = containerSub;
1503  }
1504  newContainerName = cargoContainer.Item.Prefab.Identifier.ToString();
1505  }
1506  }
1507  string msg;
1508  if (oldContainer != null)
1509  {
1510  if (newContainer == null && oldContainer == item.Container)
1511  {
1512  msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) contained inside {oldContainer.Prefab.Identifier} ({oldContainer.ID})";
1513  }
1514  else
1515  {
1516  msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) from {oldContainer.Prefab.Identifier} ({oldContainer.Tags}) to {newContainerName}";
1517  }
1518  }
1519  else
1520  {
1521  msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) to {newContainerName}";
1522  }
1523 #if DEBUG
1524  DebugConsole.NewMessage(msg);
1525 #else
1526  DebugConsole.Log(msg);
1527 #endif
1528  }
1529 
1530  foreach (var (item, _) in itemsToTransfer)
1531  {
1532  // This ensures that the new submarine takes ownership of
1533  // the items contained within the items that are being transferred directly,
1534  // i.e. circuit box components and wires
1535  PropagateSubmarineProperty(item);
1536  }
1537 
1538  static void PropagateSubmarineProperty(Item item)
1539  {
1540  foreach (var ownedContainer in item.GetComponents<ItemContainer>())
1541  {
1542  foreach (var containedItem in ownedContainer.Inventory.AllItems)
1543  {
1544  containedItem.Submarine = item.Submarine;
1545  PropagateSubmarineProperty(containedItem);
1546  }
1547  }
1548  }
1549 
1550  newSub.Info.NoItems = false;
1551  // Serialize the new sub
1552  PendingSubmarineSwitch = new SubmarineInfo(newSub);
1553  }
1554  }
1555 
1556  protected void RefreshOwnedSubmarines()
1557  {
1558  if (PendingSubmarineSwitch != null)
1559  {
1562 
1563  for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++)
1564  {
1565  if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name)
1566  {
1567  GameMain.GameSession.OwnedSubmarines[i] = previousSub;
1568  break;
1569  }
1570  }
1571  }
1572  }
1573 
1574  public void SavePets(XElement parentElement = null)
1575  {
1576  petsElement = new XElement("pets");
1578  parentElement?.Add(petsElement);
1579  }
1580 
1581  public void LoadPets()
1582  {
1583  if (petsElement != null)
1584  {
1586  }
1587  }
1588 
1589  public void SaveActiveOrders(XElement parentElement = null)
1590  {
1591  ActiveOrdersElement = new XElement("activeorders");
1593  parentElement?.Add(ActiveOrdersElement);
1594  }
1595 
1596  public void LoadActiveOrders()
1597  {
1599  }
1600  }
1601 }
readonly bool IsEndBiome
Definition: Biome.cs:16
float AdjustedMaxDifficulty
Definition: Biome.cs:22
TransitionType GetAvailableTransition(out LevelData nextLevel, out Submarine leavingSub)
Which type of transition between levels is currently possible (if any)
Action BeforeLevelLoading
Automatically cleared after triggering -> no need to unregister
static Faction GetRandomFaction(IEnumerable< Faction > factions, Random random, bool secondary=false, bool allowEmpty=true)
Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their ControlledOutpostPercentage
CampaignMode(GameModePreset preset, CampaignSettings settings)
TransitionType GetAvailableTransition()
virtual Wallet Wallet
Gets the current personal wallet In singleplayer this is the campaign bank and in multiplayer this is...
FactionAffiliation GetFactionAffiliation(Identifier factionIdentifier)
void AssignNPCMenuInteraction(Character character, InteractionType interactionType)
Faction GetRandomSecondaryFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their SecondaryControlledOutpostPercentage
void HandleSaveAndQuit()
Handles updating store stock, registering event history and relocating items (i.e....
abstract IEnumerable< CoroutineStatus > DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror)
Location GetCurrentDisplayLocation()
The location that's displayed as the "current one" in the map screen. Normally the current outpost or...
Action OnSaveAndQuit
Triggers when saving and quitting mid-round (as in, not just transferring to a new level)....
override void End(CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None)
void CheckTooManyMissions(Location currentLocation, Client sender)
static bool BlocksInteraction(InteractionType interactionType)
abstract void LoadInitialLevel()
Load the first level and start the round after loading a save file
abstract void Save(XElement element)
readonly record struct SaveInfo(string FilePath, Option< SerializableDateTime > SaveTime, string SubmarineName, ImmutableArray< string > EnabledContentPackageNames) const int MaxMoney
static Faction GetRandomFaction(IEnumerable< Faction > factions, Rand.RandSync randSync, bool secondary=false, bool allowEmpty=true)
void OutpostNPCAttacked(Character npc, Character attacker, AttackResult attackResult)
void UpdateStoreStock()
Updates store stock before saving the game
bool TryHireCharacter(Location location, CharacterInfo characterInfo, bool takeMoney=true, Client client=null, bool buyingNewCharacter=false)
static List< Submarine > GetSubsToLeaveBehind(Submarine leavingSub)
Dictionary< Identifier, List< PurchasedItem > > PurchasedItems
static ItemContainer GetOrCreateCargoContainerFor(ItemPrefab item, ISpatialEntity cargoRoomOrSpawnPoint, ref List< ItemContainer > availableContainers)
static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab)
Dictionary< Identifier, List< SoldItem > > SoldItems
static IEnumerable< ItemContainer > FindReusableCargoContainers(IEnumerable< Submarine > subs, IEnumerable< Hull > cargoRooms=null)
void SetCustomInteract(Action< Character, Character > onCustomInteract, LocalizedString hudText)
Set an action that's invoked when another character interacts with this one.
void AddMessage(string rawText, Color color, bool playSound, Identifier identifier=default, int? value=null, float lifetime=3.0f)
bool IsRemotePlayer
Is the character controlled by another human player (should always be false in single player)
Stores information about the Character that is needed between rounds in the menu etc....
bool IsNewHire
Newly hired bot that hasn't spawned yet
Responsible for keeping track of the characters in the player crew, saving and loading their orders,...
void RemoveCharacterInfo(CharacterInfo characterInfo)
Remove info of a selected character. The character will not be visible in any menus or the round summ...
IEnumerable< CharacterInfo > GetCharacterInfos()
Note: this only returns AI characters' infos in multiplayer. The infos are used to manage hiring/firi...
void AddSinglePlayerChatMessage(LocalizedString senderName, LocalizedString text, ChatMessageType messageType, Entity sender)
Adds the message to the single player chatbox.
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
void RegisterEventHistory(bool registerFinishedOnly=false)
Registers the exhaustible events in the level as exhausted, and adds the current events to the event ...
FactionPrefab Prefab
Definition: Factions.cs:18
static FactionAffiliation GetPlayerAffiliationStatus(Faction faction)
Get what kind of affiliation this faction has towards the player depending on who they chose to side ...
Definition: Factions.cs:30
Reputation Reputation
Definition: Factions.cs:17
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
readonly Identifier Identifier
static int GetSalaryFor(IReadOnlyCollection< CharacterInfo > hires)
Definition: HireManager.cs:24
static readonly List< Hull > HullList
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
bool NonPlayerTeamInteractable
Use IsPlayerInteractable to also check NonInteractable
void SetTransform(Vector2 simPosition, float rotation, bool findNewHull=true, bool setPrevTransform=true)
static readonly List< Item > ItemList
override bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
JobPrefab Prefab
Definition: Job.cs:18
OutpostGenerationParams ForceOutpostGenerationParams
Definition: LevelData.cs:44
readonly float Difficulty
Definition: LevelData.cs:23
readonly LevelType Type
Definition: LevelData.cs:19
const float HuntingGroundsDifficultyThreshold
Minimum difficulty of the level before hunting grounds can appear.
Definition: LevelData.cs:37
readonly string Seed
Definition: LevelData.cs:21
readonly Biome Biome
Definition: LevelData.cs:25
static bool IsLoadedFriendlyOutpost
Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1). Does not take reput...
static bool IsLoadedOutpost
Is there a loaded level set and is it an outpost?
static LinkedSubmarine CreateDummy(Submarine mainSub, Submarine linkedSub)
void SetMerchantFaction(Identifier factionIdentifier)
Definition: Location.cs:350
void RemoveHireableCharacter(CharacterInfo character)
Definition: Location.cs:1101
void AddStock(Dictionary< Identifier, List< SoldItem >> items)
Definition: Location.cs:1461
readonly List< LocationConnection > Connections
Definition: Location.cs:56
LocationType Type
Definition: Location.cs:91
StoreInfo GetStore(Identifier identifier)
Definition: Location.cs:1299
void Reset(CampaignMode campaign)
Definition: Location.cs:1518
Reputation Reputation
Definition: Location.cs:103
IEnumerable< Mission > SelectedMissions
Definition: Location.cs:442
void RegisterTakenItems(IEnumerable< Item > items)
Mark the items that have been taken from the outpost to prevent them from spawning when re-entering t...
Definition: Location.cs:1243
LocalizedString DisplayName
Definition: Location.cs:58
void RemoveStock(Dictionary< Identifier, List< PurchasedItem >> items)
Definition: Location.cs:1473
LevelData LevelData
Definition: Location.cs:95
void RegisterKilledCharacters(IEnumerable< Character > characters)
Mark the characters who have been killed to prevent them from spawning when re-entering the outpost
Definition: Location.cs:1261
Mersenne Twister based random
Definition: MTRandom.cs:9
int OriginalModuleIndex
The index of the outpost module this entity originally spawned in (-1 if not an outpost item)
static readonly List< MapEntity > MapEntityList
bool IsHidden
Is the entity hidden due to HiddenInGame being enabled or the layer the entity is in being hidden?
IReadOnlyList< Location > EndLocations
static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func< Location, bool > criteria, Func< LocationConnection, bool > connectionCriteria=null)
List< LocationConnection > Connections
static readonly PrefabCollection< MissionPrefab > Prefabs
Marks fields and properties as to be serialized and deserialized by INetSerializableStruct....
static void LoadPets(XElement petsElement)
Definition: PetBehavior.cs:432
static void SavePets(XElement petsElement)
Definition: PetBehavior.cs:397
readonly Identifier Identifier
Definition: Prefab.cs:34
float??????? Value
Definition: Reputation.cs:44
const float MaxReputationLossFromNPCDamage
Maximum amount of reputation loss you can get from damaging outpost NPCs per round
Definition: Reputation.cs:19
void AddReputation(float reputationChange, float maxReputationChangePerRound=float.MaxValue)
Definition: Reputation.cs:95
const float ReputationLossPerNPCDamage
Definition: Reputation.cs:10
void SetDamage(int sectionIndex, float damage, Character attacker=null, bool createNetworkEvent=true, bool isNetworkEvent=true, bool createExplosionEffect=true, bool createWallDamageProjectiles=false)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
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
readonly Dictionary< Identifier, List< Character > > OutpostNPCs
This class handles all upgrade logic. Storing, applying, checking and validation of upgrades.
FactionAffiliation
Definition: Factions.cs:9
DateTime wrapper that tries to offer a reliable string representation that's also human-friendly