Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs
T
2025-12-08 14:56:47 +00:00

1818 lines
80 KiB
C#

using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class Location
{
public class TakenItem
{
public readonly ushort OriginalID;
public readonly ushort ModuleIndex;
public readonly Identifier Identifier;
public readonly int OriginalContainerIndex;
public TakenItem(Identifier identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex)
{
OriginalID = originalID;
OriginalContainerIndex = originalContainerIndex;
ModuleIndex = moduleIndex;
Identifier = identifier;
}
public TakenItem(Item item)
{
System.Diagnostics.Debug.Assert(item.OriginalModuleIndex >= 0, "Trying to add a non-outpost item to a location's taken items");
OriginalContainerIndex = item.OriginalContainerIndex;
OriginalID = item.ID;
ModuleIndex = (ushort) item.OriginalModuleIndex;
Identifier = ((MapEntity)item).Prefab.Identifier;
}
public bool IsEqual(TakenItem obj)
{
return obj.OriginalID == OriginalID && obj.OriginalContainerIndex == OriginalContainerIndex && obj.ModuleIndex == ModuleIndex && obj.Identifier == Identifier;
}
public bool Matches(Item item)
{
if (item.OriginalContainerIndex != Entity.NullEntityID)
{
return item.OriginalContainerIndex == OriginalContainerIndex && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier;
}
else
{
return item.ID == OriginalID && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier;
}
}
}
public readonly List<LocationConnection> Connections = new List<LocationConnection>();
public LocalizedString DisplayName { get; private set; }
public Identifier NameIdentifier => nameIdentifier;
private int nameFormatIndex;
private Identifier nameIdentifier;
public int NameFormatIndex => nameFormatIndex;
/// <summary>
/// For backwards compatibility: a non-localizable name from the old text files.
/// </summary>
private string rawName;
private LocationType addInitialMissionsForType;
public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false;
public bool Visited => GameMain.GameSession?.Map?.IsVisited(this) ?? false;
/// <summary>
/// How many "world steps" (<see cref="Map.ProgressWorld(CampaignMode)"/> must pass for the stores to be reset in the location?
/// Mainly an optimization
/// </summary>
public const int ClearStoresDelay = 10;
/// <summary>
/// How many "world steps" (<see cref="Map.ProgressWorld(CampaignMode)"/> have passed since this location was last visited?
/// </summary>
public int WorldStepsSinceVisited;
public readonly Dictionary<LocationTypeChange.Requirement, int> ProximityTimer = new Dictionary<LocationTypeChange.Requirement, int>();
public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange;
public int LocationTypeChangeCooldown;
/// <summary>
/// Is some mission blocking this location from changing its type, or have location type changes been forcibly disabled on the location?
/// </summary>
public bool LocationTypeChangesBlocked => DisallowLocationTypeChanges || availableMissions.Any(m => !m.Completed && m.Prefab.BlockLocationTypeChanges);
public bool DisallowLocationTypeChanges;
public Biome Biome { get; set; }
public Vector2 MapPosition { get; private set; }
public LocationType Type { get; private set; }
public LocationType OriginalType { get; private set; }
public LevelData LevelData { get; set; }
public int PortraitId { get; private set; }
public Faction Faction { get; set; }
public Faction SecondaryFaction { get; set; }
/// <summary>
/// Not used by the vanilla game. Can be used by code mods to change the color of the location icon on the campaign map.
/// </summary>
public Color? OverrideIconColor;
public Reputation Reputation => Faction?.Reputation;
public bool IsFactionHostile => Faction?.Reputation.NormalizedValue < Reputation.HostileThreshold;
public int TurnsInRadiation { get; set; }
#region Store
public class StoreInfo
{
public Identifier Identifier { get; }
public Identifier MerchantFaction { get; private set; }
public int Balance { get; set; }
public List<PurchasedItem> Stock { get; } = new List<PurchasedItem>();
public List<ItemPrefab> DailySpecials { get; } = new List<ItemPrefab>();
public List<ItemPrefab> RequestedGoods { get; } = new List<ItemPrefab>();
/// <summary>
/// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa.
/// </summary>
public int PriceModifier { get; set; }
public Location Location { get; }
/// <summary>
/// The maximum effect positive reputation can have on store prices (e.g. 0.5 = 50% discount with max reputation).
/// </summary>
private float MaxReputationModifier => Location.StoreMaxReputationModifier;
/// <summary>
/// The minimum effect negative reputation can have on store prices (e.g. 0.5 = 50% price increase with minimum reputation).
/// </summary>
private float MinReputationModifier => Location.StoreMinReputationModifier;
private StoreInfo(Location location)
{
Location = location;
}
/// <summary>
/// Create new StoreInfo
/// </summary>
public StoreInfo(Location location, Identifier identifier) : this(location)
{
Identifier = identifier;
Balance = location.StoreInitialBalance;
Stock = CreateStock();
GenerateSpecials();
GeneratePriceModifier();
}
/// <summary>
/// Load previously saved StoreInfo
/// </summary>
public StoreInfo(Location location, XElement storeElement) : this(location)
{
Identifier = storeElement.GetAttributeIdentifier("identifier", "");
MerchantFaction = storeElement.GetAttributeIdentifier(nameof(MerchantFaction), "");
Balance = storeElement.GetAttributeInt("balance", location.StoreInitialBalance);
PriceModifier = storeElement.GetAttributeInt("pricemodifier", 0);
// Backwards compatibility: before introducing support for multiple stores, this value was saved as a store element attribute
if (storeElement.Attribute("stepssincespecialsupdated") != null)
{
location.StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0);
}
foreach (var stockElement in storeElement.GetChildElements("stock"))
{
var identifier = stockElement.GetAttributeIdentifier("id", Identifier.Empty);
if (identifier.IsEmpty || ItemPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { continue; }
int qty = stockElement.GetAttributeInt("qty", 0);
if (qty < 1) { continue; }
Stock.Add(new PurchasedItem(prefab, qty, buyer: null));
}
if (storeElement.GetChildElement("dailyspecials") is XElement specialsElement)
{
var loadedDailySpecials = LoadStoreSpecials(specialsElement);
DailySpecials.AddRange(loadedDailySpecials);
}
if (storeElement.GetChildElement("requestedgoods") is XElement goodsElement)
{
var loadedRequestedGoods = LoadStoreSpecials(goodsElement);
RequestedGoods.AddRange(loadedRequestedGoods);
}
static List<ItemPrefab> LoadStoreSpecials(XElement element)
{
var specials = new List<ItemPrefab>();
foreach (var childElement in element.GetChildElements("item"))
{
var id = childElement.GetAttributeIdentifier("id", Identifier.Empty);
if (id.IsEmpty || ItemPrefab.FindByIdentifier(id) is not ItemPrefab prefab) { continue; }
specials.Add(prefab);
}
return specials;
}
}
public static PurchasedItem CreateInitialStockItem(Location location, ItemPrefab itemPrefab, PriceInfo priceInfo)
{
int quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1);
//simulate stores stocking up if the location hasn't been visited in a while
quantity = Math.Min(quantity + location.WorldStepsSinceVisited, priceInfo.MaxAvailableAmount);
return new PurchasedItem(itemPrefab, quantity, buyer: null);
}
public List<PurchasedItem> CreateStock()
{
var stock = new List<PurchasedItem>();
foreach (var prefab in ItemPrefab.Prefabs)
{
if (!prefab.CanBeBoughtFrom(this, out var priceInfo)) { continue; }
stock.Add(CreateInitialStockItem(Location, prefab, priceInfo));
}
return stock;
}
public void AddStock(List<SoldItem> items)
{
if (items == null || items.None()) { return; }
DebugConsole.NewMessage($"Adding items to stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly: true);
foreach (var item in items)
{
if (Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem)
{
stockItem.Quantity += 1;
DebugConsole.NewMessage($"Added 1x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly: true);
}
else
{
DebugConsole.NewMessage($"{item.ItemPrefab.Name} not sold at location, can't add", Color.Cyan, debugOnly: true);
}
}
}
public void RemoveStock(List<PurchasedItem> items)
{
if (items == null || items.None()) { return; }
DebugConsole.NewMessage($"Removing items from stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly: true);
foreach (PurchasedItem item in items)
{
if (Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem)
{
stockItem.Quantity = Math.Max(stockItem.Quantity - item.Quantity, 0);
DebugConsole.NewMessage($"Removed {item.Quantity}x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly: true);
}
}
}
public void GenerateSpecials()
{
var availableStock = new Dictionary<ItemPrefab, float>();
foreach (var stockItem in Stock)
{
if (stockItem.Quantity < 1) { continue; }
float weight = 1.0f;
if (stockItem.ItemPrefab.GetPriceInfo(this) is PriceInfo priceInfo)
{
if (!priceInfo.CanBeSpecial) { continue; }
var baseQuantity = priceInfo.MinAvailableAmount;
weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity;
if (weight < 0.0f) { continue; }
}
availableStock.Add(stockItem.ItemPrefab, weight);
}
DailySpecials.Clear();
int extraSpecialSalesCount = GetExtraSpecialSalesCount();
for (int i = 0; i < Location.DailySpecialsCount + extraSpecialSalesCount; i++)
{
if (availableStock.None()) { break; }
var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced);
if (item == null) { break; }
DailySpecials.Add(item);
availableStock.Remove(item);
}
RequestedGoods.Clear();
for (int i = 0; i < Location.RequestedGoodsCount; i++)
{
var selectedPrefab = ItemPrefab.Prefabs.GetRandom(prefab =>
prefab.CanBeSold && !RequestedGoods.Contains(prefab) &&
prefab.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced);
if (selectedPrefab == null) { break; }
RequestedGoods.Add(selectedPrefab);
}
Location.StepsSinceSpecialsUpdated = 0;
}
public void GeneratePriceModifier()
{
PriceModifier = Rand.Range(-Location.StorePriceModifierRange, Location.StorePriceModifierRange + 1);
}
/// <param name="priceInfo">If null, item.GetPriceInfo() will be used to get it.</param>
/// /// <param name="considerDailySpecials">If false, the price won't be affected by <see cref="DailySpecialPriceModifier"/></param>
public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerDailySpecials = true)
{
priceInfo ??= item?.GetPriceInfo(this);
if (priceInfo == null) { return 0; }
float price = Location.StoreBuyPriceModifier * priceInfo.Price;
// Adjust by random price modifier
price = (100 + PriceModifier) / 100.0f * price;
price *= priceInfo.BuyingPriceMultiplier;
// Adjust by daily special status
if (considerDailySpecials && DailySpecials.Contains(item))
{
price = Location.DailySpecialPriceModifier * price;
}
// Adjust by requested good status (to avoid the store selling items that it requests potentially for less than it pays for them)
if (RequestedGoods.Contains(item))
{
price = Location.RequestGoodBuyPriceModifier * price;
}
// Adjust by current reputation
price *= GetReputationModifier(true);
// Adjust by campaign difficulty settings
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
price *= campaign.Settings.ShopPriceMultiplier;
}
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (characters.Any())
{
var faction = GetMerchantOrLocationFactionIdentifier();
if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive)
{
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false));
price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll));
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag)));
}
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false));
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag)));
}
// Price should never go below 1 mk
return Math.Max((int)price, 1);
}
/// <param name="priceInfo">If null, item.GetPriceInfo() will be used to get it.</param>
/// <param name="considerRequestedGoods">If false, the price won't be affected by <see cref="Barotrauma.Location.RequestGoodSellPriceModifier"/></param>
public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true)
{
priceInfo ??= item?.GetPriceInfo(this);
if (priceInfo == null) { return 0; }
float price = Location.StoreSellPriceModifier * priceInfo.Price;
// Adjust by random price modifier
price = (100 - PriceModifier) / 100.0f * price;
// Adjust by requested good status
if (considerRequestedGoods && RequestedGoods.Contains(item))
{
price = Location.RequestGoodSellPriceModifier * price;
}
// Adjust by location reputation
price *= GetReputationModifier(false);
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (characters.Any())
{
price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false));
price *= 1f + characters.Max(c => GetMultiplierForItem(c, item));
float GetMultiplierForItem(Character character, ItemPrefab item)
{
return
item.Tags.Sum(tag => character.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)) +
character.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, Tags.StatIdentifierTargetAll);
}
}
// Price should never go below 1 mk
return Math.Max((int)price, 1);
}
public void SetMerchantFaction(Identifier factionIdentifier)
{
MerchantFaction = factionIdentifier;
}
public Identifier GetMerchantOrLocationFactionIdentifier()
{
return MerchantFaction.IfEmpty(Location.Faction?.Prefab.Identifier ?? Identifier.Empty);
}
public float GetReputationModifier(bool buying)
{
var factionIdentifier = GetMerchantOrLocationFactionIdentifier();
var reputation = GameMain.GameSession.Campaign.GetFaction(factionIdentifier)?.Reputation;
if (reputation == null) { return 1.0f; }
if (buying)
{
if (reputation.Value > 0.0f)
{
return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MaxReputation);
}
else
{
return MathHelper.Lerp(1.0f, 1.0f + MinReputationModifier, reputation.Value / reputation.MinReputation);
}
}
else
{
if (reputation.Value > 0.0f)
{
return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MaxReputation);
}
else
{
return MathHelper.Lerp(1.0f, 1.0f - MinReputationModifier, reputation.Value / reputation.MinReputation);
}
}
}
public override string ToString()
{
return Identifier.Value;
}
}
public Dictionary<Identifier, StoreInfo> Stores { get; private set; }
private float StoreMaxReputationModifier => Type.StoreMaxReputationModifier;
private float StoreMinReputationModifier => Type.StoreMinReputationModifier;
private float StoreSellPriceModifier => Type.StoreSellPriceModifier;
private float StoreBuyPriceModifier => Type.StoreBuyPriceModifier;
private float DailySpecialPriceModifier => Type.DailySpecialPriceModifier;
private float RequestGoodBuyPriceModifier => Type.RequestGoodBuyPriceModifier;
private float RequestGoodSellPriceModifier => Type.RequestGoodPriceModifier;
public int StoreInitialBalance => Type.StoreInitialBalance;
private int StorePriceModifierRange => Type.StorePriceModifierRange;
/// <summary>
/// How many map progress steps it takes before the discounts should be updated.
/// </summary>
private const int SpecialsUpdateInterval = 3;
public int DailySpecialsCount => Type.DailySpecialsCount;
public int RequestedGoodsCount => Type.RequestedGoodsCount;
private int StepsSinceSpecialsUpdated { get; set; }
public HashSet<Identifier> StoreIdentifiers { get; } = new HashSet<Identifier>();
#endregion
private const float MechanicalMaxDiscountPercentage = 50.0f;
private const float HealMaxDiscountPercentage = 10.0f;
private readonly List<TakenItem> takenItems = new List<TakenItem>();
public IEnumerable<TakenItem> TakenItems
{
get { return takenItems; }
}
private readonly HashSet<int> killedCharacterIdentifiers = new HashSet<int>();
public IEnumerable<int> KilledCharacterIdentifiers
{
get { return killedCharacterIdentifiers; }
}
private readonly List<Mission> availableMissions = new List<Mission>();
public IEnumerable<Mission> AvailableMissions
{
get
{
availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry) || m.ForceFailure);
return availableMissions;
}
}
/// <summary>
/// Missions that are available and visible in menus (<see cref="MissionPrefab.ShowInMenus"/>)
/// </summary>
public IEnumerable<Mission> AvailableAndVisibleMissions => AvailableMissions.Where(m => m.Prefab.ShowInMenus);
private readonly List<Mission> selectedMissions = new List<Mission>();
public IEnumerable<Mission> SelectedMissions
{
get
{
selectedMissions.RemoveAll(m => !availableMissions.Contains(m));
return selectedMissions;
}
}
public void SelectMission(Mission mission)
{
if (!SelectedMissions.Contains(mission) && mission != null)
{
selectedMissions.Add(mission);
selectedMissions.Sort((m1, m2) => availableMissions.IndexOf(m1).CompareTo(availableMissions.IndexOf(m2)));
}
}
public void DeselectMission(Mission mission)
{
selectedMissions.Remove(mission);
}
public List<int> GetSelectedMissionIndices()
{
List<int> selectedMissionIndices = new List<int>();
foreach (Mission mission in SelectedMissions)
{
if (availableMissions.Contains(mission))
{
selectedMissionIndices.Add(availableMissions.IndexOf(mission));
}
}
return selectedMissionIndices;
}
public void SetSelectedMissionIndices(IEnumerable<int> missionIndices)
{
selectedMissions.Clear();
foreach (int missionIndex in missionIndices)
{
if (missionIndex < 0 || missionIndex >= availableMissions.Count)
{
DebugConsole.ThrowError($"Failed to select a mission in location \"{DisplayName}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
break;
}
selectedMissions.Add(availableMissions[missionIndex]);
}
}
private float priceMultiplier = 1.0f;
public float PriceMultiplier
{
get { return priceMultiplier; }
set { priceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f); }
}
private float mechanicalpriceMultiplier = 1.0f;
public float MechanicalPriceMultiplier
{
get => mechanicalpriceMultiplier;
set => mechanicalpriceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f);
}
public string LastTypeChangeMessage;
public int TimeSinceLastTypeChange;
public bool IsGateBetweenBiomes;
private readonly struct LoadedMission
{
public readonly MissionPrefab MissionPrefab;
public readonly int TimesAttempted;
public readonly int OriginLocationIndex;
public readonly int DestinationIndex;
public readonly bool SelectedMission;
public LoadedMission(XElement element)
{
var id = element.GetAttributeIdentifier("prefabid", Identifier.Empty);
MissionPrefab = MissionPrefab.Prefabs.TryGet(id, out var prefab) ? prefab : null;
TimesAttempted = element.GetAttributeInt("timesattempted", 0);
OriginLocationIndex = element.GetAttributeInt("origin", -1);
DestinationIndex = element.GetAttributeInt("destinationindex", -1);
SelectedMission = element.GetAttributeBool("selected", false);
}
}
private List<LoadedMission> loadedMissions;
public HireManager HireManager;
public override string ToString()
{
return $"Location ({DisplayName ?? "null"})";
}
public Location(Vector2 mapPosition, int? zone, Identifier? biomeId, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
{
Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, biomeId, requireOutpost);
AssignRandomName(Type, rand, existingLocations);
MapPosition = mapPosition;
PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
Connections = new List<LocationConnection>();
}
/// <summary>
/// Create a location from save data
/// </summary>
public Location(CampaignMode campaign, XElement element)
{
Identifier locationTypeId = element.GetAttributeIdentifier("type", "");
bool typeNotFound = GetTypeOrFallback(locationTypeId, out LocationType type);
Type = type;
Identifier originalLocationTypeId = element.GetAttributeIdentifier("originaltype", locationTypeId);
GetTypeOrFallback(originalLocationTypeId, out LocationType originalType);
OriginalType = originalType;
nameIdentifier = element.GetAttributeIdentifier(nameof(nameIdentifier), "");
if (nameIdentifier.IsEmpty)
{
//backwards compatibility
DisplayName = element.GetAttributeString("name", "");
rawName = element.GetAttributeString("rawname", element.GetAttributeString("basename", DisplayName.Value));
nameIdentifier = rawName.ToIdentifier();
}
else
{
nameFormatIndex = element.GetAttributeInt(nameof(nameFormatIndex), 0);
DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
}
LoadChangingProperties(element, campaign);
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
Identifier biomeId = element.GetAttributeIdentifier("biome", Identifier.Empty);
if (biomeId != Identifier.Empty)
{
if (Biome.Prefabs.TryGet(biomeId, out Biome biome))
{
Biome = biome;
}
else
{
DebugConsole.ThrowError($"Error while loading the campaign map: could not find a biome with the identifier \"{biomeId}\".");
}
}
if (!typeNotFound)
{
for (int i = 0; i < Type.CanChangeTo.Count; i++)
{
for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++)
{
ProximityTimer.Add(Type.CanChangeTo[i].Requirements[j], element.GetAttributeInt("proximitytimer" + i + "-" + j, 0));
}
}
LoadLocationTypeChange(element);
}
string[] takenItemStr = element.GetAttributeStringArray("takenitems", Array.Empty<string>());
foreach (string takenItem in takenItemStr)
{
string[] takenItemSplit = takenItem.Split(';');
if (takenItemSplit.Length != 4)
{
DebugConsole.ThrowError($"Error in saved location: could not parse taken item data \"{takenItem}\"");
continue;
}
if (!ushort.TryParse(takenItemSplit[1], out ushort id))
{
DebugConsole.ThrowError($"Error in saved location: could not parse taken item id \"{takenItemSplit[1]}\"");
continue;
}
if (!int.TryParse(takenItemSplit[2], out int containerIndex))
{
DebugConsole.ThrowError($"Error in saved location: could not parse taken container index \"{takenItemSplit[2]}\"");
continue;
}
if (!ushort.TryParse(takenItemSplit[3], out ushort moduleIndex))
{
DebugConsole.ThrowError($"Error in saved location: could not parse taken item module index \"{takenItemSplit[3]}\"");
continue;
}
takenItems.Add(new TakenItem(takenItemSplit[0].ToIdentifier(), id, containerIndex, moduleIndex));
}
killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty<int>()).ToHashSet();
System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!");
Type ??= LocationType.Prefabs.First();
LevelData = new LevelData(element.GetChildElement("Level"), clampDifficultyToBiome: true);
PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value);
LoadStores(element);
LoadMissions(element);
bool GetTypeOrFallback(Identifier identifier, out LocationType type)
{
if (!LocationType.Prefabs.TryGet(identifier, out type))
{
//turn lairs into abandoned outposts
if (identifier == "lair")
{
LocationType.Prefabs.TryGet("Abandoned".ToIdentifier(), out type);
addInitialMissionsForType = Type;
}
if (type == null)
{
DebugConsole.AddWarning($"Could not find location type \"{identifier}\". Using location type \"None\" instead.");
LocationType.Prefabs.TryGet("None".ToIdentifier(), out type);
type ??= LocationType.Prefabs.First();
}
if (type != null)
{
element.SetAttributeValue("type", type.Identifier.ToString());
}
return false;
}
return true;
}
}
/// <summary>
/// Load the values of properties that can change mid-campaign and need to be updated when a client receives a new campaign save from the server
/// </summary>
public void LoadChangingProperties(XElement element, CampaignMode campaign)
{
PriceMultiplier = element.GetAttributeFloat(nameof(PriceMultiplier), 1.0f);
MechanicalPriceMultiplier = element.GetAttributeFloat(nameof(MechanicalPriceMultiplier), 1.0f);
TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0);
StepsSinceSpecialsUpdated = element.GetAttributeInt(nameof(StepsSinceSpecialsUpdated), 0);
WorldStepsSinceVisited = element.GetAttributeInt(nameof(WorldStepsSinceVisited), 0);
var factionIdentifier = element.GetAttributeIdentifier("faction", Identifier.Empty);
Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
var secondaryFactionIdentifier = element.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
}
public void LoadLocationTypeChange(XElement locationElement)
{
TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0);
LocationTypeChangeCooldown = locationElement.GetAttributeInt("locationtypechangecooldown", 0);
foreach (var subElement in locationElement.Elements())
{
switch (subElement.Name.ToString())
{
case "pendinglocationtypechange":
int timer = subElement.GetAttributeInt("timer", 0);
if (subElement.Attribute("index") != null)
{
int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0);
if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >= Type.CanChangeTo.Count)
{
DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{DisplayName}\". Location index out of bounds ({locationTypeChangeIndex}).");
continue;
}
PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null);
}
else
{
Identifier missionIdentifier = subElement.GetAttributeIdentifier("missionidentifier", "");
var mission = MissionPrefab.Prefabs[missionIdentifier];
if (mission == null)
{
DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{DisplayName}\". Matching mission not found.");
continue;
}
PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission);
}
break;
}
}
}
public void LoadMissions(XElement locationElement)
{
if (locationElement.GetChildElement("missions") is XElement missionsElement)
{
loadedMissions = new List<LoadedMission>();
foreach (XElement childElement in missionsElement.GetChildElements("mission"))
{
var loadedMission = new LoadedMission(childElement);
if (loadedMission.MissionPrefab != null)
{
loadedMissions.Add(loadedMission);
}
}
}
}
public static Location CreateRandom(Vector2 position, int? zone, Identifier? biomeId, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
{
return new Location(position, zone, biomeId, rand, requireOutpost, forceLocationType, existingLocations);
}
public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true, bool unlockInitialMissions = true)
{
if (newType == Type) { return; }
if (newType == null)
{
DebugConsole.ThrowError($"Failed to change the type of the location \"{DisplayName}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
Type = newType;
if (rawName != null)
{
DebugConsole.Log($"Location {rawName} changed it's type from {Type} to {newType}");
DisplayName =
Type.NameFormats == null || !Type.NameFormats.Any() ?
rawName :
Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", rawName);
}
else
{
DebugConsole.Log($"Location {DisplayName.Value} changed it's type from {Type} to {newType}");
DisplayName =
Type.NameFormats == null || !Type.NameFormats.Any() ?
TextManager.Get(nameIdentifier) :
Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", TextManager.Get(nameIdentifier).Value);
}
TryAssignFactionBasedOnLocationType(campaign);
if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
{
if (Type.Faction == Identifier.Empty) { Faction ??= campaign.GetRandomFaction(Rand.RandSync.Unsynced); }
if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced); }
}
else
{
if (Type.Faction == Identifier.Empty) { Faction = null; }
if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction = null; }
}
if (unlockInitialMissions && !IsCriticallyRadiated())
{
UnlockInitialMissions(Rand.RandSync.Unsynced);
}
if (createStores)
{
CreateStores(force: true);
}
else
{
ClearStores();
}
}
public void TryAssignFactionBasedOnLocationType(CampaignMode campaign)
{
if (campaign == null) { return; }
if (Type.Faction != Identifier.Empty)
{
Faction = Type.Faction == "None" ? null : TryFindFaction(Type.Faction);
}
if (Type.SecondaryFaction != Identifier.Empty)
{
SecondaryFaction = Type.SecondaryFaction == "None" ? null : TryFindFaction(Type.SecondaryFaction);
}
Faction TryFindFaction(Identifier identifier)
{
var faction = campaign.GetFaction(identifier);
if (faction == null)
{
DebugConsole.ThrowError($"Error in location type \"{Type.Identifier}\": failed to find a faction with the identifier \"{identifier}\".",
contentPackage: Type.ContentPackage);
}
return faction;
}
}
public void UnlockInitialMissions(Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
{
if (Type.MissionIdentifiers.Any())
{
UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(randSync), invokingContentPackage: Type.ContentPackage);
}
if (Type.MissionTags.Any())
{
UnlockMissionByTag(Type.MissionTags.GetRandom(randSync), invokingContentPackage: Type.ContentPackage);
}
}
public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
{
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; }
AddMission(InstantiateMission(missionPrefab, connection));
}
public void UnlockMission(MissionPrefab missionPrefab)
{
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; }
AddMission(InstantiateMission(missionPrefab));
}
public Mission UnlockMissionByIdentifier(Identifier identifier, ContentPackage invokingContentPackage = null)
{
if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; }
if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; }
var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier);
if (missionPrefab == null)
{
DebugConsole.ThrowError($"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found.",
contentPackage: invokingContentPackage);
}
else
{
var mission = InstantiateMission(missionPrefab, out LocationConnection connection);
//don't allow duplicate missions in the same connection
if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
{
return null;
}
AddMission(mission);
DebugConsole.NewMessage($"Unlocked a mission by \"{identifier}\".", debugOnly: true);
return mission;
}
return null;
}
public Mission UnlockMissionByTag(Identifier tag, Random random = null, ContentPackage invokingContentPackage = null)
{
if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; }
var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Contains(tag));
if (matchingMissions.None())
{
DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found.", contentPackage: invokingContentPackage);
}
else
{
var unusedMissions = matchingMissions.Where(m => availableMissions.None(mission => mission.Prefab == m));
if (unusedMissions.Any())
{
var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)) || m.IsAllowed(this, this)));
if (suitableMissions.None())
{
suitableMissions = unusedMissions;
}
var filteredMissions = suitableMissions.Where(m => LevelData.Difficulty >= m.MinLevelDifficulty && LevelData.Difficulty <= m.MaxLevelDifficulty);
if (filteredMissions.None())
{
DebugConsole.AddWarning($"No suitable mission matching the level difficulty {LevelData.Difficulty} found with the tag \"{tag}\". Ignoring the restriction.",
contentPackage: invokingContentPackage);
}
else
{
suitableMissions = filteredMissions;
}
MissionPrefab missionPrefab =
random != null ?
ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, random) :
ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, Rand.RandSync.Unsynced);
var mission = InstantiateMission(missionPrefab, out LocationConnection connection);
//don't allow duplicate missions in the same connection
if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
{
return null;
}
AddMission(mission);
DebugConsole.NewMessage($"Unlocked a random mission by \"{tag}\": {mission.Prefab.Identifier} (difficulty level: {LevelData.Difficulty})", debugOnly: true);
return mission;
}
else
{
DebugConsole.AddWarning($"Failed to unlock a mission with the tag \"{tag}\": all available missions have already been unlocked.",
contentPackage: invokingContentPackage);
}
}
return null;
}
private void AddMission(Mission mission)
{
if (!mission.Prefab.AllowOtherMissionsInLevel)
{
availableMissions.Clear();
}
availableMissions.Add(mission);
#if CLIENT
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
#else
(GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
#endif
}
private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection)
{
if (prefab.IsAllowed(this, this))
{
connection = null;
return InstantiateMission(prefab);
}
var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this)));
if (suitableConnections.None())
{
suitableConnections = Connections.ToList();
}
//prefer connections that haven't been passed through, and connections with fewer available missions
connection = ToolBox.SelectWeightedRandom(
suitableConnections.ToList(),
suitableConnections.Select(c => GetConnectionWeight(this, c)).ToList(),
Rand.RandSync.Unsynced);
static float GetConnectionWeight(Location location, LocationConnection c)
{
Location destination = c.OtherLocation(location);
if (destination == null) { return 0; }
float minWeight = 0.0001f;
float lowWeight = 0.2f;
float normalWeight = 1.0f;
float maxWeight = 2.0f;
float weight = c.Passed ? lowWeight : normalWeight;
if (location.Biome.AllowedZones.Contains(1))
{
// In the first biome, give a stronger preference for locations that are farther to the right)
float diff = destination.MapPosition.X - location.MapPosition.X;
if (diff < 0)
{
weight *= 0.1f;
}
else
{
float maxRelevantDiff = 300;
weight = MathHelper.Lerp(weight, maxWeight, MathUtils.InverseLerp(0, maxRelevantDiff, diff));
}
}
else if (destination.MapPosition.X > location.MapPosition.X)
{
weight *= 2.0f;
}
int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination));
if (missionCount > 0)
{
weight /= missionCount * 2;
}
if (destination.IsRadiated())
{
weight *= 0.001f;
}
return MathHelper.Clamp(weight, minWeight, maxWeight);
}
return InstantiateMission(prefab, connection);
}
private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection)
{
Location destination = connection.OtherLocation(this);
var mission = prefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub);
mission.AdjustLevelData(connection.LevelData);
return mission;
}
private Mission InstantiateMission(MissionPrefab prefab)
{
var mission = prefab.Instantiate(new Location[] { this, this }, Submarine.MainSub);
mission.AdjustLevelData(LevelData);
return mission;
}
public void InstantiateLoadedMissions(Map map)
{
availableMissions.Clear();
selectedMissions.Clear();
if (loadedMissions != null && loadedMissions.Any())
{
foreach (LoadedMission loadedMission in loadedMissions)
{
Location destination;
if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.Locations.Count)
{
destination = map.Locations[loadedMission.DestinationIndex];
}
else
{
destination = Connections.First().OtherLocation(this);
}
var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub);
if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.Locations.Count)
{
mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex];
}
mission.TimesAttempted = loadedMission.TimesAttempted;
availableMissions.Add(mission);
if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
var levelData = destination == this ? LevelData : Connections.FirstOrDefault(c => c.OtherLocation(this) == destination)?.LevelData;
if (levelData != null)
{
mission.AdjustLevelData(levelData);
}
}
loadedMissions = null;
}
if (addInitialMissionsForType != null)
{
if (addInitialMissionsForType.MissionIdentifiers.Any())
{
UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandomUnsynced(), invokingContentPackage: Type.ContentPackage);
}
if (addInitialMissionsForType.MissionTags.Any())
{
UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandomUnsynced(), invokingContentPackage: Type.ContentPackage);
}
addInitialMissionsForType = null;
}
}
/// <summary>
/// Removes all unlocked missions from the location
/// </summary>
public void ClearMissions()
{
availableMissions.Clear();
selectedMissions.Clear();
}
public bool HasOutpost()
{
if (!Type.HasOutpost) { return false; }
return !IsCriticallyRadiated();
}
public bool IsCriticallyRadiated()
{
if (GameMain.GameSession?.Map?.Radiation != null)
{
return TurnsInRadiation > GameMain.GameSession.Map.Radiation.Params.CriticalRadiationThreshold;
}
return false;
}
public LocationType GetLocationTypeToDisplay(out Identifier overrideDescriptionIdentifier)
{
overrideDescriptionIdentifier = Identifier.Empty;
if (IsCriticallyRadiated() && !Type.ReplaceInRadiation.IsEmpty)
{
if (LocationType.Prefabs.TryGet(Type.ReplaceInRadiation, out LocationType newLocationType))
{
if (!newLocationType.DescriptionInRadiation.IsEmpty)
{
overrideDescriptionIdentifier = newLocationType.DescriptionInRadiation;
}
return newLocationType;
}
else
{
DebugConsole.ThrowError($"Error when trying to get a new location type for an irradiated location - location type \"{newLocationType}\" not found.");
}
}
return Type;
}
public LocationType GetLocationTypeToDisplay()
{
return GetLocationTypeToDisplay(out _);
}
public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
{
System.Diagnostics.Debug.Assert(Connections.Contains(connection));
return AvailableMissions.Where(m => m.Locations[1] == connection.OtherLocation(this));
}
public void RemoveHireableCharacter(CharacterInfo character)
{
if (!Type.HasHireableCharacters)
{
DebugConsole.ThrowErrorLocalized("Cannot hire a character from location \"" + DisplayName + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
if (HireManager == null)
{
DebugConsole.ThrowErrorLocalized("Cannot hire a character from location \"" + DisplayName + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
HireManager.RemoveCharacter(character);
}
public IEnumerable<CharacterInfo> GetHireableCharacters()
{
if (!Type.HasHireableCharacters)
{
return Enumerable.Empty<CharacterInfo>();
}
HireManager ??= new HireManager();
if (!HireManager.AvailableCharacters.Any())
{
HireManager.GenerateCharacters(location: this, amount: HireManager.MaxAvailableCharacters);
}
return HireManager.AvailableCharacters;
}
public void ForceHireableCharacters(IEnumerable<CharacterInfo> hireableCharacters)
{
HireManager ??= new HireManager();
HireManager.AvailableCharacters = hireableCharacters.ToList();
}
public void AssignRandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
{
if (!type.ForceLocationName.IsEmpty)
{
nameIdentifier = type.ForceLocationName;
DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
return;
}
nameIdentifier = type.GetRandomNameId(rand, existingLocations);
if (nameIdentifier.IsEmpty)
{
//backwards compatibility
rawName = type.GetRandomRawName(rand, existingLocations);
if (rawName.IsNullOrEmpty())
{
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.");
rawName = "none";
}
nameIdentifier = rawName.ToIdentifier();
if (type.NameFormats == null || !type.NameFormats.Any())
{
DisplayName = rawName;
}
else
{
nameFormatIndex = rand.Next() % type.NameFormats.Count;
DisplayName = type.NameFormats[nameFormatIndex].Replace("[name]", rawName);
}
}
else
{
if (type.NameFormats == null || !type.NameFormats.Any())
{
DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
return;
}
nameFormatIndex = rand.Next() % type.NameFormats.Count;
DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
}
}
public static LocalizedString GetName(Identifier locationTypeIdentifier, int nameFormatIndex, Identifier nameId)
{
if (LocationType.Prefabs.TryGet(locationTypeIdentifier, out LocationType locationType))
{
return GetName(locationType, nameFormatIndex, nameId);
}
else
{
DebugConsole.ThrowError($"Could not find the location type {locationTypeIdentifier}.\n" + Environment.StackTrace.CleanUpPath());
return new RawLString(nameId.Value);
}
}
public static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
{
if (type?.NameFormats == null || !type.NameFormats.Any() || nameFormatIndex < 0)
{
return TextManager.Get(nameId).Fallback(nameId.Value);
}
return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace("[name]", TextManager.Get(nameId).Value);
}
public void ForceName(Identifier nameId)
{
rawName = string.Empty;
nameIdentifier = nameId;
DisplayName = TextManager.Get(nameId).Fallback(nameId.Value);
}
public void LoadStores(XElement locationElement)
{
UpdateStoreIdentifiers();
Stores?.Clear();
bool hasStores = false;
foreach (var storeElement in locationElement.GetChildElements("store"))
{
hasStores = true;
Stores ??= new Dictionary<Identifier, StoreInfo>();
var identifier = storeElement.GetAttributeIdentifier("identifier", "");
if (identifier.IsEmpty)
{
// Previously saved store data (with no identifier) is discarded and new store data will be created
continue;
}
if (StoreIdentifiers.Contains(identifier))
{
if (!Stores.ContainsKey(identifier))
{
Stores.Add(identifier, new StoreInfo(this, storeElement));
}
else
{
string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": duplicate identifier.";
DebugConsole.ThrowError(msg);
GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:DuplicateStoreInfo", GameAnalyticsManager.ErrorSeverity.Error, msg);
continue;
}
}
else
{
string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier.";
DebugConsole.ThrowError(msg);
GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:IncorrectStoreIdentifier", GameAnalyticsManager.ErrorSeverity.Error, msg);
continue;
}
}
// Backwards compatibility: create new stores for any identifiers not present in the save data
if (hasStores)
{
foreach (var id in StoreIdentifiers)
{
AddNewStore(id);
}
}
}
public bool IsRadiated() => GameMain.GameSession?.Map?.Radiation != null && GameMain.GameSession.Map.Radiation.Enabled && GameMain.GameSession.Map.Radiation.DepthInRadiation(this) > 0;
/// <summary>
/// Mark the items that have been taken from the outpost to prevent them from spawning when re-entering the outpost
/// </summary>
public void RegisterTakenItems(IEnumerable<Item> items)
{
foreach (Item item in items)
{
if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.ID)) { continue; }
if (item.IsSalvageMissionItem) { continue; }
if (item.OriginalModuleIndex < 0)
{
DebugConsole.ThrowError("Tried to register a non-outpost item as being taken from the outpost.");
continue;
}
takenItems.Add(new TakenItem(item));
}
}
/// <summary>
/// Mark the characters who have been killed to prevent them from spawning when re-entering the outpost
/// </summary>
public void RegisterKilledCharacters(IEnumerable<Character> characters)
{
foreach (Character character in characters)
{
if (character?.Info == null) { continue; }
killedCharacterIdentifiers.Add(character.Info.GetIdentifier());
}
}
public void RemoveTakenItems()
{
foreach (TakenItem takenItem in takenItems)
{
Item item = Item.ItemList.Find(it => takenItem.Matches(it));
item?.Remove();
}
}
public int GetAdjustedMechanicalCost(int cost)
{
float discount = 0.0f;
if (Reputation != null)
{
discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f);
}
return (int)Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier);
}
public int GetAdjustedHealCost(int cost)
{
float discount = 0.0f;
if (Reputation != null)
{
discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f);
}
return (int) Math.Ceiling((1.0f - discount) * cost * PriceMultiplier);
}
public StoreInfo GetStore(Identifier identifier)
{
if (Stores != null && Stores.TryGetValue(identifier, out var store))
{
return store;
}
return null;
}
/// <summary>
/// Create stores and stocks for the location. If the location already has stores, the method will not do anything unless the "force" argument is true. />
/// </summary>
/// <param name="force">If true, the stores will be recreated if they already exists.</param>
public void CreateStores(bool force = false)
{
// In multiplayer, stores should be created by the server and loaded from save data by clients
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (!force && Stores != null) { return; }
UpdateStoreIdentifiers();
if (Stores != null)
{
// Remove any stores with no corresponding merchants at the location
foreach (var storeIdentifier in Stores.Keys)
{
if (!StoreIdentifiers.Contains(storeIdentifier))
{
Stores.Remove(storeIdentifier);
}
}
foreach (var identifier in StoreIdentifiers)
{
if (Stores.TryGetValue(identifier, out var store))
{
store.Balance = Math.Max(store.Balance, StoreInitialBalance);
var newStock = store.CreateStock();
foreach (var oldStockItem in store.Stock)
{
if (newStock.Find(i => i.ItemPrefab == oldStockItem.ItemPrefab) is { } newStockItem)
{
if (oldStockItem.Quantity > newStockItem.Quantity)
{
newStockItem.Quantity = oldStockItem.Quantity;
}
}
}
store.Stock.Clear();
store.Stock.AddRange(newStock);
store.GenerateSpecials();
store.GeneratePriceModifier();
}
else
{
AddNewStore(identifier);
}
}
}
else
{
foreach (var identifier in StoreIdentifiers)
{
AddNewStore(identifier);
}
}
}
public void UpdateStores(bool createStoresIfNotCreated = true)
{
// In multiplayer, stores should be updated by the server and loaded from save data by clients
if (GameMain.NetworkMember is { IsClient: true }) { return; }
if (Stores == null)
{
if (createStoresIfNotCreated) { CreateStores(); }
return;
}
var storesToRemove = new HashSet<Identifier>();
foreach (var store in Stores.Values)
{
if (!StoreIdentifiers.Contains(store.Identifier))
{
storesToRemove.Add(store.Identifier);
continue;
}
if (store.Balance < StoreInitialBalance)
{
store.Balance = Math.Min(store.Balance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance);
}
var stock = new List<PurchasedItem>(store.Stock);
var stockToRemove = new List<PurchasedItem>();
foreach (var itemPrefab in ItemPrefab.Prefabs)
{
var existingStock = stock.FirstOrDefault(s => s.ItemPrefabIdentifier == itemPrefab.Identifier);
if (itemPrefab.CanBeBoughtFrom(store, out PriceInfo priceInfo))
{
if (existingStock == null)
{
//can be bought from the location, but not in stock - some new item added by an update or mod?
stock.Add(StoreInfo.CreateInitialStockItem(this, itemPrefab, priceInfo));
}
else
{
existingStock.Quantity =
Math.Min(
existingStock.Quantity + 1,
priceInfo.MaxAvailableAmount);
}
}
else if (existingStock != null)
{
stockToRemove.Add(existingStock);
}
}
stockToRemove.ForEach(i => stock.Remove(i));
store.Stock.Clear();
store.Stock.AddRange(stock);
store.GeneratePriceModifier();
}
StepsSinceSpecialsUpdated++;
foreach (var identifier in storesToRemove)
{
Stores.Remove(identifier);
}
foreach (var identifier in StoreIdentifiers)
{
AddNewStore(identifier);
}
}
public void UpdateSpecials()
{
if (GameMain.NetworkMember is { IsClient: true } || Stores is null) { return; }
int extraSpecialSalesCount = GetExtraSpecialSalesCount();
foreach (StoreInfo store in Stores.Values)
{
if (StepsSinceSpecialsUpdated < SpecialsUpdateInterval && store.DailySpecials.Count == DailySpecialsCount + extraSpecialSalesCount) { continue; }
store.GenerateSpecials();
}
}
private void UpdateStoreIdentifiers()
{
StoreIdentifiers.Clear();
foreach (var outpostParam in OutpostGenerationParams.OutpostParams)
{
if (!outpostParam.AllowedLocationTypes.Contains(Type.Identifier)) { continue; }
foreach (var identifier in outpostParam.GetStoreIdentifiers())
{
StoreIdentifiers.Add(identifier);
}
}
}
private void AddNewStore(Identifier identifier)
{
Stores ??= new Dictionary<Identifier, StoreInfo>();
if (Stores.ContainsKey(identifier)) { return; }
var newStore = new StoreInfo(this, identifier);
Stores.Add(identifier, newStore);
}
public void AddStock(Dictionary<Identifier, List<SoldItem>> items)
{
if (items == null) { return; }
foreach (var storeItems in items)
{
if (GetStore(storeItems.Key) is { } store)
{
store.AddStock(storeItems.Value);
}
}
}
/// <summary>
/// Removes all information about stores from the location (can be used to avoid storing unnecessary
/// store info about locations that haven't been visited in a long time). The stores are automatically
/// recreated when the player enters the location.
/// </summary>
public void ClearStores()
{
Stores = null;
}
public void RemoveStock(Dictionary<Identifier, List<PurchasedItem>> items)
{
if (items == null) { return; }
foreach (var storeItems in items)
{
if (GetStore(storeItems.Key) is { } store)
{
store.RemoveStock(storeItems.Value);
}
}
}
public static int GetExtraSpecialSalesCount()
{
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (!characters.Any()) { return 0; }
return characters.Max(static c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount));
}
public bool CanHaveSubsForSale()
{
return HasOutpost() && CanHaveCampaignInteraction(CampaignMode.InteractionType.PurchaseSub);
}
public int HighestSubmarineTierAvailable(SubmarineClass submarineClass = SubmarineClass.Undefined)
{
if (CanHaveSubsForSale())
{
return Biome?.HighestSubmarineTierAvailable(submarineClass, Type.Identifier) ?? SubmarineInfo.HighestTier;
}
return 0;
}
public bool IsSubmarineAvailable(SubmarineInfo info)
{
return Biome?.IsSubmarineAvailable(info, Type.Identifier) ?? true;
}
private bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType)
{
return LevelData != null &&
LevelData.OutpostGenerationParamsExist &&
LevelData.GetSuitableOutpostGenerationParams(this, LevelData).Any(p => p.CanHaveCampaignInteraction(interactionType));
}
public void Reset(CampaignMode campaign)
{
if (Type != OriginalType && !DisallowLocationTypeChanges)
{
ChangeType(campaign, OriginalType);
PendingLocationTypeChange = null;
}
ClearStores();
ClearMissions();
LevelData?.EventHistory?.Clear();
UnlockInitialMissions();
}
public XElement Save(Map map, XElement parentElement)
{
var locationElement = new XElement("location",
new XAttribute("type", Type.Identifier),
new XAttribute("originaltype", (Type ?? OriginalType).Identifier),
/*not used currently (we load the nameIdentifier instead),
* but could make sense to include still for backwards compatibility reasons*/
new XAttribute("name", DisplayName),
new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty),
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
new XAttribute("pricemultiplier", PriceMultiplier),
new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes),
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier),
new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange),
new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation),
new XAttribute(nameof(StepsSinceSpecialsUpdated), StepsSinceSpecialsUpdated),
new XAttribute(nameof(WorldStepsSinceVisited), WorldStepsSinceVisited));
if (!rawName.IsNullOrEmpty())
{
locationElement.Add(new XAttribute(nameof(rawName), rawName));
}
else
{
locationElement.Add(new XAttribute(nameof(nameIdentifier), nameIdentifier));
locationElement.Add(new XAttribute(nameof(nameFormatIndex), nameFormatIndex));
}
if (Faction != null)
{
locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier));
}
if (SecondaryFaction != null)
{
locationElement.Add(new XAttribute("secondaryfaction", SecondaryFaction.Prefab.Identifier));
}
LevelData.Save(locationElement);
for (int i = 0; i < Type.CanChangeTo.Count; i++)
{
for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++)
{
if (ProximityTimer.ContainsKey(Type.CanChangeTo[i].Requirements[j]))
{
locationElement.Add(new XAttribute("proximitytimer" + i + "-" + j, ProximityTimer[Type.CanChangeTo[i].Requirements[j]]));
}
}
}
if (PendingLocationTypeChange.HasValue)
{
var changeElement = new XElement("pendinglocationtypechange", new XAttribute("timer", PendingLocationTypeChange.Value.delay));
if (PendingLocationTypeChange.Value.parentMission != null)
{
changeElement.Add(new XAttribute("missionidentifier", PendingLocationTypeChange.Value.parentMission.Identifier));
locationElement.Add(changeElement);
}
else
{
int index = Type.CanChangeTo.IndexOf(PendingLocationTypeChange.Value.typeChange);
changeElement.Add(new XAttribute("index", index));
if (index == -1)
{
DebugConsole.AddWarning($"Invalid location type change in the location \"{DisplayName}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
}
else
{
locationElement.Add(changeElement);
}
}
}
if (LocationTypeChangeCooldown > 0)
{
locationElement.Add(new XAttribute("locationtypechangecooldown", LocationTypeChangeCooldown));
}
if (takenItems.Any())
{
locationElement.Add(new XAttribute(
"takenitems",
string.Join(',', takenItems.Select(it => it.Identifier + ";" + it.OriginalID + ";" + it.OriginalContainerIndex + ";" + it.ModuleIndex))));
}
if (killedCharacterIdentifiers.Any())
{
locationElement.Add(new XAttribute("killedcharacters", string.Join(',', killedCharacterIdentifiers)));
}
if (Stores != null)
{
foreach (var store in Stores.Values)
{
var storeElement = new XElement("store",
new XAttribute("identifier", store.Identifier.Value),
new XAttribute(nameof(store.MerchantFaction), store.MerchantFaction),
new XAttribute("balance", store.Balance),
new XAttribute("pricemodifier", store.PriceModifier));
foreach (PurchasedItem item in store.Stock)
{
if (item?.ItemPrefab == null) { continue; }
storeElement.Add(new XElement("stock",
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity)));
}
if (store.DailySpecials.Any())
{
var dailySpecialElement = new XElement("dailyspecials");
foreach (var item in store.DailySpecials)
{
dailySpecialElement.Add(new XElement("item",
new XAttribute("id", item.Identifier)));
}
storeElement.Add(dailySpecialElement);
}
if (store.RequestedGoods.Any())
{
var requestedGoodsElement = new XElement("requestedgoods");
foreach (var item in store.RequestedGoods)
{
requestedGoodsElement.Add(new XElement("item",
new XAttribute("id", item.Identifier)));
}
storeElement.Add(requestedGoodsElement);
}
locationElement.Add(storeElement);
}
}
if (AvailableMissions is List<Mission> missions && missions.Any())
{
var missionsElement = new XElement("missions");
foreach (Mission mission in missions)
{
var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this);
var destinationIndex = map.Locations.IndexOf(location);
var originIndex = map.Locations.IndexOf(mission.OriginLocation);
missionsElement.Add(new XElement("mission",
new XAttribute("prefabid", mission.Prefab.Identifier),
new XAttribute("destinationindex", destinationIndex),
new XAttribute(nameof(Mission.TimesAttempted), mission.TimesAttempted),
new XAttribute("origin", originIndex),
new XAttribute("selected", selectedMissions.Contains(mission))));
}
locationElement.Add(missionsElement);
}
parentElement.Add(locationElement);
return locationElement;
}
public void Remove()
{
RemoveProjSpecific();
}
public void RemoveProjSpecific()
{
HireManager?.Remove();
}
public class AbilityLocation : AbilityObject, IAbilityLocation
{
public AbilityLocation(Location location)
{
Location = location;
}
public Location Location { get; set; }
}
}
}