Client LuaCsForBarotrauma
Location.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Linq;
8 using System.Xml.Linq;
9 
10 namespace Barotrauma
11 {
12  partial class Location
13  {
14  public class TakenItem
15  {
16  public readonly ushort OriginalID;
17  public readonly ushort ModuleIndex;
18  public readonly Identifier Identifier;
19  public readonly int OriginalContainerIndex;
20 
21  public TakenItem(Identifier identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex)
22  {
23  OriginalID = originalID;
24  OriginalContainerIndex = originalContainerIndex;
25  ModuleIndex = moduleIndex;
26  Identifier = identifier;
27  }
28 
29  public TakenItem(Item item)
30  {
31  System.Diagnostics.Debug.Assert(item.OriginalModuleIndex >= 0, "Trying to add a non-outpost item to a location's taken items");
32 
34  OriginalID = item.ID;
35  ModuleIndex = (ushort) item.OriginalModuleIndex;
37  }
38 
39  public bool IsEqual(TakenItem obj)
40  {
42  }
43 
44  public bool Matches(Item item)
45  {
47  {
48  return item.OriginalContainerIndex == OriginalContainerIndex && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier;
49  }
50  else
51  {
52  return item.ID == OriginalID && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier;
53  }
54  }
55  }
56 
57  public readonly List<LocationConnection> Connections = new List<LocationConnection>();
58 
59  public LocalizedString DisplayName { get; private set; }
60 
61  public Identifier NameIdentifier => nameIdentifier;
62 
63  private int nameFormatIndex;
64  private Identifier nameIdentifier;
65 
66  public int NameFormatIndex => nameFormatIndex;
67 
71  private string rawName;
72 
73  private LocationType addInitialMissionsForType;
74 
75  public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false;
76 
77  public bool Visited => GameMain.GameSession?.Map?.IsVisited(this) ?? false;
78 
79  public readonly Dictionary<LocationTypeChange.Requirement, int> ProximityTimer = new Dictionary<LocationTypeChange.Requirement, int>();
80  public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange;
82 
86  public bool LocationTypeChangesBlocked => DisallowLocationTypeChanges || availableMissions.Any(m => !m.Completed && m.Prefab.BlockLocationTypeChanges);
87 
89 
90  public Biome Biome { get; set; }
91 
92  public Vector2 MapPosition { get; private set; }
93 
94  public LocationType Type { get; private set; }
95 
96  public LocationType OriginalType { get; private set; }
97 
98  public LevelData LevelData { get; set; }
99 
100  public int PortraitId { get; private set; }
101 
102  public Faction Faction { get; set; }
103 
104  public Faction SecondaryFaction { get; set; }
105 
107 
109 
110  public int TurnsInRadiation { get; set; }
111 
112  #region Store
113 
114  public class StoreInfo
115  {
116  public Identifier Identifier { get; }
117  public Identifier MerchantFaction { get; private set; }
118  public int Balance { get; set; }
119  public List<PurchasedItem> Stock { get; } = new List<PurchasedItem>();
120  public List<ItemPrefab> DailySpecials { get; } = new List<ItemPrefab>();
121  public List<ItemPrefab> RequestedGoods { get; } = new List<ItemPrefab>();
125  public int PriceModifier { get; set; }
126  public Location Location { get; }
127  private float MaxReputationModifier => Location.StoreMaxReputationModifier;
128 
129  private StoreInfo(Location location)
130  {
131  Location = location;
132  }
133 
137  public StoreInfo(Location location, Identifier identifier) : this(location)
138  {
139  Identifier = identifier;
140  Balance = location.StoreInitialBalance;
141  Stock = CreateStock();
144  }
145 
149  public StoreInfo(Location location, XElement storeElement) : this(location)
150  {
151  Identifier = storeElement.GetAttributeIdentifier("identifier", "");
152  MerchantFaction = storeElement.GetAttributeIdentifier(nameof(MerchantFaction), "");
153  Balance = storeElement.GetAttributeInt("balance", location.StoreInitialBalance);
154  PriceModifier = storeElement.GetAttributeInt("pricemodifier", 0);
155  // Backwards compatibility: before introducing support for multiple stores, this value was saved as a store element attribute
156  if (storeElement.Attribute("stepssincespecialsupdated") != null)
157  {
158  location.StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0);
159  }
160  foreach (var stockElement in storeElement.GetChildElements("stock"))
161  {
162  var identifier = stockElement.GetAttributeIdentifier("id", Identifier.Empty);
163  if (identifier.IsEmpty || ItemPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { continue; }
164  int qty = stockElement.GetAttributeInt("qty", 0);
165  if (qty < 1) { continue; }
166  Stock.Add(new PurchasedItem(prefab, qty, buyer: null));
167  }
168  if (storeElement.GetChildElement("dailyspecials") is XElement specialsElement)
169  {
170  var loadedDailySpecials = LoadStoreSpecials(specialsElement);
171  DailySpecials.AddRange(loadedDailySpecials);
172  }
173  if (storeElement.GetChildElement("requestedgoods") is XElement goodsElement)
174  {
175  var loadedRequestedGoods = LoadStoreSpecials(goodsElement);
176  RequestedGoods.AddRange(loadedRequestedGoods);
177  }
178 
179  static List<ItemPrefab> LoadStoreSpecials(XElement element)
180  {
181  var specials = new List<ItemPrefab>();
182  foreach (var childElement in element.GetChildElements("item"))
183  {
184  var id = childElement.GetAttributeIdentifier("id", Identifier.Empty);
185  if (id.IsEmpty || ItemPrefab.FindByIdentifier(id) is not ItemPrefab prefab) { continue; }
186  specials.Add(prefab);
187  }
188  return specials;
189  }
190  }
191 
192  public static PurchasedItem CreateInitialStockItem(ItemPrefab itemPrefab, PriceInfo priceInfo)
193  {
194  int quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1);
195  return new PurchasedItem(itemPrefab, quantity, buyer: null);
196  }
197 
198  public List<PurchasedItem> CreateStock()
199  {
200  var stock = new List<PurchasedItem>();
201  foreach (var prefab in ItemPrefab.Prefabs)
202  {
203  if (!prefab.CanBeBoughtFrom(this, out var priceInfo)) { continue; }
204  stock.Add(CreateInitialStockItem(prefab, priceInfo));
205  }
206  return stock;
207  }
208 
209  public void AddStock(List<SoldItem> items)
210  {
211  if (items == null || items.None()) { return; }
212  DebugConsole.NewMessage($"Adding items to stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly: true);
213  foreach (var item in items)
214  {
215  if (Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem)
216  {
217  stockItem.Quantity += 1;
218  DebugConsole.NewMessage($"Added 1x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly: true);
219  }
220  else
221  {
222  DebugConsole.NewMessage($"{item.ItemPrefab.Name} not sold at location, can't add", Color.Cyan, debugOnly: true);
223  }
224  }
225  }
226 
227  public void RemoveStock(List<PurchasedItem> items)
228  {
229  if (items == null || items.None()) { return; }
230  DebugConsole.NewMessage($"Removing items from stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly: true);
231  foreach (PurchasedItem item in items)
232  {
233  if (Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem)
234  {
235  stockItem.Quantity = Math.Max(stockItem.Quantity - item.Quantity, 0);
236  DebugConsole.NewMessage($"Removed {item.Quantity}x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly: true);
237  }
238  }
239  }
240 
241  public void GenerateSpecials()
242  {
243  var availableStock = new Dictionary<ItemPrefab, float>();
244  foreach (var stockItem in Stock)
245  {
246  if (stockItem.Quantity < 1) { continue; }
247  float weight = 1.0f;
248  if (stockItem.ItemPrefab.GetPriceInfo(this) is PriceInfo priceInfo)
249  {
250  if (!priceInfo.CanBeSpecial) { continue; }
251  var baseQuantity = priceInfo.MinAvailableAmount;
252  weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity;
253  if (weight < 0.0f) { continue; }
254  }
255  availableStock.Add(stockItem.ItemPrefab, weight);
256  }
257  DailySpecials.Clear();
258  int extraSpecialSalesCount = GetExtraSpecialSalesCount();
259  for (int i = 0; i < Location.DailySpecialsCount + extraSpecialSalesCount; i++)
260  {
261  if (availableStock.None()) { break; }
262  var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced);
263  if (item == null) { break; }
264  DailySpecials.Add(item);
265  availableStock.Remove(item);
266  }
267  RequestedGoods.Clear();
268  for (int i = 0; i < Location.RequestedGoodsCount; i++)
269  {
270  var item = ItemPrefab.Prefabs.GetRandom(p =>
271  p.CanBeSold && !RequestedGoods.Contains(p) &&
272  p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced);
273  if (item == null) { break; }
274  RequestedGoods.Add(item);
275  }
276  Location.StepsSinceSpecialsUpdated = 0;
277  }
278 
279  public void GeneratePriceModifier()
280  {
281  PriceModifier = Rand.Range(-Location.StorePriceModifierRange, Location.StorePriceModifierRange + 1);
282  }
283 
286  public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerDailySpecials = true)
287  {
288  priceInfo ??= item?.GetPriceInfo(this);
289  if (priceInfo == null) { return 0; }
290  float price = priceInfo.Price;
291  // Adjust by random price modifier
292  price = (100 + PriceModifier) / 100.0f * price;
293  price *= priceInfo.BuyingPriceMultiplier;
294  // Adjust by daily special status
295  if (considerDailySpecials && DailySpecials.Contains(item))
296  {
297  price = Location.DailySpecialPriceModifier * price;
298  }
299  // Adjust by current reputation
300  price *= GetReputationModifier(true);
301 
302  // Adjust by campaign difficulty settings
303  if (GameMain.GameSession?.Campaign is CampaignMode campaign)
304  {
305  price *= campaign.Settings.ShopPriceMultiplier;
306  }
307 
308  var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
309  if (characters.Any())
310  {
312  if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive)
313  {
314  price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false));
315  price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll));
316  price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag)));
317  }
318  price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false));
319  price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag)));
320  }
321  // Price should never go below 1 mk
322  return Math.Max((int)price, 1);
323  }
324 
327  public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true)
328  {
329  priceInfo ??= item?.GetPriceInfo(this);
330  if (priceInfo == null) { return 0; }
331  float price = Location.StoreSellPriceModifier * priceInfo.Price;
332  // Adjust by random price modifier
333  price = (100 - PriceModifier) / 100.0f * price;
334  // Adjust by requested good status
335  if (considerRequestedGoods && RequestedGoods.Contains(item))
336  {
337  price = Location.RequestGoodPriceModifier * price;
338  }
339  // Adjust by location reputation
340  price *= GetReputationModifier(false);
341 
342  var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
343  if (characters.Any())
344  {
345  price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false));
346  price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)));
347  }
348 
349  // Price should never go below 1 mk
350  return Math.Max((int)price, 1);
351  }
352 
353  public void SetMerchantFaction(Identifier factionIdentifier)
354  {
355  MerchantFaction = factionIdentifier;
356  }
357 
359  {
360  return MerchantFaction.IfEmpty(Location.Faction?.Prefab.Identifier ?? Identifier.Empty);
361  }
362 
363  public float GetReputationModifier(bool buying)
364  {
365  var factionIdentifier = GetMerchantOrLocationFactionIdentifier();
366  var reputation = GameMain.GameSession.Campaign.GetFaction(factionIdentifier)?.Reputation;
367  if (reputation == null) { return 1.0f; }
368  if (buying)
369  {
370  if (reputation.Value > 0.0f)
371  {
372  return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MaxReputation);
373  }
374  else
375  {
376  return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation);
377  }
378  }
379  else
380  {
381  if (reputation.Value > 0.0f)
382  {
383  return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MaxReputation);
384  }
385  else
386  {
387  return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation);
388  }
389  }
390  }
391 
392  public override string ToString()
393  {
394  return Identifier.Value;
395  }
396  }
397 
398  public Dictionary<Identifier, StoreInfo> Stores { get; set; }
399 
400  private float StoreMaxReputationModifier => Type.StoreMaxReputationModifier;
401  private float StoreSellPriceModifier => Type.StoreSellPriceModifier;
402  private float DailySpecialPriceModifier => Type.DailySpecialPriceModifier;
403  private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier;
405  private int StorePriceModifierRange => Type.StorePriceModifierRange;
406 
410  private const int SpecialsUpdateInterval = 3;
413  private int StepsSinceSpecialsUpdated { get; set; }
414  public HashSet<Identifier> StoreIdentifiers { get; } = new HashSet<Identifier>();
415 
416  #endregion
417 
418  private const float MechanicalMaxDiscountPercentage = 50.0f;
419  private const float HealMaxDiscountPercentage = 10.0f;
420 
421  private readonly List<TakenItem> takenItems = new List<TakenItem>();
422  public IEnumerable<TakenItem> TakenItems
423  {
424  get { return takenItems; }
425  }
426 
427  private readonly HashSet<int> killedCharacterIdentifiers = new HashSet<int>();
428  public IEnumerable<int> KilledCharacterIdentifiers
429  {
430  get { return killedCharacterIdentifiers; }
431  }
432 
433  private readonly List<Mission> availableMissions = new List<Mission>();
434  public IEnumerable<Mission> AvailableMissions
435  {
436  get
437  {
438  availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry));
439  return availableMissions;
440  }
441  }
442 
443  private readonly List<Mission> selectedMissions = new List<Mission>();
444  public IEnumerable<Mission> SelectedMissions
445  {
446  get
447  {
448  selectedMissions.RemoveAll(m => !availableMissions.Contains(m));
449  return selectedMissions;
450  }
451  }
452 
453 
454 
455  public void SelectMission(Mission mission)
456  {
457  if (!SelectedMissions.Contains(mission) && mission != null)
458  {
459  selectedMissions.Add(mission);
460  selectedMissions.Sort((m1, m2) => availableMissions.IndexOf(m1).CompareTo(availableMissions.IndexOf(m2)));
461  }
462  }
463 
464  public void DeselectMission(Mission mission)
465  {
466  selectedMissions.Remove(mission);
467  }
468 
469 
470  public List<int> GetSelectedMissionIndices()
471  {
472  List<int> selectedMissionIndices = new List<int>();
473  foreach (Mission mission in SelectedMissions)
474  {
475  if (availableMissions.Contains(mission))
476  {
477  selectedMissionIndices.Add(availableMissions.IndexOf(mission));
478  }
479  }
480  return selectedMissionIndices;
481  }
482 
483  public void SetSelectedMissionIndices(IEnumerable<int> missionIndices)
484  {
485  selectedMissions.Clear();
486  foreach (int missionIndex in missionIndices)
487  {
488  if (missionIndex < 0 || missionIndex >= availableMissions.Count)
489  {
490  DebugConsole.ThrowError($"Failed to select a mission in location \"{DisplayName}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
491  break;
492  }
493  selectedMissions.Add(availableMissions[missionIndex]);
494  }
495  }
496 
497  private float priceMultiplier = 1.0f;
498  public float PriceMultiplier
499  {
500  get { return priceMultiplier; }
501  set { priceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f); }
502  }
503 
504  private float mechanicalpriceMultiplier = 1.0f;
506  {
507  get => mechanicalpriceMultiplier;
508  set => mechanicalpriceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f);
509  }
510 
511  public string LastTypeChangeMessage;
512 
514 
515  public bool IsGateBetweenBiomes;
516 
517  private readonly struct LoadedMission
518  {
519  public readonly MissionPrefab MissionPrefab;
520  public readonly int TimesAttempted;
521  public readonly int OriginLocationIndex;
522  public readonly int DestinationIndex;
523  public readonly bool SelectedMission;
524 
525  public LoadedMission(XElement element)
526  {
527  var id = element.GetAttributeIdentifier("prefabid", Identifier.Empty);
528  MissionPrefab = MissionPrefab.Prefabs.TryGet(id, out var prefab) ? prefab : null;
529  TimesAttempted = element.GetAttributeInt("timesattempted", 0);
530  OriginLocationIndex = element.GetAttributeInt("origin", -1);
531  DestinationIndex = element.GetAttributeInt("destinationindex", -1);
532  SelectedMission = element.GetAttributeBool("selected", false);
533  }
534  }
535 
536  private List<LoadedMission> loadedMissions;
537 
539 
540  public override string ToString()
541  {
542  return $"Location ({DisplayName ?? "null"})";
543  }
544 
545  public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
546  {
547  Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost);
548  CreateRandomName(Type, rand, existingLocations);
549  MapPosition = mapPosition;
550  PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
551  Connections = new List<LocationConnection>();
552  }
553 
557  public Location(CampaignMode campaign, XElement element)
558  {
559  Identifier locationTypeId = element.GetAttributeIdentifier("type", "");
560  bool typeNotFound = GetTypeOrFallback(locationTypeId, out LocationType type);
561  Type = type;
562 
563  Identifier originalLocationTypeId = element.GetAttributeIdentifier("originaltype", locationTypeId);
564  GetTypeOrFallback(originalLocationTypeId, out LocationType originalType);
565  OriginalType = originalType;
566 
567  nameIdentifier = element.GetAttributeIdentifier(nameof(nameIdentifier), "");
568  if (nameIdentifier.IsEmpty)
569  {
570  //backwards compatibility
571  DisplayName = element.GetAttributeString("name", "");
572  rawName = element.GetAttributeString("rawname", element.GetAttributeString("basename", DisplayName.Value));
573  nameIdentifier = rawName.ToIdentifier();
574  }
575  else
576  {
577  nameFormatIndex = element.GetAttributeInt(nameof(nameFormatIndex), 0);
578  DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
579  }
580 
581  MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
582 
583  PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
584  IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
585  MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
586  TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0);
587  StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0);
588 
589  var factionIdentifier = element.GetAttributeIdentifier("faction", Identifier.Empty);
590  if (!factionIdentifier.IsEmpty)
591  {
592  Faction = campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
593  }
594  var secondaryFactionIdentifier = element.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
595  if (!secondaryFactionIdentifier.IsEmpty)
596  {
597  SecondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
598  }
599  Identifier biomeId = element.GetAttributeIdentifier("biome", Identifier.Empty);
600  if (biomeId != Identifier.Empty)
601  {
602  if (Biome.Prefabs.TryGet(biomeId, out Biome biome))
603  {
604  Biome = biome;
605  }
606  else
607  {
608  DebugConsole.ThrowError($"Error while loading the campaign map: could not find a biome with the identifier \"{biomeId}\".");
609  }
610  }
611 
612  if (!typeNotFound)
613  {
614  for (int i = 0; i < Type.CanChangeTo.Count; i++)
615  {
616  for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++)
617  {
618  ProximityTimer.Add(Type.CanChangeTo[i].Requirements[j], element.GetAttributeInt("proximitytimer" + i + "-" + j, 0));
619  }
620  }
621 
622  LoadLocationTypeChange(element);
623  }
624 
625  string[] takenItemStr = element.GetAttributeStringArray("takenitems", Array.Empty<string>());
626  foreach (string takenItem in takenItemStr)
627  {
628  string[] takenItemSplit = takenItem.Split(';');
629  if (takenItemSplit.Length != 4)
630  {
631  DebugConsole.ThrowError($"Error in saved location: could not parse taken item data \"{takenItem}\"");
632  continue;
633  }
634  if (!ushort.TryParse(takenItemSplit[1], out ushort id))
635  {
636  DebugConsole.ThrowError($"Error in saved location: could not parse taken item id \"{takenItemSplit[1]}\"");
637  continue;
638  }
639  if (!int.TryParse(takenItemSplit[2], out int containerIndex))
640  {
641  DebugConsole.ThrowError($"Error in saved location: could not parse taken container index \"{takenItemSplit[2]}\"");
642  continue;
643  }
644  if (!ushort.TryParse(takenItemSplit[3], out ushort moduleIndex))
645  {
646  DebugConsole.ThrowError($"Error in saved location: could not parse taken item module index \"{takenItemSplit[3]}\"");
647  continue;
648  }
649  takenItems.Add(new TakenItem(takenItemSplit[0].ToIdentifier(), id, containerIndex, moduleIndex));
650  }
651 
652  killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty<int>()).ToHashSet();
653 
654  System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!");
655  Type ??= LocationType.Prefabs.First();
656 
657  LevelData = new LevelData(element.GetChildElement("Level"), clampDifficultyToBiome: true);
658 
659  PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value);
660 
661  LoadStores(element);
662  LoadMissions(element);
663 
664  bool GetTypeOrFallback(Identifier identifier, out LocationType type)
665  {
666  if (!LocationType.Prefabs.TryGet(identifier, out type))
667  {
668  //turn lairs into abandoned outposts
669  if (identifier == "lair")
670  {
671  LocationType.Prefabs.TryGet("Abandoned".ToIdentifier(), out type);
672  addInitialMissionsForType = Type;
673  }
674  if (type == null)
675  {
676  DebugConsole.AddWarning($"Could not find location type \"{identifier}\". Using location type \"None\" instead.");
677  LocationType.Prefabs.TryGet("None".ToIdentifier(), out type);
678  type ??= LocationType.Prefabs.First();
679  }
680  if (type != null)
681  {
682  element.SetAttributeValue("type", type.Identifier.ToString());
683  }
684  return false;
685  }
686  return true;
687  }
688  }
689 
690  public void LoadLocationTypeChange(XElement locationElement)
691  {
692  TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0);
693  LocationTypeChangeCooldown = locationElement.GetAttributeInt("locationtypechangecooldown", 0);
694  foreach (var subElement in locationElement.Elements())
695  {
696  switch (subElement.Name.ToString())
697  {
698  case "pendinglocationtypechange":
699  int timer = subElement.GetAttributeInt("timer", 0);
700  if (subElement.Attribute("index") != null)
701  {
702  int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0);
703  if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >= Type.CanChangeTo.Count)
704  {
705  DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{DisplayName}\". Location index out of bounds ({locationTypeChangeIndex}).");
706  continue;
707  }
708  PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null);
709  }
710  else
711  {
712  Identifier missionIdentifier = subElement.GetAttributeIdentifier("missionidentifier", "");
713  var mission = MissionPrefab.Prefabs[missionIdentifier];
714  if (mission == null)
715  {
716  DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{DisplayName}\". Matching mission not found.");
717  continue;
718  }
719  PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission);
720  }
721  break;
722  }
723  }
724  }
725 
726  public void LoadMissions(XElement locationElement)
727  {
728  if (locationElement.GetChildElement("missions") is XElement missionsElement)
729  {
730  loadedMissions = new List<LoadedMission>();
731  foreach (XElement childElement in missionsElement.GetChildElements("mission"))
732  {
733  var loadedMission = new LoadedMission(childElement);
734  if (loadedMission.MissionPrefab != null)
735  {
736  loadedMissions.Add(loadedMission);
737  }
738  }
739  }
740  }
741 
742  public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
743  {
744  return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
745  }
746 
747  public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true)
748  {
749  if (newType == Type) { return; }
750 
751  if (newType == null)
752  {
753  DebugConsole.ThrowError($"Failed to change the type of the location \"{DisplayName}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
754  return;
755  }
756 
757  Type = newType;
758  if (rawName != null)
759  {
760  DebugConsole.Log($"Location {rawName} changed it's type from {Type} to {newType}");
761  DisplayName =
762  Type.NameFormats == null || !Type.NameFormats.Any() ?
763  rawName :
764  Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", rawName);
765  }
766  else
767  {
768  DebugConsole.Log($"Location {DisplayName.Value} changed it's type from {Type} to {newType}");
769  DisplayName =
770  Type.NameFormats == null || !Type.NameFormats.Any() ?
771  TextManager.Get(nameIdentifier) :
772  Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", TextManager.Get(nameIdentifier).Value);
773  }
774 
776  if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
777  {
778  if (Type.Faction == Identifier.Empty) { Faction ??= campaign.GetRandomFaction(Rand.RandSync.Unsynced); }
779  if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced); }
780  }
781  else
782  {
783  if (Type.Faction == Identifier.Empty) { Faction = null; }
784  if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction = null; }
785  }
786 
787  UnlockInitialMissions(Rand.RandSync.Unsynced);
788 
789  if (createStores)
790  {
791  CreateStores(force: true);
792  }
793  }
794 
796  {
797  if (campaign == null) { return; }
798  if (Type.Faction != Identifier.Empty)
799  {
800  Faction = Type.Faction == "None" ? null : TryFindFaction(Type.Faction);
801  }
802  if (Type.SecondaryFaction != Identifier.Empty)
803  {
804  SecondaryFaction = Type.SecondaryFaction == "None" ? null : TryFindFaction(Type.SecondaryFaction);
805  }
806 
807  Faction TryFindFaction(Identifier identifier)
808  {
809  var faction = campaign.GetFaction(identifier);
810  if (faction == null)
811  {
812  DebugConsole.ThrowError($"Error in location type \"{Type.Identifier}\": failed to find a faction with the identifier \"{identifier}\".",
813  contentPackage: Type.ContentPackage);
814  }
815  return faction;
816  }
817  }
818 
819  public void UnlockInitialMissions(Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
820  {
821  if (Type.MissionIdentifiers.Any())
822  {
823  UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(randSync), invokingContentPackage: Type.ContentPackage);
824  }
825  if (Type.MissionTags.Any())
826  {
827  UnlockMissionByTag(Type.MissionTags.GetRandom(randSync), invokingContentPackage: Type.ContentPackage);
828  }
829  }
830 
831  public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
832  {
833  if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
834  if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; }
835  AddMission(InstantiateMission(missionPrefab, connection));
836  }
837 
838  public void UnlockMission(MissionPrefab missionPrefab)
839  {
840  if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
841  if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; }
842  AddMission(InstantiateMission(missionPrefab));
843  }
844 
845  public Mission UnlockMissionByIdentifier(Identifier identifier, ContentPackage invokingContentPackage = null)
846  {
847  if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; }
848  if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; }
849 
850  var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier);
851  if (missionPrefab == null)
852  {
853  DebugConsole.ThrowError($"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found.",
854  contentPackage: invokingContentPackage);
855  }
856  else
857  {
858  var mission = InstantiateMission(missionPrefab, out LocationConnection connection);
859  //don't allow duplicate missions in the same connection
860  if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
861  {
862  return null;
863  }
864  AddMission(mission);
865  DebugConsole.NewMessage($"Unlocked a mission by \"{identifier}\".", debugOnly: true);
866  return mission;
867  }
868  return null;
869  }
870 
871  public Mission UnlockMissionByTag(Identifier tag, Random random = null, ContentPackage invokingContentPackage = null)
872  {
873  if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; }
874  var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Contains(tag));
875  if (matchingMissions.None())
876  {
877  DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found.", contentPackage: invokingContentPackage);
878  }
879  else
880  {
881  var unusedMissions = matchingMissions.Where(m => availableMissions.None(mission => mission.Prefab == m));
882  if (unusedMissions.Any())
883  {
884  var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)) || m.IsAllowed(this, this)));
885  if (suitableMissions.None())
886  {
887  suitableMissions = unusedMissions;
888  }
889  var filteredMissions = suitableMissions.Where(m => LevelData.Difficulty >= m.MinLevelDifficulty && LevelData.Difficulty <= m.MaxLevelDifficulty);
890  if (filteredMissions.None())
891  {
892  DebugConsole.AddWarning($"No suitable mission matching the level difficulty {LevelData.Difficulty} found with the tag \"{tag}\". Ignoring the restriction.",
893  contentPackage: invokingContentPackage);
894  }
895  else
896  {
897  suitableMissions = filteredMissions;
898  }
899  MissionPrefab missionPrefab =
900  random != null ?
901  ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, random) :
902  ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, Rand.RandSync.Unsynced);
903 
904  var mission = InstantiateMission(missionPrefab, out LocationConnection connection);
905  //don't allow duplicate missions in the same connection
906  if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
907  {
908  return null;
909  }
910  AddMission(mission);
911  DebugConsole.NewMessage($"Unlocked a random mission by \"{tag}\": {mission.Prefab.Identifier} (difficulty level: {LevelData.Difficulty})", debugOnly: true);
912  return mission;
913  }
914  else
915  {
916  DebugConsole.AddWarning($"Failed to unlock a mission with the tag \"{tag}\": all available missions have already been unlocked.",
917  contentPackage: invokingContentPackage);
918  }
919  }
920 
921  return null;
922  }
923 
924  private void AddMission(Mission mission)
925  {
926  if (!mission.Prefab.AllowOtherMissionsInLevel)
927  {
928  availableMissions.Clear();
929  }
930  availableMissions.Add(mission);
931 #if CLIENT
932  GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
933 #else
934  (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
935 #endif
936  }
937 
938  private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection)
939  {
940  if (prefab.IsAllowed(this, this))
941  {
942  connection = null;
943  return InstantiateMission(prefab);
944  }
945 
946  var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this)));
947  if (suitableConnections.None())
948  {
949  suitableConnections = Connections.ToList();
950  }
951  //prefer connections that haven't been passed through, and connections with fewer available missions
952  connection = ToolBox.SelectWeightedRandom(
953  suitableConnections.ToList(),
954  suitableConnections.Select(c => GetConnectionWeight(this, c)).ToList(),
955  Rand.RandSync.Unsynced);
956 
957  static float GetConnectionWeight(Location location, LocationConnection c)
958  {
959  Location destination = c.OtherLocation(location);
960  if (destination == null) { return 0; }
961  float minWeight = 0.0001f;
962  float lowWeight = 0.2f;
963  float normalWeight = 1.0f;
964  float maxWeight = 2.0f;
965  float weight = c.Passed ? lowWeight : normalWeight;
966  if (location.Biome.AllowedZones.Contains(1))
967  {
968  // In the first biome, give a stronger preference for locations that are farther to the right)
969  float diff = destination.MapPosition.X - location.MapPosition.X;
970  if (diff < 0)
971  {
972  weight *= 0.1f;
973  }
974  else
975  {
976  float maxRelevantDiff = 300;
977  weight = MathHelper.Lerp(weight, maxWeight, MathUtils.InverseLerp(0, maxRelevantDiff, diff));
978  }
979  }
980  else if (destination.MapPosition.X > location.MapPosition.X)
981  {
982  weight *= 2.0f;
983  }
984  int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination));
985  if (missionCount > 0)
986  {
987  weight /= missionCount * 2;
988  }
989  if (destination.IsRadiated())
990  {
991  weight *= 0.001f;
992  }
993  return MathHelper.Clamp(weight, minWeight, maxWeight);
994  }
995 
996  return InstantiateMission(prefab, connection);
997  }
998 
999  private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection)
1000  {
1001  Location destination = connection.OtherLocation(this);
1002  var mission = prefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub);
1003  mission.AdjustLevelData(connection.LevelData);
1004  return mission;
1005  }
1006 
1007  private Mission InstantiateMission(MissionPrefab prefab)
1008  {
1009  var mission = prefab.Instantiate(new Location[] { this, this }, Submarine.MainSub);
1010  mission.AdjustLevelData(LevelData);
1011  return mission;
1012  }
1013 
1015  {
1016  availableMissions.Clear();
1017  selectedMissions.Clear();
1018  if (loadedMissions != null && loadedMissions.Any())
1019  {
1020  foreach (LoadedMission loadedMission in loadedMissions)
1021  {
1022  Location destination;
1023  if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.Locations.Count)
1024  {
1025  destination = map.Locations[loadedMission.DestinationIndex];
1026  }
1027  else
1028  {
1029  destination = Connections.First().OtherLocation(this);
1030  }
1031  var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub);
1032  if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.Locations.Count)
1033  {
1034  mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex];
1035  }
1036  mission.TimesAttempted = loadedMission.TimesAttempted;
1037  availableMissions.Add(mission);
1038  if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
1039  }
1040  loadedMissions = null;
1041  }
1042  if (addInitialMissionsForType != null)
1043  {
1044  if (addInitialMissionsForType.MissionIdentifiers.Any())
1045  {
1046  UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandomUnsynced(), invokingContentPackage: Type.ContentPackage);
1047  }
1048  if (addInitialMissionsForType.MissionTags.Any())
1049  {
1050  UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandomUnsynced(), invokingContentPackage: Type.ContentPackage);
1051  }
1052  addInitialMissionsForType = null;
1053  }
1054  }
1055 
1059  public void ClearMissions()
1060  {
1061  availableMissions.Clear();
1062  selectedMissions.Clear();
1063  }
1064 
1065  public bool HasOutpost()
1066  {
1067  if (!Type.HasOutpost) { return false; }
1068 
1069  return !IsCriticallyRadiated();
1070  }
1071 
1072  public bool IsCriticallyRadiated()
1073  {
1074  if (GameMain.GameSession?.Map?.Radiation != null)
1075  {
1076  return TurnsInRadiation > GameMain.GameSession.Map.Radiation.Params.CriticalRadiationThreshold;
1077  }
1078 
1079  return false;
1080  }
1081 
1083  {
1084  if (IsCriticallyRadiated() && !Type.ReplaceInRadiation.IsEmpty)
1085  {
1086  if (LocationType.Prefabs.TryGet(Type.ReplaceInRadiation, out LocationType newLocationType))
1087  {
1088  return newLocationType;
1089  }
1090  else
1091  {
1092  DebugConsole.ThrowError($"Error when trying to get a new location type for an irradiated location - location type \"{newLocationType}\" not found.");
1093  }
1094  }
1095  return Type;
1096  }
1097 
1098  public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
1099  {
1100  System.Diagnostics.Debug.Assert(Connections.Contains(connection));
1101  return AvailableMissions.Where(m => m.Locations[1] == connection.OtherLocation(this));
1102  }
1103 
1105  {
1107  {
1108  DebugConsole.ThrowErrorLocalized("Cannot hire a character from location \"" + DisplayName + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace());
1109  return;
1110  }
1111  if (HireManager == null)
1112  {
1113  DebugConsole.ThrowErrorLocalized("Cannot hire a character from location \"" + DisplayName + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace());
1114  return;
1115  }
1116 
1117  HireManager.RemoveCharacter(character);
1118  }
1119 
1120  public IEnumerable<CharacterInfo> GetHireableCharacters()
1121  {
1123  {
1124  return Enumerable.Empty<CharacterInfo>();
1125  }
1126 
1127  HireManager ??= new HireManager();
1128 
1129  if (!HireManager.AvailableCharacters.Any())
1130  {
1132  }
1134  }
1135 
1136  public void ForceHireableCharacters(IEnumerable<CharacterInfo> hireableCharacters)
1137  {
1138  HireManager ??= new HireManager();
1139  HireManager.AvailableCharacters = hireableCharacters.ToList();
1140  }
1141 
1142  private void CreateRandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
1143  {
1144  if (!type.ForceLocationName.IsEmpty)
1145  {
1146  nameIdentifier = type.ForceLocationName;
1147  DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
1148  return;
1149  }
1150  nameIdentifier = type.GetRandomNameId(rand, existingLocations);
1151  if (nameIdentifier.IsEmpty)
1152  {
1153  //backwards compatibility
1154  rawName = type.GetRandomRawName(rand, existingLocations);
1155  if (rawName.IsNullOrEmpty())
1156  {
1157  DebugConsole.ThrowError($"Failed to generate a name for a location of the type {type.Identifier}. No names found in localization files or the .txt files.");
1158  rawName = "none";
1159  }
1160  nameIdentifier = rawName.ToIdentifier();
1161  if (type.NameFormats == null || !type.NameFormats.Any())
1162  {
1163  DisplayName = rawName;
1164  }
1165  else
1166  {
1167  nameFormatIndex = rand.Next() % type.NameFormats.Count;
1168  DisplayName = type.NameFormats[nameFormatIndex].Replace("[name]", rawName);
1169  }
1170  }
1171  else
1172  {
1173  if (type.NameFormats == null || !type.NameFormats.Any())
1174  {
1175  DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
1176  return;
1177  }
1178  nameFormatIndex = rand.Next() % type.NameFormats.Count;
1179  DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
1180  }
1181  }
1182 
1183  public static LocalizedString GetName(Identifier locationTypeIdentifier, int nameFormatIndex, Identifier nameId)
1184  {
1185  if (LocationType.Prefabs.TryGet(locationTypeIdentifier, out LocationType locationType))
1186  {
1187  return GetName(locationType, nameFormatIndex, nameId);
1188  }
1189  else
1190  {
1191  DebugConsole.ThrowError($"Could not find the location type {locationTypeIdentifier}.\n" + Environment.StackTrace.CleanUpPath());
1192  return new RawLString(nameId.Value);
1193  }
1194  }
1195 
1196  public static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
1197  {
1198  if (type?.NameFormats == null || !type.NameFormats.Any() || nameFormatIndex < 0)
1199  {
1200  return TextManager.Get(nameId);
1201  }
1202  return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace("[name]", TextManager.Get(nameId).Value);
1203  }
1204 
1205  public void ForceName(Identifier nameId)
1206  {
1207  rawName = string.Empty;
1208  nameIdentifier = nameId;
1209  DisplayName = TextManager.Get(nameId).Fallback(nameId.Value);
1210  }
1211 
1212  public void LoadStores(XElement locationElement)
1213  {
1214  UpdateStoreIdentifiers();
1215  Stores?.Clear();
1216  foreach (var storeElement in locationElement.GetChildElements("store"))
1217  {
1218  Stores ??= new Dictionary<Identifier, StoreInfo>();
1219  var identifier = storeElement.GetAttributeIdentifier("identifier", "");
1220  if (identifier.IsEmpty)
1221  {
1222  // Previously saved store data (with no identifier) is discarded and new store data will be created
1223  continue;
1224  }
1225  if (StoreIdentifiers.Contains(identifier))
1226  {
1227  if (!Stores.ContainsKey(identifier))
1228  {
1229  Stores.Add(identifier, new StoreInfo(this, storeElement));
1230  }
1231  else
1232  {
1233  string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": duplicate identifier.";
1234  DebugConsole.ThrowError(msg);
1235  GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:DuplicateStoreInfo", GameAnalyticsManager.ErrorSeverity.Error, msg);
1236  continue;
1237  }
1238  }
1239  else
1240  {
1241  string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier.";
1242  DebugConsole.ThrowError(msg);
1243  GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:IncorrectStoreIdentifier", GameAnalyticsManager.ErrorSeverity.Error, msg);
1244  continue;
1245  }
1246  }
1247  // Backwards compatibility: create new stores for any identifiers not present in the save data
1248  foreach (var id in StoreIdentifiers)
1249  {
1250  AddNewStore(id);
1251  }
1252  }
1253 
1254  public bool IsRadiated() => GameMain.GameSession?.Map?.Radiation != null && GameMain.GameSession.Map.Radiation.Enabled && GameMain.GameSession.Map.Radiation.Contains(this);
1255 
1259  public void RegisterTakenItems(IEnumerable<Item> items)
1260  {
1261  foreach (Item item in items)
1262  {
1263  if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.ID)) { continue; }
1264  if (item.IsSalvageMissionItem) { continue; }
1265  if (item.OriginalModuleIndex < 0)
1266  {
1267  DebugConsole.ThrowError("Tried to register a non-outpost item as being taken from the outpost.");
1268  continue;
1269  }
1270  takenItems.Add(new TakenItem(item));
1271  }
1272  }
1273 
1277  public void RegisterKilledCharacters(IEnumerable<Character> characters)
1278  {
1279  foreach (Character character in characters)
1280  {
1281  if (character?.Info == null) { continue; }
1282  killedCharacterIdentifiers.Add(character.Info.GetIdentifier());
1283  }
1284  }
1285 
1286  public void RemoveTakenItems()
1287  {
1288  foreach (TakenItem takenItem in takenItems)
1289  {
1290  Item item = Item.ItemList.Find(it => takenItem.Matches(it));
1291  item?.Remove();
1292  }
1293  }
1294 
1295  public int GetAdjustedMechanicalCost(int cost)
1296  {
1297  float discount = 0.0f;
1298  if (Reputation != null)
1299  {
1300  discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f);
1301  }
1302  return (int)Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier);
1303  }
1304 
1305  public int GetAdjustedHealCost(int cost)
1306  {
1307  float discount = 0.0f;
1308  if (Reputation != null)
1309  {
1310  discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f);
1311  }
1312  return (int) Math.Ceiling((1.0f - discount) * cost * PriceMultiplier);
1313  }
1314 
1315  public StoreInfo GetStore(Identifier identifier)
1316  {
1317  if (Stores != null && Stores.TryGetValue(identifier, out var store))
1318  {
1319  return store;
1320  }
1321  return null;
1322  }
1323 
1325  public void CreateStores(bool force = false)
1326  {
1327  // In multiplayer, stores should be created by the server and loaded from save data by clients
1328  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
1329  if (!force && Stores != null) { return; }
1330  UpdateStoreIdentifiers();
1331  if (Stores != null)
1332  {
1333  // Remove any stores with no corresponding merchants at the location
1334  foreach (var storeIdentifier in Stores.Keys)
1335  {
1336  if (!StoreIdentifiers.Contains(storeIdentifier))
1337  {
1338  Stores.Remove(storeIdentifier);
1339  }
1340  }
1341  foreach (var identifier in StoreIdentifiers)
1342  {
1343  if (Stores.TryGetValue(identifier, out var store))
1344  {
1345  store.Balance = Math.Max(store.Balance, StoreInitialBalance);
1346  var newStock = store.CreateStock();
1347  foreach (var oldStockItem in store.Stock)
1348  {
1349  if (newStock.Find(i => i.ItemPrefab == oldStockItem.ItemPrefab) is { } newStockItem)
1350  {
1351  if (oldStockItem.Quantity > newStockItem.Quantity)
1352  {
1353  newStockItem.Quantity = oldStockItem.Quantity;
1354  }
1355  }
1356  }
1357  store.Stock.Clear();
1358  store.Stock.AddRange(newStock);
1359  store.GenerateSpecials();
1360  store.GeneratePriceModifier();
1361  }
1362  else
1363  {
1364  AddNewStore(identifier);
1365  }
1366  }
1367  }
1368  else
1369  {
1370  foreach (var identifier in StoreIdentifiers)
1371  {
1372  AddNewStore(identifier);
1373  }
1374  }
1375  }
1376 
1377  public void UpdateStores()
1378  {
1379  // In multiplayer, stores should be updated by the server and loaded from save data by clients
1380  if (GameMain.NetworkMember is { IsClient: true }) { return; }
1381  if (Stores == null)
1382  {
1383  CreateStores();
1384  return;
1385  }
1386  var storesToRemove = new HashSet<Identifier>();
1387  foreach (var store in Stores.Values)
1388  {
1389  if (!StoreIdentifiers.Contains(store.Identifier))
1390  {
1391  storesToRemove.Add(store.Identifier);
1392  continue;
1393  }
1394  if (store.Balance < StoreInitialBalance)
1395  {
1396  store.Balance = Math.Min(store.Balance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance);
1397  }
1398  var stock = new List<PurchasedItem>(store.Stock);
1399  var stockToRemove = new List<PurchasedItem>();
1400 
1401  foreach (var itemPrefab in ItemPrefab.Prefabs)
1402  {
1403  var existingStock = stock.FirstOrDefault(s => s.ItemPrefab == itemPrefab);
1404  if (itemPrefab.CanBeBoughtFrom(store, out PriceInfo priceInfo))
1405  {
1406  if (existingStock == null)
1407  {
1408  //can be bought from the location, but not in stock - some new item added by an update or mod?
1409  stock.Add(StoreInfo.CreateInitialStockItem(itemPrefab, priceInfo));
1410  }
1411  else
1412  {
1413  existingStock.Quantity =
1414  Math.Min(
1415  existingStock.Quantity + 1,
1416  priceInfo.MaxAvailableAmount);
1417  }
1418  }
1419  else if (existingStock != null)
1420  {
1421  stockToRemove.Add(existingStock);
1422  }
1423  }
1424 
1425  stockToRemove.ForEach(i => stock.Remove(i));
1426  store.Stock.Clear();
1427  store.Stock.AddRange(stock);
1428  store.GeneratePriceModifier();
1429  }
1430 
1431  StepsSinceSpecialsUpdated++;
1432  foreach (var identifier in storesToRemove)
1433  {
1434  Stores.Remove(identifier);
1435  }
1436  foreach (var identifier in StoreIdentifiers)
1437  {
1438  AddNewStore(identifier);
1439  }
1440  }
1441 
1442  public void UpdateSpecials()
1443  {
1444  if (GameMain.NetworkMember is { IsClient: true } || Stores is null) { return; }
1445 
1446  int extraSpecialSalesCount = GetExtraSpecialSalesCount();
1447 
1448  foreach (StoreInfo store in Stores.Values)
1449  {
1450  if (StepsSinceSpecialsUpdated < SpecialsUpdateInterval && store.DailySpecials.Count == DailySpecialsCount + extraSpecialSalesCount) { continue; }
1451 
1452  store.GenerateSpecials();
1453  }
1454  }
1455 
1456  private void UpdateStoreIdentifiers()
1457  {
1458  StoreIdentifiers.Clear();
1459  foreach (var outpostParam in OutpostGenerationParams.OutpostParams)
1460  {
1461  if (!outpostParam.AllowedLocationTypes.Contains(Type.Identifier)) { continue; }
1462  foreach (var identifier in outpostParam.GetStoreIdentifiers())
1463  {
1464  StoreIdentifiers.Add(identifier);
1465  }
1466  }
1467  }
1468 
1469  private void AddNewStore(Identifier identifier)
1470  {
1471  Stores ??= new Dictionary<Identifier, StoreInfo>();
1472  if (Stores.ContainsKey(identifier)) { return; }
1473  var newStore = new StoreInfo(this, identifier);
1474  Stores.Add(identifier, newStore);
1475  }
1476 
1477  public void AddStock(Dictionary<Identifier, List<SoldItem>> items)
1478  {
1479  if (items == null) { return; }
1480  foreach (var storeItems in items)
1481  {
1482  if (GetStore(storeItems.Key) is { } store)
1483  {
1484  store.AddStock(storeItems.Value);
1485  }
1486  }
1487  }
1488 
1489  public void RemoveStock(Dictionary<Identifier, List<PurchasedItem>> items)
1490  {
1491  if (items == null) { return; }
1492  foreach (var storeItems in items)
1493  {
1494  if (GetStore(storeItems.Key) is { } store)
1495  {
1496  store.RemoveStock(storeItems.Value);
1497  }
1498  }
1499  }
1500 
1501  public static int GetExtraSpecialSalesCount()
1502  {
1503  var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
1504  if (!characters.Any()) { return 0; }
1505  return characters.Max(static c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount));
1506  }
1507 
1508  public bool CanHaveSubsForSale()
1509  {
1510  return HasOutpost() && CanHaveCampaignInteraction(CampaignMode.InteractionType.PurchaseSub);
1511  }
1512 
1513  public int HighestSubmarineTierAvailable(SubmarineClass submarineClass = SubmarineClass.Undefined)
1514  {
1515  if (CanHaveSubsForSale())
1516  {
1518  }
1519  return 0;
1520  }
1521 
1523  {
1524  return Biome?.IsSubmarineAvailable(info, Type.Identifier) ?? true;
1525  }
1526 
1527  private bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType)
1528  {
1529  return LevelData != null &&
1531  LevelData.GetSuitableOutpostGenerationParams(this, LevelData).Any(p => p.CanHaveCampaignInteraction(interactionType));
1532  }
1533 
1534  public void Reset(CampaignMode campaign)
1535  {
1537  {
1538  ChangeType(campaign, OriginalType);
1539  PendingLocationTypeChange = null;
1540  }
1541  CreateStores(force: true);
1542  ClearMissions();
1543  LevelData?.EventHistory?.Clear();
1545  }
1546 
1547  public XElement Save(Map map, XElement parentElement)
1548  {
1549  var locationElement = new XElement("location",
1550  new XAttribute("type", Type.Identifier),
1551  new XAttribute("originaltype", (Type ?? OriginalType).Identifier),
1552  /*not used currently (we load the nameIdentifier instead),
1553  * but could make sense to include still for backwards compatibility reasons*/
1554  new XAttribute("name", DisplayName),
1555  new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty),
1556  new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
1557  new XAttribute("pricemultiplier", PriceMultiplier),
1558  new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes),
1559  new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier),
1560  new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange),
1561  new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation),
1562  new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated));
1563 
1564  if (!rawName.IsNullOrEmpty())
1565  {
1566  locationElement.Add(new XAttribute(nameof(rawName), rawName));
1567  }
1568  else
1569  {
1570  locationElement.Add(new XAttribute(nameof(nameIdentifier), nameIdentifier));
1571  locationElement.Add(new XAttribute(nameof(nameFormatIndex), nameFormatIndex));
1572  }
1573 
1574  if (Faction != null)
1575  {
1576  locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier));
1577  }
1578  if (SecondaryFaction != null)
1579  {
1580  locationElement.Add(new XAttribute("secondaryfaction", SecondaryFaction.Prefab.Identifier));
1581  }
1582 
1583  LevelData.Save(locationElement);
1584 
1585  for (int i = 0; i < Type.CanChangeTo.Count; i++)
1586  {
1587  for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++)
1588  {
1589  if (ProximityTimer.ContainsKey(Type.CanChangeTo[i].Requirements[j]))
1590  {
1591  locationElement.Add(new XAttribute("proximitytimer" + i + "-" + j, ProximityTimer[Type.CanChangeTo[i].Requirements[j]]));
1592  }
1593  }
1594  }
1595 
1596  if (PendingLocationTypeChange.HasValue)
1597  {
1598  var changeElement = new XElement("pendinglocationtypechange", new XAttribute("timer", PendingLocationTypeChange.Value.delay));
1599  if (PendingLocationTypeChange.Value.parentMission != null)
1600  {
1601  changeElement.Add(new XAttribute("missionidentifier", PendingLocationTypeChange.Value.parentMission.Identifier));
1602  locationElement.Add(changeElement);
1603  }
1604  else
1605  {
1606  int index = Type.CanChangeTo.IndexOf(PendingLocationTypeChange.Value.typeChange);
1607  changeElement.Add(new XAttribute("index", index));
1608  if (index == -1)
1609  {
1610  DebugConsole.AddWarning($"Invalid location type change in the location \"{DisplayName}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
1611  }
1612  else
1613  {
1614  locationElement.Add(changeElement);
1615  }
1616  }
1617  }
1618 
1620  {
1621  locationElement.Add(new XAttribute("locationtypechangecooldown", LocationTypeChangeCooldown));
1622  }
1623 
1624  if (takenItems.Any())
1625  {
1626  locationElement.Add(new XAttribute(
1627  "takenitems",
1628  string.Join(',', takenItems.Select(it => it.Identifier + ";" + it.OriginalID + ";" + it.OriginalContainerIndex + ";" + it.ModuleIndex))));
1629  }
1630  if (killedCharacterIdentifiers.Any())
1631  {
1632  locationElement.Add(new XAttribute("killedcharacters", string.Join(',', killedCharacterIdentifiers)));
1633  }
1634 
1635  if (Stores != null)
1636  {
1637  foreach (var store in Stores.Values)
1638  {
1639  var storeElement = new XElement("store",
1640  new XAttribute("identifier", store.Identifier.Value),
1641  new XAttribute(nameof(store.MerchantFaction), store.MerchantFaction),
1642  new XAttribute("balance", store.Balance),
1643  new XAttribute("pricemodifier", store.PriceModifier));
1644  foreach (PurchasedItem item in store.Stock)
1645  {
1646  if (item?.ItemPrefab == null) { continue; }
1647  storeElement.Add(new XElement("stock",
1648  new XAttribute("id", item.ItemPrefab.Identifier),
1649  new XAttribute("qty", item.Quantity)));
1650  }
1651  if (store.DailySpecials.Any())
1652  {
1653  var dailySpecialElement = new XElement("dailyspecials");
1654  foreach (var item in store.DailySpecials)
1655  {
1656  dailySpecialElement.Add(new XElement("item",
1657  new XAttribute("id", item.Identifier)));
1658  }
1659  storeElement.Add(dailySpecialElement);
1660  }
1661  if (store.RequestedGoods.Any())
1662  {
1663  var requestedGoodsElement = new XElement("requestedgoods");
1664  foreach (var item in store.RequestedGoods)
1665  {
1666  requestedGoodsElement.Add(new XElement("item",
1667  new XAttribute("id", item.Identifier)));
1668  }
1669  storeElement.Add(requestedGoodsElement);
1670  }
1671  locationElement.Add(storeElement);
1672  }
1673  }
1674 
1675  if (AvailableMissions is List<Mission> missions && missions.Any())
1676  {
1677  var missionsElement = new XElement("missions");
1678  foreach (Mission mission in missions)
1679  {
1680  var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this);
1681  var destinationIndex = map.Locations.IndexOf(location);
1682  var originIndex = map.Locations.IndexOf(mission.OriginLocation);
1683  missionsElement.Add(new XElement("mission",
1684  new XAttribute("prefabid", mission.Prefab.Identifier),
1685  new XAttribute("destinationindex", destinationIndex),
1686  new XAttribute(nameof(Mission.TimesAttempted), mission.TimesAttempted),
1687  new XAttribute("origin", originIndex),
1688  new XAttribute("selected", selectedMissions.Contains(mission))));
1689  }
1690  locationElement.Add(missionsElement);
1691  }
1692 
1693  parentElement.Add(locationElement);
1694 
1695  return locationElement;
1696  }
1697 
1698  public void Remove()
1699  {
1701  }
1702 
1703  public void RemoveProjSpecific()
1704  {
1705  HireManager?.Remove();
1706  }
1707 
1709  {
1710  public AbilityLocation(Location location)
1711  {
1712  Location = location;
1713  }
1714 
1715  public Location Location { get; set; }
1716  }
1717  }
1718 }
static readonly PrefabCollection< Biome > Prefabs
Definition: Biome.cs:10
int HighestSubmarineTierAvailable(SubmarineClass subClass, Identifier locationType)
Definition: Biome.cs:86
bool IsSubmarineAvailable(SubmarineInfo info, Identifier locationType)
Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their ControlledOutpostPercentage
FactionAffiliation GetFactionAffiliation(Identifier factionIdentifier)
Faction GetRandomSecondaryFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their SecondaryControlledOutpostPercentage
Stores information about the Character that is needed between rounds in the menu etc....
int GetIdentifier()
Returns a presumably (not guaranteed) unique and persistent hash using the (current) Name,...
const ushort NullEntityID
Definition: Entity.cs:14
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
FactionPrefab Prefab
Definition: Factions.cs:18
Reputation Reputation
Definition: Factions.cs:17
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
void RemoveCharacter(CharacterInfo character)
Definition: HireManager.cs:19
void GenerateCharacters(Location location, int amount)
Definition: HireManager.cs:41
const int MaxAvailableCharacters
Definition: HireManager.cs:12
List< CharacterInfo > AvailableCharacters
Definition: HireManager.cs:9
static readonly List< Item > ItemList
static readonly PrefabCollection< ItemPrefab > Prefabs
PriceInfo GetPriceInfo(Location.StoreInfo store)
override ImmutableHashSet< Identifier > Tags
readonly List< Identifier > EventHistory
Events that have previously triggered in this level. Used for making events the player hasn't seen ye...
Definition: LevelData.cs:81
readonly float Difficulty
Definition: LevelData.cs:24
void Save(XElement parentElement)
Definition: LevelData.cs:356
static IEnumerable< OutpostGenerationParams > GetSuitableOutpostGenerationParams(Location location, LevelData levelData)
Definition: LevelData.cs:337
bool OutpostGenerationParamsExist
Definition: LevelData.cs:335
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
AbilityLocation(Location location)
Definition: Location.cs:1710
int PriceModifier
In percentages. Larger values make buying more expensive and selling less profitable,...
Definition: Location.cs:125
int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo=null, bool considerRequestedGoods=true)
Definition: Location.cs:327
void SetMerchantFaction(Identifier factionIdentifier)
Definition: Location.cs:353
List< PurchasedItem > CreateStock()
Definition: Location.cs:198
override string ToString()
Definition: Location.cs:392
StoreInfo(Location location, Identifier identifier)
Create new StoreInfo
Definition: Location.cs:137
Identifier GetMerchantOrLocationFactionIdentifier()
Definition: Location.cs:358
float GetReputationModifier(bool buying)
Definition: Location.cs:363
List< ItemPrefab > DailySpecials
Definition: Location.cs:120
StoreInfo(Location location, XElement storeElement)
Load previously saved StoreInfo
Definition: Location.cs:149
void AddStock(List< SoldItem > items)
Definition: Location.cs:209
int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo=null, bool considerDailySpecials=true)
Definition: Location.cs:286
List< PurchasedItem > Stock
Definition: Location.cs:119
void RemoveStock(List< PurchasedItem > items)
Definition: Location.cs:227
List< ItemPrefab > RequestedGoods
Definition: Location.cs:121
static PurchasedItem CreateInitialStockItem(ItemPrefab itemPrefab, PriceInfo priceInfo)
Definition: Location.cs:192
bool IsEqual(TakenItem obj)
Definition: Location.cs:39
readonly Identifier Identifier
Definition: Location.cs:18
readonly int OriginalContainerIndex
Definition: Location.cs:19
bool Matches(Item item)
Definition: Location.cs:44
readonly ushort OriginalID
Definition: Location.cs:16
readonly ushort ModuleIndex
Definition: Location.cs:17
TakenItem(Identifier identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex)
Definition: Location.cs:21
Location OtherLocation(Location location)
void RemoveHireableCharacter(CharacterInfo character)
Definition: Location.cs:1104
void ChangeType(CampaignMode campaign, LocationType newType, bool createStores=true)
Definition: Location.cs:747
int GetAdjustedHealCost(int cost)
Definition: Location.cs:1305
void LoadStores(XElement locationElement)
Definition: Location.cs:1212
IEnumerable< Mission > GetMissionsInConnection(LocationConnection connection)
Definition: Location.cs:1098
void ForceName(Identifier nameId)
Definition: Location.cs:1205
int TimeSinceLastTypeChange
Definition: Location.cs:513
LocationTypeChange typeChange
Definition: Location.cs:80
void ForceHireableCharacters(IEnumerable< CharacterInfo > hireableCharacters)
Definition: Location.cs:1136
void AddStock(Dictionary< Identifier, List< SoldItem >> items)
Definition: Location.cs:1477
readonly List< LocationConnection > Connections
Definition: Location.cs:57
HashSet< Identifier > StoreIdentifiers
Definition: Location.cs:414
LocationType Type
Definition: Location.cs:94
HireManager HireManager
Definition: Location.cs:538
override string ToString()
Definition: Location.cs:540
float MechanicalPriceMultiplier
Definition: Location.cs:506
StoreInfo GetStore(Identifier identifier)
Definition: Location.cs:1315
XElement Save(Map map, XElement parentElement)
Definition: Location.cs:1547
bool IsGateBetweenBiomes
Definition: Location.cs:515
bool IsCriticallyRadiated()
Definition: Location.cs:1072
Mission UnlockMissionByTag(Identifier tag, Random random=null, ContentPackage invokingContentPackage=null)
Definition: Location.cs:871
static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
Definition: Location.cs:1196
Location(CampaignMode campaign, XElement element)
Create a location from save data
Definition: Location.cs:557
Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost=false, LocationType forceLocationType=null, IEnumerable< Location > existingLocations=null)
Definition: Location.cs:545
static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType=null, IEnumerable< Location > existingLocations=null)
Definition: Location.cs:742
IEnumerable< Mission > AvailableMissions
Definition: Location.cs:435
void LoadLocationTypeChange(XElement locationElement)
Definition: Location.cs:690
void Reset(CampaignMode campaign)
Definition: Location.cs:1534
Identifier NameIdentifier
Definition: Location.cs:61
IEnumerable< int > KilledCharacterIdentifiers
Definition: Location.cs:429
void SelectMission(Mission mission)
Definition: Location.cs:455
bool CanHaveSubsForSale()
Definition: Location.cs:1508
IEnumerable< Mission > SelectedMissions
Definition: Location.cs:445
void TryAssignFactionBasedOnLocationType(CampaignMode campaign)
Definition: Location.cs:795
string LastTypeChangeMessage
Definition: Location.cs:511
bool LocationTypeChangesBlocked
Is some mission blocking this location from changing its type, or have location type changes been for...
Definition: Location.cs:86
void ClearMissions()
Removes all unlocked missions from the location
Definition: Location.cs:1059
void UnlockInitialMissions(Rand.RandSync randSync=Rand.RandSync.ServerAndClient)
Definition: Location.cs:819
int GetAdjustedMechanicalCost(int cost)
Definition: Location.cs:1295
Vector2 MapPosition
Definition: Location.cs:92
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:1259
bool IsSubmarineAvailable(SubmarineInfo info)
Definition: Location.cs:1522
LocationType GetLocationType()
Definition: Location.cs:1082
List< int > GetSelectedMissionIndices()
Definition: Location.cs:470
bool DisallowLocationTypeChanges
Definition: Location.cs:88
readonly Dictionary< LocationTypeChange.Requirement, int > ProximityTimer
Definition: Location.cs:79
void UnlockMission(MissionPrefab missionPrefab)
Definition: Location.cs:838
void RemoveProjSpecific()
Definition: Location.cs:1703
void InstantiateLoadedMissions(Map map)
Definition: Location.cs:1014
void DeselectMission(Mission mission)
Definition: Location.cs:464
void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
Definition: Location.cs:831
void RemoveTakenItems()
Definition: Location.cs:1286
LocalizedString DisplayName
Definition: Location.cs:59
void RemoveStock(Dictionary< Identifier, List< PurchasedItem >> items)
Definition: Location.cs:1489
int HighestSubmarineTierAvailable(SubmarineClass submarineClass=SubmarineClass.Undefined)
Definition: Location.cs:1513
LevelData LevelData
Definition: Location.cs:98
int LocationTypeChangeCooldown
Definition: Location.cs:81
LocationType OriginalType
Definition: Location.cs:96
Faction SecondaryFaction
Definition: Location.cs:104
IEnumerable< CharacterInfo > GetHireableCharacters()
Definition: Location.cs:1120
void CreateStores(bool force=false)
If true, the stores will be recreated if they already exists.
Definition: Location.cs:1325
void LoadMissions(XElement locationElement)
Definition: Location.cs:726
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:1277
Dictionary< Identifier, StoreInfo > Stores
Definition: Location.cs:398
Mission UnlockMissionByIdentifier(Identifier identifier, ContentPackage invokingContentPackage=null)
Definition: Location.cs:845
static int GetExtraSpecialSalesCount()
Definition: Location.cs:1501
static LocalizedString GetName(Identifier locationTypeIdentifier, int nameFormatIndex, Identifier nameId)
Definition: Location.cs:1183
IEnumerable< TakenItem > TakenItems
Definition: Location.cs:423
void SetSelectedMissionIndices(IEnumerable< int > missionIndices)
Definition: Location.cs:483
readonly CharacterTeamType OutpostTeam
Definition: LocationType.cs:34
readonly ImmutableArray< Identifier > MissionIdentifiers
Definition: LocationType.cs:43
readonly Identifier ForceLocationName
Definition: LocationType.cs:30
IReadOnlyList< string > NameFormats
Definition: LocationType.cs:69
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...
readonly ImmutableArray< Identifier > MissionTags
Definition: LocationType.cs:44
static LocationType Random(Random rand, int? zone=null, bool requireOutpost=false, Func< LocationType, bool > predicate=null)
readonly List< LocationTypeChange > CanChangeTo
Definition: LocationType.cs:41
static readonly PrefabCollection< LocationType > Prefabs
Definition: LocationType.cs:15
int StorePriceModifierRange
In percentages
Identifier GetRandomNameId(Random rand, IEnumerable< Location > existingLocations)
string GetRandomRawName(Random rand, IEnumerable< Location > existingLocations)
For backwards compatibility. Chooses a random name from the names defined in the ....
Identifier ReplaceInRadiation
Definition: LocationType.cs:92
int OriginalModuleIndex
The index of the outpost module this entity originally spawned in (-1 if not an outpost item)
static MapEntityPrefab FindByIdentifier(Identifier identifier)
Location OriginLocation
Where was this mission received from? Affects which faction we give reputation for if the mission is ...
static readonly PrefabCollection< MissionPrefab > Prefabs
static readonly PrefabCollection< OutpostGenerationParams > OutpostParams
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
int MinAvailableAmount
Minimum number of items available at a given store
Definition: PriceInfo.cs:16
int MaxAvailableAmount
Maximum number of items available at a given store. Defaults to 20% more than the minimum amount.
Definition: PriceInfo.cs:21
bool CanBeSpecial
Can the item be a Daily Special or a Requested Good
Definition: PriceInfo.cs:25
float??????? Value
Definition: Reputation.cs:44
const float HostileThreshold
Definition: Reputation.cs:9
float NormalizedValue
Reputation value normalized to the range of 0-1
Definition: Reputation.cs:39
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
CharacterType
Definition: Enums.cs:711
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:195
FactionAffiliation
Definition: Factions.cs:9