Unstable 1.2.4.0

This commit is contained in:
Markus Isberg
2023-11-30 13:53:00 +02:00
parent 8a2e2ea0ae
commit fb5ea537bf
210 changed files with 4201 additions and 1283 deletions
@@ -442,6 +442,11 @@ namespace Barotrauma
public List<DummyFireSource> FakeFireSources { get; private set; }
/// <summary>
/// Can be used by conditionals
/// </summary>
public int FireCount => FireSources?.Count ?? 0;
public BallastFloraBehavior BallastFlora { get; set; }
public Hull(Rectangle rectangle)
@@ -633,14 +633,14 @@ namespace Barotrauma
{
endHole = new Tunnel(
TunnelType.SidePath,
new List<Point>() { startPosition, startExitPosition, new Point(0, Size.Y) },
new List<Point>() { startPosition, new Point(0, startPosition.Y) },
minWidth, parentTunnel: mainPath);
}
else
{
endHole = new Tunnel(
TunnelType.SidePath,
new List<Point>() { endPosition, endExitPosition, Size },
new List<Point>() { endPosition, new Point(Size.X, endPosition.Y) },
minWidth, parentTunnel: mainPath);
}
Tunnels.Add(endHole);
@@ -4122,7 +4122,7 @@ namespace Barotrauma
if (location != null)
{
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})");
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);
}
else
@@ -4230,7 +4230,20 @@ namespace Barotrauma
}
}
spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1);
Vector2 preferredSpawnPos = i == 0 ? StartPosition : EndPosition;
//if we're placing the outpost at the end of the level, close to the bottom-right,
//and there's a hole leading out the right side of the level, move the spawn position towards that hole.
//Makes outpost placement a little nicer in levels with lots of verticality: if there's a tall vertical
//shaft leading down to the end position, we don't want the outpost to be placed all the way up to wherever the
//ceiling is at the top of that shaft.
if (i == 1 && GenerationParams.CreateHoleNextToEnd &&
preferredSpawnPos.X > Size.X * 0.75f &&
preferredSpawnPos.Y < Size.Y * 0.25f)
{
preferredSpawnPos.X = (preferredSpawnPos.X + Size.X) / 2;
}
spawnPos = outpost.FindSpawnPos(preferredSpawnPos, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1);
if (Type == LevelData.LevelType.Outpost)
{
spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2);
@@ -4254,7 +4267,7 @@ namespace Barotrauma
if (StartLocation != null)
{
outpost.TeamID = StartLocation.Type.OutpostTeam;
outpost.Info.Name = StartLocation.Name;
outpost.Info.Name = StartLocation.DisplayName.Value;
}
}
else
@@ -4263,7 +4276,7 @@ namespace Barotrauma
if (EndLocation != null)
{
outpost.TeamID = EndLocation.Type.OutpostTeam;
outpost.Info.Name = EndLocation.Name;
outpost.Info.Name = EndLocation.DisplayName.Value;
}
}
}
@@ -241,7 +241,7 @@ namespace Barotrauma
/// </summary>
public LevelData(Location location, Map map, float difficulty)
{
Seed = location.BaseName + map.Locations.IndexOf(location);
Seed = location.NameIdentifier.Value + map.Locations.IndexOf(location);
Biome = location.Biome;
Type = LevelType.Outpost;
Difficulty = difficulty;
@@ -639,6 +639,7 @@ namespace Barotrauma
public override void Remove()
{
objectsInRange.Clear();
if (objects != null)
{
foreach (LevelObject obj in objects)
@@ -55,8 +55,17 @@ namespace Barotrauma
public readonly List<LocationConnection> Connections = new List<LocationConnection>();
private string baseName;
public LocalizedString DisplayName { get; private set; }
public Identifier NameIdentifier => nameIdentifier;
private int nameFormatIndex;
private Identifier nameIdentifier;
/// <summary>
/// For backwards compatibility: a non-localizable name from the old text files.
/// </summary>
private string rawName;
private LocationType addInitialMissionsForType;
@@ -75,10 +84,6 @@ namespace Barotrauma
public bool DisallowLocationTypeChanges;
public string BaseName { get => baseName; }
public string Name { get; private set; }
public Biome Biome { get; set; }
public Vector2 MapPosition { get; private set; }
@@ -309,7 +314,7 @@ namespace Barotrauma
if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive)
{
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false));
price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, new Identifier("all")));
price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll));
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag)));
}
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false));
@@ -484,7 +489,7 @@ namespace Barotrauma
{
if (missionIndex < 0 || missionIndex >= availableMissions.Count)
{
DebugConsole.ThrowError($"Failed to select a mission in location \"{Name}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
DebugConsole.ThrowError($"Failed to select a mission in location \"{DisplayName}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
break;
}
selectedMissions.Add(availableMissions[missionIndex]);
@@ -536,15 +541,15 @@ namespace Barotrauma
public override string ToString()
{
return $"Location ({Name ?? "null"})";
return $"Location ({DisplayName ?? "null"})";
}
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
{
Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost);
Name = RandomName(Type, rand, existingLocations);
CreateRandomName(Type, rand, existingLocations);
MapPosition = mapPosition;
PortraitId = ToolBox.StringToInt(Name);
PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
Connections = new List<LocationConnection>();
}
@@ -561,9 +566,21 @@ namespace Barotrauma
GetTypeOrFallback(originalLocationTypeId, out LocationType originalType);
OriginalType = originalType;
baseName = element.GetAttributeString("basename", "");
Name = element.GetAttributeString("name", "");
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
nameIdentifier = element.GetAttributeIdentifier(nameof(nameIdentifier), "");
if (nameIdentifier.IsEmpty)
{
//backwards compatibility
rawName = element.GetAttributeString("basename", "");
nameIdentifier = rawName.ToIdentifier();
DisplayName = element.GetAttributeString("name", "");
}
else
{
nameFormatIndex = element.GetAttributeInt(nameof(nameFormatIndex), 0);
DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
}
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
@@ -641,7 +658,7 @@ namespace Barotrauma
LevelData = new LevelData(element.GetChildElement("Level"), clampDifficultyToBiome: true);
PortraitId = ToolBox.StringToInt(Name);
PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value);
LoadStores(element);
LoadMissions(element);
@@ -687,7 +704,7 @@ namespace Barotrauma
int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0);
if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >= Type.CanChangeTo.Count)
{
DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{Name}\". Location index out of bounds ({locationTypeChangeIndex}).");
DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{DisplayName}\". Location index out of bounds ({locationTypeChangeIndex}).");
continue;
}
PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null);
@@ -698,7 +715,7 @@ namespace Barotrauma
var mission = MissionPrefab.Prefabs[missionIdentifier];
if (mission == null)
{
DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found.");
DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{DisplayName}\". Matching mission not found.");
continue;
}
PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission);
@@ -735,14 +752,27 @@ namespace Barotrauma
if (newType == null)
{
DebugConsole.ThrowError($"Failed to change the type of the location \"{Name}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
DebugConsole.ThrowError($"Failed to change the type of the location \"{DisplayName}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType);
Type = newType;
Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
if (rawName != null)
{
DebugConsole.Log($"Location {rawName} changed it's type from {Type} to {newType}");
DisplayName =
Type.NameFormats == null || !Type.NameFormats.Any() ?
rawName :
Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", rawName);
}
else
{
DebugConsole.Log($"Location {DisplayName.Value} changed it's type from {Type} to {newType}");
DisplayName =
Type.NameFormats == null || !Type.NameFormats.Any() ?
TextManager.Get(nameIdentifier) :
Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", TextManager.Get(nameIdentifier).Value);
}
if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
{
@@ -1058,12 +1088,12 @@ namespace Barotrauma
{
if (!Type.HasHireableCharacters)
{
DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace());
DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
if (HireManager == null)
{
DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace());
DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -1086,22 +1116,52 @@ namespace Barotrauma
return HireManager.AvailableCharacters;
}
private string RandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
private void CreateRandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
{
if (!type.ForceLocationName.IsNullOrEmpty())
if (!type.ForceLocationName.IsEmpty)
{
baseName = type.ForceLocationName.Value;
return baseName;
nameIdentifier = type.ForceLocationName;
DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
return;
}
nameIdentifier = type.GetRandomNameId(rand, existingLocations);
if (nameIdentifier.IsEmpty)
{
rawName = type.GetRandomRawName(rand, existingLocations);
if (rawName.IsNullOrEmpty())
{
DebugConsole.ThrowError($"Failed to generate a name for a location of the type {type.Identifier}. No names found in localization files or the .txt files.");
rawName = "none";
}
nameIdentifier = rawName.ToIdentifier();
DisplayName = rawName;
}
else
{
if (type.NameFormats == null || !type.NameFormats.Any())
{
DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
return;
}
nameFormatIndex = rand.Next() % type.NameFormats.Count;
DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
}
baseName = type.GetRandomName(rand, existingLocations);
if (type.NameFormats == null || !type.NameFormats.Any()) { return baseName; }
nameFormatIndex = rand.Next() % type.NameFormats.Count;
return type.NameFormats[nameFormatIndex].Replace("[name]", baseName);
}
public void ForceName(string name)
private static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
{
baseName = Name = name;
if (type?.NameFormats == null || !type.NameFormats.Any())
{
return TextManager.Get(nameId);
}
return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace("[name]", TextManager.Get(nameId).Value);
}
public void ForceName(Identifier nameId)
{
rawName = string.Empty;
nameIdentifier = nameId;
DisplayName = TextManager.Get(nameId).Fallback(nameId.Value);
}
public void LoadStores(XElement locationElement)
@@ -1125,7 +1185,7 @@ namespace Barotrauma
}
else
{
string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": duplicate identifier.";
string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": duplicate identifier.";
DebugConsole.ThrowError(msg);
GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:DuplicateStoreInfo", GameAnalyticsManager.ErrorSeverity.Error, msg);
continue;
@@ -1133,7 +1193,7 @@ namespace Barotrauma
}
else
{
string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier.";
string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier.";
DebugConsole.ThrowError(msg);
GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:IncorrectStoreIdentifier", GameAnalyticsManager.ErrorSeverity.Error, msg);
continue;
@@ -1444,8 +1504,9 @@ namespace Barotrauma
var locationElement = new XElement("location",
new XAttribute("type", Type.Identifier),
new XAttribute("originaltype", (Type ?? OriginalType).Identifier),
new XAttribute("basename", BaseName),
new XAttribute("name", Name),
/*not used currently (we load the nameIdentifier instead),
* but could make sense to include still for backwards compatibility reasons*/
new XAttribute("name", DisplayName),
new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty),
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
new XAttribute("pricemultiplier", PriceMultiplier),
@@ -1455,6 +1516,16 @@ namespace Barotrauma
new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation),
new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated));
if (!rawName.IsNullOrEmpty())
{
locationElement.Add(new XAttribute(nameof(rawName), rawName));
}
else
{
locationElement.Add(new XAttribute(nameof(nameIdentifier), nameIdentifier));
locationElement.Add(new XAttribute(nameof(nameFormatIndex), nameFormatIndex));
}
if (Faction != null)
{
locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier));
@@ -1491,7 +1562,7 @@ namespace Barotrauma
changeElement.Add(new XAttribute("index", index));
if (index == -1)
{
DebugConsole.AddWarning($"Invalid location type change in the location \"{Name}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
DebugConsole.AddWarning($"Invalid location type change in the location \"{DisplayName}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
}
else
{
@@ -1,4 +1,5 @@
using Barotrauma.IO;
using Barotrauma.Extensions;
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -13,7 +14,7 @@ namespace Barotrauma
{
public static readonly PrefabCollection<LocationType> Prefabs = new PrefabCollection<LocationType>();
private readonly ImmutableArray<string> names;
private readonly ImmutableArray<string> rawNames;
private readonly ImmutableArray<Sprite> portraits;
//<name, commonness>
@@ -26,7 +27,7 @@ namespace Barotrauma
public readonly LocalizedString Name;
public readonly LocalizedString Description;
public readonly LocalizedString ForceLocationName;
public readonly Identifier ForceLocationName;
public readonly float BeaconStationChance;
@@ -54,12 +55,20 @@ namespace Barotrauma
private set;
}
private readonly ImmutableArray<Identifier>? nameIdentifiers = null;
private LanguageIdentifier nameFormatLanguage;
private ImmutableArray<string>? nameFormats = null;
public IReadOnlyList<string> NameFormats
{
get
{
nameFormats ??= TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray();
if (nameFormats == null || GameSettings.CurrentConfig.Language != nameFormatLanguage)
{
nameFormats = TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray();
nameFormatLanguage = GameSettings.CurrentConfig.Language;
}
return nameFormats;
}
}
@@ -143,29 +152,37 @@ namespace Barotrauma
if (element.GetAttribute("name") != null)
{
ForceLocationName = TextManager.Get(element.GetAttributeString("name", string.Empty));
ForceLocationName = element.GetAttributeIdentifier("name", string.Empty);
}
else
{
string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" });
var names = new List<string>();
foreach (string rawPath in rawNamePaths)
//backwards compatibility for location names defined in a text file
string[] rawNamePaths = element.GetAttributeStringArray("namefile", Array.Empty<string>());
if (rawNamePaths.Any())
{
try
foreach (string rawPath in rawNamePaths)
{
var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim());
names.AddRange(File.ReadAllLines(path.Value).ToList());
try
{
var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim());
names.AddRange(File.ReadAllLines(path.Value).ToList());
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e);
}
}
catch (Exception e)
if (!names.Any())
{
DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e);
names.Add("ERROR: No names found");
}
this.rawNames = names.ToImmutableArray();
}
if (!names.Any())
else
{
names.Add("ERROR: No names found");
nameIdentifiers = element.GetAttributeIdentifierArray("nameidentifiers", new Identifier[] { Identifier }).ToImmutableArray();
}
this.names = names.ToImmutableArray();
}
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty<string>());
@@ -259,17 +276,64 @@ namespace Barotrauma
return portraits[Math.Abs(randomSeed) % portraits.Length];
}
public string GetRandomName(Random rand, IEnumerable<Location> existingLocations)
public Identifier GetRandomNameId(Random rand, IEnumerable<Location> existingLocations)
{
if (nameIdentifiers == null)
{
return Identifier.Empty;
}
List<Identifier> nameIds = new List<Identifier>();
foreach (var nameId in nameIdentifiers)
{
int index = 0;
while (true)
{
Identifier tag = $"LocationName.{nameId}.{index}".ToIdentifier();
if (TextManager.ContainsTag(tag, TextManager.DefaultLanguage))
{
nameIds.Add(tag);
index++;
}
else
{
if (index == 0)
{
DebugConsole.ThrowError($"Could not find any location names for the location type {Identifier}. Name identifier: {nameId}");
}
break;
}
}
}
if (nameIds.None())
{
return Identifier.Empty;
}
if (existingLocations != null)
{
var unusedNames = names.Where(name => !existingLocations.Any(l => l.BaseName == name)).ToList();
var unusedNameIds = nameIds.FindAll(nameId => existingLocations.None(l => l.NameIdentifier == nameId));
if (unusedNameIds.Count > 0)
{
return unusedNameIds[rand.Next() % unusedNameIds.Count];
}
}
return nameIds[rand.Next() % nameIds.Count];
}
/// <summary>
/// For backwards compatibility. Chooses a random name from the names defined in the .txt name files (<see cref="rawNamePaths"/>).
/// </summary>
public string GetRandomRawName(Random rand, IEnumerable<Location> existingLocations)
{
if (rawNames == null || rawNames.None()) { return string.Empty; }
if (existingLocations != null)
{
var unusedNames = rawNames.Where(name => !existingLocations.Any(l => l.DisplayName.Value == name)).ToList();
if (unusedNames.Count > 0)
{
return unusedNames[rand.Next() % unusedNames.Count];
}
}
return names[rand.Next() % names.Length];
return rawNames[rand.Next() % rawNames.Length];
}
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func<LocationType, bool> predicate = null)
@@ -262,9 +262,9 @@ namespace Barotrauma
foreach (var endLocation in EndLocations)
{
if (endLocation.Type?.ForceLocationName != null &&
!endLocation.Type.ForceLocationName.IsNullOrEmpty())
!endLocation.Type.ForceLocationName.IsEmpty)
{
endLocation.ForceName(endLocation.Type.ForceLocationName.Value);
endLocation.ForceName(endLocation.Type.ForceLocationName);
}
}
@@ -1005,10 +1005,10 @@ namespace Barotrauma
CurrentLocation.CreateStores();
OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation));
if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } })
if (GameMain.GameSession is { Campaign.CampaignMetadata: { } metadata })
{
metadata.SetValue("campaign.location.id".ToIdentifier(), CurrentLocationIndex);
metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.Name);
metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.NameIdentifier.Value);
metadata.SetValue("campaign.location.biome".ToIdentifier(), CurrentLocation.Biome?.Identifier ?? "null".ToIdentifier());
metadata.SetValue("campaign.location.type".ToIdentifier(), CurrentLocation.Type?.Identifier ?? "null".ToIdentifier());
}
@@ -1077,7 +1077,7 @@ namespace Barotrauma
if (SelectedConnection?.Locked ?? false)
{
string errorMsg =
$"A locked connection was selected ({SelectedConnection.Locations[0].Name} -> {SelectedConnection.Locations[1].Name}." +
$"A locked connection was selected ({SelectedConnection.Locations[0].DisplayName} -> {SelectedConnection.Locations[1].DisplayName}." +
$" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n"
+ Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
@@ -1093,7 +1093,7 @@ namespace Barotrauma
{
if (!Locations.Contains(location))
{
string errorMsg = "Failed to select a location. " + (location?.Name ?? "null") + " not found in the map.";
string errorMsg = $"Failed to select a location. {location?.DisplayName ?? "null"} not found in the map.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Map.SelectLocation:LocationNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return;
@@ -1301,11 +1301,11 @@ namespace Barotrauma
private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change)
{
string prevName = location.Name;
LocalizedString prevName = location.DisplayName;
if (!LocationType.Prefabs.TryGet(change.ChangeToType, out var newType))
{
DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found.");
DebugConsole.ThrowError($"Failed to change the type of the location \"{location.DisplayName}\". Location type \"{change.ChangeToType}\" not found.");
return false;
}
@@ -1372,7 +1372,7 @@ namespace Barotrauma
}
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change);
partial void ChangeLocationTypeProjSpecific(Location location, LocalizedString prevName, LocationTypeChange change);
partial void ClearAnimQueue();
@@ -1498,7 +1498,7 @@ namespace Barotrauma
}
Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
string prevLocationName = location.Name;
LocalizedString prevLocationName = location.DisplayName;
LocationType prevLocationType = location.Type;
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
location.ChangeType(campaign, newLocationType);
@@ -1619,7 +1619,7 @@ namespace Barotrauma
//this should not be possible, you can't enter non-outpost locations (= natural formations)
if (CurrentLocation != null && !CurrentLocation.Type.HasOutpost && SelectedConnection == null)
{
DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.Name}). Loading the first adjacent connection...");
DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.DisplayName}). Loading the first adjacent connection...");
SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation));
}
}
@@ -576,15 +576,10 @@ namespace Barotrauma
base.Remove();
MapEntityList.Remove(this);
#if CLIENT
Submarine.ForceRemoveFromVisibleEntities(this);
if (SelectedList.Contains(this))
{
SelectedList = SelectedList.Where(e => e != this).ToHashSet();
}
SelectedList.Remove(this);
#endif
if (aiTarget != null)
{
aiTarget.Remove();
@@ -686,6 +681,9 @@ namespace Barotrauma
Move(-relative * 2.0f);
}
public virtual Quad2D GetTransformedQuad()
=> Quad2D.FromSubmarineRectangle(rect);
public static List<MapEntity> LoadAll(Submarine submarine, XElement parentElement, string filePath, int idOffset)
{
IdRemap idRemap = new IdRemap(parentElement, idOffset);
@@ -733,6 +733,12 @@ namespace Barotrauma
}
}
public override Quad2D GetTransformedQuad()
=> Quad2D.FromSubmarineRectangle(rect).Rotated(
FlippedX != FlippedY
? rotationRad
: -rotationRad);
/// <summary>
/// Checks if there's a structure items can be attached to at the given position and returns it.
/// </summary>
@@ -912,6 +918,12 @@ namespace Barotrauma
return Sections[sectionIndex].damage >= MaxHealth * LeakThreshold;
}
public bool SectionIsLeakingFromOutside(int sectionIndex)
{
if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return false; }
return SectionIsLeaking(sectionIndex) && !Sections[sectionIndex].gap.IsRoomToRoom;
}
public int SectionLength(int sectionIndex)
{
if (sectionIndex < 0 || sectionIndex >= Sections.Length) return 0;
@@ -1304,8 +1316,8 @@ namespace Barotrauma
{
if (damageDiff < 0.0f)
{
attacker.Info?.IncreaseSkillLevel("mechanical".ToIdentifier(),
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f));
attacker.Info?.ApplySkillGain(Barotrauma.Tags.MechanicalSkill,
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage);
}
}
}
@@ -505,51 +505,58 @@ namespace Barotrauma
minWidth += padding;
minHeight += padding;
Vector2 limits = GetHorizontalLimits(spawnPos, minWidth, minHeight, 0);
if (verticalMoveDir != 0)
int iterations = 0;
const int maxIterations = 5;
do
{
verticalMoveDir = Math.Sign(verticalMoveDir);
//do a raycast towards the top/bottom of the level depending on direction
Vector2 potentialPos = new Vector2(spawnPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0);
//3 raycasts (left, middle and right side of the sub, so we don't accidentally raycast up a passage too narrow for the sub)
for (int x = -1; x <= 1; x++)
Vector2 potentialPos = spawnPos;
if (verticalMoveDir != 0)
{
Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x;
if (PickBody(
ConvertUnits.ToSimUnits(spawnPos + xOffset),
ConvertUnits.ToSimUnits(potentialPos + xOffset),
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null)
verticalMoveDir = Math.Sign(verticalMoveDir);
//do a raycast towards the top/bottom of the level depending on direction
Vector2 rayEnd = new Vector2(potentialPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0);
Vector2 closestPickedPos = rayEnd;
//multiple raycast across the width of the sub (so we don't accidentally raycast up a passage too narrow for the sub)
for (float x = -1; x <= 1; x += 0.2f)
{
int offsetFromWall = 10 * -verticalMoveDir;
//if the raycast hit a wall, attempt to place the spawnpos there
if (verticalMoveDir > 0)
Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x;
xOffset.X += subDockingPortOffset;
if (PickBody(
ConvertUnits.ToSimUnits(potentialPos + xOffset),
ConvertUnits.ToSimUnits(rayEnd + xOffset),
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
customPredicate: (Fixture f) =>
{
return f.UserData is not VoronoiCell { IsDestructible: true };
}) != null)
{
potentialPos.Y = Math.Min(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall);
}
else
{
potentialPos.Y = Math.Max(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall);
//if the raycast hit a wall, attempt to place the spawnpos there
int offsetFromWall = 10 * -verticalMoveDir;
float pickedPos = ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall;
closestPickedPos.Y =
verticalMoveDir > 0 ?
Math.Min(closestPickedPos.Y, pickedPos) :
Math.Max(closestPickedPos.Y, pickedPos);
}
}
potentialPos.Y = closestPickedPos.Y;
}
//step away from the top/bottom of the level, or from whatever wall the raycast hit,
//until we found a spot where there's enough room to place the sub
float dist = Math.Abs(potentialPos.Y - spawnPos.Y);
for (float d = dist; d > 0; d -= 100.0f)
Vector2 limits = GetHorizontalLimits(new Vector2(potentialPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir)),
maxHorizontalMoveAmount: minWidth, minHeight, verticalMoveDir, padding);
if (limits.Y - limits.X >= minWidth)
{
float y = spawnPos.Y + verticalMoveDir * d;
limits = GetHorizontalLimits(new Vector2(spawnPos.X, y), minWidth, minHeight, verticalMoveDir);
if (limits.Y - limits.X > minWidth)
{
spawnPos = new Vector2(spawnPos.X, y - (dockedBorders.Height * 0.5f * verticalMoveDir));
break;
}
}
}
Vector2 newSpawnPos = new Vector2(spawnPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir));
bool couldMoveInVerticalMoveDir = Math.Sign(newSpawnPos.Y - spawnPos.Y) == Math.Sign(verticalMoveDir);
if (!couldMoveInVerticalMoveDir) { break; }
spawnPos = ClampToHorizontalLimits(newSpawnPos, limits);
}
static Vector2 GetHorizontalLimits(Vector2 spawnPos, float minWidth, float minHeight, int verticalMoveDir)
iterations++;
} while (iterations < maxIterations);
Vector2 GetHorizontalLimits(Vector2 spawnPos, float maxHorizontalMoveAmount, float minHeight, int verticalMoveDir, int padding)
{
Vector2 refPos = spawnPos - Vector2.UnitY * minHeight * 0.5f * Math.Sign(verticalMoveDir);
@@ -580,34 +587,44 @@ namespace Barotrauma
if (Math.Abs(ruin.Area.Center.Y - refPos.Y) > (minHeight + ruin.Area.Height) * 0.5f) { continue; }
if (ruin.Area.Center.X < refPos.X)
{
minX = Math.Max(minX, ruin.Area.Right + 100.0f);
minX = Math.Max(minX, ruin.Area.Right + padding);
}
else
{
maxX = Math.Min(maxX, ruin.Area.X - 100.0f);
maxX = Math.Min(maxX, ruin.Area.X - padding);
}
}
return new Vector2(Math.Max(minX, spawnPos.X - minWidth), Math.Min(maxX, spawnPos.X + minWidth));
minX += subDockingPortOffset;
maxX += subDockingPortOffset;
return new Vector2(
Math.Max(Math.Max(minX, spawnPos.X - maxHorizontalMoveAmount - padding), 0),
Math.Min(Math.Min(maxX, spawnPos.X + maxHorizontalMoveAmount + padding), Level.Loaded.Size.X));
}
if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X)
Vector2 ClampToHorizontalLimits(Vector2 spawnPos, Vector2 limits)
{
//no walls found at either side, just use the initial spawnpos and hope for the best
}
else if (limits.X < 0)
{
//no wall found at the left side, spawn to the left from the right-side wall
spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset;
}
else if (limits.Y > Level.Loaded.Size.X)
{
//no wall found at right side, spawn to the right from the left-side wall
spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset;
}
else
{
//walls found at both sides, use their midpoint
spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset;
if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X)
{
//no walls found at either side, just use the initial spawnpos and hope for the best
}
else if (limits.X < 0)
{
//no wall found at the left side, spawn to the left from the right-side wall
spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset;
}
else if (limits.Y > Level.Loaded.Size.X)
{
//no wall found at right side, spawn to the right from the left-side wall
spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset;
}
else
{
//walls found at both sides, use their midpoint
spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset;
}
return spawnPos;
}
spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - padding * 2);
@@ -929,11 +946,17 @@ namespace Barotrauma
return true;
}
/// <summary>
/// check visibility between two points (in sim units)
/// Check visibility between two points (in sim units).
/// </summary>
/// <returns>a physics body that was between the points (or null)</returns>
public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true)
///
/// <param name="ignoreBranches">Should plants' branches be ignored?</param>
/// <param name="blocksVisibilityPredicate">If the predicate returns false, the fixture is ignored even if it would normally block visibility.</param>
/// <returns>A physics body that was between the points (or null)</returns>
public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true,
Predicate<Fixture> blocksVisibilityPredicate = null)
{
Body closestBody = null;
float closestFraction = 1.0f;
@@ -968,7 +991,10 @@ namespace Barotrauma
if (sectionIndex > -1 && structure.SectionBodyDisabled(sectionIndex)) { return -1; }
}
}
if (blocksVisibilityPredicate != null && !blocksVisibilityPredicate(fixture))
{
return -1;
}
if (fraction < closestFraction)
{
closestBody = fixture.Body;
@@ -1885,12 +1911,11 @@ namespace Barotrauma
Unloading = true;
try
{
#if CLIENT
RoundSound.RemoveAllRoundSounds();
GameMain.LightManager?.ClearLights();
depthSortedDamageable.Clear();
#endif
var _loaded = new List<Submarine>(loaded);
foreach (Submarine sub in _loaded)
{
@@ -1925,9 +1950,11 @@ namespace Barotrauma
Ragdoll.RemoveAll();
PhysicsBody.RemoveAll();
StatusEffect.StopAll();
GameMain.World = null;
Powered.Grids.Clear();
Powered.ChangedConnections.Clear();
GC.Collect();
@@ -1947,6 +1974,7 @@ namespace Barotrauma
outdoorNodes?.Clear();
outdoorNodes = null;
obstructedNodes.Clear();
GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged?.TryDeregister(upgradeEventIdentifier);
@@ -1958,11 +1986,17 @@ namespace Barotrauma
visibleEntities = null;
bodyDist.Clear();
bodies.Clear();
if (MainSub == this) { MainSub = null; }
if (MainSubs[1] == this) { MainSubs[1] = null; }
ConnectedDockingPorts?.Clear();
Powered.ChangedConnections.Clear();
Powered.Grids.Clear();
loaded.Remove(this);
}
@@ -169,7 +169,12 @@ namespace Barotrauma
bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None;
Rectangle rect = wall.Rect;
SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider);
var transformedQuad = wall.GetTransformedQuad();
AddPointToExtents(transformedQuad.A, hasCollider: hasCollider);
AddPointToExtents(transformedQuad.B, hasCollider: hasCollider);
AddPointToExtents(transformedQuad.C, hasCollider: hasCollider);
AddPointToExtents(transformedQuad.D, hasCollider: hasCollider);
if (hasCollider)
{
farseerBody.CreateRectangle(
@@ -188,7 +193,8 @@ namespace Barotrauma
if (hull.Submarine != submarine || hull.IdFreed) { continue; }
Rectangle rect = hull.Rect;
SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider: true);
AddPointToExtents(new Vector2(rect.X, rect.Y - rect.Height), hasCollider: true);
AddPointToExtents(new Vector2(rect.Right, rect.Y), hasCollider: true);
farseerBody.CreateRectangle(
ConvertUnits.ToSimUnits(rect.Width),
@@ -221,33 +227,42 @@ namespace Barotrauma
float simWidth = ConvertUnits.ToSimUnits(width);
float simHeight = ConvertUnits.ToSimUnits(height);
if (radius > 0f || (width > 0f && height > 0f))
{
var transformedQuad = item.GetTransformedQuad();
AddPointToExtents(transformedQuad.A, hasCollider: true);
AddPointToExtents(transformedQuad.B, hasCollider: true);
AddPointToExtents(transformedQuad.C, hasCollider: true);
AddPointToExtents(transformedQuad.D, hasCollider: true);
}
if (width > 0.0f && height > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos, collisionCategory, collidesWith));
SetExtents(item.Position - new Vector2(width, height) / 2, item.Position + new Vector2(width, height) / 2, hasCollider: true);
AddPointToExtents(item.Position - new Vector2(width, height) / 2, hasCollider: true);
AddPointToExtents(item.Position + new Vector2(width, height) / 2, hasCollider: true);
}
else if (radius > 0.0f && width > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos, collisionCategory, collidesWith));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith));
SetExtents(item.Position - new Vector2(width / 2 + radius, height / 2), item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true);
AddPointToExtents(item.Position - new Vector2(width / 2 + radius, height / 2), hasCollider: true);
AddPointToExtents(item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true);
}
else if (radius > 0.0f && height > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos, collisionCategory, collidesWith));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith));
SetExtents(item.Position - new Vector2(width / 2, height / 2 + radius), item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true);
AddPointToExtents(item.Position - new Vector2(width / 2, height / 2 + radius), hasCollider: true);
AddPointToExtents(item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true);
}
else if (radius > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos, collisionCategory, collidesWith));
visibleMinExtents.X = Math.Min(item.Position.X - radius, visibleMinExtents.X);
visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y);
visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X);
visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y);
SetExtents(item.Position - new Vector2(radius, radius), item.Position + new Vector2(radius, radius), hasCollider: true);
AddPointToExtents(item.Position - new Vector2(radius, radius), hasCollider: true);
AddPointToExtents(item.Position + new Vector2(radius, radius), hasCollider: true);
}
item.StaticFixtures.ForEach(f => f.UserData = item);
}
@@ -268,18 +283,18 @@ namespace Barotrauma
Body = new PhysicsBody(farseerBody);
void SetExtents(Vector2 min, Vector2 max, bool hasCollider)
void AddPointToExtents(Vector2 point, bool hasCollider)
{
visibleMinExtents.X = Math.Min(min.X, visibleMinExtents.X);
visibleMinExtents.Y = Math.Min(min.Y, visibleMinExtents.Y);
visibleMaxExtents.X = Math.Max(max.X, visibleMaxExtents.X);
visibleMaxExtents.Y = Math.Max(max.Y, visibleMaxExtents.Y);
visibleMinExtents.X = Math.Min(point.X, visibleMinExtents.X);
visibleMinExtents.Y = Math.Min(point.Y, visibleMinExtents.Y);
visibleMaxExtents.X = Math.Max(point.X, visibleMaxExtents.X);
visibleMaxExtents.Y = Math.Max(point.Y, visibleMaxExtents.Y);
if (hasCollider)
{
minExtents.X = Math.Min(min.X, minExtents.X);
minExtents.Y = Math.Min(min.Y, minExtents.Y);
maxExtents.X = Math.Max(max.X, maxExtents.X);
maxExtents.Y = Math.Max(max.Y, maxExtents.Y);
minExtents.X = Math.Min(point.X, minExtents.X);
minExtents.Y = Math.Min(point.Y, minExtents.Y);
maxExtents.X = Math.Max(point.X, maxExtents.X);
maxExtents.Y = Math.Max(point.Y, maxExtents.Y);
}
}
}