Unstable 0.17.0.0

This commit is contained in:
Markus Isberg
2022-02-26 02:43:01 +09:00
parent a83f375681
commit 3974067915
913 changed files with 32472 additions and 32364 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; }
@@ -18,45 +18,72 @@ 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;
public Vector2 ShakeAmount;
// 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 +118,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 +128,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 +256,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 +269,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 +286,12 @@ namespace Barotrauma.MapCreatures.Behavior
public float PowerConsumptionTimer;
private float defenseCooldown, toxinsCooldown, fireCheckCooldown;
private float damageIndicatorTimer, selfDamageTimer, toxinsTimer;
private float selfDamageTimer, toxinsTimer;
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 +354,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 +393,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 +421,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 +451,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 +468,7 @@ namespace Barotrauma.MapCreatures.Behavior
BlockedSides = (TileSide) blockedSides,
IsRoot = isRoot
};
if (newBranch.IsRoot) { root = newBranch; }
if (claimedId > -1)
{
@@ -437,6 +484,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 +502,25 @@ 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; }
branch.Health += branchHealAmount;
branch.AccumulatedDamage -= branchHealAmount;
}
}
StateMachine.Update(deltaTime);
if (HasBrokenThrough)
@@ -512,12 +555,12 @@ namespace Barotrauma.MapCreatures.Behavior
// This entire scope is probably very heavy for GC, need to experiment
if (toxinsTimer > 0.1f)
{
if (!string.IsNullOrWhiteSpace(AttackItemPrefab))
if (!AttackItemPrefab.IsEmpty)
{
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 +577,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 +607,49 @@ 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))
{
DamageBranch(branch, deltaTime * MathHelper.Lerp(10.0f, 0.01f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth), 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 +730,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 +764,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 +792,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 +821,28 @@ namespace Barotrauma.MapCreatures.Behavior
GrowthWarps--;
}
int rootGrowthCount = Branches.Count(b => b.IsRootGrowth);
if (rootGrowthCount < GetDesiredRootGrowthAmount())
{
if (root != null)
{
Vector2 rootGrowthPos = Rand.Vector(rootGrowthCount * 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);
#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;
@@ -894,6 +971,14 @@ namespace Barotrauma.MapCreatures.Behavior
public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null)
{
float damage = amount;
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
branch.DamageVisualizationTimer = 1.0f;
}
if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; }
// damage is handled server side currently
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -922,12 +1007,10 @@ namespace Barotrauma.MapCreatures.Behavior
}
}
branch.AccumulatedDamage += damage;
branch.Health -= damage;
if (type != AttackType.Other)
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
branch.AccumulatedDamage += damage;
Anger += damage * 0.001f;
}
@@ -935,30 +1018,13 @@ namespace Barotrauma.MapCreatures.Behavior
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 +1035,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 +1076,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);
@@ -1050,8 +1141,10 @@ namespace Barotrauma.MapCreatures.Behavior
public void Kill()
{
Branches.ForEachMod(RemoveBranch);
Parent.BallastFlora = null;
foreach (var branch in Branches)
{
branch.DisconnectedFromRoot = true;
}
foreach (Item target in ClaimedTargets)
{
@@ -1059,6 +1152,19 @@ namespace Barotrauma.MapCreatures.Behavior
}
StateMachine?.State?.Exit();
#if SERVER
SendNetworkMessage(this, NetworkHeader.Kill);
#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 +1173,9 @@ namespace Barotrauma.MapCreatures.Behavior
GameMain.World.Remove(body);
}
_entityList.Remove(this);
#if SERVER
SendNetworkMessage(this, NetworkHeader.Kill);
SendNetworkMessage(this, NetworkHeader.Remove);
#endif
}
@@ -1093,9 +1200,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;
@@ -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() { }
}
}
@@ -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,6 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Barotrauma.IO;
using System.Linq;
using System.Text;
@@ -70,6 +71,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 +93,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);
@@ -482,7 +482,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); }
}
@@ -136,7 +136,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;
@@ -706,7 +706,7 @@ namespace Barotrauma
base.ShallowRemove();
GapList.Remove(this);
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
hull.ConnectedGaps.Remove(this);
}
@@ -717,7 +717,7 @@ namespace Barotrauma
base.Remove();
GapList.Remove(this);
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
hull.ConnectedGaps.Remove(this);
}
@@ -734,7 +734,7 @@ 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;
@@ -101,8 +101,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 +124,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 +155,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 +179,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 +191,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; }
@@ -314,7 +305,7 @@ namespace Barotrauma
}
}
[Serialize(100000.0f, true)]
[Serialize(100000.0f, IsPropertySaveable.Yes)]
public float Oxygen
{
get { return oxygen; }
@@ -332,7 +323,7 @@ namespace Barotrauma
roomName.Contains("airlock", 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 +338,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; }
@@ -469,7 +460,7 @@ namespace Barotrauma
};
}
hullList.Add(this);
HullList.Add(this);
if (submarine == null || !submarine.Loading)
{
@@ -488,11 +479,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 +507,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 +530,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 +624,7 @@ namespace Barotrauma
public override void ShallowRemove()
{
base.Remove();
hullList.Remove(this);
HullList.Remove(this);
if (Submarine == null || (!Submarine.Loading && !Submarine.Unloading))
{
@@ -659,7 +653,7 @@ namespace Barotrauma
public override void Remove()
{
base.Remove();
hullList.Remove(this);
HullList.Remove(this);
BallastFlora?.Remove();
if (Submarine != null && !Submarine.Loading && !Submarine.Unloading)
@@ -712,7 +706,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,7 +726,7 @@ 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)
@@ -1110,12 +1104,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 +1129,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 +1158,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 +1168,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 +1203,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 +1307,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; }
@@ -1476,7 +1470,7 @@ 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)
@@ -1507,7 +1501,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 +1519,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)
{
@@ -5,64 +5,57 @@ 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 +77,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 +117,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 +160,19 @@ namespace Barotrauma
public void Delete()
{
Dispose();
if (File.Exists(FilePath))
if (File.Exists(ContentFile.Path))
{
try
{
File.Delete(FilePath);
File.Delete(ContentFile.Path);
}
catch (Exception e)
{
DebugConsole.ThrowError("Deleting item assembly \"" + name + "\" failed.", 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;
@@ -11,6 +11,7 @@ using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.IO;
using Voronoi2;
namespace Barotrauma
@@ -400,6 +401,8 @@ namespace Barotrauma
Loaded = this;
Generating = true;
Rand.Tracker.Reset();
Rand.Tracker.Active = true;
EqualityCheckValues.Clear();
EntitiesBeforeGenerate = GetEntities().ToList();
EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count();
@@ -410,7 +413,7 @@ namespace Barotrauma
EndLocation = GameMain.GameSession?.EndLocation;
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
LevelObjectManager = new LevelObjectManager();
@@ -420,11 +423,8 @@ namespace Barotrauma
#if CLIENT
if (backgroundCreatureManager == null)
{
var files = GameMain.Instance.GetFilesOfType(ContentType.BackgroundCreaturePrefabs);
if (files.Count() > 0)
backgroundCreatureManager = new BackgroundCreatureManager(files);
else
backgroundCreatureManager = new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml");
var files = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<BackgroundCreaturePrefabsFile>()).ToArray();
backgroundCreatureManager = files.Any() ? new BackgroundCreatureManager(files) : new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml");
}
#endif
Stopwatch sw = new Stopwatch();
@@ -476,7 +476,7 @@ namespace Barotrauma
(int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y));
endExitPosition = new Point(endPosition.X, borders.Bottom);
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
//----------------------------------------------------------------------------------
//generate the initial nodes for the main path and smaller tunnels
@@ -550,20 +550,20 @@ namespace Barotrauma
Tunnels.Add(abyssTunnel);
}
int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.Server);
int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.ServerAndClient);
for (int j = 0; j < sideTunnelCount; j++)
{
if (mainPath.Nodes.Count < 4) { break; }
var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole && t != abyssTunnel);
Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.Server)];
Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.ServerAndClient)];
if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; }
Point branchStart = tunnelToBranchOff.Nodes[Rand.Range(0, tunnelToBranchOff.Nodes.Count / 3, Rand.RandSync.Server)];
Point branchEnd = tunnelToBranchOff.Nodes[Rand.Range(tunnelToBranchOff.Nodes.Count / 3 * 2, tunnelToBranchOff.Nodes.Count - 1, Rand.RandSync.Server)];
Point branchStart = tunnelToBranchOff.Nodes[Rand.Range(0, tunnelToBranchOff.Nodes.Count / 3, Rand.RandSync.ServerAndClient)];
Point branchEnd = tunnelToBranchOff.Nodes[Rand.Range(tunnelToBranchOff.Nodes.Count / 3 * 2, tunnelToBranchOff.Nodes.Count - 1, Rand.RandSync.ServerAndClient)];
var sidePathNodes = GeneratePathNodes(branchStart, branchEnd, pathBorders, tunnelToBranchOff, GenerationParams.SideTunnelVariance);
//make sure the path is wide enough to pass through
int pathWidth = Rand.Range(GenerationParams.MinSideTunnelRadius.X, GenerationParams.MinSideTunnelRadius.Y, Rand.RandSync.Server);
int pathWidth = Rand.Range(GenerationParams.MinSideTunnelRadius.X, GenerationParams.MinSideTunnelRadius.Y, Rand.RandSync.ServerAndClient);
Tunnels.Add(new Tunnel(TunnelType.SidePath, sidePathNodes, pathWidth, parentTunnel: tunnelToBranchOff));
}
@@ -572,7 +572,7 @@ namespace Barotrauma
GenerateAbyssArea();
GenerateCaves(mainPath);
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
//----------------------------------------------------------------------------------
//generate voronoi sites
@@ -583,21 +583,21 @@ namespace Barotrauma
Point siteVariance = GenerationParams.VoronoiSiteVariance;
siteCoordsX = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
siteCoordsY = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
int caveSiteInterval = 500;
const int caveSiteInterval = 500;
for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X)
{
for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y)
{
int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.Server);
int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.Server);
int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient);
int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient);
bool closeToTunnel = false;
bool closeToCave = false;
foreach (Tunnel tunnel in Tunnels)
{
float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y));
for (int i = 1; i < tunnel.Nodes.Count; i++)
{
float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y));
if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; }
if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; }
if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; }
@@ -607,7 +607,7 @@ namespace Barotrauma
if (Math.Sqrt(tunnelDistSqr) < minDist)
{
closeToTunnel = true;
tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY));
//tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY));
if (tunnel.Type == TunnelType.Cave)
{
closeToCave = true;
@@ -620,7 +620,7 @@ namespace Barotrauma
if (!closeToTunnel)
{
//make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry
if (Rand.Range(0, 10, Rand.RandSync.Server) != 0) { continue; }
if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; }
}
if (!TooClose(siteX, siteY))
@@ -635,8 +635,8 @@ namespace Barotrauma
{
for (int y2 = y; y2 < y + siteInterval.Y; y2 += caveSiteInterval)
{
int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server);
int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server);
int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
if (!TooClose(caveSiteX, caveSiteY))
{
@@ -677,7 +677,7 @@ namespace Barotrauma
}
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
//----------------------------------------------------------------------------------
// construct the voronoi graph and cells
@@ -778,7 +778,7 @@ namespace Barotrauma
for (int i = 0; i < GenerationParams.IslandCount; i++)
{
if (potentialIslands.Count == 0) { break; }
var island = potentialIslands.GetRandom(Rand.RandSync.Server);
var island = potentialIslands.GetRandom(Rand.RandSync.ServerAndClient);
island.CellType = CellType.Solid;
island.Island = true;
pathCells.Remove(island);
@@ -795,7 +795,7 @@ namespace Barotrauma
startPosition.X = (int)pathCells[0].Site.Coord.X;
startExitPosition.X = startPosition.X;
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
//----------------------------------------------------------------------------------
// remove unnecessary cells and create some holes at the bottom of the level
@@ -862,8 +862,6 @@ namespace Barotrauma
// mirror if needed
//----------------------------------------------------------------------------------
int asdfasdf = Rand.Int(int.MaxValue, Rand.RandSync.Server);
if (mirror)
{
HashSet<GraphEdge> mirroredEdges = new HashSet<GraphEdge>();
@@ -1008,7 +1006,7 @@ namespace Barotrauma
caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells));
foreach (var caveCell in caveCells)
{
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio)
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio)
{
var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f);
if (chunk != null)
@@ -1020,7 +1018,7 @@ namespace Barotrauma
}
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
//----------------------------------------------------------------------------------
// create some ruins
@@ -1033,7 +1031,7 @@ namespace Barotrauma
GenerateRuin(ruinPositions[i], mirror);
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
//----------------------------------------------------------------------------------
// create floating ice chunks
@@ -1054,18 +1052,18 @@ namespace Barotrauma
for (int i = 0; i < GenerationParams.FloatingIceChunkCount; i++)
{
if (iceChunkPositions.Count == 0) { break; }
Point selectedPos = iceChunkPositions[Rand.Int(iceChunkPositions.Count, Rand.RandSync.Server)];
float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.Server);
Point selectedPos = iceChunkPositions[Rand.Int(iceChunkPositions.Count, Rand.RandSync.ServerAndClient)];
float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.ServerAndClient);
var vertices = CaveGenerator.CreateRandomChunk(chunkRadius, 8, chunkRadius * 0.8f);
var chunk = CreateIceChunk(vertices, selectedPos.ToVector2());
chunk.MoveAmount = new Vector2(0.0f, minMainPathWidth * 0.7f);
chunk.MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.Server);
chunk.MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.ServerAndClient);
ExtraWalls.Add(chunk);
iceChunkPositions.Remove(selectedPos);
}
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
//----------------------------------------------------------------------------------
// generate the bodies and rendered triangles of the cells
@@ -1170,7 +1168,7 @@ namespace Barotrauma
}
#endif
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
//----------------------------------------------------------------------------------
// create ice spires
@@ -1205,7 +1203,7 @@ namespace Barotrauma
CreateOutposts();
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
//----------------------------------------------------------------------------------
// top barrier & sea floor
@@ -1247,15 +1245,15 @@ namespace Barotrauma
CreateWrecks();
CreateBeaconStation();
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount);
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateItems();
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
#if CLIENT
backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount);
@@ -1283,7 +1281,7 @@ namespace Barotrauma
Debug.WriteLine("Seed: " + Seed);
Debug.WriteLine("**********************************************************************************");
if (GameSettings.VerboseLogging)
if (GameSettings.CurrentConfig.VerboseLogging)
{
DebugConsole.NewMessage("Generated level with the seed " + Seed + " (type: " + GenerationParams.Identifier + ")", Color.White);
}
@@ -1301,6 +1299,8 @@ namespace Barotrauma
//assign an ID to make entity events work
//ID = FindFreeID();
Generating = false;
Rand.Tracker.Active = false;
File.WriteAllLines(GameMain.NetworkMember is { IsServer: true } ? "serverrng.txt" : "clientrng.txt", Rand.Tracker.LogMsgs);
}
private List<Point> GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel, float variance)
@@ -1311,9 +1311,9 @@ namespace Barotrauma
for (int x = startPosition.X + nodeInterval.X;
x < endPosition.X - nodeInterval.X;
x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.Server))
x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.ServerAndClient))
{
Point nodePos = new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.Server));
Point nodePos = new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.ServerAndClient));
//allow placing the 2nd main path node at any height regardless of variance
//(otherwise low variance will always make the main path go through the upper part of the level)
@@ -1378,7 +1378,7 @@ namespace Barotrauma
foreach (VoronoiCell cell in cells)
{
if (cell.Edges.Any(e => e.NextToCave)) { continue; }
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > holeProbability) { continue; }
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > holeProbability) { continue; }
if (!limits.Contains(cell.Site.Coord.X, cell.Site.Coord.Y)) { continue; }
float closestDist = 0.0f;
@@ -1620,13 +1620,13 @@ namespace Barotrauma
//above the bottom of the level = can't place a point here
if (seaFloorPos > AbyssStart) { continue; }
float yPos = MathHelper.Lerp(AbyssStart, Math.Max(seaFloorPos, AbyssArea.Y), Rand.Range(0.2f, 1.0f, Rand.RandSync.Server));
float yPos = MathHelper.Lerp(AbyssStart, Math.Max(seaFloorPos, AbyssArea.Y), Rand.Range(0.2f, 1.0f, Rand.RandSync.ServerAndClient));
foreach (var abyssIsland in AbyssIslands)
{
if (abyssIsland.Area.Contains(new Point((int)xPos, (int)yPos)))
{
xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1, Rand.RandSync.Server) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f);
xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1, Rand.RandSync.ServerAndClient) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f);
}
}
@@ -1681,7 +1681,7 @@ namespace Barotrauma
Point islandSize = Vector2.Lerp(
GenerationParams.AbyssIslandSizeMin.ToVector2(),
GenerationParams.AbyssIslandSizeMax.ToVector2(),
Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)).ToPoint();
Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient)).ToPoint();
if (AbyssArea.Height < islandSize.Y) { return; }
@@ -1697,8 +1697,8 @@ namespace Barotrauma
do
{
islandPosition = new Point(
Rand.Range(AbyssArea.X, AbyssArea.Right - islandSize.X, Rand.RandSync.Server),
Rand.Range(AbyssArea.Y, AbyssArea.Bottom - islandSize.Y, Rand.RandSync.Server));
Rand.Range(AbyssArea.X, AbyssArea.Right - islandSize.X, Rand.RandSync.ServerAndClient),
Rand.Range(AbyssArea.Y, AbyssArea.Bottom - islandSize.Y, Rand.RandSync.ServerAndClient));
//move the island above the sea floor geometry
islandPosition.Y = Math.Max(islandPosition.Y, (int)GetBottomPosition(islandPosition.X).Y + 500);
@@ -1714,7 +1714,7 @@ namespace Barotrauma
break;
}
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > GenerationParams.AbyssIslandCaveProbability)
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > GenerationParams.AbyssIslandCaveProbability)
{
float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f;
var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance);
@@ -1735,8 +1735,8 @@ namespace Barotrauma
{
for (int y = islandArea.Y; y < islandArea.Bottom; y += siteInterval.Y)
{
siteCoordsX.Add(x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server));
siteCoordsY.Add(y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server));
siteCoordsX.Add(x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.ServerAndClient));
siteCoordsY.Add(y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.ServerAndClient));
}
}
@@ -1765,7 +1765,7 @@ namespace Barotrauma
}
}
var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: true, rand: Rand.RandSync.Server);
var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: true, rand: Rand.RandSync.ServerAndClient);
float caveScaleRelativeToIsland = 0.7f;
GenerateCave(
@@ -1786,12 +1786,12 @@ namespace Barotrauma
new Point(0, BottomPos)
};
int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax + 1, Rand.RandSync.Server);
int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax + 1, Rand.RandSync.ServerAndClient);
for (int i = 0; i < mountainCount; i++)
{
bottomPositions.Add(
new Point(Size.X / (mountainCount + 1) * (i + 1),
BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax + 1, Rand.RandSync.Server)));
BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax + 1, Rand.RandSync.ServerAndClient)));
}
bottomPositions.Add(new Point(Size.X, BottomPos));
@@ -1804,8 +1804,8 @@ namespace Barotrauma
bottomPositions.Insert(i + 1,
new Point(
(bottomPositions[i].X + bottomPositions[i + 1].X) / 2,
(bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance + 1, Rand.RandSync.Server)));
i++;
(bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance + 1, Rand.RandSync.ServerAndClient)));
i++;
}
currInverval /= 2;
@@ -1834,10 +1834,10 @@ namespace Barotrauma
{
for (int i = 0; i < GenerationParams.CaveCount; i++)
{
var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: false, rand: Rand.RandSync.Server);
var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: false, rand: Rand.RandSync.ServerAndClient);
Point caveSize = new Point(
Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.Server),
Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.Server));
Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.ServerAndClient),
Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.ServerAndClient));
int padding = (int)(caveSize.X * 1.2f);
Rectangle allowedArea = new Rectangle(padding, padding, Size.X - padding * 2, Size.Y - padding * 2);
@@ -1891,12 +1891,12 @@ namespace Barotrauma
Tunnels.Add(tunnel);
caveBranches.Add(tunnel);
int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount + 1, Rand.RandSync.Server);
int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount + 1, Rand.RandSync.ServerAndClient);
for (int j = 0; j < branches; j++)
{
Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.Server);
Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.Server)].ToVector2();
Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.Server)].ToVector2();
Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.ServerAndClient);
Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.ServerAndClient)].ToVector2();
Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.ServerAndClient)].ToVector2();
var branchSegments = MathUtils.GenerateJaggedLine(
branchStartPos, branchEndPos,
iterations: 3,
@@ -1930,17 +1930,17 @@ namespace Barotrauma
private void GenerateRuin(Point ruinPos, bool mirror)
{
var ruinGenerationParams = RuinGenerationParams.GetRandom(Rand.RandSync.Server);
var ruinGenerationParams = RuinGenerationParams.RuinParams.GetRandom(Rand.RandSync.ServerAndClient);
LocationType locationType = StartLocation?.Type;
if (locationType == null)
{
locationType = LocationType.List.GetRandom(Rand.RandSync.Server);
locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
if (ruinGenerationParams.AllowedLocationTypes.Any())
{
locationType = LocationType.List.Where(lt =>
locationType = LocationType.Prefabs.Where(lt =>
ruinGenerationParams.AllowedLocationTypes.Any(allowedType =>
allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(Rand.RandSync.Server);
allowedType == "any" || lt.Identifier == allowedType)).GetRandom(Rand.RandSync.ServerAndClient);
}
}
@@ -2111,7 +2111,7 @@ namespace Barotrauma
if (pointsAboveBottom.Count == 0)
{
DebugConsole.ThrowError("Error in FindPosAwayFromMainPath: no valid positions above the bottom of the sea floor. Has the position of the sea floor been set too high up?");
return distanceField[Rand.Int(distanceField.Count, Rand.RandSync.Server)].point;
return distanceField[Rand.Int(distanceField.Count, Rand.RandSync.ServerAndClient)].point;
}
var validPoints = pointsAboveBottom.FindAll(d => d.distance >= minDistance && (limits == null || limits.Value.Contains(d.point)));
@@ -2154,7 +2154,7 @@ namespace Barotrauma
}
else
{
return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.Server)].point;
return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.ServerAndClient)].point;
}
}
@@ -2270,7 +2270,7 @@ namespace Barotrauma
{
const float maxLength = 15000.0f;
float minEdgeLength = 100.0f;
var mainPathPos = PositionsOfInterest.Where(pos => pos.PositionType == PositionType.MainPath).GetRandom(Rand.RandSync.Server);
var mainPathPos = PositionsOfInterest.GetRandom(pos => pos.PositionType == PositionType.MainPath, Rand.RandSync.ServerAndClient);
double closestDistSqr = double.PositiveInfinity;
GraphEdge closestEdge = null;
VoronoiCell closestCell = null;
@@ -2307,13 +2307,13 @@ namespace Barotrauma
float spireLength = (float)Math.Min(Math.Sqrt(closestDistSqr), maxLength);
spireLength *= MathHelper.Lerp(0.3f, 1.5f, Difficulty / 100.0f);
Vector2 extrudedPoint1 = closestEdge.Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server);
Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server);
Vector2 extrudedPoint1 = closestEdge.Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient);
Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient);
List<Vector2> vertices = new List<Vector2>()
{
closestEdge.Point1,
extrudedPoint1 + (extrudedPoint2 - extrudedPoint1) * Rand.Range(0.3f, 0.45f, Rand.RandSync.Server),
extrudedPoint2 + (extrudedPoint1 - extrudedPoint2) * Rand.Range(0.3f, 0.45f, Rand.RandSync.Server),
extrudedPoint1 + (extrudedPoint2 - extrudedPoint1) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient),
extrudedPoint2 + (extrudedPoint1 - extrudedPoint2) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient),
closestEdge.Point2,
};
Vector2 center = Vector2.Zero;
@@ -2364,8 +2364,8 @@ namespace Barotrauma
};
}
}
public List<string> ResourceTags { get; }
public List<string> ResourceIds { get; }
public List<Identifier> ResourceTags { get; }
public List<Identifier> ResourceIds { get; }
public List<ClusterLocation> ClusterLocations { get; }
public TunnelType TunnelType { get; }
@@ -2374,8 +2374,8 @@ namespace Barotrauma
Id = id;
Position = position;
ShouldContainResources = shouldContainResources;
ResourceTags = new List<string>();
ResourceIds = new List<string>();
ResourceTags = new List<Identifier>();
ResourceIds = new List<Identifier>();
ClusterLocations = new List<ClusterLocation>();
TunnelType = tunnelType;
}
@@ -2417,14 +2417,14 @@ namespace Barotrauma
// Such as the exploding crystals in The Great Sea
private void GenerateItems()
{
string levelName = GenerationParams.Identifier.ToLowerInvariant();
Identifier levelName = GenerationParams.Identifier;
float minCommonness = float.MaxValue, maxCommonness = float.MinValue;
List<(ItemPrefab itemPrefab, float commonness)> levelResources = new List<(ItemPrefab itemPrefab, float commonness)>();
var fixedResources = new List<(ItemPrefab itemPrefab, ItemPrefab.FixedQuantityResourceInfo resourceInfo)>();
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier))
{
if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) ||
itemPrefab.LevelCommonness.TryGetValue("", out commonness))
itemPrefab.LevelCommonness.TryGetValue(Identifier.Empty, out commonness))
{
if (commonness <= 0.0f) { continue; }
if (commonness < minCommonness) { minCommonness = commonness; }
@@ -2432,7 +2432,7 @@ namespace Barotrauma
levelResources.Add((itemPrefab, commonness));
}
else if (itemPrefab.LevelQuantity.TryGetValue(levelName, out var fixedQuantityResourceInfo) ||
itemPrefab.LevelQuantity.TryGetValue("", out fixedQuantityResourceInfo))
itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo))
{
fixedResources.Add((itemPrefab, fixedQuantityResourceInfo));
}
@@ -2450,12 +2450,12 @@ namespace Barotrauma
var location = allValidLocations.GetRandom(l =>
{
if (l.Cell == null || l.Edge == null) { return false; }
if (resourceInfo.IsIslandSpecifc && !l.Cell.Island) { return false; }
if (resourceInfo.IsIslandSpecific && !l.Cell.Island) { return false; }
if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > startPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; }
if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; }
return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _);
}, randSync: Rand.RandSync.Server);
}, randSync: Rand.RandSync.ServerAndClient);
if (location.Cell == null || location.Edge == null) { break; }
@@ -2478,10 +2478,10 @@ namespace Barotrauma
if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; }
l.InitializeResources();
return l.Resources.Count <= GetMaxResourcesOnEdge(itemPrefab, l, out _);
}, randSync: Rand.RandSync.Server);
}, randSync: Rand.RandSync.ServerAndClient);
if (location.Cell == null || location.Edge == null) { break; }
int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.Server);
int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.ServerAndClient);
PlaceResources(itemPrefab, clusterSize, location, out var abyssResources);
var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true);
abyssClusterLocation.Resources.AddRange(abyssResources);
@@ -2509,7 +2509,7 @@ namespace Barotrauma
var intervalRange = tunnel.Type != TunnelType.Cave ? GenerationParams.ResourceIntervalRange : GenerationParams.CaveResourceIntervalRange;
do
{
var distance = Rand.Range(intervalRange.X, intervalRange.Y, sync: Rand.RandSync.Server);
var distance = Rand.Range(intervalRange.X, intervalRange.Y, sync: Rand.RandSync.ServerAndClient);
reachedLastNode = !CalculatePositionOnPath();
var id = Tunnels.IndexOf(tunnel) + ":" + nextPathPointId++;
var spawnChance = tunnel.Type == TunnelType.Cave || tunnel.ParentTunnel?.Type == TunnelType.Cave ?
@@ -2517,7 +2517,7 @@ namespace Barotrauma
var containsResources = true;
if (spawnChance < 1.0f)
{
var spawnPointRoll = Rand.Range(0.0f, 1.0f, sync: Rand.RandSync.Server);
var spawnPointRoll = Rand.Range(0.0f, 1.0f, sync: Rand.RandSync.ServerAndClient);
containsResources = spawnPointRoll <= spawnChance;
}
var tunnelType = tunnel.Type;
@@ -2544,7 +2544,7 @@ namespace Barotrauma
}
int itemCount = 0;
string[] exclusiveResourceTags = new string[2] { "ore", "plant" };
Identifier[] exclusiveResourceTags = new Identifier[2] { "ore".ToIdentifier(), "plant".ToIdentifier() };
// Create first cluster for each spawn point
foreach (var pathPoint in PathPoints.Where(p => p.ShouldContainResources))
@@ -2563,14 +2563,14 @@ namespace Barotrauma
{
var availablePathPoints = PathPoints.Where(p =>
p.ShouldContainResources && p.NextClusterProbability > 0 &&
!excludedPathPointIds.Contains(p.Id));
!excludedPathPointIds.Contains(p.Id)).ToList();
if (availablePathPoints.None()) { break; }
var pathPoint = ToolBox.SelectWeightedRandom(
availablePathPoints.ToList(),
availablePathPoints,
availablePathPoints.Select(p => p.NextClusterProbability).ToList(),
Rand.RandSync.Server);
Rand.RandSync.ServerAndClient);
GenerateAdditionalCluster(pathPoint);
}
@@ -2580,9 +2580,9 @@ namespace Barotrauma
while (itemCount < GenerationParams.ItemCount)
{
// We need to start filling some of the path points previously set to not contain resources
var availablePathPoints = PathPoints.Where(p => !excludedPathPointIds.Contains(p.Id) && p.ClusterLocations.None());
if (availablePathPoints.None()) { break; }
var pathPoint = availablePathPoints.GetRandom(randSync: Rand.RandSync.Server);
Func<PathPoint, bool> availablePathPoints = p => !excludedPathPointIds.Contains(p.Id) && p.ClusterLocations.None();
if (PathPoints.None(availablePathPoints)) { break; }
var pathPoint = PathPoints.GetRandom(availablePathPoints, randSync: Rand.RandSync.ServerAndClient);
if (!GenerateFirstCluster(pathPoint))
{
excludedPathPointIds.Add(pathPoint.Id);
@@ -2716,7 +2716,7 @@ namespace Barotrauma
if (validLocations.Any())
{
var location = validLocations.GetRandom(randSync: Rand.RandSync.Server);
var location = validLocations.GetRandom(randSync: Rand.RandSync.ServerAndClient);
if (CreateResourceCluster(pathPoint, location))
{
var i = allValidLocations.FindIndex(l => l.Equals(location));
@@ -2764,7 +2764,7 @@ namespace Barotrauma
selectedPrefab = ToolBox.SelectWeightedRandom(
levelResources.Select(it => it.itemPrefab).ToList(),
levelResources.Select(it => it.commonness).ToList(),
Rand.RandSync.Server);
Rand.RandSync.ServerAndClient);
selectedPrefab.Tags.ForEach(t =>
{
if (exclusiveResourceTags.Contains(t))
@@ -2781,7 +2781,7 @@ namespace Barotrauma
selectedPrefab = ToolBox.SelectWeightedRandom(
filteredResources.Select(it => it.itemPrefab).ToList(),
filteredResources.Select(it => it.commonness).ToList(),
Rand.RandSync.Server);
Rand.RandSync.ServerAndClient);
}
if (selectedPrefab == null) { return false; }
@@ -2800,7 +2800,7 @@ namespace Barotrauma
if (maxClusterSize < 1) { return false; }
var minClusterSize = Math.Min(GenerationParams.ResourceClusterSizeRange.X, maxClusterSize);
var resourcesInCluster = maxClusterSize == 1 ? 1 : Rand.Range(minClusterSize, maxClusterSize + 1, sync: Rand.RandSync.Server);
var resourcesInCluster = maxClusterSize == 1 ? 1 : Rand.Range(minClusterSize, maxClusterSize + 1, sync: Rand.RandSync.ServerAndClient);
if (resourcesInCluster < 1) { return false; }
@@ -2863,7 +2863,7 @@ namespace Barotrauma
}
}
var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.Server);
var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.ServerAndClient);
var poiPos = poi.Position.ToVector2();
allValidLocations.Sort((x, y) => Vector2.DistanceSquared(poiPos, x.EdgeCenter)
.CompareTo(Vector2.DistanceSquared(poiPos, y.EdgeCenter)));
@@ -2969,11 +2969,11 @@ namespace Barotrauma
var lerpAmount = 0.0f;
for (int i = 1; i < resourceCount; i++)
{
var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.Server);
var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient);
lerpAmount += ((1.0f - overlap) * resourcePrefab.Size.X) / edgeLength.Value;
lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f);
}
var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.Server);
var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.ServerAndClient);
placedResources = new List<Item>();
for (int i = 0; i < resourceCount; i++)
{
@@ -2981,7 +2981,7 @@ namespace Barotrauma
var item = new Item(resourcePrefab, selectedPos, submarine: null);
Vector2 edgeNormal = location.Edge.GetNormal(location.Cell);
float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f));
moveAmount += (item.GetComponent<LevelResource>()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server);
moveAmount += (item.GetComponent<LevelResource>()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient);
item.Move(edgeNormal * moveAmount, ignoreContacts: true);
if (item.GetComponent<Holdable>() is Holdable h)
{
@@ -3012,7 +3012,7 @@ namespace Barotrauma
{
TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter);
Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.Server), Rand.RandSync.Server);
Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient);
if (!cells.Any(c => c.IsPointInside(startPos + offset)))
{
startPos += offset;
@@ -3081,7 +3081,7 @@ namespace Barotrauma
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position;
position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position;
return false;
}
@@ -3122,7 +3122,7 @@ namespace Barotrauma
return false;
}
position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position;
position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position;
return true;
}
@@ -3366,9 +3366,9 @@ namespace Barotrauma
private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type)
{
var tempSW = new Stopwatch();
// Min distance between a sub and the start/end/other sub.
float minDistance = Sonar.DefaultSonarRange;
const float minDistance = Sonar.DefaultSonarRange;
var waypoints = WayPoint.WayPointList.Where(wp =>
wp.Submarine == null &&
wp.SpawnType == SpawnType.Path &&
@@ -3376,7 +3376,7 @@ namespace Barotrauma
!IsCloseToStart(wp.WorldPosition, minDistance) &&
!IsCloseToEnd(wp.WorldPosition, minDistance)).ToList();
var subDoc = SubmarineInfo.OpenFile(contentFile.Path);
var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value);
Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
// Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by.
@@ -3419,7 +3419,7 @@ namespace Barotrauma
{
Debug.WriteLine($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)");
tempSW.Restart();
SubmarineInfo info = new SubmarineInfo(contentFile.Path)
SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value)
{
Type = type
};
@@ -3431,13 +3431,13 @@ namespace Barotrauma
PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: sub));
foreach (Hull hull in sub.GetHulls(false))
{
if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.WreckHullFloodingChance)
if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.WreckHullFloodingChance)
{
hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.Server);
hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient);
}
}
// Only spawn thalamus when the wreck has some thalamus items defined.
if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus")))
if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus")))
{
if (!sub.CreateWreckAI())
{
@@ -3550,7 +3550,7 @@ namespace Barotrauma
spawnPoint = Vector2.Zero;
while (waypoints.Any())
{
var wp = waypoints.GetRandom(Rand.RandSync.Server);
var wp = waypoints.GetRandom(Rand.RandSync.ServerAndClient);
waypoints.Remove(wp);
if (!IsBlocked(wp.WorldPosition, paddedDimensions))
{
@@ -3665,17 +3665,19 @@ namespace Barotrauma
{
var totalSW = new Stopwatch();
totalSW.Start();
var wreckFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Wreck).ToList();
var wreckFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<WreckFile>())
.OrderBy(f => f.UintIdentifier).ToList();
if (wreckFiles.None())
{
DebugConsole.ThrowError("No wreck files found in the selected content packages!");
return;
}
wreckFiles.Shuffle(Rand.RandSync.Server);
wreckFiles.Shuffle(Rand.RandSync.ServerAndClient);
int minWreckCount = Math.Min(Loaded.GenerationParams.MinWreckCount, wreckFiles.Count);
int maxWreckCount = Math.Min(Loaded.GenerationParams.MaxWreckCount, wreckFiles.Count);
int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.Server);
int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.ServerAndClient);
if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireWreck) ?? false)
{
@@ -3693,7 +3695,7 @@ namespace Barotrauma
ContentFile contentFile = wreckFiles.First();
wreckFiles.RemoveAt(0);
if (contentFile == null) { continue; }
string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path);
string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value);
if (SpawnSubOnPath(wreckName, contentFile, SubmarineType.Wreck) != null)
{
//placed successfully
@@ -3701,7 +3703,7 @@ namespace Barotrauma
}
attempts++;
}
}
totalSW.Stop();
Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)");
@@ -3743,8 +3745,10 @@ namespace Barotrauma
private void CreateOutposts()
{
var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost).ToList();
if (!outpostFiles.Any() && !OutpostGenerationParams.Params.Any() && LevelData.ForceOutpostGenerationParams == null)
var outpostFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostFile>())
.OrderBy(f => f.UintIdentifier).ToList();
if (!outpostFiles.Any() && !OutpostGenerationParams.OutpostParams.Any() && LevelData.ForceOutpostGenerationParams == null)
{
DebugConsole.ThrowError("No outpost files found in the selected content packages");
return;
@@ -3768,7 +3772,7 @@ namespace Barotrauma
Submarine outpost;
if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null)
{
if (OutpostGenerationParams.Params.Any() || LevelData.ForceOutpostGenerationParams != null)
if (OutpostGenerationParams.OutpostParams.Any() || LevelData.ForceOutpostGenerationParams != null)
{
Location location = i == 0 ? StartLocation : EndLocation;
@@ -3779,31 +3783,29 @@ namespace Barotrauma
}
else
{
var suitableParams = OutpostGenerationParams.Params
.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
if (!suitableParams.Any())
{
suitableParams = OutpostGenerationParams.Params
.Where(p => location == null || !p.AllowedLocationTypes.Any());
suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any());
if (!suitableParams.Any())
{
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
suitableParams = OutpostGenerationParams.OutpostParams;
}
}
if (!suitableParams.Any())
{
DebugConsole.ThrowError("No suitable outpost generation parameters found for the location type \"" + location.Type.Identifier + "\". Selecting random parameters.");
suitableParams = OutpostGenerationParams.Params;
}
outpostGenerationParams = suitableParams.GetRandom(Rand.RandSync.Server);
outpostGenerationParams = suitableParams.GetRandom(Rand.RandSync.ServerAndClient);
}
LocationType locationType = location?.Type;
if (locationType == null)
{
locationType = LocationType.List.GetRandom(Rand.RandSync.Server);
locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
if (outpostGenerationParams.AllowedLocationTypes.Any())
{
locationType = LocationType.List.Where(lt =>
outpostGenerationParams.AllowedLocationTypes.Any(allowedType =>
allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(Rand.RandSync.Server);
locationType = LocationType.Prefabs.GetRandom(lt =>
outpostGenerationParams.AllowedLocationTypes.Any(allowedType =>
allowedType == "any" || lt.Identifier == allowedType), Rand.RandSync.ServerAndClient);
}
}
@@ -3820,7 +3822,7 @@ namespace Barotrauma
foreach (string categoryToHide in locationType.HideEntitySubcategories)
{
foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && (me.prefab?.HasSubCategory(categoryToHide) ?? false)))
foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && (me.Prefab?.HasSubCategory(categoryToHide) ?? false)))
{
entityToHide.HiddenInGame = true;
}
@@ -3830,8 +3832,8 @@ namespace Barotrauma
{
DebugConsole.NewMessage($"Loading a pre-built outpost for the {(isStart ? "start" : "end")} of the level...");
//backwards compatibility: if there are no generation params available, try to load an outpost file saved as a sub
ContentFile outpostFile = outpostFiles.GetRandom(Rand.RandSync.Server);
outpostInfo = new SubmarineInfo(outpostFile.Path)
ContentFile outpostFile = outpostFiles.GetRandom(Rand.RandSync.ServerAndClient);
outpostInfo = new SubmarineInfo(outpostFile.Path.Value)
{
Type = SubmarineType.Outpost
};
@@ -3937,14 +3939,16 @@ namespace Barotrauma
private void CreateBeaconStation()
{
if (!LevelData.HasBeaconStation) { return; }
var beaconStationFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.BeaconStation).ToList();
var beaconStationFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<BeaconStationFile>())
.OrderBy(f => f.UintIdentifier).ToList();
if (beaconStationFiles.None())
{
DebugConsole.ThrowError("No BeaconStation files found in the selected content packages!");
return;
}
var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.Server);
string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path);
var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient);
string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value);
BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation);
if (BeaconStation == null) { return; }
@@ -3976,10 +3980,7 @@ namespace Barotrauma
Repairable repairable = reactorItem.GetComponent<Repairable>();
if (repairable != null)
{
if (repairable != null)
{
repairable.DeteriorationSpeed = 0.0f;
}
repairable.DeteriorationSpeed = 0.0f;
}
}
if (LevelData.IsBeaconActive)
@@ -3988,7 +3989,7 @@ namespace Barotrauma
reactorContainer.ContainableItemIdentifiers.Any() && ItemPrefab.Prefabs.ContainsKey(reactorContainer.ContainableItemIdentifiers.FirstOrDefault()))
{
ItemPrefab fuelPrefab = ItemPrefab.Prefabs[reactorContainer.ContainableItemIdentifiers.FirstOrDefault()];
Spawner.AddToSpawnQueue(
Spawner.AddItemToSpawnQueue(
fuelPrefab, reactorContainer.Inventory,
onSpawned: (it) => reactorComponent.PowerUpImmediately());
}
@@ -4007,7 +4008,7 @@ namespace Barotrauma
foreach (Item item in reactorContainer.Inventory.AllItems)
{
if (item.NonInteractable) { continue; }
Spawner.AddToRemoveQueue(item);
Spawner.AddItemToRemoveQueue(item);
}
}
@@ -4141,8 +4142,8 @@ namespace Barotrauma
job ??= selectedPrefab.GetJobPrefab();
if (job == null) { continue; }
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, randSync: Rand.RandSync.Server);
var corpse = Character.Create(CharacterPrefab.HumanConfigFile, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, randSync: Rand.RandSync.ServerAndClient);
var corpse = Character.Create(CharacterPrefab.HumanSpeciesName, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
corpse.AnimController.FindHull(worldPos, setSubmarine: true);
corpse.TeamID = CharacterTeamType.None;
corpse.EnableDespawn = false;
@@ -4163,7 +4164,7 @@ namespace Barotrauma
bool TryGetExtraSpawnPoint(out Vector2 point)
{
point = Vector2.Zero;
var hull = Hull.hullList.FindAll(h => h.Submarine == wreck).GetRandom(Rand.RandSync.Unsynced);
var hull = Hull.HullList.FindAll(h => h.Submarine == wreck).GetRandomUnsynced();
if (hull != null)
{
point = hull.WorldPosition;
@@ -92,7 +92,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 +106,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 +129,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(
@@ -168,7 +168,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 +183,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 +194,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,76 @@ 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(-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 +411,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 +421,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 +431,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 +441,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 +522,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 +554,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 +601,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;
@@ -140,7 +139,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 +166,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 +180,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 +210,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 +228,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 +259,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)
@@ -275,49 +276,49 @@ namespace Barotrauma
{
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;
@@ -577,7 +578,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 +586,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()
@@ -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);
@@ -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,34 @@ 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
};
foreach (RuinGenerationParams generationParams in RuinParams)
{
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig))
foreach (RuinConfigFile configFile in ContentPackageManager.AllPackages.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 +57,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;
@@ -16,10 +16,10 @@ namespace Barotrauma
{
public readonly ushort OriginalID;
public readonly ushort ModuleIndex;
public readonly string Identifier;
public readonly Identifier Identifier;
public readonly int OriginalContainerIndex;
public TakenItem(string identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex)
public TakenItem(Identifier identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex)
{
OriginalID = originalID;
OriginalContainerIndex = originalContainerIndex;
@@ -34,7 +34,7 @@ namespace Barotrauma
OriginalContainerIndex = item.OriginalContainerIndex;
OriginalID = item.ID;
ModuleIndex = (ushort) item.OriginalModuleIndex;
Identifier = item.prefab.Identifier;
Identifier = ((MapEntity)item).Prefab.Identifier;
}
public bool IsEqual(TakenItem obj)
@@ -46,11 +46,11 @@ namespace Barotrauma
{
if (item.OriginalContainerIndex != Entity.NullEntityID)
{
return item.OriginalContainerIndex == OriginalContainerIndex && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier;
return item.OriginalContainerIndex == OriginalContainerIndex && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier;
}
else
{
return item.ID == OriginalID && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier;
return item.ID == OriginalID && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier;
}
}
}
@@ -252,7 +252,7 @@ namespace Barotrauma
return $"Location ({Name ?? "null"})";
}
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType? forceLocationType = null, IEnumerable<Location> existingLocations = null)
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
{
Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost);
Name = RandomName(Type, rand, existingLocations);
@@ -263,22 +263,21 @@ namespace Barotrauma
public Location(XElement element)
{
string locationType = element.GetAttributeString("type", "");
Type = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase));
Identifier locationType = element.GetAttributeIdentifier("type", "");
Type = LocationType.Prefabs[locationType];
bool typeNotFound = false;
if (Type == null)
{
//turn lairs into abandoned outposts
if (locationType.Equals("lair", StringComparison.OrdinalIgnoreCase))
if (locationType == "lair")
{
Type ??= LocationType.List.Find(lt => lt.Identifier.Equals("Abandoned", StringComparison.OrdinalIgnoreCase));
Type ??= LocationType.Prefabs["Abandoned"];
addInitialMissionsForType = Type;
}
if (Type == null)
{
DebugConsole.AddWarning($"Could not find location type \"{locationType}\". Using location type \"None\" instead.");
Type ??= LocationType.List.Find(lt => lt.Identifier.Equals("None", StringComparison.OrdinalIgnoreCase));
Type ??= LocationType.List.First();
Type ??= LocationType.Prefabs["None"] ?? LocationType.Prefabs.First();
}
if (Type != null)
{
@@ -287,8 +286,8 @@ namespace Barotrauma
typeNotFound = true;
}
string originalLocationType = element.GetAttributeString("originaltype", locationType);
OriginalType = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase));
Identifier originalLocationType = element.GetAttributeIdentifier("originaltype", locationType);
OriginalType = LocationType.Prefabs[locationType];
baseName = element.GetAttributeString("basename", "");
Name = element.GetAttributeString("name", "");
@@ -312,7 +311,7 @@ namespace Barotrauma
LoadLocationTypeChange(element);
}
string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]);
string[] takenItemStr = element.GetAttributeStringArray("takenitems", Array.Empty<string>());
foreach (string takenItem in takenItemStr)
{
string[] takenItemSplit = takenItem.Split(';');
@@ -336,15 +335,15 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error in saved location: could not parse taken item module index \"{takenItemSplit[3]}\"");
continue;
}
takenItems.Add(new TakenItem(takenItemSplit[0], id, containerIndex, moduleIndex));
takenItems.Add(new TakenItem(takenItemSplit[0].ToIdentifier(), id, containerIndex, moduleIndex));
}
killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", new int[0]).ToHashSet();
killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty<int>()).ToHashSet();
System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationType}\"!");
if (Type == null)
{
Type = LocationType.List.First();
Type = LocationType.Prefabs.First();
}
LevelData = new LevelData(element.Element("Level"));
@@ -359,7 +358,7 @@ namespace Barotrauma
{
TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0);
LocationTypeChangeCooldown = locationElement.GetAttributeInt("locationtypechangecooldown", 0);
foreach (XElement subElement in locationElement.Elements())
foreach (var subElement in locationElement.Elements())
{
switch (subElement.Name.ToString())
{
@@ -377,8 +376,8 @@ namespace Barotrauma
}
else
{
string missionIdentifier = subElement.GetAttributeString("missionidentifier", "");
var mission = MissionPrefab.List.Find(mp => mp.Identifier.Equals(missionIdentifier, StringComparison.OrdinalIgnoreCase));
Identifier missionIdentifier = subElement.GetAttributeIdentifier("missionidentifier", "");
var mission = MissionPrefab.Prefabs[missionIdentifier];
if (mission == null)
{
DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found.");
@@ -400,7 +399,7 @@ namespace Barotrauma
{
var id = childElement.GetAttributeString("prefabid", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = MissionPrefab.List.Find(p => p.Identifier.Equals(id, StringComparison.OrdinalIgnoreCase));
var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id);
if (prefab == null) { continue; }
var destination = childElement.GetAttributeInt("destinationindex", -1);
var selected = childElement.GetAttributeBool("selected", false);
@@ -410,7 +409,7 @@ namespace Barotrauma
}
public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType? forceLocationType = null, IEnumerable<Location> existingLocations = null)
public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
{
return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
}
@@ -432,11 +431,11 @@ namespace Barotrauma
if (Type.MissionIdentifiers.Any())
{
UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom());
UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandomUnsynced());
}
if (Type.MissionTags.Any())
{
UnlockMissionByTag(Type.MissionTags.GetRandom());
UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced());
}
CreateStore(force: true);
@@ -446,11 +445,11 @@ namespace Barotrauma
{
if (Type.MissionIdentifiers.Any())
{
UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(Rand.RandSync.Server));
UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(Rand.RandSync.ServerAndClient));
}
if (Type.MissionTags.Any())
{
UnlockMissionByTag(Type.MissionTags.GetRandom(Rand.RandSync.Server));
UnlockMissionByTag(Type.MissionTags.GetRandom(Rand.RandSync.ServerAndClient));
}
}
@@ -474,11 +473,11 @@ namespace Barotrauma
#endif
}
public MissionPrefab UnlockMissionByIdentifier(string identifier)
public MissionPrefab UnlockMissionByIdentifier(Identifier identifier)
{
if (AvailableMissions.Any(m => m.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase))) { return null; }
if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; }
var missionPrefab = MissionPrefab.List.Find(mp => mp.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase));
var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier);
if (missionPrefab == null)
{
DebugConsole.ThrowError($"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found.");
@@ -500,9 +499,9 @@ namespace Barotrauma
return null;
}
public MissionPrefab UnlockMissionByTag(string tag)
public MissionPrefab UnlockMissionByTag(Identifier tag)
{
var matchingMissions = MissionPrefab.List.FindAll(mp => mp.Tags.Any(t => t.Equals(tag, StringComparison.OrdinalIgnoreCase)));
var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Any(t => t == tag));
if (!matchingMissions.Any())
{
DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions not found.");
@@ -623,11 +622,11 @@ namespace Barotrauma
{
if (addInitialMissionsForType.MissionIdentifiers.Any())
{
UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandom());
UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandomUnsynced());
}
if (addInitialMissionsForType.MissionTags.Any())
{
UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandom());
UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandomUnsynced());
}
addInitialMissionsForType = null;
}
@@ -661,7 +660,7 @@ namespace Barotrauma
public LocationType GetLocationType()
{
if (IsCriticallyRadiated() && LocationType.List.FirstOrDefault(lt => lt.Identifier.Equals(Type.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newLocationType)
if (IsCriticallyRadiated() && LocationType.Prefabs[Type.ReplaceInRadiation] is { } newLocationType)
{
return newLocationType;
}
@@ -757,8 +756,8 @@ namespace Barotrauma
List<ItemPrefab> specials = new List<ItemPrefab>();
foreach (var childElement in element.GetChildElements("item"))
{
var id = childElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var id = childElement.GetAttributeIdentifier("id", Identifier.Empty);
if (id.IsEmpty) { continue; }
var prefab = ItemPrefab.Find(null, id);
if (prefab == null) { continue; }
specials.Add(prefab);
@@ -1045,9 +1044,9 @@ namespace Barotrauma
RequestedGoods.Clear();
for (int i = 0; i < RequestedGoodsCount; i++)
{
var item = ItemPrefab.Prefabs.GetRandom(p =>
var item = ItemPrefab.Prefabs.GetRandom<ItemPrefab>(p =>
p.CanBeSold && !RequestedGoods.Contains(p) &&
p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial);
p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced);
if (item == null) { break; }
RequestedGoods.Add(item);
}
@@ -7,23 +7,24 @@ using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Collections.Immutable;
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 LocalizedString Name;
public readonly float BeaconStationChance;
@@ -31,8 +32,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 +45,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
{
@@ -104,32 +113,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)
{
@@ -151,31 +158,16 @@ namespace Barotrauma
CommonnessPerZone[zoneIndex] = zoneCommonness;
}
hireableJobs = new List<Tuple<JobPrefab, float>>();
foreach (XElement subElement in element.Elements())
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);
@@ -216,16 +208,17 @@ namespace Barotrauma
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 +245,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 +260,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;
@@ -214,13 +214,13 @@ 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))
if (location.Type.Identifier != "city" &&
location.Type.Identifier != "outpost")
{
continue;
}
@@ -252,8 +252,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)));
}
}
@@ -297,12 +297,12 @@ 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);
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient), requireOutpost: false, existingLocations: Locations);
Locations.Add(newLocations[i]);
}
@@ -394,7 +394,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 +447,10 @@ 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)));
#warning TODO: determinism?
leftMostLocation.ChangeType(LocationType.Prefabs.First(lt => lt.HasOutpost && lt.Identifier != "abandoned"));
}
leftMostLocation.IsGateBetweenBiomes = true;
Connections[i].Locked = true;
@@ -473,10 +474,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 +523,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 +543,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)];
}
}
}
@@ -692,10 +685,10 @@ namespace Barotrauma
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());
}
}
@@ -999,7 +992,7 @@ namespace Barotrauma
{
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.");
@@ -1053,7 +1046,7 @@ namespace Barotrauma
return;
}
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -1083,14 +1076,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);
@@ -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;
@@ -27,23 +27,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());
}
}
}
@@ -108,19 +107,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; }
@@ -131,7 +130,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; }
@@ -147,7 +146,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
@@ -166,10 +165,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;
@@ -225,14 +224,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>
@@ -249,7 +248,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;
}
@@ -303,12 +302,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);
}
@@ -331,7 +330,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>
@@ -344,7 +343,7 @@ namespace Barotrauma
return false;
}
if (disallowedUpgrades.Contains(upgrade.Identifier)) { return false; }
if (DisallowedUpgradeSet.Contains(upgrade.Identifier)) { return false; }
Upgrade existingUpgrade = GetUpgrade(upgrade.Identifier);
@@ -560,7 +559,7 @@ namespace Barotrauma
/// </summary>
public static void UpdateAll(float deltaTime, Camera cam)
{
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
hull.Update(deltaTime, cam);
}
@@ -635,7 +634,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();
@@ -658,7 +657,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)
{
@@ -672,7 +671,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 + ".");
@@ -683,7 +682,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,10 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
@@ -23,7 +25,7 @@ namespace Barotrauma
Legacy = 1024
}
abstract partial class MapEntityPrefab : IPrefab, IDisposable
abstract partial class MapEntityPrefab : PrefabWithUintIdentifier
{
public static IEnumerable<MapEntityPrefab> List
{
@@ -51,207 +53,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 +72,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 +120,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 +150,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 +1,209 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
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()
public static class Cache
{
try
private const string cachePath = "Data/hashcache.txt";
private readonly static List<(string Path, Md5Hash Hash, DateTime DateTime)> Entries
= new List<(string Path, Md5Hash Hash, DateTime DateTime)>();
public static void Load()
{
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))
var lines = File.ReadAllLines(cachePath);
if (Version.TryParse(lines[0], out var cacheVersion) && cacheVersion == GameMain.Version)
{
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))
for (int i = 1; i < lines.Length; i++)
{
cache.Add(path, new Tuple<Md5Hash, long>(hash, timeLong));
string[] split = lines[i].Split('|');
string path = split[0].CleanUpPathCrossPlatform();
Md5Hash hash = Md5Hash.StringAsHash(split[1]);
DateTime? dateTime = null;
if (long.TryParse(split[2], out long dateTimeUlong))
{
dateTime = DateTime.FromBinary(dateTimeUlong);
}
if (File.Exists(path) && dateTime.HasValue && dateTime >= File.GetLastWriteTime(path))
{
Entries.Add((path, hash, dateTime.Value));
}
}
}
}
catch (Exception e)
public static void Add(string path, Md5Hash hash, DateTime dateTime)
{
DebugConsole.NewMessage($"Failed to load hash cache: {e.Message}\n{e.StackTrace.CleanupStackTrace()}", Microsoft.Xna.Framework.Color.Orange);
cache.Clear();
path = path.CleanUpPathCrossPlatform();
Remove(path);
Entries.Add((path, hash, dateTime));
}
public static void Remove(string path)
{
path = path.CleanUpPathCrossPlatform();
Entries.RemoveAll(e => e.Path == path);
}
}
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
public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32));
private static readonly Regex removeWhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
//thanks to Jlobblet for this regex
private static readonly Regex stringHashRegex = new Regex(@"^[0-9a-fA-F]{7,32}$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
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);
}
public readonly byte[] ByteRepresentation;
public readonly string StringRepresentation;
public readonly string ShortRepresentation;
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)
private static void CalculateHash(byte[] bytes, out string stringRepresentation, out byte[] byteRepresentation)
{
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();
byteRepresentation = byteHash;
stringRepresentation = ByteRepresentationToStringRepresentation(byteHash);
}
}
private static string ByteRepresentationToStringRepresentation(byte[] byteHash)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < byteHash.Length; i++)
{
sb.Append(byteHash[i].ToString("X2"));
}
return sb.ToString();
}
private static byte[] StringRepresentationToByteRepresentation(string strHash)
{
var byteRepresentation = new byte[strHash.Length / 2];
for (int i = 0; i < byteRepresentation.Length; i++)
{
byteRepresentation[i] = Convert.ToByte(strHash.Substring(i * 2, 2), 16);
}
return byteRepresentation;
}
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)
private Md5Hash(string md5Hash)
{
if (!string.IsNullOrWhiteSpace(filename))
StringRepresentation = md5Hash;
ByteRepresentation = StringRepresentationToByteRepresentation(StringRepresentation);
ShortRepresentation = GetShortHash(md5Hash);
}
private Md5Hash(byte[] bytes, bool calculate)
{
if (calculate)
{
filename = filename.CleanUpPath();
lock (cache)
{
if (cache.ContainsKey(filename))
{
cache.Remove(filename);
return true;
}
}
CalculateHash(bytes, out StringRepresentation, out ByteRepresentation);
}
else
{
StringRepresentation = ByteRepresentationToStringRepresentation(bytes);
ByteRepresentation = bytes;
}
ShortRepresentation = GetShortHash(StringRepresentation);
}
public static Md5Hash StringAsHash(string hash)
{
if (!stringHashRegex.IsMatch(hash)) { throw new ArgumentException($"{hash} is not a valid hash"); }
return new Md5Hash(hash);
}
public static Md5Hash CalculateForBytes(byte[] bytes)
{
return new Md5Hash(bytes, calculate: true);
}
public static Md5Hash BytesAsHash(byte[] bytes)
{
return new Md5Hash(bytes, calculate: false);
}
[Flags]
public enum StringHashOptions
{
BytePerfect = 0,
IgnoreCase = 0x1,
IgnoreWhitespace = 0x2
}
public static Md5Hash CalculateForFile(string path, StringHashOptions options)
{
if (options.HasFlag(StringHashOptions.IgnoreWhitespace) || options.HasFlag(StringHashOptions.IgnoreCase))
{
string str = File.ReadAllText(path, Encoding.UTF8);
return CalculateForString(str, options);
}
else
{
byte[] bytes = File.ReadAllBytes(path);
return CalculateForBytes(bytes);
}
}
public static Md5Hash CalculateForString(string str, StringHashOptions options)
{
if (options.HasFlag(StringHashOptions.IgnoreCase))
{
str = str.ToLowerInvariant();
}
if (options.HasFlag(StringHashOptions.IgnoreWhitespace))
{
str = removeWhitespaceRegex.Replace(str, "");
}
byte[] bytes = Encoding.UTF8.GetBytes(str);
return CalculateForBytes(bytes);
}
public override string ToString()
{
return StringRepresentation;
}
public override bool Equals(object? obj)
{
if (obj is Md5Hash { StringRepresentation: { } otherStr })
{
string selfStr = otherStr.Length < StringRepresentation.Length
? StringRepresentation[..otherStr.Length]
: StringRepresentation;
otherStr = StringRepresentation.Length < otherStr.Length
? otherStr[..StringRepresentation.Length]
: otherStr;
return selfStr.Equals(otherStr, StringComparison.OrdinalIgnoreCase);
}
return false;
}
public static bool operator ==(Md5Hash? a, Md5Hash? b)
=> (a is null == b is null) && (a?.Equals(b) ?? true);
public static bool operator !=(Md5Hash? a, Md5Hash? b) => !(a == b);
}
}
@@ -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,209 @@
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; }
#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 +214,22 @@ 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()
{
Params = new List<OutpostGenerationParams>();
var files = GameMain.Instance.GetFilesOfType(ContentType.OutpostConfig);
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc?.Root == null) { continue; }
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
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))
{
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)
{
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;
}
}
Params.Add(new OutpostGenerationParams(element, file.Path));
}
}
}
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;
@@ -67,10 +69,18 @@ namespace Barotrauma
private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = 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,7 +195,7 @@ 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)
@@ -196,7 +215,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 +242,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
};
@@ -386,7 +407,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 +421,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,13 +447,17 @@ 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
@@ -452,7 +477,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,7 +486,7 @@ namespace Barotrauma
if (pendingModuleFlags.Count == 0) { return true; }
List<PlacedModule> placedModules = new List<PlacedModule>();
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions().Randomize(Rand.RandSync.Server))
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient))
{
if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
if (!allowExtendBelowInitialModule)
@@ -526,15 +551,15 @@ namespace Barotrauma
PlacedModule currentModule,
OutpostModuleInfo.GapPosition gapPosition,
List<SubmarineInfo> availableModules,
List<string> pendingModuleFlags,
List<Identifier> pendingModuleFlags,
List<PlacedModule> selectedModules,
LocationType locationType)
{
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);
@@ -547,7 +572,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")
@@ -711,19 +736,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 +756,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,21 +767,21 @@ 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)
{
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())) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition));
}
else
{
@@ -772,7 +797,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())
@@ -783,11 +808,11 @@ 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);
}
}
@@ -807,13 +832,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 +849,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 +862,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 +910,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 +940,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))
@@ -1021,9 +1046,9 @@ namespace Barotrauma
{
suitableModules = availableModules.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(suitableModules, (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(), 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}\".");
@@ -1442,50 +1467,47 @@ 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)>();
foreach (HumanPrefab humanPrefab in outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.ServerAndClient))
{
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server);
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 +1521,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,46 @@ 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; }
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 +68,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 +83,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);
}
}
@@ -28,6 +28,7 @@ namespace Barotrauma
/// 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;
public bool DisplayNonEmpty { get; } = false;
/// <summary>
@@ -48,7 +49,7 @@ namespace Barotrauma
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)
{
Price = price;
CanBeBought = canBeBought;
@@ -58,38 +59,41 @@ namespace Barotrauma
MaxAvailableAmount = Math.Max(maxAmount, minAmount);
MinLevelDifficulty = minLevelDifficulty;
CanBeSpecial = canBeSpecial;
DisplayNonEmpty = displayNonEmpty;
}
public static List<Tuple<string, PriceInfo>> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
public static List<Tuple<Identifier, PriceInfo>> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
{
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);
bool soldByDefault = element.GetAttributeBool("soldbydefault", true);
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);
var priceInfos = new List<Tuple<Identifier, PriceInfo>>();
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,
float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f);
bool sold = childElement.GetAttributeBool("sold", soldByDefault);
priceInfos.Add(new Tuple<Identifier, PriceInfo>(childElement.GetAttributeIdentifier("locationtype", ""),
new PriceInfo((int)(priceMultiplier * basePrice), sold,
sold ? GetMinAmount(childElement, minAmount) : 0,
sold ? GetMaxAmount(childElement, maxAmount) : 0,
canBeSpecial,
childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty), childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier))));
childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty),
childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier),
displayNonEmpty)));
}
var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true);
bool canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true);
defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations,
minAmount: canBeBoughtAtOtherLocations ? minAmount : 0,
maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0,
canBeSpecial,
minLevelDifficulty, buyingPriceMultiplier);
canBeBoughtAtOtherLocations ? minAmount : 0,
canBeBoughtAtOtherLocations ? maxAmount : 0,
canBeSpecial, minLevelDifficulty, buyingPriceMultiplier, displayNonEmpty);
return priceInfos;
}
@@ -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", "");
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)
@@ -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 => Name.Value;
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,155 +115,112 @@ 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);
}
Name = 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))
if (Name.IsNullOrEmpty())
{
if (string.IsNullOrEmpty(nameIdentifier))
Name = TextManager.Get($"EntityName.{Identifier}");
if (!nameIdentifier.IsEmpty)
{
sp.name = TextManager.Get("EntityName." + sp.identifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty;
Name = TextManager.Get($"EntityName.{nameIdentifier}").Fallback(Name);
}
else
if (!fallbackNameIdentifier.IsEmpty)
{
sp.name = TextManager.Get("EntityName." + nameIdentifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty;
Name = Name.Fallback(TextManager.Get($"EntityName.{fallbackNameIdentifier}"));
}
}
if (string.IsNullOrEmpty(sp.name))
{
sp.name = TextManager.Get("EntityName." + sp.identifier, returnNull: true) ?? $"Not defined ({sp.identifier})";
}
sp.Tags = new HashSet<string>();
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)
{
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);
Sprite = new Sprite(subElement, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null &&
subElement.Attribute("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.Attribute("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.Attribute("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);
}
@@ -347,101 +234,111 @@ namespace Barotrauma
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 CLIENT
DecorativeSprites = decorativeSprites.ToImmutableArray();
DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary();
#endif
if (parentType == "wrecked")
{
if (!string.IsNullOrEmpty(sp.Name))
if (!Name.IsNullOrEmpty())
{
sp.name = TextManager.GetWithVariable("wreckeditemformat", "[name]", sp.name);
Name = TextManager.GetWithVariable("wreckeditemformat", "[name]", Name);
}
}
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)
{
sp.size = Vector2.Zero;
Size = Vector2.Zero;
if (element.Attribute("width") == null && element.Attribute("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;
Tags = tags.ToImmutableHashSet();
AllowedLinks = Enumerable.Empty<Identifier>().ToImmutableHashSet();
}
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);
}
}
}
@@ -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)
@@ -990,7 +990,7 @@ namespace Barotrauma
{
if (e.Submarine == this)
{
Spawner.AddToRemoveQueue(e);
Spawner.AddEntityToRemoveQueue(e);
}
}
@@ -1114,7 +1114,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 +1212,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 +1285,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 +1348,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 +1538,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 +1663,7 @@ namespace Barotrauma
Unloading = true;
#if CLIENT
RemoveAllRoundSounds();
RoundSound.RemoveAllRoundSounds();
GameMain.LightManager?.ClearLights();
#endif
@@ -1697,6 +1706,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,
@@ -31,10 +31,7 @@ namespace Barotrauma
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 +56,13 @@ namespace Barotrauma
set;
}
public string DisplayName
public LocalizedString DisplayName
{
get;
set;
}
public string Description
public LocalizedString Description
{
get;
set;
@@ -170,7 +167,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 +196,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 +217,8 @@ namespace Barotrauma
}
try
{
Name = DisplayName = Path.GetFileNameWithoutExtension(filePath);
DisplayName = Path.GetFileNameWithoutExtension(filePath);
Name = DisplayName.Value;
}
catch (Exception e)
{
@@ -228,7 +227,7 @@ namespace Barotrauma
if (!string.IsNullOrWhiteSpace(hash))
{
this.hash = new Md5Hash(hash);
this.hash = Md5Hash.StringAsHash(hash);
}
IsFileCorrupted = false;
@@ -314,11 +313,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 +376,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 +402,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 +419,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 +452,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 +462,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 +481,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 +539,7 @@ namespace Barotrauma
}
SaveUtil.CompressStringToFile(filePath, doc.ToString());
Md5Hash.RemoveFromCache(filePath);
Md5Hash.Cache.Remove(filePath);
}
public static void AddToSavedSubs(SubmarineInfo subInfo)
@@ -578,10 +571,7 @@ 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--)
{
@@ -589,7 +579,7 @@ namespace Barotrauma
{
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; }
}
@@ -640,11 +630,11 @@ namespace Barotrauma
}
}
foreach (ContentFile subFile in contentPackageSubs)
foreach (BaseSubFile subFile in contentPackageSubs)
{
if (!filePaths.Any(fp => Path.GetFullPath(fp) == Path.GetFullPath(subFile.Path)))
if (!filePaths.Any(fp => fp == subFile.Path))
{
filePaths.Add(subFile.Path);
filePaths.Add(subFile.Path.Value);
}
}
@@ -661,7 +651,7 @@ namespace Barotrauma
TextManager.Get("Error"),
TextManager.GetWithVariable("SubLoadError", "[subname]", subInfo.Name) + "\n" +
TextManager.GetWithVariable("DeleteFileVerification", "[filename]", subInfo.Name),
new string[] { TextManager.Get("Yes"), TextManager.Get("No") });
new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });
string filePath = path;
deleteSubPrompt.Buttons[0].OnClicked += (btn, userdata) =>
@@ -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)
{
@@ -587,7 +585,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)
@@ -876,9 +874,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 +920,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; }
@@ -966,13 +964,9 @@ namespace Barotrauma
{
Stairs = null;
Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - Vector2.UnitY * 2.0f, null, Physics.CollisionStairs);
if (pickedBody != null && pickedBody.UserData is Structure)
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;
}
}
@@ -985,13 +979,12 @@ 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),
@@ -1000,8 +993,10 @@ namespace Barotrauma
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 +1009,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))