Merge branch 'dev' of https://github.com/Regalis11/Barotrauma.git into unstable-tests

This commit is contained in:
Evil Factory
2022-04-08 12:52:28 -03:00
990 changed files with 44338 additions and 38589 deletions
@@ -1,5 +1,10 @@
using System;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text;
namespace Barotrauma
@@ -8,7 +13,80 @@ namespace Barotrauma
{
public static readonly PrefabCollection<CoreEntityPrefab> Prefabs = new PrefabCollection<CoreEntityPrefab>();
private readonly ConstructorInfo constructor;
private CoreEntityPrefab(
Identifier identifier,
ConstructorInfo constructor,
bool resizeHorizontal = false,
bool resizeVertical = false,
bool linkable = false,
IEnumerable<Identifier> allowedLinks = null,
IEnumerable<string> aliases = null)
: base(identifier)
{
this.constructor = constructor;
this.Name = TextManager.Get($"EntityName.{identifier}");
this.Description = TextManager.Get($"EntityDescription.{identifier}");
this.ResizeHorizontal = resizeHorizontal;
this.ResizeVertical = resizeVertical;
this.Linkable = linkable;
this.AllowedLinks = (allowedLinks ?? Enumerable.Empty<Identifier>()).ToImmutableHashSet();
this.Aliases = (aliases ?? Enumerable.Empty<string>()).Concat(identifier.Value.ToEnumerable()).ToImmutableHashSet();
}
public static void InitCorePrefabs()
{
CoreEntityPrefab ep = new CoreEntityPrefab(
"hull".ToIdentifier(),
typeof(Hull).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }),
resizeHorizontal: true,
resizeVertical: true,
linkable: true,
allowedLinks: new Identifier[] { "hull".ToIdentifier() });
Prefabs.Add(ep, false);
ep = new CoreEntityPrefab(
"gap".ToIdentifier(),
typeof(Gap).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }),
resizeHorizontal: true,
resizeVertical: true);
Prefabs.Add(ep, false);
ep = new CoreEntityPrefab(
"waypoint".ToIdentifier(),
typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }));
Prefabs.Add(ep, false);
ep = new CoreEntityPrefab(
"spawnpoint".ToIdentifier(),
typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }));
Prefabs.Add(ep, false);
}
protected override void CreateInstance(Rectangle rect)
{
if (constructor == null) return;
object[] lobject = new object[] { this, rect };
constructor.Invoke(lobject);
}
private bool disposed = false;
public override Sprite Sprite => null;
public override string OriginalName => Name.Value;
public override LocalizedString Name { get; }
public override ImmutableHashSet<Identifier> Tags { get; } = Enumerable.Empty<Identifier>().ToImmutableHashSet();
public override ImmutableHashSet<Identifier> AllowedLinks { get; }
public override MapEntityCategory Category => MapEntityCategory.Structure;
public override ImmutableHashSet<string> Aliases { get; }
public override void Dispose()
{
if (disposed) { return; }
@@ -7,6 +7,7 @@ using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
@@ -18,45 +19,74 @@ namespace Barotrauma.MapCreatures.Behavior
public readonly BallastFloraBehavior? ParentBallastFlora;
public int ID = -1;
public Item ClaimedItem;
public Item? ClaimedItem;
public int ClaimedItemId = -1;
public float MaxHealth = 100f;
public float Health = 100f;
private float health = 100;
public float Health
{
get { return health; }
set { health = MathHelper.Clamp(value, 0.0f, MaxHealth); }
}
public float RemoveTimer = 60.0f;
public bool SpawningItem;
public Item? AttackItem;
public bool IsRoot;
/// <summary>
/// Decorative branches that grow around the root
/// </summary>
public bool IsRootGrowth;
public bool Removed;
public bool DisconnectedFromRoot;
public Hull? CurrentHull;
public float Pulse = 1.0f;
private bool inflate;
private float pulseDelay = Rand.Range(0f, 3f);
public readonly BallastFloraBranch? ParentBranch;
/// <summary>
/// How far from the root this branch is
/// </summary>
public readonly int BranchDepth;
public float AccumulatedDamage;
public float DamageVisualizationTimer;
#if CLIENT
public Vector2 ShakeAmount;
#endif
// Adjacent tiles, used to free up sides when this branch gets removed
public readonly Dictionary<TileSide, BallastFloraBranch> Connections = new Dictionary<TileSide, BallastFloraBranch>();
public BallastFloraBranch(BallastFloraBehavior? parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null)
public BallastFloraBranch(BallastFloraBehavior? parent, BallastFloraBranch? parentBranch, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null)
: base(null, position, type, flowerConfig, leafConfig, rect)
{
ParentBranch = parentBranch;
ParentBallastFlora = parent;
if (parentBranch != null)
{
BranchDepth = parentBranch.BranchDepth + 1;
}
}
public void UpdateHealth()
{
if (MaxHealth <= Health) { return; }
Color healthColor = Color.White * (1.0f - Health / MaxHealth);
HealthColor = healthColor;
HealthColor = Color.Lerp(HealthColor, healthColor, 0.05f);
}
public void UpdatePulse(float deltaTime, float inflateSpeed, float deflateSpeed, float delay)
{
if (ParentBallastFlora == null) { return; }
if (ParentBallastFlora == null || DisconnectedFromRoot) { return; }
if (pulseDelay > 0)
{
@@ -91,7 +121,7 @@ namespace Barotrauma.MapCreatures.Behavior
public List<Tuple<Vector2, Vector2>> debugSearchLines = new List<Tuple<Vector2, Vector2>>();
#endif
private static List<BallastFloraBehavior> _entityList = new List<BallastFloraBehavior>();
private readonly static List<BallastFloraBehavior> _entityList = new List<BallastFloraBehavior>();
public static IEnumerable<BallastFloraBehavior> EntityList => _entityList;
public enum NetworkHeader
@@ -101,119 +131,126 @@ namespace Barotrauma.MapCreatures.Behavior
BranchCreate,
BranchRemove,
BranchDamage,
Infect
Infect,
Remove
}
public enum AttackType
{
Fire,
Explosives,
Other
Other,
CutFromRoot
}
public struct AITarget
{
public string[] Tags;
public Identifier[] Tags;
public int Priority;
public AITarget(XElement element)
public AITarget(ContentXElement element)
{
Tags = element.GetAttributeStringArray("tags", new string[0]);
Tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>())!;
Priority = element.GetAttributeInt("priority", 0);
}
public bool Matches(Item item)
{
foreach (string tag in item.GetTags())
{
foreach (Identifier targetTag in Tags)
{
foreach (string targetTag in Tags)
{
if (tag == targetTag) { return true; }
}
if (item.HasTag(targetTag)) { return true; }
}
return false;
}
}
[Serialize(0.25f, true, "Scale of the branches")]
[Serialize(0.25f, IsPropertySaveable.Yes, "Scale of the branches.")]
public float BaseBranchScale { get; set; }
[Serialize(0.25f, true, "Scale of the flowers")]
[Serialize(0.25f, IsPropertySaveable.Yes, "Scale of the flowers.")]
public float BaseFlowerScale { get; set; }
[Serialize(0.5f, true, "Scale of the leaves")]
[Serialize(0.5f, IsPropertySaveable.Yes, "Scale of the leaves.")]
public float BaseLeafScale { get; set; }
[Serialize(0.33f, true, "Chance for a flower to appear on the branch")]
[Serialize(0.33f, IsPropertySaveable.Yes, "Chance for a flower to appear on a branch.")]
public float FlowerProbability { get; set; }
[Serialize(0.7f, true, "Change for leaves to appear for the branch")]
[Serialize(0.7f, IsPropertySaveable.Yes, "Chance for leaves to appear on a branch.")]
public float LeafProbability { get; set; }
[Serialize(3f, true, "Delay between pulses")]
[Serialize(3f, IsPropertySaveable.Yes, "Delay between pulses.")]
public float PulseDelay { get; set; }
[Serialize(3f, true, "How fast the flower inflates during a pulse")]
[Serialize(3f, IsPropertySaveable.Yes, "How fast the flower inflates during a pulse.")]
public float PulseInflateSpeed { get; set; }
[Serialize(1f, true, "How fast the flower deflates")]
[Serialize(1f, IsPropertySaveable.Yes, "How fast the flower deflates.")]
public float PulseDeflateSpeed { get; set; }
[Serialize(32, true, "How many vines must grow before the plant breaks thru the wall")]
[Serialize(32, IsPropertySaveable.Yes, "How many vines must grow before the plant breaks through the wall.")]
public int BreakthroughPoint { get; set; }
[Serialize(false, true, "Has the plant grown large enough to expose itself")]
[Serialize(false, IsPropertySaveable.Yes, "Has the plant grown large enough to expose itself.")]
public bool HasBrokenThrough { get; set; }
[Serialize(300, true, "How far the ballast flora can detect items")]
[Serialize(300, IsPropertySaveable.Yes, "How far the ballast flora can detect items from.")]
public int Sight { get; set; }
[Serialize(100, true, "How much health the branches have")]
[Serialize(100, IsPropertySaveable.Yes, "How much health the branches have.")]
public int BranchHealth { get; set; }
[Serialize(400, true, "How much health the stem has")]
public int StemHealth { get; set; }
[Serialize(400, IsPropertySaveable.Yes, "How much health the root has.")]
public int RootHealth { get; set; }
[Serialize(300f, true, "How much power the ballast flora takes from junction boxes")]
[Serialize(0.0005f, 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.")]
public int MaxBranchHealthRegenDistance { get; set; }
[Serialize("255,255,255,255", IsPropertySaveable.Yes)]
public Color RootColor { get; set; }
[Serialize(300f, IsPropertySaveable.Yes, "How much power the ballast flora takes from junction boxes.")]
public float PowerConsumptionMin { get; set; }
[Serialize(3000f, true, "How much the power drain spikes")]
[Serialize(3000f, IsPropertySaveable.Yes, "How much the power drain spikes.")]
public float PowerConsumptionMax { get; set; }
[Serialize(10f, true, "How long it takes for power drain to wind down")]
[Serialize(10f, IsPropertySaveable.Yes, "How long it takes for power drain to wind down.")]
public float PowerConsumptionDuration { get; set; }
[Serialize(250f, true, "How much power does it take to accelerate growth")]
[Serialize(250f, IsPropertySaveable.Yes, "How much power does it take to accelerate growth.")]
public float PowerRequirement { get; set; }
[Serialize(5f, true, "Maximum anger, anger increases when the plant gets damaged and increases growth speed")]
[Serialize(5f, IsPropertySaveable.Yes, "Maximum anger, anger increases when the plant gets damaged and increases growth speed.")]
public float MaxAnger { get; set; }
[Serialize(10000f, true, "Maximum power buffer")]
[Serialize(10000f, IsPropertySaveable.Yes, "Maximum power buffer.")]
public float MaxPowerCapacity { get; set; }
[Serialize("", true, "Item prefab that is spawned when threatened")]
public string AttackItemPrefab { get; set; } = "";
[Serialize("", IsPropertySaveable.Yes, "Item prefab that is spawned when threatened.")]
public Identifier AttackItemPrefab { get; set; } = Identifier.Empty;
[Serialize(0.8f, true, "How resistant the ballast flora is to exlposives before it blooms")]
[Serialize(0.8f, IsPropertySaveable.Yes, "How resistant the ballast flora is to explosives before it blooms.")]
public float ExplosionResistance { get; set; }
[Serialize(5f, true, "How much damage is taken from open fires")]
[Serialize(5f, IsPropertySaveable.Yes, "How much damage is taken from open fires.")]
public float FireVulnerability { get; set; }
[Serialize(0.5f, true, "How much resistance against fire is gained while submerged.")]
[Serialize(0.5f, IsPropertySaveable.Yes, "How much resistance against fire is gained while submerged.")]
public float SubmergedWaterResistance { get; set; }
[Serialize(0.8f, true, "What depth the branches will be drawn on")]
[Serialize(0.8f, IsPropertySaveable.Yes, "What depth the branches will be drawn on.")]
public float BranchDepth { get; set; }
[Serialize("", true, "What sound to play when the ballast flora bursts thru walls")]
[Serialize("", IsPropertySaveable.Yes, "What sound to play when the ballast flora bursts through walls.")]
public string BurstSound { get; set; } = "";
private float availablePower;
[Serialize(0f, true, "How much power the ballast flora has stored.")]
[Serialize(0f, IsPropertySaveable.Yes, "How much power the ballast flora has stored.")]
public float AvailablePower
{
get => availablePower;
@@ -222,7 +259,7 @@ namespace Barotrauma.MapCreatures.Behavior
private float anger;
[Serialize(1f, true, "How enraged the flora is, affects how fast it grows.")]
[Serialize(1f, IsPropertySaveable.Yes, "How enraged the flora is, affects how fast it grows.")]
public float Anger
{
get => anger;
@@ -235,13 +272,13 @@ namespace Barotrauma.MapCreatures.Behavior
public BallastFloraPrefab Prefab { get; private set; }
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public Vector2 Offset;
public readonly List<Item> ClaimedTargets = new List<Item>();
public readonly List<PowerTransfer> ClaimedJunctionBoxes = new List<PowerTransfer>();
public readonly List<PowerContainer> ClaimedBatteries = new List<PowerContainer>();
public readonly HashSet<Item> ClaimedTargets = new HashSet<Item>();
public readonly HashSet<PowerTransfer> ClaimedJunctionBoxes = new HashSet<PowerTransfer>();
public readonly HashSet<PowerContainer> ClaimedBatteries = new HashSet<PowerContainer>();
public readonly Dictionary<Item, int> IgnoredTargets = new Dictionary<Item, int>();
private readonly List<Tuple<UInt16, int>> tempClaimedTargets = new List<Tuple<ushort, int>>();
@@ -252,11 +289,12 @@ namespace Barotrauma.MapCreatures.Behavior
public float PowerConsumptionTimer;
private float defenseCooldown, toxinsCooldown, fireCheckCooldown;
private float damageIndicatorTimer, selfDamageTimer, toxinsTimer;
private float selfDamageTimer, toxinsTimer, toxinsSpawnTimer;
private readonly List<BallastFloraBranch> branchesVulnerableToFire = new List<BallastFloraBranch>();
public readonly List<BallastFloraBranch> Branches = new List<BallastFloraBranch>();
private BallastFloraBranch? root;
private readonly List<Body> bodies = new List<Body>();
public readonly BallastFloraStateMachine StateMachine;
@@ -319,15 +357,15 @@ namespace Barotrauma.MapCreatures.Behavior
SerializableProperties = SerializableProperty.DeserializeProperties(this, prefab.Element);
LoadPrefab(prefab.Element);
StateMachine = new BallastFloraStateMachine(this);
if (firstGrowth) { GenerateStem(); }
if (firstGrowth) { GenerateRoot(); }
_entityList.Add(this);
}
partial void LoadPrefab(XElement element);
partial void LoadPrefab(ContentXElement element);
public void LoadTargets(XElement element)
public void LoadTargets(ContentXElement element)
{
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
Targets.Add(new AITarget(subElement));
}
@@ -358,6 +396,10 @@ namespace Barotrauma.MapCreatures.Behavior
{
be.Add(new XAttribute("claimed", (int)(branch.ClaimedItem?.ID ?? -1)));
}
if (branch.ParentBranch != null)
{
be.Add(new XAttribute("parentbranch", (int)(branch.ParentBranch?.ID ?? -1)));
}
saveElement.Add(be);
}
@@ -382,7 +424,7 @@ namespace Barotrauma.MapCreatures.Behavior
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
Offset = element.GetAttributeVector2("offset", Vector2.Zero);
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -412,8 +454,15 @@ namespace Barotrauma.MapCreatures.Behavior
int sides = getInt("sides");
int blockedSides = getInt("blockedsides");
int claimedId = branchElement.GetAttributeInt("claimed", -1);
int parentBranchId = branchElement.GetAttributeInt("parentbranch", -1);
BallastFloraBranch newBranch = new BallastFloraBranch(this, pos, VineTileType.CrossJunction, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafconfig))
BallastFloraBranch? parentBranch = null;
if (parentBranchId > -1)
{
parentBranch = Branches[parentBranchId];
}
BallastFloraBranch newBranch = new BallastFloraBranch(this, parentBranch, pos, VineTileType.CrossJunction, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafconfig))
{
ID = id,
Health = health,
@@ -422,6 +471,7 @@ namespace Barotrauma.MapCreatures.Behavior
BlockedSides = (TileSide) blockedSides,
IsRoot = isRoot
};
if (newBranch.IsRoot) { root = newBranch; }
if (claimedId > -1)
{
@@ -437,6 +487,15 @@ namespace Barotrauma.MapCreatures.Behavior
public void Update(float deltaTime)
{
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
if (Branches.Count == 0)
{
Remove();
return;
}
}
foreach (BallastFloraBranch branch in Branches)
{
branch.UpdateScale(deltaTime);
@@ -446,38 +505,26 @@ namespace Barotrauma.MapCreatures.Behavior
#endif
}
if (damageIndicatorTimer <= 0)
{
foreach (BallastFloraBranch branch in Branches)
{
if (branch.AccumulatedDamage > 0)
{
#if CLIENT
CreateDamageParticle(branch, branch.AccumulatedDamage);
if (GameMain.DebugDraw)
{
var pos = (Parent?.Position ?? Vector2.Zero) + Offset + branch.Position;
GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUI.Style.Red, pos, Vector2.UnitY * 10.0f, 3f, playSound: false, subId: Parent?.Submarine?.ID ?? -1);
}
#elif SERVER
SendNetworkMessage(this, NetworkHeader.BranchDamage, branch, branch.AccumulatedDamage);
#endif
}
branch.AccumulatedDamage = 0f;
}
damageIndicatorTimer = 1f;
}
damageIndicatorTimer -= deltaTime;
UpdateDamage(deltaTime);
UpdatePowerDrain(deltaTime);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (root != null && HealthRegenPerBranch > 0.0f)
{
float healAmount = Branches.Count(b => !b.IsRoot && !b.IsRootGrowth && !b.DisconnectedFromRoot) * HealthRegenPerBranch;
foreach (BallastFloraBranch branch in Branches)
{
if (branch.Health > branch.MaxHealth * 0.9f || branch.DisconnectedFromRoot) { continue; }
float branchHealAmount = (float)(MaxBranchHealthRegenDistance - branch.BranchDepth) / MaxBranchHealthRegenDistance * healAmount;
if (branchHealAmount <= 0.0f) { continue; }
float prevHealth = branch.Health;
branch.Health += branchHealAmount;
branch.AccumulatedDamage += (prevHealth - branch.Health);
}
}
StateMachine.Update(deltaTime);
if (HasBrokenThrough)
@@ -509,15 +556,16 @@ namespace Barotrauma.MapCreatures.Behavior
Anger -= deltaTime;
}
// This entire scope is probably very heavy for GC, need to experiment
if (toxinsTimer > 0.1f)
{
if (!string.IsNullOrWhiteSpace(AttackItemPrefab))
toxinsSpawnTimer -= deltaTime;
if (!AttackItemPrefab.IsEmpty && toxinsSpawnTimer <= 0.0f)
{
toxinsSpawnTimer = 1.0f;
Dictionary<Hull, List<BallastFloraBranch>> branches = new Dictionary<Hull, List<BallastFloraBranch>>();
foreach (BallastFloraBranch branch in Branches)
{
if (branch.CurrentHull == null || branch.FlowerConfig.Variant < 0) { continue; }
if (branch.CurrentHull == null || branch.FlowerConfig.Variant < 0 || branch.DisconnectedFromRoot) { continue; }
if (branches.TryGetValue(branch.CurrentHull, out List<BallastFloraBranch>? list))
{
@@ -534,11 +582,12 @@ namespace Barotrauma.MapCreatures.Behavior
List<BallastFloraBranch> list = branches[hull];
if (!list.Any(HasAcidEmitter))
{
BallastFloraBranch randomBranch = branches[hull].GetRandom();
BallastFloraBranch randomBranch = branches[hull].GetRandomUnsynced();
randomBranch.SpawningItem = true;
ItemPrefab prefab = ItemPrefab.Find(null, AttackItemPrefab);
Entity.Spawner?.AddToSpawnQueue(prefab, Parent.Position + Offset + randomBranch.Position, Parent.Submarine, onSpawned: item =>
#warning TODO: Parent needs a nullability sanity check
Entity.Spawner?.AddItemToSpawnQueue(prefab, Parent!.Position + Offset + randomBranch.Position, Parent.Submarine, onSpawned: item =>
{
randomBranch.AttackItem = item;
randomBranch.SpawningItem = false;
@@ -563,38 +612,50 @@ namespace Barotrauma.MapCreatures.Behavior
}
}
partial void UpdateDamage(float deltaTime);
private readonly List<BallastFloraBranch> toBeRemoved = new List<BallastFloraBranch>();
private void UpdateSelfDamage(float deltaTime)
{
if (selfDamageTimer <= 0)
{
bool hasRoot = false;
foreach (BallastFloraBranch branch in Branches)
{
if (branch.IsRoot)
{
hasRoot = true;
break;
}
}
if (!hasRoot)
{
Kill();
return;
}
if (!HasBrokenThrough && !CanGrowMore())
{
Branches.ForEachMod(branch =>
{
float maxHealth = branch.IsRoot ? StemHealth : BranchHealth;
float maxHealth = branch.IsRoot ? RootHealth : BranchHealth;
DamageBranch(branch, Rand.Range(1f, maxHealth), AttackType.Other);
});
}
selfDamageTimer = 1f;
}
toBeRemoved.Clear();
foreach (BallastFloraBranch branch in Branches)
{
if (branch.ParentBranch != null && (branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f))
{
float speed = MathHelper.Lerp(5.0f, 0.1f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth);
DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot);
}
if (branch.Health <= 0.0f)
{
if (branch.ClaimedItem != null)
{
RemoveClaim(branch.ClaimedItem);
branch.ClaimedItem = null;
}
branch.RemoveTimer -= deltaTime;
if (branch.RemoveTimer <= 0.0f)
{
toBeRemoved.Add(branch);
}
}
}
foreach (BallastFloraBranch branch in toBeRemoved)
{
RemoveBranch(branch);
}
selfDamageTimer -= deltaTime;
}
@@ -675,20 +736,25 @@ namespace Barotrauma.MapCreatures.Behavior
branch.CurrentHull = Hull.FindHull(GetWorldPosition() + branch.Position, Parent, true);
}
private void GenerateStem()
private void GenerateRoot()
{
BallastFloraBranch stem = new BallastFloraBranch(this, Vector2.Zero, VineTileType.Stem, FoliageConfig.EmptyConfig, FoliageConfig.EmptyConfig)
if (root != null)
{
DebugConsole.ThrowError("Error in ballast flora: tried to grow a root even though root has already been created.\n" + Environment.StackTrace);
}
root = new BallastFloraBranch(this, null, Vector2.Zero, VineTileType.Stem, FoliageConfig.EmptyConfig, FoliageConfig.EmptyConfig)
{
BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right,
GrowthStep = 1f,
Health = StemHealth,
MaxHealth = StemHealth,
MaxHealth = RootHealth,
Health = RootHealth,
IsRoot = true,
CurrentHull = Parent
};
Branches.Add(stem);
CreateBody(stem);
Branches.Add(root);
CreateBody(root);
}
public float GetGrowthSpeed(float deltaTime)
@@ -704,19 +770,19 @@ namespace Barotrauma.MapCreatures.Behavior
return deltaTime;
}
public bool TryGrowBranch(BallastFloraBranch parent, TileSide side, out List<BallastFloraBranch> result)
public bool TryGrowBranch(BallastFloraBranch parent, TileSide side, out List<BallastFloraBranch> result, bool isRootGrowth = false, Vector2? forcePosition = null)
{
result = new List<BallastFloraBranch>();
if (parent.IsSideBlocked(side)) { return false; }
if (!isRootGrowth && parent.IsSideBlocked(side)) { return false; }
Vector2 pos = parent.AdjacentPositions[side];
Vector2 pos = forcePosition ?? parent.AdjacentPositions[side];
Rectangle rect = VineTile.CreatePlantRect(pos);
if (CollidesWithWorld(rect))
if (CollidesWithWorld(rect, checkOtherBranches: !isRootGrowth))
{
parent.BlockedSides |= side;
parent.FailedGrowthAttempts++;
return false;
return false;
}
FoliageConfig flowerConfig = FoliageConfig.EmptyConfig;
@@ -732,21 +798,22 @@ namespace Barotrauma.MapCreatures.Behavior
leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, 0.5f, 1.0f);
}
BallastFloraBranch newBranch = new BallastFloraBranch(this, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect)
BallastFloraBranch newBranch = new BallastFloraBranch(this, parent, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect)
{
ID = CreateID(),
MaxHealth = BranchHealth,
Health = BranchHealth,
MaxHealth = BranchHealth
IsRootGrowth = isRootGrowth
};
SetHull(newBranch);
if (newBranch.CurrentHull == null || newBranch.CurrentHull.Submarine != Parent.Submarine)
{
parent.BlockedSides |= side;
if (!isRootGrowth) { parent.BlockedSides |= side; }
parent.FailedGrowthAttempts++;
return false;
}
}
UpdateConnections(newBranch, parent);
@@ -760,12 +827,28 @@ namespace Barotrauma.MapCreatures.Behavior
GrowthWarps--;
}
int rootGrowthCount = Branches.Count(b => b.IsRootGrowth);
if (rootGrowthCount < GetDesiredRootGrowthAmount())
{
if (root != null)
{
Vector2 rootGrowthPos = Rand.Vector(Math.Max(rootGrowthCount, 1) * Rand.Range(3.0f, 5.0f));
TryGrowBranch(root, TileSide.None, out List<BallastFloraBranch> newRootGrowth, isRootGrowth: true, forcePosition: rootGrowthPos);
}
}
#if SERVER
SendNetworkMessage(this, NetworkHeader.BranchCreate, newBranch, parent.ID);
CreateNetworkMessage(new BranchCreateEventData(newBranch, parent));
#endif
return true;
}
private int GetDesiredRootGrowthAmount()
{
if (root == null) { return 0; }
return MathHelper.Clamp(Branches.Count(b => !b.IsRootGrowth && b.Health > 0) / 20, 3, 30);
}
public bool BranchContainsTarget(BallastFloraBranch branch, Item target)
{
Rectangle worldRect = branch.Rect;
@@ -797,7 +880,7 @@ namespace Barotrauma.MapCreatures.Behavior
#if SERVER
if (!load)
{
SendNetworkMessage(this, NetworkHeader.Infect, target.ID, true, branch);
CreateNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch));
}
#endif
}
@@ -874,8 +957,6 @@ namespace Barotrauma.MapCreatures.Behavior
/// <param name="branch"></param>
private void CreateBody(BallastFloraBranch branch)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
Rectangle rect = branch.Rect;
Vector2 pos = Parent.Position + Offset + branch.Position;
@@ -894,8 +975,44 @@ namespace Barotrauma.MapCreatures.Behavior
public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null)
{
float damage = amount;
// damage is handled server side currently
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
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)
{
branch.DamageVisualizationTimer = 1.0f;
}
if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; }
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
branch.AccumulatedDamage += damage;
Anger += damage * 0.001f;
}
if (GameMain.NetworkMember != null)
{
// damage is handled server side
if (GameMain.NetworkMember.IsClient)
{
return;
}
else
{
//accumulate damage on the server's side to ensure clients get notified
if (type == AttackType.Other || type == AttackType.CutFromRoot)
{
branch.AccumulatedDamage += damage;
}
}
}
if (attacker != null && toxinsCooldown <= 0)
{
@@ -917,48 +1034,26 @@ namespace Barotrauma.MapCreatures.Behavior
StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker));
defenseCooldown = 180f;
}
defenseCooldown = 10f;
else
{
defenseCooldown = 10f;
}
}
}
branch.AccumulatedDamage += damage;
branch.Health -= damage;
if (type != AttackType.Other)
{
Anger += damage * 0.001f;
}
#if SERVER
GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage);
#endif
if (branch.Health < 0)
if (branch.Health <= 0 && type != AttackType.CutFromRoot)
{
RemoveBranch(branch);
if (branch.IsRoot) { Kill(); }
}
}
public void Remove()
{
foreach (Body body in bodies)
{
GameMain.World.Remove(body);
}
Parent.BallastFlora = null;
Branches.Clear();
foreach (Item target in ClaimedTargets)
{
target.Infector = null;
}
_entityList.Remove(this);
}
public void RemoveBranch(BallastFloraBranch branch)
{
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
@@ -969,6 +1064,21 @@ 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);
bodies.ForEachMod(body =>
{
if (body.UserData == branch)
@@ -995,11 +1105,21 @@ namespace Barotrauma.MapCreatures.Behavior
});
#if CLIENT
CreateDeathParticle(branch);
CreateDeathParticle(branch, 1.0f);
#endif
if (isClient) { return; }
int rootGrowthCount = Branches.Count(b => b.IsRootGrowth);
if (rootGrowthCount > GetDesiredRootGrowthAmount())
{
var rootGrowth = Branches.LastOrDefault(b => b.IsRootGrowth);
if (rootGrowth != null)
{
RemoveBranch(rootGrowth);
}
}
if (branch.ClaimedItem != null)
{
RemoveClaim(branch.ClaimedItem);
@@ -1013,7 +1133,7 @@ namespace Barotrauma.MapCreatures.Behavior
#if SERVER
if (!wasRemoved)
{
SendNetworkMessage(this, NetworkHeader.BranchRemove, branch);
CreateNetworkMessage(new BranchRemoveEventData(branch));
}
#endif
}
@@ -1044,14 +1164,16 @@ namespace Barotrauma.MapCreatures.Behavior
}
});
#if SERVER
SendNetworkMessage(this, NetworkHeader.Infect, item.ID, false);
CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null));
#endif
}
public void Kill()
{
Branches.ForEachMod(RemoveBranch);
Parent.BallastFlora = null;
foreach (var branch in Branches)
{
branch.DisconnectedFromRoot = true;
}
foreach (Item target in ClaimedTargets)
{
@@ -1059,6 +1181,19 @@ namespace Barotrauma.MapCreatures.Behavior
}
StateMachine?.State?.Exit();
#if SERVER
CreateNetworkMessage(new KillEventData());
#endif
}
public void Remove()
{
Kill();
Branches.ForEachMod(RemoveBranch);
Branches.Clear();
toBeRemoved.Clear();
Parent.BallastFlora = null;
// clean up leftover (can probably be removed)
foreach (Body body in bodies)
@@ -1067,8 +1202,9 @@ namespace Barotrauma.MapCreatures.Behavior
GameMain.World.Remove(body);
}
_entityList.Remove(this);
#if SERVER
SendNetworkMessage(this, NetworkHeader.Kill);
CreateNetworkMessage(new KillEventData());
#endif
}
@@ -1093,9 +1229,9 @@ namespace Barotrauma.MapCreatures.Behavior
private bool CanGrowMore() => Branches.Any(b => b.CanGrowMore());
private bool CollidesWithWorld(Rectangle rect)
private bool CollidesWithWorld(Rectangle rect, bool checkOtherBranches = true)
{
if (Branches.Any(g => g.Rect.Contains(rect))) { return true; }
if (checkOtherBranches && Branches.Any(g => g.Rect.Contains(rect))) { return true; }
Rectangle worldRect = rect;
worldRect.Location = (Parent.Position + Offset).ToPoint() + worldRect.Location;
@@ -0,0 +1,74 @@
using Barotrauma.Networking;
namespace Barotrauma.MapCreatures.Behavior
{
internal partial class BallastFloraBehavior
{
public interface IEventData : NetEntityEvent.IData
{
public NetworkHeader NetworkHeader { get; }
}
public readonly struct SpawnEventData : IEventData
{
public NetworkHeader NetworkHeader => NetworkHeader.Spawn;
}
private readonly struct KillEventData : IEventData
{
public NetworkHeader NetworkHeader => NetworkHeader.Kill;
}
private readonly struct BranchCreateEventData : IEventData
{
public NetworkHeader NetworkHeader => NetworkHeader.BranchCreate;
public readonly BallastFloraBranch NewBranch;
public readonly BallastFloraBranch Parent;
public BranchCreateEventData(BallastFloraBranch newBranch, BallastFloraBranch parent)
{
NewBranch = newBranch;
Parent = parent;
}
}
private readonly struct BranchRemoveEventData : IEventData
{
public NetworkHeader NetworkHeader => NetworkHeader.BranchRemove;
public readonly BallastFloraBranch Branch;
public BranchRemoveEventData(BallastFloraBranch branch)
{
Branch = branch;
}
}
private readonly struct BranchDamageEventData : IEventData
{
public NetworkHeader NetworkHeader => NetworkHeader.BranchDamage;
public readonly BallastFloraBranch Branch;
public BranchDamageEventData(BallastFloraBranch branch)
{
Branch = branch;
}
}
private readonly struct InfectEventData : IEventData
{
public enum InfectState { Yes, No }
public NetworkHeader NetworkHeader => NetworkHeader.Infect;
public readonly Item Item;
public readonly InfectState Infect;
public readonly BallastFloraBranch Infector;
public InfectEventData(Item item, InfectState infect, BallastFloraBranch infector)
{
Item = item;
Infect = infect;
Infector = infector;
}
}
}
}
@@ -4,125 +4,28 @@ using System.Xml.Linq;
namespace Barotrauma
{
class BallastFloraPrefab : IPrefab, IDisposable
class BallastFloraPrefab : Prefab
{
public string OriginalName { get; }
public string Identifier { get; }
public string FilePath { get; }
public XElement Element { get; }
public ContentPackage ContentPackage { get; private set; }
public LocalizedString DisplayName { get; }
public ContentXElement Element { get; }
public bool Disposed;
public static readonly PrefabCollection<BallastFloraPrefab> Prefabs = new PrefabCollection<BallastFloraPrefab>();
private BallastFloraPrefab(XElement element, string filePath, bool isOverride)
public BallastFloraPrefab(ContentXElement element, BallastFloraFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
Identifier = element.GetAttributeString("identifier", "");
OriginalName = element.GetAttributeString("name", "");
DisplayName = TextManager.Get(Identifier).Fallback(OriginalName);
Element = element;
FilePath = filePath;
Prefabs.Add(this, isOverride);
}
public static BallastFloraPrefab Find(string idenfitier)
public static BallastFloraPrefab Find(Identifier identifier)
{
return !string.IsNullOrWhiteSpace(idenfitier) ? Prefabs.Find(prefab => prefab.Identifier == idenfitier) : null;
return Prefabs.ContainsKey(identifier) ? Prefabs[identifier] : null;
}
public static void LoadAll(IEnumerable files)
{
DebugConsole.Log("Loading map creature prefabs: ");
foreach (ContentFile file in files) { LoadFromFile(file); }
}
public static void LoadFromFile(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
var rootElement = doc?.Root;
if (rootElement == null) { return; }
switch (rootElement.Name.ToString().ToLowerInvariant())
{
case "ballastflorabehavior":
{
new BallastFloraPrefab(rootElement, file.Path, false) { ContentPackage = file.ContentPackage };
break;
}
case "ballastflorabehaviors":
{
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
XElement upgradeElement = element.GetChildElement("mapcreature");
if (upgradeElement != null)
{
new BallastFloraPrefab(upgradeElement, file.Path, true) { ContentPackage = file.ContentPackage };
}
else
{
DebugConsole.ThrowError($"Cannot find a map creature element from the children of the override element defined in {file.Path}");
}
}
else
{
if (element.Name.ToString().Equals("mapcreature", StringComparison.OrdinalIgnoreCase))
{
new BallastFloraPrefab(element, file.Path, false) { ContentPackage = file.ContentPackage };
}
}
}
break;
}
case "override":
{
XElement mapCreatures = rootElement.GetChildElement("ballastflorabehaviors");
if (mapCreatures != null)
{
foreach (XElement element in mapCreatures.Elements())
{
new BallastFloraPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage };
}
}
foreach (XElement element in rootElement.GetChildElements("ballastflorabehavior"))
{
new BallastFloraPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage };
}
break;
}
default:
{
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}\n " +
"Valid elements are: \"MapCreature\", \"MapCreatures\" and \"Override\".");
break;
}
}
}
private void Dispose(bool disposing)
{
if (!Disposed)
{
if (disposing)
{
Prefabs.Remove(this);
}
}
Disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public override void Dispose() { }
}
}
@@ -17,6 +17,8 @@ namespace Barotrauma.MapCreatures.Behavior
{
lastState = State;
State?.Exit();
State = null;
newState.Enter();
State = newState;
}
@@ -35,11 +37,9 @@ namespace Barotrauma.MapCreatures.Behavior
{
case ExitState.Running:
break;
case ExitState.ReturnLast when lastState != null && lastState.GetState() == ExitState.Running:
EnterState(lastState);
break;
default:
EnterState(new GrowIdleState(parent));
break;
@@ -18,7 +18,7 @@ namespace Barotrauma.MapCreatures.Behavior
private bool tryDrown;
private readonly Character attacker;
public DefendWithPumpState(BallastFloraBranch branch, List<Item> items, Character attacker)
public DefendWithPumpState(BallastFloraBranch branch, IEnumerable<Item> items, Character attacker)
{
targetBranch = branch;
this.attacker = attacker;
@@ -59,11 +59,14 @@ namespace Barotrauma.MapCreatures.Behavior
protected virtual void Grow()
{
List<BallastFloraBranch> newTiles = GrowRandomly();
List<BallastFloraBranch> newBranches = GrowRandomly();
#if DEBUG
Behavior.debugSearchLines.Clear();
#endif
if (newTiles.Any(TryScanTargets)) { return; }
foreach (var branch in newBranches)
{
TryScanTargets(branch);
}
}
public void UpdateIgnoredTargets()
@@ -85,23 +88,20 @@ namespace Barotrauma.MapCreatures.Behavior
private List<BallastFloraBranch> GrowRandomly()
{
List<BallastFloraBranch> newBranches = new List<BallastFloraBranch>();
List<BallastFloraBranch> newList = new List<BallastFloraBranch>(Behavior.Branches);
foreach (BallastFloraBranch branch in newList)
{
if (branch.FailedGrowthAttempts > 8 || !branch.CanGrowMore()) { continue; }
List<BallastFloraBranch> availableBranches = Behavior.Branches.Where(b => !b.DisconnectedFromRoot && b.FailedGrowthAttempts <= 8 && b.CanGrowMore()).ToList();
if (availableBranches.Count == 0) { return availableBranches; }
if (Rand.Range(0, Behavior.Branches.Count(tile => tile.CanGrowMore())) != 0) { continue; }
//prefer growing from the branches furthest from the root (ones with the largest branch depth)
var branch = ToolBox.SelectWeightedRandom(availableBranches, b => (float)b.BranchDepth, Rand.RandSync.Unsynced);
TileSide side = branch.GetRandomFreeSide();
TileSide side = branch.GetRandomFreeSide();
if (side == TileSide.None) { return availableBranches; }
if (side == TileSide.None) { continue; }
Behavior.TryGrowBranch(branch, side, out List<BallastFloraBranch> result);
availableBranches.Clear();
availableBranches.Add(branch);
Behavior.TryGrowBranch(branch, side, out List<BallastFloraBranch> result);
newBranches.AddRange(result);
}
return newBranches;
return availableBranches;
}
private Item? ScanForTargets(VineTile branch)
@@ -117,22 +117,22 @@ namespace Barotrauma.MapCreatures.Behavior
int highestPriority = 0;
Item? currentItem = null;
foreach (Item item in Item.ItemList.Where(it => !Behavior.ClaimedTargets.Contains(it)))
foreach (Item item in Item.ItemList)
{
if (item.Submarine != parent.Submarine || Vector2.DistanceSquared(worldPos, item.WorldPosition) > Behavior.Sight * Behavior.Sight) { continue; }
if (Behavior.ClaimedTargets.Contains(item)) { continue; }
if (Behavior.IgnoredTargets.ContainsKey(item)) { continue; }
int priority = 0;
foreach (BallastFloraBehavior.AITarget target in Behavior.Targets)
{
if (!target.Matches(item) || target.Priority <= highestPriority) { continue; }
if (target.Priority <= highestPriority || !target.Matches(item)) { continue; }
priority = target.Priority;
break;
}
if (priority == 0) { continue; }
if (item.Submarine != parent.Submarine || Vector2.Distance(worldPos, item.WorldPosition) > Behavior.Sight) { continue; }
Vector2 itemSimPos = ConvertUnits.ToSimUnits(item.Position);
#if DEBUG
@@ -156,6 +156,7 @@ namespace Barotrauma.MapCreatures.Behavior
{
foreach (BallastFloraBranch existingBranch in Behavior.Branches)
{
if (existingBranch.Health <= 0 || existingBranch.IsRootGrowth) { continue; }
if (Behavior.BranchContainsTarget(existingBranch, currentItem))
{
Behavior.ClaimTarget(currentItem, existingBranch);
@@ -53,7 +53,7 @@ namespace Barotrauma.MapCreatures.Behavior
List<BallastFloraBranch> newList = new List<BallastFloraBranch>(TargetBranches);
foreach (BallastFloraBranch branch in newList)
{
if (branch.FailedGrowthAttempts > 8 || !branch.CanGrowMore()) { continue; }
if (branch.FailedGrowthAttempts > 8 || branch.DisconnectedFromRoot || !branch.CanGrowMore()) { continue; }
// Get what side gets us closest to the target
TileSide side = GetClosestSide(branch, Target.WorldPosition);
@@ -1,9 +1,11 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Barotrauma.IO;
using System.Linq;
using System.Text;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -70,6 +72,14 @@ namespace Barotrauma
public double SpawnTime => spawnTime;
private readonly double spawnTime;
private static UInt64 creationCounter = 0;
private readonly static object creationCounterMutex = new object();
public readonly string CreationStackTrace;
public readonly UInt64 CreationIndex;
public string ErrorLine
=> $"- {ID}: {this} ({Submarine?.Info?.Name ?? "[null]"} {Submarine?.ID ?? 0}) {CreationStackTrace}";
public Entity(Submarine submarine, ushort id)
{
this.Submarine = submarine;
@@ -84,6 +94,31 @@ namespace Barotrauma
}
dictionary.Add(ID, this);
CreationStackTrace = "";
#if DEBUG
var st = new StackTrace(skipFrames: 2, fNeedFileInfo: true);
var frames = st.GetFrames();
int frameCount = 0;
foreach (var frame in frames)
{
string fileName = frame.GetFileName();
if ((fileName?.Contains("BarotraumaClient") ?? false) || (fileName?.Contains("BarotraumaServer") ?? false)) { break; }
fileName = Path.GetFileNameWithoutExtension(fileName);
int fileLineNumber = frame.GetFileLineNumber();
if (fileName.IsNullOrEmpty() || fileLineNumber <= 0) { continue; }
CreationStackTrace += $"{fileName}@{fileLineNumber}; ";
}
#endif
#warning TODO: consider removing this mutex, entity creation probably shouldn't be multithreaded
lock (creationCounterMutex)
{
CreationIndex = creationCounter;
creationCounter++;
}
}
protected virtual ushort DetermineID(ushort id, Submarine submarine)
@@ -59,10 +59,10 @@ namespace Barotrauma
smoke = true;
flames = true;
underwaterBubble = true;
ignoreFireEffectsForTags = new string[0];
ignoreFireEffectsForTags = Array.Empty<string>();
}
public Explosion(XElement element, string parentDebugName)
public Explosion(ContentXElement element, string parentDebugName)
{
Attack = new Attack(element, parentDebugName + ", Explosion");
@@ -80,7 +80,7 @@ namespace Barotrauma
playTinnitus = element.GetAttributeBool("playtinnitus", showEffects);
applyFireEffects = element.GetAttributeBool("applyfireeffects", flames && showEffects);
ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", new string[0], convertToLowerInvariant: true);
ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", Array.Empty<string>(), convertToLowerInvariant: true);
ignoreCover = element.GetAttributeBool("ignorecover", false);
onlyInside = element.GetAttributeBool("onlyinside", false);
@@ -88,7 +88,7 @@ namespace Barotrauma
flash = element.GetAttributeBool("flash", showEffects);
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
if (element.GetAttribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow);
EmpStrength = element.GetAttributeFloat("empstrength", 0.0f);
@@ -133,6 +133,7 @@ namespace Barotrauma
{
displayRange *= 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius);
Attack.DamageMultiplier *= 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionDamage);
Attack.SourceItem ??= sourceItem;
}
Vector2 cameraPos = GameMain.GameScreen.Cam.Position;
@@ -237,9 +238,9 @@ namespace Barotrauma
if (!fireProof)
{
item.ApplyStatusEffects(ActionType.OnFire, 1.0f);
if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (item.Condition <= 0.0f && GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire });
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFire));
}
}
}
@@ -337,11 +338,17 @@ namespace Barotrauma
}
}
AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker);
if (attackData.Afflictions != null)
{
modifiedAfflictions.AddRange(attackData.Afflictions);
}
//use a position slightly from the limb's position towards the explosion
//ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods
Vector2 dir = worldPosition - limb.WorldPosition;
Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f;
AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier);
AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier);
damages.Add(limb, attackResult.Damage);
if (attack.StatusEffects != null && attack.StatusEffects.Any())
@@ -482,7 +489,7 @@ namespace Barotrauma
{
List<BallastFloraBehavior> ballastFlorae = new List<BallastFloraBehavior>();
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
if (hull.BallastFlora != null) { ballastFlorae.Add(hull.BallastFlora); }
}
@@ -363,9 +363,9 @@ namespace Barotrauma
if (item.Position.Y < position.Y - size.Y || item.Position.Y > hull.Rect.Y) { continue; }
item.ApplyStatusEffects(ActionType.OnFire, deltaTime);
if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (item.Condition <= 0.0f && GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire });
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFire));
}
}
}
@@ -137,7 +137,7 @@ namespace Barotrauma
{ }
public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, ushort id = Entity.NullEntityID)
: base(MapEntityPrefab.Find(null, "gap"), submarine, id)
: base(MapEntityPrefab.FindByIdentifier("gap".ToIdentifier()), submarine, id)
{
this.rect = rect;
flowForce = Vector2.Zero;
@@ -249,16 +249,17 @@ namespace Barotrauma
}
linkedTo.Clear();
int tolerance = 1;
Vector2[] searchPos = new Vector2[2];
if (IsHorizontal)
{
searchPos[0] = new Vector2(rect.X, rect.Y - rect.Height / 2);
searchPos[1] = new Vector2(rect.Right, rect.Y - rect.Height / 2);
searchPos[0] = new Vector2(rect.X - tolerance, rect.Y - rect.Height / 2);
searchPos[1] = new Vector2(rect.Right + tolerance, rect.Y - rect.Height / 2);
}
else
{
searchPos[0] = new Vector2(rect.Center.X, rect.Y);
searchPos[1] = new Vector2(rect.Center.X, rect.Y - rect.Height);
searchPos[0] = new Vector2(rect.Center.X, rect.Y + tolerance);
searchPos[1] = new Vector2(rect.Center.X, rect.Y - rect.Height - tolerance);
}
for (int i = 0; i < 2; i++)
@@ -712,7 +713,7 @@ namespace Barotrauma
base.ShallowRemove();
GapList.Remove(this);
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
hull.ConnectedGaps.Remove(this);
}
@@ -723,7 +724,7 @@ namespace Barotrauma
base.Remove();
GapList.Remove(this);
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
hull.ConnectedGaps.Remove(this);
}
@@ -740,11 +741,11 @@ namespace Barotrauma
if (!DisableHullRechecks) FindHulls();
}
public static Gap Load(XElement element, Submarine submarine, IdRemap idRemap)
public static Gap Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect = Rectangle.Empty;
if (element.Attribute("rect") != null)
if (element.GetAttribute("rect") != null)
{
rect = element.GetAttributeRect("rect", Rectangle.Empty);
}
@@ -752,15 +753,15 @@ namespace Barotrauma
{
//backwards compatibility
rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
int.Parse(element.Attribute("width").Value),
int.Parse(element.Attribute("height").Value));
int.Parse(element.GetAttribute("x").Value),
int.Parse(element.GetAttribute("y").Value),
int.Parse(element.GetAttribute("width").Value),
int.Parse(element.GetAttribute("height").Value));
}
bool isHorizontal = rect.Height > rect.Width;
var horizontalAttribute = element.Attribute("horizontal");
var horizontalAttribute = element.GetAttribute("horizontal");
if (horizontalAttribute != null)
{
isHorizontal = horizontalAttribute.Value.ToString() == "true";
@@ -23,6 +23,10 @@ namespace Barotrauma
public readonly Vector2 Noise;
public readonly Color DirtColor;
#if CLIENT
public Sprite GrimeSprite;
#endif
public float ColorStrength
{
get;
@@ -51,6 +55,9 @@ namespace Barotrauma
PerlinNoise.GetPerlin(Rect.Y / 1000.0f + 0.5f, Rect.X / 1000.0f + 0.5f));
Color = DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X);
#if CLIENT
GrimeSprite = DecalManager.GrimeSprites[$"{nameof(GrimeSprite)}{index % DecalManager.GrimeSpriteCount}"].Sprite;
#endif
}
public BackgroundSection(Rectangle rect, ushort index, float colorStrength, Color color, ushort rowIndex)
@@ -68,6 +75,9 @@ namespace Barotrauma
PerlinNoise.GetPerlin(Rect.Y / 1000.0f + 0.5f, Rect.X / 1000.0f + 0.5f));
DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X);
#if CLIENT
GrimeSprite = DecalManager.GrimeSprites[$"{nameof(GrimeSprite)}{index % DecalManager.GrimeSpriteCount}"].Sprite;
#endif
}
public bool SetColor(Color color)
@@ -101,8 +111,8 @@ namespace Barotrauma
partial class Hull : MapEntity, ISerializableEntity, IServerSerializable
{
public static List<Hull> hullList = new List<Hull>();
public static List<EntityGrid> EntityGrids { get; } = new List<EntityGrid>();
public readonly static List<Hull> HullList = new List<Hull>();
public readonly static List<EntityGrid> EntityGrids = new List<EntityGrid>();
public static bool ShowHulls = true;
@@ -124,8 +134,8 @@ namespace Barotrauma
public const int BackgroundSectionsPerNetworkEvent = 16;
public readonly Dictionary<string, SerializableProperty> properties;
public Dictionary<string, SerializableProperty> SerializableProperties
public readonly Dictionary<Identifier, SerializableProperty> properties;
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
get { return properties; }
}
@@ -155,32 +165,23 @@ namespace Barotrauma
public readonly List<Gap> ConnectedGaps = new List<Gap>();
public override string Name
{
get
{
return "Hull";
}
}
public override string Name => "Hull";
public string DisplayName
public LocalizedString DisplayName
{
get;
private set;
}
private readonly HashSet<string> moduleTags = new HashSet<string>();
private readonly HashSet<Identifier> moduleTags = new HashSet<Identifier>();
/// <summary>
/// Inherited flags from outpost generation.
/// </summary>
public IEnumerable<string> OutpostModuleTags
{
get { return moduleTags; }
}
public IEnumerable<Identifier> OutpostModuleTags => moduleTags;
private string roomName;
[Editable, Serialize("", true, translationTextTag: "RoomName.")]
[Editable, Serialize("", IsPropertySaveable.Yes, translationTextTag: "RoomName.")]
public string RoomName
{
get { return roomName; }
@@ -188,7 +189,7 @@ namespace Barotrauma
{
if (roomName == value) { return; }
roomName = value;
DisplayName = TextManager.Get(roomName, returnNull: true) ?? roomName;
DisplayName = TextManager.Get(roomName).Fallback(roomName);
if (!IsWetRoom && ForceAsWetRoom)
{
IsWetRoom = true;
@@ -200,7 +201,7 @@ namespace Barotrauma
private Color ambientLight;
[Editable, Serialize("0,0,0,0", true)]
[Editable, Serialize("0,0,0,0", IsPropertySaveable.Yes)]
public Color AmbientLight
{
get { return ambientLight; }
@@ -283,38 +284,22 @@ namespace Barotrauma
get { return Submarine == null ? surface : surface + Submarine.Position.Y; }
}
private float dirtiedVolume = 0.0f;
public float WaterVolume
{
get { return waterVolume; }
set
{
if (!MathUtils.IsValid(value)) return;
if (!MathUtils.IsValid(value)) { return; }
waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress);
if (waterVolume < Volume) { Pressure = rect.Y - rect.Height + waterVolume / rect.Width; }
if (waterVolume > 0.0f)
{
update = true;
if (BackgroundSections != null)
{
float volumeMultiplier = Math.Clamp(waterVolume / Volume, 0f, 1f);
if (Math.Abs(volumeMultiplier - dirtiedVolume) > 0.075f)
{
RefreshSubmergedSections(new Rectangle(new Point(0, -rect.Height), new Point(rect.Width, (int)(rect.Height * volumeMultiplier))));
dirtiedVolume = volumeMultiplier;
}
}
}
else
{
submergedSections.Clear();
dirtiedVolume = 0.0f;
}
}
}
[Serialize(100000.0f, true)]
[Serialize(100000.0f, IsPropertySaveable.Yes)]
public float Oxygen
{
get { return oxygen; }
@@ -329,10 +314,11 @@ namespace Barotrauma
roomName != null && (
roomName.Contains("ballast", StringComparison.OrdinalIgnoreCase) ||
roomName.Contains("bilge", StringComparison.OrdinalIgnoreCase) ||
roomName.Contains("airlock", StringComparison.OrdinalIgnoreCase));
roomName.Contains("airlock", StringComparison.OrdinalIgnoreCase) ||
roomName.Contains("dockingport", StringComparison.OrdinalIgnoreCase));
private bool isWetRoom;
[Editable, Serialize(false, true, description: "It's normal for this hull to be filled with water. If the room name contains 'ballast', 'bilge', or 'airlock', you can't disable this setting.")]
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "It's normal for this hull to be filled with water. If the room name contains 'ballast', 'bilge', or 'airlock', you can't disable this setting.")]
public bool IsWetRoom
{
get { return isWetRoom; }
@@ -347,7 +333,7 @@ namespace Barotrauma
}
private bool avoidStaying;
[Editable, Serialize(false, true, description: "Bots avoid staying here, but they are still allowed to access the room when needed and go through it. Forced true for wet rooms.")]
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Bots avoid staying here, but they are still allowed to access the room when needed and go through it. Forced true for wet rooms.")]
public bool AvoidStaying
{
get { return avoidStaying || IsWetRoom; }
@@ -399,8 +385,6 @@ namespace Barotrauma
private readonly HashSet<int> pendingSectionUpdates = new HashSet<int>();
private readonly List<BackgroundSection> submergedSections = new List<BackgroundSection>();
public int xBackgroundMax, yBackgroundMax;
public bool SupportsPaintedColors
@@ -469,7 +453,7 @@ namespace Barotrauma
};
}
hullList.Add(this);
HullList.Add(this);
if (submarine == null || !submarine.Loading)
{
@@ -488,11 +472,11 @@ namespace Barotrauma
public static Rectangle GetBorders()
{
if (!hullList.Any()) return Rectangle.Empty;
if (!HullList.Any()) return Rectangle.Empty;
Rectangle rect = hullList[0].rect;
Rectangle rect = HullList[0].rect;
foreach (Hull hull in hullList)
foreach (Hull hull in HullList)
{
if (hull.Rect.X < rect.X)
{
@@ -516,12 +500,15 @@ namespace Barotrauma
public override MapEntity Clone()
{
var clone = new Hull(MapEntityPrefab.Find(null, "hull"), rect, Submarine);
foreach (KeyValuePair<string, SerializableProperty> property in SerializableProperties)
var clone = new Hull(MapEntityPrefab.FindByIdentifier("hull".ToIdentifier()), rect, Submarine);
foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
{
if (!property.Value.Attributes.OfType<Editable>().Any()) { continue; }
clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
}
#if CLIENT
clone.lastAmbientLightEditTime = 0.0;
#endif
return clone;
}
@@ -536,17 +523,17 @@ namespace Barotrauma
{
var newGrid = new EntityGrid(submarine, 200.0f);
EntityGrids.Add(newGrid);
foreach (Hull hull in hullList)
foreach (Hull hull in HullList)
{
if (hull.Submarine == submarine && !hull.IdFreed) { newGrid.InsertEntity(hull); }
}
return newGrid;
}
public void SetModuleTags(IEnumerable<string> tags)
public void SetModuleTags(IEnumerable<Identifier> tags)
{
moduleTags.Clear();
foreach (string tag in tags)
foreach (Identifier tag in tags)
{
moduleTags.Add(tag);
}
@@ -630,7 +617,7 @@ namespace Barotrauma
public override void ShallowRemove()
{
base.Remove();
hullList.Remove(this);
HullList.Remove(this);
if (Submarine == null || (!Submarine.Loading && !Submarine.Unloading))
{
@@ -659,7 +646,7 @@ namespace Barotrauma
public override void Remove()
{
base.Remove();
hullList.Remove(this);
HullList.Remove(this);
BallastFlora?.Remove();
if (Submarine != null && !Submarine.Loading && !Submarine.Unloading)
@@ -669,7 +656,6 @@ namespace Barotrauma
}
BackgroundSections?.Clear();
submergedSections?.Clear();
List<FireSource> fireSourcesToRemove = new List<FireSource>(FireSources);
foreach (FireSource fireSource in fireSourcesToRemove)
@@ -712,7 +698,7 @@ namespace Barotrauma
return null;
}
var decal = GameMain.DecalManager.Prefabs.Find(p => p.UIntIdentifier == decalId);
var decal = DecalManager.Prefabs.Find(p => p.UintIdentifier == decalId);
if (decal == null)
{
DebugConsole.ThrowError($"Could not find a decal prefab with the UInt identifier {decalId}!");
@@ -732,12 +718,12 @@ namespace Barotrauma
if (decals.Count >= MaxDecalsPerHull) { return null; }
var decal = GameMain.DecalManager.CreateDecal(decalName, scale, worldPosition, this, spriteIndex);
var decal = DecalManager.CreateDecal(decalName, scale, worldPosition, this, spriteIndex);
if (decal != null)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { false });
GameMain.NetworkMember.CreateEntityEvent(this, new DecalEventData());
}
decals.Add(decal);
}
@@ -745,6 +731,96 @@ namespace Barotrauma
return decal;
}
#region Shared network write
private void SharedStatusWrite(IWriteMessage msg)
{
msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8);
msg.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16);
for (int i = 0; i < Math.Min(FireSources.Count, 16); i++)
{
var fireSource = FireSources[i];
Vector2 normalizedPos = new Vector2(
(fireSource.Position.X - rect.X) / rect.Width,
(fireSource.Position.Y - (rect.Y - rect.Height)) / rect.Height);
msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.X, 0.0f, 1.0f), 0.0f, 1.0f, 8);
msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.Y, 0.0f, 1.0f), 0.0f, 1.0f, 8);
msg.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8);
}
}
private void SharedBackgroundSectionsWrite(IWriteMessage msg, in BackgroundSectionsEventData backgroundSectionsEventData)
{
int sectorToUpdate = backgroundSectionsEventData.SectorStartIndex;
int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent;
int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1);
msg.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1);
for (int i = start; i < end; i++)
{
msg.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8);
msg.Write(BackgroundSections[i].Color.PackedValue);
}
}
#endregion
#region Shared network read
public readonly struct NetworkFireSource
{
public readonly Vector2 Position;
public readonly float Size;
public NetworkFireSource(Hull hull, Vector2 normalizedPosition, float normalizedSize)
{
Position = hull.Rect.Location.ToVector2()
+ new Vector2(0, -hull.Rect.Height)
+ normalizedPosition * hull.Rect.Size.ToVector2();
Size = normalizedSize * hull.Rect.Width;
}
}
private void SharedStatusRead(IReadMessage msg, out float newWaterVolume, out NetworkFireSource[] newFireSources)
{
newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume;
int fireSourceCount = msg.ReadRangedInteger(0, 16);
newFireSources = new NetworkFireSource[fireSourceCount];
for (int i = 0; i < fireSourceCount; i++)
{
float x = MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f);
float y = MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f);
float size = msg.ReadRangedSingle(0.0f, 1.0f, 8);
newFireSources[i] = new NetworkFireSource(this, new Vector2(x, y), size);
}
}
private readonly struct BackgroundSectionNetworkUpdate
{
public readonly int SectionIndex;
public readonly Color Color;
public readonly float ColorStrength;
public BackgroundSectionNetworkUpdate(int sectionIndex, Color color, float colorStrength)
{
SectionIndex = sectionIndex;
Color = color;
ColorStrength = colorStrength;
}
}
private void SharedBackgroundSectionRead(IReadMessage msg, Action<BackgroundSectionNetworkUpdate> action, out int sectorToUpdate)
{
sectorToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1);
int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent;
int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1);
for (int i = start; i < end; i++)
{
float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8);
Color color = new Color(msg.ReadUInt32());
action(new BackgroundSectionNetworkUpdate(i, color, colorStrength));
}
}
#endregion
public override void Update(float deltaTime, Camera cam)
{
@@ -888,12 +964,6 @@ namespace Barotrauma
}
}
//0.016 increase every ~2000 frames = reaches full dirtiness in ~35 minutes
if (submergedSections.Count > 0 && Submarine != null && Submarine.Info.Type == SubmarineType.Player && Rand.Int(2000) == 1)
{
DirtySections(submergedSections, deltaTime);
}
if (waterVolume < Volume)
{
LethalPressure -= 10.0f * deltaTime;
@@ -1110,12 +1180,12 @@ namespace Barotrauma
/// </summary>
public static Hull FindHullUnoptimized(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
{
if (guess != null && hullList.Contains(guess))
if (guess != null && HullList.Contains(guess))
{
if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess;
}
foreach (Hull hull in hullList)
foreach (Hull hull in HullList)
{
if (Submarine.RectContains(useWorldCoordinates ? hull.WorldRect : hull.rect, position, inclusive)) return hull;
}
@@ -1135,15 +1205,15 @@ namespace Barotrauma
else
{
Hull h = c.CurrentHull;
hullList.ForEach(j => j.Visible = false);
HullList.ForEach(j => j.Visible = false);
List<Hull> visibleHulls;
if (h == null || c.Submarine == null)
{
visibleHulls = hullList.FindAll(j => j.CanSeeOther(null, false));
visibleHulls = HullList.FindAll(j => j.CanSeeOther(null, false));
}
else
{
visibleHulls = hullList.FindAll(j => h.CanSeeOther(j, true));
visibleHulls = HullList.FindAll(j => h.CanSeeOther(j, true));
}
visibleHulls.ForEach(j => j.Visible = true);
foreach (Item it in Item.ItemList)
@@ -1164,7 +1234,7 @@ namespace Barotrauma
foreach (Gap g in ConnectedGaps)
{
if (g.ConnectedWall != null && g.ConnectedWall.CastShadow) continue;
List<Hull> otherHulls = hullList.FindAll(h => h.ConnectedGaps.Contains(g) && h != this);
List<Hull> otherHulls = HullList.FindAll(h => h.ConnectedGaps.Contains(g) && h != this);
retVal = otherHulls.Any(h => h == other);
if (!retVal && allowIndirect) retVal = otherHulls.Any(h => h.CanSeeOther(other, false));
if (retVal) return true;
@@ -1174,7 +1244,7 @@ namespace Barotrauma
{
foreach (Gap g in ConnectedGaps)
{
if (g.ConnectedDoor != null && !hullList.Any(h => h.ConnectedGaps.Contains(g) && h != this)) return true;
if (g.ConnectedDoor != null && !HullList.Any(h => h.ConnectedGaps.Contains(g) && h != this)) return true;
}
List<MapEntity> structures = mapEntityList.FindAll(me => me is Structure && me.Rect.Intersects(Rect));
return structures.Any(st => !(st as Structure).CastShadow);
@@ -1209,7 +1279,7 @@ namespace Barotrauma
if (moduleFlags != null && moduleFlags.Any() &&
(Submarine.Info.Type == SubmarineType.OutpostModule || Submarine.Info.Type == SubmarineType.Outpost))
{
if (moduleFlags.Contains("airlock") &&
if (moduleFlags.Contains("airlock".ToIdentifier()) &&
ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null))
{
return "RoomName.Airlock";
@@ -1313,7 +1383,7 @@ namespace Barotrauma
public static Hull GetCleanTarget(Vector2 worldPosition)
{
foreach (Hull hull in hullList)
foreach (Hull hull in HullList)
{
Rectangle worldRect = hull.WorldRect;
if (worldPosition.X < worldRect.X || worldPosition.X > worldRect.Right) { continue; }
@@ -1366,17 +1436,6 @@ namespace Barotrauma
}
}
public void RefreshSubmergedSections(Rectangle waterArea)
{
if (BackgroundSections == null) { return; }
submergedSections.Clear();
foreach (var section in GetBackgroundSectionsViaContaining(waterArea))
{
submergedSections.Add(section);
}
}
public bool DoesSectionMatch(int index, int row)
{
return index >= 0 && row >= 0 && BackgroundSections.Count > index && BackgroundSections[index] != null && BackgroundSections[index].RowIndex == row;
@@ -1443,15 +1502,6 @@ namespace Barotrauma
}
}
public void DirtySections(List<BackgroundSection> sections, float dirtyVal)
{
if (sections == null) { return; }
for (int i = 0; i < sections.Count; i++)
{
IncreaseSectionColorOrStrength(sections[i], sections[i].DirtColor, dirtyVal, false, false);
}
}
public void CleanSection(BackgroundSection section, float cleanVal, bool updateRequired)
{
bool decalsCleaned = false;
@@ -1476,10 +1526,10 @@ namespace Barotrauma
}
#endregion
public static Hull Load(XElement element, Submarine submarine, IdRemap idRemap)
public static Hull Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect;
if (element.Attribute("rect") != null)
if (element.GetAttribute("rect") != null)
{
rect = element.GetAttributeRect("rect", Rectangle.Empty);
}
@@ -1487,10 +1537,10 @@ namespace Barotrauma
{
//backwards compatibility
rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
int.Parse(element.Attribute("width").Value),
int.Parse(element.Attribute("height").Value));
int.Parse(element.GetAttribute("x").Value),
int.Parse(element.GetAttribute("y").Value),
int.Parse(element.GetAttribute("width").Value),
int.Parse(element.GetAttribute("height").Value));
}
var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine, idRemap.GetOffsetId(element))
@@ -1507,7 +1557,7 @@ namespace Barotrauma
hull.OriginalAmbientLight = XMLExtensions.ParseColor(originalAmbientLight, false);
}
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -1525,7 +1575,7 @@ namespace Barotrauma
}
break;
case "ballastflorabehavior":
string identifier = subElement.GetAttributeString("identifier", string.Empty);
Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
BallastFloraPrefab prefab = BallastFloraPrefab.Find(identifier);
if (prefab != null)
{
@@ -1554,7 +1604,7 @@ namespace Barotrauma
}
SerializableProperty.DeserializeProperties(hull, element);
if (element.Attribute("oxygen") == null) { hull.Oxygen = hull.Volume; }
if (element.GetAttribute("oxygen") == null) { hull.Oxygen = hull.Volume; }
return hull;
}
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.MapCreatures.Behavior;
using Barotrauma.Networking;
namespace Barotrauma
{
partial class Hull
{
[Flags]
public enum EventType
{
Status = 0,
Decal = 1,
BackgroundSections = 2,
BallastFlora = 3,
MinValue = 0,
MaxValue = 3
}
public interface IEventData : NetEntityEvent.IData
{
public EventType EventType { get; }
}
private readonly struct StatusEventData : IEventData
{
public EventType EventType => EventType.Status;
}
private readonly struct DecalEventData : IEventData
{
public EventType EventType => EventType.Decal;
public readonly Decal Decal;
public DecalEventData(Decal decal)
{
Decal = decal;
}
}
private readonly struct BackgroundSectionsEventData : IEventData
{
public EventType EventType => EventType.BackgroundSections;
public readonly int SectorStartIndex;
public BackgroundSectionsEventData(int sectorStartIndex)
{
SectorStartIndex = sectorStartIndex;
}
}
public readonly struct BallastFloraEventData : IEventData
{
public EventType EventType => EventType.BallastFlora;
public readonly BallastFloraBehavior Behavior;
public readonly BallastFloraBehavior.IEventData SubEventData;
public BallastFloraEventData(BallastFloraBehavior behavior, BallastFloraBehavior.IEventData subEventData)
{
Behavior = behavior;
SubEventData = subEventData;
}
}
}
}
@@ -1,24 +1,31 @@
using Microsoft.Xna.Framework;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
interface IDamageable
{
Vector2 SimPosition
{
get;
}
Vector2 WorldPosition
{
get;
}
float Health
{
get;
}
Vector2 SimPosition { get; }
Vector2 WorldPosition { get; }
float Health { get; }
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound=true);
public readonly struct AttackEventData
{
public readonly ISpatialEntity Attacker;
public readonly IDamageable TargetEntity;
public readonly Limb TargetLimb;
public readonly Vector2 AttackSimPosition;
public AttackEventData(ISpatialEntity attacker, IDamageable targetEntity, Limb targetLimb, Vector2 attackSimPosition)
{
Attacker = attacker;
TargetEntity = targetEntity;
TargetLimb = targetLimb;
AttackSimPosition = attackSimPosition;
}
}
}
}
@@ -5,64 +5,56 @@ using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Immutable;
using System.Net;
namespace Barotrauma
{
partial class ItemAssemblyPrefab : MapEntityPrefab
{
private readonly string name;
public override string Name { get { return name; } }
public static readonly PrefabCollection<ItemAssemblyPrefab> Prefabs = new PrefabCollection<ItemAssemblyPrefab>();
public static readonly string VanillaSaveFolder = Path.Combine("Content", "Items", "Assemblies");
public static readonly string SaveFolder = "ItemAssemblies";
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
private readonly XElement configElement;
public List<Pair<MapEntityPrefab, Rectangle>> DisplayEntities
public readonly ImmutableArray<(Identifier Identifier, Rectangle Rect)> DisplayEntities;
public readonly Rectangle Bounds;
public override LocalizedString Name { get; }
public override Sprite Sprite => null;
public override string OriginalName => Name.Value;
public override ImmutableHashSet<Identifier> Tags { get; }
public override ImmutableHashSet<Identifier> AllowedLinks => null;
public override MapEntityCategory Category => MapEntityCategory.ItemAssembly;
public override ImmutableHashSet<string> Aliases => null;
protected override Identifier DetermineIdentifier(XElement element)
{
get;
private set;
return element.GetAttributeIdentifier("identifier", element.GetAttributeIdentifier("name", ""));
}
public Rectangle Bounds;
public ItemAssemblyPrefab(string filePath, bool allowOverwrite = false)
public ItemAssemblyPrefab(ContentXElement element, ItemAssemblyFile file) : base(element, file)
{
FilePath = filePath;
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null) { return; }
XElement element = doc.Root;
if (element.IsOverride())
{
element = element.Elements().First();
}
originalName = element.GetAttributeString("name", "");
identifier = element.GetAttributeString("identifier", null) ?? originalName.ToLowerInvariant().Replace(" ", "");
configElement = element;
Category = MapEntityCategory.ItemAssembly;
SerializableProperty.DeserializeProperties(this, configElement);
name = TextManager.Get("EntityName." + identifier, returnNull: true) ?? originalName;
Description = TextManager.Get("EntityDescription." + identifier, returnNull: true) ?? Description;
Name = TextManager.Get($"EntityName.{Identifier}").Fallback(element.GetAttributeString("name", ""));
Description = TextManager.Get($"EntityDescription.{Identifier}");
Tags = Enumerable.Empty<Identifier>().ToImmutableHashSet();
List<ushort> containedItemIDs = new List<ushort>();
foreach (XElement entityElement in element.Elements())
{
var containerElement = entityElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("itemcontainer", StringComparison.OrdinalIgnoreCase));
var containerElement = entityElement.GetChildElement("itemcontainer");
if (containerElement == null) { continue; }
string containedString = containerElement.GetAttributeString("contained", "");
@@ -84,52 +76,36 @@ namespace Barotrauma
int minX = int.MaxValue, minY = int.MaxValue;
int maxX = int.MinValue, maxY = int.MinValue;
DisplayEntities = new List<Pair<MapEntityPrefab, Rectangle>>();
var displayEntities = new List<(Identifier, Rectangle)>();
foreach (XElement entityElement in element.Elements())
{
ushort id = (ushort)entityElement.GetAttributeInt("ID", 0);
if (id > 0 && containedItemIDs.Contains(id)) { continue; }
string identifier = entityElement.GetAttributeString("identifier", entityElement.Name.ToString().ToLowerInvariant());
MapEntityPrefab mapEntity = List.FirstOrDefault(p => p.Identifier == identifier);
if (mapEntity == null)
{
string entityName = entityElement.GetAttributeString("name", "");
mapEntity = List.FirstOrDefault(p => p.Name == entityName);
}
Identifier identifier = entityElement.GetAttributeIdentifier("identifier", entityElement.Name.ToString().ToLowerInvariant());
Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty);
if (mapEntity != null && !entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase)))
if (!entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase)))
{
if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) { DisplayEntities.Add(new Pair<MapEntityPrefab, Rectangle>(mapEntity, rect)); }
if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) { displayEntities.Add((identifier, rect)); }
minX = Math.Min(minX, rect.X);
minY = Math.Min(minY, rect.Y - rect.Height);
maxX = Math.Max(maxX, rect.Right);
maxY = Math.Max(maxY, rect.Y);
}
}
DisplayEntities = displayEntities.ToImmutableArray();
Bounds = minX == int.MaxValue ?
new Rectangle(0, 0, 1, 1) :
new Rectangle(minX, minY, maxX - minX, maxY - minY);
if (allowOverwrite && Prefabs.ContainsKey(identifier))
{
Prefabs.Remove(Prefabs[identifier]);
}
Prefabs.Add(this, doc.Root.IsOverride());
}
public static void Remove(string filePath)
{
Prefabs.RemoveByFile(filePath);
}
protected override void CreateInstance(Rectangle rect)
{
#if CLIENT
var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub, selectInstance: Screen.Selected == GameMain.SubEditorScreen);
if (Screen.Selected is SubEditorScreen)
if (Screen.Selected is SubEditorScreen && loaded.Any())
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false, handleInventoryBehavior: false));
}
@@ -140,7 +116,7 @@ namespace Barotrauma
public List<MapEntity> CreateInstance(Vector2 position, Submarine sub, bool selectInstance = false)
{
return PasteEntities(position, sub, configElement, FilePath, selectInstance);
return PasteEntities(position, sub, configElement, ContentFile.Path.Value, selectInstance);
}
public static List<MapEntity> PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false)
@@ -183,53 +159,22 @@ namespace Barotrauma
public void Delete()
{
Dispose();
if (File.Exists(FilePath))
try
{
try
if (ContentPackage is { Files: { Length: 1 } }
&& ContentPackageManager.LocalPackages.Contains(ContentPackage))
{
File.Delete(FilePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Deleting item assembly \"" + name + "\" failed.", e);
Directory.Delete(ContentPackage.Dir, recursive: true);
ContentPackageManager.LocalPackages.Refresh();
ContentPackageManager.EnabledPackages.DisableRemovedMods();
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e);
}
}
public static void LoadAll()
{
if (GameSettings.VerboseLogging)
{
DebugConsole.Log("Loading item assembly prefabs: ");
}
List<string> itemAssemblyFiles = new List<string>();
//find assembly files in the item assembly folders
if (Directory.Exists(VanillaSaveFolder))
{
itemAssemblyFiles.AddRange(Directory.GetFiles(VanillaSaveFolder));
}
if (Directory.Exists(SaveFolder))
{
itemAssemblyFiles.AddRange(Directory.GetFiles(SaveFolder));
}
//find assembly files in selected content packages
foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages)
{
foreach (string filePath in cp.GetFilesOfType(ContentType.ItemAssembly))
{
//ignore files that have already been added (= file saved to item assembly folder)
if (itemAssemblyFiles.Any(f => Path.GetFullPath(f) == Path.GetFullPath(filePath))) { continue; }
itemAssemblyFiles.Add(filePath);
}
}
foreach (string file in itemAssemblyFiles)
{
new ItemAssemblyPrefab(file);
}
}
public override void Dispose() { }
}
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Xml.Linq;
namespace Barotrauma
{
class Biome : PrefabWithUintIdentifier
{
public readonly static PrefabCollection<Biome> Prefabs = new PrefabCollection<Biome>();
public readonly Identifier OldIdentifier;
public readonly LocalizedString DisplayName;
public readonly LocalizedString Description;
public readonly bool IsEndBiome;
public readonly ImmutableHashSet<int> AllowedZones;
public Biome(ContentXElement element, LevelGenerationParametersFile file) : base(file, ParseIdentifier(element))
{
OldIdentifier = element.GetAttributeIdentifier("oldidentifier", Identifier.Empty);
DisplayName =
TextManager.Get("biomename." + Identifier).Fallback(
element.GetAttributeString("name", "Biome"));
Description =
TextManager.Get("biomedescription." + Identifier).Fallback(
element.GetAttributeString("description", ""));
IsEndBiome = element.GetAttributeBool("endbiome", false);
AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet();
}
public static Identifier ParseIdentifier(ContentXElement element)
{
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
if (identifier.IsEmpty)
{
identifier = element.GetAttributeIdentifier("name", "");
DebugConsole.ThrowError("Error in biome \"" + identifier + "\": identifier missing, using name as the identifier.");
}
return identifier;
}
public override void Dispose() { }
}
}
@@ -7,23 +7,18 @@ using System.Xml.Linq;
namespace Barotrauma
{
class CaveGenerationParams : ISerializableEntity
class CaveGenerationParams : PrefabWithUintIdentifier, ISerializableEntity
{
public static List<CaveGenerationParams> CaveParams { get; private set; }
public readonly static PrefabCollection<CaveGenerationParams> CaveParams = new PrefabCollection<CaveGenerationParams>();
public string Name
{
get { return Identifier; }
}
public readonly string Identifier;
public string Name => Identifier.Value;
private int minWidth, maxWidth;
private int minHeight, maxHeight;
private int minBranchCount, maxBranchCount;
public Dictionary<string, SerializableProperty> SerializableProperties
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
get;
set;
@@ -33,100 +28,100 @@ namespace Barotrauma
/// Overrides the commonness of the object in a specific level type.
/// Key = name of the level type, value = commonness in that level type.
/// </summary>
public readonly Dictionary<string, float> OverrideCommonness = new Dictionary<string, float>();
public readonly Dictionary<Identifier, float> OverrideCommonness = new Dictionary<Identifier, float>();
[Editable, Serialize(1.0f, true)]
[Editable, Serialize(1.0f, IsPropertySaveable.Yes)]
public float Commonness
{
get;
private set;
}
[Serialize(8000, true), Editable(MinValueInt = 1000, MaxValueInt = 100000)]
[Serialize(8000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 100000)]
public int MinWidth
{
get { return minWidth; }
set { minWidth = Math.Max(value, 1000); }
}
[Serialize(10000, true), Editable(MinValueInt = 1000, MaxValueInt = 1000000)]
[Serialize(10000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 1000000)]
public int MaxWidth
{
get { return maxWidth; }
set { maxWidth = Math.Max(value, minWidth); }
}
[Serialize(8000, true), Editable(MinValueInt = 1000, MaxValueInt = 100000)]
[Serialize(8000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 100000)]
public int MinHeight
{
get { return minHeight; }
set { minHeight = Math.Max(value, 1000); }
}
[Serialize(10000, true), Editable(MinValueInt = 1000, MaxValueInt = 1000000)]
[Serialize(10000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 1000000)]
public int MaxHeight
{
get { return maxHeight; }
set { maxHeight = Math.Max(value, minHeight); }
}
[Serialize(2, true), Editable(MinValueInt = 0, MaxValueInt = 10)]
[Serialize(2, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)]
public int MinBranchCount
{
get { return minBranchCount; }
set { minBranchCount = Math.Max(value, 0); }
}
[Serialize(4, true), Editable(MinValueInt = 0, MaxValueInt = 10)]
[Serialize(4, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)]
public int MaxBranchCount
{
get { return maxBranchCount; }
set { maxBranchCount = Math.Max(value, minBranchCount); }
}
[Serialize(50, true), Editable(MinValueInt = 0, MaxValueInt = 1000)]
[Serialize(50, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000)]
public int LevelObjectAmount
{
get;
set;
}
[Serialize(0.1f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )]
[Serialize(0.1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )]
public float DestructibleWallRatio
{
get;
set;
}
public Sprite WallSprite { get; private set; }
public Sprite WallEdgeSprite { get; private set; }
public readonly Sprite WallSprite;
public readonly Sprite WallEdgeSprite;
public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, bool abyss, Rand.RandSync rand)
{
if (CaveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f))
var caveParams = CaveParams.OrderBy(p => p.UintIdentifier).ToList();
if (caveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f))
{
return CaveParams.First();
return caveParams.First();
}
return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand);
return ToolBox.SelectWeightedRandom(caveParams.ToList(), caveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand);
}
public float GetCommonness(LevelGenerationParams generationParams, bool abyss)
{
if (generationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(abyss ? "abyss" : generationParams.Identifier, out float commonness))
if (generationParams != null &&
generationParams.Identifier != Identifier.Empty &&
OverrideCommonness.TryGetValue(abyss ? "abyss".ToIdentifier() : generationParams.Identifier, out float commonness))
{
return commonness;
}
return Commonness;
}
private CaveGenerationParams(XElement element)
public CaveGenerationParams(ContentXElement element, CaveGenerationParametersFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
Identifier = element == null ? "default" : element.GetAttributeString("identifier", null) ?? element.Name.ToString();
Identifier = Identifier.ToLowerInvariant();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -137,7 +132,7 @@ namespace Barotrauma
WallEdgeSprite = new Sprite(subElement);
break;
case "overridecommonness":
string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant();
Identifier levelType = subElement.GetAttributeIdentifier("leveltype", "");
if (!OverrideCommonness.ContainsKey(levelType))
{
OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f));
@@ -147,71 +142,16 @@ namespace Barotrauma
}
}
public static void LoadPresets()
{
CaveParams = new List<CaveGenerationParams>();
var files = GameMain.Instance.GetFilesOfType(ContentType.CaveGenerationParameters);
if (!files.Any())
{
files = new List<ContentFile>() { new ContentFile("Content/Map/CaveGenerationParameters.xml", ContentType.CaveGenerationParameters) };
}
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
CaveParams.Clear();
DebugConsole.NewMessage($"Overriding cave generation parameters with '{file.Path}'", Color.Yellow);
}
foreach (XElement element in mainElement.Elements())
{
bool isOverride = element.IsOverride();
if (isOverride)
{
string identifier = element.FirstElement().GetAttributeString("identifier", "");
var existingParams = CaveParams.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase));
if (existingParams != null)
{
DebugConsole.NewMessage($"Overriding the cave generation parameters '{identifier}' using the file '{file.Path}'", Color.Yellow);
CaveParams.Remove(existingParams);
}
CaveParams.Add(new CaveGenerationParams(element.FirstElement()));
}
else
{
string identifier = element.FirstElement().GetAttributeString("identifier", "");
var existingParams = CaveParams.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase));
if (existingParams != null)
{
DebugConsole.ThrowError($"Duplicate cave generation parameters: '{identifier}' defined in {element.Name} of '{file.Path}'. Use <override></override> tags to override the generation parameters.");
continue;
}
else
{
CaveParams.Add(new CaveGenerationParams(element));
}
}
}
}
}
public void Save(XElement element)
{
SerializableProperty.SerializeProperties(this, element, true);
foreach (KeyValuePair<string, float> overrideCommonness in OverrideCommonness)
foreach (KeyValuePair<Identifier, float> overrideCommonness in OverrideCommonness)
{
bool elementFound = false;
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("overridecommonness", StringComparison.OrdinalIgnoreCase)
&& subElement.GetAttributeString("leveltype", "").Equals(overrideCommonness.Key, StringComparison.OrdinalIgnoreCase))
if (subElement.NameAsIdentifier() == "overridecommonness"
&& subElement.GetAttributeIdentifier("leveltype", "") == overrideCommonness.Key)
{
subElement.Attribute("commonness").Value = overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture);
elementFound = true;
@@ -226,5 +166,10 @@ namespace Barotrauma
}
}
}
public override void Dispose()
{
WallSprite?.Remove(); WallEdgeSprite?.Remove();
}
}
}
@@ -279,7 +279,7 @@ namespace Barotrauma
{
float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount));
float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.Server);
float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.ServerAndClient);
Vector2 extrudedPoint =
edge.Point1 +
edgeDir * (i / (float)pointCount) +
@@ -469,7 +469,7 @@ namespace Barotrauma
for (int i = 0; i < vertexCount; i++)
{
Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
verts.Add(new Vector2(dir.X * width / 2, dir.Y * height / 2) + dir * Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server));
verts.Add(new Vector2(dir.X * width / 2, dir.Y * height / 2) + dir * Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.ServerAndClient));
angle += angleStep;
}
return verts;
File diff suppressed because it is too large Load Diff
@@ -31,8 +31,20 @@ namespace Barotrauma
public bool HasHuntingGrounds, OriginallyHadHuntingGrounds;
/// <summary>
/// Minimum difficulty of the level before hunting grounds can appear.
/// </summary>
public const float HuntingGroundsDifficultyThreshold = 25;
/// <summary>
/// Probability of hunting grounds appearing in 100% difficulty levels.
/// </summary>
public const float MaxHuntingGroundsProbability = 0.3f;
public OutpostGenerationParams ForceOutpostGenerationParams;
public bool AllowInvalidOutpost;
public readonly Point Size;
public readonly int InitialDepth;
@@ -92,7 +104,7 @@ namespace Barotrauma
OriginallyHadHuntingGrounds = element.GetAttributeBool("originallyhadhuntinggrounds", HasHuntingGrounds);
string generationParamsId = element.GetAttributeString("generationparams", "");
GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || l.OldIdentifier == generationParamsId);
GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || (!l.OldIdentifier.IsEmpty && l.OldIdentifier == generationParamsId));
if (GenerationParams == null)
{
DebugConsole.ThrowError($"Error while loading a level. Could not find level generation params with the ID \"{generationParamsId}\".");
@@ -106,18 +118,18 @@ namespace Barotrauma
InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin);
string biomeIdentifier = element.GetAttributeString("biome", "");
Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeIdentifier || b.OldIdentifier == biomeIdentifier);
Biome = Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeIdentifier || (!b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeIdentifier));
if (Biome == null)
{
DebugConsole.ThrowError($"Error in level data: could not find the biome \"{biomeIdentifier}\".");
Biome = LevelGenerationParams.GetBiomes().First();
Biome = Biome.Prefabs.First();
}
string[] prefabNames = element.GetAttributeStringArray("eventhistory", new string[] { });
EventHistory.AddRange(EventSet.PrefabList.Where(p => prefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase))));
EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n)));
string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { });
NonRepeatableEvents.AddRange(EventSet.PrefabList.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase))));
NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)));
}
@@ -129,7 +141,7 @@ namespace Barotrauma
Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName;
Biome = locationConnection.Biome;
Type = LevelType.LocationConnection;
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Biome);
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Biome.Identifier);
Difficulty = locationConnection.Difficulty;
float sizeFactor = MathUtils.InverseLerp(
@@ -150,11 +162,7 @@ namespace Barotrauma
}
else
{
//minimum difficulty of the level before hunting grounds can appear
float huntingGroundsDifficultyThreshold = 25;
//probability of hunting grounds appearing in 100% difficulty levels
float maxHuntingGroundsProbability = 0.3f;
HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability;
HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(HuntingGroundsDifficultyThreshold, 100.0f, Difficulty) * MaxHuntingGroundsProbability;
HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max();
}
IsBeaconActive = false;
@@ -168,7 +176,7 @@ namespace Barotrauma
Seed = location.BaseName;
Biome = location.Biome;
Type = LevelType.Outpost;
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Biome);
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Biome.Identifier);
Difficulty = 0.0f;
var rand = new MTRandom(ToolBox.StringToInt(Seed));
@@ -183,7 +191,7 @@ namespace Barotrauma
{
if (string.IsNullOrEmpty(seed))
{
seed = Rand.Range(0, int.MaxValue, Rand.RandSync.Server).ToString();
seed = Rand.Range(0, int.MaxValue, Rand.RandSync.ServerAndClient).ToString();
}
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
@@ -194,18 +202,18 @@ namespace Barotrauma
if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); }
var biome =
LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ??
LevelGenerationParams.GetBiomes().GetRandom(Rand.RandSync.Server);
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.Server),
Rand.Range(0.0f, 1.0f, Rand.RandSync.Server),
difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient),
Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient),
generationParams,
biome);
if (type == LevelType.LocationConnection)
{
float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server);
float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient);
levelData.HasBeaconStation = beaconRng < 0.5f;
levelData.IsBeaconActive = beaconRng > 0.25f;
}
@@ -1,68 +1,20 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class Biome
class LevelGenerationParams : PrefabWithUintIdentifier, ISerializableEntity
{
public readonly string Identifier;
public readonly string OldIdentifier;
public readonly string DisplayName;
public readonly string Description;
public readonly static PrefabCollection<LevelGenerationParams> LevelParams = new PrefabCollection<LevelGenerationParams>();
public readonly bool IsEndBiome;
public string Name => Identifier.Value;
public readonly List<int> AllowedZones = new List<int>();
public Biome(string name, string description)
{
Identifier = name;
Description = description;
}
public Biome(XElement element)
{
Identifier = element.GetAttributeString("identifier", "");
OldIdentifier = element.GetAttributeString("oldidentifier", null);
if (string.IsNullOrEmpty(Identifier))
{
Identifier = element.GetAttributeString("name", "");
DebugConsole.ThrowError("Error in biome \"" + Identifier + "\": identifier missing, using name as the identifier.");
}
DisplayName =
TextManager.Get("biomename." + Identifier, returnNull: true) ??
element.GetAttributeString("name", "Biome") ??
TextManager.Get("biomename." + Identifier);
Description =
TextManager.Get("biomedescription." + Identifier, returnNull: true) ??
element.GetAttributeString("description", "") ??
TextManager.Get("biomedescription." + Identifier);
IsEndBiome = element.GetAttributeBool("endbiome", false);
AllowedZones.AddRange(element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }));
}
}
class LevelGenerationParams : ISerializableEntity
{
public static List<LevelGenerationParams> LevelParams { get; private set; }
private static List<Biome> biomes;
public string Name
{
get { return Identifier; }
}
public readonly string Identifier;
public readonly string OldIdentifier;
public Identifier OldIdentifier { get; }
private int minWidth, maxWidth, height;
@@ -100,48 +52,44 @@ namespace Barotrauma
private int initialDepthMin, initialDepthMax;
//which biomes can this type of level appear in
private readonly List<Biome> allowedBiomes = new List<Biome>();
public readonly ImmutableHashSet<Identifier> AllowedBiomeIdentifiers;
public readonly bool AnyBiomeAllowed;
public IEnumerable<Biome> AllowedBiomes
{
get { return allowedBiomes; }
}
public Dictionary<string, SerializableProperty> SerializableProperties
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
get;
set;
}
[Serialize(LevelData.LevelType.LocationConnection, true), Editable]
[Serialize(LevelData.LevelType.LocationConnection, IsPropertySaveable.Yes), Editable]
public LevelData.LevelType Type
{
get;
set;
}
[Serialize("27,30,36", true), Editable]
[Serialize("27,30,36", IsPropertySaveable.Yes), Editable]
public Color AmbientLightColor
{
get;
set;
}
[Serialize("20,40,50", true), Editable()]
[Serialize("20,40,50", IsPropertySaveable.Yes), Editable()]
public Color BackgroundTextureColor
{
get;
set;
}
[Serialize("20,40,50", true), Editable]
[Serialize("20,40,50", IsPropertySaveable.Yes), Editable]
public Color BackgroundColor
{
get;
set;
}
[Serialize("255,255,255", true), Editable]
[Serialize("255,255,255", IsPropertySaveable.Yes), Editable]
public Color WallColor
{
get;
@@ -149,7 +97,7 @@ namespace Barotrauma
}
private Vector2 startPosition;
[Serialize("0,0", true, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)]
[Serialize("0,0", IsPropertySaveable.Yes, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)]
public Vector2 StartPosition
{
get { return startPosition; }
@@ -162,7 +110,7 @@ namespace Barotrauma
}
private Vector2 endPosition;
[Serialize("1,0", true, "End position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)]
[Serialize("1,0", IsPropertySaveable.Yes, "End position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)]
public Vector2 EndPosition
{
get { return endPosition; }
@@ -174,70 +122,70 @@ namespace Barotrauma
}
}
[Serialize(true, true, "Should there be a hole in the wall next to the end outpost (can be used to prevent players from having to backtrack if they approach the outpost from the wrong side of the main path's walls)."), Editable]
[Serialize(true, IsPropertySaveable.Yes, "Should there be a hole in the wall next to the end outpost (can be used to prevent players from having to backtrack if they approach the outpost from the wrong side of the main path's walls)."), Editable]
public bool CreateHoleNextToEnd
{
get;
set;
}
[Serialize(true, true, "Should the generator force a hole to the bottom of the level to ensure there's a way to the abyss."), Editable]
[Serialize(true, IsPropertySaveable.Yes, "Should the generator force a hole to the bottom of the level to ensure there's a way to the abyss."), Editable]
public bool CreateHoleToAbyss
{
get;
set;
}
[Serialize(1000, true, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)]
[Serialize(1000, IsPropertySaveable.Yes, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)]
public int LevelObjectAmount
{
get;
set;
}
[Serialize(80, true, description: "The total number of decorative background creatures."), Editable(MinValueInt = 0, MaxValueInt = 1000)]
[Serialize(80, IsPropertySaveable.Yes, description: "The total number of decorative background creatures."), Editable(MinValueInt = 0, MaxValueInt = 1000)]
public int BackgroundCreatureAmount
{
get;
set;
}
[Serialize(100000, true), Editable]
[Serialize(100000, IsPropertySaveable.Yes), Editable]
public int MinWidth
{
get { return minWidth; }
set { minWidth = MathHelper.Clamp(value, 2000, 1000000); }
}
[Serialize(100000, true), Editable]
[Serialize(100000, IsPropertySaveable.Yes), Editable]
public int MaxWidth
{
get { return maxWidth; }
set { maxWidth = MathHelper.Clamp(value, 2000, 1000000); }
}
[Serialize(50000, true), Editable]
[Serialize(50000, IsPropertySaveable.Yes), Editable]
public int Height
{
get { return height; }
set { height = MathHelper.Clamp(value, 2000, 1000000); }
}
[Serialize(80000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
[Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
public int InitialDepthMin
{
get { return initialDepthMin; }
set { initialDepthMin = Math.Max(value, 0); }
}
[Serialize(80000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
[Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
public int InitialDepthMax
{
get { return initialDepthMax; }
set { initialDepthMax = Math.Max(value, initialDepthMin); }
}
[Serialize(6500, true), Editable(MinValueInt = 5000, MaxValueInt = 1000000)]
[Serialize(6500, IsPropertySaveable.Yes), Editable(MinValueInt = 5000, MaxValueInt = 1000000)]
public int MinTunnelRadius
{
get;
@@ -245,7 +193,7 @@ namespace Barotrauma
}
[Serialize("0,1", true), Editable]
[Serialize("0,1", IsPropertySaveable.Yes), Editable]
public Point SideTunnelCount
{
get;
@@ -253,21 +201,21 @@ namespace Barotrauma
}
[Serialize(0.5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
[Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float SideTunnelVariance
{
get;
set;
}
[Serialize("2000,6000", true), Editable]
[Serialize("2000,6000", IsPropertySaveable.Yes), Editable]
public Point MinSideTunnelRadius
{
get;
set;
}
[Editable, Serialize("3000, 3000", true, description: "How far from each other voronoi sites are placed. " +
[Editable, Serialize("3000, 3000", IsPropertySaveable.Yes, description: "How far from each other voronoi sites are placed. " +
"Sites determine shape of the voronoi graph which the level walls are generated from. " +
"(Decreasing this value causes the number of sites, and the complexity of the level, to increase exponentially - be careful when adjusting)")]
public Point VoronoiSiteInterval
@@ -280,7 +228,7 @@ namespace Barotrauma
}
}
[Editable, Serialize("700,700", true, description: "How much random variation to apply to the positions of the voronoi sites on each axis. " +
[Editable, Serialize("700,700", IsPropertySaveable.Yes, description: "How much random variation to apply to the positions of the voronoi sites on each axis. " +
"Small values produce roughly rectangular level walls. The larger the values are, the less uniform the shapes get.")]
public Point VoronoiSiteVariance
{
@@ -293,7 +241,7 @@ namespace Barotrauma
}
}
[Editable(MinValueInt = 500, MaxValueInt = 10000), Serialize(5000, true, description: "The edges of the individual wall cells are subdivided into edges of this size. "
[Editable(MinValueInt = 500, MaxValueInt = 10000), Serialize(5000, IsPropertySaveable.Yes, description: "The edges of the individual wall cells are subdivided into edges of this size. "
+ "Can be used in conjunction with the rounding values to make the cells rounder. Smaller values will make the cells look smoother, " +
"but make the level more performance-intensive as the number of polygons used in rendering and physics calculations increases.")]
public int CellSubdivisionLength
@@ -306,7 +254,7 @@ namespace Barotrauma
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, true, description: "How much the individual wall cells are rounded. "
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the individual wall cells are rounded. "
+ "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")]
public float CellRoundingAmount
{
@@ -317,7 +265,7 @@ namespace Barotrauma
}
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, true, description: "How much random variance is applied to the edges of the cells. "
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, IsPropertySaveable.Yes, description: "How much random variance is applied to the edges of the cells. "
+ "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")]
public float CellIrregularity
{
@@ -330,7 +278,7 @@ namespace Barotrauma
[Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }),
Serialize("5000, 10000", true, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")]
Serialize("5000, 10000", IsPropertySaveable.Yes, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")]
public Point MainPathNodeIntervalRange
{
get { return mainPathNodeIntervalRange; }
@@ -341,42 +289,42 @@ namespace Barotrauma
}
}
[Serialize(0.5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
[Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float MainPathVariance
{
get;
set;
}
[Editable, Serialize(5, true, description: "The number of caves placed along the main path.")]
[Editable, Serialize(5, IsPropertySaveable.Yes, description: "The number of caves placed along the main path.")]
public int CaveCount
{
get { return caveCount; }
set { caveCount = MathHelper.Clamp(value, 0, 100); }
}
[Serialize(100, true), Editable(MinValueInt = 0, MaxValueInt = 10000)]
[Serialize(100, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10000)]
public int ItemCount
{
get;
set;
}
[Serialize("19200,38400", true, description: "The minimum and maximum distance between two resource spawn points on a path."), Editable(100, 100000)]
[Serialize("19200,38400", IsPropertySaveable.Yes, description: "The minimum and maximum distance between two resource spawn points on a path."), Editable(100, 100000)]
public Point ResourceIntervalRange
{
get;
set;
}
[Serialize("9600,19200", true, description: "The minimum and maximum distance between two resource spawn points on a cave path."), Editable(100, 100000)]
[Serialize("9600,19200", IsPropertySaveable.Yes, description: "The minimum and maximum distance between two resource spawn points on a cave path."), Editable(100, 100000)]
public Point CaveResourceIntervalRange
{
get;
set;
}
[Serialize("2,8", true, description: "The minimum and maximum amount of resources in a single cluster. " +
[Serialize("2,8", IsPropertySaveable.Yes, description: "The minimum and maximum amount of resources in a single cluster. " +
"In addition to this, resource commonness affects the cluster size. Less common resources spawn in smaller clusters."), Editable(1, 20)]
public Point ResourceClusterSizeRange
{
@@ -384,76 +332,90 @@ namespace Barotrauma
set;
}
[Serialize(0.3f, true, description: "How likely a resource spawn point on a path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(0.3f, IsPropertySaveable.Yes, description: "How likely a resource spawn point on a path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
public float ResourceSpawnChance { get; set; }
[Serialize(1.0f, true, description: "How likely a resource spawn point on a cave path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "How likely a resource spawn point on a cave path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
public float CaveResourceSpawnChance { get; set; }
[Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20)]
[Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int FloatingIceChunkCount
{
get;
set;
}
[Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 100)]
[Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 100)]
public int IslandCount
{
get;
set;
}
[Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20)]
[Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int IceSpireCount
{
get;
set;
}
[Serialize(5, true), Editable(MinValueInt = 0, MaxValueInt = 20)]
[Serialize(5, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int AbyssIslandCount
{
get;
set;
}
[Serialize("4000,7000", true), Editable]
[Serialize("4000,7000", IsPropertySaveable.Yes), Editable]
public Point AbyssIslandSizeMin
{
get;
set;
}
[Serialize("8000,10000", true), Editable]
[Serialize("8000,10000", IsPropertySaveable.Yes), Editable]
public Point AbyssIslandSizeMax
{
get;
set;
}
[Serialize(0.5f, true), Editable()]
[Serialize(0.5f, IsPropertySaveable.Yes), Editable()]
public float AbyssIslandCaveProbability
{
get;
set;
}
[Serialize(-300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)]
[Serialize(10, IsPropertySaveable.Yes, description: "Minimum 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 AbyssResourceClustersMin
{
get;
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)]
public int AbyssResourceClustersMax
{
get;
set;
}
[Serialize(-300000, IsPropertySaveable.Yes, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)]
public int SeaFloorDepth
{
get { return seaFloorBaseDepth; }
set { seaFloorBaseDepth = MathHelper.Clamp(value, Level.MaxEntityDepth, 0); }
}
[Serialize(1000, true, description: "Variance of the depth of the sea floor. Smaller values produce a smoother sea floor."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100000.0f)]
[Serialize(1000, IsPropertySaveable.Yes, description: "Variance of the depth of the sea floor. Smaller values produce a smoother sea floor."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100000.0f)]
public int SeaFloorVariance
{
get { return seaFloorVariance; }
set { seaFloorVariance = value; }
}
[Serialize(0, true, description: "The minimum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)]
[Serialize(0, IsPropertySaveable.Yes, description: "The minimum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int MountainCountMin
{
get { return mountainCountMin; }
@@ -463,7 +425,7 @@ namespace Barotrauma
}
}
[Serialize(0, true, description: "The maximum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)]
[Serialize(0, IsPropertySaveable.Yes, description: "The maximum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int MountainCountMax
{
get { return mountainCountMax; }
@@ -473,7 +435,7 @@ namespace Barotrauma
}
}
[Serialize(1000, true, description: "The minimum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
[Serialize(1000, IsPropertySaveable.Yes, description: "The minimum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
public int MountainHeightMin
{
get { return mountainHeightMin; }
@@ -483,7 +445,7 @@ namespace Barotrauma
}
}
[Serialize(5000, true, description: "The maximum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
[Serialize(5000, IsPropertySaveable.Yes, description: "The maximum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
public int MountainHeightMax
{
get { return mountainHeightMax; }
@@ -493,72 +455,72 @@ namespace Barotrauma
}
}
[Serialize(1, true, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)]
[Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)]
public int RuinCount { get; set; }
[Serialize(1, true, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)]
[Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)]
public int MinWreckCount { get; set; }
[Serialize(1, true, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)]
[Serialize(1, IsPropertySaveable.Yes, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)]
public int MaxWreckCount { get; set; }
// TODO: Move the wreck parameters under a separate class?
#region Wreck parameters
[Serialize(1, true, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)]
[Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int MinCorpseCount { get; set; }
[Serialize(5, true, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)]
[Serialize(5, IsPropertySaveable.Yes, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int MaxCorpseCount { get; set; }
[Serialize(0.0f, true, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
public float ThalamusProbability { get; set; }
[Serialize(0.5f, true, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(0.5f, IsPropertySaveable.Yes, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
public float WreckHullFloodingChance { get; set; }
[Serialize(0.1f, true, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(0.1f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
public float WreckFloodingHullMinWaterPercentage { get; set; }
[Serialize(1.0f, true, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
public float WreckFloodingHullMaxWaterPercentage { get; set; }
#endregion
[Serialize(0.4f, true, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()]
[Serialize(0.4f, IsPropertySaveable.Yes, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()]
public float BottomHoleProbability
{
get { return bottomHoleProbability; }
set { bottomHoleProbability = MathHelper.Clamp(value, 0.0f, 1.0f); }
}
[Serialize(1.0f, true, description: "Scale of the water particle texture."), Editable]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Scale of the water particle texture."), Editable]
public float WaterParticleScale
{
get { return waterParticleScale; }
private set { waterParticleScale = Math.Max(value, 0.01f); }
}
[Serialize(2048.0f, true, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)]
[Serialize(2048.0f, IsPropertySaveable.Yes, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)]
public float WallTextureSize
{
get;
private set;
}
[Serialize(2048.0f, true), Editable(minValue: 10.0f, maxValue: 10000.0f)]
[Serialize(2048.0f, IsPropertySaveable.Yes), Editable(minValue: 10.0f, maxValue: 10000.0f)]
public float WallEdgeTextureWidth
{
get;
private set;
}
[Serialize(120.0f, true, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)]
[Serialize(120.0f, IsPropertySaveable.Yes, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)]
public float WallEdgeExpandOutwardsAmount
{
get;
private set;
}
[Serialize(1000.0f, true, description: "How far inside the level walls the edge texture continues."), Editable(minValue: 0.0f, maxValue: 10000.0f)]
[Serialize(1000.0f, IsPropertySaveable.Yes, description: "How far inside the level walls the edge texture continues."), Editable(minValue: 0.0f, maxValue: 10000.0f)]
public float WallEdgeExpandInwardsAmount
{
get;
@@ -574,38 +536,31 @@ namespace Barotrauma
public Sprite WallSpriteDestroyed { get; private set; }
public Sprite WaterParticles { get; private set; }
public static IEnumerable<Biome> GetBiomes()
{
return biomes;
}
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, Biome biome = null)
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, Identifier biome = default)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
if (LevelParams == null || !LevelParams.Any())
if (!LevelParams.Any())
{
DebugConsole.ThrowError("Level generation presets not found - using default presets");
return new LevelGenerationParams(null);
throw new InvalidOperationException("Level generation presets not found - using default presets");
}
var matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type && lp.allowedBiomes.Any());
if (biome == null)
var matchingLevelParams = LevelParams.Where(lp =>
lp.Type == type &&
(lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) &&
!lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier()));
matchingLevelParams = biome.IsEmpty
? matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome))
: matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biome));
if (!matchingLevelParams.Any())
{
matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.All(b => b.IsEndBiome));
}
else
{
matchingLevelParams = matchingLevelParams.FindAll(lp => lp.allowedBiomes.Contains(biome));
}
if (matchingLevelParams.Count == 0)
{
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{(biome?.Identifier ?? "null")}\", type: \"{type}\"!");
if (biome != null)
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")");
if (!biome.IsEmpty)
{
//try to find params that at least have a suitable type
matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type);
if (matchingLevelParams.Count == 0)
matchingLevelParams = LevelParams.Where(lp => lp.Type == type);
if (!matchingLevelParams.Any())
{
//still not found, give up and choose some params randomly
matchingLevelParams = LevelParams;
@@ -613,53 +568,22 @@ namespace Barotrauma
}
}
return matchingLevelParams[Rand.Range(0, matchingLevelParams.Count, Rand.RandSync.Server)];
return matchingLevelParams.GetRandom(Rand.RandSync.ServerAndClient);
}
private LevelGenerationParams(XElement element)
public LevelGenerationParams(ContentXElement element, LevelGenerationParametersFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName))
{
Identifier = element == null ? "default" :
element.GetAttributeString("identifier", null) ?? element.Name.ToString();
OldIdentifier = element?.GetAttributeString("oldidentifier", null)?.ToLowerInvariant();
Identifier = Identifier.ToLowerInvariant();
OldIdentifier = element.GetAttributeIdentifier("oldidentifier", Identifier.Empty);
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (element == null) { return; }
if (element is null) { throw new ArgumentNullException($"{nameof(element)} is null"); }
string biomeStr = element.GetAttributeString("biomes", "");
if (string.IsNullOrWhiteSpace(biomeStr) || biomeStr.Equals("any", StringComparison.OrdinalIgnoreCase))
{
allowedBiomes = new List<Biome>(biomes);
}
else
{
string[] biomeNames = biomeStr.Split(',');
for (int i = 0; i < biomeNames.Length; i++)
{
string biomeName = biomeNames[i].Trim().ToLowerInvariant();
if (biomeName == "none") { continue; }
var allowedBiomeIdentifiers = element.GetAttributeIdentifierArray("biomes", Array.Empty<Identifier>()).ToHashSet();
AnyBiomeAllowed = allowedBiomeIdentifiers.Contains("any".ToIdentifier());
allowedBiomeIdentifiers.Remove("any".ToIdentifier());
AllowedBiomeIdentifiers = allowedBiomeIdentifiers.ToImmutableHashSet();
Biome matchingBiome = biomes.Find(b =>
b.Identifier.Equals(biomeName, StringComparison.OrdinalIgnoreCase) || (b.OldIdentifier?.Equals(biomeName, StringComparison.OrdinalIgnoreCase) ?? false));
if (matchingBiome == null)
{
matchingBiome = biomes.Find(b => b.DisplayName.Equals(biomeName, StringComparison.OrdinalIgnoreCase));
if (matchingBiome == null)
{
DebugConsole.ThrowError("Error in level generation parameters: biome \"" + biomeName + "\" not found.");
continue;
}
else
{
DebugConsole.NewMessage("Please use biome identifiers instead of names in level generation parameter \"" + Identifier + "\".", Color.Orange);
}
}
allowedBiomes.Add(matchingBiome);
}
}
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -691,87 +615,6 @@ namespace Barotrauma
}
}
public static void LoadPresets()
{
LevelParams = new List<LevelGenerationParams>();
biomes = new List<Biome>();
var files = GameMain.Instance.GetFilesOfType(ContentType.LevelGenerationParameters);
if (!files.Any())
{
files = new List<ContentFile>() { new ContentFile("Content/Map/LevelGenerationParameters.xml", ContentType.LevelGenerationParameters) };
}
List<XElement> biomeElements = new List<XElement>();
Dictionary<string, XElement> levelParamElements = new Dictionary<string, XElement>();
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
biomeElements.Clear();
DebugConsole.NewMessage($"Overriding biomes with '{file.Path}'", Color.Yellow);
}
else if (biomeElements.Any() && mainElement.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase))
{
DebugConsole.ThrowError($"Error in '{file.Path}': Another level generation parameter file already loaded! Use <override></override> tags to override the biomes.");
break;
}
foreach (XElement element in mainElement.Elements())
{
bool isOverride = element.IsOverride();
if (isOverride)
{
if (element.FirstElement().Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase))
{
biomeElements.Clear();
biomeElements.AddRange(element.FirstElement().Elements());
DebugConsole.NewMessage($"Overriding biomes with '{file.Path}'", Color.Yellow);
}
else
{
string identifier = element.FirstElement().GetAttributeString("identifier", null) ?? element.GetAttributeString("name", "");
if (levelParamElements.ContainsKey(identifier))
{
DebugConsole.NewMessage($"Overriding the level generation parameters '{identifier}' using the file '{file.Path}'", Color.Yellow);
levelParamElements.Remove(identifier);
}
levelParamElements.Add(identifier, element.FirstElement());
}
}
else if (element.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase))
{
biomeElements.AddRange(element.Elements());
}
else
{
string identifier = element.GetAttributeString("identifier", null) ?? element.GetAttributeString("name", "");
if (levelParamElements.ContainsKey(identifier))
{
DebugConsole.ThrowError($"Duplicate level generation parameters: '{identifier}' defined in {element.Name} of '{file.Path}'. Use <override></override> tags to override the generation parameters.");
continue;
}
else
{
levelParamElements.Add(identifier, element);
}
}
}
}
foreach (XElement biomeElement in biomeElements)
{
biomes.Add(new Biome(biomeElement));
}
foreach (XElement levelParamElement in levelParamElements.Values)
{
LevelParams.Add(new LevelGenerationParams(levelParamElement));
}
}
public override void Dispose() { }
}
}
@@ -81,7 +81,7 @@ namespace Barotrauma
public string Name => Prefab?.Name ?? "LevelObject (null)";
public Dictionary<string, SerializableProperty> SerializableProperties { get; } = new Dictionary<string, SerializableProperty>();
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; } = new Dictionary<Identifier, SerializableProperty>();
public Level.Cave ParentCave;
@@ -93,7 +93,7 @@ namespace Barotrauma
Rotation = rotation;
Health = prefab.Health;
spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.Server) : -1;
spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.ServerAndClient) : -1;
if (Sprite != null && prefab.SpriteSpecificPhysicsBodyElements.ContainsKey(Sprite))
{
@@ -115,7 +115,7 @@ namespace Barotrauma
Physics.CollisionWall | Physics.CollisionCharacter;
}
foreach (XElement triggerElement in prefab.LevelTriggerElements)
foreach (var triggerElement in prefab.LevelTriggerElements)
{
Triggers ??= new List<LevelTrigger>();
Vector2 triggerPosition = triggerElement.GetAttributeVector2("position", Vector2.Zero) * scale;
@@ -147,7 +147,7 @@ namespace Barotrauma
if (overrideProperties == null) { continue; }
if (overrideProperties.Sprites.Count > 0)
{
spriteIndex = Rand.Int(overrideProperties.Sprites.Count, Rand.RandSync.Server);
spriteIndex = Rand.Int(overrideProperties.Sprites.Count, Rand.RandSync.ServerAndClient);
break;
}
}
@@ -7,7 +7,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Voronoi2;
using Barotrauma.Extensions;
@@ -31,6 +30,16 @@ namespace Barotrauma
{
}
private readonly struct EventData : NetEntityEvent.IData
{
public readonly LevelObject LevelObject;
public EventData(LevelObject levelObject)
{
LevelObject = levelObject;
}
}
class SpawnPosition
{
public readonly GraphEdge GraphEdge;
@@ -140,7 +149,7 @@ namespace Barotrauma
new GraphEdge(level.EndPosition - Vector2.UnitX, level.EndPosition + Vector2.UnitX),
-Vector2.UnitY, LevelObjectPrefab.SpawnPosType.LevelEnd, Alignment.Top));
var availablePrefabs = new List<LevelObjectPrefab>(LevelObjectPrefab.List);
var availablePrefabs =LevelObjectPrefab.Prefabs.OrderBy(p => p.UintIdentifier).ToList();
objects = new List<LevelObject>();
updateableObjects = new List<LevelObject>();
@@ -167,7 +176,7 @@ namespace Barotrauma
suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList());
}
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server);
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient);
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; }
PlaceObject(prefab, spawnPosition, level);
if (prefab.MaxCount < amount)
@@ -181,7 +190,8 @@ namespace Barotrauma
foreach (Level.Cave cave in level.Caves)
{
availablePrefabs = new List<LevelObjectPrefab>(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall)));
availablePrefabs = LevelObjectPrefab.Prefabs.Where(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall))
.OrderBy(p => p.UintIdentifier).ToList();
availableSpawnPositions.Clear();
suitableSpawnPositions.Clear();
spawnPositionWeights.Clear();
@@ -210,7 +220,7 @@ namespace Barotrauma
spawnPositionWeights.Add(prefab,
suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList());
}
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server);
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient);
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; }
PlaceObject(prefab, spawnPosition, level, cave);
if (prefab.MaxCount < amount)
@@ -228,7 +238,8 @@ namespace Barotrauma
{
Rand.SetSyncedSeed(ToolBox.StringToInt(level.Seed));
var availablePrefabs = new List<LevelObjectPrefab>(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.NestWall)));
var availablePrefabs = LevelObjectPrefab.Prefabs.Where(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.NestWall))
.OrderBy(p => p.UintIdentifier).ToList();
Dictionary<LevelObjectPrefab, List<SpawnPosition>> suitableSpawnPositions = new Dictionary<LevelObjectPrefab, List<SpawnPosition>>();
Dictionary<LevelObjectPrefab, List<float>> spawnPositionWeights = new Dictionary<LevelObjectPrefab, List<float>>();
@@ -258,7 +269,7 @@ namespace Barotrauma
spawnPositionWeights.Add(prefab,
suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList());
}
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server);
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient);
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; }
PlaceObject(prefab, spawnPosition, level);
if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount)
@@ -271,53 +282,53 @@ namespace Barotrauma
private void PlaceObject(LevelObjectPrefab prefab, SpawnPosition spawnPosition, Level level, Level.Cave parentCave = null)
{
float rotation = 0.0f;
if (prefab.AlignWithSurface && spawnPosition.Normal.LengthSquared() > 0.001f && spawnPosition != null)
if (prefab.AlignWithSurface && spawnPosition != null && spawnPosition.Normal.LengthSquared() > 0.001f)
{
rotation = MathUtils.VectorToAngle(new Vector2(spawnPosition.Normal.Y, spawnPosition.Normal.X));
}
rotation += Rand.Range(prefab.RandomRotationRad.X, prefab.RandomRotationRad.Y, Rand.RandSync.Server);
rotation += Rand.Range(prefab.RandomRotationRad.X, prefab.RandomRotationRad.Y, Rand.RandSync.ServerAndClient);
Vector2 position = Vector2.Zero;
Vector2 edgeDir = Vector2.UnitX;
if (spawnPosition == null)
{
position = new Vector2(
Rand.Range(0.0f, level.Size.X, Rand.RandSync.Server),
Rand.Range(0.0f, level.Size.Y, Rand.RandSync.Server));
Rand.Range(0.0f, level.Size.X, Rand.RandSync.ServerAndClient),
Rand.Range(0.0f, level.Size.Y, Rand.RandSync.ServerAndClient));
}
else
{
edgeDir = (spawnPosition.GraphEdge.Point1 - spawnPosition.GraphEdge.Point2) / spawnPosition.Length;
position = spawnPosition.GraphEdge.Point2 + edgeDir * Rand.Range(prefab.MinSurfaceWidth / 2.0f, spawnPosition.Length - prefab.MinSurfaceWidth / 2.0f, Rand.RandSync.Server);
position = spawnPosition.GraphEdge.Point2 + edgeDir * Rand.Range(prefab.MinSurfaceWidth / 2.0f, spawnPosition.Length - prefab.MinSurfaceWidth / 2.0f, Rand.RandSync.ServerAndClient);
}
if (!MathUtils.NearlyEqual(prefab.RandomOffset.X, 0.0f) || !MathUtils.NearlyEqual(prefab.RandomOffset.Y, 0.0f))
{
Vector2 offsetDir = spawnPosition.Normal.LengthSquared() > 0.001f ? spawnPosition.Normal : Rand.Vector(1.0f, Rand.RandSync.Server);
position += offsetDir * Rand.Range(prefab.RandomOffset.X, prefab.RandomOffset.Y, Rand.RandSync.Server);
Vector2 offsetDir = spawnPosition.Normal.LengthSquared() > 0.001f ? spawnPosition.Normal : Rand.Vector(1.0f, Rand.RandSync.ServerAndClient);
position += offsetDir * Rand.Range(prefab.RandomOffset.X, prefab.RandomOffset.Y, Rand.RandSync.ServerAndClient);
}
var newObject = new LevelObject(prefab,
new Vector3(position, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.Server)), Rand.Range(prefab.MinSize, prefab.MaxSize, Rand.RandSync.Server), rotation);
new Vector3(position, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.ServerAndClient)), Rand.Range(prefab.MinSize, prefab.MaxSize, Rand.RandSync.ServerAndClient), rotation);
AddObject(newObject, level);
newObject.ParentCave = parentCave;
foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects)
{
int childCount = Rand.Range(child.MinCount, child.MaxCount + 1, Rand.RandSync.Server);
int childCount = Rand.Range(child.MinCount, child.MaxCount + 1, Rand.RandSync.ServerAndClient);
for (int j = 0; j < childCount; j++)
{
var matchingPrefabs = LevelObjectPrefab.List.Where(p => child.AllowedNames.Contains(p.Name));
var matchingPrefabs = LevelObjectPrefab.Prefabs.Where(p => child.AllowedNames.Contains(p.Name));
int prefabCount = matchingPrefabs.Count();
var childPrefab = prefabCount == 0 ? null : matchingPrefabs.ElementAt(Rand.Range(0, prefabCount, Rand.RandSync.Server));
var childPrefab = prefabCount == 0 ? null : matchingPrefabs.ElementAt(Rand.Range(0, prefabCount, Rand.RandSync.ServerAndClient));
if (childPrefab == null) { continue; }
Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server) * prefab.MinSurfaceWidth;
Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient) * prefab.MinSurfaceWidth;
var childObject = new LevelObject(childPrefab,
new Vector3(childPos, Rand.Range(childPrefab.DepthRange.X, childPrefab.DepthRange.Y, Rand.RandSync.Server)),
Rand.Range(childPrefab.MinSize, childPrefab.MaxSize, Rand.RandSync.Server),
rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.Server));
new Vector3(childPos, Rand.Range(childPrefab.DepthRange.X, childPrefab.DepthRange.Y, Rand.RandSync.ServerAndClient)),
Rand.Range(childPrefab.MinSize, childPrefab.MaxSize, Rand.RandSync.ServerAndClient),
rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.ServerAndClient));
AddObject(childObject, level);
childObject.ParentCave = parentCave;
@@ -521,12 +532,12 @@ namespace Barotrauma
foreach (LevelObject obj in updateableObjects)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember is { IsServer: true })
{
obj.NetworkUpdateTimer -= deltaTime;
if (obj.NeedsNetworkSyncing && obj.NetworkUpdateTimer <= 0.0f)
{
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { obj });
GameMain.NetworkMember.CreateEntityEvent(this, new EventData(obj));
obj.NeedsNetworkSyncing = false;
obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval;
}
@@ -577,7 +588,7 @@ namespace Barotrauma
if (availablePrefabs.Sum(p => p.GetCommonness(generationParams)) <= 0.0f) { return null; }
return ToolBox.SelectWeightedRandom(
availablePrefabs,
availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.Server);
availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.ServerAndClient);
}
private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList<LevelObjectPrefab> availablePrefabs, bool requireCaveSpecificOverride)
@@ -585,7 +596,7 @@ namespace Barotrauma
if (availablePrefabs.Sum(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)) <= 0.0f) { return null; }
return ToolBox.SelectWeightedRandom(
availablePrefabs,
availablePrefabs.Select(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)).ToList(), Rand.RandSync.Server);
availablePrefabs.Select(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)).ToList(), Rand.RandSync.ServerAndClient);
}
public override void Remove()
@@ -606,9 +617,10 @@ namespace Barotrauma
partial void RemoveProjSpecific();
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
LevelObject obj = extraData[0] as LevelObject;
if (!(extraData is EventData eventData)) { throw new Exception($"Malformed LevelObjectManager event: expected {nameof(LevelObjectManager)}.{nameof(EventData)}"); }
LevelObject obj = eventData.LevelObject;
msg.WriteRangedInteger(objects.IndexOf(obj), 0, objects.Count);
obj.ServerWrite(msg, c);
}
@@ -1,14 +1,15 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class LevelObjectPrefab : ISerializableEntity
partial class LevelObjectPrefab : PrefabWithUintIdentifier, ISerializableEntity
{
public static List<LevelObjectPrefab> List { get; } = new List<LevelObjectPrefab>();
public readonly static PrefabCollection<LevelObjectPrefab> Prefabs = new PrefabCollection<LevelObjectPrefab>();
public class ChildObject
{
@@ -24,7 +25,7 @@ namespace Barotrauma
public ChildObject(XElement element)
{
AllowedNames = element.GetAttributeStringArray("names", new string[0]).ToList();
AllowedNames = element.GetAttributeStringArray("names", Array.Empty<string>()).ToList();
MinCount = element.GetAttributeInt("mincount", 1);
MaxCount = Math.Max(element.GetAttributeInt("maxcount", 1), MinCount);
}
@@ -58,13 +59,13 @@ namespace Barotrauma
private set;
}
[Serialize(1.0f, false), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
[Serialize(1.0f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
public float MinSize
{
get;
private set;
}
[Serialize(1.0f, false), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
[Serialize(1.0f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
public float MaxSize
{
get;
@@ -74,14 +75,14 @@ namespace Barotrauma
/// <summary>
/// Which sides of a wall the object can appear on.
/// </summary>
[Serialize((Alignment.Top | Alignment.Bottom | Alignment.Left | Alignment.Right), true, description: "Which sides of a wall the object can spawn on."), Editable]
[Serialize((Alignment.Top | Alignment.Bottom | Alignment.Left | Alignment.Right), IsPropertySaveable.Yes, description: "Which sides of a wall the object can spawn on."), Editable]
public Alignment Alignment
{
get;
private set;
}
[Serialize(SpawnPosType.Wall, false), Editable()]
[Serialize(SpawnPosType.Wall, IsPropertySaveable.No), Editable()]
public SpawnPosType SpawnPos
{
get;
@@ -94,13 +95,13 @@ namespace Barotrauma
private set;
}
public readonly List<XElement> LevelTriggerElements;
public readonly List<ContentXElement> LevelTriggerElements;
/// <summary>
/// Overrides the commonness of the object in a specific level type.
/// Key = name of the level type, value = commonness in that level type.
/// </summary>
public Dictionary<string, float> OverrideCommonness;
public readonly Dictionary<Identifier, float> OverrideCommonness;
public XElement PhysicsBodyElement
{
@@ -120,14 +121,14 @@ namespace Barotrauma
} = new Dictionary<Sprite, XElement>();
[Serialize(10000, false, description: "Maximum number of this specific object per level."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
[Serialize(10000, IsPropertySaveable.No, description: "Maximum number of this specific object per level."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
public int MaxCount
{
get;
private set;
}
[Serialize("0.0,1.0", true), Editable]
[Serialize("0.0,1.0", IsPropertySaveable.Yes), Editable]
public Vector2 DepthRange
{
get;
@@ -135,7 +136,7 @@ namespace Barotrauma
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f),
Serialize(0.0f, true, description: "The tendency for the prefab to form clusters. Used as an exponent for perlin noise values that are used to determine the probability for an object to spawn at a specific position.")]
Serialize(0.0f, IsPropertySaveable.Yes, description: "The tendency for the prefab to form clusters. Used as an exponent for perlin noise values that are used to determine the probability for an object to spawn at a specific position.")]
/// <summary>
/// The tendency for the prefab to form clusters. Used as an exponent for perlin noise values
/// that are used to determine the probability for an object to spawn at a specific position.
@@ -147,7 +148,7 @@ namespace Barotrauma
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f),
Serialize(0.0f, true, description: "A value between 0-1 that determines the z-coordinate to sample perlin noise from when determining the probability " +
Serialize(0.0f, IsPropertySaveable.Yes, description: "A value between 0-1 that determines the z-coordinate to sample perlin noise from when determining the probability " +
" for an object to spawn at a specific position. Using the same (or close) value for different objects means the objects tend " +
"to form clusters in the same areas.")]
/// <summary>
@@ -162,35 +163,35 @@ namespace Barotrauma
private set;
}
[Editable, Serialize("0,0", true, description: "Random offset from the surface the object spawns on.")]
[Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "Random offset from the surface the object spawns on.")]
public Vector2 RandomOffset
{
get;
private set;
}
[Editable, Serialize(false, true, description: "Should the object be rotated to align it with the wall surface it spawns on.")]
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Should the object be rotated to align it with the wall surface it spawns on.")]
public bool AlignWithSurface
{
get;
private set;
}
[Editable, Serialize(true, true, description: "Can the object be placed near the start of the level.")]
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the object be placed near the start of the level.")]
public bool AllowAtStart
{
get;
private set;
}
[Editable, Serialize(true, true, description: "Can the object be placed near the end of the level.")]
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the object be placed near the end of the level.")]
public bool AllowAtEnd
{
get;
private set;
}
[Serialize(0.0f, true, description: "Minimum length of a graph edge the object can spawn on."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum length of a graph edge the object can spawn on."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
/// <summary>
/// Minimum length of a graph edge the object can spawn on.
/// </summary>
@@ -201,7 +202,7 @@ namespace Barotrauma
}
private Vector2 randomRotation;
[Editable, Serialize("0.0,0.0", true, description: "How much the rotation of the object can vary (min and max values in degrees).")]
[Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "How much the rotation of the object can vary (min and max values in degrees).")]
public Vector2 RandomRotation
{
get { return new Vector2(MathHelper.ToDegrees(randomRotation.X), MathHelper.ToDegrees(randomRotation.Y)); }
@@ -214,7 +215,7 @@ namespace Barotrauma
public Vector2 RandomRotationRad => randomRotation;
private float swingAmount;
[Serialize(0.0f, true, description: "How much the object swings (in degrees)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 360.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much the object swings (in degrees)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 360.0f)]
public float SwingAmount
{
get { return MathHelper.ToDegrees(swingAmount); }
@@ -226,28 +227,28 @@ namespace Barotrauma
public float SwingAmountRad => swingAmount;
[Serialize(0.0f, true, description: "How fast the object swings."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the object swings."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float SwingFrequency
{
get;
private set;
}
[Editable, Serialize("0.0,0.0", true, description: "How much the scale of the object oscillates on each axis. A value of 0.5,0.5 would make the object's scale oscillate from 100% to 150%.")]
[Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "How much the scale of the object oscillates on each axis. A value of 0.5,0.5 would make the object's scale oscillate from 100% to 150%.")]
public Vector2 ScaleOscillation
{
get;
private set;
}
[Serialize(0.0f, true, description: "How fast the object's scale oscillates."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the object's scale oscillates."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float ScaleOscillationFrequency
{
get;
private set;
}
[Editable, Serialize(1.0f, true, description: "How likely it is for the object to spawn in a level. " +
[Editable, Serialize(1.0f, IsPropertySaveable.Yes, description: "How likely it is for the object to spawn in a level. " +
"This is relative to the commonness of the other objects - for example, having an object with " +
"a commonness of 1 and another with a commonness of 10 would mean the latter appears in levels 10 times as frequently as the former. " +
"The commonness value can be overridden on specific level types.")]
@@ -257,45 +258,35 @@ namespace Barotrauma
private set;
}
[Serialize(0.0f, true, description: "How much the object disrupts submarine's sonar."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much the object disrupts submarine's sonar."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float SonarDisruption
{
get;
private set;
}
[Serialize(false, true, description: "Can the object take damage from weapons/attacks that damage level walls."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Can the object take damage from weapons/attacks that damage level walls."), Editable]
public bool TakeLevelWallDamage
{
get;
private set;
}
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool HideWhenBroken
{
get;
private set;
}
[Serialize(100.0f, true), Editable]
[Serialize(100.0f, IsPropertySaveable.Yes), Editable]
public float Health
{
get;
private set;
}
public string Identifier
{
get;
set;
}
public string Name
{
get { return Identifier; }
}
public string Name => Identifier.Value;
public List<ChildObject> ChildObjects
{
@@ -303,7 +294,7 @@ namespace Barotrauma
private set;
}
public Dictionary<string, SerializableProperty> SerializableProperties
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
get; private set;
}
@@ -323,91 +314,20 @@ namespace Barotrauma
return "LevelObjectPrefab (" + Identifier + ")";
}
public static void LoadAll()
{
List.Clear();
var files = GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs);
if (files.Count() > 0)
{
foreach (var file in files)
{
LoadConfig(file.Path);
}
}
else
{
LoadConfig("Content/LevelObjects/LevelObject/Prefabs.xml");
}
}
private static void LoadConfig(string configPath)
{
try
{
XDocument doc = XMLExtensions.TryLoadXml(configPath);
if (doc == null) { return; }
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
DebugConsole.NewMessage($"Overriding all level object prefabs with '{configPath}'", Color.Yellow);
List.Clear();
}
else if (List.Any())
{
DebugConsole.Log($"Loading additional level object prefabs from file '{configPath}'");
}
foreach (XElement subElement in mainElement.Elements())
{
var element = subElement.IsOverride() ? subElement.FirstElement() : subElement;
string identifier = element.GetAttributeString("identifier", "");
var existingPrefab = List.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase));
if (existingPrefab != null)
{
if (subElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding the existing level object prefab '{identifier}' using the file '{configPath}'", Color.Yellow);
List.Remove(existingPrefab);
}
else
{
DebugConsole.ThrowError($"Error in '{configPath}': Duplicate level object prefab '{identifier}' found in '{configPath}'! Each level object prefab must have a unique identifier. " +
"Use <override></override> tags to override prefabs.");
continue;
}
}
List.Add(new LevelObjectPrefab(element));
}
}
catch (Exception e)
{
DebugConsole.ThrowError(string.Format("Failed to load LevelObject prefabs from {0}", configPath), e);
}
}
public LevelObjectPrefab(XElement element, string identifier = null)
public LevelObjectPrefab(ContentXElement element, LevelObjectPrefabsFile file, Identifier identifierOverride = default) : base(file, ParseIdentifier(identifierOverride, element))
{
ChildObjects = new List<ChildObject>();
LevelTriggerElements = new List<XElement>();
LevelTriggerElements = new List<ContentXElement>();
OverrideProperties = new List<LevelObjectPrefab>();
OverrideCommonness = new Dictionary<string, float>();
OverrideCommonness = new Dictionary<Identifier, float>();
Identifier = null;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (element != null)
{
Config = element;
Identifier = element.GetAttributeString("identifier", null) ?? identifier;
if (string.IsNullOrEmpty(Identifier))
{
#if DEBUG
DebugConsole.ThrowError($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead...");
#else
DebugConsole.AddWarning($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead...");
#endif
Identifier = element.Name.ToString();
}
LoadElements(element, -1);
LoadElements(file, element, -1);
InitProjSpecific(element);
}
@@ -419,10 +339,26 @@ namespace Barotrauma
}
}
private void LoadElements(XElement element, int parentTriggerIndex)
public static Identifier ParseIdentifier(Identifier identifierOverride, XElement element)
{
if (!identifierOverride.IsEmpty) { return identifierOverride; }
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
if (identifier.IsEmpty)
{
#if DEBUG
DebugConsole.ThrowError($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead...");
#else
DebugConsole.AddWarning($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead...");
#endif
identifier = element.NameAsIdentifier();
}
return identifier;
}
private void LoadElements(LevelObjectPrefabsFile file, ContentXElement element, int parentTriggerIndex)
{
int propertyOverrideCount = 0;
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -430,8 +366,8 @@ namespace Barotrauma
var newSprite = new Sprite(subElement, lazyLoad: true);
Sprites.Add(newSprite);
var spriteSpecificPhysicsBodyElement =
subElement.Element("PhysicsBody") ?? subElement.Element("Body") ??
subElement.Element("physicsbody") ?? subElement.Element("body");
subElement.GetChildElement("PhysicsBody") ?? subElement.GetChildElement("Body") ??
subElement.GetChildElement("physicsbody") ?? subElement.GetChildElement("body");
if (spriteSpecificPhysicsBodyElement != null)
{
SpriteSpecificPhysicsBodyElements.Add(newSprite, spriteSpecificPhysicsBodyElement);
@@ -441,7 +377,7 @@ namespace Barotrauma
DeformableSprite = new DeformableSprite(subElement, lazyLoad: true);
break;
case "overridecommonness":
string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant();
Identifier levelType = subElement.GetAttributeIdentifier("leveltype", Identifier.Empty);
if (!OverrideCommonness.ContainsKey(levelType))
{
OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f));
@@ -451,13 +387,13 @@ namespace Barotrauma
case "trigger":
OverrideProperties.Add(null);
LevelTriggerElements.Add(subElement);
LoadElements(subElement, LevelTriggerElements.Count - 1);
LoadElements(file, subElement, LevelTriggerElements.Count - 1);
break;
case "childobject":
ChildObjects.Add(new ChildObject(subElement));
break;
case "overrideproperties":
var propertyOverride = new LevelObjectPrefab(subElement, identifier: Identifier + "-" + propertyOverrideCount);
var propertyOverride = new LevelObjectPrefab(subElement, file, identifierOverride: $"{Identifier}-{propertyOverrideCount}".ToIdentifier());
OverrideProperties[OverrideProperties.Count - 1] = propertyOverride;
if (!propertyOverride.Sprites.Any() && propertyOverride.DeformableSprite == null)
{
@@ -475,12 +411,13 @@ namespace Barotrauma
}
}
partial void InitProjSpecific(XElement element);
partial void InitProjSpecific(ContentXElement element);
public float GetCommonness(CaveGenerationParams generationParams, bool requireCaveSpecificOverride = true)
{
if (generationParams?.Identifier != null &&
if (generationParams != null &&
generationParams.Identifier != Identifier.Empty &&
OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness))
{
return commonness;
@@ -490,13 +427,16 @@ namespace Barotrauma
public float GetCommonness(LevelGenerationParams generationParams)
{
if (generationParams?.Identifier != null &&
if (generationParams != null &&
generationParams.Identifier != Identifier.Empty &&
(OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) ||
(generationParams.OldIdentifier != null && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness))))
(!generationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness))))
{
return commonness;
}
return Commonness;
}
public override void Dispose() { }
}
}
@@ -184,7 +184,7 @@ namespace Barotrauma
set;
}
public string InfectIdentifier
public Identifier InfectIdentifier
{
get;
set;
@@ -199,7 +199,7 @@ namespace Barotrauma
private bool triggeredOnce;
private readonly bool triggerOnce;
public LevelTrigger(XElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "")
public LevelTrigger(ContentXElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "")
{
TriggererPosition = new Dictionary<Entity, Vector2>();
@@ -223,7 +223,7 @@ namespace Barotrauma
cameraShake = element.GetAttributeFloat("camerashake", 0.0f);
InfectIdentifier = element.GetAttributeString("infectidentifier", null);
InfectIdentifier = element.GetAttributeIdentifier("infectidentifier", Identifier.Empty);
InfectionChance = element.GetAttributeFloat("infectionchance", 0.05f);
triggerOnce = element.GetAttributeBool("triggeronce", false);
@@ -235,7 +235,7 @@ namespace Barotrauma
UseNetworkSyncing = element.GetAttributeBool("networksyncing", false);
unrotatedForce =
element.Attribute("force") != null && element.Attribute("force").Value.Contains(',') ?
element.GetAttribute("force") != null && element.GetAttribute("force").Value.Contains(',') ?
element.GetAttributeVector2("force", Vector2.Zero) :
new Vector2(element.GetAttributeFloat("force", 0.0f), 0.0f);
@@ -264,7 +264,7 @@ namespace Barotrauma
TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f);
var tagsArray = element.GetAttributeStringArray("tags", new string[0]);
var tagsArray = element.GetAttributeStringArray("tags", Array.Empty<string>());
foreach (string tag in tagsArray)
{
tags.Add(tag.ToLowerInvariant());
@@ -272,7 +272,7 @@ namespace Barotrauma
if (triggeredBy.HasFlag(TriggererType.OtherTrigger))
{
var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", new string[0]);
var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", Array.Empty<string>());
foreach (string tag in otherTagsArray)
{
allowedOtherTriggerTags.Add(tag.ToLowerInvariant());
@@ -280,7 +280,7 @@ namespace Barotrauma
}
string debugName = string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : $"LevelTrigger in {parentDebugName}";
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -317,12 +317,12 @@ namespace Barotrauma
-sa * unrotatedForce.X + ca * unrotatedForce.Y);
}
public static void LoadStatusEffect(List<StatusEffect> statusEffects, XElement element, string parentDebugName)
public static void LoadStatusEffect(List<StatusEffect> statusEffects, ContentXElement element, string parentDebugName)
{
statusEffects.Add(StatusEffect.Load(element, parentDebugName));
}
public static void LoadAttack(XElement element, string parentDebugName, bool triggerOnce, List<Attack> attacks)
public static void LoadAttack(ContentXElement element, string parentDebugName, bool triggerOnce, List<Attack> attacks)
{
var attack = new Attack(element, parentDebugName);
if (!triggerOnce)
@@ -574,7 +574,7 @@ namespace Barotrauma
else if (triggerer is Submarine submarine)
{
ApplyAttacks(attacks, worldPosition, deltaTime);
if (!string.IsNullOrWhiteSpace(InfectIdentifier))
if (!InfectIdentifier.IsEmpty)
{
submarine.AttemptBallastFloraInfection(InfectIdentifier, deltaTime, InfectionChance);
}
@@ -3,6 +3,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Immutable;
using Barotrauma.Extensions;
#if DEBUG
using System.Xml;
#else
@@ -20,93 +22,38 @@ namespace Barotrauma.RuinGeneration
class RuinGenerationParams : OutpostGenerationParams
{
public static List<RuinGenerationParams> RuinParams
{
get
{
if (paramsList == null)
{
LoadAll();
}
return paramsList;
}
}
public readonly static PrefabCollection<RuinGenerationParams> RuinParams =
new PrefabCollection<RuinGenerationParams>();
private static List<RuinGenerationParams> paramsList;
public override string Name => "RuinGenerationParams";
private readonly string filePath;
private RuinGenerationParams(XElement element, string filePath) : base(element, filePath)
{
this.filePath = filePath;
}
public static RuinGenerationParams GetRandom(Rand.RandSync randSync = Rand.RandSync.Server)
{
if (paramsList == null) { LoadAll(); }
if (paramsList.Count == 0)
{
DebugConsole.ThrowError("No ruin configuration files found in any content package.");
return new RuinGenerationParams(null, null);
}
return paramsList[Rand.Int(paramsList.Count, randSync)];
}
private static void LoadAll()
{
paramsList = new List<RuinGenerationParams>();
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig))
{
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
if (doc?.Root == null) { continue; }
foreach (XElement subElement in doc.Root.Elements())
{
var mainElement = subElement;
if (subElement.IsOverride())
{
mainElement = subElement.FirstElement();
paramsList.Clear();
DebugConsole.NewMessage($"Overriding all ruin generation parameters using the file {configFile.Path}.", Color.Yellow);
}
else if (paramsList.Any())
{
DebugConsole.NewMessage($"Adding additional ruin generation parameters from file '{configFile.Path}'");
}
var newParams = new RuinGenerationParams(mainElement, configFile.Path);
paramsList.Add(newParams);
}
}
}
public static void ClearAll()
{
paramsList?.Clear();
paramsList = null;
}
public RuinGenerationParams(ContentXElement element, RuinConfigFile file) : base(element, file) { }
public static void SaveAll()
{
#warning TODO: revise
System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings
{
Indent = true,
NewLineOnAttributes = true
};
IEnumerable<ContentPackage> packages = ContentPackageManager.LocalPackages;
#if DEBUG
packages = packages.Union(ContentPackageManager.VanillaCorePackage.ToEnumerable());
#endif
foreach (RuinGenerationParams generationParams in RuinParams)
{
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig))
foreach (RuinConfigFile configFile in packages.SelectMany(p => p.GetFiles<RuinConfigFile>()))
{
if (configFile.Path != generationParams.filePath) { continue; }
if (configFile.Path != generationParams.ContentFile.Path) { continue; }
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
if (doc == null) { continue; }
SerializableProperty.SerializeProperties(generationParams, doc.Root);
using (var writer = XmlWriter.Create(configFile.Path, settings))
using (var writer = XmlWriter.Create(configFile.Path.Value, settings))
{
doc.WriteTo(writer);
writer.Flush();
@@ -114,5 +61,7 @@ namespace Barotrauma.RuinGeneration
}
}
}
public override void Dispose() { }
}
}
@@ -67,7 +67,7 @@ namespace Barotrauma.RuinGeneration
if (interestingPosCount == 0)
{
//make sure there's at least one PositionsOfInterest in the ruins
level.PositionsOfInterest.Add(new Level.InterestingPosition(waypoints.GetRandom(Rand.RandSync.Server).WorldPosition.ToPoint(), Level.PositionType.Ruin, this));
level.PositionsOfInterest.Add(new Level.InterestingPosition(waypoints.GetRandom(Rand.RandSync.ServerAndClient).WorldPosition.ToPoint(), Level.PositionType.Ruin, this));
}
}
}
@@ -5,6 +5,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.IO;
using Barotrauma.Extensions;
using System.Collections.Immutable;
namespace Barotrauma
{
@@ -21,10 +23,26 @@ namespace Barotrauma
}
public readonly SubmarineInfo subInfo;
public LinkedSubmarinePrefab(SubmarineInfo subInfo)
public override Sprite Sprite => null;
public override string OriginalName => Name.Value;
public override LocalizedString Name => subInfo.Name;
public override ImmutableHashSet<Identifier> Tags => null;
public override ImmutableHashSet<Identifier> AllowedLinks => null;
public override MapEntityCategory Category => MapEntityCategory.Misc;
public override ImmutableHashSet<string> Aliases { get; }
public LinkedSubmarinePrefab(SubmarineInfo subInfo) : base(subInfo.Name.ToIdentifier())
{
this.subInfo = subInfo;
Aliases = Name.Value.ToEnumerable().ToImmutableHashSet();
}
protected override void CreateInstance(Rectangle rect)
@@ -71,6 +89,8 @@ namespace Barotrauma
return true;
}
}
public int CargoCapacity { get; private set; }
public LinkedSubmarine(Submarine submarine, ushort id = Entity.NullEntityID)
: base(null, submarine, id)
@@ -110,6 +130,7 @@ namespace Barotrauma
{
LinkedSubmarine sl = new LinkedSubmarine(mainSub, id);
sl.GenerateWallVertices(element);
sl.CargoCapacity = element.GetAttributeInt("cargocapacity", 0);
if (sl.wallVertices.Any())
{
sl.Rect = new Rectangle(
@@ -152,7 +173,7 @@ namespace Barotrauma
if (element.Name != "Structure") { continue; }
string name = element.GetAttributeString("name", "");
string identifier = element.GetAttributeString("identifier", "");
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
StructurePrefab prefab = Structure.FindPrefab(name, identifier);
if (prefab == null) { continue; }
@@ -173,7 +194,7 @@ namespace Barotrauma
}
// LinkedSubmarine.Load() is called from MapEntity.LoadAll()
public static LinkedSubmarine Load(XElement element, Submarine submarine, IdRemap idRemap)
public static LinkedSubmarine Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero);
LinkedSubmarine linkedSub;
@@ -206,14 +227,16 @@ namespace Barotrauma
}
}
linkedSub.filePath = element.GetAttributeString("filepath", "");
int[] linkedToIds = element.GetAttributeIntArray("linkedto", new int[0]);
#warning TODO: revise
linkedSub.filePath = element.GetAttributeContentPath("filepath")?.Value ?? string.Empty;
int[] linkedToIds = element.GetAttributeIntArray("linkedto", Array.Empty<int>());
for (int i = 0; i < linkedToIds.Length; i++)
{
linkedSub.linkedToID.Add(idRemap.GetOffsetId(linkedToIds[i]));
}
linkedSub.originalLinkedToID = idRemap.GetOffsetId(element.GetAttributeInt("originallinkedto", 0));
linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0);
linkedSub.CargoCapacity = element.GetAttributeInt("cargocapacity", 0);
return linkedSub.loadSub ? linkedSub : null;
}
@@ -269,7 +292,7 @@ namespace Barotrauma
DockingPort linkedPort = null;
DockingPort myPort = null;
MapEntity linkedItem = linkedTo.FirstOrDefault(lt => (lt is Item) && ((Item)lt).GetComponent<DockingPort>() != null);
MapEntity linkedItem = linkedTo.FirstOrDefault(lt => (lt as Item)?.GetComponent<DockingPort>() != null);
if (linkedItem == null)
{
linkedPort = DockingPort.List.FirstOrDefault(dp => dp.DockingTarget != null && dp.DockingTarget.Item.Submarine == sub);
@@ -349,7 +372,7 @@ namespace Barotrauma
wall.SetDamage(i, 0, createNetworkEvent: false);
}
}
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine != sub) { continue; }
hull.WaterVolume = 0.0f;
File diff suppressed because it is too large Load Diff
@@ -1,29 +1,29 @@
using Microsoft.Xna.Framework;
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class LocationType
class LocationType : PrefabWithUintIdentifier
{
public static readonly List<LocationType> List = new List<LocationType>();
public static readonly PrefabCollection<LocationType> Prefabs = new PrefabCollection<LocationType>();
private readonly List<string> names;
private readonly List<Sprite> portraits = new List<Sprite>();
//<name, commonness>
private readonly List<Tuple<JobPrefab, float>> hireableJobs;
private readonly ImmutableArray<(Identifier Name, float Commonness)> hireableJobs;
private readonly float totalHireableWeight;
public Dictionary<int, float> CommonnessPerZone = new Dictionary<int, float>();
public readonly string Identifier;
public readonly string Name;
public readonly Dictionary<int, float> CommonnessPerZone = new Dictionary<int, float>();
public readonly Dictionary<int, int> MinCountPerZone = new Dictionary<int, int>();
public readonly LocalizedString Name;
public readonly float BeaconStationChance;
@@ -31,8 +31,8 @@ namespace Barotrauma
public readonly List<LocationTypeChange> CanChangeTo = new List<LocationTypeChange>();
public readonly List<string> MissionIdentifiers = new List<string>();
public readonly List<string> MissionTags = new List<string>();
public readonly ImmutableArray<Identifier> MissionIdentifiers;
public readonly ImmutableArray<Identifier> MissionTags;
public readonly List<string> HideEntitySubcategories = new List<string>();
@@ -44,7 +44,15 @@ namespace Barotrauma
private set;
}
public List<string> NameFormats { get; private set; }
private ImmutableArray<string>? nameFormats = null;
public IReadOnlyList<string> NameFormats
{
get
{
nameFormats ??= TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray();
return nameFormats;
}
}
public bool HasHireableCharacters
{
@@ -56,7 +64,7 @@ namespace Barotrauma
get;
private set;
}
public string ReplaceInRadiation { get; }
public Sprite Sprite { get; private set; }
@@ -77,6 +85,8 @@ namespace Barotrauma
/// In percentages
/// </summary>
public int StorePriceModifierRange { get; } = 5;
public int DailySpecialsCount { get; } = 1;
public int RequestedGoodsCount { get; } = 1;
public List<StoreBalanceStatus> StoreBalanceStatuses { get; } = new List<StoreBalanceStatus>()
{
@@ -104,32 +114,30 @@ namespace Barotrauma
return $"LocationType (" + Identifier + ")";
}
private LocationType(XElement element)
public LocationType(ContentXElement element, LocationTypesFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName))
{
Identifier = element.GetAttributeString("identifier", element.Name.ToString());
Name = TextManager.Get("LocationName." + Identifier, fallBackTag: "unknown");
Name = TextManager.Get("LocationName." + Identifier, "unknown");
BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f);
NameFormats = TextManager.GetAll("LocationNameFormat." + Identifier);
UseInMainMenu = element.GetAttributeBool("useinmainmenu", false);
HasOutpost = element.GetAttributeBool("hasoutpost", true);
IsEnterable = element.GetAttributeBool("isenterable", HasOutpost);
MissionIdentifiers = element.GetAttributeStringArray("missionidentifiers", new string[0]).ToList();
MissionTags = element.GetAttributeStringArray("missiontags", new string[0]).ToList();
MissionIdentifiers = element.GetAttributeIdentifierArray("missionidentifiers", Array.Empty<Identifier>()).ToImmutableArray();
MissionTags = element.GetAttributeIdentifierArray("missiontags", Array.Empty<Identifier>()).ToImmutableArray();
HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", new string[0]).ToList();
HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", Array.Empty<string>()).ToList();
ReplaceInRadiation = element.GetAttributeString(nameof(ReplaceInRadiation).ToLower(), "");
string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC");
Enum.TryParse(teamStr, out OutpostTeam);
string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt");
ContentPath nameFile = element.GetAttributeContentPath("namefile") ?? ContentPath.FromRaw(null, "Content/Map/locationNames.txt");
try
{
names = File.ReadAllLines(nameFile).ToList();
names = File.ReadAllLines(nameFile.Value).ToList();
}
catch (Exception e)
{
@@ -137,7 +145,7 @@ namespace Barotrauma
names = new List<string>() { "Name file not found" };
}
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", new string[] { "" });
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty<string>());
foreach (string commonnessPerZoneStr in commonnessPerZoneStrs)
{
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
@@ -145,37 +153,36 @@ namespace Barotrauma
!int.TryParse(splitCommonnessPerZone[0].Trim(), out int zoneIndex) ||
!float.TryParse(splitCommonnessPerZone[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float zoneCommonness))
{
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone0index: zone0commonness, zone1index: zone1commonness\"");
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone1index: zone1commonness, zone2index: zone2commonness\"");
break;
}
CommonnessPerZone[zoneIndex] = zoneCommonness;
}
hireableJobs = new List<Tuple<JobPrefab, float>>();
foreach (XElement subElement in element.Elements())
string[] minCountPerZoneStrs = element.GetAttributeStringArray("mincountperzone", Array.Empty<string>());
foreach (string minCountPerZoneStr in minCountPerZoneStrs)
{
string[] splitMinCountPerZone = minCountPerZoneStr.Split(':');
if (splitMinCountPerZone.Length != 2 ||
!int.TryParse(splitMinCountPerZone[0].Trim(), out int zoneIndex) ||
!int.TryParse(splitMinCountPerZone[1].Trim(), out int minCount))
{
DebugConsole.ThrowError("Failed to read minimum count values for location type \"" + Identifier + "\" - minimum counts should be given in the format \"zone1index: zone1mincount, zone2index: zone2mincount\"");
break;
}
MinCountPerZone[zoneIndex] = minCount;
}
var hireableJobs = new List<(Identifier, float)>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "hireable":
string jobIdentifier = subElement.GetAttributeString("identifier", "");
JobPrefab jobPrefab = null;
if (jobIdentifier == "")
{
DebugConsole.ThrowError("Error in location type \""+ Identifier + "\" - hireable jobs should be configured using identifiers instead of names.");
}
else
{
jobPrefab = JobPrefab.Get(jobIdentifier.ToLowerInvariant());
}
if (jobPrefab == null)
{
DebugConsole.ThrowError("Error in in location type " + Identifier + " - could not find a job with the identifier \"" + jobIdentifier + "\".");
continue;
}
Identifier jobIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
float jobCommonness = subElement.GetAttributeFloat("commonness", 1.0f);
totalHireableWeight += jobCommonness;
Tuple<JobPrefab, float> hireableJob = new Tuple<JobPrefab, float>(jobPrefab, jobCommonness);
hireableJobs.Add(hireableJob);
hireableJobs.Add((jobIdentifier, jobCommonness));
break;
case "symbol":
Sprite = new Sprite(subElement, lazyLoad: true);
@@ -213,19 +220,22 @@ namespace Barotrauma
StoreBalanceStatuses.Add(new StoreBalanceStatus(percentage, modifier, color));
}
}
DailySpecialsCount = subElement.GetAttributeInt("dailyspecialscount", DailySpecialsCount);
RequestedGoodsCount = subElement.GetAttributeInt("requestedgoodscount", RequestedGoodsCount);
break;
}
}
this.hireableJobs = hireableJobs.ToImmutableArray();
}
public JobPrefab GetRandomHireable()
{
float randFloat = Rand.Range(0.0f, totalHireableWeight, Rand.RandSync.Server);
float randFloat = Rand.Range(0.0f, totalHireableWeight, Rand.RandSync.ServerAndClient);
foreach (Tuple<JobPrefab, float> hireable in hireableJobs)
foreach ((Identifier jobIdentifier, float commonness) in hireableJobs)
{
if (randFloat < hireable.Item2) return hireable.Item1;
randFloat -= hireable.Item2;
if (randFloat < commonness) { return JobPrefab.Prefabs[jobIdentifier]; }
randFloat -= commonness;
}
return null;
@@ -252,12 +262,13 @@ namespace Barotrauma
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false)
{
Debug.Assert(List.Count > 0, "LocationType.list.Count == 0, you probably need to initialize LocationTypes");
Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes");
List<LocationType> allowedLocationTypes =
List.FindAll(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost));
LocationType[] allowedLocationTypes =
Prefabs.Where(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost))
.OrderBy(p => p.UintIdentifier).ToArray();
if (allowedLocationTypes.Count == 0)
if (allowedLocationTypes.Length == 0)
{
DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!");
}
@@ -266,82 +277,15 @@ namespace Barotrauma
{
return ToolBox.SelectWeightedRandom(
allowedLocationTypes,
allowedLocationTypes.Select(a => a.CommonnessPerZone[zone.Value]).ToList(),
allowedLocationTypes.Select(a => a.CommonnessPerZone[zone.Value]).ToArray(),
rand);
}
else
{
return allowedLocationTypes[rand.Next() % allowedLocationTypes.Count];
return allowedLocationTypes[rand.Next() % allowedLocationTypes.Length];
}
}
public static void Init()
{
List.Clear();
var locationTypeFiles = GameMain.Instance.GetFilesOfType(ContentType.LocationTypes);
if (!locationTypeFiles.Any())
{
DebugConsole.ThrowError("No location types configured in any of the selected content packages. Attempting to load from the vanilla content package...");
locationTypeFiles = ContentPackage.GetFilesOfType(GameMain.VanillaContent.ToEnumerable(), ContentType.LocationTypes);
if (!locationTypeFiles.Any())
{
throw new Exception("No location types configured in any of the selected content packages. Please try uninstalling mods or reinstalling the game.");
}
}
foreach (ContentFile file in locationTypeFiles)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
DebugConsole.NewMessage($"Overriding all location types with '{file.Path}'", Color.Yellow);
List.Clear();
}
else if (List.Any())
{
DebugConsole.NewMessage($"Loading additional location types from file '{file.Path}'");
}
foreach (XElement sourceElement in mainElement.Elements())
{
var element = sourceElement;
bool allowOverriding = false;
if (sourceElement.IsOverride())
{
element = sourceElement.FirstElement();
allowOverriding = true;
}
string identifier = element.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"Error in '{file.Path}': No identifier defined for {element.Name.ToString()}");
continue;
}
var duplicate = List.FirstOrDefault(l => l.Identifier == identifier);
if (duplicate != null)
{
if (allowOverriding)
{
List.Remove(duplicate);
DebugConsole.NewMessage($"Overriding the location type with the identifier '{identifier}' with '{file.Path}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Error in '{file.Path}': Duplicate identifier defined with the identifier '{identifier}'");
continue;
}
}
LocationType locationType = new LocationType(element);
List.Add(locationType);
}
}
foreach (EventSet eventSet in EventSet.List)
{
eventSet.CheckLocationTypeErrors();
}
}
public override void Dispose() { }
}
}
@@ -1,8 +1,10 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -21,7 +23,7 @@ namespace Barotrauma
/// <summary>
/// The change can only happen if there's at least one of the given types of locations near this one
/// </summary>
public readonly List<string> RequiredLocations;
public readonly ImmutableArray<Identifier> RequiredLocations;
/// <summary>
/// How close the location needs to be to one of the RequiredLocations for the change to occur
@@ -55,7 +57,7 @@ namespace Barotrauma
public Requirement(XElement element, LocationTypeChange change)
{
RequiredLocations = element.GetAttributeStringArray("requiredlocations", element.GetAttributeStringArray("requiredadjacentlocations", new string[0])).ToList();
RequiredLocations = element.GetAttributeIdentifierArray("requiredlocations", element.GetAttributeIdentifierArray("requiredadjacentlocations", Array.Empty<Identifier>())).ToImmutableArray();
RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1);
ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f);
RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1);
@@ -124,9 +126,9 @@ namespace Barotrauma
}
}
public readonly string CurrentType;
public readonly Identifier CurrentType;
public readonly string ChangeToType;
public readonly Identifier ChangeToType;
/// <summary>
/// Base probability per turn for the location to change if near one of the RequiredLocations
@@ -137,12 +139,33 @@ namespace Barotrauma
public List<Requirement> Requirements = new List<Requirement>();
public List<string> Messages = new List<string>();
private readonly bool requireChangeMessages;
private readonly string messageTag;
private ImmutableArray<string>? messages = null;
public IReadOnlyList<string> Messages
{
get
{
if (!messages.HasValue)
{
messages = TextManager.GetAll(messageTag).ToImmutableArray();
if (messages.Value.None())
{
if (requireChangeMessages)
{
DebugConsole.ThrowError($"No messages defined for the location type change {CurrentType} -> {ChangeToType}");
}
}
}
return messages.Value;
}
}
/// <summary>
/// The change can't happen if there's one or more of the given types of locations near this one
/// </summary>
public readonly List<string> DisallowedAdjacentLocations;
public readonly ImmutableArray<Identifier> DisallowedAdjacentLocations;
/// <summary>
/// How close the location needs to be to one of the DisallowedAdjacentLocations for the change to be disabled
@@ -156,14 +179,14 @@ namespace Barotrauma
public readonly Point RequiredDurationRange;
public LocationTypeChange(string currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f)
public LocationTypeChange(Identifier currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f)
{
CurrentType = currentType;
ChangeToType = element.GetAttributeString("type", element.GetAttributeString("to", ""));
ChangeToType = element.GetAttributeIdentifier("type", element.GetAttributeIdentifier("to", ""));
RequireDiscovered = element.GetAttributeBool("requirediscovered", false);
DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList();
DisallowedAdjacentLocations = element.GetAttributeIdentifierArray("disallowedadjacentlocations", Array.Empty<Identifier>()).ToImmutableArray();
DisallowedProximity = Math.Max(element.GetAttributeInt("disallowedproximity", 1), 1);
RequiredDurationRange = element.GetAttributePoint("requireddurationrange", Point.Zero);
@@ -184,19 +207,10 @@ namespace Barotrauma
RequiredDurationRange = new Point(element.GetAttributeInt("requiredduration", 0));
}
string messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType);
this.requireChangeMessages = requireChangeMessages;
messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType);
Messages = TextManager.GetAll(messageTag);
if (Messages == null)
{
if (requireChangeMessages)
{
DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType);
}
Messages = new List<string>();
}
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("requirement", StringComparison.OrdinalIgnoreCase))
{
@@ -88,7 +88,7 @@ namespace Barotrauma
bool lairsFound = false;
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -112,11 +112,11 @@ namespace Barotrauma
System.Diagnostics.Debug.Assert(!Locations.Contains(null));
for (int i = 0; i < Locations.Count; i++)
{
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}", -100, 100, Rand.Range(-10, 11, Rand.RandSync.Server));
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient));
}
List<XElement> connectionElements = new List<XElement>();
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -133,10 +133,10 @@ namespace Barotrauma
Locations[locationIndices.Y].Connections.Add(connection);
connection.LevelData = new LevelData(subElement.Element("Level"));
string biomeId = subElement.GetAttributeString("biome", "");
connection.Biome =
LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeId) ??
LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.OldIdentifier == biomeId) ??
LevelGenerationParams.GetBiomes().First();
connection.Biome =
Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeId) ??
Biome.Prefabs.FirstOrDefault(b => !b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeId) ??
Biome.Prefabs.First();
Connections.Add(connection);
connectionElements.Add(subElement);
break;
@@ -182,8 +182,7 @@ namespace Barotrauma
{
for (int i = 0; i < Connections.Count; i++)
{
float maxHuntingGroundsProbability = 0.3f;
Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * maxHuntingGroundsProbability;
Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * LevelData.MaxHuntingGroundsProbability;
connectionElements[i].SetAttributeValue("hashuntinggrounds", true);
}
}
@@ -214,25 +213,33 @@ namespace Barotrauma
for (int i = 0; i < Locations.Count; i++)
{
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}", -100, 100, Rand.Range(-10, 11, Rand.RandSync.Server));
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient));
}
foreach (Location location in Locations)
{
if (!location.Type.Identifier.Equals("city", StringComparison.OrdinalIgnoreCase) &&
!location.Type.Identifier.Equals("outpost", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (location.Type.Identifier != "outpost") { continue; }
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
{
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
}
}
//if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost
if (CurrentLocation == null)
{
foreach (Location location in Locations)
{
if (!location.Type.HasOutpost) { continue; }
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.");
CurrentLocation.Discover(true);
CurrentLocation.CreateStore();
CurrentLocation.CreateStores();
InitProjectSpecific();
}
@@ -252,8 +259,8 @@ namespace Barotrauma
for (float y = 10.0f; y < Height - 10.0f; y += generationParams.VoronoiSiteInterval.Y)
{
voronoiSites.Add(new Vector2(
x + generationParams.VoronoiSiteVariance.X * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server),
y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server)));
x + generationParams.VoronoiSiteVariance.X * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient),
y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient)));
}
}
@@ -274,6 +281,7 @@ namespace Barotrauma
}
voronoiSites.Clear();
Dictionary<int, List<Location>> locationsPerZone = new Dictionary<int, List<Location>>();
foreach (GraphEdge edge in edges)
{
if (edge.Point1 == edge.Point2) { continue; }
@@ -297,12 +305,29 @@ namespace Barotrauma
Vector2[] points = new Vector2[] { edge.Point1, edge.Point2 };
int positionIndex = Rand.Int(1, Rand.RandSync.Server);
int positionIndex = Rand.Int(1, Rand.RandSync.ServerAndClient);
Vector2 position = points[positionIndex];
if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; }
int zone = GetZoneIndex(position.X);
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, existingLocations: Locations);
if (!locationsPerZone.ContainsKey(zone))
{
locationsPerZone[zone] = new List<Location>();
}
LocationType forceLocationType = null;
foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
{
if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount)
{
forceLocationType = locationType;
break;
}
}
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient),
requireOutpost: false, forceLocationType: forceLocationType, existingLocations: Locations);
locationsPerZone[zone].Add(newLocations[i]);
Locations.Add(newLocations[i]);
}
@@ -394,7 +419,7 @@ namespace Barotrauma
connectionsBetweenZones[i] = new List<LocationConnection>();
}
var shuffledConnections = Connections.ToList();
shuffledConnections.Shuffle(Rand.RandSync.Server);
shuffledConnections.Shuffle(Rand.RandSync.ServerAndClient);
foreach (var connection in shuffledConnections)
{
int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X);
@@ -447,9 +472,9 @@ namespace Barotrauma
Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ?
Connections[i].Locations[0] :
Connections[i].Locations[1];
if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase))
if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned")
{
leftMostLocation.ChangeType(LocationType.List.First(lt => lt.HasOutpost && !lt.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase)));
leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"));
}
leftMostLocation.IsGateBetweenBiomes = true;
Connections[i].Locked = true;
@@ -473,10 +498,10 @@ namespace Barotrauma
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.Server), 1.2f, 100.0f);
//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.Server), 1.0f, 100.0f);
connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-random, random, Rand.RandSync.ServerAndClient), 1.0f, 100.0f);
}
AssignBiomes();
@@ -522,21 +547,13 @@ namespace Barotrauma
{
float zoneWidth = Width / generationParams.DifficultyZones;
int zoneIndex = (int)Math.Floor(xPos / zoneWidth) + 1;
if (zoneIndex < 1)
{
return LevelGenerationParams.GetBiomes().First();
}
else if (zoneIndex >= generationParams.DifficultyZones)
{
return LevelGenerationParams.GetBiomes().Last();
}
return LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
zoneIndex = Math.Clamp(zoneIndex, 1, generationParams.DifficultyZones - 1);
return Biome.Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
}
private void AssignBiomes()
{
var biomes = LevelGenerationParams.GetBiomes();
var biomes = Biome.Prefabs;
float zoneWidth = Width / generationParams.DifficultyZones;
List<Biome> allowedBiomes = new List<Biome>(10);
@@ -550,7 +567,7 @@ namespace Barotrauma
{
if (location.MapPosition.X < zoneX)
{
location.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.Server)];
location.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.ServerAndClient)];
}
}
}
@@ -687,15 +704,15 @@ namespace Barotrauma
CurrentLocation.Discover();
SelectedLocation = null;
CurrentLocation.CreateStore();
CurrentLocation.CreateStores();
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } })
{
metadata.SetValue("campaign.location.id", CurrentLocationIndex);
metadata.SetValue("campaign.location.name", CurrentLocation.Name);
metadata.SetValue("campaign.location.biome", CurrentLocation.Biome?.Identifier ?? "null");
metadata.SetValue("campaign.location.type", CurrentLocation.Type?.Identifier ?? "null");
metadata.SetValue("campaign.location.id".ToIdentifier(), CurrentLocationIndex);
metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.Name);
metadata.SetValue("campaign.location.biome".ToIdentifier(), CurrentLocation.Biome?.Identifier ?? "null".ToIdentifier());
metadata.SetValue("campaign.location.type".ToIdentifier(), CurrentLocation.Type?.Identifier ?? "null".ToIdentifier());
}
}
@@ -726,7 +743,7 @@ namespace Barotrauma
}
}
CurrentLocation.CreateStore();
CurrentLocation.CreateStores();
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
}
@@ -864,16 +881,14 @@ namespace Barotrauma
if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; }
ProgressLocationTypeChanges(location);
if (location.Discovered)
if (!ProgressLocationTypeChanges(location) && location.Discovered)
{
location.UpdateStore();
location.UpdateStores();
}
}
}
private void ProgressLocationTypeChanges(Location location)
private bool ProgressLocationTypeChanges(Location location)
{
location.TimeSinceLastTypeChange++;
location.LocationTypeChangeCooldown--;
@@ -893,9 +908,8 @@ namespace Barotrauma
location.PendingLocationTypeChange.Value.parentMission);
if (location.PendingLocationTypeChange.Value.delay <= 0)
{
ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange);
return ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange);
}
return;
}
}
@@ -927,9 +941,9 @@ namespace Barotrauma
}
else
{
ChangeLocationType(location, selectedTypeChange);
return ChangeLocationType(location, selectedTypeChange);
}
return;
return false;
}
}
@@ -948,6 +962,8 @@ namespace Barotrauma
}
}
}
return false;
}
public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation)
@@ -995,15 +1011,15 @@ namespace Barotrauma
return distance;
}
private void ChangeLocationType(Location location, LocationTypeChange change)
private bool ChangeLocationType(Location location, LocationTypeChange change)
{
string prevName = location.Name;
var newType = LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase));
var newType = LocationType.Prefabs[change.ChangeToType];
if (newType == null)
{
DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found.");
return;
return false;
}
if (newType.OutpostTeam != location.Type.OutpostTeam ||
@@ -1020,6 +1036,7 @@ namespace Barotrauma
location.TimeSinceLastTypeChange = 0;
location.LocationTypeChangeCooldown = change.CooldownAfterChange;
location.PendingLocationTypeChange = null;
return true;
}
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change);
@@ -1053,7 +1070,7 @@ namespace Barotrauma
return;
}
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -1083,14 +1100,14 @@ namespace Barotrauma
}
}
string locationType = subElement.GetAttributeString("type", "");
Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
string prevLocationName = location.Name;
LocationType prevLocationType = location.Type;
LocationType newLocationType = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)) ?? LocationType.List.First();
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
location.ChangeType(newLocationType);
if (showNotifications && prevLocationType != location.Type)
{
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase));
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier);
if (change != null)
{
ChangeLocationTypeProjSpecific(location, prevLocationName, change);
@@ -1098,7 +1115,7 @@ namespace Barotrauma
}
}
location.LoadStore(subElement);
location.LoadStores(subElement);
location.LoadMissions(subElement);
break;
@@ -1,30 +1,31 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class MapGenerationParams : ISerializableEntity
class MapGenerationParams : Prefab, ISerializableEntity
{
private static MapGenerationParams instance;
private static string loadedFile;
public static readonly PrefabSelector<MapGenerationParams> Params = new PrefabSelector<MapGenerationParams>();
public static MapGenerationParams Instance
{
get
{
return instance;
return Params.ActivePrefab;
}
}
#if DEBUG
[Serialize(true, true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool ShowLocations { get; set; }
[Serialize(true, true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool ShowLevelTypeNames { get; set; }
[Serialize(true, true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool ShowOverlay { get; set; }
#else
public readonly bool ShowLocations = true;
@@ -32,70 +33,70 @@ namespace Barotrauma
public readonly bool ShowOverlay = true;
#endif
[Serialize(6, true)]
[Serialize(6, IsPropertySaveable.Yes)]
public int DifficultyZones { get; set; } //Number of difficulty zones
[Serialize(8000, true), Editable]
[Serialize(8000, IsPropertySaveable.Yes), Editable]
public int Width { get; set; }
[Serialize(500, true), Editable]
[Serialize(500, IsPropertySaveable.Yes), Editable]
public int Height { get; set; }
[Serialize(20.0f, true, description: "Connections with a length smaller or equal to this generate the smallest possible levels (using the MinWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)]
[Serialize(20.0f, IsPropertySaveable.Yes, description: "Connections with a length smaller or equal to this generate the smallest possible levels (using the MinWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)]
public float SmallLevelConnectionLength { get; set; }
[Serialize(200.0f, true, description: "Connections with a length larger or equal to this generate the largest possible levels (using the MaxWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)]
[Serialize(200.0f, IsPropertySaveable.Yes, description: "Connections with a length larger or equal to this generate the largest possible levels (using the MaxWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)]
public float LargeLevelConnectionLength { get; set; }
[Serialize("20,20", true, description: "How far from each other voronoi sites are placed. " +
[Serialize("20,20", IsPropertySaveable.Yes, description: "How far from each other voronoi sites are placed. " +
"Sites determine shape of the voronoi graph. Locations are placed at the vertices of the voronoi cells. " +
"(Decreasing this value causes the number of sites, and the complexity of the map, to increase exponentially - be careful when adjusting)"), Editable]
public Point VoronoiSiteInterval { get; set; }
[Serialize("5,5", true), Editable]
[Serialize("5,5", IsPropertySaveable.Yes), Editable]
public Point VoronoiSiteVariance { get; set; }
[Serialize(10.0f, true, description: "Connections smaller than this are removed."), Editable(0.0f, 500.0f)]
[Serialize(10.0f, IsPropertySaveable.Yes, description: "Connections smaller than this are removed."), Editable(0.0f, 500.0f)]
public float MinConnectionDistance { get; set; }
[Serialize(5.0f, true, description: "Locations that are closer than this to another location are removed."), Editable(0.0f, 100.0f)]
[Serialize(5.0f, IsPropertySaveable.Yes, description: "Locations that are closer than this to another location are removed."), Editable(0.0f, 100.0f)]
public float MinLocationDistance { get; set; }
[Serialize(0.1f, true, description: "ConnectionIterationMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)]
[Serialize(0.1f, IsPropertySaveable.Yes, description: "ConnectionIterationMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)]
public float ConnectionIndicatorIterationMultiplier { get; set; }
[Serialize(0.1f, true, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)]
[Serialize(0.1f, IsPropertySaveable.Yes, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)]
public float ConnectionIndicatorDisplacementMultiplier { get; set; }
public int[] GateCount { get; private set; }
public readonly ImmutableArray<int> GateCount;
#if CLIENT
[Serialize(0.75f, true), Editable(DecimalCount = 2)]
[Serialize(0.75f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public float MinZoom { get; set; }
[Serialize(1.5f, true), Editable(DecimalCount = 2)]
[Serialize(1.5f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public float MaxZoom { get; set; }
[Serialize(1.0f, true), Editable(DecimalCount = 2)]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public float MapTileScale { get; set; }
[Serialize(15.0f, true, description: "Size of the location icons in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)]
[Serialize(15.0f, IsPropertySaveable.Yes, description: "Size of the location icons in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)]
public float LocationIconSize { get; set; }
[Serialize(5.0f, true, description: "Width of the connections between locations, in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)]
[Serialize(5.0f, IsPropertySaveable.Yes, description: "Width of the connections between locations, in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)]
public float LocationConnectionWidth { get; set; }
[Serialize("220,220,100,255", true, description: "The color used to display the indicators (current location, selected location, etc)."), Editable()]
[Serialize("220,220,100,255", IsPropertySaveable.Yes, description: "The color used to display the indicators (current location, selected location, etc)."), Editable()]
public Color IndicatorColor { get; set; }
[Serialize("150,150,150,255", true, description: "The color used to display the connections between locations."), Editable()]
[Serialize("150,150,150,255", IsPropertySaveable.Yes, description: "The color used to display the connections between locations."), Editable()]
public Color ConnectionColor { get; set; }
[Serialize("150,150,150,255", true, description: "The color used to display the connections between locations when they're highlighted."), Editable()]
[Serialize("150,150,150,255", IsPropertySaveable.Yes, description: "The color used to display the connections between locations when they're highlighted."), Editable()]
public Color HighlightedConnectionColor { get; set; }
[Serialize("150,150,150,255", true, description: "The color used to display the connections the player hasn't travelled through."), Editable()]
[Serialize("150,150,150,255", IsPropertySaveable.Yes, description: "The color used to display the connections the player hasn't travelled through."), Editable()]
public Color UnvisitedConnectionColor { get; set; }
public Sprite ConnectionSprite { get; private set; }
@@ -110,110 +111,36 @@ namespace Barotrauma
public Sprite CurrentLocationIndicator { get; private set; }
public Sprite SelectedLocationIndicator { get; private set; }
private readonly Dictionary<string, List<Sprite>> mapTiles = new Dictionary<string, List<Sprite>>();
public Dictionary<string, List<Sprite>> MapTiles
{
get { return mapTiles; }
}
public readonly ImmutableDictionary<Identifier, ImmutableArray<Sprite>> MapTiles;
#endif
public string Name
{
get { return GetType().ToString(); }
}
public string Name => GetType().ToString();
public Dictionary<string, SerializableProperty> SerializableProperties
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
get; private set;
}
public RadiationParams RadiationParams;
public static void Init()
{
var files = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.MapGenerationParameters);
if (!files.Any())
{
DebugConsole.ThrowError("No map generation parameters found in the selected content packages!");
return;
}
// Let's not actually load the parameters until we have solved which file is the last, because loading the parameters takes some resources that would also need to be released.
XElement selectedElement = null;
string selectedFile = null;
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
if (selectedElement != null)
{
DebugConsole.NewMessage($"Overriding the map generation parameters with '{file.Path}'", Color.Yellow);
}
}
else if (selectedElement != null)
{
DebugConsole.ThrowError($"Error in {file.Path}: Another map generation parameter file already loaded! Use <override></override> tags to override it.");
break;
}
selectedElement = mainElement;
selectedFile = file.Path;
}
if (selectedFile == loadedFile) { return; }
#if CLIENT
if (instance != null)
{
instance?.ConnectionSprite?.Remove();
instance?.PassedConnectionSprite?.Remove();
instance?.SelectedLocationIndicator?.Remove();
instance?.CurrentLocationIndicator?.Remove();
instance?.DecorativeGraphSprite?.Remove();
instance?.MissionIcon?.Remove();
instance?.TypeChangeIcon?.Remove();
instance?.FogOfWarSprite?.Remove();
foreach (List<Sprite> spriteList in instance.mapTiles.Values)
{
foreach (Sprite sprite in spriteList)
{
sprite.Remove();
}
}
instance.mapTiles.Clear();
}
#endif
instance = null;
if (selectedElement == null)
{
DebugConsole.ThrowError("Could not find a valid element in the map generation parameter files!");
}
else
{
instance = new MapGenerationParams(selectedElement);
loadedFile = selectedFile;
}
}
private MapGenerationParams(XElement element)
public MapGenerationParams(ContentXElement element, MapGenerationParametersFile file) : base(file, file.Path.Value.ToIdentifier())
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
GateCount = element.GetAttributeIntArray("gatecount", null) ?? element.GetAttributeIntArray("GateCount", null);
if (GateCount == null)
var gateCount = element.GetAttributeIntArray("gatecount", null) ?? element.GetAttributeIntArray("GateCount", null);
if (gateCount == null)
{
GateCount = new int[DifficultyZones];
gateCount = new int[DifficultyZones];
for (int i = 0; i < DifficultyZones; i++)
{
GateCount[i] = 1;
gateCount[i] = 1;
}
}
GateCount = gateCount.ToImmutableArray();
foreach (XElement subElement in element.Elements())
Dictionary<Identifier, List<Sprite>> mapTiles = new Dictionary<Identifier, List<Sprite>>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -225,7 +152,7 @@ namespace Barotrauma
PassedConnectionSprite = new Sprite(subElement);
break;
case "maptile":
string biome = subElement.GetAttributeString("biome", "");
Identifier biome = subElement.GetAttributeIdentifier("biome", "");
if (!mapTiles.ContainsKey(biome))
{
mapTiles[biome] = new List<Sprite>();
@@ -257,6 +184,30 @@ namespace Barotrauma
break;
}
}
#if CLIENT
MapTiles = mapTiles.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary();
#endif
}
public override void Dispose()
{
#if CLIENT
ConnectionSprite?.Remove();
PassedConnectionSprite?.Remove();
SelectedLocationIndicator?.Remove();
CurrentLocationIndicator?.Remove();
DecorativeGraphSprite?.Remove();
MissionIcon?.Remove();
TypeChangeIcon?.Remove();
FogOfWarSprite?.Remove();
foreach (ImmutableArray<Sprite> spriteList in MapTiles.Values)
{
foreach (Sprite sprite in spriteList)
{
sprite.Remove();
}
}
#endif
}
}
}
@@ -12,18 +12,18 @@ namespace Barotrauma
{
public string Name => nameof(Radiation);
[Serialize(defaultValue: 0f, isSaveable: true)]
[Serialize(defaultValue: 0f, isSaveable: IsPropertySaveable.Yes)]
public float Amount { get; set; }
[Serialize(defaultValue: true, isSaveable: true)]
[Serialize(defaultValue: true, isSaveable: IsPropertySaveable.Yes)]
public bool Enabled { get; set; }
public Dictionary<string, SerializableProperty> SerializableProperties { get; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; }
public readonly Map Map;
public readonly RadiationParams Params;
private Affliction radiationAffliction;
private Affliction? radiationAffliction;
private float radiationTimer;
@@ -7,39 +7,39 @@ namespace Barotrauma
internal class RadiationParams: ISerializableEntity
{
public string Name => nameof(RadiationParams);
public Dictionary<string, SerializableProperty> SerializableProperties { get; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; }
[Serialize(defaultValue: -100f, isSaveable: false, "How much radiation the world starts with.")]
[Serialize(defaultValue: -100f, isSaveable: IsPropertySaveable.No, "How much radiation the world starts with.")]
public float StartingRadiation { get; set; }
[Serialize(defaultValue: 100f, isSaveable: false, "How much radiation is added on each step.")]
[Serialize(defaultValue: 100f, isSaveable: IsPropertySaveable.No, "How much radiation is added on each step.")]
public float RadiationStep { get; set; }
[Serialize(defaultValue: 10, isSaveable: false, "How many turns in radiation does it take for an outpost to be removed from the map.")]
[Serialize(defaultValue: 10, isSaveable: IsPropertySaveable.No, "How many turns in radiation does it take for an outpost to be removed from the map.")]
public int CriticalRadiationThreshold { get; set; }
[Serialize(defaultValue: 3, isSaveable: false, "Minimum amount of outposts in the level that cannot be removed due to radiation.")]
[Serialize(defaultValue: 3, isSaveable: IsPropertySaveable.No, "Minimum amount of outposts in the level that cannot be removed due to radiation.")]
public int MinimumOutpostAmount { get; set; }
[Serialize(defaultValue: 3f, isSaveable: false, "How fast the radiation increase animation goes.")]
[Serialize(defaultValue: 3f, isSaveable: IsPropertySaveable.No, "How fast the radiation increase animation goes.")]
public float AnimationSpeed { get; set; }
[Serialize(defaultValue: 10f, isSaveable: false, "How long it takes to apply more radiation damage while in a radiated zone.")]
[Serialize(defaultValue: 10f, isSaveable: IsPropertySaveable.No, "How long it takes to apply more radiation damage while in a radiated zone.")]
public float RadiationDamageDelay { get; set; }
[Serialize(defaultValue: 1f, isSaveable: false, "How much is the radiation affliction increased by while in a radiated zone.")]
[Serialize(defaultValue: 1f, isSaveable: IsPropertySaveable.No, "How much is the radiation affliction increased by while in a radiated zone.")]
public float RadiationDamageAmount { get; set; }
[Serialize(defaultValue: -1.0f, isSaveable: false, "Maximum amount of radiation.")]
[Serialize(defaultValue: -1.0f, isSaveable: IsPropertySaveable.No, "Maximum amount of radiation.")]
public float MaxRadiation { get; set; }
[Serialize(defaultValue: "139,0,0,85", isSaveable: false, "The color of the radiated area.")]
[Serialize(defaultValue: "139,0,0,85", isSaveable: IsPropertySaveable.No, "The color of the radiated area.")]
public Color RadiationAreaColor { get; set; }
[Serialize(defaultValue: "255,0,0,255", isSaveable: false, "The tint of the radiation border sprites.")]
[Serialize(defaultValue: "255,0,0,255", isSaveable: IsPropertySaveable.No, "The tint of the radiation border sprites.")]
public Color RadiationBorderTint { get; set; }
[Serialize(defaultValue: 16.66f, isSaveable: false, "Speed of the border spritesheet animation.")]
[Serialize(defaultValue: 16.66f, isSaveable: IsPropertySaveable.No, "Speed of the border spritesheet animation.")]
public float BorderAnimationSpeed { get; set; }
public RadiationParams(XElement element)
@@ -14,7 +14,7 @@ namespace Barotrauma
{
public static List<MapEntity> mapEntityList = new List<MapEntity>();
public readonly MapEntityPrefab prefab;
public readonly MapEntityPrefab Prefab;
protected List<ushort> linkedToID;
public List<ushort> unresolvedLinkedToID;
@@ -28,23 +28,22 @@ namespace Barotrauma
/// </summary>
protected readonly List<Upgrade> Upgrades = new List<Upgrade>();
public HashSet<string> disallowedUpgrades = new HashSet<string>();
[Editable, Serialize("", true)]
public readonly HashSet<Identifier> DisallowedUpgradeSet = new HashSet<Identifier>();
[Editable, Serialize("", IsPropertySaveable.Yes)]
public string DisallowedUpgrades
{
get { return string.Join(",", disallowedUpgrades); }
get { return string.Join(",", DisallowedUpgradeSet); }
set
{
disallowedUpgrades.Clear();
DisallowedUpgradeSet.Clear();
if (!string.IsNullOrWhiteSpace(value))
{
string[] splitTags = value.Split(',');
foreach (string tag in splitTags)
{
string[] splitTag = tag.Trim().Split(':');
splitTag[0] = splitTag[0].ToLowerInvariant();
disallowedUpgrades.Add(string.Join(":", splitTag));
DisallowedUpgradeSet.Add(string.Join(":", splitTag).ToIdentifier());
}
}
}
@@ -109,19 +108,19 @@ namespace Barotrauma
get { return false; }
}
public List<string> AllowedLinks => prefab == null ? new List<string>() : prefab.AllowedLinks;
public IEnumerable<Identifier> AllowedLinks => Prefab == null ? Enumerable.Empty<Identifier>() : Prefab.AllowedLinks;
public bool ResizeHorizontal
{
get { return prefab != null && prefab.ResizeHorizontal; }
get { return Prefab != null && Prefab.ResizeHorizontal; }
}
public bool ResizeVertical
{
get { return prefab != null && prefab.ResizeVertical; }
get { return Prefab != null && Prefab.ResizeVertical; }
}
//for upgrading the dimensions of the entity from xml
[Serialize(0, false)]
[Serialize(0, IsPropertySaveable.No)]
public int RectWidth
{
get { return rect.Width; }
@@ -132,7 +131,7 @@ namespace Barotrauma
}
}
//for upgrading the dimensions of the entity from xml
[Serialize(0, false)]
[Serialize(0, IsPropertySaveable.No)]
public int RectHeight
{
get { return rect.Height; }
@@ -148,7 +147,7 @@ namespace Barotrauma
public bool SpriteDepthOverrideIsSet { get; private set; }
public float SpriteOverrideDepth => SpriteDepth;
private float _spriteOverrideDepth = float.NaN;
[Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, true)]
[Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, IsPropertySaveable.Yes)]
public float SpriteDepth
{
get
@@ -167,10 +166,10 @@ namespace Barotrauma
}
}
[Serialize(1f, true), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)]
[Serialize(1f, IsPropertySaveable.Yes), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)]
public virtual float Scale { get; set; } = 1;
[Editable, Serialize(false, true)]
[Editable, Serialize(false, IsPropertySaveable.Yes)]
public bool HiddenInGame
{
get;
@@ -226,14 +225,14 @@ namespace Barotrauma
}
}
[Serialize(true, true)]
[Serialize(true, IsPropertySaveable.Yes)]
public bool RemoveIfLinkedOutpostDoorInUse
{
get;
protected set;
} = true;
[Serialize("", true, "Submarine editor layer")]
[Serialize("", IsPropertySaveable.Yes, "Submarine editor layer")]
public string Layer { get; set; }
/// <summary>
@@ -250,7 +249,7 @@ namespace Barotrauma
public MapEntity(MapEntityPrefab prefab, Submarine submarine, ushort id) : base(submarine, id)
{
this.prefab = prefab;
this.Prefab = prefab;
Scale = prefab != null ? prefab.Scale : 1;
}
@@ -304,12 +303,12 @@ namespace Barotrauma
return (Submarine.RectContains(WorldRect, position));
}
public bool HasUpgrade(string identifier)
public bool HasUpgrade(Identifier identifier)
{
return GetUpgrade(identifier) != null;
}
public Upgrade GetUpgrade(string identifier)
public Upgrade GetUpgrade(Identifier identifier)
{
return Upgrades.Find(upgrade => upgrade.Identifier == identifier);
}
@@ -332,7 +331,7 @@ namespace Barotrauma
{
AddUpgrade(upgrade, createNetworkEvent);
}
DebugConsole.Log($"Set (ID: {ID} {prefab.Name})'s \"{upgrade.Prefab.Name}\" upgrade to level {upgrade.Level}");
DebugConsole.Log($"Set (ID: {ID} {Prefab.Name})'s \"{upgrade.Prefab.Name}\" upgrade to level {upgrade.Level}");
}
/// <summary>
@@ -345,7 +344,7 @@ namespace Barotrauma
return false;
}
if (disallowedUpgrades.Contains(upgrade.Identifier)) { return false; }
if (DisallowedUpgradeSet.Contains(upgrade.Identifier)) { return false; }
Upgrade existingUpgrade = GetUpgrade(upgrade.Identifier);
@@ -655,7 +654,7 @@ namespace Barotrauma
IdRemap idRemap = new IdRemap(parentElement, idOffset);
List<MapEntity> entities = new List<MapEntity>();
foreach (XElement element in parentElement.Elements())
foreach (var element in parentElement.Elements())
{
string typeName = element.Name.ToString();
@@ -678,7 +677,7 @@ namespace Barotrauma
if (t == typeof(Structure))
{
string name = element.Attribute("name").Value;
string identifier = element.GetAttributeString("identifier", "");
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
StructurePrefab structurePrefab = Structure.FindPrefab(name, identifier);
if (structurePrefab == null)
{
@@ -692,7 +691,7 @@ namespace Barotrauma
try
{
MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine), typeof(IdRemap) });
MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(ContentXElement), typeof(Submarine), typeof(IdRemap) });
if (loadMethod == null)
{
DebugConsole.ThrowError("Could not find the method \"Load\" in " + t + ".");
@@ -703,7 +702,7 @@ namespace Barotrauma
}
else
{
object newEntity = loadMethod.Invoke(t, new object[] { element, submarine, idRemap });
object newEntity = loadMethod.Invoke(t, new object[] { element.FromPackage(null), submarine, idRemap });
if (newEntity != null)
{
entities.Add((MapEntity)newEntity);
@@ -1,8 +1,8 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using Barotrauma.Extensions;
namespace Barotrauma
@@ -11,19 +11,23 @@ namespace Barotrauma
enum MapEntityCategory
{
Structure = 1,
Decorative = 2,
Machine = 4,
Equipment = 8,
Electrical = 16,
Material = 32,
Misc = 64,
Alien = 128,
Wrecked = 256,
ItemAssembly = 512,
Legacy = 1024
Decorative = 2,
Machine = 4,
Medical = 8,
Weapon = 16,
Diving = 32,
Equipment = 64,
Fuel = 128,
Electrical = 256,
Material = 1024,
Alien = 2048,
Wrecked = 4096,
ItemAssembly = 8192,
Legacy = 16384,
Misc = 32768
}
abstract partial class MapEntityPrefab : IPrefab, IDisposable
abstract partial class MapEntityPrefab : PrefabWithUintIdentifier
{
public static IEnumerable<MapEntityPrefab> List
{
@@ -51,207 +55,15 @@ namespace Barotrauma
}
}
protected string originalName;
protected string identifier;
public Sprite sprite;
//which prefab has been selected for placing
public static MapEntityPrefab Selected { get; set; }
//the position where the structure is being placed (needed when stretching the structure)
protected static Vector2 placePosition;
protected ConstructorInfo constructor;
//is it possible to stretch the entity horizontally/vertically
[Serialize(false, false)]
public bool ResizeHorizontal { get; protected set; }
[Serialize(false, false)]
public bool ResizeVertical { get; protected set; }
//which prefab has been selected for placing
protected static MapEntityPrefab selected;
public string OriginalName
{
get { return originalName; }
}
public virtual string Name
{
get { return originalName; }
}
public string GetItemNameTextId()
{
var textId = $"entityname.{Identifier}";
return TextManager.ContainsTag(textId) ? textId : null;
}
public string GetHullNameTextId()
{
var textId = $"roomname.{Identifier}";
return TextManager.ContainsTag(textId) ? textId : null;
}
//Used to differentiate between items when saving/loading
//Allows changing the name of an item without breaking existing subs or having multiple items with the same name
public string Identifier
{
get { return identifier; }
}
public string FilePath { get; protected set; }
public ContentPackage ContentPackage { get; protected set; }
public HashSet<string> Tags
{
get;
protected set;
} = new HashSet<string>();
public static MapEntityPrefab Selected
{
get { return selected; }
set { selected = value; }
}
[Serialize("", false)]
public string Description
{
get;
protected set;
}
[Serialize("", false)]
public string AllowedUpgrades { get; set; }
[Serialize(false, false)]
public bool HideInMenus { get; set; }
[Serialize("", false)]
public string Subcategory { get; set; }
[Serialize(false, false)]
public bool Linkable
{
get;
protected set;
}
/// <summary>
/// Links defined to identifiers.
/// </summary>
public List<string> AllowedLinks { get; protected set; } = new List<string>();
public MapEntityCategory Category
{
get;
protected set;
}
[Serialize("1.0,1.0,1.0,1.0", false)]
public Color SpriteColor
{
get;
protected set;
}
[Serialize(1f, true), Editable(0.1f, 10f, DecimalCount = 3)]
public float Scale { get; protected set; }
//If a matching prefab is not found when loading a sub, the game will attempt to find a prefab with a matching alias.
//(allows changing names while keeping backwards compatibility with older sub files)
public HashSet<string> Aliases
{
get;
protected set;
}
public static void Init()
{
CoreEntityPrefab ep = new CoreEntityPrefab
{
identifier = "hull",
originalName = TextManager.Get("EntityName.hull"),
Description = TextManager.Get("EntityDescription.hull"),
constructor = typeof(Hull).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }),
ResizeHorizontal = true,
ResizeVertical = true,
Linkable = true
};
ep.AllowedLinks.Add("hull");
ep.Aliases = new HashSet<string> { "hull" };
CoreEntityPrefab.Prefabs.Add(ep, false);
ep = new CoreEntityPrefab
{
identifier = "gap",
originalName = TextManager.Get("EntityName.gap"),
Description = TextManager.Get("EntityDescription.gap"),
constructor = typeof(Gap).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }),
ResizeHorizontal = true,
ResizeVertical = true
};
CoreEntityPrefab.Prefabs.Add(ep, false);
ep.Aliases = new HashSet<string> { "gap" };
ep = new CoreEntityPrefab
{
identifier = "waypoint",
originalName = TextManager.Get("EntityName.waypoint"),
Description = TextManager.Get("EntityDescription.waypoint"),
constructor = typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })
};
CoreEntityPrefab.Prefabs.Add(ep, false);
ep.Aliases = new HashSet<string> { "waypoint" };
ep = new CoreEntityPrefab
{
identifier = "spawnpoint",
originalName = TextManager.Get("EntityName.spawnpoint"),
Description = TextManager.Get("EntityDescription.spawnpoint"),
constructor = typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })
};
CoreEntityPrefab.Prefabs.Add(ep, false);
ep.Aliases = new HashSet<string> { "spawnpoint" };
}
public abstract void Dispose();
public MapEntityPrefab()
{
Category = MapEntityCategory.Structure;
}
public string[] GetAllowedUpgrades()
{
return string.IsNullOrWhiteSpace(AllowedUpgrades) ? new string[0] : AllowedUpgrades.Split(",");
}
public bool HasSubCategory(string subcategory)
{
return subcategory?.Equals(this.Subcategory, StringComparison.OrdinalIgnoreCase) ?? false;
}
protected virtual void CreateInstance(Rectangle rect)
{
if (constructor == null) return;
object[] lobject = new object[] { this, rect };
constructor.Invoke(lobject);
}
#if DEBUG
public void DebugCreateInstance()
{
Rectangle rect = new Rectangle(new Point((int)Screen.Selected.Cam.WorldViewCenter.X, (int)Screen.Selected.Cam.WorldViewCenter.Y), new Point((int)Submarine.GridSize.X, (int)Submarine.GridSize.Y));
CreateInstance(rect);
}
#endif
public static bool SelectPrefab(object selection)
{
if ((selected = selection as MapEntityPrefab) != null)
if ((Selected = selection as MapEntityPrefab) != null)
{
placePosition = Vector2.Zero;
return true;
@@ -262,37 +74,45 @@ namespace Barotrauma
}
}
//a method that allows the GUIListBoxes to check through a delegate if the entityprefab is still selected
public static object GetSelected()
{
return (object)Selected;
}
/// <summary>
/// Find a matching map entity prefab
/// </summary>
/// <param name="name">The name of the item (can be omitted when searching based on identifier)</param>
/// <param name="identifier">The identifier of the item (if null, the identifier is ignored and the search is done only based on the name)</param>
[Obsolete("Prefer MapEntityPrefab.FindByIdentifier or MapEntityPrefab.FindByName")]
public static MapEntityPrefab Find(string name, string identifier = null, bool showErrorMessages = true)
{
if (name != null)
{
name = name.ToLowerInvariant();
}
return Find(name, (identifier ?? "").ToIdentifier(), showErrorMessages);
}
[Obsolete("Prefer MapEntityPrefab.FindByIdentifier or MapEntityPrefab.FindByName")]
public static MapEntityPrefab Find(string name, Identifier identifier, bool showErrorMessages = true)
{
//try to search based on identifier first
if (string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(identifier))
if (string.IsNullOrEmpty(name) && !identifier.IsEmpty)
{
foreach (MapEntityPrefab prefab in List)
{
if (prefab.identifier == identifier) { return prefab; }
}
if (CoreEntityPrefab.Prefabs.ContainsKey(identifier)) { return CoreEntityPrefab.Prefabs[identifier]; }
if (StructurePrefab.Prefabs.ContainsKey(identifier)) { return StructurePrefab.Prefabs[identifier]; }
if (ItemPrefab.Prefabs.ContainsKey(identifier)) { return ItemPrefab.Prefabs[identifier]; }
if (ItemAssemblyPrefab.Prefabs.ContainsKey(identifier)) { return ItemAssemblyPrefab.Prefabs[identifier]; }
}
foreach (MapEntityPrefab prefab in List)
{
if (identifier != null)
if (!identifier.IsEmpty)
{
if (prefab.identifier != identifier)
if (prefab.Identifier != identifier)
{
if (prefab.Aliases != null && prefab.Aliases.Any(a => a.Equals(identifier, StringComparison.OrdinalIgnoreCase)))
if (prefab.Aliases != null && prefab.Aliases.Any(a => a == identifier))
{
return prefab;
}
}
continue;
}
else
@@ -302,8 +122,8 @@ namespace Barotrauma
}
if (!string.IsNullOrEmpty(name))
{
if (prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) ||
prefab.originalName.Equals(name, StringComparison.OrdinalIgnoreCase) ||
if (prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) ||
prefab.OriginalName.Equals(name, StringComparison.OrdinalIgnoreCase) ||
(prefab.Aliases != null && prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase))))
{
return prefab;
@@ -332,28 +152,133 @@ namespace Barotrauma
return List.FirstOrDefault(p => predicate(p));
}
public static MapEntityPrefab FindByName(string name)
{
if (name.IsNullOrEmpty()) { throw new ArgumentException($"{nameof(name)} must not be null or empty"); }
return Find(prefab =>
prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) ||
prefab.OriginalName.Equals(name, StringComparison.OrdinalIgnoreCase) ||
(prefab.Aliases != null &&
prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase))));
}
public static MapEntityPrefab FindByIdentifier(Identifier identifier)
=> CoreEntityPrefab.Prefabs.TryGet(identifier, out var corePrefab) ? corePrefab
: ItemPrefab.Prefabs.TryGet(identifier, out var itemPrefab) ? itemPrefab
: StructurePrefab.Prefabs.TryGet(identifier, out var structurePrefab) ? structurePrefab
: ItemAssemblyPrefab.Prefabs.TryGet(identifier, out var itemAssemblyPrefab) ? itemAssemblyPrefab
: (MapEntityPrefab)null;
public abstract Sprite Sprite { get; }
public abstract string OriginalName { get; }
public abstract LocalizedString Name { get; }
public abstract ImmutableHashSet<Identifier> Tags { get; }
/// <summary>
/// Links defined to identifiers.
/// </summary>
public abstract ImmutableHashSet<Identifier> AllowedLinks { get; }
public abstract MapEntityCategory Category { get; }
//If a matching prefab is not found when loading a sub, the game will attempt to find a prefab with a matching alias.
//(allows changing names while keeping backwards compatibility with older sub files)
public abstract ImmutableHashSet<string> Aliases { get; }
//is it possible to stretch the entity horizontally/vertically
[Serialize(false, IsPropertySaveable.No)]
public bool ResizeHorizontal { get; protected set; }
[Serialize(false, IsPropertySaveable.No)]
public bool ResizeVertical { get; protected set; }
[Serialize("", IsPropertySaveable.No)]
public LocalizedString Description { get; protected set; }
[Serialize("", IsPropertySaveable.No)]
public string AllowedUpgrades { get; protected set; }
[Serialize(false, IsPropertySaveable.No)]
public bool HideInMenus { get; protected set; }
[Serialize("", IsPropertySaveable.No)]
public string Subcategory { get; protected set; }
[Serialize(false, IsPropertySaveable.No)]
public bool Linkable { get; protected set; }
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No)]
public Color SpriteColor { get; protected set; }
[Serialize(1f, IsPropertySaveable.Yes), Editable(0.1f, 10f, DecimalCount = 3)]
public float Scale { get; protected set; }
protected MapEntityPrefab(Identifier identifier) : base(null, identifier) { }
public MapEntityPrefab(ContentXElement element, ContentFile file) : base(file, element) { }
public string GetItemNameTextId()
{
var textId = $"entityname.{Identifier}";
return TextManager.ContainsTag(textId) ? textId : null;
}
public string GetHullNameTextId()
{
var textId = $"roomname.{Identifier}";
return TextManager.ContainsTag(textId) ? textId : null;
}
private string cachedAllowedUpgrades = "";
private ImmutableHashSet<Identifier> allowedUpgradeSet;
public IEnumerable<Identifier> GetAllowedUpgrades()
{
if (string.IsNullOrWhiteSpace(AllowedUpgrades)) { return Enumerable.Empty<Identifier>(); }
if (allowedUpgradeSet is null || cachedAllowedUpgrades != AllowedUpgrades)
{
allowedUpgradeSet = AllowedUpgrades.Split(",").ToIdentifiers().ToImmutableHashSet();
cachedAllowedUpgrades = AllowedUpgrades;
}
return allowedUpgradeSet;
}
public bool HasSubCategory(string subcategory)
{
return subcategory?.Equals(this.Subcategory, StringComparison.OrdinalIgnoreCase) ?? false;
}
protected abstract void CreateInstance(Rectangle rect);
#if DEBUG
public void DebugCreateInstance()
{
Rectangle rect = new Rectangle(new Point((int)Screen.Selected.Cam.WorldViewCenter.X, (int)Screen.Selected.Cam.WorldViewCenter.Y), new Point((int)Submarine.GridSize.X, (int)Submarine.GridSize.Y));
CreateInstance(rect);
}
#endif
/// <summary>
/// Check if the name or any of the aliases of this prefab match the given name.
/// </summary>
public bool NameMatches(string name, StringComparison comparisonType) => originalName.Equals(name, comparisonType) || (Aliases != null && Aliases.Any(a => a.Equals(name, comparisonType)));
public bool NameMatches(string name, StringComparison comparisonType) => OriginalName.Equals(name, comparisonType) || (Aliases != null && Aliases.Any(a => a.Equals(name, comparisonType)));
public bool NameMatches(IEnumerable<string> allowedNames, StringComparison comparisonType) => allowedNames.Any(n => NameMatches(n, comparisonType));
public bool IsLinkAllowed(MapEntityPrefab target)
{
if (target == null) { return false; }
if (target is StructurePrefab && AllowedLinks.Contains("structure")) { return true; }
if (target is ItemPrefab && AllowedLinks.Contains("item")) { return true; }
if (target is LinkedSubmarinePrefab && Tags.Contains("dock")) { return true; }
if (this is LinkedSubmarinePrefab && target.Tags.Contains("dock")) { return true; }
return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(identifier)
|| target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t));
}
//a method that allows the GUIListBoxes to check through a delegate if the entityprefab is still selected
public static object GetSelected()
{
return (object)selected;
if (target is StructurePrefab && AllowedLinks.Contains("structure".ToIdentifier())) { return true; }
if (target is ItemPrefab && AllowedLinks.Contains("item".ToIdentifier())) { return true; }
if (target is LinkedSubmarinePrefab && Tags.Contains("dock".ToIdentifier())) { return true; }
if (this is LinkedSubmarinePrefab && target.Tags.Contains("dock".ToIdentifier())) { return true; }
return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(Identifier)
|| target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t));
}
}
}
@@ -1,233 +0,0 @@
using System;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma
{
public class Md5Hash
{
private static readonly Regex removeWhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private const string cachePath = "Data/hashcache.txt";
private static readonly Dictionary<string, Tuple<Md5Hash, long>> cache = new Dictionary<string, Tuple<Md5Hash, long>>();
public static void LoadCache()
{
try
{
if (!File.Exists(cachePath)) { return; }
string[] lines = File.ReadAllLines(cachePath, Encoding.UTF8);
if (lines.Length <= 0 || lines[0] != GameMain.Version.ToString()) { return; }
foreach (string line in lines.Skip(1))
{
if (string.IsNullOrWhiteSpace(line)) { continue; }
string[] parts = line.Split('|');
if (parts.Length < 3) { continue; }
string path = parts[0].CleanUpPath();
string hashStr = parts[1];
long timeLong = long.Parse(parts[2]);
Md5Hash hash = new Md5Hash(hashStr);
DateTime time = DateTime.FromBinary(timeLong);
if (File.GetLastWriteTime(path) == time && !cache.ContainsKey(path))
{
cache.Add(path, new Tuple<Md5Hash, long>(hash, timeLong));
}
}
}
catch (Exception e)
{
DebugConsole.NewMessage($"Failed to load hash cache: {e.Message}\n{e.StackTrace.CleanupStackTrace()}", Microsoft.Xna.Framework.Color.Orange);
cache.Clear();
}
}
public static void SaveCache()
{
#if SERVER
//don't save to the cache if the server is owned by a client,
//since this suggests that they're running concurrently and
//will interfere with each other here
if (GameMain.Server?.OwnerConnection != null) { return; }
#endif
string[] lines = new string[cache.Count + 1];
lines[0] = GameMain.Version.ToString();
int i = 1;
foreach (KeyValuePair<string, Tuple<Md5Hash, long>> kpv in cache)
{
lines[i] = kpv.Key + "|" + kpv.Value.Item1 + "|" + kpv.Value.Item2;
i++;
}
File.WriteAllLines(cachePath, lines, Encoding.UTF8);
}
private bool LoadFromCache(string filename)
{
if (!string.IsNullOrWhiteSpace(filename))
{
filename = filename.CleanUpPath();
lock (cache)
{
if (cache.ContainsKey(filename))
{
Hash = cache[filename].Item1.Hash;
ShortHash = cache[filename].Item1.ShortHash;
return true;
}
}
}
return false;
}
public void SaveToCache(string filename, long? time = null)
{
if (string.IsNullOrWhiteSpace(filename)) { return; }
lock (cache)
{
filename = filename.CleanUpPath();
Tuple<Md5Hash, long> cacheVal = new Tuple<Md5Hash, long>(this, time ?? File.GetLastWriteTime(filename).ToBinary());
if (cache.ContainsKey(filename))
{
cache[filename] = cacheVal;
}
else
{
cache.Add(filename, cacheVal);
}
SaveCache();
}
}
public static Md5Hash FetchFromCache(string filename)
{
Md5Hash newHash = new Md5Hash();
if (newHash.LoadFromCache(filename)) { return newHash; }
return null;
}
public string Hash { get; private set; }
public string ShortHash { get; private set; }
private Md5Hash()
{
this.Hash = null;
ShortHash = null;
}
public Md5Hash(string md5Hash)
{
this.Hash = md5Hash;
ShortHash = GetShortHash(md5Hash);
}
public Md5Hash(byte[] bytes)
{
Hash = CalculateHash(bytes);
ShortHash = GetShortHash(Hash);
}
public Md5Hash(FileStream fileStream, string filename = null, bool tryLoadFromCache = true)
{
if (tryLoadFromCache)
{
if (LoadFromCache(filename)) { return; }
}
Hash = CalculateHash(fileStream);
ShortHash = GetShortHash(Hash);
SaveToCache(filename);
}
public Md5Hash(XDocument doc, string filename = null, bool tryLoadFromCache = true)
{
if (tryLoadFromCache)
{
if (LoadFromCache(filename)) { return; }
}
if (doc == null) { return; }
string docString = removeWhitespaceRegex.Replace(doc.ToString(), "");
byte[] inputBytes = Encoding.ASCII.GetBytes(docString);
Hash = CalculateHash(inputBytes);
ShortHash = GetShortHash(Hash);
SaveToCache(filename);
}
public override string ToString()
{
return Hash;
}
private string CalculateHash(FileStream stream)
{
using (MD5 md5 = MD5.Create())
{
byte[] byteHash = md5.ComputeHash(stream);
// step 2, convert byte array to hex string
StringBuilder sb = new StringBuilder();
for (int i = 0; i < byteHash.Length; i++)
{
sb.Append(byteHash[i].ToString("X2"));
}
return sb.ToString();
}
}
private string CalculateHash(byte[] bytes)
{
using (MD5 md5 = MD5.Create())
{
byte[] byteHash = md5.ComputeHash(bytes);
// step 2, convert byte array to hex string
StringBuilder sb = new StringBuilder();
for (int i = 0; i < byteHash.Length; i++)
{
sb.Append(byteHash[i].ToString("X2"));
}
return sb.ToString();
}
}
public static string GetShortHash(string fullHash)
{
if (string.IsNullOrEmpty(fullHash)) { return ""; }
return fullHash.Length < 7 ? fullHash : fullHash.Substring(0, 7);
}
public static bool RemoveFromCache(string filename)
{
if (!string.IsNullOrWhiteSpace(filename))
{
filename = filename.CleanUpPath();
lock (cache)
{
if (cache.ContainsKey(filename))
{
cache.Remove(filename);
return true;
}
}
}
return false;
}
}
}
@@ -1,93 +1,37 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal class NPCSet : IDisposable
internal class NPCSet : Prefab
{
private static List<NPCSet>? Sets { get; set; }
public readonly static PrefabCollection<NPCSet> Sets = new PrefabCollection<NPCSet>();
private string Identifier { get; }
private readonly List<HumanPrefab> Humans = new List<HumanPrefab>();
private readonly ImmutableArray<HumanPrefab> Humans;
private bool Disposed { get; set; }
private NPCSet(XElement element, string filePath)
public NPCSet(ContentXElement element, NPCSetsFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
Identifier = element.GetAttributeString("identifier", string.Empty);
foreach (XElement npcElement in element.Elements())
{
Humans.Add(new HumanPrefab(npcElement, filePath));
}
Humans = element.Elements().Select(npcElement => new HumanPrefab(npcElement, file)).ToImmutableArray();
}
public static HumanPrefab? Get(string identifier, string npcidentifier)
public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier)
{
HumanPrefab prefab = Sets.Where(set => set.Identifier == identifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault();
HumanPrefab? prefab = Sets.Where(set => set.Identifier == setIdentifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault();
if (prefab == null)
{
DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{identifier}\".");
DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\".");
return null;
}
return new HumanPrefab(prefab.Element, prefab.FilePath);
}
public static void LoadSets()
{
Sets?.ForEach(set => set.Dispose());
Sets = new List<NPCSet>();
IEnumerable<ContentFile> files = GameMain.Instance.GetFilesOfType(ContentType.NPCSets);
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
XElement? rootElement = doc?.Root;
if (doc == null || rootElement == null) { continue; }
if (doc.Root.IsOverride())
{
Sets.Clear();
DebugConsole.NewMessage($"Overriding all NPC sets with '{file.Path}'", Color.Yellow);
}
foreach (XElement element in rootElement.Elements())
{
bool isOverride = element.IsOverride();
XElement sourceElement = isOverride ? element.FirstElement() : element;
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
string identifier = sourceElement.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"No identifier defined for the NPC set config '{elementName}' in file '{file.Path}'");
continue;
}
var existingParams = Sets.Find(set => set.Identifier == identifier);
if (existingParams != null)
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding NPC set config '{identifier}' using the file '{file.Path}'", Color.Yellow);
Sets.Remove(existingParams);
}
else
{
DebugConsole.ThrowError($"Duplicate NPC set config: '{identifier}' defined in {elementName} of '{file.Path}'");
continue;
}
}
Sets.Add(new NPCSet(element, file.Path));
}
}
return prefab;
}
private void Dispose(bool disposing)
@@ -103,7 +47,7 @@ namespace Barotrauma
Disposed = true;
}
public void Dispose()
public override void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
@@ -1,164 +1,211 @@
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
{
class OutpostGenerationParams : ISerializableEntity
class OutpostGenerationParams : PrefabWithUintIdentifier, ISerializableEntity
{
public static List<OutpostGenerationParams> Params { get; private set; }
public readonly static PrefabCollection<OutpostGenerationParams> OutpostParams = new PrefabCollection<OutpostGenerationParams>();
public virtual string Name { get; private set; }
public string Identifier { get; private set; }
private readonly List<string> allowedLocationTypes = new List<string>();
private readonly HashSet<Identifier> allowedLocationTypes = new HashSet<Identifier>();
/// <summary>
/// Identifiers of the location types this outpost can appear in. If empty, can appear in all types of locations.
/// </summary>
public IEnumerable<string> AllowedLocationTypes
public IEnumerable<Identifier> AllowedLocationTypes
{
get { return allowedLocationTypes; }
}
[Serialize(10, isSaveable: true), Editable(MinValueInt = 1, MaxValueInt = 50)]
[Serialize(10, IsPropertySaveable.Yes), Editable(MinValueInt = 1, MaxValueInt = 50)]
public int TotalModuleCount
{
get;
set;
}
[Serialize(200.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
[Serialize(200.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float MinHallwayLength
{
get;
set;
}
[Serialize(false, isSaveable: true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool AlwaysDestructible
{
get;
set;
}
[Serialize(false, isSaveable: true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool AlwaysRewireable
{
get;
set;
}
[Serialize(false, isSaveable: true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool AllowStealing
{
get;
set;
}
[Serialize(true, isSaveable: true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool SpawnCrewInsideOutpost
{
get;
set;
}
[Serialize(true, isSaveable: true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool LockUnusedDoors
{
get;
set;
}
[Serialize(true, isSaveable: true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool RemoveUnusedGaps
{
get;
set;
}
[Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float MinWaterPercentage
{
get;
set;
}
[Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float MaxWaterPercentage
{
get;
set;
}
[Serialize("", isSaveable: true), Editable]
[Serialize("", IsPropertySaveable.Yes), Editable]
public string ReplaceInRadiation { get; set; }
private readonly Dictionary<string, int> moduleCounts = new Dictionary<string, int>();
private readonly Dictionary<Identifier, int> moduleCounts = new Dictionary<Identifier, int>();
public IEnumerable<KeyValuePair<string, int>> ModuleCounts
public IReadOnlyDictionary<Identifier, int> ModuleCounts
{
get { return moduleCounts; }
}
private readonly List<List<HumanPrefab>> humanPrefabLists = new List<List<HumanPrefab>>();
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
protected OutpostGenerationParams(XElement element, string filePath)
private class NpcCollection : IReadOnlyList<HumanPrefab>
{
Identifier = element.GetAttributeString("identifier", "");
Name = element.GetAttributeString("name", Identifier);
allowedLocationTypes = element.GetAttributeStringArray("allowedlocationtypes", Array.Empty<string>()).ToList();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
private class Entry
{
private readonly HumanPrefab humanPrefab = null;
private readonly Identifier setIdentifier = Identifier.Empty;
private readonly Identifier npcIdentifier = Identifier.Empty;
public Entry(HumanPrefab humanPrefab)
{
this.humanPrefab = humanPrefab;
}
if (element == null) { return; }
foreach (XElement subElement in element.Elements())
public Entry(Identifier setIdentifier, Identifier npcIdentifier)
{
this.setIdentifier = setIdentifier;
this.npcIdentifier = npcIdentifier;
}
public HumanPrefab HumanPrefab
=> humanPrefab ?? NPCSet.Get(setIdentifier, npcIdentifier);
}
private readonly List<Entry> entries = new List<Entry>();
public void Add(HumanPrefab humanPrefab)
=> entries.Add(new Entry(humanPrefab));
public void Add(Identifier setIdentifier, Identifier npcIdentifier)
=> entries.Add(new Entry(setIdentifier, npcIdentifier));
public IEnumerator<HumanPrefab> GetEnumerator()
{
foreach (var entry in entries)
{
yield return entry.HumanPrefab;
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public int Count => entries.Count;
public HumanPrefab this[int index] => entries[index].HumanPrefab;
}
private readonly ImmutableArray<IReadOnlyList<HumanPrefab>> humanPrefabCollections;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
private ImmutableHashSet<Identifier> StoreIdentifiers { get; set; }
#warning TODO: this shouldn't really accept any ContentFile, issue is that RuinConfigFile and OutpostConfigFile are separate derived classes
public OutpostGenerationParams(ContentXElement element, ContentFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
Name = element.GetAttributeString("name", Identifier.Value);
allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty<Identifier>()).ToHashSet();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
var humanPrefabCollections = new List<IReadOnlyList<HumanPrefab>>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "modulecount":
string moduleFlag = (subElement.GetAttributeString("flag", null) ?? subElement.GetAttributeString("moduletype", "")).ToLowerInvariant();
Identifier moduleFlag = subElement.GetAttributeIdentifier("flag", subElement.GetAttributeIdentifier("moduletype", ""));
moduleCounts[moduleFlag] = subElement.GetAttributeInt("count", 0);
break;
case "npcs":
humanPrefabLists.Add(new List<HumanPrefab>());
foreach (XElement npcElement in subElement.Elements())
var newCollection = new NpcCollection();
foreach (var npcElement in subElement.Elements())
{
string from = npcElement.GetAttributeString("from", string.Empty);
// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
if (!string.IsNullOrWhiteSpace(from))
Identifier from = npcElement.GetAttributeIdentifier("from", Identifier.Empty);
if (from != Identifier.Empty)
{
HumanPrefab prefab = NPCSet.Get(from, npcElement.GetAttributeString("identifier", string.Empty));
if (prefab != null)
{
humanPrefabLists.Last().Add(prefab);
}
newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty));
}
else
{
humanPrefabLists.Last().Add(new HumanPrefab(npcElement, filePath));
newCollection.Add(new HumanPrefab(npcElement, file));
}
}
humanPrefabCollections.Add(newCollection);
break;
}
}
this.humanPrefabCollections = humanPrefabCollections.ToImmutableArray();
}
public int GetModuleCount(string moduleFlag)
public int GetModuleCount(Identifier moduleFlag)
{
if (string.IsNullOrEmpty(moduleFlag) || moduleFlag == "none") { return int.MaxValue; }
if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return int.MaxValue; }
return moduleCounts.ContainsKey(moduleFlag) ? moduleCounts[moduleFlag] : 0;
}
public void SetModuleCount(string moduleFlag, int count)
public void SetModuleCount(Identifier moduleFlag, int count)
{
if (string.IsNullOrEmpty(moduleFlag) || moduleFlag == "none") { return; }
if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return; }
if (count <= 0)
{
moduleCounts.Remove(moduleFlag);
@@ -169,66 +216,42 @@ namespace Barotrauma
}
}
public void SetAllowedLocationTypes(IEnumerable<string> allowedLocationTypes)
public void SetAllowedLocationTypes(IEnumerable<Identifier> allowedLocationTypes)
{
this.allowedLocationTypes.Clear();
foreach (string locationType in allowedLocationTypes)
foreach (Identifier locationType in allowedLocationTypes)
{
if (locationType.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; }
if (locationType == "any") { continue; }
this.allowedLocationTypes.Add(locationType);
}
}
public IEnumerable<HumanPrefab> GetHumanPrefabs(Rand.RandSync randSync)
public IReadOnlyList<HumanPrefab> GetHumanPrefabs(Rand.RandSync randSync)
{
if (humanPrefabLists == null || !humanPrefabLists.Any()) { return Enumerable.Empty<HumanPrefab>(); }
return humanPrefabLists.GetRandom(randSync);
if (!humanPrefabCollections.Any()) { return Array.Empty<HumanPrefab>(); }
return humanPrefabCollections.GetRandom(randSync);
}
public static void LoadPresets()
public ImmutableHashSet<Identifier> GetStoreIdentifiers()
{
Params = new List<OutpostGenerationParams>();
var files = GameMain.Instance.GetFilesOfType(ContentType.OutpostConfig);
foreach (ContentFile file in files)
if (StoreIdentifiers == null)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc?.Root == null) { continue; }
var mainElement = doc.Root;
if (doc.Root.IsOverride())
var storeIdentifiers = new HashSet<Identifier>();
foreach (var collection in humanPrefabCollections)
{
Params.Clear();
DebugConsole.NewMessage($"Overriding all outpost generation parameters with '{file.Path}'", Color.Yellow);
}
foreach (XElement element in mainElement.Elements())
{
bool isOverride = element.IsOverride();
XElement sourceElement = isOverride ? element.FirstElement() : element;
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
string identifier = sourceElement.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
foreach (var prefab in collection)
{
DebugConsole.ThrowError($"No identifier defined for the outpost config '{elementName}' in file '{file.Path}'");
continue;
}
var existingParams = Params.Find(p => p.Identifier == identifier);
if (existingParams != null)
{
if (isOverride)
if (prefab?.CampaignInteractionType == CampaignMode.InteractionType.Store)
{
DebugConsole.NewMessage($"Overriding outpost config '{identifier}' using the file '{file.Path}'", Color.Yellow);
Params.Remove(existingParams);
}
else
{
DebugConsole.ThrowError($"Duplicate outpost config: '{identifier}' defined in {elementName} of '{file.Path}'");
continue;
storeIdentifiers.Add(prefab.Identifier);
}
}
Params.Add(new OutpostGenerationParams(element, file.Path));
}
StoreIdentifiers = storeIdentifiers.ToImmutableHashSet();
}
return StoreIdentifiers;
}
public override void Dispose() { }
}
}
@@ -3,7 +3,9 @@ using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
namespace Barotrauma
{
@@ -26,7 +28,7 @@ namespace Barotrauma
public OutpostModuleInfo.GapPosition UsedGapPositions = 0;
public readonly HashSet<string> FulfilledModuleTypes = new HashSet<string>();
public readonly HashSet<Identifier> FulfilledModuleTypes = new HashSet<Identifier>();
public Vector2 Offset;
@@ -55,22 +57,30 @@ namespace Barotrauma
}
}
public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false)
public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
return Generate(generationParams, locationType, location: null, onlyEntrance);
return Generate(generationParams, locationType, location: null, onlyEntrance, allowInvalidOutpost);
}
public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false)
public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
return Generate(generationParams, location.Type, location, onlyEntrance);
return Generate(generationParams, location.Type, location, onlyEntrance, allowInvalidOutpost);
}
private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false)
private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
var outpostModuleFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.OutpostModule);
var outpostModuleFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostModuleFile>())
.OrderBy(f => f.UintIdentifier).ToArray();
var uintIdDupes = outpostModuleFiles.Where(f1 =>
outpostModuleFiles.Any(f2 => f1 != f2 && f1.UintIdentifier == f2.UintIdentifier)).ToArray();
if (uintIdDupes.Any())
{
throw new Exception($"OutpostModuleFile UintIdentifier duplicates found: {uintIdDupes.Select(f => f.Path)}");
}
if (location != null)
{
if (location.IsCriticallyRadiated() && OutpostGenerationParams.Params.FirstOrDefault(p => p.Identifier.Equals(generationParams.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newParams)
if (location.IsCriticallyRadiated() && OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.Identifier == generationParams.ReplaceInRadiation) is { } newParams)
{
generationParams = newParams;
}
@@ -80,21 +90,21 @@ namespace Barotrauma
//load the infos of the outpost module files
List<SubmarineInfo> outpostModules = new List<SubmarineInfo>();
foreach (ContentFile outpostModuleFile in outpostModuleFiles)
foreach (var outpostModuleFile in outpostModuleFiles)
{
var subInfo = new SubmarineInfo(outpostModuleFile.Path);
var subInfo = new SubmarineInfo(outpostModuleFile.Path.Value);
if (subInfo.OutpostModuleInfo != null)
{
if (generationParams is RuinGeneration.RuinGenerationParams)
{
//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") &&
if (!subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()) &&
!generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Key)))
{
continue;
}
}
else if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin"))
else if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()))
{
continue;
}
@@ -131,14 +141,23 @@ namespace Barotrauma
selectedModules.Clear();
//select which module types the outpost should consist of
var pendingModuleFlags = onlyEntrance ? new List<string>() { generationParams.ModuleCounts.First().Key } : SelectModules(outpostModules, generationParams);
foreach (string flag in pendingModuleFlags.Distinct().ToList())
List<Identifier> pendingModuleFlags;
using (var md5 = MD5.Create())
{
if (flag.Equals("none", StringComparison.OrdinalIgnoreCase)) { continue; }
#warning TODO: cursed
pendingModuleFlags = onlyEntrance
? generationParams.ModuleCounts
.Keys.OrderBy(k => ToolBox.IdentifierToUint32Hash(k, md5))
.First().ToEnumerable().ToList()
: SelectModules(outpostModules, generationParams);
}
foreach (Identifier flag in pendingModuleFlags)
{
if (flag == "none") { continue; }
int pendingCount = pendingModuleFlags.Count(f => f == flag);
int availableModuleCount =
outpostModules
.Where(m => m.OutpostModuleInfo.ModuleFlags.Any(f => f.Equals(flag, StringComparison.OrdinalIgnoreCase)))
.Where(m => m.OutpostModuleInfo.ModuleFlags.Any(f => f == flag))
.Select(m => m.OutpostModuleInfo.MaxCount)
.DefaultIfEmpty(0)
.Sum();
@@ -154,7 +173,7 @@ namespace Barotrauma
}
//the first module is spawned separately, remove it from the list of pending modules
string initialModuleFlag = pendingModuleFlags.FirstOrDefault() ?? "airlock";
Identifier initialModuleFlag = pendingModuleFlags.FirstOrDefault().IfEmpty("airlock".ToIdentifier());
pendingModuleFlags.Remove(initialModuleFlag);
var initialModule = GetRandomModule(outpostModules, initialModuleFlag, locationType);
@@ -162,9 +181,9 @@ namespace Barotrauma
{
throw new Exception("Failed to generate an outpost (no airlock modules found).");
}
foreach (string initialFlag in initialModule.OutpostModuleInfo.ModuleFlags)
foreach (Identifier initialFlag in initialModule.OutpostModuleInfo.ModuleFlags)
{
if (pendingModuleFlags.Contains("initialFlag")) { pendingModuleFlags.Remove(initialFlag); }
if (pendingModuleFlags.Contains("initialFlag".ToIdentifier())) { pendingModuleFlags.Remove(initialFlag); }
}
if (remainingTries == 1)
@@ -176,14 +195,21 @@ namespace Barotrauma
selectedModules.Add(new PlacedModule(initialModule, null, OutpostModuleInfo.GapPosition.None));
selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag);
AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams);
if (pendingModuleFlags.Any(flag => !flag.Equals("none", StringComparison.OrdinalIgnoreCase)))
if (pendingModuleFlags.Any(flag => flag != "none"))
{
remainingTries--;
if (remainingTries <= 0)
if (!allowInvalidOutpost)
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough doors at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
remainingTries--;
if (remainingTries <= 0)
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
}
continue;
}
else
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed.");
}
continue;
}
var outpostInfo = new SubmarineInfo()
@@ -196,7 +222,7 @@ namespace Barotrauma
sub.Info.OutpostGenerationParams = generationParams;
if (!generationFailed)
{
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine != sub) { continue; }
if (string.IsNullOrEmpty(hull.RoomName) ||
@@ -223,12 +249,14 @@ namespace Barotrauma
DebugConsole.NewMessage("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost);
var outpostFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostFile>())
.OrderBy(f => f.UintIdentifier).ToArray();
if (!outpostFiles.Any())
{
throw new Exception("Failed to generate an outpost. Could not generate an outpost from the available outpost modules and there are no pre-built outposts available.");
}
var prebuiltOutpostInfo = new SubmarineInfo(outpostFiles.GetRandom(Rand.RandSync.Server).Path)
var prebuiltOutpostInfo = new SubmarineInfo(outpostFiles.GetRandom(Rand.RandSync.ServerAndClient).Path.Value)
{
Type = SubmarineType.Outpost
};
@@ -307,7 +335,10 @@ namespace Barotrauma
selectedModule.Offset =
(selectedModule.PreviousGap.WorldPosition + selectedModule.PreviousModule.Offset) -
selectedModule.ThisGap.WorldPosition;
selectedModule.Offset += moveDir * generationParams.MinHallwayLength;
if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null)
{
selectedModule.Offset += moveDir * generationParams.MinHallwayLength;
}
}
entities[selectedModule] = moduleEntities;
}
@@ -386,7 +417,7 @@ namespace Barotrauma
}
else
{
hull.WaterVolume = hull.Volume * Rand.Range(generationParams.MinWaterPercentage, generationParams.MaxWaterPercentage, Rand.RandSync.Server) * 0.01f;
hull.WaterVolume = hull.Volume * Rand.Range(generationParams.MinWaterPercentage, generationParams.MaxWaterPercentage, Rand.RandSync.ServerAndClient) * 0.01f;
}
}
}
@@ -400,13 +431,13 @@ namespace Barotrauma
/// <summary>
/// Select the number and types of the modules to use in the outpost
/// </summary>
private static List<string> SelectModules(IEnumerable<SubmarineInfo> modules, OutpostGenerationParams generationParams)
private static List<Identifier> SelectModules(IEnumerable<SubmarineInfo> modules, OutpostGenerationParams generationParams)
{
int totalModuleCount = generationParams.TotalModuleCount;
var pendingModuleFlags = new List<string>();
var pendingModuleFlags = new List<Identifier>();
bool availableModulesFound = true;
string initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Key;
Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Key;
pendingModuleFlags.Add(initialModuleFlag);
while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound)
{
@@ -426,19 +457,33 @@ namespace Barotrauma
pendingModuleFlags.Add(moduleFlag.Key);
}
}
pendingModuleFlags.Shuffle(Rand.RandSync.Server);
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);
while (pendingModuleFlags.Count < totalModuleCount)
{
//don't place "none" modules at the end because
// a. "filler rooms" at the end of a hallway are pointless
// b. placing the unnecessary filler rooms first give more options for the placement of the more important modules
pendingModuleFlags.Insert(Rand.Int(pendingModuleFlags.Count - 1, Rand.RandSync.Server), "none");
pendingModuleFlags.Insert(Rand.Int(pendingModuleFlags.Count - 1, Rand.RandSync.ServerAndClient), "none".ToIdentifier());
}
//make sure the initial module is inserted first
pendingModuleFlags.Remove(initialModuleFlag);
pendingModuleFlags.Insert(0, initialModuleFlag);
if (pendingModuleFlags.Count > totalModuleCount)
{
DebugConsole.ThrowError($"Error during outpost generation. {pendingModuleFlags.Count} modules set to be used the outpost, but total module count is only {totalModuleCount}. Leaving out some of the modules...");
int removeCount = pendingModuleFlags.Count - totalModuleCount;
for (int i = 0; i < removeCount; i++)
{
pendingModuleFlags.Remove(pendingModuleFlags.Last());
}
}
return pendingModuleFlags;
}
@@ -452,7 +497,7 @@ namespace Barotrauma
/// <param name="selectedModules">The modules we've already selected to be used in the outpost.</param>
private static bool AppendToModule(PlacedModule currentModule,
List<SubmarineInfo> availableModules,
List<string> pendingModuleFlags,
List<Identifier> pendingModuleFlags,
List<PlacedModule> selectedModules,
LocationType locationType,
bool retry = true,
@@ -461,46 +506,71 @@ namespace Barotrauma
if (pendingModuleFlags.Count == 0) { return true; }
List<PlacedModule> placedModules = new List<PlacedModule>();
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions().Randomize(Rand.RandSync.Server))
for (int i = 0; i < 2; i++)
{
if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
if (!allowExtendBelowInitialModule)
//try placing a module meant for this location type first, and if that fails, try choosing whatever fits
bool allowDifferentLocationType = i > 0;
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient))
{
//don't continue downwards if it'd extend below the airlock
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; }
}
if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition))
{
var newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType);
if (newModule != null) { placedModules.Add(newModule); }
if (pendingModuleFlags.Count == 0) { return true; }
if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
if (!allowExtendBelowInitialModule)
{
//don't continue downwards if it'd extend below the airlock
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; }
}
PlacedModule newModule = null;
//try appending to the current module if possible
if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition))
{
newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType);
}
if (newModule != null)
{
placedModules.Add(newModule);
}
else
{
//couldn't append to current module, try one of the other placed modules
foreach (PlacedModule otherModule in selectedModules)
{
if (otherModule == currentModule) { continue; }
foreach (OutpostModuleInfo.GapPosition otherGapPosition in
GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g)))
{
newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType);
if (newModule != null)
{
placedModules.Add(newModule);
break;
}
}
if (newModule != null) { break; }
}
}
if (pendingModuleFlags.Count == 0) { return true; }
}
}
//couldn't place anything, retry
if (placedModules.Count == 0 && retry && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule.PreviousModule))
//couldn't place a module anywhere, we're probably fucked!
if (placedModules.Count == 0 && retry && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule))
{
//try to append to some other module first
foreach (PlacedModule otherModule in selectedModules)
{
if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule))
{
return true;
}
}
//try to replace the previously placed module with something else that we can append to
var failedModule = currentModule;
for (int i = 0; i < 10; i++)
{
selectedModules.Remove(currentModule);
assertAllPreviousModulesPresent();
//readd the module types that the previous module was supposed to fulfill to the pending module types
pendingModuleFlags.AddRange(currentModule.FulfilledModuleTypes);
if (!availableModules.Contains(currentModule.Info)) { availableModules.Add(currentModule.Info); }
//retry
currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType);
currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType: true);
assertAllPreviousModulesPresent();
if (currentModule == null) { break; }
if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule))
{
assertAllPreviousModulesPresent();
return true;
}
}
@@ -509,9 +579,14 @@ namespace Barotrauma
foreach (PlacedModule placedModule in placedModules)
{
AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType);
AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: allowExtendBelowInitialModule);
}
return placedModules.Count > 0;
void assertAllPreviousModulesPresent()
{
System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule)));
}
}
/// <summary>
@@ -526,18 +601,19 @@ namespace Barotrauma
PlacedModule currentModule,
OutpostModuleInfo.GapPosition gapPosition,
List<SubmarineInfo> availableModules,
List<string> pendingModuleFlags,
List<Identifier> pendingModuleFlags,
List<PlacedModule> selectedModules,
LocationType locationType)
LocationType locationType,
bool allowDifferentLocationType)
{
if (pendingModuleFlags.Count == 0) { return null; }
string flagToPlace = "none";
Identifier flagToPlace = "none".ToIdentifier();
SubmarineInfo nextModule = null;
foreach (string moduleFlag in pendingModuleFlags)
foreach (Identifier moduleFlag in pendingModuleFlags)
{
flagToPlace = moduleFlag;
nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType);
nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType, allowDifferentLocationType);
if (nextModule != null) { break; }
}
@@ -547,7 +623,7 @@ namespace Barotrauma
{
Offset = currentModule.Offset + GetMoveDir(gapPosition),
};
foreach (string moduleFlag in nextModule.OutpostModuleInfo.ModuleFlags)
foreach (Identifier moduleFlag in nextModule.OutpostModuleInfo.ModuleFlags)
{
if (!pendingModuleFlags.Contains(moduleFlag)) { continue; }
if (moduleFlag != "none" || flagToPlace == "none")
@@ -578,6 +654,7 @@ namespace Barotrauma
foreach (PlacedModule otherModule in modules2)
{
if (module == otherModule) { continue; }
if (module.PreviousModule == otherModule && module.PreviousGap.ConnectedDoor == null && module.ThisGap.ConnectedDoor == null) { continue; }
if (ModulesOverlap(module, otherModule))
{
module1 = module;
@@ -675,6 +752,7 @@ namespace Barotrauma
bool solutionFound = false;
foreach (PlacedModule module in movableModules)
{
if (module.ThisGap.ConnectedDoor == null && module.PreviousGap.ConnectedDoor == null) { continue; }
Vector2 moveDir = GetMoveDir(module.ThisGapPosition);
Vector2 moveStep = moveDir * 50.0f;
Vector2 currentMove = Vector2.Zero;
@@ -711,19 +789,19 @@ namespace Barotrauma
return solutionFound;
}
private static SubmarineInfo GetRandomModule(IEnumerable<SubmarineInfo> modules, string moduleFlag, LocationType locationType)
private static SubmarineInfo GetRandomModule(IEnumerable<SubmarineInfo> modules, Identifier moduleFlag, LocationType locationType)
{
IEnumerable<SubmarineInfo> availableModules = null;
if (string.IsNullOrEmpty(moduleFlag) || moduleFlag.Equals("none"))
if (moduleFlag.IsEmpty || moduleFlag == "none")
{
availableModules = modules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || m.OutpostModuleInfo.ModuleFlags.Contains("none"));
availableModules = modules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier()));
}
else
{
availableModules = modules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
if (moduleFlag != "hallwayhorizontal" && moduleFlag != "hallwayvertical")
{
availableModules = availableModules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayhorizontal") && !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayvertical"));
availableModules = availableModules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayhorizontal".ToIdentifier()) && !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayvertical".ToIdentifier()));
}
}
@@ -731,7 +809,7 @@ namespace Barotrauma
//try to search for modules made specifically for this location type first
var modulesSuitableForLocationType =
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant()));
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
//if not found, search for modules suitable for any location type
if (!modulesSuitableForLocationType.Any())
@@ -742,52 +820,62 @@ namespace Barotrauma
if (!modulesSuitableForLocationType.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.Server);
return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
}
else
{
return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.Server);
return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
}
}
private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable<SubmarineInfo> modules, string moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType)
private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable<SubmarineInfo> modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType)
{
IEnumerable<SubmarineInfo> availableModules = null;
if (string.IsNullOrEmpty(moduleFlag) || moduleFlag.Equals("none"))
if (moduleFlag.IsEmpty || moduleFlag.Equals("none"))
{
availableModules = modules
.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none")) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition));
.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())));
}
else
{
availableModules = modules
.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition));
.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
}
availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
if (prevModule != null)
{
availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule) && CanAttachTo(prevModule, m.OutpostModuleInfo));
availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo));
}
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.ToLowerInvariant()));
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
//if not found, search for modules suitable for any location type
if (!modulesSuitableForLocationType.Any())
if (allowDifferentLocationType && !modulesSuitableForLocationType.Any())
{
modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any());
}
if (!modulesSuitableForLocationType.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.Server);
if (allowDifferentLocationType)
{
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);
}
else
{
return null;
}
}
else
{
return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.Server);
return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
}
}
@@ -807,13 +895,13 @@ namespace Barotrauma
}
}
private static IEnumerable<OutpostModuleInfo.GapPosition> GapPositions()
private readonly static OutpostModuleInfo.GapPosition[] GapPositions = new[]
{
yield return OutpostModuleInfo.GapPosition.Right;
yield return OutpostModuleInfo.GapPosition.Left;
yield return OutpostModuleInfo.GapPosition.Top;
yield return OutpostModuleInfo.GapPosition.Bottom;
}
OutpostModuleInfo.GapPosition.Right,
OutpostModuleInfo.GapPosition.Left,
OutpostModuleInfo.GapPosition.Top,
OutpostModuleInfo.GapPosition.Bottom
};
private static OutpostModuleInfo.GapPosition GetOpposingGapPosition(OutpostModuleInfo.GapPosition thisGapPosition)
{
@@ -824,7 +912,7 @@ namespace Barotrauma
OutpostModuleInfo.GapPosition.Bottom => OutpostModuleInfo.GapPosition.Top,
OutpostModuleInfo.GapPosition.Top => OutpostModuleInfo.GapPosition.Bottom,
OutpostModuleInfo.GapPosition.None => OutpostModuleInfo.GapPosition.None,
_ => throw new InvalidOperationException()
_ => throw new ArgumentException()
};
}
@@ -837,7 +925,7 @@ namespace Barotrauma
OutpostModuleInfo.GapPosition.Bottom => Vector2.UnitY,
OutpostModuleInfo.GapPosition.Top => -Vector2.UnitY,
OutpostModuleInfo.GapPosition.None => Vector2.Zero,
_ => throw new InvalidOperationException()
_ => throw new ArgumentException()
};
}
@@ -885,7 +973,7 @@ namespace Barotrauma
private static bool CanAttachTo(OutpostModuleInfo from, OutpostModuleInfo to)
{
if (!from.AllowAttachToModules.Any() || from.AllowAttachToModules.All(s => s.Equals("any", StringComparison.OrdinalIgnoreCase))) { return true; }
if (!from.AllowAttachToModules.Any() || from.AllowAttachToModules.All(s => s == "any")) { return true; }
return from.AllowAttachToModules.Any(s => to.ModuleFlags.Contains(s));
}
@@ -915,7 +1003,7 @@ namespace Barotrauma
{
for (int i = 0; i < thisJunctionBox.Connections.Count && i < previousJunctionBox.Connections.Count; i++)
{
var wirePrefab = MapEntityPrefab.Find(name: null, identifier: thisJunctionBox.Connections[i].IsPower ? "redwire" : "bluewire") as ItemPrefab;
var wirePrefab = MapEntityPrefab.FindByIdentifier((thisJunctionBox.Connections[i].IsPower ? "redwire" : "bluewire").ToIdentifier()) as ItemPrefab;
var wire = new Item(wirePrefab, thisJunctionBox.Item.Position, sub).GetComponent<Wire>();
if (!thisJunctionBox.Connections[i].TryAddLink(wire))
@@ -1006,6 +1094,10 @@ namespace Barotrauma
}
thisWayPoint.Remove();
}
else
{
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.PreviousModule.Info.Name}\".");
}
gapToRemove.ConnectedDoor?.Item.Remove();
if (hallwayLength <= 1.0f) { gapToRemove?.Remove(); }
@@ -1014,16 +1106,20 @@ namespace Barotrauma
if (hallwayLength <= 1.0f) { continue; }
var suitableModules = availableModules.Where(m =>
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) &&
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s)));
if (suitableModules.Count() == 0)
Identifier moduleFlag = (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier();
var hallwayModules = availableModules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
var suitableHallwayModules = hallwayModules.Where(m =>
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) &&
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s)));
if (suitableHallwayModules.Count() == 0)
{
suitableModules = availableModules.Where(m =>
suitableHallwayModules = hallwayModules.Where(m =>
!m.OutpostModuleInfo.AllowAttachToModules.Any() ||
m.OutpostModuleInfo.AllowAttachToModules.All(s => s.Equals("any", StringComparison.OrdinalIgnoreCase)));
m.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any"));
}
var hallwayInfo = GetRandomModule(suitableModules, isHorizontal ? "hallwayhorizontal" : "hallwayvertical", locationType);
var hallwayInfo = GetRandomModule(suitableHallwayModules, moduleFlag, locationType);
if (hallwayInfo == null)
{
DebugConsole.ThrowError($"Generating hallways between outpost modules failed. No {(isHorizontal ? "horizontal" : "vertical")} hallway modules suitable for use between the modules \"{module.Info.DisplayName}\" and \"{module.PreviousModule.Info.DisplayName}\".");
@@ -1145,7 +1241,7 @@ namespace Barotrauma
var startWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.ThisGap);
if (startWaypoint == null)
{
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.Info.Name}\".");
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {module.ThisGapPosition.ToString().ToLower()} gap of the module \"{module.Info.Name}\".");
continue;
}
var endWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.PreviousGap);
@@ -1442,50 +1538,49 @@ namespace Barotrauma
if (outpost?.Info?.OutpostGenerationParams == null) { return; }
List<HumanPrefab> killedCharacters = new List<HumanPrefab>();
Dictionary<HumanPrefab, CharacterInfo> selectedCharacters = new Dictionary<HumanPrefab, CharacterInfo>();
foreach (HumanPrefab humanPrefab in outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.Server))
List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)> selectedCharacters
= new List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)>();
var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.ServerAndClient);
foreach (HumanPrefab humanPrefab in humanPrefabs)
{
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server);
if (humanPrefab is null) { continue; }
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.ServerAndClient), randSync: Rand.RandSync.ServerAndClient);
if (location != null && location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier()))
{
killedCharacters.Add(humanPrefab);
continue;
}
selectedCharacters.Add(humanPrefab, characterInfo);
selectedCharacters.Add((humanPrefab, characterInfo));
}
//replace killed characters with new ones
foreach (HumanPrefab killedCharacter in killedCharacters)
{
int tries = 0;
while (tries < 100)
for (int tries = 0; tries < 100; tries++)
{
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: killedCharacter.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server);
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: killedCharacter.GetJobPrefab(Rand.RandSync.ServerAndClient), randSync: Rand.RandSync.ServerAndClient);
if (!location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier()))
{
selectedCharacters.Add(killedCharacter, characterInfo);
selectedCharacters.Add((killedCharacter, characterInfo));
break;
}
}
}
foreach (var selectedCharacter in selectedCharacters)
foreach ((var humanPrefab, var characterInfo) in selectedCharacters)
{
HumanPrefab humanPrefab = selectedCharacter.Key;
CharacterInfo characterInfo = selectedCharacter.Value;
Rand.SetSyncedSeed(ToolBox.StringToInt(characterInfo.Name));
ISpatialEntity gotoTarget = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, humanPrefab.GetModuleFlags(), humanPrefab.GetSpawnPointTags());
if (gotoTarget == null)
{
gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.Server);
gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.ServerAndClient);
}
characterInfo.TeamID = CharacterTeamType.FriendlyNPC;
var npc = Character.Create(CharacterPrefab.HumanConfigFile, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
var npc = Character.Create(CharacterPrefab.HumanSpeciesName, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
npc.AnimController.FindHull(gotoTarget.WorldPosition, setSubmarine: true);
npc.TeamID = CharacterTeamType.FriendlyNPC;
npc.Prefab = humanPrefab;
npc.HumanPrefab = humanPrefab;
if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier))
{
outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List<Character>());
@@ -1499,7 +1594,7 @@ namespace Barotrauma
{
npc.AddStaticHealthMultiplier(humanPrefab.HealthMultiplier);
}
humanPrefab.GiveItems(npc, outpost, Rand.RandSync.Server);
humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient);
foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true))
{
item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing;
@@ -18,46 +18,49 @@ namespace Barotrauma
Bottom = 8
}
private readonly HashSet<string> moduleFlags = new HashSet<string>();
public IEnumerable<string> ModuleFlags
private readonly HashSet<Identifier> moduleFlags = new HashSet<Identifier>();
public IEnumerable<Identifier> ModuleFlags
{
get { return moduleFlags; }
}
private readonly HashSet<string> allowAttachToModules = new HashSet<string>();
public IEnumerable<string> AllowAttachToModules
private readonly HashSet<Identifier> allowAttachToModules = new HashSet<Identifier>();
public IEnumerable<Identifier> AllowAttachToModules
{
get { return allowAttachToModules; }
}
private readonly HashSet<string> allowedLocationTypes = new HashSet<string>();
public IEnumerable<string> AllowedLocationTypes
private readonly HashSet<Identifier> allowedLocationTypes = new HashSet<Identifier>();
public IEnumerable<Identifier> AllowedLocationTypes
{
get { return allowedLocationTypes; }
}
[Serialize(100, isSaveable: true, description: "How many instances of this module can be used in one outpost."), Editable]
[Serialize(100, IsPropertySaveable.Yes, description: "How many instances of this module can be used in one outpost."), Editable]
public int MaxCount { get; set; }
[Serialize(10.0f, isSaveable: true, description: "How likely it is for the module to get picked when selecting from a set of modules during the outpost generation."), Editable]
[Serialize(10.0f, IsPropertySaveable.Yes, description: "How likely it is for the module to get picked when selecting from a set of modules during the outpost generation."), Editable]
public float Commonness { get; set; }
[Serialize(GapPosition.None, isSaveable: true, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")]
[Serialize(GapPosition.None, IsPropertySaveable.Yes, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")]
public GapPosition GapPositions { get; set; }
[Serialize(GapPosition.Right | GapPosition.Left | GapPosition.Bottom | GapPosition.Top, IsPropertySaveable.Yes, description: "Which sides of this module are allowed to attach to the previously placed module. E.g. if you want a module to always attach to the left side of the docking module, you could set this to Right.")]
public GapPosition CanAttachToPrevious { get; set; }
public string Name { get; private set; }
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public OutpostModuleInfo(SubmarineInfo submarineInfo, XElement element)
{
Name = $"OutpostModuleInfo ({submarineInfo.Name})";
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
SetFlags(
element.GetAttributeStringArray("flags", null, convertToLowerInvariant: true) ??
element.GetAttributeStringArray("moduletypes", new string[0], convertToLowerInvariant: true));
SetAllowAttachTo(element.GetAttributeStringArray("allowattachto", new string[0], convertToLowerInvariant: true));
allowedLocationTypes = new HashSet<string>(element.GetAttributeStringArray("allowedlocationtypes", new string[0], convertToLowerInvariant: true));
element.GetAttributeIdentifierArray("flags", null) ??
element.GetAttributeIdentifierArray("moduletypes", Array.Empty<Identifier>()));
SetAllowAttachTo(element.GetAttributeIdentifierArray("allowattachto", Array.Empty<Identifier>()));
allowedLocationTypes = new HashSet<Identifier>(element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty<Identifier>()));
}
public OutpostModuleInfo(SubmarineInfo submarineInfo)
@@ -68,12 +71,12 @@ namespace Barotrauma
public OutpostModuleInfo(OutpostModuleInfo original)
{
Name = original.Name;
moduleFlags = new HashSet<string>(original.moduleFlags);
allowAttachToModules = new HashSet<string>(original.allowAttachToModules);
allowedLocationTypes = new HashSet<string>(original.allowedLocationTypes);
SerializableProperties = new Dictionary<string, SerializableProperty>();
moduleFlags = new HashSet<Identifier>(original.moduleFlags);
allowAttachToModules = new HashSet<Identifier>(original.allowAttachToModules);
allowedLocationTypes = new HashSet<Identifier>(original.allowedLocationTypes);
SerializableProperties = new Dictionary<Identifier, SerializableProperty>();
GapPositions = original.GapPositions;
foreach (KeyValuePair<string, SerializableProperty> kvp in original.SerializableProperties)
foreach (KeyValuePair<Identifier, SerializableProperty> kvp in original.SerializableProperties)
{
SerializableProperties.Add(kvp.Key, kvp.Value);
if (SerializableProperty.GetSupportedTypeName(kvp.Value.PropertyType) != null)
@@ -83,51 +86,51 @@ namespace Barotrauma
}
}
public void SetFlags(IEnumerable<string> newFlags)
public void SetFlags(IEnumerable<Identifier> newFlags)
{
moduleFlags.Clear();
if (newFlags.Contains("hallwayhorizontal"))
if (newFlags.Contains("hallwayhorizontal".ToIdentifier()))
{
moduleFlags.Add("hallwayhorizontal");
if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); }
moduleFlags.Add("hallwayhorizontal".ToIdentifier());
if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); }
return;
}
if (newFlags.Contains("hallwayvertical"))
if (newFlags.Contains("hallwayvertical".ToIdentifier()))
{
moduleFlags.Add("hallwayvertical");
if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); }
moduleFlags.Add("hallwayvertical".ToIdentifier());
if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); }
return;
}
if (!newFlags.Any())
{
moduleFlags.Add("none");
moduleFlags.Add("none".ToIdentifier());
}
foreach (string flag in newFlags)
foreach (Identifier flag in newFlags)
{
if (flag == "none" && newFlags.Count() > 1) { continue; }
moduleFlags.Add(flag.ToLowerInvariant());
moduleFlags.Add(flag);
}
}
public void SetAllowAttachTo(IEnumerable<string> allowAttachTo)
public void SetAllowAttachTo(IEnumerable<Identifier> allowAttachTo)
{
allowAttachToModules.Clear();
if (!allowAttachTo.Any())
{
allowAttachToModules.Add("any");
allowAttachToModules.Add("any".ToIdentifier());
}
foreach (string flag in allowAttachTo)
foreach (Identifier flag in allowAttachTo)
{
if (flag == "any" && allowAttachTo.Count() > 1) { continue; }
allowAttachToModules.Add(flag);
}
}
public void SetAllowedLocationTypes(IEnumerable<string> allowedLocationTypes)
public void SetAllowedLocationTypes(IEnumerable<Identifier> allowedLocationTypes)
{
this.allowedLocationTypes.Clear();
foreach (string locationType in allowedLocationTypes)
foreach (Identifier locationType in allowedLocationTypes)
{
if (locationType.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; }
if (locationType == "any") { continue; }
this.allowedLocationTypes.Add(locationType);
}
}
@@ -6,29 +6,31 @@ namespace Barotrauma
{
class PriceInfo
{
public readonly int Price;
public readonly bool CanBeBought;
public int Price { get; }
public bool CanBeBought { get; }
//minimum number of items available at a given store
public readonly int MinAvailableAmount;
public int MinAvailableAmount { get; }
//maximum number of items available at a given store
public readonly int MaxAvailableAmount;
public int MaxAvailableAmount { get; }
/// <summary>
/// Can the item be a Daily Special or a Requested Good
/// </summary>
public bool CanBeSpecial { get; }
/// <summary>
/// The item isn't available in stores unless the level's difficulty is above this value
/// </summary>
public int MinLevelDifficulty { get; }
/// <summary>
/// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store.
/// </summary>
public float BuyingPriceMultiplier { get; } = 1f;
public bool DisplayNonEmpty { get; } = false;
public Identifier StoreIdentifier { get; }
/// <summary>
/// Used when both <see cref="MinAvailableAmount"/> and <see cref="MaxAvailableAmount"/> are set to 0.
/// </summary>
public const int DefaultAmount = 5;
/// <summary>
/// Can the item be a Daily Special or a Requested Good
/// </summary>
public readonly bool CanBeSpecial;
/// <summary>
/// The item isn't available in stores unless the level's difficulty is above this value
/// </summary>
public readonly int MinLevelDifficulty;
/// <summary>
/// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store.
/// </summary>
public readonly float BuyingPriceMultiplier = 1f;
/// <summary>
/// Support for the old style of determining item prices
@@ -41,14 +43,16 @@ namespace Barotrauma
MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
BuyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
CanBeBought = true;
var minAmount = GetMinAmount(element);
int minAmount = GetMinAmount(element);
MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity);
var maxAmount = GetMaxAmount(element);
int maxAmount = GetMaxAmount(element);
maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity);
MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount);
}
public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f)
public PriceInfo(int price, bool canBeBought,
int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f,
bool displayNonEmpty = false, string storeIdentifier = null)
{
Price = price;
CanBeBought = canBeBought;
@@ -58,46 +62,64 @@ namespace Barotrauma
MaxAvailableAmount = Math.Max(maxAmount, minAmount);
MinLevelDifficulty = minLevelDifficulty;
CanBeSpecial = canBeSpecial;
DisplayNonEmpty = displayNonEmpty;
StoreIdentifier = new Identifier(storeIdentifier);
}
public static List<Tuple<string, PriceInfo>> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
public static List<PriceInfo> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
{
var priceInfos = new List<PriceInfo>();
defaultPrice = null;
var basePrice = element.GetAttributeInt("baseprice", 0);
var soldByDefault = element.GetAttributeBool("soldbydefault", true);
var minAmount = GetMinAmount(element);
var maxAmount = GetMaxAmount(element);
var minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
var canBeSpecial = element.GetAttributeBool("canbespecial", true);
var buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
var priceInfos = new List<Tuple<string, PriceInfo>>();
int basePrice = element.GetAttributeInt("baseprice", 0);
int minAmount = GetMinAmount(element);
int maxAmount = GetMaxAmount(element);
int minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
bool canBeSpecial = element.GetAttributeBool("canbespecial", true);
float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false);
bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true));
foreach (XElement childElement in element.GetChildElements("price"))
{
var priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f);
var sold = childElement.GetAttributeBool("sold", soldByDefault);
priceInfos.Add(new Tuple<string, PriceInfo>(childElement.GetAttributeString("locationtype", "").ToLowerInvariant(),
new PriceInfo(price: (int)(priceMultiplier * basePrice), canBeBought: sold,
minAmount: sold ? GetMinAmount(childElement, minAmount) : 0,
maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0,
canBeSpecial,
childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty), childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier))));
float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f);
bool sold = childElement.GetAttributeBool("sold", soldByDefault);
int storeMinLevelDifficulty = childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty);
float storeBuyingMultiplier = childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier);
string backwardsCompatibleIdentifier = childElement.GetAttributeString("locationtype", "");
if (!string.IsNullOrEmpty(backwardsCompatibleIdentifier))
{
backwardsCompatibleIdentifier = $"merchant{backwardsCompatibleIdentifier}";
}
string storeIdentifier = childElement.GetAttributeString("storeidentifier", backwardsCompatibleIdentifier);
// TODO: Add some error messages if we have defined the min or max amount while the item is not sold
var priceInfo = new PriceInfo((int)(priceMultiplier * basePrice),
sold,
sold ? GetMinAmount(childElement, minAmount) : 0,
sold ? GetMaxAmount(childElement, maxAmount) : 0,
canBeSpecial,
storeMinLevelDifficulty,
storeBuyingMultiplier,
displayNonEmpty,
storeIdentifier);
priceInfos.Add(priceInfo);
}
var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true);
defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations,
minAmount: canBeBoughtAtOtherLocations ? minAmount : 0,
maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0,
canBeSpecial,
minLevelDifficulty, buyingPriceMultiplier);
bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false));
defaultPrice = new PriceInfo(basePrice,
soldElsewhere,
soldElsewhere ? minAmount : 0,
soldElsewhere ? maxAmount : 0,
canBeSpecial,
minLevelDifficulty,
buyingPriceMultiplier,
displayNonEmpty);
return priceInfos;
}
private static int GetMinAmount(XElement element, int defaultValue = 0) => element != null ?
element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) : defaultValue;
element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) :
defaultValue;
private static int GetMaxAmount(XElement element, int defaultValue = 0) => element != null ?
element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) : defaultValue;
element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) :
defaultValue;
}
}
@@ -48,11 +48,11 @@ namespace Barotrauma
{
if (!subs.Any()) yield return CoroutineStatus.Success;
Character.Controlled = null;
cam.TargetPos = Vector2.Zero;
#if CLIENT
Character.Controlled = null;
GameMain.LightManager.LosEnabled = false;
#endif
cam.TargetPos = Vector2.Zero;
Level.Loaded.TopBarrier.Enabled = false;
@@ -8,6 +8,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Immutable;
using Barotrauma.Abilities;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
@@ -56,9 +57,9 @@ namespace Barotrauma
private readonly List<Vector2> bodyDebugDimensions = new List<Vector2>();
#if DEBUG
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
#else
[Serialize(false, true)]
[Serialize(false, IsPropertySaveable.Yes)]
#endif
public bool Indestructible
{
@@ -75,7 +76,7 @@ namespace Barotrauma
public override Sprite Sprite
{
get { return prefab.sprite; }
get { return base.Prefab.Sprite; }
}
public bool IsPlatform
@@ -91,7 +92,7 @@ namespace Barotrauma
public override string Name
{
get { return prefab.Name; }
get { return base.Prefab.Name.Value; }
}
public bool HasBody
@@ -115,7 +116,7 @@ namespace Barotrauma
private float? maxHealth;
[Serialize(100.0f, true), Editable(MinValueFloat = 0)]
[Serialize(100.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0)]
public float MaxHealth
{
get => maxHealth ?? Prefab.Health;
@@ -124,7 +125,7 @@ namespace Barotrauma
private float crushDepth;
[Serialize(Level.DefaultRealWorldCrushDepth, true)]
[Serialize(Level.DefaultRealWorldCrushDepth, IsPropertySaveable.Yes)]
public float CrushDepth
{
get => crushDepth;
@@ -163,29 +164,26 @@ namespace Barotrauma
private set;
}
public StructurePrefab Prefab => prefab as StructurePrefab;
public new StructurePrefab Prefab => base.Prefab as StructurePrefab;
public HashSet<string> Tags
{
get { return prefab.Tags; }
}
public ImmutableHashSet<Identifier> Tags => Prefab.Tags;
protected Color spriteColor;
[Editable, Serialize("1.0,1.0,1.0,1.0", true)]
[Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)]
public Color SpriteColor
{
get { return spriteColor; }
set { spriteColor = value; }
}
[Editable, Serialize(false, true)]
[Editable, Serialize(false, IsPropertySaveable.Yes)]
public bool UseDropShadow
{
get;
private set;
}
[Editable, Serialize("0,0", true, description: "The position of the drop shadow relative to the structure. If set to zero, the shadow is positioned automatically so that it points towards the sub's center of mass.")]
[Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "The position of the drop shadow relative to the structure. If set to zero, the shadow is positioned automatically so that it points towards the sub's center of mass.")]
public Vector2 DropShadowOffset
{
get;
@@ -201,7 +199,7 @@ namespace Barotrauma
if (scale == value) { return; }
scale = MathHelper.Clamp(value, 0.1f, 10.0f);
float relativeScale = scale / prefab.Scale;
float relativeScale = scale / base.Prefab.Scale;
if (!ResizeHorizontal || !ResizeVertical)
{
@@ -230,7 +228,7 @@ namespace Barotrauma
protected Vector2 textureScale = Vector2.One;
[Editable(DecimalCount = 3, MinValueFloat = 0.01f, MaxValueFloat = 10f, ValueStep = 0.1f), Serialize("1.0, 1.0", false)]
[Editable(DecimalCount = 3, MinValueFloat = 0.01f, MaxValueFloat = 10f, ValueStep = 0.1f), Serialize("1.0, 1.0", IsPropertySaveable.No)]
public Vector2 TextureScale
{
get { return textureScale; }
@@ -250,7 +248,7 @@ namespace Barotrauma
}
protected Vector2 textureOffset = Vector2.Zero;
[Editable(MinValueFloat = -1000f, MaxValueFloat = 1000f, ValueStep = 10f), Serialize("0.0, 0.0", true)]
[Editable(MinValueFloat = -1000f, MaxValueFloat = 1000f, ValueStep = 10f), Serialize("0.0, 0.0", IsPropertySaveable.Yes)]
public Vector2 TextureOffset
{
get { return textureOffset; }
@@ -343,14 +341,14 @@ namespace Barotrauma
}
}
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool NoAITarget
{
get;
private set;
}
public Dictionary<string, SerializableProperty> SerializableProperties
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
get;
private set;
@@ -406,7 +404,7 @@ namespace Barotrauma
rect = rectangle;
TextureScale = sp.TextureScale;
spriteColor = prefab.SpriteColor;
spriteColor = base.Prefab.SpriteColor;
if (sp.IsHorizontal.HasValue)
{
IsHorizontal = sp.IsHorizontal.Value;
@@ -461,7 +459,7 @@ namespace Barotrauma
SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this);
#if CLIENT
foreach (XElement subElement in sp.ConfigElement.Elements())
foreach (var subElement in sp.ConfigElement.Elements())
{
if (subElement.Name.ToString().Equals("light", StringComparison.OrdinalIgnoreCase))
{
@@ -524,7 +522,7 @@ namespace Barotrauma
{
defaultRect = defaultRect
};
foreach (KeyValuePair<string, SerializableProperty> property in SerializableProperties)
foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
{
if (!property.Value.Attributes.OfType<Editable>().Any()) { continue; }
clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
@@ -572,13 +570,13 @@ namespace Barotrauma
{
if (FlippedX && IsHorizontal)
{
xsections = (int)Math.Ceiling((float)rect.Width / prefab.sprite.SourceRect.Width);
width = prefab.sprite.SourceRect.Width;
xsections = (int)Math.Ceiling((float)rect.Width / base.Prefab.Sprite.SourceRect.Width);
width = base.Prefab.Sprite.SourceRect.Width;
}
else if (FlippedY && !IsHorizontal)
{
ysections = (int)Math.Ceiling((float)rect.Height / prefab.sprite.SourceRect.Height);
width = prefab.sprite.SourceRect.Height;
ysections = (int)Math.Ceiling((float)rect.Height / base.Prefab.Sprite.SourceRect.Height);
width = base.Prefab.Sprite.SourceRect.Height;
}
else
{
@@ -1184,7 +1182,7 @@ namespace Barotrauma
{
if (damageDiff < 0.0f)
{
attacker.Info?.IncreaseSkillLevel("mechanical",
attacker.Info?.IncreaseSkillLevel("mechanical".ToIdentifier(),
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f));
}
}
@@ -1374,10 +1372,10 @@ namespace Barotrauma
}
}
public static Structure Load(XElement element, Submarine submarine, IdRemap idRemap)
public static Structure Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
string name = element.Attribute("name").Value;
string identifier = element.GetAttributeString("identifier", "");
string name = element.GetAttribute("name").Value;
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
StructurePrefab prefab = FindPrefab(name, identifier);
if (prefab == null)
@@ -1398,7 +1396,7 @@ namespace Barotrauma
}
bool hasDamage = false;
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -1421,7 +1419,7 @@ namespace Barotrauma
break;
case "upgrade":
{
var upgradeIdentifier = subElement.GetAttributeString("identifier", string.Empty);
var upgradeIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier);
int level = subElement.GetAttributeInt("level", 1);
if (upgradePrefab != null)
@@ -1442,12 +1440,12 @@ namespace Barotrauma
if (element.GetAttributeBool("flippedy", false)) { s.FlipY(false); }
//structures with a body drop a shadow by default
if (element.Attribute("usedropshadow") == null)
if (element.GetAttribute("usedropshadow") == null)
{
s.UseDropShadow = prefab.Body;
}
if (element.Attribute("noaitarget") == null)
if (element.GetAttribute("noaitarget") == null)
{
s.NoAITarget = prefab.NoAITarget;
}
@@ -1460,18 +1458,18 @@ namespace Barotrauma
return s;
}
public static StructurePrefab FindPrefab(string name, string identifier)
public static StructurePrefab FindPrefab(string name, Identifier identifier)
{
StructurePrefab prefab = null;
if (string.IsNullOrEmpty(identifier))
if (identifier.IsEmpty)
{
//legacy support:
//1. attempt to find a prefab with an empty identifier and a matching name
prefab = MapEntityPrefab.Find(name, "") as StructurePrefab;
//2. not found, attempt to find a prefab with a matching name
if (prefab == null) prefab = MapEntityPrefab.Find(name) as StructurePrefab;
if (prefab == null) { prefab = MapEntityPrefab.Find(name) as StructurePrefab; }
//3. not found, attempt to find a prefab that uses the previous name as an identifier
if (prefab == null) prefab = MapEntityPrefab.Find(null, name) as StructurePrefab;
if (prefab == null) { prefab = MapEntityPrefab.Find(null, name) as StructurePrefab; }
}
else
{
@@ -1488,8 +1486,8 @@ namespace Barotrauma
int height = ResizeVertical ? rect.Height : defaultRect.Height;
element.Add(
new XAttribute("name", prefab.Name),
new XAttribute("identifier", prefab.Identifier),
new XAttribute("name", base.Prefab.Name),
new XAttribute("identifier", base.Prefab.Identifier),
new XAttribute("ID", ID),
new XAttribute("rect",
(int)(rect.X - Submarine.HiddenSubPosition.X) + "," +
@@ -5,6 +5,7 @@ using System.Linq;
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.IO;
using System.Collections.Immutable;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
#endif
@@ -15,169 +16,98 @@ namespace Barotrauma
{
public static readonly PrefabCollection<StructurePrefab> Prefabs = new PrefabCollection<StructurePrefab>();
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public override LocalizedString Name { get; }
private string name;
public override string Name
{
get { return name; }
}
public readonly ContentXElement ConfigElement;
public XElement ConfigElement { get; private set; }
private bool canSpriteFlipX, canSpriteFlipY;
private float health;
//default size
private Vector2 size;
//does the structure have a physics body
[Serialize(false, false)]
public bool Body
{
get;
private set;
}
//rotation of the physics body in degrees
[Serialize(0.0f, false)]
public float BodyRotation
{
get;
private set;
}
//in display units
[Serialize(0.0f, false)]
public float BodyWidth
{
get;
private set;
}
//in display units
[Serialize(0.0f, false)]
public float BodyHeight
{
get;
private set;
}
//in display units
[Serialize("0.0,0.0", false)]
public Vector2 BodyOffset
{
get;
private set;
}
[Serialize(false, false)]
public bool Platform
{
get;
private set;
}
[Serialize(false, false)]
public bool AllowAttachItems
{
get;
private set;
}
[Serialize(0.0f, false)]
public float MinHealth
{
get;
set;
}
[Serialize(100.0f, false)]
public float Health
{
get { return health; }
set { health = Math.Max(value, MinHealth); }
}
[Serialize(true, false)]
public bool IndestructibleInOutposts
{
get;
set;
}
[Serialize(false, false)]
public bool CastShadow
{
get;
private set;
}
public readonly bool CanSpriteFlipX;
public readonly bool CanSpriteFlipY;
/// <summary>
/// If null, the orientation is determined automatically based on the dimensions of the structure instances
/// </summary>
public bool? IsHorizontal
public readonly bool? IsHorizontal;
public Vector2 ScaledSize => Size * Scale;
public readonly Sprite BackgroundSprite;
public override Sprite Sprite { get; }
public override string OriginalName { get; }
public override ImmutableHashSet<Identifier> Tags { get; }
public override ImmutableHashSet<Identifier> AllowedLinks { get; }
public override MapEntityCategory Category { get; }
public override ImmutableHashSet<string> Aliases { get; }
//does the structure have a physics body
[Serialize(false, IsPropertySaveable.No)]
public bool Body { get; private set; }
//rotation of the physics body in degrees
[Serialize(0.0f, IsPropertySaveable.No)]
public float BodyRotation { get; private set; }
//in display units
[Serialize(0.0f, IsPropertySaveable.No)]
public float BodyWidth { get; private set; }
//in display units
[Serialize(0.0f, IsPropertySaveable.No)]
public float BodyHeight { get; private set; }
//in display units
[Serialize("0.0,0.0", IsPropertySaveable.No)]
public Vector2 BodyOffset { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool Platform { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool AllowAttachItems { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
public float MinHealth { get; private set; }
private float health;
[Serialize(100.0f, IsPropertySaveable.No)]
public float Health
{
get;
private set;
get { return health; }
private set { health = Math.Max(value, MinHealth); }
}
[Serialize(Direction.None, false)]
public Direction StairDirection
{
get;
private set;
}
[Serialize(true, IsPropertySaveable.No)]
public bool IndestructibleInOutposts { get; private set; }
[Serialize(45.0f, false)]
public float StairAngle
{
get;
private set;
}
[Serialize(false, IsPropertySaveable.No)]
public bool CastShadow { get; private set; }
[Serialize(false, false)]
public bool NoAITarget
{
get;
private set;
}
[Serialize(Direction.None, IsPropertySaveable.No)]
public Direction StairDirection { get; private set; }
public bool CanSpriteFlipX
{
get { return canSpriteFlipX; }
}
[Serialize(45.0f, IsPropertySaveable.No)]
public float StairAngle { get; private set; }
public bool CanSpriteFlipY
{
get { return canSpriteFlipY; }
}
[Serialize(false, IsPropertySaveable.No)]
public bool NoAITarget { get; private set; }
[Serialize("0,0", true)]
public Vector2 Size
{
get { return size; }
private set { size = value; }
}
[Serialize("0,0", IsPropertySaveable.Yes)]
public Vector2 Size { get; private set; }
[Serialize("", true)]
[Serialize("", IsPropertySaveable.Yes)]
public string DamageSound { get; private set; }
public Vector2 ScaledSize => size * Scale;
protected Vector2 textureScale = Vector2.One;
[Editable(DecimalCount = 3), Serialize("1.0, 1.0", true)]
[Editable(DecimalCount = 3), Serialize("1.0, 1.0", IsPropertySaveable.Yes)]
public Vector2 TextureScale
{
get { return textureScale; }
set
private set
{
textureScale = new Vector2(
MathHelper.Clamp(value.X, 0.01f, 10),
@@ -185,263 +115,228 @@ namespace Barotrauma
}
}
public Sprite BackgroundSprite
protected override Identifier DetermineIdentifier(XElement element)
{
get;
private set;
}
public static void LoadAll(IEnumerable<ContentFile> files)
{
foreach (ContentFile file in files)
Identifier identifier = base.DetermineIdentifier(element);
string originalName = element.GetAttributeString("name", "");
if (identifier.IsEmpty && !string.IsNullOrEmpty(originalName))
{
LoadFromFile(file);
}
}
public static void LoadFromFile(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var rootElement = doc.Root;
if (rootElement.IsOverride())
{
foreach (var element in rootElement.Elements())
string categoryStr = element.GetAttributeString("category", "Misc");
if (Enum.TryParse(categoryStr, true, out MapEntityCategory category) && category.HasFlag(MapEntityCategory.Legacy))
{
foreach (var childElement in element.Elements())
{
Load(childElement, true, file);
}
}
}
else
{
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
foreach (var childElement in element.Elements())
{
Load(childElement, true, file);
}
}
else
{
Load(element, false, file);
}
identifier = $"legacystructure_{originalName.Replace(" ", "")}".ToIdentifier();
}
}
return identifier;
}
public static void RemoveByFile(string filePath)
public StructurePrefab(ContentXElement element, StructureFile file) : base(element, file)
{
Prefabs.RemoveByFile(filePath);
}
OriginalName = element.GetAttributeString("name", "");
ConfigElement = element;
private static StructurePrefab Load(XElement element, bool allowOverride, ContentFile file)
{
StructurePrefab sp = new StructurePrefab
{
originalName = element.GetAttributeString("name", ""),
FilePath = file.Path,
ContentPackage = file.ContentPackage
};
sp.name = sp.originalName;
sp.ConfigElement = element;
sp.identifier = element.GetAttributeString("identifier", "");
var parentType = element.Parent?.GetAttributeString("prefabtype", "") ?? string.Empty;
string nameIdentifier = element.GetAttributeString("nameidentifier", "");
var parentType = element.Parent?.GetAttributeIdentifier("prefabtype", Identifier.Empty) ?? Identifier.Empty;
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", "");
//only used if the item doesn't have a name/description defined in the currently selected language
string fallbackNameIdentifier = element.GetAttributeString("fallbacknameidentifier", "");
Identifier fallbackNameIdentifier = element.GetAttributeIdentifier("fallbacknameidentifier", "");
string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", "");
Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", "");
if (string.IsNullOrEmpty(sp.originalName))
Name = TextManager.Get(nameIdentifier.IsEmpty
? $"EntityName.{Identifier}"
: $"EntityName.{nameIdentifier}",
$"EntityName.{fallbackNameIdentifier}");
if (parentType == "wrecked")
{
if (string.IsNullOrEmpty(nameIdentifier))
{
sp.name = TextManager.Get("EntityName." + sp.identifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty;
}
else
{
sp.name = TextManager.Get("EntityName." + nameIdentifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty;
}
Name = TextManager.GetWithVariable("wreckeditemformat", "[name]", Name);
}
if (string.IsNullOrEmpty(sp.name))
{
sp.name = TextManager.Get("EntityName." + sp.identifier, returnNull: true) ?? $"Not defined ({sp.identifier})";
}
sp.Tags = new HashSet<string>();
Name = Name.Fallback(OriginalName);
var tags = new HashSet<Identifier>();
string joinedTags = element.GetAttributeString("tags", "");
if (string.IsNullOrEmpty(joinedTags)) joinedTags = element.GetAttributeString("Tags", "");
foreach (string tag in joinedTags.Split(','))
{
sp.Tags.Add(tag.Trim().ToLowerInvariant());
tags.Add(tag.Trim().ToIdentifier());
}
if (element.Attribute("ishorizontal") != null)
if (element.GetAttribute("ishorizontal") != null)
{
sp.IsHorizontal = element.GetAttributeBool("ishorizontal", false);
IsHorizontal = element.GetAttributeBool("ishorizontal", false);
}
foreach (XElement subElement in element.Elements())
#if CLIENT
var decorativeSprites = new List<DecorativeSprite>();
var decorativeSpriteGroups = new Dictionary<int, List<DecorativeSprite>>();
#endif
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString())
{
case "sprite":
sp.sprite = new Sprite(subElement, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null &&
subElement.Attribute("sheetindex") == null)
Sprite = new Sprite(subElement, lazyLoad: true);
if (subElement.GetAttribute("sourcerect") == null &&
subElement.GetAttribute("sheetindex") == null)
{
DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + sp.name + "\"!");
DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + Name + "\"!");
}
#if CLIENT
if (subElement.GetAttributeBool("fliphorizontal", false))
sp.sprite.effects = SpriteEffects.FlipHorizontally;
Sprite.effects = SpriteEffects.FlipHorizontally;
if (subElement.GetAttributeBool("flipvertical", false))
sp.sprite.effects = SpriteEffects.FlipVertically;
Sprite.effects = SpriteEffects.FlipVertically;
#endif
sp.canSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
sp.canSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
if (subElement.Attribute("name") == null && !string.IsNullOrWhiteSpace(sp.Name))
if (subElement.GetAttribute("name") == null && !Name.IsNullOrWhiteSpace())
{
sp.sprite.Name = sp.Name;
Sprite.Name = Name.Value;
}
sp.sprite.EntityID = sp.identifier;
Sprite.EntityIdentifier = Identifier;
break;
case "backgroundsprite":
sp.BackgroundSprite = new Sprite(subElement, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null && sp.sprite != null)
BackgroundSprite = new Sprite(subElement, lazyLoad: true);
if (subElement.GetAttribute("sourcerect") == null && Sprite != null)
{
sp.BackgroundSprite.SourceRect = sp.sprite.SourceRect;
sp.BackgroundSprite.size = sp.sprite.size;
sp.BackgroundSprite.size.X *= sp.sprite.SourceRect.Width;
sp.BackgroundSprite.size.Y *= sp.sprite.SourceRect.Height;
sp.BackgroundSprite.RelativeOrigin = subElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f));
BackgroundSprite.SourceRect = Sprite.SourceRect;
BackgroundSprite.size = Sprite.size;
BackgroundSprite.size.X *= Sprite.SourceRect.Width;
BackgroundSprite.size.Y *= Sprite.SourceRect.Height;
BackgroundSprite.RelativeOrigin = subElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f));
}
#if CLIENT
if (subElement.GetAttributeBool("fliphorizontal", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipHorizontally; }
if (subElement.GetAttributeBool("flipvertical", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipVertically; }
sp.BackgroundSpriteColor = subElement.GetAttributeColor("color", Color.White);
if (subElement.GetAttributeBool("fliphorizontal", false)) { BackgroundSprite.effects = SpriteEffects.FlipHorizontally; }
if (subElement.GetAttributeBool("flipvertical", false)) { BackgroundSprite.effects = SpriteEffects.FlipVertically; }
BackgroundSpriteColor = subElement.GetAttributeColor("color", Color.White);
#endif
break;
case "decorativesprite":
#if CLIENT
string decorativeSpriteFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
if (subElement.DoesAttributeReferenceFileNameAlone("texture"))
{
decorativeSpriteFolder = Path.GetDirectoryName(file.Path);
}
int groupID = 0;
DecorativeSprite decorativeSprite = null;
if (subElement.Attribute("texture") == null)
if (subElement.GetAttribute("texture") == null)
{
groupID = subElement.GetAttributeInt("randomgroupid", 0);
}
else
{
decorativeSprite = new DecorativeSprite(subElement, decorativeSpriteFolder, lazyLoad: true);
sp.DecorativeSprites.Add(decorativeSprite);
decorativeSprites.Add(decorativeSprite);
groupID = decorativeSprite.RandomGroupID;
}
if (!sp.DecorativeSpriteGroups.ContainsKey(groupID))
if (!decorativeSpriteGroups.ContainsKey(groupID))
{
sp.DecorativeSpriteGroups.Add(groupID, new List<DecorativeSprite>());
decorativeSpriteGroups.Add(groupID, new List<DecorativeSprite>());
}
sp.DecorativeSpriteGroups[groupID].Add(decorativeSprite);
decorativeSpriteGroups[groupID].Add(decorativeSprite);
#endif
break;
}
}
if (string.Equals(parentType, "wrecked", StringComparison.OrdinalIgnoreCase))
{
if (!string.IsNullOrEmpty(sp.Name))
{
sp.name = TextManager.GetWithVariable("wreckeditemformat", "[name]", sp.name);
}
}
#if CLIENT
DecorativeSprites = decorativeSprites.ToImmutableArray();
DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary();
#endif
string categoryStr = element.GetAttributeString("category", "Structure");
if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category))
{
category = MapEntityCategory.Structure;
category = MapEntityCategory.Structure;
}
sp.Category = category;
Category = category;
if (category.HasFlag(MapEntityCategory.Legacy))
{
if (string.IsNullOrWhiteSpace(sp.identifier))
{
sp.identifier = "legacystructure_" + sp.name.ToLowerInvariant().Replace(" ", "");
}
}
sp.Aliases =
(element.GetAttributeStringArray("aliases", null) ??
element.GetAttributeStringArray("Aliases", new string[0])).ToHashSet();
Aliases =
(element.GetAttributeStringArray("aliases", null, convertToLowerInvariant: true) ??
element.GetAttributeStringArray("Aliases", Array.Empty<string>(), convertToLowerInvariant: true)).ToImmutableHashSet();
string nonTranslatedName = element.GetAttributeString("name", null) ?? element.Name.ToString();
sp.Aliases.Add(nonTranslatedName.ToLowerInvariant());
Aliases.Add(nonTranslatedName.ToLowerInvariant());
SerializableProperty.DeserializeProperties(sp, element);
if (sp.Body)
SerializableProperty.DeserializeProperties(this, element);
if (Body)
{
sp.Tags.Add("wall");
tags.Add("wall".ToIdentifier());
}
if (string.IsNullOrEmpty(sp.Description))
if (Description.IsNullOrEmpty())
{
if (!string.IsNullOrEmpty(descriptionIdentifier))
if (!descriptionIdentifier.IsEmpty)
{
sp.Description = TextManager.Get("EntityDescription." + descriptionIdentifier, returnNull: true) ?? string.Empty;
Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}");
}
else if (string.IsNullOrEmpty(nameIdentifier))
else if (nameIdentifier.IsEmpty)
{
sp.Description = TextManager.Get("EntityDescription." + sp.identifier, returnNull: true) ?? string.Empty;
Description = TextManager.Get($"EntityDescription.{Identifier}");
}
else
{
sp.Description = TextManager.Get("EntityDescription." + nameIdentifier, true) ?? string.Empty;
Description = TextManager.Get($"EntityDescription.{nameIdentifier}");
}
}
//backwards compatibility
if (element.Attribute("size") == null)
if (element.GetAttribute("size") == null)
{
sp.size = Vector2.Zero;
if (element.Attribute("width") == null && element.Attribute("height") == null)
Size = Vector2.Zero;
if (element.GetAttribute("width") == null && element.GetAttribute("height") == null)
{
sp.size.X = sp.sprite.SourceRect.Width;
sp.size.Y = sp.sprite.SourceRect.Height;
Size = Sprite.SourceRect.Size.ToVector2();
}
else
{
sp.size.X = element.GetAttributeFloat("width", 0.0f);
sp.size.Y = element.GetAttributeFloat("height", 0.0f);
Size = new Vector2(
element.GetAttributeFloat("width", 0.0f),
element.GetAttributeFloat("height", 0.0f));
}
}
//backwards compatibility
if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase))
{
sp.Category = MapEntityCategory.Wrecked;
sp.Subcategory = "Thalamus";
Category = MapEntityCategory.Wrecked;
Subcategory = "Thalamus";
}
if (string.IsNullOrEmpty(sp.identifier))
if (Identifier == Identifier.Empty)
{
DebugConsole.ThrowError(
"Structure prefab \"" + sp.name + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading.");
"Structure prefab \"" + Name + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading.");
}
Prefabs.Add(sp, allowOverride);
return sp;
#if DEBUG
if (!Category.HasFlag(MapEntityCategory.Legacy) && !HideInMenus)
{
if (!string.IsNullOrEmpty(OriginalName))
{
DebugConsole.AddWarning($"Structure \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages.");
}
}
#endif
Tags = tags.ToImmutableHashSet();
AllowedLinks = ImmutableHashSet<Identifier>.Empty;
}
protected override void CreateInstance(Rectangle rect)
{
throw new NotImplementedException();
}
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
}
}
@@ -21,7 +21,7 @@ namespace Barotrauma
None = 0, Left = 1, Right = 2
}
partial class Submarine : Entity, IServerSerializable
partial class Submarine : Entity, IServerPositionSync
{
public SubmarineInfo Info { get; private set; }
@@ -253,7 +253,7 @@ namespace Barotrauma
get { return subBody == null ? Vector2.Zero : subBody.Velocity; }
set
{
if (subBody == null) return;
if (subBody == null) { return; }
subBody.Velocity = value;
}
}
@@ -291,7 +291,7 @@ namespace Barotrauma
public int CalculateBasePrice()
{
int minPrice = 1000;
float volume = Hull.hullList.Where(h => h.Submarine == this).Sum(h => h.Volume);
float volume = Hull.HullList.Where(h => h.Submarine == this).Sum(h => h.Volume);
float itemValue = Item.ItemList.Where(it => it.Submarine == this).Sum(it => it.Prefab.GetMinPrice() ?? 0);
float price = volume / 500.0f + itemValue / 100.0f;
System.Diagnostics.Debug.Assert(price >= 0);
@@ -300,7 +300,7 @@ namespace Barotrauma
private float ballastFloraTimer;
public bool ImmuneToBallastFlora { get; set; }
public void AttemptBallastFloraInfection(string identifier, float deltaTime, float probability)
public void AttemptBallastFloraInfection(Identifier identifier, float deltaTime, float probability)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (ImmuneToBallastFlora) { return; }
@@ -557,7 +557,7 @@ namespace Barotrauma
public Rectangle CalculateDimensions(bool onlyHulls = true)
{
List<MapEntity> entities = onlyHulls ?
Hull.hullList.FindAll(h => h.Submarine == this).Cast<MapEntity>().ToList() :
Hull.HullList.FindAll(h => h.Submarine == this).Cast<MapEntity>().ToList() :
MapEntity.mapEntityList.FindAll(me => me.Submarine == this);
//ignore items whose body is disabled (wires, items inside cabinets)
@@ -969,8 +969,6 @@ namespace Barotrauma
public void Update(float deltaTime)
{
//if (PlayerInput.KeyHit(InputType.Crouch) && (this == MainSub)) FlipX();
if (Info.IsWreck)
{
WreckAI?.Update(deltaTime);
@@ -990,7 +988,7 @@ namespace Barotrauma
{
if (e.Submarine == this)
{
Spawner.AddToRemoveQueue(e);
Spawner.AddEntityToRemoveQueue(e);
}
}
@@ -1114,7 +1112,7 @@ namespace Barotrauma
float waterVolume = 0.0f;
float volume = 0.0f;
float excessWater = 0.0f;
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine != this) { continue; }
waterVolume += hull.WaterVolume;
@@ -1212,7 +1210,7 @@ namespace Barotrauma
/// </summary>
public bool IsConnectedTo(Submarine otherSub) => this == otherSub || GetConnectedSubs().Contains(otherSub);
public List<Hull> GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.hullList);
public List<Hull> GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.HullList);
public List<Gap> GetGaps(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Gap.GapList);
public List<Item> GetItems(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Item.ItemList);
public List<WayPoint> GetWaypoints(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, WayPoint.WayPointList);
@@ -1285,7 +1283,7 @@ namespace Barotrauma
if (element.Name != "Structure") { continue; }
string name = element.GetAttributeString("name", "");
string identifier = element.GetAttributeString("identifier", "");
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
StructurePrefab prefab = Structure.FindPrefab(name, identifier);
if (prefab == null || !prefab.Body) { continue; }
@@ -1348,7 +1346,7 @@ namespace Barotrauma
}
Vector2 center = Vector2.Zero;
var matchingHulls = Hull.hullList.FindAll(h => h.Submarine == this);
var matchingHulls = Hull.HullList.FindAll(h => h.Submarine == this);
if (matchingHulls.Any())
{
@@ -1538,7 +1536,16 @@ namespace Barotrauma
Rectangle dimensions = VisibleBorders;
element.Add(new XAttribute("dimensions", XMLExtensions.Vector2ToString(dimensions.Size.ToVector2())));
var cargoContainers = GetCargoContainers();
element.Add(new XAttribute("cargocapacity", cargoContainers.Sum(c => c.container.Capacity)));
int cargoCapacity = cargoContainers.Sum(c => c.container.Capacity);
foreach (MapEntity me in MapEntity.mapEntityList)
{
if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this)
{
cargoCapacity += linkedSub.CargoCapacity;
}
}
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 ?? ""));
@@ -1654,7 +1661,7 @@ namespace Barotrauma
Unloading = true;
#if CLIENT
RemoveAllRoundSounds();
RoundSound.RemoveAllRoundSounds();
GameMain.LightManager?.ClearLights();
#endif
@@ -1697,6 +1704,8 @@ namespace Barotrauma
GameMain.World?.Clear();
GameMain.World = null;
Powered.Grids.Clear();
GC.Collect();
Unloading = false;
@@ -118,7 +118,7 @@ namespace Barotrauma
Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero;
Vector2 visibleMinExtents = Vector2.Zero, visibleMaxExtents = Vector2.Zero;
Body farseerBody = null;
if (!Hull.hullList.Any(h => h.Submarine == sub))
if (!Hull.HullList.Any(h => h.Submarine == sub))
{
farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f);
if (showWarningMessages)
@@ -156,7 +156,7 @@ namespace Barotrauma
}
}
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine != submarine || hull.IdFreed) { continue; }
@@ -446,7 +446,7 @@ namespace Barotrauma
{
float waterVolume = 0.0f;
float volume = 0.0f;
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine != submarine) continue;
@@ -850,7 +850,7 @@ namespace Barotrauma
{
errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server.";
}
if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg);
if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"SubmarineBody.ApplyImpact:InvalidImpulse",
GameAnalyticsManager.ErrorSeverity.Error,
@@ -28,13 +28,8 @@ namespace Barotrauma
partial class SubmarineInfo : IDisposable
{
public const string SavePath = "Submarines";
private static List<SubmarineInfo> savedSubmarines = new List<SubmarineInfo>();
public static IEnumerable<SubmarineInfo> SavedSubmarines
{
get { return savedSubmarines; }
}
public static IEnumerable<SubmarineInfo> SavedSubmarines => savedSubmarines;
private Task hashTask;
private Md5Hash hash;
@@ -59,13 +54,13 @@ namespace Barotrauma
set;
}
public string DisplayName
public LocalizedString DisplayName
{
get;
set;
}
public string Description
public LocalizedString Description
{
get;
set;
@@ -170,7 +165,7 @@ namespace Barotrauma
get
{
if (requiredContentPackagesInstalled.HasValue) { return requiredContentPackagesInstalled.Value; }
return RequiredContentPackages.All(cp => GameMain.Config.AllEnabledPackages.Any(cp2 => cp2.Name == cp));
return RequiredContentPackages.All(reqName => ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.NameMatches(reqName)));
}
set
{
@@ -199,13 +194,14 @@ namespace Barotrauma
public OutpostGenerationParams OutpostGenerationParams;
public readonly Dictionary<string, List<Character>> OutpostNPCs = new Dictionary<string, List<Character>>();
public readonly Dictionary<Identifier, List<Character>> OutpostNPCs = new Dictionary<Identifier, List<Character>>();
//constructors & generation ----------------------------------------------------
public SubmarineInfo()
{
FilePath = null;
Name = DisplayName = TextManager.Get("UnspecifiedSubFileName");
DisplayName = TextManager.Get("UnspecifiedSubFileName");
Name = DisplayName.Value;
IsFileCorrupted = false;
RequiredContentPackages = new HashSet<string>();
}
@@ -219,7 +215,8 @@ namespace Barotrauma
}
try
{
Name = DisplayName = Path.GetFileNameWithoutExtension(filePath);
DisplayName = Path.GetFileNameWithoutExtension(filePath);
Name = DisplayName.Value;
}
catch (Exception e)
{
@@ -228,7 +225,7 @@ namespace Barotrauma
if (!string.IsNullOrWhiteSpace(hash))
{
this.hash = new Md5Hash(hash);
this.hash = Md5Hash.StringAsHash(hash);
}
IsFileCorrupted = false;
@@ -267,7 +264,7 @@ namespace Barotrauma
GameVersion = original.GameVersion;
Type = original.Type;
SubmarineClass = original.SubmarineClass;
hash = !string.IsNullOrEmpty(original.FilePath) ? original.MD5Hash : null;
hash = !string.IsNullOrEmpty(original.FilePath) && File.Exists(original.FilePath) ? original.MD5Hash : null;
Dimensions = original.Dimensions;
CargoCapacity = original.CargoCapacity;
FilePath = original.FilePath;
@@ -300,7 +297,7 @@ namespace Barotrauma
DebugConsole.NewMessage("Opening submarine file \"" + FilePath + "\" failed, retrying in 250 ms...");
Thread.Sleep(250);
}
if (doc == null || doc.Root == null)
if (doc?.Root == null)
{
IsFileCorrupted = true;
return;
@@ -314,11 +311,9 @@ namespace Barotrauma
private void Init()
{
DisplayName = TextManager.Get("Submarine.Name." + Name, true);
if (string.IsNullOrEmpty(DisplayName)) { DisplayName = Name; }
DisplayName = TextManager.Get("Submarine.Name." + Name).Fallback(Name);
Description = TextManager.Get("Submarine.Description." + Name, true);
if (string.IsNullOrEmpty(Description)) { Description = SubmarineElement.GetAttributeString("description", ""); }
Description = TextManager.Get("Submarine.Description." + Name).Fallback(SubmarineElement.GetAttributeString("description", ""));
EqualityCheckVal = SubmarineElement.GetAttributeInt("checkval", 0);
@@ -379,7 +374,7 @@ namespace Barotrauma
}
RequiredContentPackages.Clear();
string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", new string[0]);
string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", Array.Empty<string>());
foreach (string contentPackageName in contentPackageNames)
{
RequiredContentPackages.Add(contentPackageName);
@@ -405,14 +400,9 @@ namespace Barotrauma
var vanilla = GameMain.VanillaContent;
if (vanilla != null)
{
var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine)
.Concat(vanilla.GetFilesOfType(ContentType.Wreck))
.Concat(vanilla.GetFilesOfType(ContentType.BeaconStation))
.Concat(vanilla.GetFilesOfType(ContentType.EnemySubmarine))
.Concat(vanilla.GetFilesOfType(ContentType.Outpost))
.Concat(vanilla.GetFilesOfType(ContentType.OutpostModule));
string pathToCompare = FilePath.Replace(@"\", @"/").ToLowerInvariant();
if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").ToLowerInvariant() == pathToCompare))
var vanillaSubs = vanilla.GetFiles<BaseSubFile>();
string pathToCompare = FilePath.CleanUpPath();
if (vanillaSubs.Any(sub => sub.Path == pathToCompare))
{
return true;
}
@@ -427,7 +417,8 @@ namespace Barotrauma
hashTask = new Task(() =>
{
hash = new Md5Hash(doc, FilePath);
hash = Md5Hash.CalculateForString(doc.ToString(), Md5Hash.StringHashOptions.IgnoreWhitespace);
Md5Hash.Cache.Add(FilePath, hash, DateTime.UtcNow);
});
hashTask.Start();
}
@@ -459,7 +450,7 @@ namespace Barotrauma
LeftBehindSubDockingPortOccupied = false;
LeftBehindDockingPortIDs.Clear();
BlockedDockingPortIDs.Clear();
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("linkedsubmarine", StringComparison.OrdinalIgnoreCase)) { continue; }
if (subElement.Attribute("location") == null) { continue; }
@@ -469,7 +460,7 @@ namespace Barotrauma
LeftBehindDockingPortIDs.Add(targetDockingPortID);
XElement targetPortElement = targetDockingPortID == 0 ? null :
element.Elements().FirstOrDefault(e => e.GetAttributeInt("ID", 0) == targetDockingPortID);
if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", new int[0]).Length > 0)
if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", Array.Empty<int>()).Length > 0)
{
BlockedDockingPortIDs.Add(targetDockingPortID);
LeftBehindSubDockingPortOccupied = true;
@@ -488,7 +479,7 @@ namespace Barotrauma
foreach (var structureElement in SubmarineElement.GetChildElements("structure"))
{
string name = structureElement.Attribute("name")?.Value ?? "";
string identifier = structureElement.GetAttributeString("identifier", "");
Identifier identifier = structureElement.GetAttributeIdentifier("identifier", "");
var structurePrefab = Structure.FindPrefab(name, identifier);
if (structurePrefab == null || !structurePrefab.Body) { continue; }
if (!structureCrushDepthsDefined && structureElement.Attribute("crushdepth") != null)
@@ -546,7 +537,7 @@ namespace Barotrauma
}
SaveUtil.CompressStringToFile(filePath, doc.ToString());
Md5Hash.RemoveFromCache(filePath);
Md5Hash.Cache.Remove(filePath);
}
public static void AddToSavedSubs(SubmarineInfo subInfo)
@@ -578,73 +569,26 @@ namespace Barotrauma
public static void RefreshSavedSubs()
{
var contentPackageSubs = ContentPackage.GetFilesOfType(
GameMain.Config.AllEnabledPackages,
ContentType.Submarine, ContentType.Outpost, ContentType.OutpostModule,
ContentType.Wreck, ContentType.BeaconStation, ContentType.EnemySubmarine);
var contentPackageSubs = ContentPackageManager.EnabledPackages.All.SelectMany(c => c.GetFiles<BaseSubFile>());
for (int i = savedSubmarines.Count - 1; i >= 0; i--)
{
if (File.Exists(savedSubmarines[i].FilePath))
{
bool isDownloadedSub = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SaveUtil.SubmarineDownloadFolder);
bool isInSubmarinesFolder = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath);
bool isInContentPackage = contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath());
bool isInContentPackage = contentPackageSubs.Any(f => f.Path == savedSubmarines[i].FilePath);
if (isDownloadedSub) { continue; }
if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && (isInSubmarinesFolder || isInContentPackage)) { continue; }
if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && isInContentPackage) { continue; }
}
savedSubmarines[i].Dispose();
}
if (!Directory.Exists(SavePath))
List<string> filePaths = new List<string>();
foreach (BaseSubFile subFile in contentPackageSubs)
{
try
if (!filePaths.Any(fp => fp == subFile.Path))
{
Directory.CreateDirectory(SavePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Directory \"" + SavePath + "\" not found and creating the directory failed.", e);
return;
}
}
List<string> filePaths;
string[] subDirectories;
try
{
filePaths = Directory.GetFiles(SavePath).ToList();
subDirectories = Directory.GetDirectories(SavePath).Where(s =>
{
DirectoryInfo dir = new DirectoryInfo(s);
return !dir.Attributes.HasFlag(System.IO.FileAttributes.Hidden) && !dir.Name.StartsWith(".");
}).ToArray();
}
catch (Exception e)
{
DebugConsole.ThrowError("Couldn't open directory \"" + SavePath + "\"!", e);
return;
}
foreach (string subDirectory in subDirectories)
{
try
{
filePaths.AddRange(Directory.GetFiles(subDirectory).ToList());
}
catch (Exception e)
{
DebugConsole.ThrowError("Couldn't open subdirectory \"" + subDirectory + "\"!", e);
return;
}
}
foreach (ContentFile subFile in contentPackageSubs)
{
if (!filePaths.Any(fp => Path.GetFullPath(fp) == Path.GetFullPath(subFile.Path)))
{
filePaths.Add(subFile.Path);
filePaths.Add(subFile.Path.Value);
}
}
@@ -653,34 +597,7 @@ namespace Barotrauma
foreach (string path in filePaths)
{
var subInfo = new SubmarineInfo(path);
if (subInfo.IsFileCorrupted)
{
#if CLIENT
if (DebugConsole.IsOpen) { DebugConsole.Toggle(); }
var deleteSubPrompt = new GUIMessageBox(
TextManager.Get("Error"),
TextManager.GetWithVariable("SubLoadError", "[subname]", subInfo.Name) + "\n" +
TextManager.GetWithVariable("DeleteFileVerification", "[filename]", subInfo.Name),
new string[] { TextManager.Get("Yes"), TextManager.Get("No") });
string filePath = path;
deleteSubPrompt.Buttons[0].OnClicked += (btn, userdata) =>
{
try
{
File.Delete(filePath);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to delete file \"{filePath}\".", e);
}
deleteSubPrompt.Close();
return true;
};
deleteSubPrompt.Buttons[1].OnClicked += deleteSubPrompt.Close;
#endif
}
else
if (!subInfo.IsFileCorrupted)
{
savedSubmarines.Add(subInfo);
}
@@ -27,7 +27,7 @@ namespace Barotrauma
public Ladder Ladders;
public Structure Stairs;
private List<string> tags;
private HashSet<Identifier> tags;
public bool isObstructed;
@@ -78,10 +78,7 @@ namespace Barotrauma
}
}
public IEnumerable<string> Tags
{
get { return tags; }
}
public IEnumerable<Identifier> Tags => tags;
public JobPrefab AssignedJob { get; private set; }
@@ -114,7 +111,7 @@ namespace Barotrauma
public WayPoint(Rectangle newRect, Submarine submarine)
: this (MapEntityPrefab.Find(null, "waypoint"), newRect, submarine)
: this (MapEntityPrefab.FindByIdentifier("waypoint".ToIdentifier()), newRect, submarine)
{
}
@@ -122,8 +119,8 @@ namespace Barotrauma
: base (prefab, submarine, id)
{
rect = newRect;
idCardTags = new string[0];
tags = new List<string>();
idCardTags = Array.Empty<string>();
tags = new HashSet<Identifier>();
#if CLIENT
if (iconSprites == null)
@@ -165,7 +162,7 @@ namespace Barotrauma
public static bool GenerateSubWaypoints(Submarine submarine)
{
if (!Hull.hullList.Any())
if (!Hull.HullList.Any())
{
DebugConsole.ThrowError("Couldn't generate waypoints: no hulls found.");
return false;
@@ -189,13 +186,14 @@ namespace Barotrauma
door.Body.Enabled = true;
}
}
bool isFlooded = submarine.Info.IsRuin || submarine.Info.Type == SubmarineType.OutpostModule && submarine.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin");
bool isFlooded = submarine.Info.IsRuin || submarine.Info.Type == SubmarineType.OutpostModule && submarine.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier());
float diffFromHullEdge = 50;
float minDist = 100.0f;
float heightFromFloor = 110.0f;
float hullMinHeight = 100;
var removals = new HashSet<WayPoint>();
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
if (isFlooded)
{
@@ -315,7 +313,16 @@ namespace Barotrauma
for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
{
var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
if (previousWaypoint != null) { wayPoint.ConnectTo(previousWaypoint); }
// Too close to stairs, will be assigned as a stair point -> remove
if (wayPoint.FindStairs() != null)
{
removals.Add(wayPoint);
continue;
}
if (previousWaypoint != null)
{
wayPoint.ConnectTo(previousWaypoint);
}
previousWaypoint = wayPoint;
}
if (previousWaypoint == null)
@@ -512,25 +519,29 @@ namespace Barotrauma
}
}
}
removals.ForEach(wp => wp.Remove());
removals.Clear();
// Stairs
foreach (MapEntity mapEntity in mapEntityList.ToList())
{
if (!(mapEntity is Structure structure)) { continue; }
if (structure.StairDirection == Direction.None) { continue; }
WayPoint[] stairPoints = new WayPoint[3];
float margin = -32;
stairPoints[0] = new WayPoint(
new Vector2(structure.Rect.X - 32.0f,
structure.Rect.Y - (structure.StairDirection == Direction.Left ? 80 : structure.Rect.Height) + heightFromFloor), SpawnType.Path, submarine);
stairPoints[0] = new WayPoint(new Vector2(
structure.Rect.X + 5,
structure.Rect.Y - (structure.StairDirection == Direction.Left ? margin : structure.Rect.Height - 100)), SpawnType.Path, submarine);
stairPoints[1] = new WayPoint(
new Vector2(structure.Rect.Right + 32.0f,
structure.Rect.Y - (structure.StairDirection == Direction.Left ? structure.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine);
stairPoints[1] = new WayPoint(new Vector2(
structure.Rect.Right - 5,
structure.Rect.Y - (structure.StairDirection == Direction.Left ? structure.Rect.Height - 100 : margin)), SpawnType.Path, submarine);
for (int i = 0; i < 2; i++)
{
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(100, 70));
WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.5f, minDist / 2));
if (closest == null) { continue; }
stairPoints[i].ConnectTo(closest);
}
@@ -539,9 +550,8 @@ namespace Barotrauma
stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position) / 2, SpawnType.Path, submarine);
stairPoints[0].ConnectTo(stairPoints[2]);
stairPoints[2].ConnectTo(stairPoints[1]);
stairPoints.ForEach(wp => wp.FindStairs());
}
removals.ForEach(wp => wp.Remove());
removals.Clear();
foreach (Item item in Item.ItemList)
{
@@ -587,7 +597,7 @@ namespace Barotrauma
Body pickedBody = Submarine.PickBody(
ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)),
prevPos, ignoredBodies, Physics.CollisionWall, false,
(Fixture f) => f.Body.UserData is Item && ((Item)f.Body.UserData).GetComponent<Door>() != null);
(Fixture f) => f.Body.UserData is Item pickedItem && pickedItem.GetComponent<Door>() != null);
Door pickedDoor = null;
if (pickedBody != null)
@@ -842,7 +852,11 @@ namespace Barotrauma
var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, ignoreLevel: true, ignoreSubs: true, ignoreSensors: false);
if (body != null && body != ignoredBody && !(body.UserData is Submarine))
{
if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall))
if (body.UserData is Structure)
{
continue;
}
if (body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall) && body.UserData is Item i && i.GetComponent<Door>() != null)
{
continue;
}
@@ -876,9 +890,9 @@ namespace Barotrauma
return WayPointList.GetRandom(wp =>
(ignoreSubmarine || wp.Submarine == sub) &&
wp.spawnType == spawnType &&
(string.IsNullOrEmpty(spawnPointTag) || wp.Tags.Any(t => t.Equals(spawnPointTag, StringComparison.OrdinalIgnoreCase))) &&
(spawnPointTag.IsNullOrEmpty() || wp.Tags.Any(t => t == spawnPointTag)) &&
(assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob)),
useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced);
useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced);
}
public static WayPoint[] SelectCrewSpawnPoints(List<CharacterInfo> crew, Submarine submarine)
@@ -922,7 +936,7 @@ namespace Barotrauma
var nonJobSpecificPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human && wp.AssignedJob == null);
if (nonJobSpecificPoints.Any())
{
assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.Server)];
assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.ServerAndClient)];
}
if (assignedWayPoints[i] != null) { continue; }
@@ -962,18 +976,15 @@ namespace Barotrauma
FindStairs();
}
private void FindStairs()
private Structure FindStairs()
{
Stairs = null;
Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - Vector2.UnitY * 2.0f, null, Physics.CollisionStairs);
if (pickedBody != null && pickedBody.UserData is Structure)
Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - new Vector2(0, 1.2f), null, Physics.CollisionStairs);
if (pickedBody != null && pickedBody.UserData is Structure structure && structure.StairDirection != Direction.None)
{
Structure structure = (Structure)pickedBody.UserData;
if (structure != null && structure.StairDirection != Direction.None)
{
Stairs = structure;
}
Stairs = structure;
}
return Stairs;
}
public void InitializeLinks()
@@ -985,23 +996,24 @@ namespace Barotrauma
}
if (ladderId > 0)
{
Item ladderItem = FindEntityByID(ladderId) as Item;
if (ladderItem != null) { Ladders = ladderItem.GetComponent<Ladder>(); }
if (FindEntityByID(ladderId) is Item ladderItem) { Ladders = ladderItem.GetComponent<Ladder>(); }
ladderId = 0;
}
}
public static WayPoint Load(XElement element, Submarine submarine, IdRemap idRemap)
public static WayPoint Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
int.Parse(element.GetAttribute("x").Value),
int.Parse(element.GetAttribute("y").Value),
(int)Submarine.GridSize.X, (int)Submarine.GridSize.Y);
Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType);
WayPoint w = new WayPoint(MapEntityPrefab.Find(null, spawnType == SpawnType.Path ? "waypoint" : "spawnpoint"), rect, submarine, idRemap.GetOffsetId(element));
w.spawnType = spawnType;
WayPoint w = new WayPoint(MapEntityPrefab.FindByIdentifier((spawnType == SpawnType.Path ? "waypoint" : "spawnpoint").ToIdentifier()), rect, submarine, idRemap.GetOffsetId(element))
{
spawnType = spawnType
};
string idCardDescString = element.GetAttributeString("idcarddesc", "");
if (!string.IsNullOrWhiteSpace(idCardDescString))
@@ -1014,7 +1026,7 @@ namespace Barotrauma
w.IdCardTags = idCardTagString.Split(',');
}
w.tags = element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true).ToList();
w.tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
string jobIdentifier = element.GetAttributeString("job", "").ToLowerInvariant();
if (!string.IsNullOrWhiteSpace(jobIdentifier))
@@ -1029,9 +1041,9 @@ namespace Barotrauma
w.gapId = idRemap.GetOffsetId(element.GetAttributeInt("gap", 0));
int i = 0;
while (element.Attribute("linkedto" + i) != null)
while (element.GetAttribute("linkedto" + i) != null)
{
int srcId = int.Parse(element.Attribute("linkedto" + i).Value);
int srcId = int.Parse(element.GetAttribute("linkedto" + i).Value);
int destId = idRemap.GetOffsetId(srcId);
if (destId > 0)
{