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
@@ -34,6 +34,7 @@ namespace Barotrauma
this.Linkable = linkable;
this.AllowedLinks = (allowedLinks ?? Enumerable.Empty<Identifier>()).ToImmutableHashSet();
this.Aliases = (aliases ?? Enumerable.Empty<string>()).Concat(identifier.Value.ToEnumerable()).ToImmutableHashSet();
this.Scale = 1;
}
public static CoreEntityPrefab HullPrefab { get; private set; }
@@ -570,11 +570,11 @@ namespace Barotrauma.MapCreatures.Behavior
if (HasBrokenThrough)
{
// I wasn't 100% sure what the performance impact on this so I decide to limit it to only check every 5 seconds
// I wasn't 100% sure what the performance impact on this so I decide to limit it to only check every 10 seconds
if (fireCheckCooldown <= 0)
{
UpdateFireSources();
fireCheckCooldown = 5f;
fireCheckCooldown = 10f;
}
else
{
@@ -8,6 +8,8 @@ namespace Barotrauma
private Vector2 maxSize;
public bool CausedByPsychosis;
protected override float SpreadToOtherHullsProbability => 0.0f;
public DummyFireSource(Vector2 maxSize, Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) :
base(worldPosition, spawningHull, sourceCharacter: null, isNetworkMessage: isNetworkMessage)
@@ -22,10 +22,11 @@ namespace Barotrauma
/// How often the FireSource checks whether it can spread to nearby hulls.
/// </summary>
const float SpreadToOtherHullsInterval = 5.0f;
/// <summary>
/// The probability of the fire spreading to a nearby hull when the <see cref="TrySpreadToNearbyHulls"/> check is made.
/// </summary>
const float SpreadToOtherHullsProbability = 0.15f;
protected virtual float SpreadToOtherHullsProbability => 0.15f;
protected Hull hull;
@@ -252,11 +253,14 @@ namespace Barotrauma
LimitSize();
spreadToOtherHullsTimer -= deltaTime;
if (spreadToOtherHullsTimer <= 0.0f)
if (SpreadToOtherHullsProbability > 0.0f)
{
TrySpreadToNearbyHulls();
spreadToOtherHullsTimer = SpreadToOtherHullsInterval;
spreadToOtherHullsTimer -= deltaTime;
if (spreadToOtherHullsTimer <= 0.0f)
{
TrySpreadToNearbyHulls();
spreadToOtherHullsTimer = SpreadToOtherHullsInterval;
}
}
if (size.X > 256.0f && this is not DummyFireSource)
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
@@ -829,13 +830,14 @@ namespace Barotrauma
foreach (var connectedGap in hull.ConnectedGaps)
{
if (connectedGap == this) { continue; }
if (connectedGap.IsRoomToRoom != IsRoomToRoom) { continue; }
//let the "more open" gap reduce this gap's flow rate
//or if they're both equally open, let the one that was created first handle it
//(note that we can't use Entity.ID here because gaps on walls don't have IDs)
if (connectedGap.open > open ||
(connectedGap.open == open && connectedGap.CreationIndex < CreationIndex))
{
Rectangle intersection = Rectangle.Intersect(rect, connectedGap.rect);
Rectangle intersection = Rectangle.Intersect(rect.ToWorldRect(), connectedGap.rect.ToWorldRect());
if (intersection.Width > 0 && intersection.Height > 0)
{
//reduce flow rate based on how much of this gap is covered by the connected one, and how open the connected one is
@@ -843,6 +845,7 @@ namespace Barotrauma
intersection.Height / (float)rect.Height :
intersection.Width / (float)rect.Width;
overlappingGapFlowRateReduction += relativeOverlap * connectedGap.open;
overlappingGaps.Add(connectedGap);
}
}
if (overlappingGapFlowRateReduction >= 1.0f)
@@ -442,6 +442,8 @@ namespace Barotrauma
/// </summary>
public bool IsBlue => ColorExtensions.IsBlueDominant(AveragePaintedColor, minimumAlpha: 100);
public const int MaxFireSources = 16;
public List<FireSource> FireSources { get; private set; }
public List<DummyFireSource> FakeFireSources { get; private set; }
@@ -731,6 +733,10 @@ namespace Barotrauma
}
else
{
if (FireSources.Count >= MaxFireSources)
{
return;
}
FireSources.Add(fireSource);
}
}
@@ -781,8 +787,9 @@ namespace Barotrauma
{
msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8);
msg.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16);
for (int i = 0; i < Math.Min(FireSources.Count, 16); i++)
System.Diagnostics.Debug.Assert(FireSources.Count <= MaxFireSources, $"Too many fire sources ({FireSources.Count}) in hull {ID} (max {MaxFireSources}).");
msg.WriteRangedInteger(Math.Min(FireSources.Count, MaxFireSources), 0, MaxFireSources);
for (int i = 0; i < Math.Min(FireSources.Count, MaxFireSources); i++)
{
var fireSource = FireSources[i];
Vector2 normalizedPos = new Vector2(
@@ -828,7 +835,7 @@ namespace Barotrauma
{
newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume;
int fireSourceCount = msg.ReadRangedInteger(0, 16);
int fireSourceCount = msg.ReadRangedInteger(0, MaxFireSources);
newFireSources = new NetworkFireSource[fireSourceCount];
for (int i = 0; i < fireSourceCount; i++)
{
@@ -2,6 +2,7 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
@@ -4481,6 +4481,10 @@ namespace Barotrauma
}
}
}
bool onlyEntrance = LevelData.Type != LevelData.LevelType.Outpost;
LocationType locationType = location?.Type;
if (missionForcedOutpostParamsId != null &&
OutpostGenerationParams.OutpostParams.TryGet(missionForcedOutpostParamsId, out var missionForcedOutpostParams))
{
@@ -4490,6 +4494,13 @@ namespace Barotrauma
{
outpostGenerationParams = LevelData.ForceOutpostGenerationParams;
}
else if (locationType != null &&
locationType.GetForcedOutpostGenerationParams() is { } forcedOutpostGenerationParams &&
//do not use the forced parameters if we want to generate only the entrance, and the parameters define a full, pre-built outpost
(!onlyEntrance || forcedOutpostGenerationParams.OutpostFilePath.IsNullOrEmpty()))
{
outpostGenerationParams = forcedOutpostGenerationParams;
}
else
{
outpostGenerationParams =
@@ -4497,7 +4508,6 @@ namespace Barotrauma
LevelData.GetSuitableOutpostGenerationParams(location, LevelData).GetRandom(Rand.RandSync.ServerAndClient);
}
LocationType locationType = location?.Type;
if (locationType == null)
{
locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
@@ -4512,7 +4522,7 @@ namespace Barotrauma
if (location != null)
{
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.DisplayName}, level type: {LevelData.Type})");
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: onlyEntrance, LevelData.AllowInvalidOutpost);
}
else
{
@@ -370,19 +370,36 @@ namespace Barotrauma
public static IEnumerable<OutpostGenerationParams> GetSuitableOutpostGenerationParams(Location location, LevelData levelData)
{
var suitableParams = OutpostGenerationParams.OutpostParams
.Where(p => p.LevelType == null || levelData.Type == p.LevelType)
var paramsForGameMode = OutpostGenerationParams.OutpostParams.Where(p =>
p.AllowedGameModeIdentifiers.None() || GameMain.GameSession?.GameMode is not GameMode gameMode || p.AllowedGameModeIdentifiers.Contains(gameMode.Preset.Identifier));
var paramsWithMatchingLevelType = paramsForGameMode
.Where(p => p.LevelType == null || levelData.Type == p.LevelType);
//1. try finding params specifically for this location type
var suitableParams = paramsWithMatchingLevelType
.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
if (!suitableParams.Any())
{
suitableParams = OutpostGenerationParams.OutpostParams
.Where(p => p.LevelType == null || levelData.Type == p.LevelType)
.Where(p => location == null || !p.AllowedLocationTypes.Any());
//2. not found, if the location type is configured to use the modules of some other location type,
// see if we could use that location type's generation params
if (!location.Type.UseOutpostModulesOfLocationType.IsEmpty)
{
suitableParams = paramsWithMatchingLevelType
.Where(p => p.AllowedLocationTypes.Contains(location.Type.UseOutpostModulesOfLocationType));
}
if (!suitableParams.Any())
{
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
suitableParams = OutpostGenerationParams.OutpostParams;
//3. still not found, choose some parameters that are suitable for any location type
suitableParams = paramsWithMatchingLevelType
.Where(p => location == null || !p.AllowedLocationTypes.Any());
if (!suitableParams.Any())
{
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
suitableParams = paramsForGameMode;
}
}
}
return suitableParams;
}
@@ -37,7 +37,7 @@ namespace Barotrauma.RuinGeneration
public void Generate(Level level, LocationType locationType, Point position, bool mirror = false)
{
Submarine = OutpostGenerator.Generate(generationParams, locationType, onlyEntrance: false);
Submarine = OutpostGenerator.Generate(generationParams, locationType, onlyEntrance: false, allowInvalidOutpost: level.LevelData.AllowInvalidOutpost);
Submarine.Info.Name = $"Ruin ({level.Seed})";
Submarine.Info.Type = SubmarineType.Ruin;
Submarine.TeamID = CharacterTeamType.None;
@@ -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;
}
}
}
@@ -47,9 +47,8 @@ namespace Barotrauma
public readonly List<MapEntity> linkedTo = new List<MapEntity>();
protected bool flippedX, flippedY;
public bool FlippedX { get { return flippedX; } }
public bool FlippedY { get { return flippedY; } }
public bool FlippedX { get; protected set; }
public bool FlippedY { get; protected set; }
public bool ShouldBeSaved = true;
@@ -91,6 +90,16 @@ namespace Barotrauma
}
}
public virtual float RotationRad { get; protected set; }
/// <summary>
/// Rotation taking into account flipping: if the entity is flipped on either axis, the rotation is negated
/// (but not if it's flipped on both axes, two flips is essentially double negation).
/// </summary>
public float RotationRadWithFlipping => FlippedX ^ FlippedY ? -RotationRad : RotationRad;
public float RotationWithFlipping => MathHelper.ToDegrees(RotationRadWithFlipping);
public virtual Rectangle Rect
{
get { return rect; }
@@ -697,9 +706,10 @@ namespace Barotrauma
/// Flip the entity horizontally
/// </summary>
/// <param name="relativeToSub">Should the entity be flipped across the y-axis of the sub it's inside</param>
public virtual void FlipX(bool relativeToSub)
/// <param name="force">Forces the item to be flipped even if it's configured not to be flippable.</param>
public virtual void FlipX(bool relativeToSub, bool force = false)
{
flippedX = !flippedX;
FlippedX = !FlippedX;
if (!relativeToSub || Submarine == null) { return; }
Vector2 relative = WorldPosition - Submarine.WorldPosition;
@@ -711,9 +721,10 @@ namespace Barotrauma
/// Flip the entity vertically
/// </summary>
/// <param name="relativeToSub">Should the entity be flipped across the x-axis of the sub it's inside</param>
public virtual void FlipY(bool relativeToSub)
/// <param name="force">Forces the item to be flipped even if it's configured not to be flippable.</param>
public virtual void FlipY(bool relativeToSub, bool force = false)
{
flippedY = !flippedY;
FlippedY = !FlippedY;
if (!relativeToSub || Submarine == null) { return; }
Vector2 relative = WorldPosition - Submarine.WorldPosition;
@@ -23,6 +23,12 @@ namespace Barotrauma
get { return allowedLocationTypes; }
}
private readonly HashSet<Identifier> allowedGameModeIdentifiers = new HashSet<Identifier>();
public IEnumerable<Identifier> AllowedGameModeIdentifiers
{
get { return allowedGameModeIdentifiers; }
}
[Serialize(-1, IsPropertySaveable.Yes, description: "Should this type of outpost be forced to the locations at the end of the campaign map? 0 = first end level, 1 = second end level, and so on."), Editable(MinValueInt = -1, MaxValueInt = 10)]
public int ForceToEndLocationIndex
@@ -279,6 +285,7 @@ namespace Barotrauma
{
Name = element.GetAttributeString("name", Identifier.Value);
allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty<Identifier>()).ToHashSet();
allowedGameModeIdentifiers = element.GetAttributeIdentifierArray("allowedgamemodes", Array.Empty<Identifier>()).ToHashSet();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (element.GetAttribute("leveltype") != null)
@@ -1,5 +1,6 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.RuinGeneration;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -110,6 +111,17 @@ namespace Barotrauma
}
}
var prebuiltOutpostInfo = ChooseOutpost(generationParams);
prebuiltOutpostInfo.Type = SubmarineType.Outpost;
sub = new Submarine(prebuiltOutpostInfo);
sub.Info.OutpostGenerationParams = generationParams;
location?.RemoveTakenItems();
EnableFactionSpecificEntities(sub, location);
return sub;
}
private static SubmarineInfo ChooseOutpost(OutpostGenerationParams generationParams)
{
var outpostFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostFile>())
.Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path))
@@ -120,6 +132,50 @@ namespace Barotrauma
{
outpostInfos.Add(new SubmarineInfo(outpostFile.Path.Value));
}
//if there's missions selected that allow outpost selection from some specific set of outposts,
//choose one of those outposts
List<SubmarineInfo> outpostInfosSuitableForMission = new List<SubmarineInfo>();
if (GameMain.GameSession?.GameMode is { } gameMode)
{
foreach (var mission in gameMode.Missions)
{
if (!mission.Prefab.AllowOutpostSelectionFromTag.IsEmpty)
{
foreach (var outpostInfo in outpostInfos)
{
if (outpostInfo.OutpostTags.Contains(mission.Prefab.AllowOutpostSelectionFromTag) &&
!outpostInfosSuitableForMission.Contains(outpostInfo))
{
outpostInfosSuitableForMission.Add(outpostInfo);
}
}
}
}
}
//if an outpost has been select in the server settings, choose that...
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings &&
serverSettings.SelectedOutpostName != "Random")
{
//...but only if the outpost is suitable for the mission (or if the mission has no specific requirements for the outpost)
if (outpostInfosSuitableForMission.None() ||
outpostInfosSuitableForMission.Any(outpostInfo => outpostInfo.OutpostTags.Contains(serverSettings.SelectedOutpostName)))
{
var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
if (matchingOutpost != null)
{
return matchingOutpost;
}
}
}
if (outpostInfosSuitableForMission.Any())
{
return outpostInfosSuitableForMission.GetRandom(Rand.RandSync.ServerAndClient);
}
if (generationParams.OutpostTag.IsEmpty)
{
outpostInfos = outpostInfos.FindAll(o => o.OutpostTags.None());
@@ -139,24 +195,7 @@ namespace Barotrauma
{
throw new Exception("Failed to generate an outpost. Could not generate an outpost from the available outpost modules and there are no pre-built outposts available.");
}
var prebuiltOutpostInfo = outpostInfos.GetRandom(Rand.RandSync.ServerAndClient);
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings &&
serverSettings.SelectedOutpostName != "Random")
{
var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
if (matchingOutpost != null)
{
prebuiltOutpostInfo = matchingOutpost;
}
}
prebuiltOutpostInfo.Type = SubmarineType.Outpost;
sub = new Submarine(prebuiltOutpostInfo);
sub.Info.OutpostGenerationParams = generationParams;
location?.RemoveTakenItems();
EnableFactionSpecificEntities(sub, location);
return sub;
return outpostInfos.GetRandom(Rand.RandSync.ServerAndClient);
}
private static Submarine GenerateFromModules(OutpostGenerationParams generationParams, OutpostModuleFile[] outpostModuleFiles, Submarine sub, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
@@ -260,13 +299,15 @@ namespace Barotrauma
if (hasForceOutpostWithInitialFlag)
{
DebugConsole.NewMessage($"Using Force outpost module as initial in Outpost generation: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}", Color.Yellow);
DebugConsole.NewMessage($"Forcing module \"{GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}\" as the initial module...", Color.Yellow);
usedForceOutpostModule = GameMain.GameSession.ForceOutpostModule;
GameMain.GameSession.ForceOutpostModule = null;
}
if (initialModule == null)
{
//reset the forced outpost module so that it won't be used
//if we attempt to generate a new outpost later after this failed attempt
GameMain.GameSession.ForceOutpostModule = null;
throw new Exception("Failed to generate an outpost (no airlock modules found).");
}
foreach (Identifier initialFlag in initialModule.OutpostModuleInfo.ModuleFlags)
@@ -297,25 +338,34 @@ namespace Barotrauma
remainingOutpostGenerationTries--;
continue;
}
DebugConsole.ThrowError($"Could not place force outpost module: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}");
GameMain.GameSession.ForceOutpostModule = null;
DebugConsole.ThrowError($"Could not force the outpost module \"{GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}\" to the outpost. Loading the module as-is...");
return null;
}
if (GameMain.GameSession != null)
{
GameMain.GameSession.ForceOutpostModule = null;
}
if (pendingModuleFlags.Any(flag => flag != "none"))
{
if (!allowInvalidOutpost)
{
remainingOutpostGenerationTries--;
if (remainingOutpostGenerationTries <= 0)
if (remainingOutpostGenerationTries > 0)
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
//tries left -> don't finish generating the outpost, try generating another layout
continue;
}
else
{
//out of tries, log an error, but let the method continue into loading the outpost (even if it doesn't have all the required modules)
DebugConsole.AddSafeError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
}
continue;
}
else
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed.");
DebugConsole.AddSafeError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed.");
}
}
@@ -354,7 +404,7 @@ namespace Barotrauma
remainingOutpostGenerationTries--;
}
DebugConsole.AddSafeError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
DebugConsole.AddSafeError("Failed to generate an outpost with a valid layout and all the required modules. Trying to use a pre-built outpost instead...");
return null;
List<MapEntity> loadEntities(Submarine sub)
@@ -463,10 +513,13 @@ namespace Barotrauma
int maxMoveAmount = Math.Max(2000, selectedModules.Max(m => Math.Max(m.Bounds.Width, m.Bounds.Height)));
bool overlapsFound = true;
PlacedModule overlappingModule1, overlappingModule2, moduleBelowAirlock;
int iteration = 0;
const int MaxIterations = 20;
while (overlapsFound)
{
overlapsFound = false;
overlappingModule1 = overlappingModule2 = moduleBelowAirlock = null;
foreach (PlacedModule placedModule in selectedModules)
{
if (placedModule.PreviousModule == null) { continue; }
@@ -478,6 +531,8 @@ namespace Barotrauma
int remainingOverlapPreventionTries = 10;
while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingOverlapPreventionTries > 0)
{
overlappingModule1 = module1;
overlappingModule2 = module2;
overlapsFound = true;
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, generationParams.MinHallwayLength, maxMoveAmount, out Dictionary<PlacedModule, Vector2> solution))
{
@@ -492,16 +547,40 @@ namespace Barotrauma
}
remainingOverlapPreventionTries--;
}
if (remainingOutpostGenerationTries > MaxOutpostGenerationRetries / 2 &&
//check that the module doesn't extend below the airlock and potentially overlap with the sub
if (generationParams is not RuinGenerationParams &&
//if we've already exhausted half of the retries, accept potential overlaps
remainingOutpostGenerationTries > MaxOutpostGenerationRetries / 2 &&
//if the module is horizontally very far, it's ok to expand below the airlock
(placedModule.Bounds.X + placedModule.Offset.X < 5000 && placedModule.Bounds.Right + placedModule.Offset.X > -5000) &&
ModuleBelowInitialModule(placedModule, selectedModules.First()))
{
moduleBelowAirlock = placedModule;
overlapsFound = true;
}
}
iteration++;
if (iteration > 10)
if (iteration > MaxIterations)
{
#if DEBUG
string warningMsg = "Failed to create an outpost layout with no overlaps.";
if (overlappingModule1 != null && overlappingModule2 != null)
{
warningMsg += $" Overlapping modules: {overlappingModule1.Info.Name}, {overlappingModule2.Info.Name}.";
}
if (moduleBelowAirlock != null)
{
warningMsg += $" Module below airlock: {moduleBelowAirlock.Info.Name}.";
}
if (remainingOutpostGenerationTries > 0)
{
warningMsg += " Retrying...";
}
DebugConsole.AddWarning(warningMsg);
#endif
generationFailed = true;
break;
}
@@ -931,7 +1010,7 @@ namespace Barotrauma
Rectangle initialModuleBounds = initialModule.Bounds;
initialModuleBounds.Location += (initialModule.Offset + initialModule.MoveOffset).ToPoint();
return bounds.Bottom < initialModuleBounds.Bottom;
return bounds.Y < initialModuleBounds.Y;
}
/// <summary>
@@ -1053,8 +1132,8 @@ namespace Barotrauma
}
modulesWithCorrectFlags = modulesWithCorrectFlags.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
var suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
var suitableModulesForAnyOutpost = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
var suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, requireLocationTypeSpecific: true);
var suitableModulesForAnyOutpost = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, requireLocationTypeSpecific: false);
if (!suitableModules.Any())
{
//no suitable module found, see if we can find a "generic" module that's not meant for any specific type of outpost
@@ -1062,12 +1141,12 @@ namespace Barotrauma
//still not found, see if we can find something that's otherwise suitable but not meant to attach to the previous module
if (!suitableModules.Any())
{
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, requireLocationTypeSpecific: true);
}
//still not found! Try if we can find a generic module that's not meant to attach to the previous module
if (!suitableModules.Any())
{
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, requireLocationTypeSpecific: false);
}
}
@@ -1109,20 +1188,12 @@ namespace Barotrauma
return suitableModule;
}
IEnumerable<SubmarineInfo> GetSuitableModules(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific)
IEnumerable<SubmarineInfo> GetSuitableModules(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool requireLocationTypeSpecific)
{
IEnumerable<SubmarineInfo> suitable = modules;
if (requireCorrectLocationType)
{
if (disallowNonLocationTypeSpecific)
{
//don't use OutpostModuleInfo.IsLocationTypeAllowed here - we're trying to choose a module specifically for this location type, not modules suitable for any location type
suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
}
else
{
suitable = modules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType));
}
suitable = modules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType, requireLocationTypeSpecific: requireLocationTypeSpecific));
}
if (requireAllowAttachToPrevious && prevModule != null)
{
@@ -138,10 +138,15 @@ namespace Barotrauma
return allowedLocationTypes.None() || allowedLocationTypes.Contains("Any".ToIdentifier());
}
public bool IsAllowedInLocationType(LocationType locationType)
/// <param name="requireLocationTypeSpecific">Does the module need to be explicitly configured as suitable for the location type, or is it ok if it's allowed for any location type?</param>
public bool IsAllowedInLocationType(LocationType locationType, bool requireLocationTypeSpecific = false)
{
if (locationType == null || IsAllowedInAnyLocationType()) { return true; }
return allowedLocationTypes.Contains(locationType.Identifier);
if (locationType == null) { return true; }
if (!requireLocationTypeSpecific && IsAllowedInAnyLocationType()) { return true; }
return
allowedLocationTypes.Contains(locationType.Identifier) ||
(!locationType.UseOutpostModulesOfLocationType.IsEmpty && allowedLocationTypes.Contains(locationType.UseOutpostModulesOfLocationType));
}
public void DetermineGapPositions(Submarine sub)
@@ -251,14 +251,13 @@ namespace Barotrauma
}
}
protected float rotationRad = 0f;
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)]
public float Rotation
{
get => MathHelper.ToDegrees(rotationRad);
get => MathHelper.ToDegrees(RotationRad);
set
{
rotationRad = MathHelper.WrapAngle(MathHelper.ToRadians(value));
RotationRad = MathHelper.WrapAngle(MathHelper.ToRadians(value));
if (StairDirection != Direction.None)
{
CreateStairBodies();
@@ -373,7 +372,7 @@ namespace Barotrauma
{
get
{
float rotation = MathHelper.ToRadians(Prefab.BodyRotation) + this.rotationRad;
float rotation = MathHelper.ToRadians(Prefab.BodyRotation) + this.RotationRad;
if (IsHorizontal)
{
if (FlippedX) { rotation = -MathHelper.Pi - rotation; }
@@ -396,9 +395,9 @@ namespace Barotrauma
get
{
Vector2 bodyOffset = Prefab.BodyOffset;
if (rotationRad != 0f)
if (RotationRad != 0f)
{
bodyOffset = MathUtils.RotatePoint(bodyOffset, -rotationRad);
bodyOffset = MathUtils.RotatePoint(bodyOffset, -RotationRad);
}
if (FlippedX) { bodyOffset.X = -bodyOffset.X; }
if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; }
@@ -627,7 +626,7 @@ namespace Barotrauma
Body newBody = GameMain.World.CreateRectangle(bodyWidth, bodyHeight, 1.5f);
var rotationWithFlip = FlippedX ^ FlippedY ? -rotationRad : rotationRad;
float rotationWithFlip = RotationRadWithFlipping;
newBody.BodyType = BodyType.Static;
Vector2 stairRectHeightDiff = new Vector2(0f, stairHeight / 2.0f - rect.Height / 2.0f);
@@ -764,8 +763,8 @@ namespace Barotrauma
public override Quad2D GetTransformedQuad()
=> Quad2D.FromSubmarineRectangle(rect).Rotated(
FlippedX != FlippedY
? rotationRad
: -rotationRad);
? RotationRad
: -RotationRad);
/// <summary>
/// Checks if there's a structure items can be attached to at the given position and returns it.
@@ -1280,7 +1279,7 @@ namespace Barotrauma
gapRect.Width += 20;
gapRect.Height += 20;
bool rotatedEnoughToChangeOrientation = (MathUtils.WrapAngleTwoPi(rotationRad - MathHelper.PiOver4) % MathHelper.Pi < MathHelper.PiOver2);
bool rotatedEnoughToChangeOrientation = (MathUtils.WrapAngleTwoPi(RotationRad - MathHelper.PiOver4) % MathHelper.Pi < MathHelper.PiOver2);
if (rotatedEnoughToChangeOrientation)
{
var center = gapRect.Location + gapRect.Size.FlipY() / new Point(2);
@@ -1345,6 +1344,9 @@ namespace Barotrauma
if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom)
{
CreateWallDamageExplosion(gap, attacker, createWallDamageProjectiles);
#if CLIENT
SteamTimelineManager.OnHullBreached(this);
#endif
}
}
@@ -1378,7 +1380,7 @@ namespace Barotrauma
{
const float explosionRange = 500.0f;
float explosionStrength = gap.Open;
var linkedHull = gap.linkedTo.FirstOrDefault() as Hull;
if (linkedHull != null)
{
@@ -1595,7 +1597,7 @@ namespace Barotrauma
partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation);
public override void FlipX(bool relativeToSub)
public override void FlipX(bool relativeToSub, bool force = false)
{
base.FlipX(relativeToSub);
@@ -1623,7 +1625,7 @@ namespace Barotrauma
}
}
public override void FlipY(bool relativeToSub)
public override void FlipY(bool relativeToSub, bool force = false)
{
base.FlipY(relativeToSub);
@@ -1141,6 +1141,17 @@ namespace Barotrauma
}
}
foreach (var dockingPort in DockingPort.List)
{
//a little hacky: undock and redock to ensure the hulls and gaps between docking ports are correct
//after all the parts of the submarine have been flipped and moved to correct places.
if (dockingPort.DockingTarget is { } dockingTarget)
{
dockingPort.Undock();
dockingPort.Dock(dockingTarget);
}
}
Item.UpdateHulls();
Gap.UpdateHulls();
#if CLIENT
@@ -2090,6 +2101,10 @@ namespace Barotrauma
foreach (Submarine sub in _loaded)
{
sub.Remove();
if (sub.Info.LazyLoad)
{
sub.Info.UnloadSubmarineElement();
}
}
loaded.Clear();
@@ -1,4 +1,4 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -194,10 +194,27 @@ namespace Barotrauma
set;
}
/// <summary>
/// When enabled, the <see cref="SubmarineElement">XML element is not loaded</see> until it is accessed.
/// </summary>
public readonly bool LazyLoad;
private XElement submarineElement;
public XElement SubmarineElement
{
get;
private set;
get
{
if (LazyLoad && submarineElement == null)
{
Reload();
}
return submarineElement;
}
private set
{
submarineElement = value;
}
}
public override string ToString()
@@ -263,7 +280,11 @@ namespace Barotrauma
RequiredContentPackages = new HashSet<string>();
}
public SubmarineInfo(string filePath, string hash = "", XElement element = null, bool tryLoad = true)
/// <summary>
/// Creates a new SubmarineInfo from a file.
/// </summary>
/// <param name="lazyLoad">When enabled, the <see cref="SubmarineElement">XML element is not loaded</see> until it is accessed.</param>
public SubmarineInfo(string filePath, string hash = "", XElement element = null, bool tryLoad = true, bool lazyLoad = false)
{
FilePath = filePath;
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
@@ -296,11 +317,17 @@ namespace Barotrauma
else
{
SubmarineElement = element;
}
}
Name = SubmarineElement.GetAttributeString("name", null) ?? Name;
Init();
if (lazyLoad)
{
LazyLoad = true;
SubmarineElement = null;
}
}
public SubmarineInfo(Submarine sub) : this(sub.Info)
@@ -512,6 +539,11 @@ namespace Barotrauma
if (savedSubmarines.Contains(this)) { savedSubmarines.Remove(this); }
}
public void UnloadSubmarineElement()
{
SubmarineElement = null;
}
public bool IsVanillaSubmarine()
{
if (FilePath == null) { return false; }
@@ -689,7 +721,7 @@ namespace Barotrauma
RemoveSavedSub(filePath);
if (File.Exists(filePath))
{
var subInfo = new SubmarineInfo(filePath);
var subInfo = new SubmarineInfo(filePath, lazyLoad: true);
if (!subInfo.IsFileCorrupted)
{
savedSubmarines.Add(subInfo);