This commit is contained in:
Evil Factory
2022-06-15 13:26:49 -03:00
410 changed files with 11140 additions and 5815 deletions
@@ -25,6 +25,7 @@ namespace Barotrauma
IEnumerable<string> aliases = null)
: base(identifier)
{
System.Diagnostics.Debug.Assert(constructor != null);
this.constructor = constructor;
this.Name = TextManager.Get($"EntityName.{identifier}");
this.Description = TextManager.Get($"EntityDescription.{identifier}");
@@ -35,40 +36,52 @@ namespace Barotrauma
this.Aliases = (aliases ?? Enumerable.Empty<string>()).Concat(identifier.Value.ToEnumerable()).ToImmutableHashSet();
}
public static CoreEntityPrefab HullPrefab { get; private set; }
public static CoreEntityPrefab GapPrefab { get; private set; }
public static CoreEntityPrefab WayPointPrefab { get; private set; }
public static CoreEntityPrefab SpawnPointPrefab { get; private set; }
public static void InitCorePrefabs()
{
CoreEntityPrefab ep = new CoreEntityPrefab(
HullPrefab = new CoreEntityPrefab(
"hull".ToIdentifier(),
typeof(Hull).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }),
typeof(Hull).GetConstructor(new Type[] { typeof(Rectangle) }),
resizeHorizontal: true,
resizeVertical: true,
linkable: true,
allowedLinks: new Identifier[] { "hull".ToIdentifier() });
Prefabs.Add(ep, false);
Prefabs.Add(HullPrefab, false);
ep = new CoreEntityPrefab(
GapPrefab = new CoreEntityPrefab(
"gap".ToIdentifier(),
typeof(Gap).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }),
typeof(Gap).GetConstructor(new Type[] { typeof(Rectangle) }),
resizeHorizontal: true,
resizeVertical: true);
Prefabs.Add(ep, false);
Prefabs.Add(GapPrefab, false);
ep = new CoreEntityPrefab(
WayPointPrefab = new CoreEntityPrefab(
"waypoint".ToIdentifier(),
typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }));
Prefabs.Add(ep, false);
Prefabs.Add(WayPointPrefab, false);
ep = new CoreEntityPrefab(
SpawnPointPrefab = new CoreEntityPrefab(
"spawnpoint".ToIdentifier(),
typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }));
Prefabs.Add(ep, false);
Prefabs.Add(SpawnPointPrefab, false);
}
protected override void CreateInstance(Rectangle rect)
{
if (constructor == null) return;
object[] lobject = new object[] { this, rect };
constructor.Invoke(lobject);
if (this == WayPointPrefab || this == SpawnPointPrefab)
{
object[] lobject = new object[] { this, rect };
constructor.Invoke(lobject);
}
else
{
object[] lobject = new object[] { rect };
constructor.Invoke(lobject);
}
}
private bool disposed = false;
@@ -214,7 +214,7 @@ namespace Barotrauma.MapCreatures.Behavior
[Serialize(400, IsPropertySaveable.Yes, "How much health the root has.")]
public int RootHealth { get; set; }
[Serialize(0.0005f, IsPropertySaveable.Yes, "How fast the root's health regenerates per each grown branch.")]
[Serialize(0.00025f, IsPropertySaveable.Yes, "How fast the root's health regenerates per each grown branch.")]
public float HealthRegenPerBranch { get; set; }
[Serialize(30, IsPropertySaveable.Yes, "How far away from the root branches can regenerate health (in number of branches). The amount of regen decreases lineary further from the root.")]
@@ -399,6 +399,7 @@ namespace Barotrauma.MapCreatures.Behavior
new XAttribute("pos", XMLExtensions.Vector2ToString(branch.Position)),
new XAttribute("ID", branch.ID),
new XAttribute("isroot", branch.IsRoot),
new XAttribute("isrootgrowth", branch.IsRootGrowth),
new XAttribute("health", branch.Health.ToString("G", CultureInfo.InvariantCulture)),
new XAttribute("maxhealth", branch.MaxHealth.ToString("G", CultureInfo.InvariantCulture)),
new XAttribute("sides", (int)branch.Sides),
@@ -457,16 +458,34 @@ namespace Barotrauma.MapCreatures.Behavior
foreach ((BallastFloraBranch branch, int parentBranchId) in branches)
{
if (parentBranchId > -1 && parentBranchId < Branches.Count)
if (parentBranchId > -1)
{
branch.ParentBranch = Branches[parentBranchId];
var parentBranch = Branches.Find(b => b.ID == parentBranchId);
if (parentBranch == null)
{
DebugConsole.AddWarning($"Error while loading ballast flora: couldn't find a parent branch with the ID {parentBranchId}");
}
else
{
branch.ParentBranch = parentBranch;
}
}
}
if (root == null)
{
Branches.ForEach(b => b.DisconnectedFromRoot = true);
}
else
{
CheckDisconnectedFromRoot();
}
void LoadBranch(XElement branchElement, IdRemap idRemap)
{
Vector2 pos = branchElement.GetAttributeVector2("pos", Vector2.Zero);
bool isRoot = branchElement.GetAttributeBool("isroot", false);
bool isRootGrowth = branchElement.GetAttributeBool("isrootgrowth", false);
int flowerConfig = getInt("flowerconfig");
int leafconfig = getInt("leafconfig");
int id = getInt("ID");
@@ -484,7 +503,8 @@ namespace Barotrauma.MapCreatures.Behavior
MaxHealth = maxhealth,
Sides = (TileSide) sides,
BlockedSides = (TileSide) blockedSides,
IsRoot = isRoot
IsRoot = isRoot,
IsRootGrowth = isRootGrowth
};
branches.Add((newBranch, parentBranchId));
@@ -649,10 +669,14 @@ namespace Barotrauma.MapCreatures.Behavior
toBeRemoved.Clear();
foreach (BallastFloraBranch branch in Branches)
{
if (branch.ParentBranch != null && (branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f))
if (!branch.IsRoot)
{
float speed = MathHelper.Lerp(5.0f, 0.1f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth);
DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot);
if (branch.ParentBranch == null || branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f)
{
float parentHealth = branch.ParentBranch == null ? 0.0f : branch.ParentBranch.Health / branch.ParentBranch.MaxHealth;
float speed = MathHelper.Lerp(5.0f, 0.1f, parentHealth);
DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot);
}
}
if (branch.Health <= 0.0f)
{
@@ -767,7 +791,8 @@ namespace Barotrauma.MapCreatures.Behavior
MaxHealth = RootHealth,
Health = RootHealth,
IsRoot = true,
CurrentHull = Parent
CurrentHull = Parent,
ID = CreateID()
};
Branches.Add(root);
@@ -992,14 +1017,6 @@ namespace Barotrauma.MapCreatures.Behavior
public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null)
{
float damage = amount;
if (damage > 0)
{
damage = Math.Min(damage, branch.Health);
}
else
{
damage = Math.Max(damage, branch.Health - branch.MaxHealth);
}
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
@@ -1058,6 +1075,14 @@ namespace Barotrauma.MapCreatures.Behavior
}
}
if (damage > 0)
{
damage = Math.Min(damage, branch.Health);
}
else
{
damage = Math.Max(damage, branch.Health - branch.MaxHealth);
}
branch.Health -= damage;
#if SERVER
@@ -1071,6 +1096,25 @@ namespace Barotrauma.MapCreatures.Behavior
}
}
private void CheckDisconnectedFromRoot()
{
bool foundDisconnected;
do
{
foundDisconnected = false;
foreach (BallastFloraBranch branch in Branches)
{
if (branch.ParentBranch == null || branch.DisconnectedFromRoot) { continue; }
if (branch.ParentBranch.Removed || branch.ParentBranch.DisconnectedFromRoot)
{
branch.DisconnectedFromRoot = true;
foundDisconnected = true;
}
}
} while (foundDisconnected);
}
public void RemoveBranch(BallastFloraBranch branch)
{
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
@@ -1081,20 +1125,7 @@ namespace Barotrauma.MapCreatures.Behavior
Branches.Remove(branch);
branch.Removed = true;
bool foundDisconnected = false;
do
{
foundDisconnected = false;
foreach (BallastFloraBranch otherBranch in Branches)
{
if (otherBranch.ParentBranch == null || otherBranch.DisconnectedFromRoot) { continue; }
if (otherBranch.ParentBranch.Removed || otherBranch.ParentBranch.DisconnectedFromRoot)
{
otherBranch.DisconnectedFromRoot = true;
foundDisconnected = true;
}
}
} while (foundDisconnected);
CheckDisconnectedFromRoot();
bodies.ForEachMod(body =>
{
@@ -1148,7 +1179,7 @@ namespace Barotrauma.MapCreatures.Behavior
return;
}
#if SERVER
if (!wasRemoved)
if (!wasRemoved && Parent != null && !Parent.Removed)
{
CreateNetworkMessage(new BranchRemoveEventData(branch));
}
@@ -1181,7 +1212,10 @@ namespace Barotrauma.MapCreatures.Behavior
}
});
#if SERVER
CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null));
if (!item.Removed && Parent != null && !Parent.Removed)
{
CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null));
}
#endif
}
@@ -1199,7 +1233,10 @@ namespace Barotrauma.MapCreatures.Behavior
StateMachine?.State?.Exit();
#if SERVER
CreateNetworkMessage(new KillEventData());
if (Parent != null && !Parent.Removed)
{
CreateNetworkMessage(new KillEventData());
}
#endif
}
@@ -1220,8 +1257,11 @@ namespace Barotrauma.MapCreatures.Behavior
}
_entityList.Remove(this);
#if SERVER
CreateNetworkMessage(new RemoveEventData());
#if SERVER
if (Parent != null && !Parent.Removed)
{
CreateNetworkMessage(new RemoveEventData());
}
#endif
}
@@ -121,7 +121,7 @@ namespace Barotrauma
}
}
public Gap(MapEntityPrefab prefab, Rectangle rectangle)
public Gap(Rectangle rectangle)
: this(rectangle, Submarine.MainSub)
{
#if CLIENT
@@ -137,7 +137,7 @@ namespace Barotrauma
{ }
public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, ushort id = Entity.NullEntityID)
: base(MapEntityPrefab.FindByIdentifier("gap".ToIdentifier()), submarine, id)
: base(CoreEntityPrefab.GapPrefab, submarine, id)
{
this.rect = rect;
flowForce = Vector2.Zero;
@@ -149,11 +149,12 @@ namespace Barotrauma
InsertToList();
float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2;
outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize);
outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize,
BodyType.Static,
Physics.CollisionWall,
Physics.CollisionCharacter,
findNewContacts: false);
outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})";
outsideCollisionBlocker.BodyType = BodyType.Static;
outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall;
outsideCollisionBlocker.CollidesWith = Physics.CollisionCharacter;
outsideCollisionBlocker.Enabled = false;
#if CLIENT
Resized += newRect => IsHorizontal = newRect.Width < newRect.Height;
@@ -166,7 +167,7 @@ namespace Barotrauma
return new Gap(rect, IsHorizontal, Submarine);
}
public override void Move(Vector2 amount)
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
if (!MathUtils.IsValid(amount))
{
@@ -327,14 +328,6 @@ namespace Barotrauma
{
lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f);
}
if (FlowTargetHull != null && IsRoomToRoom)
{
var otherRoom = linkedTo[1] == FlowTargetHull ? linkedTo[0] : linkedTo[1];
if ((otherRoom as Hull).Volume < FlowTargetHull.Volume)
{
lerpedFlowForce = Vector2.Zero;
}
}
openedTimer -= deltaTime;
@@ -410,8 +410,8 @@ namespace Barotrauma
public BallastFloraBehavior BallastFlora { get; set; }
public Hull(MapEntityPrefab prefab, Rectangle rectangle)
: this (prefab, rectangle, Submarine.MainSub)
public Hull(Rectangle rectangle)
: this (rectangle, Submarine.MainSub)
{
#if CLIENT
if (SubEditorScreen.IsSubEditor())
@@ -421,8 +421,8 @@ namespace Barotrauma
#endif
}
public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID)
: base (prefab, submarine, id)
public Hull(Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID)
: base (CoreEntityPrefab.HullPrefab, submarine, id)
{
rect = rectangle;
@@ -500,7 +500,7 @@ namespace Barotrauma
public override MapEntity Clone()
{
var clone = new Hull(MapEntityPrefab.FindByIdentifier("hull".ToIdentifier()), rect, Submarine);
var clone = new Hull(rect, Submarine);
foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
{
if (!property.Value.Attributes.OfType<Editable>().Any()) { continue; }
@@ -590,7 +590,7 @@ namespace Barotrauma
return index;
}
public override void Move(Vector2 amount)
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
if (!MathUtils.IsValid(amount))
{
@@ -1543,7 +1543,7 @@ namespace Barotrauma
int.Parse(element.GetAttribute("height").Value));
}
var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine, idRemap.GetOffsetId(element))
var hull = new Hull(rect, submarine, idRemap.GetOffsetId(element))
{
WaterVolume = element.GetAttributeFloat("pressure", 0.0f)
};
@@ -10,12 +10,11 @@ using System.Net;
namespace Barotrauma
{
#warning TODO: MapEntityPrefab should be constrained further to not include item assemblies, as assemblies are effectively not entities at all
partial class ItemAssemblyPrefab : MapEntityPrefab
{
public static readonly PrefabCollection<ItemAssemblyPrefab> Prefabs = new PrefabCollection<ItemAssemblyPrefab>();
public static readonly string VanillaSaveFolder = Path.Combine("Content", "Items", "Assemblies");
private readonly XElement configElement;
public readonly ImmutableArray<(Identifier Identifier, Rectangle Rect)> DisplayEntities;
@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -14,6 +11,8 @@ namespace Barotrauma
public readonly LocalizedString Description;
public readonly bool IsEndBiome;
public readonly float MinDifficulty;
public readonly float MaxDifficulty;
public readonly ImmutableHashSet<int> AllowedZones;
@@ -30,8 +29,9 @@ namespace Barotrauma
element.GetAttributeString("description", ""));
IsEndBiome = element.GetAttributeBool("endbiome", false);
AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet();
MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0);
MaxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100);
}
public static Identifier ParseIdentifier(ContentXElement element)
@@ -96,24 +96,31 @@ namespace Barotrauma
public readonly Sprite WallSprite;
public readonly Sprite WallEdgeSprite;
public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, bool abyss, Rand.RandSync rand)
public static CaveGenerationParams GetRandom(Level level, bool abyss, Rand.RandSync rand)
{
var caveParams = CaveParams.OrderBy(p => p.UintIdentifier).ToList();
if (caveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f))
if (caveParams.All(p => p.GetCommonness(level.LevelData, abyss) <= 0.0f))
{
return caveParams.First();
}
return ToolBox.SelectWeightedRandom(caveParams.ToList(), caveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand);
return ToolBox.SelectWeightedRandom(caveParams.ToList(), caveParams.Select(p => p.GetCommonness(level.LevelData, abyss)).ToList(), rand);
}
public float GetCommonness(LevelGenerationParams generationParams, bool abyss)
public float GetCommonness(LevelData levelData, bool abyss)
{
if (generationParams != null &&
generationParams.Identifier != Identifier.Empty &&
OverrideCommonness.TryGetValue(abyss ? "abyss".ToIdentifier() : generationParams.Identifier, out float commonness))
if (levelData.GenerationParams != null && levelData.GenerationParams.Identifier != Identifier.Empty &&
OverrideCommonness.TryGetValue(abyss ? "abyss".ToIdentifier() : levelData.GenerationParams.Identifier, out float commonness))
{
return commonness;
}
if (levelData?.Biome != null)
{
if (OverrideCommonness.TryGetValue(levelData.Biome.Identifier, out float biomeCommonness))
{
return biomeCommonness;
}
}
return Commonness;
}
@@ -157,9 +157,6 @@ namespace Barotrauma
public static List<VoronoiCell> GeneratePath(List<VoronoiCell> targetCells, List<VoronoiCell> cells)
{
Stopwatch sw2 = new Stopwatch();
sw2.Start();
List<VoronoiCell> pathCells = new List<VoronoiCell>();
if (targetCells.Count == 0) { return pathCells; }
@@ -213,10 +210,6 @@ namespace Barotrauma
} while (currentCell != targetCells[targetCells.Count - 1] && iterationsLeft > 0);
Debug.WriteLine("gettooclose: " + sw2.ElapsedMilliseconds + " ms");
sw2.Restart();
return pathCells;
}
@@ -351,7 +344,7 @@ namespace Barotrauma
BodyType = BodyType.Static,
CollisionCategories = Physics.CollisionLevel
};
GameMain.World.Add(cellBody);
GameMain.World.Add(cellBody, findNewContacts: false);
for (int n = cells.Count - 1; n >= 0; n-- )
{
@@ -429,7 +422,9 @@ namespace Barotrauma
Vertices bodyVertices = new Vertices(triangles[i]);
PolygonShape polygon = new PolygonShape(bodyVertices, 5.0f);
Fixture fixture = new Fixture(polygon)
Fixture fixture = new Fixture(polygon,
Physics.CollisionLevel,
Physics.CollisionAll)
{
UserData = cell
};
@@ -446,8 +441,6 @@ namespace Barotrauma
}
cell.Body = cellBody;
}
cellBody.CollisionCategories = Physics.CollisionLevel;
cellBody.ResetMassData();
return cellBody;
@@ -49,7 +49,7 @@ namespace Barotrauma
Cave = 0x4,
Ruin = 0x8,
Wreck = 0x10,
BeaconStation = 0x20, // Not used anywhere
BeaconStation = 0x20,
Abyss = 0x40,
AbyssCave = 0x80
}
@@ -299,11 +299,50 @@ namespace Barotrauma
/// Random integers generated during the level generation. If these values differ between clients/server,
/// it means the levels aren't identical for some reason and there will most likely be major ID mismatches.
/// </summary>
public List<int> EqualityCheckValues
public enum LevelGenStage
{
get;
private set;
} = new List<int>();
LevelGenParams,
Size,
GenStart,
TunnelGen,
AbyssGen,
CaveGen,
VoronoiGen,
VoronoiGen2,
VoronoiGen3,
Ruins,
FloatingIce,
LevelBodies,
IceSpires,
TopAndBottom,
PlaceLevelObjects,
GenerateItems,
Finish
}
private readonly Dictionary<LevelGenStage, int> equalityCheckValues = Enum.GetValues(typeof(LevelGenStage))
.Cast<LevelGenStage>()
.Select(k => (k, 0))
.ToDictionary();
public IReadOnlyDictionary<LevelGenStage, int> EqualityCheckValues => equalityCheckValues;
private void GenerateEqualityCheckValue(LevelGenStage stage)
{
equalityCheckValues[stage] = Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient);
}
private void SetEqualityCheckValue(LevelGenStage stage, int value)
{
equalityCheckValues[stage] = value;
}
private void ClearEqualityCheckValues()
{
foreach (LevelGenStage stage in Enum.GetValues(typeof(LevelGenStage)))
{
equalityCheckValues[stage] = 0;
}
}
public List<Entity> EntitiesBeforeGenerate { get; private set; } = new List<Entity>();
public int EntityCountBeforeGenerate { get; private set; }
@@ -356,6 +395,13 @@ namespace Barotrauma
/// </summary>
public static bool IsLoadedOutpost => Loaded?.Type == LevelData.LevelType.Outpost;
/// <summary>
/// Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1)
/// </summary>
public static bool IsLoadedFriendlyOutpost =>
loaded?.Type == LevelData.LevelType.Outpost &&
(loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC || loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.Team1);
public LevelGenerationParams GenerationParams
{
get { return LevelData.GenerationParams; }
@@ -382,7 +428,7 @@ namespace Barotrauma
borders = new Rectangle(Point.Zero, levelData.Size);
}
public static Level Generate(LevelData levelData, bool mirror, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
public static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
{
Debug.Assert(levelData.Biome != null);
if (levelData.Biome == null) { throw new ArgumentException("Biome was null"); }
@@ -394,27 +440,35 @@ namespace Barotrauma
preSelectedStartOutpost = startOutpost,
preSelectedEndOutpost = endOutpost
};
level.Generate(mirror);
level.Generate(mirror, startLocation, endLocation);
return level;
}
private void Generate(bool mirror)
private void Generate(bool mirror, Location startLocation, Location endLocation)
{
Loaded?.Remove();
Loaded = this;
Generating = true;
#if CLIENT
Debug.Assert(GenerationParams.Identifier != "coldcavernstutorial" || GameMain.GameSession?.GameMode == null || GameMain.GameSession.GameMode is TutorialMode);
#endif
Debug.Assert(GenerationParams.AnyBiomeAllowed || GenerationParams.AllowedBiomeIdentifiers.Contains(LevelData.Biome.Identifier));
DebugConsole.NewMessage("Level identifier: " + GenerationParams.Identifier);
EqualityCheckValues.Clear();
ClearEqualityCheckValues();
EntitiesBeforeGenerate = GetEntities().ToList();
EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count();
if (LevelData.ForceOutpostGenerationParams == null)
{
StartLocation = GameMain.GameSession?.StartLocation;
EndLocation = GameMain.GameSession?.EndLocation;
StartLocation = startLocation;
EndLocation = endLocation;
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.GenStart);
SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier));
SetEqualityCheckValue(LevelGenStage.Size, borders.Width ^ borders.Height << 16);
GenerateEqualityCheckValue(LevelGenStage.TunnelGen);
LevelObjectManager = new LevelObjectManager();
@@ -462,7 +516,7 @@ namespace Barotrauma
Rectangle pathBorders = borders;
pathBorders.Inflate(
-Math.Min(Math.Min(minMainPathWidth * 2, MaxSubmarineWidth), borders.Width / 5),
-Math.Min(minMainPathWidth, borders.Height / 5));
-Math.Min(minMainPathWidth * 2, borders.Height / 5));
if (pathBorders.Width <= 0) { throw new InvalidOperationException($"The width of the level's path area is invalid ({pathBorders.Width})"); }
if (pathBorders.Height <= 0) { throw new InvalidOperationException($"The height of the level's path area is invalid ({pathBorders.Height})"); }
@@ -477,7 +531,7 @@ namespace Barotrauma
(int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y));
endExitPosition = new Point(endPosition.X, borders.Bottom);
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.TunnelGen);
//----------------------------------------------------------------------------------
//generate the initial nodes for the main path and smaller tunnels
@@ -552,10 +606,12 @@ namespace Barotrauma
}
int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.ServerAndClient);
for (int j = 0; j < sideTunnelCount; j++)
{
if (mainPath.Nodes.Count < 4) { break; }
var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole && t != abyssTunnel);
Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.ServerAndClient)];
if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; }
@@ -570,10 +626,16 @@ namespace Barotrauma
CalculateTunnelDistanceField(null);
GenerateSeaFloorPositions();
GenerateEqualityCheckValue(LevelGenStage.AbyssGen);
GenerateAbyssArea();
GenerateEqualityCheckValue(LevelGenStage.CaveGen);
GenerateCaves(mainPath);
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen);
//----------------------------------------------------------------------------------
//generate voronoi sites
@@ -678,7 +740,7 @@ namespace Barotrauma
}
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen2);
//----------------------------------------------------------------------------------
// construct the voronoi graph and cells
@@ -796,7 +858,7 @@ namespace Barotrauma
startPosition.X = (int)pathCells[0].Site.Coord.X;
startExitPosition.X = startPosition.X;
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen3);
//----------------------------------------------------------------------------------
// remove unnecessary cells and create some holes at the bottom of the level
@@ -1025,7 +1087,7 @@ namespace Barotrauma
}
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.Ruins);
//----------------------------------------------------------------------------------
// create some ruins
@@ -1038,7 +1100,7 @@ namespace Barotrauma
GenerateRuin(ruinPositions[i], mirror);
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.FloatingIce);
//----------------------------------------------------------------------------------
// create floating ice chunks
@@ -1070,7 +1132,7 @@ namespace Barotrauma
}
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.LevelBodies);
//----------------------------------------------------------------------------------
// generate the bodies and rendered triangles of the cells
@@ -1175,7 +1237,7 @@ namespace Barotrauma
}
#endif
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.IceSpires);
//----------------------------------------------------------------------------------
// create ice spires
@@ -1210,7 +1272,7 @@ namespace Barotrauma
CreateOutposts();
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.TopAndBottom);
//----------------------------------------------------------------------------------
// top barrier & sea floor
@@ -1252,15 +1314,15 @@ namespace Barotrauma
CreateWrecks();
CreateBeaconStation();
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.PlaceLevelObjects);
LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount);
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.GenerateItems);
GenerateItems();
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.Finish);
#if CLIENT
backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount);
@@ -1658,10 +1720,11 @@ namespace Barotrauma
#endif
}
}
else
else if (abyssHeight > 30000)
{
//if the bottom of the abyss area is below crush depth, try to move it up to keep (most) of the abyss content above crush depth
if (abyssEndY + CrushDepth < 0)
//but only if start of the abyss is above crush depth (no point in doing this if all of it is below crush depth)
if (abyssEndY + CrushDepth < 0 && abyssStartY > -CrushDepth)
{
abyssEndY += Math.Min(-(abyssEndY + (int)CrushDepth), abyssHeight / 2);
}
@@ -1770,7 +1833,7 @@ namespace Barotrauma
}
}
var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: true, rand: Rand.RandSync.ServerAndClient);
var caveParams = CaveGenerationParams.GetRandom(this, abyss: true, rand: Rand.RandSync.ServerAndClient);
float caveScaleRelativeToIsland = 0.7f;
GenerateCave(
@@ -1839,7 +1902,7 @@ namespace Barotrauma
{
for (int i = 0; i < GenerationParams.CaveCount; i++)
{
var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: false, rand: Rand.RandSync.ServerAndClient);
var caveParams = CaveGenerationParams.GetRandom(this, abyss: false, rand: Rand.RandSync.ServerAndClient);
Point caveSize = new Point(
Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.ServerAndClient),
Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.ServerAndClient));
@@ -2429,6 +2492,7 @@ namespace Barotrauma
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier))
{
if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) ||
itemPrefab.LevelCommonness.TryGetValue(LevelData.Biome.Identifier, out commonness) ||
itemPrefab.LevelCommonness.TryGetValue(Identifier.Empty, out commonness))
{
if (commonness <= 0.0f) { continue; }
@@ -2606,7 +2670,7 @@ namespace Barotrauma
#if DEBUG
DebugConsole.NewMessage("Level resources spawned: " + itemCount + "\n" +
" Spawn points containing resources: " + PathPoints.Where(p => p.ClusterLocations.Any()).Count() + "/" + PathPoints.Count + "\n" +
" Total value: "+ PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)))+" mk");
" Total value: " + PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))) + " mk");
if (AbyssResources.Count > 0)
{
@@ -3187,7 +3251,8 @@ namespace Barotrauma
if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); }
float t = (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X);
Debug.Assert(t <= 1.0f);
//t can go slightly outside the 0-1 due to rounding, safe to ignore
Debug.Assert(t <= 1.001f && t >= -0.001f);
t = MathHelper.Clamp(t, 0.0f, 1.0f);
float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t);
@@ -3469,6 +3534,8 @@ namespace Barotrauma
}
else if (type == SubmarineType.BeaconStation)
{
PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.BeaconStation, submarine: sub));
sub.ShowSonarMarker = false;
sub.DockedTo.ForEach(s => s.ShowSonarMarker = false);
sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static;
@@ -3688,6 +3755,7 @@ namespace Barotrauma
if (wreckFiles.None())
{
DebugConsole.ThrowError("No wreck files found in the selected content packages!");
Wrecks = new List<Submarine>();
return;
}
wreckFiles.Shuffle(Rand.RandSync.ServerAndClient);
@@ -3881,7 +3949,7 @@ namespace Barotrauma
//the submarine port has to be at the top of the sub
if (port.Item.WorldPosition.Y < Submarine.MainSub.WorldPosition.Y) { continue; }
float dist = Math.Abs(port.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X);
if (dist < closestDistance)
if (dist < closestDistance || subPort.MainDockingPort)
{
subPort = port;
closestDistance = dist;
@@ -3964,11 +4032,35 @@ namespace Barotrauma
DebugConsole.ThrowError("No BeaconStation files found in the selected content packages!");
return;
}
var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon);
for (int i = beaconStationFiles.Count - 1; i >= 0; i--)
{
var beaconStationFile = beaconStationFiles[i];
var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value);
Debug.Assert(matchingInfo != null);
if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo)
{
if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty)
{
beaconStationFiles.RemoveAt(i);
}
}
}
if (beaconStationFiles.None())
{
DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!");
return;
}
var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient);
string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value);
BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation);
if (BeaconStation == null) { return; }
if (BeaconStation == null)
{
LevelData.HasBeaconStation = false;
return;
}
Item sonarItem = Item.ItemList.Find(it => it.Submarine == BeaconStation && it.GetComponent<Sonar>() != null);
if (sonarItem == null)
@@ -3984,6 +4076,11 @@ namespace Barotrauma
if (!LevelData.HasBeaconStation) { return; }
if (GameMain.NetworkMember?.IsClient ?? false) { return; }
if (BeaconStation == null)
{
throw new InvalidOperationException("Failed to prepare beacon station (no beacon station in the level).");
}
List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
Item reactorItem = beaconItems.Find(it => it.GetComponent<Reactor>() != null);
@@ -4019,24 +4116,22 @@ namespace Barotrauma
{
if (!(GameMain.NetworkMember?.IsClient ?? false))
{
//empty the reactor
if (reactorContainer != null)
bool allowDisconnectedWires = true;
bool allowDamagedWalls = true;
if (BeaconStation.Info?.BeaconStationInfo is BeaconStationInfo info)
{
foreach (Item item in reactorContainer.Inventory.AllItems)
{
if (item.NonInteractable) { continue; }
Spawner.AddItemToRemoveQueue(item);
}
allowDisconnectedWires = info.AllowDisconnectedWires;
allowDamagedWalls = info.AllowDamagedWalls;
}
//remove wires
float removeWireMinDifficulty = 20.0f;
float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f;
if (removeWireProbability > 0.0f)
if (removeWireProbability > 0.0f && allowDisconnectedWires)
{
foreach (Item item in beaconItems.Where(it => it.GetComponent<Wire>() != null).ToList())
{
if (item.NonInteractable) { continue; }
if (item.NonInteractable || item.InvulnerableToDamage) { continue; }
Wire wire = item.GetComponent<Wire>();
if (wire.Locked) { continue; }
if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent<ConnectionPanel>().Locked))
@@ -4056,8 +4151,8 @@ namespace Barotrauma
connection.ConnectionPanel.DisconnectedWires.Add(wire);
wire.RemoveConnection(connection.Item);
#if SERVER
connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel);
wire.CreateNetworkEvent();
connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel);
wire.CreateNetworkEvent();
#endif
}
}
@@ -4065,23 +4160,25 @@ namespace Barotrauma
}
}
//break powered items
foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable)))
if (allowDamagedWalls)
{
if (item.NonInteractable) { continue; }
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f)
//break powered items
foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable)))
{
item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced);
if (item.NonInteractable || item.InvulnerableToDamage) { continue; }
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f)
{
item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced);
}
}
}
//poke holes in the walls
foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation))
{
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f)
//poke holes in the walls
foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation))
{
int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced);
structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced));
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f)
{
int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced);
structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced));
}
}
}
}
@@ -4112,12 +4209,12 @@ namespace Barotrauma
int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount + 1);
var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null);
var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path);
pathPoints.Shuffle(Rand.RandSync.Unsynced);
var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse);
corpsePoints.Shuffle(Rand.RandSync.Unsynced);
if (!corpsePoints.Any() && !pathPoints.Any()) { continue; }
pathPoints.Shuffle(Rand.RandSync.Unsynced);
// Sort by job so that we first spawn those with a predefined job (might have special id cards)
corpsePoints = corpsePoints.OrderBy(p => p.AssignedJob == null).ThenBy(p => Rand.Value()).ToList();
var usedJobs = new HashSet<JobPrefab>();
int spawnCounter = 0;
for (int j = 0; j < corpseCount; j++)
{
@@ -4126,18 +4223,18 @@ namespace Barotrauma
CorpsePrefab selectedPrefab;
if (job == null)
{
selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck);
selectedPrefab = GetCorpsePrefab(usedJobs);
}
else
{
selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck && (p.Job == "any" || p.Job == job.Identifier));
selectedPrefab = GetCorpsePrefab(usedJobs, p => p.Job == "any" || p.Job == job.Identifier);
if (selectedPrefab == null)
{
corpsePoints.Remove(sp);
pathPoints.Remove(sp);
sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null);
// Deduce the job from the selected prefab
selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck);
selectedPrefab = GetCorpsePrefab(usedJobs);
}
}
if (selectedPrefab == null) { continue; }
@@ -4156,28 +4253,65 @@ namespace Barotrauma
pathPoints.Remove(sp);
}
job ??= selectedPrefab.GetJobPrefab();
job ??= selectedPrefab.GetJobPrefab(predicate: p => !usedJobs.Contains(p));
if (job == null) { continue; }
if (job.Identifier == "captain" || job.Identifier == "engineer" || job.Identifier == "medicaldoctor" || job.Identifier == "securityofficer")
{
// Only spawn one of these jobs per wreck
usedJobs.Add(job);
}
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, randSync: Rand.RandSync.ServerAndClient);
var corpse = Character.Create(CharacterPrefab.HumanSpeciesName, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
corpse.AnimController.FindHull(worldPos, setSubmarine: true);
corpse.TeamID = CharacterTeamType.None;
corpse.EnableDespawn = false;
selectedPrefab.GiveItems(corpse, wreck);
corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200));
bool applyBurns = Rand.Value() < 0.1f;
bool applyDamage = Rand.Value() < 0.3f;
foreach (var limb in corpse.AnimController.Limbs)
{
if (applyDamage && (limb.type == LimbType.Head || Rand.Value() < 0.5f))
{
var prefab = AfflictionPrefab.BiteWounds;
float max = prefab.MaxStrength / prefab.DamageOverlayAlpha;
corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
}
if (applyBurns)
{
var prefab = AfflictionPrefab.Burn;
float max = prefab.MaxStrength / prefab.BurnOverlayAlpha;
corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
}
static float GetStrength(Limb limb, float max)
{
float strength = Rand.Range(0, max);
if (limb.type != LimbType.Head)
{
strength = Math.Min(strength, Rand.Range(0, max));
}
return strength;
}
}
corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
corpse.GiveIdCardTags(sp);
#if SERVER
if (selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0)
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0)
{
corpse.Wallet.Give(Rand.Range(selectedPrefab.MinMoney, selectedPrefab.MaxMoney, Rand.RandSync.Unsynced));
}
#endif
spawnCounter++;
static CorpsePrefab GetCorpsePrefab(Func<CorpsePrefab, bool> predicate)
static CorpsePrefab GetCorpsePrefab(HashSet<JobPrefab> usedJobs, Func<CorpsePrefab, bool> predicate = null)
{
IEnumerable<CorpsePrefab> filteredPrefabs = CorpsePrefab.Prefabs.Where(predicate);
IEnumerable<CorpsePrefab> filteredPrefabs = CorpsePrefab.Prefabs.Where(p =>
usedJobs.None(j => j.Identifier == p.Job.ToIdentifier()) &&
p.SpawnPosition == PositionType.Wreck &&
(predicate == null || predicate(p)));
return ToolBox.SelectWeightedRandom(filteredPrefabs.ToList(), filteredPrefabs.Select(p => p.Commonness).ToList(), Rand.RandSync.Unsynced);
}
}
@@ -4270,7 +4404,7 @@ namespace Barotrauma
blockedRects?.Clear();
EntitiesBeforeGenerate?.Clear();
EqualityCheckValues?.Clear();
ClearEqualityCheckValues();
if (Ruins != null)
{
@@ -20,7 +20,7 @@ namespace Barotrauma
public readonly string Seed;
public float Difficulty;
public readonly float Difficulty;
public readonly Biome Biome;
@@ -90,10 +90,10 @@ namespace Barotrauma
(int)MathUtils.Round(generationParams.Height, Level.GridCellSize));
}
public LevelData(XElement element)
public LevelData(XElement element, float? forceDifficulty = null)
{
Seed = element.GetAttributeString("seed", "");
Difficulty = element.GetAttributeFloat("difficulty", 0.0f);
Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f);
Size = element.GetAttributePoint("size", new Point(1000));
Enum.TryParse(element.GetAttributeString("type", "LocationConnection"), out Type);
@@ -141,8 +141,8 @@ namespace Barotrauma
Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName;
Biome = locationConnection.Biome;
Type = LevelType.LocationConnection;
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Biome.Identifier);
Difficulty = locationConnection.Difficulty;
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Difficulty, Biome.Identifier);
float sizeFactor = MathUtils.InverseLerp(
MapGenerationParams.Instance.SmallLevelConnectionLength,
@@ -171,13 +171,13 @@ namespace Barotrauma
/// <summary>
/// Instantiates level data using the properties of the location
/// </summary>
public LevelData(Location location)
public LevelData(Location location, float difficulty)
{
Seed = location.BaseName;
Biome = location.Biome;
Type = LevelType.Outpost;
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Biome.Identifier);
Difficulty = 0.0f;
Difficulty = difficulty;
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Difficulty, Biome.Identifier);
var rand = new MTRandom(ToolBox.StringToInt(Seed));
int width = (int)MathHelper.Lerp(GenerationParams.MinWidth, GenerationParams.MaxWidth, (float)rand.NextDouble());
@@ -200,14 +200,16 @@ namespace Barotrauma
(requireOutpost ? LevelType.Outpost : LevelType.LocationConnection) :
generationParams.Type;
if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); }
float selectedDifficulty = difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient);
if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type, selectedDifficulty); }
var biome =
Biome.Prefabs.FirstOrDefault(b => generationParams?.AllowedBiomeIdentifiers.Contains(b.Identifier) ?? false) ??
Biome.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
var levelData = new LevelData(
seed,
difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient),
selectedDifficulty,
Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient),
generationParams,
biome);
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -68,6 +67,27 @@ namespace Barotrauma
set;
}
[Serialize(1.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float Commonness
{
get;
set;
}
[Serialize(0.0f, IsPropertySaveable.Yes, "The difficulty of the level has to be above or equal to this for these parameters to get chosen for the level."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float MinLevelDifficulty
{
get;
set;
}
[Serialize(100.0f, IsPropertySaveable.Yes, "The difficulty of the level has to be below or equal to this for these parameters to get chosen for the level."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float MaxLevelDifficulty
{
get;
set;
}
[Serialize("27,30,36", IsPropertySaveable.Yes), Editable]
public Color AmbientLightColor
{
@@ -394,7 +414,7 @@ namespace Barotrauma
set;
}
[Serialize(50, IsPropertySaveable.Yes, description: "Maximum number of resource clusters in the abyss (the actual number is picked between min and max according to the level difficulty)"), Editable(MinValueInt = 0, MaxValueInt = 1000)]
[Serialize(40, IsPropertySaveable.Yes, description: "Maximum number of resource clusters in the abyss (the actual number is picked between min and max according to the level difficulty)"), Editable(MinValueInt = 0, MaxValueInt = 1000)]
public int AbyssResourceClustersMax
{
get;
@@ -536,7 +556,26 @@ namespace Barotrauma
public Sprite WallSpriteDestroyed { get; private set; }
public Sprite WaterParticles { get; private set; }
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, Identifier biome = default)
#warning TODO: this should be in the unit test project (#3164)
public static void CheckValidity()
{
foreach (Biome biome in Biome.Prefabs)
{
for (float i = 0.0f; i <= 100.0f; i += 0.5f)
{
if (GetRandom("test", LevelData.LevelType.LocationConnection, i, biome.Identifier) == null)
{
DebugConsole.ThrowError($"No suitable level generation parameters found for a specific type of level (level type: LocationConnection, difficulty: {i}, biome: {biome.Identifier})");
}
if (GetRandom("test", LevelData.LevelType.Outpost, i, biome.Identifier) == null)
{
DebugConsole.ThrowError($"No suitable level generation parameters found for a specific type of level (level type: Outpost, difficulty: {i}, biome: {biome.Identifier})");
}
}
}
}
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biome = default)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
@@ -545,7 +584,9 @@ namespace Barotrauma
throw new InvalidOperationException("Level generation presets not found - using default presets");
}
var matchingLevelParams = LevelParams.Where(lp =>
var levelParamsOrdered = LevelParams.OrderBy(l => l.UintIdentifier);
var matchingLevelParams = levelParamsOrdered.Where(lp =>
lp.Type == type &&
(lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) &&
!lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier()));
@@ -559,16 +600,25 @@ namespace Barotrauma
if (!biome.IsEmpty)
{
//try to find params that at least have a suitable type
matchingLevelParams = LevelParams.Where(lp => lp.Type == type);
matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type);
if (!matchingLevelParams.Any())
{
//still not found, give up and choose some params randomly
matchingLevelParams = LevelParams;
matchingLevelParams = levelParamsOrdered;
}
}
}
return matchingLevelParams.GetRandom(Rand.RandSync.ServerAndClient);
if (!matchingLevelParams.Any(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty))
{
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})");
}
else
{
matchingLevelParams = matchingLevelParams.Where(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty);
}
return ToolBox.SelectWeightedRandom(matchingLevelParams, p => p.Commonness, Rand.RandSync.ServerAndClient);
}
public LevelGenerationParams(ContentXElement element, LevelGenerationParametersFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName))
@@ -102,32 +102,44 @@ namespace Barotrauma
foreach (Structure structure in Structure.WallList)
{
if (!structure.HasBody || structure.HiddenInGame) { continue; }
LevelObjectPrefab.SpawnPosType spawnPosType = LevelObjectPrefab.SpawnPosType.None;
if (level.Ruins.Any(r => r.Submarine == structure.Submarine))
{
if (structure.IsHorizontal)
{
bool topHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitY * 64) != null;
bool bottomHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitY * 64) != null;
if (topHull && bottomHull ) { continue; }
spawnPosType = LevelObjectPrefab.SpawnPosType.RuinWall;
}
else if (structure.Submarine?.Info?.Type == SubmarineType.Outpost)
{
spawnPosType = LevelObjectPrefab.SpawnPosType.OutpostWall;
}
else
{
continue;
}
availableSpawnPositions.Add(new SpawnPosition(
new GraphEdge(new Vector2(structure.WorldRect.X, structure.WorldPosition.Y), new Vector2(structure.WorldRect.Right, structure.WorldPosition.Y)),
bottomHull ? Vector2.UnitY : -Vector2.UnitY,
LevelObjectPrefab.SpawnPosType.RuinWall,
bottomHull ? Alignment.Bottom : Alignment.Top));
}
else
{
bool rightHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitX * 64) != null;
bool leftHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitX * 64) != null;
if (rightHull && leftHull) { continue; }
if (structure.IsHorizontal)
{
bool topHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitY * 64) != null;
bool bottomHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitY * 64) != null;
if (topHull && bottomHull) { continue; }
availableSpawnPositions.Add(new SpawnPosition(
new GraphEdge(new Vector2(structure.WorldPosition.X, structure.WorldRect.Y), new Vector2(structure.WorldPosition.X, structure.WorldRect.Y - structure.WorldRect.Height)),
leftHull ? Vector2.UnitX : -Vector2.UnitX,
LevelObjectPrefab.SpawnPosType.RuinWall,
leftHull ? Alignment.Left : Alignment.Right));
}
availableSpawnPositions.Add(new SpawnPosition(
new GraphEdge(new Vector2(structure.WorldRect.X, structure.WorldPosition.Y), new Vector2(structure.WorldRect.Right, structure.WorldPosition.Y)),
bottomHull ? Vector2.UnitY : -Vector2.UnitY,
spawnPosType,
bottomHull ? Alignment.Bottom : Alignment.Top));
}
else
{
bool rightHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitX * 64) != null;
bool leftHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitX * 64) != null;
if (rightHull && leftHull) { continue; }
availableSpawnPositions.Add(new SpawnPosition(
new GraphEdge(new Vector2(structure.WorldPosition.X, structure.WorldRect.Y), new Vector2(structure.WorldPosition.X, structure.WorldRect.Y - structure.WorldRect.Height)),
leftHull ? Vector2.UnitX : -Vector2.UnitX,
spawnPosType,
leftHull ? Alignment.Left : Alignment.Right));
}
}
@@ -158,7 +170,7 @@ namespace Barotrauma
for (int i = 0; i < amount; i++)
{
//get a random prefab and find a place to spawn it
LevelObjectPrefab prefab = GetRandomPrefab(level.GenerationParams, availablePrefabs);
LevelObjectPrefab prefab = GetRandomPrefab(level, availablePrefabs);
if (prefab == null) { continue; }
if (!suitableSpawnPositions.ContainsKey(prefab))
{
@@ -583,12 +595,12 @@ namespace Barotrauma
}
}
private LevelObjectPrefab GetRandomPrefab(LevelGenerationParams generationParams, IList<LevelObjectPrefab> availablePrefabs)
private LevelObjectPrefab GetRandomPrefab(Level level, IList<LevelObjectPrefab> availablePrefabs)
{
if (availablePrefabs.Sum(p => p.GetCommonness(generationParams)) <= 0.0f) { return null; }
if (availablePrefabs.Sum(p => p.GetCommonness(level.LevelData)) <= 0.0f) { return null; }
return ToolBox.SelectWeightedRandom(
availablePrefabs,
availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.ServerAndClient);
availablePrefabs.Select(p => p.GetCommonness(level.LevelData)).ToList(), Rand.RandSync.ServerAndClient);
}
private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList<LevelObjectPrefab> availablePrefabs, bool requireCaveSpecificOverride)
@@ -44,6 +44,7 @@ namespace Barotrauma
MainPath = 64,
LevelStart = 128,
LevelEnd = 256,
OutpostWall = 512,
Wall = MainPathWall | SidePathWall | CaveWall,
}
@@ -425,15 +426,21 @@ namespace Barotrauma
return requireCaveSpecificOverride ? 0.0f : Commonness;
}
public float GetCommonness(LevelGenerationParams generationParams)
{
if (generationParams != null &&
generationParams.Identifier != Identifier.Empty &&
(OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) ||
(!generationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness))))
public float GetCommonness(LevelData levelData)
{
if (levelData.GenerationParams != null && levelData.GenerationParams.Identifier != Identifier.Empty &&
OverrideCommonness.TryGetValue(levelData.GenerationParams.Identifier, out float commonness) ||
(!levelData.GenerationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(levelData.GenerationParams.OldIdentifier, out commonness)))
{
return commonness;
}
if (levelData?.Biome != null)
{
if (OverrideCommonness.TryGetValue(levelData.Biome.Identifier, out float biomeCommonness))
{
return biomeCommonness;
}
}
return Commonness;
}
@@ -6,7 +6,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -152,12 +152,6 @@ namespace Barotrauma
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
#if CLIENT
VertexBuffer?.Dispose();
VertexBuffer = null;
@@ -1,12 +1,11 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.IO;
using Barotrauma.Extensions;
using System.Collections.Immutable;
namespace Barotrauma
{
@@ -82,6 +81,8 @@ namespace Barotrauma
private XElement saveElement;
private Vector2? positionRelativeToMainSub;
public override bool Linkable
{
get
@@ -215,16 +216,19 @@ namespace Barotrauma
saveElement = element
};
if (!string.IsNullOrWhiteSpace(levelSeed) && levelData != null &&
levelData.Seed != levelSeed && !linkedSub.purchasedLostShuttles)
{
linkedSub.loadSub = false;
}
else
bool levelMatches = string.IsNullOrWhiteSpace(levelSeed) || levelData == null || levelData.Seed == levelSeed;
//don't load a sub that was left in this level if we have a submarine switch pending
//to make sure it gets ignored during the submarine switch and item transfer (reloading and saving it during the switch makes it not considered "left behind")
if ((levelMatches || linkedSub.purchasedLostShuttles) && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null)
{
linkedSub.loadSub = true;
linkedSub.rect.Location = MathUtils.ToPoint(pos);
}
else
{
linkedSub.loadSub = false;
}
}
#warning TODO: revise
@@ -253,6 +257,15 @@ namespace Barotrauma
}
}
public void SetPositionRelativeToMainSub()
{
if (positionRelativeToMainSub.HasValue)
{
Sub.SetPosition(Submarine.WorldPosition + positionRelativeToMainSub.Value);
}
positionRelativeToMainSub = null;
}
public override void OnMapLoaded()
{
if (!loadSub) { return; }
@@ -279,14 +292,14 @@ namespace Barotrauma
if (worldPos != Vector2.Zero)
{
if (GameMain.GameSession != null && GameMain.GameSession.MirrorLevel)
{
{
worldPos.X = GameMain.GameSession.LevelData.Size.X - worldPos.X;
}
sub.SetPosition(worldPos);
}
else
{
sub.SetPosition(WorldPosition);
sub.SetPosition(WorldPosition);
}
DockingPort linkedPort = null;
@@ -308,8 +321,29 @@ namespace Barotrauma
{
linkedPort = (FindEntityByID(originalLinkedToID) as Item)?.GetComponent<DockingPort>();
}
if (linkedPort == null) { return; }
}
if (linkedPort == null)
{
if (worldPos == Vector2.Zero)
{
Vector2 relativePos = saveElement.GetAttributeVector2("posrelativetomainsub", Vector2.Zero);
if (relativePos != Vector2.Zero)
{
positionRelativeToMainSub = relativePos;
}
else
{
DebugConsole.ThrowError("Something went wrong when loading a linked submarine - the save didn't include a world position, a linked port or position relative to the main sub.");
}
}
else
{
sub.Submarine = Submarine;
}
return;
}
originalLinkedPort = linkedPort;
ushort originalMyId = childRemap.GetOffsetId(originalMyPortID);
@@ -432,7 +466,7 @@ namespace Barotrauma
if (sub != null)
{
bool leaveBehind = false;
if (!sub.DockedTo.Contains(Submarine.MainSub))
if (sub.Submarine != null && !sub.DockedTo.Contains(sub.Submarine))
{
System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit);
if (Submarine.MainSub.AtEndExit)
@@ -457,8 +491,9 @@ namespace Barotrauma
}
else
{
if (saveElement.Attribute("location") != null) saveElement.Attribute("location").Remove();
if (saveElement.Attribute("worldpos") != null) saveElement.Attribute("worldpos").Remove();
if (saveElement.Attribute("location") != null) { saveElement.Attribute("location").Remove(); }
if (saveElement.Attribute("worldpos") != null) { saveElement.Attribute("worldpos").Remove(); }
saveElement.SetAttributeValue("posrelativetomainsub", XMLExtensions.Vector2ToString(sub.WorldPosition - Submarine.WorldPosition));
}
saveElement.SetAttributeValue("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition));
}
@@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using StoreBalanceStatus = Barotrauma.LocationType.StoreBalanceStatus;
namespace Barotrauma
{
@@ -92,21 +91,8 @@ namespace Barotrauma
public class StoreInfo
{
private int balance;
public Identifier Identifier { get; }
public int Balance
{
get
{
return balance;
}
set
{
balance = value;
ActiveBalanceStatus = Location.GetStoreBalanceStatus(value);
}
}
public int Balance { get; set; }
public List<PurchasedItem> Stock { get; } = new List<PurchasedItem>();
public List<ItemPrefab> DailySpecials { get; } = new List<ItemPrefab>();
public List<ItemPrefab> RequestedGoods { get; } = new List<ItemPrefab>();
@@ -114,8 +100,6 @@ namespace Barotrauma
/// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa.
/// </summary>
public int PriceModifier { get; set; }
public StoreBalanceStatus ActiveBalanceStatus { get; private set; }
public Color BalanceColor => ActiveBalanceStatus.Color;
public Location Location { get; }
private StoreInfo(Location location)
@@ -298,14 +282,7 @@ namespace Barotrauma
price = Location.DailySpecialPriceModifier * price;
}
// Adjust by current location reputation
if (Location.Reputation.Value > 0.0f)
{
price = MathHelper.Lerp(1.0f, 1.0f - Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MaxReputation) * price;
}
else
{
price = MathHelper.Lerp(1.0f, 1.0f + Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MinReputation) * price;
}
price *= Location.GetStoreReputationModifier(true);
// Price should never go below 1 mk
return Math.Max((int)price, 1);
}
@@ -319,22 +296,13 @@ namespace Barotrauma
float price = Location.StoreSellPriceModifier * priceInfo.Price;
// Adjust by random price modifier
price = (100 - PriceModifier) / 100.0f * price;
// Adjust by current store balance
price = ActiveBalanceStatus.SellPriceModifier * price;
// Adjust by requested good status
if (considerRequestedGoods && RequestedGoods.Contains(item))
{
price = Location.RequestGoodPriceModifier * price;
}
// Adjust by current location reputation
if (Location.Reputation.Value > 0.0f)
{
price = MathHelper.Lerp(1.0f, 1.0f + Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MaxReputation) * price;
}
else
{
price = MathHelper.Lerp(1.0f, 1.0f - Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MinReputation) * price;
}
price *= Location.GetStoreReputationModifier(false);
// Price should never go below 1 mk
return Math.Max((int)price, 1);
}
@@ -353,7 +321,6 @@ namespace Barotrauma
private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier;
public int StoreInitialBalance => Type.StoreInitialBalance;
private int StorePriceModifierRange => Type.StorePriceModifierRange;
private List<StoreBalanceStatus> StoreBalanceStatuses => Type.StoreBalanceStatuses;
/// <summary>
/// How many map progress steps it takes before the discounts should be updated.
@@ -518,6 +485,19 @@ namespace Barotrauma
TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0);
StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0);
Identifier biomeId = element.GetAttributeIdentifier("biome", Identifier.Empty);
if (biomeId != Identifier.Empty)
{
if (Biome.Prefabs.TryGet(biomeId, out Biome biome))
{
Biome = biome;
}
else
{
DebugConsole.ThrowError($"Error while loading the campaign map: could not find a biome with the identifier \"{biomeId}\".");
}
}
if (!typeNotFound)
{
for (int i = 0; i < Type.CanChangeTo.Count; i++)
@@ -806,22 +786,41 @@ namespace Barotrauma
static float GetConnectionWeight(Location location, LocationConnection c)
{
float weight = c.Passed ? 1.0f : 5.0f;
Location destination = c.OtherLocation(location);
if (destination != null)
if (destination == null) { return 0; }
float minWeight = 0.0001f;
float lowWeight = 0.2f;
float normalWeight = 1.0f;
float maxWeight = 2.0f;
float weight = c.Passed ? lowWeight : normalWeight;
if (location.Biome.AllowedZones.Contains(1))
{
if (destination.MapPosition.X > location.MapPosition.X) { weight *= 2.0f; }
int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination));
if (missionCount > 0)
{
weight /= missionCount * 2;
}
if (destination.IsRadiated())
// In the first biome, give a stronger preference for locations that are farther to the right)
float diff = destination.MapPosition.X - location.MapPosition.X;
if (diff < 0)
{
weight *= 0.001f;
weight *= 0.1f;
}
else
{
float maxRelevantDiff = 300;
weight = MathHelper.Lerp(weight, maxWeight, MathUtils.InverseLerp(0, maxRelevantDiff, diff));
}
}
return weight;
else if (destination.MapPosition.X > location.MapPosition.X)
{
weight *= 2.0f;
}
int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination));
if (missionCount > 0)
{
weight /= missionCount * 2;
}
if (destination.IsRadiated())
{
weight *= 0.001f;
}
return MathHelper.Clamp(weight, minWeight, maxWeight);
}
return InstantiateMission(prefab, connection);
@@ -1224,6 +1223,32 @@ namespace Barotrauma
}
}
public float GetStoreReputationModifier(bool buying)
{
if (buying)
{
if (Reputation.Value > 0.0f)
{
return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation);
}
else
{
return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation);
}
}
else
{
if (Reputation.Value > 0.0f)
{
return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation);
}
else
{
return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation);
}
}
}
public int GetExtraSpecialSalesCount()
{
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
@@ -1231,21 +1256,6 @@ namespace Barotrauma
return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount));
}
public StoreBalanceStatus GetStoreBalanceStatus(int balance)
{
StoreBalanceStatus nextStatus = StoreBalanceStatuses[0];
for (int i = 1; i < StoreBalanceStatuses.Count; i++)
{
var status = StoreBalanceStatuses[i];
if (status.PercentageOfInitialBalance < nextStatus.PercentageOfInitialBalance &&
((float)balance / StoreInitialBalance) < status.PercentageOfInitialBalance)
{
nextStatus = status;
}
}
return nextStatus;
}
public void Discover(bool checkTalents = true)
{
if (Discovered) { return; }
@@ -1277,6 +1287,7 @@ namespace Barotrauma
new XAttribute("originaltype", (Type ?? OriginalType).Identifier),
new XAttribute("basename", BaseName),
new XAttribute("name", Name),
new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty),
new XAttribute("discovered", Discovered),
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
new XAttribute("pricemultiplier", PriceMultiplier),
@@ -88,27 +88,6 @@ namespace Barotrauma
public int DailySpecialsCount { get; } = 1;
public int RequestedGoodsCount { get; } = 1;
public List<StoreBalanceStatus> StoreBalanceStatuses { get; } = new List<StoreBalanceStatus>()
{
new StoreBalanceStatus(1.0f, 1.0f, Color.White),
new StoreBalanceStatus(0.5f, 0.75f, Color.Orange),
new StoreBalanceStatus(0.25f, 0.2f, Color.Red)
};
public struct StoreBalanceStatus
{
public float PercentageOfInitialBalance { get; }
public float SellPriceModifier { get; }
public Color Color { get; }
public StoreBalanceStatus(float percentage, float sellPriceModifier, Color color)
{
PercentageOfInitialBalance = percentage;
SellPriceModifier = sellPriceModifier;
Color = color;
}
}
public override string ToString()
{
return $"LocationType (" + Identifier + ")";
@@ -208,18 +187,6 @@ namespace Barotrauma
RequestGoodPriceModifier = subElement.GetAttributeFloat("requestgoodpricemodifier", RequestGoodPriceModifier);
StoreInitialBalance = subElement.GetAttributeInt("initialbalance", StoreInitialBalance);
StorePriceModifierRange = subElement.GetAttributeInt("pricemodifierrange", StorePriceModifierRange);
var balanceStatusElements = subElement.GetChildElements("balancestatus");
if (balanceStatusElements.Any())
{
StoreBalanceStatuses.Clear();
foreach (var balanceStatusElement in balanceStatusElements)
{
float percentage = balanceStatusElement.GetAttributeFloat("percentage", 1.0f);
float modifier = balanceStatusElement.GetAttributeFloat("sellpricemodifier", 1.0f);
Color color = balanceStatusElement.GetAttributeColor("color", Color.White);
StoreBalanceStatuses.Add(new StoreBalanceStatus(percentage, modifier, color));
}
}
DailySpecialsCount = subElement.GetAttributeInt("dailyspecialscount", DailySpecialsCount);
RequestedGoodsCount = subElement.GetAttributeInt("requestedgoodscount", RequestedGoodsCount);
break;
@@ -78,7 +78,7 @@ namespace Barotrauma
/// <summary>
/// Load a previously saved campaign map from XML
/// </summary>
private Map(CampaignMode campaign, XElement element, CampaignSettings settings) : this(settings)
private Map(CampaignMode campaign, XElement element) : this(campaign.Settings)
{
Seed = element.GetAttributeString("seed", "a");
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
@@ -104,7 +104,7 @@ namespace Barotrauma
case "radiation":
Radiation = new Radiation(this, generationParams.RadiationParams, subElement)
{
Enabled = settings.RadiationEnabled
Enabled = campaign.Settings.RadiationEnabled
};
break;
}
@@ -131,18 +131,27 @@ namespace Barotrauma
};
Locations[locationIndices.X].Connections.Add(connection);
Locations[locationIndices.Y].Connections.Add(connection);
connection.LevelData = new LevelData(subElement.Element("Level"));
string biomeId = subElement.GetAttributeString("biome", "");
connection.Biome =
Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeId) ??
Biome.Prefabs.FirstOrDefault(b => !b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeId) ??
Biome.Prefabs.First();
connection.Difficulty = MathHelper.Clamp(connection.Difficulty, connection.Biome.MinDifficulty, connection.Biome.MaxDifficulty);
connection.LevelData = new LevelData(subElement.Element("Level"), connection.Difficulty);
Connections.Add(connection);
connectionElements.Add(subElement);
break;
}
}
//backwards compatibility: location biomes weren't saved (or used for anything) previously,
//assign them if they haven't been assigned
Random rand = new MTRandom(ToolBox.StringToInt(Seed));
if (Locations.First().Biome == null)
{
AssignBiomes(rand);
}
int startLocationindex = element.GetAttributeInt("startlocation", -1);
if (startLocationindex > 0 && startLocationindex < Locations.Count)
{
@@ -199,12 +208,12 @@ namespace Barotrauma
/// <summary>
/// Generate a new campaign map from the seed
/// </summary>
public Map(CampaignMode campaign, string seed, CampaignSettings settings) : this(settings)
public Map(CampaignMode campaign, string seed) : this(campaign.Settings)
{
Seed = seed;
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
Generate();
Generate(campaign.Settings);
if (Locations.Count == 0)
{
@@ -219,10 +228,7 @@ namespace Barotrauma
foreach (Location location in Locations)
{
if (location.Type.Identifier != "outpost") { continue; }
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
{
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
}
SetStartLocation(location);
}
//if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost
if (CurrentLocation == null)
@@ -230,17 +236,47 @@ namespace Barotrauma
foreach (Location location in Locations)
{
if (!location.Type.HasOutpost) { continue; }
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
SetStartLocation(location);
}
}
void SetStartLocation(Location location)
{
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
{
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
}
}
System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
int loops = campaign.CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0);
if (loops == 0 && (campaign.Settings.Difficulty == GameDifficulty.Easy || campaign.Settings.Difficulty == GameDifficulty.Medium))
{
if (StartLocation != null)
{
StartLocation.LevelData = new LevelData(StartLocation, 0);
}
//ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy
foreach (var locationConnection in StartLocation.Connections)
{
if (locationConnection.Difficulty > 0.0f)
{
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
locationConnection.Difficulty = 0.0f;
locationConnection.LevelData = new LevelData(locationConnection);
}
}
}
System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
CurrentLocation.Discover(true);
CurrentLocation.CreateStores();
foreach (var location in Locations)
{
location.UnlockInitialMissions();
}
InitProjectSpecific();
}
@@ -248,7 +284,7 @@ namespace Barotrauma
#region Generation
private void Generate()
private void Generate(CampaignSettings settings)
{
Connections.Clear();
Locations.Clear();
@@ -266,7 +302,6 @@ namespace Barotrauma
Voronoi voronoi = new Voronoi(0.5f);
List<GraphEdge> edges = voronoi.MakeVoronoiGraph(voronoiSites, Width, Height);
float zoneWidth = Width / generationParams.DifficultyZones;
Vector2 margin = new Vector2(
Math.Min(10, Width * 0.1f),
@@ -282,6 +317,7 @@ 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; }
@@ -316,12 +352,26 @@ namespace Barotrauma
}
LocationType forceLocationType = null;
foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
if (!possibleStartOutpostCreated)
{
if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount)
float zoneWidth = Width / generationParams.DifficultyZones;
float threshold = zoneWidth * 0.1f;
if (position.X < threshold)
{
forceLocationType = locationType;
break;
LocationType.Prefabs.TryGet("outpost", out forceLocationType);
possibleStartOutpostCreated = true;
}
}
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;
}
}
}
@@ -427,9 +477,7 @@ namespace Barotrauma
if (zone1 == zone2) { continue; }
if (zone1 > zone2)
{
int temp = zone2;
zone2 = zone1;
zone1 = temp;
(zone1, zone2) = (zone2, zone1);
}
if (generationParams.GateCount[zone1] == 0) { continue; }
@@ -495,38 +543,46 @@ namespace Barotrauma
//remove orphans
Locations.RemoveAll(l => !Connections.Any(c => c.Locations.Contains(l)));
AssignBiomes(new MTRandom(ToolBox.StringToInt(Seed)));
foreach (LocationConnection connection in Connections)
{
//float difficulty = GetLevelDifficulty(connection.CenterPos.X / Width);
//connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-10.0f, 0.0f, Rand.RandSync.ServerAndClient), 1.2f, 100.0f);
float difficulty = connection.CenterPos.X / Width * 100;
float random = difficulty > 10 ? 5 : 0;
connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-random, random, Rand.RandSync.ServerAndClient), 1.0f, 100.0f);
if (connection.Locations.Any(l => l.IsGateBetweenBiomes))
{
connection.Difficulty = connection.Locations.Min(l => l.Biome.MaxDifficulty);
}
else
{
connection.Difficulty = CalculateDifficulty(connection.CenterPos.X, connection.Biome);
}
}
AssignBiomes();
CreateEndLocation();
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location)
{
Difficulty = MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f)
//Difficulty = MathHelper.Clamp(GetLevelDifficulty(location.MapPosition.X / Width), 0.0f, 100.0f)
};
location.UnlockInitialMissions();
location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome));
}
foreach (LocationConnection connection in Connections)
{
connection.LevelData = new LevelData(connection);
}
float GetLevelDifficulty(float areaDifficulty)
float CalculateDifficulty(float mapPosition, Biome biome)
{
const float CurveModifier = 1.5f;
const float DifficultyMultiplier = 1.14f;
const float BaseDifficulty = -3f;
return (float)(1 - Math.Pow(1 - areaDifficulty, CurveModifier)) * DifficultyMultiplier * 100f + BaseDifficulty;
float settingsFactor = settings.LevelDifficultyMultiplier;
float minDifficulty = 0;
float maxDifficulty = 100;
float difficulty = mapPosition / Width * 100;
System.Diagnostics.Debug.Assert(biome != null);
if (biome != null)
{
minDifficulty = biome.MinDifficulty;
maxDifficulty = biome.MaxDifficulty;
float diff = 1 - settingsFactor;
difficulty *= 1 - (1f / biome.AllowedZones.Max() * diff);
}
return MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty);
}
}
@@ -551,7 +607,7 @@ namespace Barotrauma
return Biome.Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
}
private void AssignBiomes()
private void AssignBiomes(Random rand)
{
var biomes = Biome.Prefabs;
float zoneWidth = Width / generationParams.DifficultyZones;
@@ -567,7 +623,7 @@ namespace Barotrauma
{
if (location.MapPosition.X < zoneX)
{
location.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.ServerAndClient)];
location.Biome = allowedBiomes[rand.Next() % allowedBiomes.Count];
}
}
}
@@ -608,6 +664,11 @@ namespace Barotrauma
if (EndLocation == null || previousToEndLocation == null) { return; }
if (LocationType.Prefabs.TryGet("none", out LocationType locationType))
{
previousToEndLocation.ChangeType(locationType);
}
//remove all locations from the end biome except the end location
for (int i = Locations.Count - 1; i >= 0; i--)
{
@@ -627,7 +688,7 @@ namespace Barotrauma
}
//removed all connections from the second-to-last location, need to reconnect it
if (!previousToEndLocation.Connections.Any())
if (previousToEndLocation.Connections.None())
{
Location connectTo = Locations.First();
foreach (Location location in Locations)
@@ -734,6 +795,7 @@ namespace Barotrauma
CurrentLocation = Locations[index];
CurrentLocation.Discover();
CurrentLocation.CreateStores();
if (prevLocation != CurrentLocation)
{
var connection = CurrentLocation.Connections.Find(c => c.Locations.Contains(prevLocation));
@@ -741,10 +803,8 @@ namespace Barotrauma
{
connection.Passed = true;
}
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
}
CurrentLocation.CreateStores();
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
}
public void SelectLocation(int index)
@@ -764,6 +824,7 @@ namespace Barotrauma
return;
}
Location prevSelected = SelectedLocation;
SelectedLocation = Locations[index];
var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation();
SelectedConnection =
@@ -773,7 +834,10 @@ namespace Barotrauma
{
DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
}
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
if (prevSelected != SelectedLocation)
{
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
}
}
public void SelectLocation(Location location)
@@ -786,13 +850,17 @@ namespace Barotrauma
return;
}
Location prevSelected = SelectedLocation;
SelectedLocation = location;
SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
if (SelectedConnection?.Locked ?? false)
{
DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
}
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
if (prevSelected != SelectedLocation)
{
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
}
}
public void SelectMission(IEnumerable<int> missionIndices)
@@ -805,23 +873,24 @@ namespace Barotrauma
return;
}
CurrentLocation.SetSelectedMissionIndices(missionIndices);
foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList())
if (!missionIndices.SequenceEqual(GetSelectedMissionIndices()))
{
if (selectedMission.Locations[0] != CurrentLocation ||
selectedMission.Locations[1] != CurrentLocation)
CurrentLocation.SetSelectedMissionIndices(missionIndices);
foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList())
{
if (SelectedConnection == null) { return; }
//the destination must be the same as the destination of the mission
if (selectedMission.Locations[1] != SelectedLocation)
if (selectedMission.Locations[0] != CurrentLocation ||
selectedMission.Locations[1] != CurrentLocation)
{
CurrentLocation.DeselectMission(selectedMission);
if (SelectedConnection == null) { return; }
//the destination must be the same as the destination of the mission
if (selectedMission.Locations[1] != SelectedLocation)
{
CurrentLocation.DeselectMission(selectedMission);
}
}
}
OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions);
}
OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions);
}
public void SelectRandomLocation(bool preferUndiscovered)
@@ -1015,8 +1084,7 @@ namespace Barotrauma
{
string prevName = location.Name;
var newType = LocationType.Prefabs[change.ChangeToType];
if (newType == null)
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.");
return false;
@@ -1046,9 +1114,9 @@ namespace Barotrauma
/// <summary>
/// Load a previously saved map from an xml element
/// </summary>
public static Map Load(CampaignMode campaign, XElement element, CampaignSettings settings)
public static Map Load(CampaignMode campaign, XElement element)
{
Map map = new Map(campaign, element, settings);
Map map = new Map(campaign, element);
map.LoadState(element, false);
#if CLIENT
map.DrawOffset = -map.CurrentLocation.MapPosition;
@@ -293,7 +293,7 @@ namespace Barotrauma
}
}
public virtual void Move(Vector2 amount)
public virtual void Move(Vector2 amount, bool ignoreContacts = false)
{
rect.X += (int)amount.X;
rect.Y += (int)amount.Y;
@@ -493,25 +493,33 @@ namespace Barotrauma
protected void InsertToList()
{
int i = 0;
if (Sprite == null)
{
mapEntityList.Add(this);
return;
}
int i = 0;
while (i < mapEntityList.Count)
{
i++;
Sprite existingSprite = mapEntityList[i - 1].Sprite;
if (existingSprite == null) continue;
#if CLIENT
if (existingSprite.Texture == this.Sprite.Texture) break;
#endif
if (mapEntityList[i - 1]?.Prefab == Prefab)
{
mapEntityList.Insert(i, this);
return;
}
}
#if CLIENT
i = 0;
while (i < mapEntityList.Count)
{
i++;
Sprite existingSprite = mapEntityList[i - 1].Sprite;
if (existingSprite == null) { continue; }
if (existingSprite.Texture == this.Sprite.Texture) { break; }
}
#endif
mapEntityList.Insert(i, this);
}
@@ -563,6 +571,10 @@ namespace Barotrauma
{
mapEntityUpdateTick++;
#if CLIENT
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
#endif
if (mapEntityUpdateTick % MapEntityUpdateInterval == 0)
{
@@ -597,6 +609,12 @@ namespace Barotrauma
Powered.UpdatePower(deltaTime * PoweredUpdateInterval);
}
#if CLIENT
sw.Stop();
GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Misc", sw.ElapsedTicks);
sw.Restart();
#endif
if (mapEntityUpdateTick % MapEntityUpdateInterval == 0)
{
foreach (Item item in Item.ItemList)
@@ -613,9 +631,14 @@ namespace Barotrauma
item.Update(deltaTime, cam);
}
#if CLIENT
sw.Stop();
GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Items", sw.ElapsedTicks);
sw.Restart();
#endif
if (mapEntityUpdateTick % MapEntityUpdateInterval == 0)
{
UpdateAllProjSpecific(deltaTime * MapEntityUpdateInterval);
Spawner?.Update();
@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
class BeaconStationInfo : ISerializableEntity
{
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool AllowDamagedWalls { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool AllowDisconnectedWires { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes), Editable]
public float MinLevelDifficulty { get; set; }
[Serialize(100.0f, IsPropertySaveable.Yes), Editable]
public float MaxLevelDifficulty { get; set; }
public string Name { get; private set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public BeaconStationInfo(SubmarineInfo submarineInfo, XElement element)
{
Name = $"BeaconStationInfo ({submarineInfo.Name})";
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public BeaconStationInfo(SubmarineInfo submarineInfo)
{
Name = $"BeaconStationInfo ({submarineInfo.Name})";
SerializableProperties = SerializableProperty.DeserializeProperties(this);
}
public BeaconStationInfo(BeaconStationInfo original)
{
Name = original.Name;
SerializableProperties = new Dictionary<Identifier, SerializableProperty>();
foreach (KeyValuePair<Identifier, SerializableProperty> kvp in original.SerializableProperties)
{
SerializableProperties.Add(kvp.Key, kvp.Value);
if (SerializableProperty.GetSupportedTypeName(kvp.Value.PropertyType) != null)
{
kvp.Value.TrySetValue(this, kvp.Value.GetValue(original));
}
}
}
public void Save(XElement element)
{
SerializableProperty.SerializeProperties(this, element);
}
}
}
@@ -34,23 +34,6 @@ namespace Barotrauma
return prefab;
}
private void Dispose(bool disposing)
{
if (!Disposed)
{
if (disposing)
{
Humans.Clear();
}
}
Disposed = true;
}
public override void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public override void Dispose() { }
}
}
@@ -1,11 +1,9 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -98,9 +96,29 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes), Editable]
public string ReplaceInRadiation { get; set; }
private readonly Dictionary<Identifier, int> moduleCounts = new Dictionary<Identifier, int>();
public class ModuleCount
{
public Identifier Identifier;
public int Count;
public int Order;
public IReadOnlyDictionary<Identifier, int> ModuleCounts
public ModuleCount(ContentXElement element)
{
Identifier = element.GetAttributeIdentifier("flag", element.GetAttributeIdentifier("moduletype", ""));
Count = element.GetAttributeInt("count", 0);
Order = element.GetAttributeInt("order", 0);
}
public ModuleCount(Identifier id, int count)
{
Identifier = id;
Count = count;
}
}
private readonly List<ModuleCount> moduleCounts = new List<ModuleCount>();
public IReadOnlyList<ModuleCount> ModuleCounts
{
get { return moduleCounts; }
}
@@ -171,8 +189,7 @@ namespace Barotrauma
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "modulecount":
Identifier moduleFlag = subElement.GetAttributeIdentifier("flag", subElement.GetAttributeIdentifier("moduletype", ""));
moduleCounts[moduleFlag] = subElement.GetAttributeInt("count", 0);
moduleCounts.Add(new ModuleCount(subElement));
break;
case "npcs":
var newCollection = new NpcCollection();
@@ -200,7 +217,7 @@ namespace Barotrauma
public int GetModuleCount(Identifier moduleFlag)
{
if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return int.MaxValue; }
return moduleCounts.ContainsKey(moduleFlag) ? moduleCounts[moduleFlag] : 0;
return moduleCounts.FirstOrDefault(m => m.Identifier == moduleFlag)?.Count ?? 0;
}
public void SetModuleCount(Identifier moduleFlag, int count)
@@ -208,11 +225,19 @@ namespace Barotrauma
if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return; }
if (count <= 0)
{
moduleCounts.Remove(moduleFlag);
moduleCounts.RemoveAll(m => m.Identifier == moduleFlag);
}
else
{
moduleCounts[moduleFlag] = count;
var moduleCount = moduleCounts.FirstOrDefault(m => m.Identifier == moduleFlag);
if (moduleCount == null)
{
moduleCounts.Add(new ModuleCount(moduleFlag, count));
}
else
{
moduleCount.Count = count;
}
}
}
@@ -99,7 +99,7 @@ namespace Barotrauma
{
//if the module doesn't have the ruin flag or any other flag used in the generation params, don't use it in ruins
if (!subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()) &&
!generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Key)))
!generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Identifier)))
{
continue;
}
@@ -141,16 +141,11 @@ namespace Barotrauma
selectedModules.Clear();
//select which module types the outpost should consist of
List<Identifier> pendingModuleFlags;
using (var md5 = MD5.Create())
{
#warning TODO: cursed
pendingModuleFlags = onlyEntrance
? generationParams.ModuleCounts
.Keys.OrderBy(k => ToolBox.IdentifierToUint32Hash(k, md5))
.First().ToEnumerable().ToList()
: SelectModules(outpostModules, generationParams);
}
List<Identifier> pendingModuleFlags =
onlyEntrance ?
generationParams.ModuleCounts.First().Identifier.ToEnumerable().ToList() :
SelectModules(outpostModules, generationParams);
foreach (Identifier flag in pendingModuleFlags)
{
if (flag == "none") { continue; }
@@ -437,31 +432,27 @@ namespace Barotrauma
var pendingModuleFlags = new List<Identifier>();
bool availableModulesFound = true;
Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Key;
Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Identifier;
pendingModuleFlags.Add(initialModuleFlag);
while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound)
{
availableModulesFound = false;
foreach (var moduleFlag in generationParams.ModuleCounts)
{
if (pendingModuleFlags.Count(m => m == moduleFlag.Key) >= generationParams.GetModuleCount(moduleFlag.Key))
if (pendingModuleFlags.Count(m => m == moduleFlag.Identifier) >= generationParams.GetModuleCount(moduleFlag.Identifier))
{
continue;
}
if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag.Key)))
if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag.Identifier)))
{
DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleFlag.Key}\" found).");
DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleFlag.Identifier}\" found).");
continue;
}
availableModulesFound = true;
pendingModuleFlags.Add(moduleFlag.Key);
pendingModuleFlags.Add(moduleFlag.Identifier);
}
}
using (MD5 md5 = MD5.Create())
{
pendingModuleFlags.Sort((i1, i2) => (int)ToolBox.StringToUInt32Hash(i1.Value.ToLowerInvariant(), md5) - (int)ToolBox.StringToUInt32Hash(i2.Value.ToLowerInvariant(), md5));
}
pendingModuleFlags.Shuffle(Rand.RandSync.ServerAndClient);
pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f)).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient));
while (pendingModuleFlags.Count < totalModuleCount)
{
//don't place "none" modules at the end because
@@ -610,7 +601,7 @@ namespace Barotrauma
Identifier flagToPlace = "none".ToIdentifier();
SubmarineInfo nextModule = null;
foreach (Identifier moduleFlag in pendingModuleFlags)
foreach (Identifier moduleFlag in pendingModuleFlags.OrderByDescending(f => currentModule?.Info?.OutpostModuleInfo.AllowAttachToModules.Contains(f) ?? false))
{
flagToPlace = moduleFlag;
nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType, allowDifferentLocationType);
@@ -830,43 +821,44 @@ namespace Barotrauma
private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable<SubmarineInfo> modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType)
{
IEnumerable<SubmarineInfo> availableModules = null;
IEnumerable<SubmarineInfo> modulesWithCorrectFlags = null;
if (moduleFlag.IsEmpty || moduleFlag.Equals("none"))
{
availableModules = modules
modulesWithCorrectFlags = modules
.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())));
}
else
{
availableModules = modules
modulesWithCorrectFlags = modules
.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
}
modulesWithCorrectFlags = modulesWithCorrectFlags.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
if (prevModule != null)
var suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
if (!suitableModules.Any())
{
availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo));
//no suitable module found, see if we can find a "generic" module that's not meant for any specific type of outpost
suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
//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 = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: 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 = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
}
}
if (availableModules.Count() == 0) { return null; }
//try to search for modules made specifically for this location type first
var modulesSuitableForLocationType =
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
//if not found, search for modules suitable for any location type
if (allowDifferentLocationType && !modulesSuitableForLocationType.Any())
{
modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any());
}
if (!modulesSuitableForLocationType.Any())
if (!suitableModules.Any())
{
if (allowDifferentLocationType)
{
if (modulesWithCorrectFlags.Any())
DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange);
return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
return ToolBox.SelectWeightedRandom(modulesWithCorrectFlags.ToList(), modulesWithCorrectFlags.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
}
else
{
@@ -875,7 +867,28 @@ namespace Barotrauma
}
else
{
return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
return ToolBox.SelectWeightedRandom(suitableModules.ToList(), suitableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
}
IEnumerable<SubmarineInfo> GetSuitable(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific)
{
IEnumerable<SubmarineInfo> suitable = modules;
if (requireCorrectLocationType)
{
if (disallowNonLocationTypeSpecific)
{
suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
}
else
{
suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier) || !m.OutpostModuleInfo.AllowedLocationTypes.Any());
}
}
if (requireAllowAttachToPrevious && prevModule != null)
{
suitable = suitable.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));
}
return suitable;
}
}
@@ -1026,6 +1039,17 @@ namespace Barotrauma
module.ThisGapPosition == OutpostModuleInfo.GapPosition.Left ||
module.ThisGapPosition == OutpostModuleInfo.GapPosition.Right;
if (!module.ThisGap.linkedTo.Any())
{
DebugConsole.ThrowError($"Error during outpost generation: {module.ThisGapPosition} gap in module \"{module.Info.Name}\" was not linked to any hulls.");
continue;
}
if (!module.PreviousGap.linkedTo.Any())
{
DebugConsole.ThrowError($"Error during outpost generation: {GetOpposingGapPosition(module.ThisGapPosition)} gap in module \"{module.PreviousModule.Info.Name}\" was not linked to any hulls.");
continue;
}
MapEntity leftHull = module.ThisGap.Position.X < module.PreviousGap.Position.X ? module.ThisGap.linkedTo[0] : module.PreviousGap.linkedTo[0];
MapEntity rightHull = module.ThisGap.Position.X > module.PreviousGap.Position.X ?
module.ThisGap.linkedTo.Count == 1 ? module.ThisGap.linkedTo[0] : module.ThisGap.linkedTo[1] :
@@ -1077,7 +1101,7 @@ namespace Barotrauma
{
foreach (Connection c in gapToRemove.ConnectedDoor.Item.Connections)
{
c.Wires.ForEach(w => w?.Item.Remove());
c.Wires.ToArray().ForEach(w => w?.Item.Remove());
}
}
@@ -1428,7 +1452,7 @@ namespace Barotrauma
{
foreach (Connection connection in linkedItem.Connections)
{
foreach (Wire w in connection.Wires)
foreach (Wire w in connection.Wires.ToArray())
{
w?.Item.Remove();
}
@@ -1590,10 +1614,6 @@ namespace Barotrauma
{
npc.CharacterHealth.Unkillable = true;
}
else
{
npc.AddStaticHealthMultiplier(humanPrefab.HealthMultiplier);
}
humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient);
foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true))
{
@@ -354,7 +354,7 @@ namespace Barotrauma
private set;
}
public override void Move(Vector2 amount)
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
if (!MathUtils.IsValid(amount))
{
@@ -377,7 +377,15 @@ namespace Barotrauma
Vector2 simAmount = ConvertUnits.ToSimUnits(amount);
foreach (Body b in Bodies)
{
b.SetTransform(b.Position + simAmount, b.Rotation);
Vector2 pos = b.Position + simAmount;
if (ignoreContacts)
{
b.SetTransformIgnoreContacts(ref pos, b.Rotation);
}
else
{
b.SetTransform(pos, b.Rotation);
}
}
}
@@ -1208,7 +1216,7 @@ namespace Barotrauma
private void UpdateSections()
{
if (Bodies == null) return;
if (Bodies == null) { return; }
foreach (Body b in Bodies)
{
GameMain.World.Remove(b);
@@ -1281,9 +1289,9 @@ namespace Barotrauma
Body newBody = GameMain.World.CreateRectangle(
ConvertUnits.ToSimUnits(rect.Width),
ConvertUnits.ToSimUnits(rect.Height),
1.5f);
newBody.BodyType = BodyType.Static;
//newBody.Position = ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2.0f, rect.Y - rect.Height / 2.0f));
1.5f,
bodyType: BodyType.Static,
findNewContacts: false);
newBody.Friction = 0.5f;
newBody.OnCollision += OnWallCollision;
newBody.CollisionCategories = (Prefab.Platform) ? Physics.CollisionPlatform : Physics.CollisionWall;
@@ -1292,15 +1300,16 @@ namespace Barotrauma
Vector2 structureCenter = ConvertUnits.ToSimUnits(Position);
if (BodyRotation != 0.0f)
{
newBody.Position = structureCenter + bodyOffset + new Vector2(
Vector2 pos = structureCenter + bodyOffset + new Vector2(
(float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation),
(float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation))
* ConvertUnits.ToSimUnits(diffFromCenter);
newBody.Rotation = -BodyRotation;
newBody.SetTransformIgnoreContacts(ref pos, -BodyRotation);
}
else
{
newBody.Position = structureCenter + (IsHorizontal ? Vector2.UnitX : Vector2.UnitY) * ConvertUnits.ToSimUnits(diffFromCenter) + bodyOffset;
Vector2 pos = structureCenter + (IsHorizontal ? Vector2.UnitX : Vector2.UnitY) * ConvertUnits.ToSimUnits(diffFromCenter) + bodyOffset;
newBody.SetTransformIgnoreContacts(ref pos, newBody.Rotation);
}
if (createConvexHull)
@@ -1302,189 +1302,200 @@ namespace Barotrauma
public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
{
Loading = true;
loaded.Add(this);
Info = new SubmarineInfo(info);
ConnectedDockingPorts = new Dictionary<Submarine, DockingPort>();
//place the sub above the top of the level
HiddenSubPosition = HiddenSubStartPosition;
if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null)
GameMain.World.Enabled = false;
try
{
HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y;
}
loaded.Add(this);
foreach (Submarine sub in loaded)
{
HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f);
}
Info = new SubmarineInfo(info);
IdOffset = IdRemap.DetermineNewOffset();
ConnectedDockingPorts = new Dictionary<Submarine, DockingPort>();
List<MapEntity> newEntities = new List<MapEntity>();
if (loadEntities == null)
{
if (Info.SubmarineElement != null)
//place the sub above the top of the level
HiddenSubPosition = HiddenSubStartPosition;
if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null)
{
newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath, IdOffset);
}
}
else
{
newEntities = loadEntities(this);
newEntities.ForEach(me => me.Submarine = this);
}
if (newEntities != null)
{
foreach (var e in newEntities)
{
if (linkedRemap != null) { e.ResolveLinks(linkedRemap); }
e.unresolvedLinkedToID = null;
}
}
Vector2 center = Vector2.Zero;
var matchingHulls = Hull.HullList.FindAll(h => h.Submarine == this);
if (matchingHulls.Any())
{
Vector2 topLeft = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y);
Vector2 bottomRight = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y);
foreach (Hull hull in matchingHulls)
{
if (hull.Rect.X < topLeft.X) topLeft.X = hull.Rect.X;
if (hull.Rect.Y > topLeft.Y) topLeft.Y = hull.Rect.Y;
if (hull.Rect.Right > bottomRight.X) bottomRight.X = hull.Rect.Right;
if (hull.Rect.Y - hull.Rect.Height < bottomRight.Y) bottomRight.Y = hull.Rect.Y - hull.Rect.Height;
HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y;
}
center = (topLeft + bottomRight) / 2.0f;
center.X -= center.X % GridSize.X;
center.Y -= center.Y % GridSize.Y;
RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this));
subBody = new SubmarineBody(this, showWarningMessages);
subBody.SetPosition(HiddenSubPosition);
if (info.IsOutpost)
foreach (Submarine sub in loaded)
{
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
TeamID = CharacterTeamType.FriendlyNPC;
HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f);
}
bool indestructible =
GameMain.NetworkMember != null &&
!GameMain.NetworkMember.ServerSettings.DestructibleOutposts &&
!(info.OutpostGenerationParams?.AlwaysDestructible ?? false);
IdOffset = IdRemap.DetermineNewOffset();
foreach (MapEntity me in MapEntity.mapEntityList)
List<MapEntity> newEntities = new List<MapEntity>();
if (loadEntities == null)
{
if (Info.SubmarineElement != null)
{
if (me.Submarine != this) { continue; }
if (me is Item item)
newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath, IdOffset);
}
}
else
{
newEntities = loadEntities(this);
newEntities.ForEach(me => me.Submarine = this);
}
if (newEntities != null)
{
foreach (var e in newEntities)
{
if (linkedRemap != null) { e.ResolveLinks(linkedRemap); }
e.unresolvedLinkedToID = null;
}
}
Vector2 center = Vector2.Zero;
var matchingHulls = Hull.HullList.FindAll(h => h.Submarine == this);
if (matchingHulls.Any())
{
Vector2 topLeft = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y);
Vector2 bottomRight = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y);
foreach (Hull hull in matchingHulls)
{
if (hull.Rect.X < topLeft.X) topLeft.X = hull.Rect.X;
if (hull.Rect.Y > topLeft.Y) topLeft.Y = hull.Rect.Y;
if (hull.Rect.Right > bottomRight.X) bottomRight.X = hull.Rect.Right;
if (hull.Rect.Y - hull.Rect.Height < bottomRight.Y) bottomRight.Y = hull.Rect.Y - hull.Rect.Height;
}
center = (topLeft + bottomRight) / 2.0f;
center.X -= center.X % GridSize.X;
center.Y -= center.Y % GridSize.Y;
RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this));
subBody = new SubmarineBody(this, showWarningMessages);
Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition);
subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f);
if (info.IsOutpost)
{
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
TeamID = CharacterTeamType.FriendlyNPC;
bool indestructible =
GameMain.NetworkMember != null &&
!GameMain.NetworkMember.ServerSettings.DestructibleOutposts &&
!(info.OutpostGenerationParams?.AlwaysDestructible ?? false);
foreach (MapEntity me in MapEntity.mapEntityList)
{
item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null;
item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true;
if (item.GetComponent<Repairable>() != null && indestructible)
if (me.Submarine != this) { continue; }
if (me is Item item)
{
item.Indestructible = true;
}
foreach (ItemComponent ic in item.Components)
{
if (ic is ConnectionPanel connectionPanel)
item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null;
item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true;
if (item.GetComponent<Repairable>() != null && indestructible)
{
//prevent rewiring
if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable)
item.Indestructible = true;
}
foreach (ItemComponent ic in item.Components)
{
if (ic is ConnectionPanel connectionPanel)
{
connectionPanel.Locked = true;
//prevent rewiring
if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable)
{
connectionPanel.Locked = true;
}
}
else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
{
//prevent deattaching items from walls
#if CLIENT
if (GameMain.GameSession?.GameMode is TutorialMode) { continue; }
#endif
holdable.CanBePicked = false;
holdable.CanBeSelected = false;
}
}
else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
{
//prevent deattaching items from walls
#if CLIENT
if (GameMain.GameSession?.GameMode is TutorialMode) { continue; }
#endif
holdable.CanBePicked = false;
holdable.CanBeSelected = false;
}
}
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible)
{
structure.Indestructible = true;
}
}
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible)
}
else if (info.IsRuin)
{
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
}
}
if (entityGrid != null)
{
Hull.EntityGrids.Remove(entityGrid);
entityGrid = null;
}
entityGrid = Hull.GenerateEntityGrid(this);
for (int i = 0; i < MapEntity.mapEntityList.Count; i++)
{
if (MapEntity.mapEntityList[i].Submarine != this) { continue; }
MapEntity.mapEntityList[i].Move(HiddenSubPosition, ignoreContacts: true);
}
Loading = false;
MapEntity.MapLoaded(newEntities, true);
foreach (MapEntity me in MapEntity.mapEntityList)
{
if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this)
{
linkedSub.LinkDummyToMainSubmarine();
}
}
foreach (Hull hull in matchingHulls)
{
if (string.IsNullOrEmpty(hull.RoomName))// || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase))
{
hull.RoomName = hull.CreateRoomName();
}
}
if (GameMain.GameSession?.Campaign?.UpgradeManager != null)
{
GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged += ResetCrushDepth;
}
#if CLIENT
GameMain.LightManager.OnMapLoaded();
#endif
//if the sub was made using an older version,
//halve the brightness of the lights to make them look (almost) right on the new lighting formula
if (showWarningMessages &&
!string.IsNullOrEmpty(Info.FilePath) &&
Screen.Selected != GameMain.SubEditorScreen &&
(Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0")))
{
DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. "
+ "The game automatically adjusts the lights make them look better with the new formula, but it's recommended to open the submarine in the submarine editor and make sure everything looks right after the automatic conversion.");
foreach (Item item in Item.ItemList)
{
if (item.Submarine != this) continue;
if (item.ParentInventory != null || item.body != null) continue;
foreach (var light in item.GetComponents<LightComponent>())
{
structure.Indestructible = true;
light.LightColor = new Color(light.LightColor, light.LightColor.A / 255.0f * 0.5f);
}
}
}
else if (info.IsRuin)
{
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
}
GenerateOutdoorNodes();
}
if (entityGrid != null)
finally
{
Hull.EntityGrids.Remove(entityGrid);
entityGrid = null;
Loading = false;
GameMain.World.Enabled = true;
}
entityGrid = Hull.GenerateEntityGrid(this);
for (int i = 0; i < MapEntity.mapEntityList.Count; i++)
{
if (MapEntity.mapEntityList[i].Submarine != this) { continue; }
MapEntity.mapEntityList[i].Move(HiddenSubPosition);
}
Loading = false;
MapEntity.MapLoaded(newEntities, true);
foreach (MapEntity me in MapEntity.mapEntityList)
{
if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this)
{
linkedSub.LinkDummyToMainSubmarine();
}
}
foreach (Hull hull in matchingHulls)
{
if (string.IsNullOrEmpty(hull.RoomName))// || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase))
{
hull.RoomName = hull.CreateRoomName();
}
}
if (GameMain.GameSession?.Campaign?.UpgradeManager != null)
{
GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged += ResetCrushDepth;
}
#if CLIENT
GameMain.LightManager.OnMapLoaded();
#endif
//if the sub was made using an older version,
//halve the brightness of the lights to make them look (almost) right on the new lighting formula
if (showWarningMessages &&
!string.IsNullOrEmpty(Info.FilePath) &&
Screen.Selected != GameMain.SubEditorScreen &&
(Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0")))
{
DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. "
+ "The game automatically adjusts the lights make them look better with the new formula, but it's recommended to open the submarine in the submarine editor and make sure everything looks right after the automatic conversion.");
foreach (Item item in Item.ItemList)
{
if (item.Submarine != this) continue;
if (item.ParentInventory != null || item.body != null) continue;
var lightComponent = item.GetComponent<Items.Components.LightComponent>();
if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f);
}
}
GenerateOutdoorNodes();
}
protected override ushort DetermineID(ushort id, Submarine submarine)
@@ -1495,10 +1506,7 @@ namespace Barotrauma
public static Submarine Load(SubmarineInfo info, bool unloadPrevious, IdRemap linkedRemap = null)
{
if (unloadPrevious) { Unload(); }
Submarine sub = new Submarine(info, false, linkedRemap: linkedRemap);
return sub;
return new Submarine(info, false, linkedRemap: linkedRemap);
}
private void ResetCrushDepth()
@@ -1549,7 +1557,7 @@ namespace Barotrauma
element.Add(new XAttribute("cargocapacity", cargoCapacity));
element.Add(new XAttribute("recommendedcrewsizemin", Info.RecommendedCrewSizeMin));
element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax));
element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? ""));
element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience.ToString()));
element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages)));
if (Info.Type == SubmarineType.OutpostModule)
@@ -1599,18 +1607,14 @@ namespace Barotrauma
{
if (item.FindParentInventory(inv => inv is CharacterInventory) != null) { continue; }
#if CLIENT
if (Screen.Selected != GameMain.SubEditorScreen)
{
if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; }
}
else
if (Screen.Selected == GameMain.SubEditorScreen)
{
e.Submarine = this;
}
#else
if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; }
#endif
if (e.Submarine != this) { continue; }
var rootContainer = item.GetRootContainer();
if (rootContainer != null && rootContainer.Submarine != this) { continue; }
}
else
{
@@ -1630,6 +1634,7 @@ namespace Barotrauma
Type = Info.Type,
FilePath = filePath,
OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null,
BeaconStationInfo = Info.BeaconStationInfo != null ? new BeaconStationInfo(Info.BeaconStationInfo) : null,
Name = Path.GetFileNameWithoutExtension(filePath)
};
#if CLIENT
@@ -1851,5 +1856,42 @@ namespace Barotrauma
}
public void RefreshOutdoorNodes() => OutdoorNodes.ForEach(n => n?.Waypoint?.FindHull());
public Item FindContainerFor(Item item, bool onlyPrimary, bool checkTransferConditions = false, bool allowConnectedSubs = false)
{
var connectedSubs = GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet();
Item selectedContainer = null;
foreach (Item potentialContainer in Item.ItemList)
{
if (potentialContainer.Removed) { continue; }
if (potentialContainer.NonInteractable) { continue; }
if (potentialContainer.HiddenInGame) { continue; }
if (allowConnectedSubs)
{
if (!connectedSubs.Contains(potentialContainer.Submarine)) { continue; }
}
else
{
if (potentialContainer.Submarine != this) { continue; }
}
if (potentialContainer == item) { continue; }
if (potentialContainer.Condition <= 0) { continue; }
if (potentialContainer.OwnInventory == null) { continue; }
if (potentialContainer.GetRootInventoryOwner() != potentialContainer) { continue; }
var container = potentialContainer.GetComponent<ItemContainer>();
if (container == null) { continue; }
if (!potentialContainer.OwnInventory.CanBePut(item)) { continue; }
if (!container.ShouldBeContained(item, out _)) { continue; }
if (!item.Prefab.IsContainerPreferred(item, container, out bool isPreferencesDefined, out bool isSecondary, checkTransferConditions: checkTransferConditions) || !isPreferencesDefined || onlyPrimary && isSecondary) { continue; }
if (potentialContainer.Submarine == this && !isSecondary)
{
//valid primary container in the same sub -> perfect, let's use that one
return potentialContainer;
}
selectedContainer = potentialContainer;
}
return selectedContainer;
}
}
}
@@ -136,7 +136,17 @@ namespace Barotrauma
HullVertices = convexHull;
farseerBody = GameMain.World.CreateBody();
farseerBody = GameMain.World.CreateBody(findNewContacts: false, bodyType: BodyType.Dynamic);
var collisionCategory = Physics.CollisionWall;
var collidesWith =
Physics.CollisionItem |
Physics.CollisionLevel |
Physics.CollisionCharacter |
Physics.CollisionProjectile |
Physics.CollisionWall;
farseerBody.CollisionCategories = collisionCategory;
farseerBody.CollidesWith = collidesWith;
farseerBody.Enabled = false;
farseerBody.UserData = this;
foreach (var mapEntity in MapEntity.mapEntityList)
{
@@ -152,7 +162,9 @@ namespace Barotrauma
ConvertUnits.ToSimUnits(wall.BodyHeight),
50.0f,
-wall.BodyRotation,
ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset)).UserData = wall;
ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset),
collisionCategory,
collidesWith).UserData = wall;
}
}
@@ -167,7 +179,9 @@ namespace Barotrauma
ConvertUnits.ToSimUnits(rect.Width),
ConvertUnits.ToSimUnits(rect.Height),
100.0f,
ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2))).UserData = hull;
ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2)),
collisionCategory,
collidesWith).UserData = hull;
}
foreach (Item item in Item.ItemList)
@@ -191,47 +205,40 @@ namespace Barotrauma
if (width > 0.0f && height > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos));
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);
}
else if (radius > 0.0f && width > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2));
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);
}
else if (radius > 0.0f && height > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2));
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);
}
else if (radius > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos));
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);
}
item.StaticFixtures.ForEach(f => f.UserData = item);
}
Borders = new Rectangle((int)minExtents.X, (int)maxExtents.Y, (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y));
VisibleBorders = new Rectangle((int)visibleMinExtents.X, (int)visibleMaxExtents.Y, (int)(visibleMaxExtents.X - visibleMinExtents.X), (int)(visibleMaxExtents.Y - visibleMinExtents.Y));
}
farseerBody.BodyType = BodyType.Dynamic;
farseerBody.CollisionCategories = Physics.CollisionWall;
farseerBody.CollidesWith =
Physics.CollisionItem |
Physics.CollisionLevel |
Physics.CollisionCharacter |
Physics.CollisionProjectile |
Physics.CollisionWall;
farseerBody.Enabled = true;
farseerBody.Restitution = Restitution;
farseerBody.Friction = Friction;
farseerBody.FixedRotation = true;
@@ -39,7 +39,15 @@ namespace Barotrauma
public SubmarineTag Tags { get; private set; }
public int RecommendedCrewSizeMin = 1, RecommendedCrewSizeMax = 2;
public string RecommendedCrewExperience;
public enum CrewExperienceLevel
{
Unknown,
CrewExperienceLow,
CrewExperienceMid,
CrewExperienceHigh
}
public CrewExperienceLevel RecommendedCrewExperience;
/// <summary>
/// A random int that gets assigned when saving the sub. Used in mp campaign to verify that sub files match
@@ -89,6 +97,7 @@ namespace Barotrauma
public SubmarineClass SubmarineClass;
public OutpostModuleInfo OutpostModuleInfo { get; set; }
public BeaconStationInfo BeaconStationInfo { get; set; }
public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule;
@@ -100,6 +109,8 @@ namespace Barotrauma
public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined;
public bool IsCampaignCompatibleIgnoreClass => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus);
public bool AllowPreviewImage => Type == SubmarineType.Player;
public Md5Hash MD5Hash
{
get
@@ -280,6 +291,10 @@ namespace Barotrauma
{
OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo);
}
if (original.BeaconStationInfo != null)
{
BeaconStationInfo = new BeaconStationInfo(original.BeaconStationInfo);
}
#if CLIENT
PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage) : null;
#endif
@@ -330,7 +345,24 @@ namespace Barotrauma
CargoCapacity = SubmarineElement.GetAttributeInt("cargocapacity", -1);
RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0);
RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0);
RecommendedCrewExperience = SubmarineElement.GetAttributeString("recommendedcrewexperience", "Unknown");
var recommendedCrewExperience = SubmarineElement.GetAttributeIdentifier("recommendedcrewexperience", CrewExperienceLevel.Unknown.ToIdentifier());
// Backwards compatibility
if (recommendedCrewExperience == "Beginner")
{
RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceLow;
}
else if (recommendedCrewExperience == "Intermediate")
{
RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceMid;
}
else if (recommendedCrewExperience == "Experienced")
{
RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceHigh;
}
else
{
Enum.TryParse(recommendedCrewExperience.Value, ignoreCase: true, out RecommendedCrewExperience);
}
if (SubmarineElement?.Attribute("type") != null)
{
@@ -341,6 +373,10 @@ namespace Barotrauma
{
OutpostModuleInfo = new OutpostModuleInfo(this, SubmarineElement);
}
else if (Type == SubmarineType.BeaconStation)
{
BeaconStationInfo = new BeaconStationInfo(this, SubmarineElement);
}
}
}
@@ -359,20 +395,6 @@ namespace Barotrauma
SubmarineClass = SubmarineClass.Undefined;
}
//backwards compatibility (use text tags instead of the actual text)
if (RecommendedCrewExperience == "Beginner")
{
RecommendedCrewExperience = "CrewExperienceLow";
}
else if (RecommendedCrewExperience == "Intermediate")
{
RecommendedCrewExperience = "CrewExperienceMid";
}
else if (RecommendedCrewExperience == "Experienced")
{
RecommendedCrewExperience = "CrewExperienceHigh";
}
RequiredContentPackages.Clear();
string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", Array.Empty<string>());
foreach (string contentPackageName in contentPackageNames)
@@ -528,10 +550,15 @@ namespace Barotrauma
OutpostModuleInfo.Save(newElement);
OutpostModuleInfo = new OutpostModuleInfo(this, newElement);
}
else if (Type == SubmarineType.BeaconStation)
{
BeaconStationInfo.Save(newElement);
BeaconStationInfo = new BeaconStationInfo(this, newElement);
}
XDocument doc = new XDocument(newElement);
doc.Root.Add(new XAttribute("name", Name));
if (previewImage != null)
if (previewImage != null && AllowPreviewImage)
{
doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray())));
}
@@ -590,6 +617,7 @@ namespace Barotrauma
List<string> filePaths = new List<string>();
foreach (BaseSubFile subFile in contentPackageSubs)
{
if (!File.Exists(subFile.Path.Value)) { continue; }
if (!filePaths.Any(fp => fp == subFile.Path))
{
filePaths.Add(subFile.Path.Value);
@@ -109,14 +109,21 @@ namespace Barotrauma
#endif
}
public enum Type
{
WayPoint,
SpawnPoint
}
public WayPoint(Rectangle newRect, Submarine submarine)
: this (MapEntityPrefab.FindByIdentifier("waypoint".ToIdentifier()), newRect, submarine)
: this (Type.WayPoint, newRect, submarine)
{
}
public WayPoint(MapEntityPrefab prefab, Rectangle newRect, Submarine submarine, ushort id = Entity.NullEntityID)
: base (prefab, submarine, id)
public WayPoint(Type type, Rectangle newRect, Submarine submarine, ushort id = Entity.NullEntityID)
: base (type is Type.WayPoint
? CoreEntityPrefab.WayPointPrefab
: CoreEntityPrefab.SpawnPointPrefab, submarine, id)
{
rect = newRect;
idCardTags = Array.Empty<string>();
@@ -1010,7 +1017,7 @@ namespace Barotrauma
Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType);
WayPoint w = new WayPoint(MapEntityPrefab.FindByIdentifier((spawnType == SpawnType.Path ? "waypoint" : "spawnpoint").ToIdentifier()), rect, submarine, idRemap.GetOffsetId(element))
WayPoint w = new WayPoint(spawnType == SpawnType.Path ? Type.WayPoint : Type.SpawnPoint, rect, submarine, idRemap.GetOffsetId(element))
{
spawnType = spawnType
};