Release 1.9.7.0 - Summer Update 2025

This commit is contained in:
Regalis11
2025-06-17 16:38:11 +03:00
parent 22227f13e5
commit ea5a2bc693
297 changed files with 7344 additions and 2421 deletions
@@ -478,6 +478,11 @@ namespace Barotrauma
}
}
/// <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
{
@@ -580,9 +585,9 @@ namespace Barotrauma
return $"Location ({DisplayName ?? "null"})";
}
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = 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, requireOutpost);
Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, biomeId, requireOutpost);
CreateRandomName(Type, rand, existingLocations);
MapPosition = mapPosition;
PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
@@ -782,12 +787,12 @@ namespace Barotrauma
}
}
public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
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, rand, requireOutpost, forceLocationType, existingLocations);
return new Location(position, zone, biomeId, rand, requireOutpost, forceLocationType, existingLocations);
}
public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true)
public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true, bool unlockInitialMissions = true)
{
if (newType == Type) { return; }
@@ -826,10 +831,10 @@ namespace Barotrauma
if (Type.Faction == Identifier.Empty) { Faction = null; }
if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction = null; }
}
if (!IsCriticallyRadiated())
if (unlockInitialMissions && !IsCriticallyRadiated())
{
UnlockInitialMissions(Rand.RandSync.Unsynced);
UnlockInitialMissions(Rand.RandSync.Unsynced);
}
if (createStores)
@@ -1257,7 +1262,7 @@ namespace Barotrauma
{
if (type?.NameFormats == null || !type.NameFormats.Any() || nameFormatIndex < 0)
{
return TextManager.Get(nameId);
return TextManager.Get(nameId).Fallback(nameId.Value);
}
return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace("[name]", TextManager.Get(nameId).Value);
}
@@ -13,7 +13,7 @@ namespace Barotrauma
class LocationType : PrefabWithUintIdentifier
{
public static readonly PrefabCollection<LocationType> Prefabs = new PrefabCollection<LocationType>();
private readonly ImmutableArray<string> rawNames;
private readonly ImmutableArray<Sprite> portraits;
@@ -21,13 +21,14 @@ namespace Barotrauma
private readonly ImmutableArray<(Identifier Identifier, float Commonness, bool AlwaysAvailableIfMissingFromCrew)> hireableJobs;
private readonly float totalHireableWeight;
public readonly Dictionary<int, float> CommonnessPerZone = new Dictionary<int, float>();
public readonly Dictionary<int, int> MinCountPerZone = new Dictionary<int, int>();
public readonly LocalizedString Name;
public readonly LocalizedString Description;
public readonly Identifier ForceLocationName;
/// <summary>
/// Forces all locations of this LocationType to use the given name. Can either be a text tag or the actual name.
/// </summary>
public readonly Identifier ForceLocationName;
public readonly float BeaconStationChance;
@@ -43,11 +44,151 @@ namespace Barotrauma
public readonly ImmutableArray<Identifier> MissionIdentifiers;
public readonly ImmutableArray<Identifier> MissionTags;
public readonly List<string> HideEntitySubcategories = new List<string>();
public abstract class AreaSettingData
{
public int? MinCount { get; }
public int? MaxCount { get; }
public float Commonness { get; }
public bool IsEnterable { get; private set; }
/// <summary>
/// Desired position in the biome or difficulty zone. A value between 0 and 1, where 0 is the left side of the zone/biome, and 1 the right side.
/// </summary>
public float? DesiredPosition { get; }
public bool HasCounts => MinCount.HasValue && MaxCount.HasValue && (MaxCount > 0 || MinCount > 0);
public virtual bool HasValidData => true;
public bool AllowAsBiomeGate { get; private set; }
internal AreaSettingData(int? minCount, int? maxCount, float commonness, float? desiredPosition)
{
MinCount = minCount;
MaxCount = maxCount;
Commonness = commonness;
DesiredPosition = desiredPosition;
}
public virtual bool MatchesRemainingCount(MapLocationTypeGenerator.LocationTypeCount locationTypeCount)
{
return false;
}
public virtual bool MatchesLocation(Map map, Location location)
{
return false;
}
public virtual bool MatchesZone(int zoneIndex)
{
return false;
}
public virtual bool MatchesBiome(Identifier biomeIdentifier)
{
return false;
}
public virtual bool Matches(int? zone = null, Identifier? biomeId = null)
{
return false;
}
}
public class BiomeSettingData : AreaSettingData
{
public Identifier BiomeIdentifier { get; }
public override bool HasValidData => Biome.Prefabs.ContainsKey(BiomeIdentifier);
public BiomeSettingData(Identifier biomeIdentifier, int? minCount, int? maxCount, float commonness, float? desiredPosition, LocationType locationType)
: base(minCount, maxCount, commonness, desiredPosition)
{
if (minCount > maxCount)
{
DebugConsole.AddWarning($"Error in location type {locationType.Identifier}: minimum count larger than maximum count in biome {biomeIdentifier}.",
contentPackage: locationType.ContentPackage);
}
BiomeIdentifier = biomeIdentifier;
}
public override bool MatchesRemainingCount(MapLocationTypeGenerator.LocationTypeCount locationTypeCount)
{
return locationTypeCount.BiomeId == BiomeIdentifier;
}
public override bool MatchesLocation(Map map, Location location)
{
return BiomeIdentifier == location.Biome?.Identifier;
}
public override bool MatchesBiome(Identifier biomeIdentifier)
{
return BiomeIdentifier == biomeIdentifier;
}
public override bool Matches(int? zone = null, Identifier? biomeId = null)
{
return biomeId.HasValue && biomeId == BiomeIdentifier;
}
}
public class DifficultyZoneSettingData : AreaSettingData
{
public int DifficultyZone { get; }
public DifficultyZoneSettingData(int difficultyZone, int? minCount, int? maxCount, float commonness, float? desiredPosition, LocationType locationType)
: base(minCount, maxCount, commonness, desiredPosition)
{
if (minCount > maxCount)
{
DebugConsole.AddWarning($"Error in location type {locationType.Identifier}: minimum count larger than maximum count in difficulty zone {difficultyZone}.",
contentPackage: locationType.ContentPackage);
}
DifficultyZone = difficultyZone;
}
public override bool MatchesRemainingCount(MapLocationTypeGenerator.LocationTypeCount locationTypeCount)
{
return locationTypeCount.DifficultyZone == DifficultyZone;
}
public override bool MatchesLocation(Map map, Location location)
{
return DifficultyZone == map.GetZoneIndex(location.MapPosition.X);
}
public override bool MatchesZone(int zoneIndex)
{
return DifficultyZone == zoneIndex;
}
public override bool Matches(int? zone = null, Identifier? biomeId = null)
{
return zone.HasValue && zone == DifficultyZone;
}
}
public readonly List<AreaSettingData> AreaSettings = new List<AreaSettingData>();
public readonly List<string> HideEntitySubcategories;
public enum BiomeGateSetting
{
/// <summary>
/// Can be used as a gate between biomes, but not required
/// </summary>
Allow,
/// <summary>
/// Cannot be used as a gate between biomes
/// </summary>
Deny,
/// <summary>
/// Must be used as a gate between biomes during map generation
/// </summary>
Force
}
public BiomeGateSetting BiomeGate { get; private set; }
public bool ForceAsStartOutpost { get; private set; }
/// <summary>
/// Can this location type be used in the random, non-campaign levels that don't take place in any specific zone
@@ -108,10 +249,26 @@ namespace Barotrauma
private readonly Identifier forceOutpostGenerationParamsIdentifier;
/// <summary>
/// Can be used to make the location type use the same background music tracks as another location type.
/// </summary>
public readonly Identifier BackgroundMusicLocationType;
/// <summary>
/// If set to true, only event sets that explicitly define this location type in <see cref="EventSet.LocationTypeIdentifiers"/> can be selected at this location. Defaults to false.
/// </summary>
public bool IgnoreGenericEvents { get; }
/// <summary>
/// Used as criteria for validating if a given event set is suitable for this locationType.
/// For example, if set to "city", events that appear in "city" type locations can also appear here.
/// </summary>
public Identifier EventLocationType { get; private set; }
/// <summary>
/// If set, outpost modules configured to be suitable for the specified location type can also be used in this type of location.
/// </summary>
public Identifier UseOutpostModulesOfLocationType { get; set; }
public Color SpriteColor
{
@@ -134,7 +291,7 @@ namespace Barotrauma
public int DailySpecialsCount { get; } = 1;
public int RequestedGoodsCount { get; } = 1;
public readonly bool ShowSonarMarker = true;
public readonly bool ShowSonarMarker;
public override string ToString()
{
@@ -145,13 +302,41 @@ namespace Barotrauma
{
Name = TextManager.Get("LocationName." + Identifier, "unknown");
Description = TextManager.Get("LocationDescription." + Identifier, "");
// for location types based on others, e.g., Named Unique outpost, we may want to override the name of the type to still say Outpost on the map:
var forceNameId = element.GetAttributeIdentifier("ForceLocationTypeName", string.Empty);
if (!forceNameId.IsEmpty)
{
var forcedName = TextManager.Get("LocationName." + forceNameId);
if (!forcedName.IsNullOrEmpty())
{
Name = forcedName;
}
}
var forceDescriptionId = element.GetAttributeIdentifier("ForceLocationTypeDescription", string.Empty);
if (!forceDescriptionId.IsEmpty)
{
var forcedDescription = TextManager.Get("LocationDescription." + forceDescriptionId);
if (!forcedDescription.IsNullOrEmpty())
{
Description = forcedDescription;
}
}
BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f);
UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true);
HasOutpost = element.GetAttributeBool("hasoutpost", true);
IsEnterable = element.GetAttributeBool("isenterable", HasOutpost);
AllowAsBiomeGate = element.GetAttributeBool(nameof(AllowAsBiomeGate), true);
bool allowAsBiomeGateLegacy = element.GetAttributeBool("allowasbiomegate", true);
BiomeGate = element.GetAttributeEnum("BiomeGate", def: allowAsBiomeGateLegacy ? BiomeGateSetting.Allow : BiomeGateSetting.Deny);
if (BiomeGate != BiomeGateSetting.Deny && !HasOutpost)
{
DebugConsole.AddWarning($"Potential error in location type {Identifier}: the location is set to be allowed as a biome gate, but will never be chosen as one because it has no outpost.",
contentPackage: ContentPackage);
}
ForceAsStartOutpost = element.GetAttributeBool(nameof(ForceAsStartOutpost), false);
AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true);
Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty);
@@ -168,17 +353,24 @@ namespace Barotrauma
DescriptionInRadiation = element.GetAttributeIdentifier(nameof(DescriptionInRadiation), "locationdescription.abandonedirradiated");
forceOutpostGenerationParamsIdentifier = element.GetAttributeIdentifier("forceoutpostgenerationparams", Identifier.Empty);
BackgroundMusicLocationType = element.GetAttributeIdentifier(nameof(BackgroundMusicLocationType), Identifier.Empty);
IgnoreGenericEvents = element.GetAttributeBool(nameof(IgnoreGenericEvents), false);
EventLocationType = element.GetAttributeIdentifier(nameof(EventLocationType), Identifier.Empty);
UseOutpostModulesOfLocationType = element.GetAttributeIdentifier(nameof(UseOutpostModulesOfLocationType), Identifier.Empty);
IsAnyOutpost = element.GetAttributeBool(nameof(IsAnyOutpost), def: HasOutpost);
string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC");
Enum.TryParse(teamStr, out OutpostTeam);
if (element.GetAttribute("name") != null)
if (element.GetAttribute(nameof(ForceLocationName)) != null ||
element.GetAttribute("name") != null)
{
ForceLocationName = element.GetAttributeIdentifier("name", string.Empty);
ForceLocationName = element.GetAttributeIdentifier(nameof(ForceLocationName),
//backwards compatibility
def: element.GetAttributeIdentifier("name", string.Empty));
}
else
{
@@ -214,15 +406,18 @@ namespace Barotrauma
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty<string>());
foreach (string commonnessPerZoneStr in commonnessPerZoneStrs)
{
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
if (splitCommonnessPerZone.Length != 2 ||
!int.TryParse(splitCommonnessPerZone[0].Trim(), out int zoneIndex) ||
!float.TryParse(splitCommonnessPerZone[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float zoneCommonness))
{
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone1index: zone1commonness, zone2index: zone2commonness\"");
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone1index: zone1commonness, zone2index: zone2commonness\"", contentPackage: element.ContentPackage);
break;
}
CommonnessPerZone[zoneIndex] = zoneCommonness;
if (zoneCommonness <= 0.0f) { continue; }
AugmentDifficultyZoneSettings(zoneIndex, zoneCommonness, minCount: null);
}
string[] minCountPerZoneStrs = element.GetAttributeStringArray("mincountperzone", Array.Empty<string>());
@@ -233,13 +428,40 @@ namespace Barotrauma
!int.TryParse(splitMinCountPerZone[0].Trim(), out int zoneIndex) ||
!int.TryParse(splitMinCountPerZone[1].Trim(), out int minCount))
{
DebugConsole.ThrowError("Failed to read minimum count values for location type \"" + Identifier + "\" - minimum counts should be given in the format \"zone1index: zone1mincount, zone2index: zone2mincount\"");
DebugConsole.ThrowError("Failed to read minimum zone count values for location type \"" + Identifier +
"\" - minimum zone counts should be given in the format \"zone1index: zone1mincount, zone2index: zone2mincount\"", contentPackage: element.ContentPackage);
break;
}
MinCountPerZone[zoneIndex] = minCount;
if (minCount <= 0) { continue; }
AugmentDifficultyZoneSettings(zoneIndex, zoneCommonness: null, minCount);
}
var portraits = new List<Sprite>();
var hireableJobs = new List<(Identifier, float, bool)>();
void AugmentDifficultyZoneSettings(int zoneIndex, float? zoneCommonness, int? minCount)
{
var existingSettings = AreaSettings.Find(areaSettingData => areaSettingData is DifficultyZoneSettingData difficultyZoneSettingData &&
difficultyZoneSettingData.DifficultyZone == zoneIndex);
if (existingSettings != null)
{
int index = AreaSettings.IndexOf(existingSettings);
AreaSettings[index] = new DifficultyZoneSettingData(zoneIndex,
minCount ?? existingSettings.MinCount,
//note that assigning minCount to maxCount is intentional here:
//previously it was only possible to define minCount (essentially the same as just defining "count" now)
maxCount: minCount ?? existingSettings.MaxCount,
commonness: zoneCommonness ?? existingSettings.Commonness, desiredPosition: null, locationType: this);
}
else
{
AreaSettings.Add(new DifficultyZoneSettingData(zoneIndex, minCount ?? 0, maxCount: minCount ?? 0, commonness: zoneCommonness ?? 0, desiredPosition: null, locationType: this));
}
}
var portraitsList = new List<Sprite>();
var hireableJobsList = new List<(Identifier, float, bool)>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -249,7 +471,7 @@ namespace Barotrauma
float jobCommonness = subElement.GetAttributeFloat("commonness", 1.0f);
bool availableIfMissing = subElement.GetAttributeBool("AlwaysAvailableIfMissingFromCrew", false);
totalHireableWeight += jobCommonness;
hireableJobs.Add((jobIdentifier, jobCommonness, availableIfMissing));
hireableJobsList.Add((jobIdentifier, jobCommonness, availableIfMissing));
break;
case "symbol":
Sprite = new Sprite(subElement, lazyLoad: true);
@@ -265,7 +487,7 @@ namespace Barotrauma
var portrait = new Sprite(subElement, lazyLoad: true);
if (portrait != null)
{
portraits.Add(portrait);
portraitsList.Add(portrait);
}
break;
case "store":
@@ -281,10 +503,71 @@ namespace Barotrauma
DailySpecialsCount = subElement.GetAttributeInt("dailyspecialscount", DailySpecialsCount);
RequestedGoodsCount = subElement.GetAttributeInt("requestedgoodscount", RequestedGoodsCount);
break;
case "areasettings":
ParseAreaSettings(subElement);
break;
}
}
this.portraits = portraitsList.ToImmutableArray();
this.hireableJobs = hireableJobsList.ToImmutableArray();
void ParseAreaSettings(ContentXElement areaSettingsElement)
{
Identifier biomeIdentifier = areaSettingsElement.GetAttributeIdentifier("biome", Identifier.Empty);
int zone = areaSettingsElement.GetAttributeInt("zone", 0);
if (biomeIdentifier == Identifier.Empty && zone == 0)
{
DebugConsole.ThrowError("Failed to read area settings for locationType \"" + Identifier + "\" - biome identifier and zone are both missing.", contentPackage: element.ContentPackage);
return;
}
if (biomeIdentifier != Identifier.Empty && zone != 0)
{
DebugConsole.ThrowError("Failed to read area settings for locationType \"" + Identifier + "\" - both biome identifier and zone are defined. Must be one or the other.", contentPackage: element.ContentPackage);
return;
}
bool HasComma(string intAttributeName)
{
var attr = areaSettingsElement.GetAttribute(intAttributeName);
if (attr == null) { return false;}
return attr.Value.Contains(',');
}
if (HasComma("mincount") || HasComma("maxcount") || HasComma("count"))
{
DebugConsole.LogError($"AreaSettings for locationType {Identifier} has comma inside int count attribute. This causes the resulting parse to combine the numbers, resulting in incorrect amount of locations.",
contentPackage: ContentPackage);
}
int? minCount = areaSettingsElement.GetAttributeNullableInt("mincount");
int? maxCount = areaSettingsElement.GetAttributeNullableInt("maxcount");
int? count = areaSettingsElement.GetAttributeNullableInt("count");
float? desiredPosition = areaSettingsElement.GetAttributeNullableFloat("desiredposition");
float commonness = areaSettingsElement.GetAttributeFloat("commonness", 0);
// if set, count overrides min and max count to eliminate randomness
if (count.HasValue)
{
minCount = count;
maxCount = count;
}
else if (minCount.HasValue && maxCount.HasValue && minCount <= 0 && maxCount <= 0)
{
DebugConsole.AddWarning("Failed to read count value for location type \"" + Identifier + "\" - both min and max count are 0.", contentPackage: element.ContentPackage);
return;
}
if (biomeIdentifier != Identifier.Empty)
{
AreaSettings.Add(new BiomeSettingData(biomeIdentifier, minCount, maxCount, commonness, desiredPosition, locationType: this));
}
else
{
AreaSettings.Add(new DifficultyZoneSettingData(zone, minCount, maxCount, commonness, desiredPosition, locationType: this));
}
}
this.portraits = portraits.ToImmutableArray();
this.hireableJobs = hireableJobs.ToImmutableArray();
}
public IEnumerable<JobPrefab> GetHireablesMissingFromCrew()
@@ -383,7 +666,7 @@ namespace Barotrauma
return rawNames[rand.Next() % rawNames.Length];
}
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func<LocationType, bool> predicate = null)
public static LocationType Random(Random rand, int? zone = null, Identifier? biomeId = null, bool requireOutpost = false, Func<LocationType, bool> predicate = null)
{
Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes");
@@ -392,19 +675,22 @@ namespace Barotrauma
(predicate == null || predicate(lt)) && IsValid(lt))
.OrderBy(p => p.UintIdentifier).ToArray();
bool IsValid(LocationType lt)
bool IsValid(LocationType locationType)
{
if (requireOutpost && !lt.HasOutpost) { return false; }
if (zone.HasValue)
{
if (!lt.CommonnessPerZone.ContainsKey(zone.Value)) { return false; }
}
if (requireOutpost && !locationType.HasOutpost) { return false; }
bool validZone = !zone.HasValue || locationType.AreaSettings.Any(areaSetting => areaSetting.MatchesZone(zone.Value));
bool validBiome = !biomeId.HasValue || locationType.AreaSettings.Any(areaSetting => areaSetting.MatchesBiome(biomeId.Value));
if (!validZone && !validBiome) { return false; }
//if zone is not defined, this is a "random" (non-campaign) level
//-> don't choose location types that aren't allowed in those
else if (!lt.AllowInRandomLevels)
if (!zone.HasValue && !biomeId.HasValue && !locationType.AllowInRandomLevels)
{
return false;
}
return true;
}
@@ -413,11 +699,12 @@ namespace Barotrauma
DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!");
}
if (zone.HasValue)
if (zone.HasValue || biomeId.HasValue)
{
return ToolBox.SelectWeightedRandom(
allowedLocationTypes,
allowedLocationTypes.Select(a => a.CommonnessPerZone[zone.Value]).ToArray(),
allowedLocationTypes.Select(allowedType =>
allowedType.AreaSettings.Find(areaSetting => areaSetting.MatchesZone(zone.Value) || areaSetting.MatchesBiome(biomeId.Value))?.Commonness ?? 0).ToArray(),
rand);
}
else
@@ -425,6 +712,17 @@ namespace Barotrauma
return allowedLocationTypes[rand.Next() % allowedLocationTypes.Length];
}
}
public bool IsValidForZoneOrBiome(int? zone, Identifier? biomeIdentifier)
{
//if zone is not defined, this is a "random" (non-campaign) level
//-> don't choose location types that aren't allowed in those
if (!zone.HasValue && !AllowInRandomLevels) { return false; }
if (!zone.HasValue && !biomeIdentifier.HasValue) { return true; }
return AreaSettings.Any(setting => setting.Matches(zone, biomeIdentifier));
}
public OutpostGenerationParams GetForcedOutpostGenerationParams()
{
@@ -434,6 +732,11 @@ namespace Barotrauma
}
return null;
}
public bool HasCounts()
{
return AreaSettings.Any(setting => setting.HasCounts);
}
public override void Dispose() { }
}
@@ -77,6 +77,9 @@ namespace Barotrauma
public Radiation Radiation;
private bool trackedLocationDiscoveryAndVisitOrder = true;
private IOrderedEnumerable<Biome> _orderedBiomes;
public IOrderedEnumerable<Biome> OrderedBiomes => _orderedBiomes ??= Biome.Prefabs.GetOrdered();
public Map(CampaignSettings settings)
{
@@ -240,10 +243,7 @@ namespace Barotrauma
Vector2 mapPos = new Vector2(
MathHelper.Lerp(firstEndLocation.MapPosition.X, Width, MathHelper.Lerp(0.2f, 0.8f, i / (float)missingOutpostCount)),
Height * MathHelper.Lerp(0.2f, 1.0f, (float)rand.NextDouble()));
var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations)
{
Biome = endLocations.First().Biome
};
var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, firstEndLocation.Biome.Identifier, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations);
newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f);
Locations.Add(newEndLocation);
endLocations.Add(newEndLocation);
@@ -365,6 +365,14 @@ namespace Barotrauma
{
CurrentLocation.ChangeType(campaign, tutorialOutpost);
}
else
{
var forceStartOutpostType = LocationType.Prefabs.Where(lt => lt.ForceAsStartOutpost).GetRandom(Rand.RandSync.ServerAndClient);
if (forceStartOutpostType != null)
{
CurrentLocation.ChangeType(campaign, forceStartOutpostType);
}
}
Discover(CurrentLocation);
Visit(CurrentLocation);
CurrentLocation.CreateStores();
@@ -396,13 +404,16 @@ namespace Barotrauma
y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient)));
}
}
// put some of this stuff in a helper class, this function is getting unwieldy
MapLocationTypeGenerator mapLocationTypeGenerator = new MapLocationTypeGenerator(campaign, this);
Voronoi voronoi = new Voronoi(0.5f);
List<GraphEdge> edges = voronoi.MakeVoronoiGraph(voronoiSites, Width, Height);
Vector2 margin = new Vector2(
Math.Min(10, Width * 0.1f),
Math.Min(10, Height * 0.2f));
Math.Min(10, Width * 0.1f),
Math.Min(10, Height * 0.2f));
float startX = margin.X, endX = Width - margin.X;
float startY = margin.Y, endY = Height - margin.Y;
@@ -413,8 +424,6 @@ namespace Barotrauma
}
voronoiSites.Clear();
Dictionary<int, List<Location>> locationsPerZone = new Dictionary<int, List<Location>>();
bool possibleStartOutpostCreated = false;
foreach (GraphEdge edge in edges)
{
if (edge.Point1 == edge.Point2) { continue; }
@@ -442,33 +451,18 @@ namespace Barotrauma
Vector2 position = points[positionIndex];
if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; }
int zone = GetZoneIndex(position.X);
if (!locationsPerZone.ContainsKey(zone))
{
locationsPerZone[zone] = new List<Location>();
}
LocationType forceLocationType = null;
if (forceLocationType == null)
{
foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
{
if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount)
{
forceLocationType = locationType;
break;
}
}
}
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient),
requireOutpost: false, forceLocationType: forceLocationType, existingLocations: Locations);
locationsPerZone[zone].Add(newLocations[i]);
newLocations[i] = Location.CreateRandom(position, zone, GetBiome(position.X)?.Identifier, Rand.GetRNG(Rand.RandSync.ServerAndClient),
requireOutpost: false, forceLocationType: null, existingLocations: Locations);
mapLocationTypeGenerator.AddToLocationsPerZone(zone, newLocations[i]);
Locations.Add(newLocations[i]);
}
var newConnection = new LocationConnection(newLocations[0], newLocations[1]);
Connections.Add(newConnection);
Connections.Add(newConnection);
}
//remove connections that are too short
@@ -481,7 +475,6 @@ namespace Barotrauma
{
continue;
}
//locations.Remove(connection.Locations[0]);
Connections.Remove(connection);
@@ -572,7 +565,6 @@ namespace Barotrauma
{
(zone1, zone2) = (zone2, zone1);
}
if (generationParams.GateCount[zone1] == 0) { continue; }
if (!connectionsBetweenZones[zone1].Any())
@@ -589,7 +581,7 @@ namespace Barotrauma
}
}
else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1] &&
connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1])))
connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1])))
{
connectionsBetweenZones[zone1].Add(connection);
}
@@ -599,6 +591,8 @@ namespace Barotrauma
}
}
var orderedPrefabs = LocationType.Prefabs.GetOrdered();
List<Location> forciblyReassignedGateLocations = new List<Location>();
var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList();
for (int i = Connections.Count - 1; i >= 0; i--)
{
@@ -617,20 +611,37 @@ namespace Barotrauma
{
var leftMostLocation =
Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ?
Connections[i].Locations[0] :
Connections[i].Locations[1];
Connections[i].Locations[0] :
Connections[i].Locations[1];
if (!AllowAsBiomeGate(leftMostLocation.Type))
{
var potentialGateLocationTypes = orderedPrefabs.Where(AllowAsBiomeGate);
LocationType gateLocationType =
//choose some location type that's allowed in this zone/biome, and has a non zero commonness (instead of some fixed count)
potentialGateLocationTypes.Where(lt => lt.AreaSettings.Any(areaSettings => areaSettings.MatchesLocation(this, leftMostLocation) && areaSettings.Commonness > 0)).GetRandom(Rand.RandSync.ServerAndClient) ??
//if not found, use something with a fixed count
potentialGateLocationTypes.Where(lt => lt.AreaSettings.Any(areaSettings => areaSettings.MatchesLocation(this, leftMostLocation) && areaSettings.MinCount > 0)).GetRandom(Rand.RandSync.ServerAndClient) ??
//if that's not found either, try finding a type that doesn't spawn in any biome, but is allowed as a biome gate
//(a mod might have some special "biome gate" location types that are meant just for the gate locations)
potentialGateLocationTypes.Where(lt => lt.AreaSettings.None(areaSettings => areaSettings.Commonness > 0.0f || areaSettings.HasCounts)).GetRandom(Rand.RandSync.ServerAndClient);
if (gateLocationType == null)
{
DebugConsole.ThrowError($"Failed to find a suitable location type for a gate location between zones {zone1} and {zone2}.");
continue;
}
leftMostLocation.ChangeType(
campaign,
LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => AllowAsBiomeGate(lt)),
createStores: false);
gateLocationType,
createStores: false,
unlockInitialMissions: false);
forciblyReassignedGateLocations.Add(leftMostLocation);
}
static bool AllowAsBiomeGate(LocationType lt)
{
//checking for "abandoned" is not strictly necessary here because it's now configured to not be allowed as a biome gate
//but might be better to keep it for backwards compatibility (previously we relied only on that check)
return lt.HasOutpost && lt.Identifier != "abandoned" && lt.AllowAsBiomeGate;
return lt.HasOutpost && lt.Identifier != "abandoned" && lt.BiomeGate != LocationType.BiomeGateSetting.Deny;
}
leftMostLocation.IsGateBetweenBiomes = true;
@@ -663,8 +674,8 @@ namespace Barotrauma
if (!connection.Locked) { continue; }
var rightMostLocation =
connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ?
connection.Locations[0] :
connection.Locations[1];
connection.Locations[0] :
connection.Locations[1];
//if all of the other connected locations are to the left (= if there's no path forwards from the outpost),
//create a new connection to the closest location to the right
@@ -693,8 +704,18 @@ namespace Barotrauma
//remove orphans
Locations.RemoveAll(l => !Connections.Any(c => c.Locations.Contains(l)));
AssignBiomes(Rand.GetRNG(Rand.RandSync.ServerAndClient));
AssignBiomes(new MTRandom(ToolBox.StringToInt(Seed)));
var gateLocations = Locations.Where(l => l.IsGateBetweenBiomes);
foreach (var gateLocation in forciblyReassignedGateLocations)
{
//remove the gate locations who's types we've reassigned from the remaining types left to assign,
//(i.e. if we want just 1 of some location type, and we were forced to choose it as a gate, don't use that type again)
//must be done after assigning biomes
mapLocationTypeGenerator.RemoveOneFromTotals(gateLocation.Type, gateLocation);
}
mapLocationTypeGenerator.AssignForcedBiomeGateTypes(gateLocations);
foreach (LocationConnection connection in Connections)
{
@@ -713,8 +734,13 @@ namespace Barotrauma
if (LocationType.Prefabs.TryGet("outpost", out LocationType startLocationType))
{
startLocation.ChangeType(campaign, startLocationType, createStores: false);
mapLocationTypeGenerator.AddToFilled(startLocation);
}
mapLocationTypeGenerator.AssignLocationTypesBasedOnDesiredPosition(gateLocations);
//create proper level data and stores for all locations
//(needs to be done before AssignLocationCounts, since LevelData may be required for the location type changes)
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
@@ -733,6 +759,15 @@ namespace Barotrauma
}
}
}
//needs to be done after the LevelData has been assigned above
foreach (var gateLocation in forciblyReassignedGateLocations)
{
gateLocation.UnlockInitialMissions(Rand.RandSync.ServerAndClient);
}
List<Location> locationsToAssign = Locations.ToList();
locationsToAssign.Remove(GetPreviousToEndLocation());
mapLocationTypeGenerator.AssignLocationTypesBasedOnCount(gateLocations, locations: locationsToAssign);
foreach (LocationConnection connection in Connections)
{
@@ -740,7 +775,6 @@ namespace Barotrauma
}
CreateEndLocation(campaign);
float CalculateDifficulty(float mapPosition, Biome biome)
{
float settingsFactor = campaign.Settings.LevelDifficultyMultiplier;
@@ -779,23 +813,27 @@ namespace Barotrauma
float zoneWidth = Width / generationParams.DifficultyZones;
int zoneIndex = (int)Math.Floor(xPos / zoneWidth) + 1;
zoneIndex = Math.Clamp(zoneIndex, 1, generationParams.DifficultyZones - 1);
return Biome.Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
return OrderedBiomes.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
}
/// <summary>
/// Assign biomes for the connections between locations, ad for the locations that don't yet have a biome assigned.
/// </summary>
private void AssignBiomes(Random rand)
{
var biomes = Biome.Prefabs;
float zoneWidth = Width / generationParams.DifficultyZones;
List<Biome> allowedBiomes = new List<Biome>(10);
for (int i = 0; i < generationParams.DifficultyZones; i++)
{
int zoneIndex = i + 1;
allowedBiomes.Clear();
allowedBiomes.AddRange(biomes.Where(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i)));
float zoneX = zoneWidth * (generationParams.DifficultyZones - i);
allowedBiomes.AddRange(OrderedBiomes.Where(b => b.AllowedZones.Contains(zoneIndex)));
float zoneX = zoneWidth * (zoneIndex);
foreach (Location location in Locations)
{
if (location.Biome != null) { continue; }
if (location.MapPosition.X < zoneX)
{
location.Biome = allowedBiomes[rand.Next() % allowedBiomes.Count];
@@ -812,6 +850,9 @@ namespace Barotrauma
System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null));
}
/// <summary>
/// Returns the location prior to the final location. The type of this location is hard-coded just as that of the final location.
/// </summary>
private Location GetPreviousToEndLocation()
{
Location previousToEndLocation = null;
@@ -970,9 +1011,8 @@ namespace Barotrauma
}
}
#endregion Generation
public void MoveToNextLocation()
{
if (SelectedLocation == null && Level.Loaded?.EndLocation != null)
@@ -1167,7 +1207,6 @@ namespace Barotrauma
{
List<Location> nextLocations = CurrentLocation.Connections.Where(c => !c.Locked).Select(c => c.OtherLocation(CurrentLocation)).ToList();
List<Location> undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered);
if (undiscoveredLocations.Count > 0 && preferUndiscovered)
{
SelectLocation(undiscoveredLocations[Rand.Int(undiscoveredLocations.Count, Rand.RandSync.Unsynced)]);
@@ -1285,8 +1324,8 @@ namespace Barotrauma
{
location.PendingLocationTypeChange =
(location.PendingLocationTypeChange.Value.typeChange,
location.PendingLocationTypeChange.Value.delay - 1,
location.PendingLocationTypeChange.Value.parentMission);
location.PendingLocationTypeChange.Value.delay - 1,
location.PendingLocationTypeChange.Value.parentMission);
if (location.PendingLocationTypeChange.Value.delay <= 0)
{
return ChangeLocationType(campaign, location, location.PendingLocationTypeChange.Value.typeChange);
@@ -1317,8 +1356,8 @@ namespace Barotrauma
{
location.PendingLocationTypeChange =
(selectedTypeChange,
Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
null);
Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
parentMission: null);
}
else
{
@@ -1554,7 +1593,7 @@ namespace Barotrauma
Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
LocalizedString prevLocationName = location.DisplayName;
LocationType prevLocationType = location.Type;
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.GetOrdered().First();
location.ChangeType(campaign, newLocationType);
if (showNotifications && prevLocationType != location.Type)
@@ -1636,7 +1675,7 @@ namespace Barotrauma
if (index < 0) { return null; }
return Locations[index];
}
}
void Discover(Location location)
@@ -1774,4 +1813,4 @@ namespace Barotrauma
partial void RemoveProjSpecific();
}
}
}
@@ -0,0 +1,397 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal class MapLocationTypeGenerator
{
internal class LocationTypeCount
{
public int AmountToAssign;
public int? DifficultyZone;
public Identifier? BiomeId;
public LocationTypeCount(int amountToAssign, int difficultyZone)
{
AmountToAssign = amountToAssign;
DifficultyZone = difficultyZone;
}
public LocationTypeCount(int amountToAssign, Identifier biomeId)
{
AmountToAssign = amountToAssign;
BiomeId = biomeId;
}
public string ToDebugString()
{
if (DifficultyZone.HasValue)
{
return $"x{AmountToAssign} in (zone {DifficultyZone.Value})";
}
else if (BiomeId.HasValue)
{
return $"x{AmountToAssign} in (biome {BiomeId.Value})";
}
return $"x{AmountToAssign}";
}
}
/// <summary>
/// Actual amounts of location types that need to be assigned to locations, after resolving random variation from min/max counts.
/// </summary>
private Dictionary<LocationType, List<LocationTypeCount>> locationTypeAmountsToAssign;
private readonly Map map;
private readonly CampaignMode campaign;
/// <summary>
/// List of locations that have been filled from specific requirements and should not be overwritten by some subsequent generation pass (e.g. the start outpost).
/// </summary>
private readonly List<Location> filledLocations;
private readonly Dictionary<int, List<Location>> locationsPerZone = new Dictionary<int, List<Location>>();
private readonly IOrderedEnumerable<LocationType> orderedLocationTypes = LocationType.Prefabs.GetOrdered();
private bool IsEveryLocationTypeAssigned
{
get
{
return locationTypeAmountsToAssign.SelectMany(kvp => kvp.Value)
.None(locationTypeCount => locationTypeCount.AmountToAssign > 0);
}
}
public MapLocationTypeGenerator(CampaignMode campaign, Map map)
{
filledLocations = new List<Location>();
this.map = map;
this.campaign = campaign;
locationTypeAmountsToAssign = new Dictionary<LocationType, List<LocationTypeCount>>();
GenerateTotalAmountsToAssign();
}
private void GenerateTotalAmountsToAssign()
{
foreach (var locationTypePrefab in orderedLocationTypes)
{
foreach (var areaSetting in locationTypePrefab.AreaSettings)
{
if (!areaSetting.HasCounts) { continue; }
if (!areaSetting.HasValidData)
{
// the only case for invalid data right now is the biome id
DebugConsole.AddWarning($"Biome ID is invalid for AreaSetting in locationType '{locationTypePrefab.Identifier}'. Skipping invalid setting.", locationTypePrefab.ContentPackage);
continue;
}
int amountToAdd = Rand.GetRNG(Rand.RandSync.ServerAndClient).Next(areaSetting.MinCount ?? 0, areaSetting.MaxCount ?? 0 + 1);
if (amountToAdd <= 0) { continue; }
// data is either for a biome or a zone, but not both
var existingCount = GetExistingCount(locationTypePrefab, areaSetting);
if (existingCount != null)
{
existingCount.AmountToAssign += amountToAdd;
}
else
{
if (!locationTypeAmountsToAssign.ContainsKey(locationTypePrefab))
{
locationTypeAmountsToAssign[locationTypePrefab] = new List<LocationTypeCount>();
}
locationTypeAmountsToAssign[locationTypePrefab].Add(CreateNewCount(areaSetting, amountToAdd));
}
}
}
LocationTypeCount CreateNewCount(LocationType.AreaSettingData areaSettingData, int amountToAdd)
{
if (areaSettingData is LocationType.BiomeSettingData biomeSettingData)
{
return new LocationTypeCount(amountToAdd, biomeSettingData.BiomeIdentifier);
}
else if (areaSettingData is LocationType.DifficultyZoneSettingData difficultyZoneSettingData)
{
return new LocationTypeCount(amountToAdd, difficultyZoneSettingData.DifficultyZone);
}
else
{
throw new ArgumentException("Unrecognized areaSettingData");
}
}
LocationTypeCount? GetExistingCount(LocationType locationTypePrefab, LocationType.AreaSettingData areaSettingData)
{
if (!locationTypeAmountsToAssign.TryGetValue(locationTypePrefab, out List<LocationTypeCount>? value)) { return null; }
return value.FirstOrDefault(areaSettingData.MatchesRemainingCount);
}
}
public void AddToLocationsPerZone(int zone, Location location)
{
if (!locationsPerZone.ContainsKey(zone))
{
locationsPerZone[zone] = new List<Location>();
}
locationsPerZone[zone].Add(location);
}
public void AddToFilled(Location location)
{
if (filledLocations.Contains(location)) { return; }
filledLocations.Add(location);
}
public bool IsFilled(Location location)
{
return filledLocations.Contains(location);
}
public static void ChangeLocationTypeAndName(CampaignMode campaign, Location location, LocationType suitableLocationType)
{
location.ChangeType(campaign, suitableLocationType, createStores: false, unlockInitialMissions: false);
if (!suitableLocationType.ForceLocationName.IsEmpty)
{
location.ForceName(suitableLocationType.ForceLocationName);
}
}
public void AssignForcedBiomeGateTypes(IEnumerable<Location> gateLocations)
{
foreach (Location gateLocation in gateLocations)
{
foreach (LocationType locationType in orderedLocationTypes)
{
if (locationType.BiomeGate != LocationType.BiomeGateSetting.Force) { continue; }
int zone = map.GetZoneIndex(gateLocation.MapPosition.X);
// if there are no counts left for this location type, skip it
if (locationType.HasCounts() && !TypeHasRemainingCountForLocation(locationType, gateLocation)) { continue; }
// wrong faction, can't place here
if (!locationType.Faction.IsEmpty && locationType.Faction != gateLocation.Faction?.Prefab.Identifier)
{
continue;
}
// if the location already happens to be of the type we want to assign, skip and remove from totals
if (gateLocation.Type == locationType)
{
AddToFilled(gateLocation);
RemoveOneFromTotals(locationType, gateLocation);
break;
}
if (!IsFilled(gateLocation) &&
locationType.IsValidForZoneOrBiome(zone, gateLocation.Biome.Identifier))
{
AddToFilled(gateLocation);
ChangeLocationTypeAndName(campaign, gateLocation, locationType);
RemoveOneFromTotals(locationType, gateLocation);
break;
}
}
}
}
private bool TypeHasRemainingCountForLocation(LocationType countLocationType, Location location)
{
if (!locationTypeAmountsToAssign.TryGetValue(countLocationType, out List<LocationTypeCount>? locationTypeCounts))
{
return false;
}
bool hasZoneCount = locationTypeCounts.Any(ltc => ltc.DifficultyZone == map.GetZoneIndex(location.MapPosition.X) && ltc.AmountToAssign > 0);
bool hasBiomeCount = locationTypeCounts.Any(ltc => ltc.BiomeId == location.Biome.Identifier && ltc.AmountToAssign > 0);
return hasZoneCount || hasBiomeCount;
}
private int GetRemainingCount(LocationType locationType, LocationType.AreaSettingData areaSetting)
{
locationTypeAmountsToAssign.TryGetValue(locationType, out List<LocationTypeCount>? locationTypeCounts);
if (locationTypeCounts == null || locationTypeCounts.None()) { return 0; }
var match = locationTypeCounts.FirstOrDefault(ltc => areaSetting.MatchesRemainingCount(ltc));
return match?.AmountToAssign ?? 0;
}
public void RemoveOneFromTotals(LocationType locationType, Location location)
{
if (!locationTypeAmountsToAssign.TryGetValue(locationType, out List<LocationTypeCount>? locationTypeCounts))
{
return;
}
var zoneMatch = locationTypeCounts.FirstOrDefault(ltc => ltc.AmountToAssign > 0 && ltc.DifficultyZone == map.GetZoneIndex(location.MapPosition.X));
if (zoneMatch != null)
{
zoneMatch.AmountToAssign--;
}
var biomeMatch = locationTypeCounts.FirstOrDefault(ltc => ltc.AmountToAssign > 0 && ltc.BiomeId == location.Biome.Identifier);
if (biomeMatch != null)
{
biomeMatch.AmountToAssign--;
}
}
/// <summary>
/// Assign the location types that should be placed in some specific part of a biome/zone <see cref="LocationType.AreaSettingData.DesiredPosition"/>.
/// </summary>
public void AssignLocationTypesBasedOnDesiredPosition(IEnumerable<Location> gateLocations)
{
foreach (LocationType locationType in LocationType.Prefabs)
{
foreach (var areaSetting in locationType.AreaSettings)
{
if (!areaSetting.DesiredPosition.HasValue) { continue; }
int remainingCount = GetRemainingCount(locationType, areaSetting);
if (remainingCount == 0) { continue; }
var locations = map.Locations.Where(location => areaSetting.MatchesLocation(map, location)).Where(location => !gateLocations.Contains(location));
if (locations.None()) { continue; }
FillLocations(locations, areaSetting.DesiredPosition.Value, remainingCount, locationType);
}
}
void FillLocations(IEnumerable<Location> locations, float desiredPosition, int locationCount, LocationType locationType)
{
float areaStart = locations.Min(l => l.MapPosition.X);
float areaEnd = locations.Max(l => l.MapPosition.X);
float desiredMapPosition = MathHelper.Lerp(areaStart, areaEnd, desiredPosition);
List<Location> sortedLocations = locations.Where(location => !IsFilled(location)).ToList();
sortedLocations.Sort((firstLocation, secondLocation) => Math.Abs(firstLocation.MapPosition.X - desiredMapPosition).CompareTo(secondLocation.MapPosition.X - desiredMapPosition));
for (int i = 0; i < locationCount; i++)
{
if (sortedLocations.None()) { break; }
var closestLocation = sortedLocations.First();
ChangeLocationTypeAndName(campaign, closestLocation, locationType);
AddToFilled(closestLocation);
RemoveOneFromTotals(closestLocation.Type, closestLocation);
sortedLocations.Remove(closestLocation);
}
}
}
public void AssignLocationTypesBasedOnCount(IEnumerable<Location> gateLocations, IEnumerable<Location> locations)
{
// generate lists of all the instances of location types that we are supposed to try and fit into the available locations
List<Location> shuffledLocations = locations.ToList();
shuffledLocations.Shuffle(Rand.GetRNG(Rand.RandSync.ServerAndClient));
foreach (Location location in shuffledLocations)
{
if (IsFilled(location)) { continue; }
bool isBiomeGate = gateLocations.Contains(location);
if (isBiomeGate && location.Type.BiomeGate == LocationType.BiomeGateSetting.Force)
{
//forced as a biome gate, let's not touch this location
continue;
}
if (IsEveryLocationTypeAssigned) { break; }
var suitableLocationType = TryPickSuitableLocationTypeFromTotals(location, isBiomeGate);
// if we found something suitable, change the location type and name, and add to filled locations
// (otherwise we will honor the initial random location type)
if (suitableLocationType != null)
{
ChangeLocationTypeAndName(campaign, location, suitableLocationType);
AddToFilled(location);
}
}
// warn if we couldn't fill in all the desired counts
if (!IsEveryLocationTypeAssigned)
{
ContentPackage? nonVanillaContentPackage = null;
StringBuilder sb = new StringBuilder("Following location types could not be assigned to locations:\n");
foreach ((LocationType locationType, List<LocationTypeCount> locationTypeCounts) in locationTypeAmountsToAssign)
{
foreach (var locationTypeCount in locationTypeCounts)
{
if (locationTypeCount.AmountToAssign > 0)
{
if (locationType.ContentPackage != ContentPackageManager.VanillaCorePackage)
{
nonVanillaContentPackage = locationType.ContentPackage;
}
sb.AppendLine($"- {locationType.Identifier} - {locationType.Name} ({locationTypeCount.ToDebugString()})");
}
}
}
DebugConsole.AddWarning(sb.ToString(),
//blame the mod where one of the problematic location types is defined in
contentPackage: nonVanillaContentPackage);
}
}
private LocationType? TryPickSuitableLocationTypeFromTotals(Location location, bool isBiomeGate)
{
int locationZone = map.GetZoneIndex(location.MapPosition.X);
Identifier locationBiomeId = location.Biome.Identifier;
LocationType? suitableLocationType = null;
//find location type counts that haven't been fully assigned yet
List<(LocationType LocationType, LocationTypeCount Count)> potentialLocationTypeCounts = [];
foreach ((LocationType locationType, List<LocationTypeCount> countList) in locationTypeAmountsToAssign)
{
//if we're picking a potential new type for a biome gate, it must be a location type that's allowed as a biome gate
if (isBiomeGate && locationType.BiomeGate == LocationType.BiomeGateSetting.Deny) { continue; }
foreach (var locationTypeCount in countList)
{
if (locationTypeCount.AmountToAssign > 0)
{
potentialLocationTypeCounts.Add((locationType, locationTypeCount));
}
}
}
var zoneMatches = potentialLocationTypeCounts.Where(locationTypeCount => locationTypeCount.Count.DifficultyZone == locationZone);
var biomeMatches = potentialLocationTypeCounts.Where(locationTypeCount => locationTypeCount.Count.BiomeId == locationBiomeId);
if (zoneMatches.None() && biomeMatches.None())
{
return null;
}
// if both lists have something, we will try to find a location type that is in both lists
if (zoneMatches.Any() && biomeMatches.Any())
{
var dualMatch = zoneMatches.FirstOrDefault(zoneCount => biomeMatches.Any(biomeCount => biomeCount.LocationType == zoneCount.LocationType));
if (dualMatch.LocationType != null)
{
suitableLocationType = dualMatch.LocationType;
}
}
// no dual match, find individual match
if (suitableLocationType == null)
{
suitableLocationType = zoneMatches.Any() ?
zoneMatches.First().LocationType : biomeMatches.First().LocationType;
}
if (suitableLocationType != null)
{
RemoveOneFromTotals(suitableLocationType, location);
}
return suitableLocationType;
}
}
}