(61d00a474) v0.9.7.1

This commit is contained in:
Regalis
2020-03-04 13:04:10 +01:00
parent 3c50efa5c9
commit 3c09ebe02f
5086 changed files with 786063 additions and 295871 deletions
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma
{
class CoreEntityPrefab : MapEntityPrefab
{
public static readonly PrefabCollection<CoreEntityPrefab> Prefabs = new PrefabCollection<CoreEntityPrefab>();
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
}
}
@@ -0,0 +1,46 @@
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
partial class DummyFireSource : FireSource
{
private Vector2 maxSize;
public bool Removed
{
get { return removed; }
}
public DummyFireSource(Vector2 maxSize, Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) : base(worldPosition, spawningHull, isNetworkMessage)
{
this.maxSize = maxSize;
}
public override float DamageRange
{
get { return 5f; }
}
protected override void LimitSize()
{
if (hull == null) return;
position.X = Math.Max(hull.Rect.X, position.X);
position.Y = Math.Min(hull.Rect.Y, position.Y);
size.X = Math.Min(maxSize.X, size.X);
size.Y = Math.Min(maxSize.Y, size.Y);
}
protected override void AdjustXPos(float growModifier, float deltaTime)
{
}
protected override void ReduceOxygen(float deltaTime)
{
}
}
}
@@ -0,0 +1,306 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Barotrauma
{
class Entity : ISpatialEntity
{
public const ushort NullEntityID = 0;
public const ushort EntitySpawnerID = ushort.MaxValue;
private static Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
public static List<Entity> GetEntityList()
{
return dictionary.Values.ToList();
}
public static EntitySpawner Spawner;
private ushort id;
protected AITarget aiTarget;
private bool idFreed;
public virtual bool Removed
{
get;
private set;
}
public bool IdFreed
{
get { return idFreed; }
}
public ushort ID
{
get
{
return id;
}
set
{
if (this is EntitySpawner) { return; }
if (value == NullEntityID)
{
DebugConsole.ThrowError("Cannot set the ID of an entity to " + NullEntityID +
"! The value is reserved for entity events referring to a non-existent (e.g. removed) entity.\n" + Environment.StackTrace);
return;
}
if (value == EntitySpawnerID)
{
DebugConsole.ThrowError("Cannot set the ID of an entity to " + EntitySpawnerID +
"! The value is reserved for EntitySpawner.\n" + Environment.StackTrace);
return;
}
if (dictionary.TryGetValue(id, out Entity thisEntity) && thisEntity == this)
{
dictionary.Remove(id);
}
//if there's already an entity with the same ID, give it the old ID of this one
if (dictionary.TryGetValue(value, out Entity existingEntity))
{
DebugConsole.Log(existingEntity + " had the same ID as " + this + " (" + value + ")");
dictionary.Remove(value);
dictionary.Add(id, existingEntity);
existingEntity.id = id;
DebugConsole.Log("The id of " + existingEntity + " is now " + id);
DebugConsole.Log("The id of " + this + " is now " + value);
}
id = value;
idFreed = false;
dictionary.Add(id, this);
}
}
public virtual Vector2 SimPosition
{
get { return Vector2.Zero; }
}
public virtual Vector2 Position
{
get { return Vector2.Zero; }
}
public virtual Vector2 WorldPosition
{
get { return Submarine == null ? Position : Submarine.Position + Position; }
}
public virtual Vector2 DrawPosition
{
get { return Submarine == null ? Position : Submarine.DrawPosition + Position; }
}
public Submarine Submarine
{
get;
set;
}
public AITarget AiTarget
{
get { return aiTarget; }
}
public double SpawnTime
{
get { return spawnTime; }
}
private readonly double spawnTime;
public Entity(Submarine submarine)
{
this.Submarine = submarine;
spawnTime = Timing.TotalTime;
//give a unique ID
id = this is EntitySpawner ?
EntitySpawnerID :
FindFreeID(submarine == null ? (ushort)1 : submarine.IdOffset);
dictionary.Add(id, this);
}
public static ushort FindFreeID(ushort idOffset = 0)
{
//ushort.MaxValue - 2 because 0 and ushort.MaxValue are reserved values
if (dictionary.Count >= ushort.MaxValue - 2)
{
throw new Exception("Maximum amount of entities (" + (ushort.MaxValue - 1) + ") reached!");
}
idOffset = Math.Max(idOffset, (ushort)1);
bool IDfound;
ushort id = idOffset;
do
{
id += 1;
IDfound = dictionary.ContainsKey(id);
} while (IDfound);
return id;
}
/// <summary>
/// Find an entity based on the ID
/// </summary>
public static Entity FindEntityByID(ushort ID)
{
Entity matchingEntity;
dictionary.TryGetValue(ID, out matchingEntity);
return matchingEntity;
}
public static void RemoveAll()
{
List<Entity> list = new List<Entity>(dictionary.Values);
foreach (Entity e in list)
{
try
{
e.Remove();
}
catch (Exception exception)
{
DebugConsole.ThrowError("Error while removing entity \"" + e.ToString() + "\"", exception);
GameAnalyticsManager.AddErrorEventOnce(
"Entity.RemoveAll:Exception" + e.ToString(),
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Error while removing entity \"" + e.ToString() + " (" + exception.Message + ")\n" + exception.StackTrace);
}
}
StringBuilder errorMsg = new StringBuilder();
if (dictionary.Count > 0)
{
errorMsg.AppendLine("Some entities were not removed in Entity.RemoveAll:");
foreach (Entity e in dictionary.Values)
{
errorMsg.AppendLine(" - " + e.ToString() + "(ID " + e.id + ")");
}
}
if (Item.ItemList.Count > 0)
{
errorMsg.AppendLine("Some items were not removed in Entity.RemoveAll:");
foreach (Item item in Item.ItemList)
{
errorMsg.AppendLine(" - " + item.Name + "(ID " + item.id + ")");
}
var items = new List<Item>(Item.ItemList);
foreach (Item item in items)
{
try
{
item.Remove();
}
catch (Exception exception)
{
DebugConsole.ThrowError("Error while removing item \"" + item.ToString() + "\"", exception);
}
}
Item.ItemList.Clear();
}
if (Character.CharacterList.Count > 0)
{
errorMsg.AppendLine("Some characters were not removed in Entity.RemoveAll:");
foreach (Character character in Character.CharacterList)
{
errorMsg.AppendLine(" - " + character.Name + "(ID " + character.id + ")");
}
var characters = new List<Character>(Character.CharacterList);
foreach (Character character in characters)
{
try
{
character.Remove();
}
catch (Exception exception)
{
DebugConsole.ThrowError("Error while removing character \"" + character.ToString() + "\"", exception);
}
}
Character.CharacterList.Clear();
}
if (!string.IsNullOrEmpty(errorMsg.ToString()))
{
foreach (string errorLine in errorMsg.ToString().Split('\n'))
{
DebugConsole.ThrowError(errorLine);
}
GameAnalyticsManager.AddErrorEventOnce("Entity.RemoveAll", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg.ToString());
}
dictionary.Clear();
Hull.EntityGrids.Clear();
Spawner?.Reset();
}
/// <summary>
/// Removes the entity from the entity dictionary and frees up the ID it was using.
/// </summary>
public void FreeID()
{
DebugConsole.Log("Removing entity " + ToString() + " (" + ID + ") from entity dictionary.");
if (!dictionary.TryGetValue(ID, out Entity existingEntity))
{
DebugConsole.Log("Entity " + ToString() + " (" + ID + ") not present in entity dictionary.");
GameAnalyticsManager.AddErrorEventOnce(
"Entity.FreeID:EntityNotFound" + ID,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Entity " + ToString() + " (" + ID + ") not present in entity dictionary.\n" + Environment.StackTrace);
}
else if (existingEntity != this)
{
DebugConsole.Log("Entity ID mismatch in entity dictionary. Entity " + existingEntity + " had the ID " + ID + " (expecting " + ToString() + ")");
GameAnalyticsManager.AddErrorEventOnce("Entity.FreeID:EntityMismatch" + ID,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Entity ID mismatch in entity dictionary. Entity " + existingEntity + " had the ID " + ID + " (expecting " + ToString() + ")");
foreach (var keyValuePair in dictionary.Where(kvp => kvp.Value == this).ToList())
{
dictionary.Remove(keyValuePair.Key);
}
}
dictionary.Remove(ID);
idFreed = true;
}
public virtual void Remove()
{
if (!idFreed) FreeID();
Removed = true;
}
public static void DumpIds(int count, string filename)
{
List<Entity> entities = dictionary.Values.OrderByDescending(e => e.id).ToList();
count = Math.Min(entities.Count, count);
List<string> lines = new List<string>();
for (int i = 0; i < count; i++)
{
lines.Add(entities[i].id + ": " + entities[i].ToString());
DebugConsole.ThrowError(entities[i].id + ": " + entities[i].ToString());
}
if (!string.IsNullOrWhiteSpace(filename))
{
File.WriteAllLines(filename, lines);
}
}
}
}
@@ -0,0 +1,156 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
{
class EntityGrid
{
private List<MapEntity> allEntities;
private List<MapEntity>[,] entities;
private readonly Rectangle limits;
private readonly float cellSize;
public readonly Submarine Submarine;
public Rectangle WorldRect
{
get
{
if (Submarine == null)
{
return limits;
}
else
{
return new Rectangle(
(int)(limits.X + Submarine.WorldPosition.X),
(int)(limits.Y + Submarine.WorldPosition.Y),
limits.Width, limits.Height);
}
}
}
public EntityGrid(Submarine submarine, float cellSize)
{
//make the grid slightly larger than the borders of the submarine,
//because docking ports may create gaps and hulls outside the borders
int padding = 128;
this.limits = new Rectangle(
submarine.Borders.X - padding,
submarine.Borders.Y + padding,
submarine.Borders.Width + padding * 2,
submarine.Borders.Height + padding * 2);
this.Submarine = submarine;
this.cellSize = cellSize;
InitializeGrid();
}
public EntityGrid(Rectangle worldRect, float cellSize)
{
this.limits = worldRect;
this.cellSize = cellSize;
InitializeGrid();
}
private void InitializeGrid()
{
allEntities = new List<MapEntity>();
entities = new List<MapEntity>[(int)Math.Ceiling(limits.Width / cellSize), (int)Math.Ceiling(limits.Height / cellSize)];
for (int x = 0; x < entities.GetLength(0); x++)
{
for (int y = 0; y < entities.GetLength(1); y++)
{
entities[x, y] = new List<MapEntity>();
}
}
}
public void InsertEntity(MapEntity entity)
{
Rectangle rect = entity.Rect;
//if (Submarine.Loaded != null) rect.Offset(-Submarine.HiddenSubPosition);
Rectangle indices = GetIndices(rect);
if (indices.Width < 0 || indices.X >= entities.GetLength(0) ||
indices.Height < 0 || indices.Y >= entities.GetLength(1))
{
DebugConsole.ThrowError("Error in EntityGrid.InsertEntity: " + entity + " is outside the grid");
return;
}
for (int x = Math.Max(indices.X, 0); x <= Math.Min(indices.Width, entities.GetLength(0) - 1); x++)
{
for (int y = Math.Max(indices.Y, 0); y <= Math.Min(indices.Height, entities.GetLength(1) - 1); y++)
{
entities[x, y].Add(entity);
}
}
allEntities.Add(entity);
}
public void RemoveEntity(MapEntity entity)
{
for (int x = 0; x < entities.GetLength(0); x++)
{
for (int y = 0; y < entities.GetLength(1); y++)
{
if (entities[x, y].Contains(entity)) entities[x, y].Remove(entity);
}
}
allEntities.Remove(entity);
}
public void Clear()
{
for (int x = 0; x < entities.GetLength(0); x++)
{
for (int y = 0; y < entities.GetLength(1); y++)
{
entities[x, y].Clear();
}
}
allEntities.Clear();
}
public IEnumerable<MapEntity> GetAllEntities()
{
return allEntities;
}
public List<MapEntity> GetEntities(Vector2 position)
{
if (!MathUtils.IsValid(position)) return null;
if (Submarine != null) position -= Submarine.HiddenSubPosition;
Point indices = GetIndices(position);
if (indices.X < 0 || indices.Y < 0 || indices.X >= entities.GetLength(0) || indices.Y >= entities.GetLength(1))
{
return null;
}
return entities[indices.X, indices.Y];
}
public Rectangle GetIndices(Rectangle rect)
{
Rectangle indices = Rectangle.Empty;
indices.X = (int)Math.Floor((rect.X - limits.X) / cellSize);
indices.Y = (int)Math.Floor((limits.Y - rect.Y) / cellSize);
indices.Width = (int)Math.Floor((rect.Right - limits.X) / cellSize);
indices.Height = (int)Math.Floor((limits.Y - (rect.Y - rect.Height)) / cellSize);
return indices;
}
public Point GetIndices(Vector2 position)
{
return new Point(
(int)Math.Floor((position.X - limits.X) / cellSize),
(int)Math.Floor((limits.Y - position.Y) / cellSize));
}
}
}
@@ -0,0 +1,347 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class Explosion
{
private static readonly List<Triplet<Explosion, Vector2, float>> prevExplosions = new List<Triplet<Explosion, Vector2, float>>();
private readonly Attack attack;
private readonly float force;
private readonly float cameraShake, cameraShakeRange;
private readonly Color screenColor;
private readonly float screenColorRange, screenColorDuration;
private bool sparks, shockwave, flames, smoke, flash, underwaterBubble;
private float flashDuration;
private float? flashRange;
private readonly string decal;
private readonly float decalSize;
public float EmpStrength { get; set; }
public Explosion(float range, float force, float damage, float structureDamage, float empStrength = 0.0f)
{
attack = new Attack(damage, 0.0f, 0.0f, structureDamage, range)
{
SeverLimbsProbability = 1.0f
};
this.force = force;
this.EmpStrength = empStrength;
sparks = true;
shockwave = true;
smoke = true;
flames = true;
underwaterBubble = true;
}
public Explosion(XElement element, string parentDebugName)
{
attack = new Attack(element, parentDebugName + ", Explosion");
force = element.GetAttributeFloat("force", 0.0f);
sparks = element.GetAttributeBool("sparks", true);
shockwave = element.GetAttributeBool("shockwave", true);
flames = element.GetAttributeBool("flames", true);
underwaterBubble = element.GetAttributeBool("underwaterbubble", true);
smoke = element.GetAttributeBool("smoke", true);
flash = element.GetAttributeBool("flash", true);
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
EmpStrength = element.GetAttributeFloat("empstrength", 0.0f);
decal = element.GetAttributeString("decal", "");
decalSize = element.GetAttributeFloat("decalSize", 1.0f);
cameraShake = element.GetAttributeFloat("camerashake", attack.Range * 0.1f);
cameraShakeRange = element.GetAttributeFloat("camerashakerange", attack.Range);
screenColorRange = element.GetAttributeFloat("screencolorrange", attack.Range * 0.1f);
screenColor = element.GetAttributeColor("screencolor", Color.Transparent);
screenColorDuration = element.GetAttributeFloat("screencolorduration", 0.1f);
}
public void DisableParticles()
{
sparks = false;
shockwave = false;
smoke = false;
flash = false;
flames = false;
underwaterBubble = false;
}
public List<Triplet<Explosion, Vector2, float>> GetRecentExplosions(float maxSecondsAgo)
{
return prevExplosions.FindAll(e => e.Third >= Timing.TotalTime - maxSecondsAgo);
}
public void Explode(Vector2 worldPosition, Entity damageSource, Character attacker = null)
{
prevExplosions.Add(new Triplet<Explosion, Vector2, float>(this, worldPosition, (float)Timing.TotalTime));
if (prevExplosions.Count > 100)
{
prevExplosions.RemoveAt(0);
}
Hull hull = Hull.FindHull(worldPosition);
ExplodeProjSpecific(worldPosition, hull);
float displayRange = attack.Range;
Vector2 cameraPos = Character.Controlled != null ? Character.Controlled.WorldPosition : GameMain.GameScreen.Cam.Position;
float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f;
GameMain.GameScreen.Cam.Shake = cameraShake * Math.Max((cameraShakeRange - cameraDist) / cameraShakeRange, 0.0f);
#if CLIENT
if (screenColor != Color.Transparent)
{
Color flashColor = Color.Lerp(Color.Transparent, screenColor, Math.Max((screenColorRange - cameraDist) / screenColorRange, 0.0f));
Screen.Selected.ColorFade(flashColor, Color.Transparent, screenColorDuration);
}
#endif
if (displayRange < 0.1f) { return; }
if (attack.GetStructureDamage(1.0f) > 0.0f)
{
RangedStructureDamage(worldPosition, displayRange, attack.GetStructureDamage(1.0f), attacker);
}
if (EmpStrength > 0.0f)
{
float displayRangeSqr = displayRange * displayRange;
foreach (Item item in Item.ItemList)
{
float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition);
if (distSqr > displayRangeSqr) continue;
float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange;
//damage repairable power-consuming items
var powered = item.GetComponent<Powered>();
if (powered == null || !powered.VulnerableToEMP) continue;
if (item.Repairables.Any())
{
item.Condition -= 100 * EmpStrength * distFactor;
}
//discharge batteries
var powerContainer = item.GetComponent<PowerContainer>();
if (powerContainer != null)
{
powerContainer.Charge -= powerContainer.Capacity * EmpStrength * distFactor;
}
}
}
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(attack.Stun, 0.0f) && MathUtils.NearlyEqual(attack.GetTotalDamage(false), 0.0f))
{
return;
}
DamageCharacters(worldPosition, attack, force, damageSource, attacker);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
if (flames)
{
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) { continue; }
//don't apply OnFire effects if the item is inside a fireproof container
//(or if it's inside a container that's inside a fireproof container, etc)
Item container = item.Container;
bool fireProof = false;
while (container != null)
{
if (container.FireProof) { fireProof = true; break; }
container = container.Container;
}
if (fireProof || Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.5f) { continue; }
item.ApplyStatusEffects(ActionType.OnFire, 1.0f);
if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire });
}
if (item.Prefab.DamagedByExplosions && !item.Indestructible)
{
float limbRadius = item.body == null ? 0.0f : item.body.GetMaxExtent();
float dist = Vector2.Distance(item.WorldPosition, worldPosition);
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius));
if (dist > attack.Range) { continue; }
float distFactor = 1.0f - dist / attack.Range;
float damageAmount = attack.GetItemDamage(1.0f);
item.Condition -= damageAmount * distFactor;
}
}
}
}
}
partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull);
public static void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker)
{
if (attack.Range <= 0.0f) return;
//long range for the broad distance check, because large characters may still be in range even if their collider isn't
float broadRange = Math.Max(attack.Range * 10.0f, 10000.0f);
foreach (Character c in Character.CharacterList)
{
if (!c.Enabled ||
Math.Abs(c.WorldPosition.X - worldPosition.X) > broadRange ||
Math.Abs(c.WorldPosition.Y - worldPosition.Y) > broadRange)
{
continue;
}
Vector2 explosionPos = worldPosition;
if (c.Submarine != null) { explosionPos -= c.Submarine.Position; }
Hull hull = Hull.FindHull(ConvertUnits.ToDisplayUnits(explosionPos), null, false);
bool underWater = hull == null || explosionPos.Y < hull.Surface;
explosionPos = ConvertUnits.ToSimUnits(explosionPos);
Dictionary<Limb, float> distFactors = new Dictionary<Limb, float>();
foreach (Limb limb in c.AnimController.Limbs)
{
float dist = Vector2.Distance(limb.WorldPosition, worldPosition);
//calculate distance from the "outer surface" of the physics body
//doesn't take the rotation of the limb into account, but should be accurate enough for this purpose
float limbRadius = Math.Max(Math.Max(limb.body.width * 0.5f, limb.body.height * 0.5f), limb.body.radius);
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius));
if (dist > attack.Range) { continue; }
float distFactor = 1.0f - dist / attack.Range;
//solid obstacles between the explosion and the limb reduce the effect of the explosion by 90%
if (Submarine.CheckVisibility(limb.SimPosition, explosionPos) != null) distFactor *= 0.1f;
distFactors.Add(limb, distFactor);
List<Affliction> modifiedAfflictions = new List<Affliction>();
foreach (Affliction affliction in attack.Afflictions.Keys)
{
modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / c.AnimController.Limbs.Length));
}
c.LastDamageSource = damageSource;
if (attacker == null)
{
if (damageSource is Item item)
{
attacker = item.GetComponent<Projectile>()?.User;
if (attacker == null) attacker = item.GetComponent<MeleeWeapon>()?.User;
}
}
//use a position slightly from the limb's position towards the explosion
//ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods
Vector2 hitPos = limb.WorldPosition + (worldPosition - limb.WorldPosition) / dist * 0.01f;
c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker);
if (attack.StatusEffects != null && attack.StatusEffects.Any())
{
attack.SetUser(attacker);
var statusEffectTargets = new List<ISerializableEntity>() { c, limb };
foreach (StatusEffect statusEffect in attack.StatusEffects)
{
statusEffect.Apply(ActionType.OnUse, 1.0f, damageSource, statusEffectTargets);
statusEffect.Apply(ActionType.Always, 1.0f, damageSource, statusEffectTargets);
statusEffect.Apply(underWater ? ActionType.InWater : ActionType.NotInWater, 1.0f, damageSource, statusEffectTargets);
}
}
if (limb.WorldPosition != worldPosition && !MathUtils.NearlyEqual(force, 0.0f))
{
Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition);
if (!MathUtils.IsValid(limbDiff)) limbDiff = Rand.Vector(1.0f);
Vector2 impulse = limbDiff * distFactor * force;
Vector2 impulsePoint = limb.SimPosition - limbDiff * limbRadius;
limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
//sever joints
if (c.IsDead && attack.SeverLimbsProbability > 0.0f)
{
foreach (Limb limb in c.AnimController.Limbs)
{
if (!distFactors.ContainsKey(limb)) { continue; }
if (Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability * distFactors[limb])
{
c.TrySeverLimbJoints(limb, 1.0f);
}
}
}
}
}
/// <summary>
/// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken
/// </summary>
public static Dictionary<Structure, float> RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null)
{
List<Structure> structureList = new List<Structure>();
float dist = 600.0f;
foreach (MapEntity entity in MapEntity.mapEntityList)
{
if (!(entity is Structure structure)) { continue; }
if (structure.HasBody &&
!structure.IsPlatform &&
Vector2.Distance(structure.WorldPosition, worldPosition) < dist * 3.0f)
{
structureList.Add(structure);
}
}
Dictionary<Structure, float> damagedStructures = new Dictionary<Structure, float>();
foreach (Structure structure in structureList)
{
for (int i = 0; i < structure.SectionCount; i++)
{
float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange);
if (distFactor <= 0.0f) continue;
structure.AddDamage(i, damage * distFactor, attacker);
if (damagedStructures.ContainsKey(structure))
{
damagedStructures[structure] += damage * distFactor;
}
else
{
damagedStructures.Add(structure, damage * distFactor);
}
}
}
return damagedStructures;
}
}
}
@@ -0,0 +1,367 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
#if CLIENT
using Barotrauma.Sounds;
using Barotrauma.Lights;
using Barotrauma.Particles;
#endif
using FarseerPhysics;
namespace Barotrauma
{
partial class FireSource : ISpatialEntity
{
const float OxygenConsumption = 50.0f;
const float GrowSpeed = 20.0f;
protected Hull hull;
protected Vector2 position;
protected Vector2 size;
private readonly Submarine submarine;
public Submarine Submarine => submarine;
protected bool removed;
#if CLIENT
private List<Decal> burnDecals = new List<Decal>();
#endif
public Vector2 Position
{
get { return position; }
set
{
if (!MathUtils.IsValid(value)) return;
position = value;
}
}
public Vector2 WorldPosition
{
get { return Submarine == null ? position : Submarine.Position + position; }
}
public Vector2 SimPosition => ConvertUnits.ToSimUnits(Position);
public Vector2 Size
{
get { return size; }
set
{
if (value == size) return;
Vector2 sizeChange = value - size;
size = value;
position.X -= sizeChange.X * 0.5f;
LimitSize();
}
}
public virtual float DamageRange
{
get { return (float)Math.Sqrt(size.X) * 20.0f; }
}
public Hull Hull
{
get { return hull; }
}
public FireSource(Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false)
{
hull = Hull.FindHull(worldPosition, spawningHull);
if (hull == null || worldPosition.Y < hull.WorldSurface) return;
#if CLIENT
if (!isNetworkMessage && GameMain.Client != null) return;
#endif
hull.AddFireSource(this);
position = worldPosition - new Vector2(-5.0f, 5.0f);
if (hull.Submarine != null)
{
submarine = hull.Submarine;
position -= Submarine.Position;
}
#if CLIENT
lightSource = new LightSource(this.position, 50.0f, new Color(1.0f, 0.9f, 0.7f), hull?.Submarine);
#endif
size = new Vector2(10.0f, 10.0f);
}
protected virtual void LimitSize()
{
if (hull == null) return;
position.X = Math.Max(hull.Rect.X, position.X);
position.Y = Math.Min(hull.Rect.Y, position.Y);
size.X = Math.Min(hull.Rect.Width - (position.X - hull.Rect.X), size.X);
size.Y = Math.Min(hull.Rect.Height - (hull.Rect.Y - position.Y), size.Y);
}
public static void UpdateAll(List<FireSource> fireSources, float deltaTime)
{
for (int i = fireSources.Count - 1; i >= 0; i--)
{
fireSources[i].Update(deltaTime);
}
//combine overlapping fires
for (int i = fireSources.Count - 1; i >= 0; i--)
{
for (int j = i - 1; j >= 0; j--)
{
i = Math.Min(i, fireSources.Count - 1);
j = Math.Min(j, i - 1);
if (!fireSources[i].CheckOverLap(fireSources[j])) continue;
float leftEdge = Math.Min(fireSources[i].position.X, fireSources[j].position.X);
fireSources[j].size.X =
Math.Max(fireSources[i].position.X + fireSources[i].size.X, fireSources[j].position.X + fireSources[j].size.X)
- leftEdge;
fireSources[j].position.X = leftEdge;
#if CLIENT
fireSources[j].burnDecals.AddRange(fireSources[i].burnDecals);
fireSources[j].burnDecals.Sort((d1, d2) => { return Math.Sign(d1.WorldPosition.X - d2.WorldPosition.X); });
#endif
fireSources[i].Remove();
}
}
}
private bool CheckOverLap(FireSource fireSource)
{
return !(position.X > fireSource.position.X + fireSource.size.X ||
position.X + size.X < fireSource.position.X);
}
public void Update(float deltaTime)
{
//the firesource will start to shrink if oxygen percentage is below 10
float growModifier = Math.Min((hull.OxygenPercentage / 10.0f) - 1.0f, 1.0f);
DamageCharacters(deltaTime);
DamageItems(deltaTime);
if (hull.WaterVolume > 0.0f)
{
HullWaterExtinguish(deltaTime);
if (removed) { return; }
}
ReduceOxygen(deltaTime);
AdjustXPos(growModifier, deltaTime);
size.X += GrowSpeed * growModifier * deltaTime;
size.Y = MathHelper.Clamp(size.Y + GrowSpeed * growModifier * deltaTime, 10.0f, 50.0f);
if (size.X > 50.0f)
{
this.position.Y = MathHelper.Lerp(this.position.Y, hull.Rect.Y - hull.Rect.Height + size.Y, deltaTime);
}
LimitSize();
UpdateProjSpecific(growModifier);
if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
Remove();
}
}
protected virtual void ReduceOxygen(float deltaTime)
{
hull.Oxygen -= size.X * deltaTime * OxygenConsumption;
}
protected virtual void AdjustXPos(float growModifier, float deltaTime)
{
position.X -= GrowSpeed * growModifier * 0.5f * deltaTime;
}
partial void UpdateProjSpecific(float growModifier);
private void OnChangeHull(Vector2 pos, Hull particleHull)
{
if (particleHull == hull || particleHull == null) return;
//hull already has a firesource roughly at the particles position -> don't create a new one
if (particleHull.FireSources.Find(fs => pos.X > fs.position.X - 100.0f && pos.X < fs.position.X + fs.size.X + 100.0f) != null) return;
new FireSource(new Vector2(pos.X, particleHull.WorldRect.Y - particleHull.Rect.Height + 5.0f));
}
private void DamageCharacters(float deltaTime)
{
if (size.X <= 0.0f) return;
for (int i = 0; i < Character.CharacterList.Count; i++)
{
Character c = Character.CharacterList[i];
if (c.AnimController.CurrentHull == null || c.IsDead) continue;
if (!IsInDamageRange(c, DamageRange)) continue;
float dmg = (float)Math.Sqrt(size.X) * deltaTime / c.AnimController.Limbs.Length;
foreach (Limb limb in c.AnimController.Limbs)
{
c.LastDamageSource = null;
c.DamageLimb(WorldPosition, limb, new List<Affliction>() { AfflictionPrefab.Burn.Instantiate(dmg) }, 0.0f, false, 0.0f);
}
c.ApplyStatusEffects(ActionType.OnFire, deltaTime);
}
}
public bool IsInDamageRange(Character c, float damageRange)
{
if (c.Position.X < position.X - damageRange || c.Position.X > position.X + size.X + damageRange) return false;
if (c.Position.Y < position.Y - size.Y || c.Position.Y > hull.Rect.Y) return false;
return true;
}
public bool IsInDamageRange(Vector2 worldPosition, float damageRange)
{
if (worldPosition.X < WorldPosition.X - damageRange || worldPosition.X > WorldPosition.X + size.X + damageRange) return false;
if (worldPosition.Y < WorldPosition.Y - size.Y || worldPosition.Y > hull.WorldRect.Y) return false;
return true;
}
private void DamageItems(float deltaTime)
{
if (size.X <= 0.0f) return;
#if CLIENT
if (GameMain.Client != null) return;
#endif
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) continue;
//don't apply OnFire effects if the item is inside a fireproof container
//(or if it's inside a container that's inside a fireproof container, etc)
Item container = item.Container;
while (container != null)
{
if (container.FireProof) return;
container = container.Container;
}
float range = (float)Math.Sqrt(size.X) * 10.0f;
if (item.Position.X < position.X - range || item.Position.X > position.X + size.X + range) continue;
if (item.Position.Y < position.Y - size.Y || item.Position.Y > hull.Rect.Y) continue;
item.ApplyStatusEffects(ActionType.OnFire, deltaTime);
if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire });
}
}
}
private void HullWaterExtinguish(float deltaTime)
{
//the higher the surface of the water is relative to the firesource, the faster it puts out the fire
float extinguishAmount = (hull.Surface - (position.Y - size.Y)) * deltaTime;
if (extinguishAmount < 0.0f) return;
#if CLIENT
float steamCount = Rand.Range(-5.0f, Math.Min(extinguishAmount * 100.0f, 10));
for (int i = 0; i < steamCount; i++)
{
Vector2 spawnPos = new Vector2(
WorldPosition.X + Rand.Range(0.0f, size.X),
WorldPosition.Y + 10.0f);
Vector2 speed = new Vector2((spawnPos.X - (WorldPosition.X + size.X / 2.0f)), (float)Math.Sqrt(size.X) * Rand.Range(20.0f, 25.0f));
var particle = GameMain.ParticleManager.CreateParticle("steam",
spawnPos, speed, 0.0f, hull);
}
#endif
extinguishAmount = Math.Min(size.X, extinguishAmount);
position.X += extinguishAmount / 2.0f;
size.X -= extinguishAmount;
//evaporate some of the water
hull.WaterVolume -= extinguishAmount;
if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
Remove();
}
}
public void Extinguish(float deltaTime, float amount)
{
float extinguishAmount = amount * deltaTime;
#if CLIENT
float steamCount = Rand.Range(-5.0f, (float)Math.Sqrt(amount));
for (int i = 0; i < steamCount; i++)
{
Vector2 spawnPos = new Vector2(Rand.Range(position.X, position.X + size.X), Rand.Range(position.Y - size.Y, position.Y) + 10.0f);
Vector2 speed = new Vector2((spawnPos.X - (position.X + size.X / 2.0f)), (float)Math.Sqrt(size.X) * Rand.Range(20.0f, 25.0f));
var particle = GameMain.ParticleManager.CreateParticle("steam",
spawnPos, speed, 0.0f, hull);
}
#endif
extinguishAmount = Math.Min(size.X, extinguishAmount);
position.X += extinguishAmount / 2.0f;
size.X -= extinguishAmount;
hull.WaterVolume -= extinguishAmount;
if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
Remove();
}
}
public void Extinguish(float deltaTime, float amount, Vector2 worldPosition)
{
if (IsInDamageRange(worldPosition, 100.0f))
{
Extinguish(deltaTime, amount);
}
}
public void Remove()
{
#if CLIENT
lightSource?.Remove();
lightSource = null;
foreach (Decal d in burnDecals)
{
d.StopFadeIn();
}
#endif
hull?.RemoveFire(this);
removed = true;
}
}
}
@@ -0,0 +1,772 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Xml.Linq;
namespace Barotrauma
{
partial class Gap : MapEntity
{
public static List<Gap> GapList = new List<Gap>();
const float MaxFlowForce = 500.0f;
public static bool ShowGaps = true;
const float OutsideColliderRaycastIntervalLowPrio = 1.5f;
const float OutsideColliderRaycastIntervalHighPrio = 0.1f;
public bool IsHorizontal
{
get;
private set;
}
//a value between 0.0f-1.0f (0.0 = closed, 1.0f = open)
private float open;
//the force of the water flow which is exerted on physics bodies
private Vector2 flowForce;
private Hull flowTargetHull;
private float higherSurface;
private float lowerSurface;
private Vector2 lerpedFlowForce;
//if set to true, hull connections of this gap won't be updated when changes are being done to hulls
public bool DisableHullRechecks;
//can ambient light get through the gap even if it's not open
public bool PassAmbientLight;
//a collider outside the gap (for example an ice wall next to the sub)
//used by ragdolls to prevent them from ending up inside colliders when teleporting out of the sub
private Body outsideCollisionBlocker;
private float outsideColliderRaycastTimer;
public float Open
{
get { return open; }
set { open = MathHelper.Clamp(value, 0.0f, 1.0f); }
}
public float Size => IsHorizontal ? Rect.Height : Rect.Width;
public Door ConnectedDoor;
public Structure ConnectedWall;
public Vector2 LerpedFlowForce
{
get { return lerpedFlowForce; }
}
public Hull FlowTargetHull
{
get { return flowTargetHull; }
}
public bool IsRoomToRoom
{
get
{
return linkedTo.Count == 2;
}
}
public override Rectangle Rect
{
get
{
return base.Rect;
}
set
{
base.Rect = value;
FindHulls();
}
}
public override string Name
{
get
{
return "Gap";
}
}
public Gap(MapEntityPrefab prefab, Rectangle rectangle)
: this (rectangle, Submarine.MainSub)
{ }
public Gap(Rectangle rect, Submarine submarine)
: this(rect, rect.Width < rect.Height, submarine)
{ }
public Gap(Rectangle rect, bool isHorizontal, Submarine submarine)
: base(MapEntityPrefab.Find(null, "gap"), submarine)
{
this.rect = rect;
flowForce = Vector2.Zero;
IsHorizontal = isHorizontal;
open = 1.0f;
FindHulls();
GapList.Add(this);
InsertToList();
outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * 2.0f, Vector2.UnitX * 2.0f);
outsideCollisionBlocker.BodyType = BodyType.Static;
outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall;
outsideCollisionBlocker.CollidesWith = Physics.CollisionCharacter;
outsideCollisionBlocker.Enabled = false;
Resized += newRect => IsHorizontal = newRect.Width < newRect.Height;
DebugConsole.Log("Created gap (" + ID + ")");
}
public override MapEntity Clone()
{
return new Gap(rect, IsHorizontal, Submarine);
}
public override void Move(Vector2 amount)
{
base.Move(amount);
if (!DisableHullRechecks) FindHulls();
}
public static void UpdateHulls()
{
foreach (Gap g in GapList)
{
for (int i = g.linkedTo.Count - 1; i >= 0; i--)
{
if (g.linkedTo[i].Removed)
{
g.linkedTo.RemoveAt(i);
}
}
if (g.DisableHullRechecks) continue;
g.FindHulls();
}
}
public override bool IsMouseOn(Vector2 position)
{
return ShowGaps && Submarine.RectContains(WorldRect, position) &&
!Submarine.RectContains(MathUtils.ExpandRect(WorldRect, -5), position);
}
public void AutoOrient()
{
Vector2 searchPosLeft = new Vector2(rect.X, rect.Y - rect.Height / 2);
Hull hullLeft = Hull.FindHullOld(searchPosLeft, null, false);
Vector2 searchPosRight = new Vector2(rect.Right, rect.Y - rect.Height / 2);
Hull hullRight = Hull.FindHullOld(searchPosRight, null, false);
if (hullLeft != null && hullRight != null && hullLeft != hullRight)
{
IsHorizontal = true;
return;
}
Vector2 searchPosTop = new Vector2(rect.Center.X, rect.Y);
Hull hullTop = Hull.FindHullOld(searchPosTop, null, false);
Vector2 searchPosBottom = new Vector2(rect.Center.X, rect.Y - rect.Height);
Hull hullBottom = Hull.FindHullOld(searchPosBottom, null, false);
if (hullTop != null && hullBottom != null && hullTop != hullBottom)
{
IsHorizontal = false;
return;
}
if ((hullLeft == null) != (hullRight == null))
{
IsHorizontal = true;
}
else if ((hullTop == null) != (hullBottom == null))
{
IsHorizontal = false;
}
}
private void FindHulls()
{
Hull[] hulls = new Hull[2];
linkedTo.Clear();
Vector2[] searchPos = new Vector2[2];
if (IsHorizontal)
{
searchPos[0] = new Vector2(rect.X, rect.Y - rect.Height / 2);
searchPos[1] = new Vector2(rect.Right, rect.Y - rect.Height / 2);
}
else
{
searchPos[0] = new Vector2(rect.Center.X, rect.Y);
searchPos[1] = new Vector2(rect.Center.X, rect.Y - rect.Height);
}
for (int i = 0; i < 2; i++)
{
hulls[i] = Hull.FindHullOld(searchPos[i], null, false);
if (hulls[i] == null) hulls[i] = Hull.FindHullOld(searchPos[i], null, false, true);
}
if (hulls[1] == hulls[0]) { hulls[1] = null; }
if (hulls[0] == null && hulls[1] == null) { return; }
if (hulls[0] == null && hulls[1] != null)
{
Hull temp = hulls[0];
hulls[0] = hulls[1];
hulls[1] = temp;
}
flowTargetHull = hulls[0];
for (int i = 0; i < 2; i++)
{
if (hulls[i] == null) continue;
linkedTo.Add(hulls[i]);
if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this);
}
}
public override void Update(float deltaTime, Camera cam)
{
flowForce = Vector2.Zero;
outsideColliderRaycastTimer -= deltaTime;
if (open == 0.0f || linkedTo.Count == 0)
{
lerpedFlowForce = Vector2.Zero;
return;
}
UpdateOxygen();
if (linkedTo.Count == 1)
{
//gap leading from a room to outside
UpdateRoomToOut(deltaTime);
}
else
{
//gap leading from a room to another
UpdateRoomToRoom(deltaTime);
}
flowForce.X = MathHelper.Clamp(flowForce.X, -MaxFlowForce, MaxFlowForce);
flowForce.Y = MathHelper.Clamp(flowForce.Y, -MaxFlowForce, MaxFlowForce);
lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f);
EmitParticles(deltaTime);
if (flowTargetHull != null && lerpedFlowForce.LengthSquared() > 0.0001f)
{
foreach (Character character in Character.CharacterList)
{
if (character.CurrentHull == null) continue;
if (character.CurrentHull != linkedTo[0] as Hull &&
(linkedTo.Count < 2 || character.CurrentHull != linkedTo[1] as Hull))
{
continue;
}
foreach (Limb limb in character.AnimController.Limbs)
{
if (!limb.inWater) continue;
float dist = Vector2.Distance(limb.WorldPosition, WorldPosition);
if (dist > lerpedFlowForce.Length()) continue;
Vector2 force = lerpedFlowForce / (float)Math.Max(Math.Sqrt(dist), 20.0f) * 0.025f;
//vertical gaps only apply forces if the character is roughly above/below the gap
if (!IsHorizontal)
{
float xDist = Math.Abs(limb.WorldPosition.X - WorldPosition.X);
if (xDist > rect.Width || rect.Width == 0) break;
force *= 1.0f - xDist / rect.Width;
}
if (!MathUtils.IsValid(force))
{
string errorMsg = "Attempted to apply invalid flow force to the character \"" + character.Name +
"\", gap pos: " + WorldPosition +
", limb pos: " + limb.WorldPosition +
", flowforce: " + flowForce + ", lerpedFlowForce:" + lerpedFlowForce +
", dist: " + dist;
DebugConsole.Log(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Gap.Update:InvalidFlowForce:" + character.Name,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg);
continue;
}
character.AnimController.Collider.ApplyForce(force * limb.body.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
}
}
partial void EmitParticles(float deltaTime);
void UpdateRoomToRoom(float deltaTime)
{
if (linkedTo.Count < 2) return;
Hull hull1 = (Hull)linkedTo[0];
Hull hull2 = (Hull)linkedTo[1];
Vector2 subOffset = Vector2.Zero;
if (hull1.Submarine != Submarine)
{
subOffset = Submarine.Position - hull1.Submarine.Position;
}
else if (hull2.Submarine != Submarine)
{
subOffset = hull2.Submarine.Position - Submarine.Position;
}
if (hull1.WaterVolume <= 0.0 && hull2.WaterVolume <= 0.0) return;
float size = IsHorizontal ? rect.Height : rect.Width;
//a variable affecting the water flow through the gap
//the larger the gap is, the faster the water flows
float sizeModifier = size / 100.0f * open;
//horizontal gap (such as a regular door)
if (IsHorizontal)
{
higherSurface = Math.Max(hull1.Surface, hull2.Surface + subOffset.Y);
float delta = 0.0f;
//water level is above the lower boundary of the gap
if (Math.Max(hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.Surface + subOffset.Y + hull2.WaveY[0]) > rect.Y - size)
{
int dir = (hull1.Pressure > hull2.Pressure + subOffset.Y) ? 1 : -1;
//water flowing from the righthand room to the lefthand room
if (dir == -1)
{
if (!(hull2.WaterVolume > 0.0f)) return;
lowerSurface = hull1.Surface - hull1.WaveY[hull1.WaveY.Length - 1];
//delta = Math.Min((room2.water.pressure - room1.water.pressure) * sizeModifier, Math.Min(room2.water.Volume, room2.Volume));
//delta = Math.Min(delta, room1.Volume - room1.water.Volume + Water.MaxCompress);
flowTargetHull = hull1;
//make sure not to move more than what the room contains
delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 5.0f * sizeModifier, Math.Min(hull2.WaterVolume, hull2.Volume));
//make sure not to place more water to the target room than it can hold
delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - (hull1.WaterVolume));
hull1.WaterVolume += delta;
hull2.WaterVolume -= delta;
if (hull1.WaterVolume > hull1.Volume)
{
hull1.Pressure = Math.Max(hull1.Pressure, (hull1.Pressure + hull2.Pressure+subOffset.Y) / 2);
}
flowForce = new Vector2(-delta, 0.0f);
}
else if (dir == 1)
{
if (!(hull1.WaterVolume > 0.0f)) return;
lowerSurface = hull2.Surface - hull2.WaveY[hull2.WaveY.Length - 1];
flowTargetHull = hull2;
//make sure not to move more than what the room contains
delta = Math.Min((hull1.Pressure - (hull2.Pressure + subOffset.Y)) * 5.0f * sizeModifier, Math.Min(hull1.WaterVolume, hull1.Volume));
//make sure not to place more water to the target room than it can hold
delta = Math.Min(delta, hull2.Volume * Hull.MaxCompress - (hull2.WaterVolume));
hull1.WaterVolume -= delta;
hull2.WaterVolume += delta;
if (hull2.WaterVolume > hull2.Volume)
{
hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure-subOffset.Y) + hull2.Pressure) / 2);
}
flowForce = new Vector2(delta, 0.0f);
}
if (delta > 100.0f && subOffset == Vector2.Zero)
{
float avg = (hull1.Surface + hull2.Surface) / 2.0f;
if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress &&
hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1] < rect.Y)
{
hull1.WaveVel[hull1.WaveY.Length - 1] = (avg - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 0.1f;
hull1.WaveVel[hull1.WaveY.Length - 2] = hull1.WaveVel[hull1.WaveY.Length - 1];
}
if (hull2.WaterVolume < hull2.Volume / Hull.MaxCompress &&
hull2.Surface + hull2.WaveY[0] < rect.Y)
{
hull2.WaveVel[0] = (avg - (hull2.Surface + hull2.WaveY[0])) * 0.1f;
hull2.WaveVel[1] = hull2.WaveVel[0];
}
}
}
}
else
{
//lower room is full of water
if (hull2.Pressure + subOffset.Y > hull1.Pressure && hull2.WaterVolume > 0.0f)
{
float delta = Math.Min(hull2.WaterVolume - hull2.Volume + (hull2.Volume * Hull.MaxCompress), deltaTime * 8000.0f * sizeModifier);
//make sure not to place more water to the target room than it can hold
if (hull1.WaterVolume + delta > hull1.Volume * Hull.MaxCompress)
{
delta -= (hull1.WaterVolume + delta) - (hull1.Volume * Hull.MaxCompress);
}
delta = Math.Max(delta, 0.0f);
hull1.WaterVolume += delta;
hull2.WaterVolume -= delta;
flowForce = new Vector2(
0.0f,
Math.Min(Math.Min((hull2.Pressure + subOffset.Y) - hull1.Pressure, 200.0f), delta));
flowTargetHull = hull1;
if (hull1.WaterVolume > hull1.Volume)
{
hull1.Pressure = Math.Max(hull1.Pressure, (hull1.Pressure + (hull2.Pressure + subOffset.Y)) / 2);
}
}
//there's water in the upper room, drop to lower
else if (hull1.WaterVolume > 0)
{
flowTargetHull = hull2;
//make sure the amount of water moved isn't more than what the room contains
float delta = Math.Min(hull1.WaterVolume, deltaTime * 25000f * sizeModifier);
//make sure not to place more water to the target room than it can hold
if (hull2.WaterVolume + delta > hull2.Volume * Hull.MaxCompress)
{
delta -= (hull2.WaterVolume + delta) - (hull2.Volume * Hull.MaxCompress);
}
hull1.WaterVolume -= delta;
hull2.WaterVolume += delta;
flowForce = new Vector2(
hull1.WaveY[hull1.GetWaveIndex(rect.X)] - hull1.WaveY[hull1.GetWaveIndex(rect.Right)],
MathHelper.Clamp(-delta, -200.0f, 0.0f));
if (hull2.WaterVolume > hull2.Volume)
{
hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure - subOffset.Y) + hull2.Pressure) / 2);
}
}
}
if (open > 0.0f)
{
if (hull1.WaterVolume > hull1.Volume / Hull.MaxCompress && hull2.WaterVolume > hull2.Volume / Hull.MaxCompress)
{
float avgLethality = (hull1.LethalPressure + hull2.LethalPressure) / 2.0f;
hull1.LethalPressure = avgLethality;
hull2.LethalPressure = avgLethality;
}
else
{
hull1.LethalPressure = 0.0f;
hull2.LethalPressure = 0.0f;
}
}
}
void UpdateRoomToOut(float deltaTime)
{
if (linkedTo.Count != 1) return;
float size = (IsHorizontal) ? rect.Height : rect.Width;
Hull hull1 = (Hull)linkedTo[0];
//a variable affecting the water flow through the gap
//the larger the gap is, the faster the water flows
float sizeModifier = size * open * open;
float delta = hull1.Volume * Hull.MaxCompress * sizeModifier * deltaTime;
//make sure not to place more water to the target room than it can hold
delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume);
hull1.WaterVolume += delta;
if (hull1.WaterVolume > hull1.Volume) hull1.Pressure += 0.5f;
flowTargetHull = hull1;
if (IsHorizontal)
{
//water flowing from right to left
if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f)
{
flowForce = new Vector2(-delta, 0.0f);
}
else
{
flowForce = new Vector2(delta, 0.0f);
}
higherSurface = hull1.Surface;
lowerSurface = rect.Y;
if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress &&
hull1.Surface < rect.Y)
{
if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f)
{
float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 0.1f;
vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f);
hull1.WaveVel[hull1.WaveY.Length - 1] += vel;
hull1.WaveVel[hull1.WaveY.Length - 2] += vel;
}
else
{
float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[0])) * 0.1f;
vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f);
hull1.WaveVel[0] += vel;
hull1.WaveVel[1] += vel;
}
}
else
{
hull1.LethalPressure += (Submarine != null && Submarine.AtDamageDepth) ? 100.0f * deltaTime : 10.0f * deltaTime;
}
}
else
{
if (rect.Y > hull1.Rect.Y - hull1.Rect.Height / 2.0f)
{
flowForce = new Vector2(0.0f, -delta);
}
else
{
flowForce = new Vector2(0.0f, delta);
}
if (hull1.WaterVolume >= hull1.Volume / Hull.MaxCompress)
{
hull1.LethalPressure += (Submarine != null && Submarine.AtDamageDepth) ? 100.0f * deltaTime : 10.0f * deltaTime;
}
}
}
public bool RefreshOutsideCollider()
{
if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || !(linkedTo[0] is Hull)) return false;
if (outsideColliderRaycastTimer <= 0.0f)
{
UpdateOutsideColliderPos((Hull)linkedTo[0]);
outsideColliderRaycastTimer = outsideCollisionBlocker.Enabled ?
OutsideColliderRaycastIntervalHighPrio :
OutsideColliderRaycastIntervalLowPrio;
}
return outsideCollisionBlocker.Enabled;
}
private void UpdateOutsideColliderPos(Hull hull)
{
if (Submarine == null || IsRoomToRoom) { return; }
Vector2 rayDir;
if (IsHorizontal)
{
rayDir = new Vector2(Math.Sign(rect.Center.X - hull.Rect.Center.X), 0);
}
else
{
rayDir = new Vector2(0, Math.Sign((rect.Y - rect.Height / 2) - (hull.Rect.Y - hull.Rect.Height / 2)));
}
Vector2 rayStart = ConvertUnits.ToSimUnits(WorldPosition);
Vector2 rayEnd = rayStart + rayDir * 500.0f;
var blockingBody = Submarine.CheckVisibility(rayStart, rayEnd);
if (blockingBody != null)
{
//if the ray hit the body of the submarine itself (for example, if there's 2 layers of walls) we can ignore it
if (blockingBody.UserData == Submarine) { return; }
outsideCollisionBlocker.Enabled = true;
Vector2 colliderPos = Submarine.LastPickedPosition - Submarine.SimPosition;
float colliderRotation = MathUtils.VectorToAngle(rayDir) - MathHelper.PiOver2;
outsideCollisionBlocker.SetTransformIgnoreContacts(ref colliderPos, colliderRotation);
}
else
{
outsideCollisionBlocker.Enabled = false;
}
}
private void UpdateOxygen()
{
if (linkedTo.Count < 2) { return; }
Hull hull1 = (Hull)linkedTo[0];
Hull hull2 = (Hull)linkedTo[1];
if (IsHorizontal)
{
//if the water level is above the gap, oxygen doesn't circulate
if (Math.Max(hull1.WorldSurface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.WorldSurface + hull2.WaveY[0]) > WorldRect.Y) { return; }
}
float totalOxygen = hull1.Oxygen + hull2.Oxygen;
float totalVolume = (hull1.Volume + hull2.Volume);
float deltaOxygen = (totalOxygen * hull1.Volume / totalVolume) - hull1.Oxygen;
deltaOxygen = MathHelper.Clamp(deltaOxygen, -Hull.OxygenDistributionSpeed, Hull.OxygenDistributionSpeed);
hull1.Oxygen += deltaOxygen;
hull2.Oxygen -= deltaOxygen;
}
public static Gap FindAdjacent(IEnumerable<Gap> gaps, Vector2 worldPos, float allowedOrthogonalDist)
{
foreach (Gap gap in gaps)
{
if (gap.Open == 0.0f || gap.IsRoomToRoom) continue;
if (gap.ConnectedWall != null)
{
int sectionIndex = gap.ConnectedWall.FindSectionIndex(gap.Position);
if (sectionIndex > -1 && !gap.ConnectedWall.SectionBodyDisabled(sectionIndex)) continue;
}
if (gap.IsHorizontal)
{
if (worldPos.Y < gap.WorldRect.Y && worldPos.Y > gap.WorldRect.Y - gap.WorldRect.Height &&
Math.Abs(gap.WorldRect.Center.X - worldPos.X) < allowedOrthogonalDist)
{
return gap;
}
}
else
{
if (worldPos.X > gap.WorldRect.X && worldPos.X < gap.WorldRect.Right &&
Math.Abs(gap.WorldRect.Y - gap.WorldRect.Height / 2 - worldPos.Y) < allowedOrthogonalDist)
{
return gap;
}
}
}
return null;
}
public override void ShallowRemove()
{
base.ShallowRemove();
GapList.Remove(this);
foreach (Hull hull in Hull.hullList)
{
hull.ConnectedGaps.Remove(this);
}
}
public override void Remove()
{
base.Remove();
GapList.Remove(this);
foreach (Hull hull in Hull.hullList)
{
hull.ConnectedGaps.Remove(this);
}
if (outsideCollisionBlocker != null)
{
GameMain.World.Remove(outsideCollisionBlocker);
outsideCollisionBlocker = null;
}
}
public override void OnMapLoaded()
{
if (!DisableHullRechecks) FindHulls();
}
public static Gap Load(XElement element, Submarine submarine)
{
Rectangle rect = Rectangle.Empty;
if (element.Attribute("rect") != null)
{
rect = element.GetAttributeRect("rect", Rectangle.Empty);
}
else
{
//backwards compatibility
rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
int.Parse(element.Attribute("width").Value),
int.Parse(element.Attribute("height").Value));
}
bool isHorizontal = rect.Height > rect.Width;
var horizontalAttribute = element.Attribute("horizontal");
if (horizontalAttribute != null)
{
isHorizontal = horizontalAttribute.Value.ToString() == "true";
}
Gap g = new Gap(rect, isHorizontal, submarine);
g.ID = (ushort)int.Parse(element.Attribute("ID").Value);
g.linkedToID = new List<ushort>();
return g;
}
public override XElement Save(XElement parentElement)
{
XElement element = new XElement("Gap");
element.Add(
new XAttribute("ID", ID),
new XAttribute("horizontal", IsHorizontal ? "true" : "false"));
element.Add(new XAttribute("rect",
(int)(rect.X - Submarine.HiddenSubPosition.X) + "," +
(int)(rect.Y - Submarine.HiddenSubPosition.Y) + "," +
rect.Width + "," + rect.Height));
parentElement.Add(element);
return element;
}
}
}
@@ -0,0 +1,931 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
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 static bool ShowHulls = true;
public static bool EditWater, EditFire;
public const float OxygenDistributionSpeed = 500.0f;
public const float OxygenDeteriorationSpeed = 0.3f;
public const float OxygenConsumptionSpeed = 700.0f;
public const int WaveWidth = 32;
public static float WaveStiffness = 0.02f;
public static float WaveSpread = 0.05f;
public static float WaveDampening = 0.05f;
//how much excess water the room can contain, relative to the volume of the room.
//needed to make it possible for pressure to "push" water up through U-shaped hull configurations
public const float MaxCompress = 1.05f;
public readonly Dictionary<string, SerializableProperty> properties;
public Dictionary<string, SerializableProperty> SerializableProperties
{
get { return properties; }
}
private float lethalPressure;
private float surface, drawSurface;
private float waterVolume;
private float pressure;
private float oxygen;
private bool update;
public bool Visible = true;
private float[] waveY; //displacement from the surface of the water
private float[] waveVel; //velocity of the point
private float[] leftDelta;
private float[] rightDelta;
public readonly List<Gap> ConnectedGaps = new List<Gap>();
public override string Name
{
get
{
return "Hull";
}
}
public string DisplayName
{
get;
private set;
}
private string roomName;
[Editable, Serialize("", true, translationTextTag: "RoomName.")]
public string RoomName
{
get { return roomName; }
set
{
if (roomName == value) { return; }
roomName = value;
DisplayName = TextManager.Get(roomName, returnNull: true) ?? roomName;
}
}
public override Rectangle Rect
{
get
{
return base.Rect;
}
set
{
float prevOxygenPercentage = OxygenPercentage;
base.Rect = value;
if (Submarine == null || !Submarine.Loading)
{
Item.UpdateHulls();
Gap.UpdateHulls();
}
OxygenPercentage = prevOxygenPercentage;
surface = drawSurface = rect.Y - rect.Height + WaterVolume / rect.Width;
Pressure = surface;
}
}
public override bool Linkable
{
get { return true; }
}
public float LethalPressure
{
get { return lethalPressure; }
set { lethalPressure = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
public Vector2 Size
{
get { return new Vector2(rect.Width, rect.Height); }
}
public float CeilingHeight
{
get;
private set;
}
public float Surface
{
get { return surface; }
}
public float DrawSurface
{
get { return drawSurface; }
set
{
if (Math.Abs(drawSurface - value) < 0.00001f) return;
drawSurface = MathHelper.Clamp(value, rect.Y - rect.Height, rect.Y);
update = true;
}
}
public float WorldSurface
{
get { return Submarine == null ? surface : surface + Submarine.Position.Y; }
}
public float WaterVolume
{
get { return waterVolume; }
set
{
if (!MathUtils.IsValid(value)) return;
waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress);
if (waterVolume < Volume) Pressure = rect.Y - rect.Height + waterVolume / rect.Width;
if (waterVolume > 0.0f) update = true;
}
}
[Serialize(100000.0f, true)]
public float Oxygen
{
get { return oxygen; }
set
{
if (!MathUtils.IsValid(value)) return;
oxygen = MathHelper.Clamp(value, 0.0f, Volume);
}
}
public float WaterPercentage => MathUtils.Percentage(WaterVolume, Volume);
public float OxygenPercentage
{
get { return Volume <= 0.0f ? 100.0f : oxygen / Volume * 100.0f; }
set { Oxygen = (value / 100.0f) * Volume; }
}
public float Volume
{
get { return rect.Width * rect.Height; }
}
public float Pressure
{
get { return pressure; }
set { pressure = value; }
}
public float[] WaveY
{
get { return waveY; }
}
public float[] WaveVel
{
get { return waveVel; }
}
public List<FireSource> FireSources { get; private set; }
public Hull(MapEntityPrefab prefab, Rectangle rectangle)
: this (prefab, rectangle, Submarine.MainSub)
{
}
public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine)
: base (prefab, submarine)
{
rect = rectangle;
OxygenPercentage = 100.0f;
FireSources = new List<FireSource>();
properties = SerializableProperty.GetProperties(this);
int arraySize = (int)Math.Ceiling((float)rectangle.Width / WaveWidth + 1);
waveY = new float[arraySize];
waveVel = new float[arraySize];
leftDelta = new float[arraySize];
rightDelta = new float[arraySize];
surface = rect.Y - rect.Height;
if (submarine != null)
{
aiTarget = new AITarget(this)
{
MinSightRange = 2000,
MaxSightRange = 5000,
MaxSoundRange = 5000,
SoundRange = 0
};
}
hullList.Add(this);
if (submarine == null || !submarine.Loading)
{
Item.UpdateHulls();
Gap.UpdateHulls();
}
WaterVolume = 0.0f;
InsertToList();
DebugConsole.Log("Created hull (" + ID + ")");
}
public static Rectangle GetBorders()
{
if (!hullList.Any()) return Rectangle.Empty;
Rectangle rect = hullList[0].rect;
foreach (Hull hull in hullList)
{
if (hull.Rect.X < rect.X)
{
rect.Width += rect.X - hull.rect.X;
rect.X = hull.rect.X;
}
if (hull.rect.Right > rect.Right) rect.Width = hull.rect.Right - rect.X;
if (hull.rect.Y > rect.Y)
{
rect.Height += hull.rect.Y - rect.Y;
rect.Y = hull.rect.Y;
}
if (hull.rect.Y - hull.rect.Height < rect.Y - rect.Height) rect.Height = rect.Y - (hull.rect.Y - hull.rect.Height);
}
return rect;
}
public override MapEntity Clone()
{
return new Hull(MapEntityPrefab.Find(null, "hull"), rect, Submarine);
}
public static EntityGrid GenerateEntityGrid(Rectangle worldRect)
{
var newGrid = new EntityGrid(worldRect, 200.0f);
EntityGrids.Add(newGrid);
return newGrid;
}
public static EntityGrid GenerateEntityGrid(Submarine submarine)
{
var newGrid = new EntityGrid(submarine, 200.0f);
EntityGrids.Add(newGrid);
foreach (Hull hull in hullList)
{
if (hull.Submarine == submarine) newGrid.InsertEntity(hull);
}
return newGrid;
}
public override void OnMapLoaded()
{
CeilingHeight = Rect.Height;
Body lowerPickedBody = Submarine.PickBody(SimPosition, SimPosition - new Vector2(0.0f, ConvertUnits.ToSimUnits(rect.Height / 2.0f + 0.1f)), null, Physics.CollisionWall);
if (lowerPickedBody != null)
{
Vector2 lowerPickedPos = Submarine.LastPickedPosition;
if (Submarine.PickBody(SimPosition, SimPosition + new Vector2(0.0f, ConvertUnits.ToSimUnits(rect.Height / 2.0f + 0.1f)), null, Physics.CollisionWall) != null)
{
Vector2 upperPickedPos = Submarine.LastPickedPosition;
CeilingHeight = ConvertUnits.ToDisplayUnits(upperPickedPos.Y - lowerPickedPos.Y);
}
}
Pressure = rect.Y - rect.Height + waterVolume / rect.Width;
}
public void AddToGrid(Submarine submarine)
{
foreach (EntityGrid grid in EntityGrids)
{
if (grid.Submarine != submarine) continue;
rect.Location -= MathUtils.ToPoint(submarine.HiddenSubPosition);
grid.InsertEntity(this);
rect.Location += MathUtils.ToPoint(submarine.HiddenSubPosition);
return;
}
}
public int GetWaveIndex(Vector2 position)
{
return GetWaveIndex(position.X);
}
public int GetWaveIndex(float xPos)
{
int index = (int)(xPos - rect.X) / WaveWidth;
index = (int)MathHelper.Clamp(index, 0, waveY.Length - 1);
return index;
}
public override void Move(Vector2 amount)
{
rect.X += (int)amount.X;
rect.Y += (int)amount.Y;
if (Submarine == null || !Submarine.Loading)
{
Item.UpdateHulls();
Gap.UpdateHulls();
}
surface = drawSurface = rect.Y - rect.Height + WaterVolume / rect.Width;
Pressure = surface;
}
public override void ShallowRemove()
{
base.Remove();
hullList.Remove(this);
if (Submarine == null || (!Submarine.Loading && !Submarine.Unloading))
{
Item.UpdateHulls();
Gap.UpdateHulls();
}
List<FireSource> fireSourcesToRemove = new List<FireSource>(FireSources);
foreach (FireSource fireSource in fireSourcesToRemove)
{
fireSource.Remove();
}
FireSources.Clear();
if (EntityGrids != null)
{
foreach (EntityGrid entityGrid in EntityGrids)
{
entityGrid.RemoveEntity(this);
}
}
}
public override void Remove()
{
base.Remove();
hullList.Remove(this);
if (Submarine != null && !Submarine.Loading && !Submarine.Unloading)
{
Item.UpdateHulls();
Gap.UpdateHulls();
}
List<FireSource> fireSourcesToRemove = new List<FireSource>(FireSources);
foreach (FireSource fireSource in fireSourcesToRemove)
{
fireSource.Remove();
}
FireSources.Clear();
if (EntityGrids != null)
{
foreach (EntityGrid entityGrid in EntityGrids)
{
entityGrid.RemoveEntity(this);
}
}
}
public void AddFireSource(FireSource fireSource)
{
FireSources.Add(fireSource);
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
UpdateProjSpecific(deltaTime, cam);
Oxygen -= OxygenDeteriorationSpeed * deltaTime;
FireSource.UpdateAll(FireSources, deltaTime);
if (aiTarget != null)
{
aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange;
aiTarget.SoundRange -= deltaTime * 1000.0f;
}
if (!update)
{
lethalPressure = 0.0f;
return;
}
surface = Math.Max(MathHelper.Lerp(
surface,
rect.Y - rect.Height + WaterVolume / rect.Width,
deltaTime * 10.0f), rect.Y - rect.Height);
//interpolate the position of the rendered surface towards the "target surface"
drawSurface = Math.Max(MathHelper.Lerp(
drawSurface,
rect.Y - rect.Height + WaterVolume / rect.Width,
deltaTime * 10.0f), rect.Y - rect.Height);
for (int i = 0; i < waveY.Length; i++)
{
//apply velocity
waveY[i] = waveY[i] + waveVel[i];
//if the wave attempts to go "through" the top of the hull, make it bounce back
if (surface + waveY[i] > rect.Y)
{
float excess = (surface + waveY[i]) - rect.Y;
waveY[i] -= excess;
waveVel[i] = waveVel[i] * -0.5f;
}
//if the wave attempts to go "through" the bottom of the hull, make it bounce back
else if (surface + waveY[i] < rect.Y - rect.Height)
{
float excess = (surface + waveY[i]) - (rect.Y - rect.Height);
waveY[i] -= excess;
waveVel[i] = waveVel[i] * -0.5f;
}
//acceleration
float a = -WaveStiffness * waveY[i] - waveVel[i] * WaveDampening;
waveVel[i] = waveVel[i] + a;
}
//apply spread (two iterations)
for (int j = 0; j < 2; j++)
{
for (int i = 1; i < waveY.Length - 1; i++)
{
leftDelta[i] = WaveSpread * (waveY[i] - waveY[i - 1]);
waveVel[i - 1] += leftDelta[i];
rightDelta[i] = WaveSpread * (waveY[i] - waveY[i + 1]);
waveVel[i + 1] += rightDelta[i];
}
for (int i = 1; i < waveY.Length - 1; i++)
{
waveY[i - 1] += leftDelta[i];
waveY[i + 1] += rightDelta[i];
}
}
//make waves propagate through horizontal gaps
foreach (Gap gap in ConnectedGaps)
{
if (!gap.IsRoomToRoom || !gap.IsHorizontal || gap.Open <= 0.0f) continue;
if (surface > gap.Rect.Y || surface < gap.Rect.Y - gap.Rect.Height) continue;
Hull hull2 = this == gap.linkedTo[0] as Hull ? (Hull)gap.linkedTo[1] : (Hull)gap.linkedTo[0];
float otherSurfaceY = hull2.surface;
if (otherSurfaceY > gap.Rect.Y || otherSurfaceY < gap.Rect.Y - gap.Rect.Height) continue;
float surfaceDiff = (surface - otherSurfaceY) * gap.Open;
if (this != gap.linkedTo[0] as Hull)
{
//the first hull linked to the gap handles the wave propagation,
//the second just updates the surfaces to the same level
if (surfaceDiff < 32.0f)
{
hull2.waveY[hull2.waveY.Length - 1] = surfaceDiff * 0.5f;
waveY[0] = -surfaceDiff * 0.5f;
}
continue;
}
for (int j = 0; j < 2; j++)
{
int i = waveY.Length - 1;
leftDelta[i] = WaveSpread * (waveY[i] - waveY[i - 1]);
waveVel[i - 1] += leftDelta[i];
rightDelta[i] = WaveSpread * (waveY[i] - hull2.waveY[0] + surfaceDiff);
hull2.waveVel[0] += rightDelta[i];
i = 0;
hull2.leftDelta[i] = WaveSpread * (hull2.waveY[i] - waveY[waveY.Length - 1] - surfaceDiff);
waveVel[waveVel.Length - 1] += hull2.leftDelta[i];
hull2.rightDelta[i] = WaveSpread * (hull2.waveY[i] - hull2.waveY[i + 1]);
hull2.waveVel[i + 1] += hull2.rightDelta[i];
}
if (surfaceDiff < 32.0f)
{
//update surfaces to the same level
hull2.waveY[0] = surfaceDiff * 0.5f;
waveY[waveY.Length - 1] = -surfaceDiff * 0.5f;
}
else
{
hull2.waveY[0] += rightDelta[waveY.Length - 1];
waveY[waveY.Length - 1] += hull2.leftDelta[0];
}
}
if (waterVolume < Volume)
{
LethalPressure -= 10.0f * deltaTime;
if (WaterVolume <= 0.0f)
{
//wait for the surface to be lerped back to bottom and the waves to settle until disabling update
if (drawSurface > rect.Y - rect.Height + 1) return;
for (int i = 1; i < waveY.Length - 1; i++)
{
if (waveY[i] > 0.1f) return;
}
update = false;
}
}
}
partial void UpdateProjSpecific(float deltaTime, Camera cam);
public void ApplyFlowForces(float deltaTime, Item item)
{
if (item.body.Mass <= 0.0f)
{
return;
}
foreach (var gap in ConnectedGaps.Where(gap => gap.Open > 0))
{
var distance = MathHelper.Max(Vector2.DistanceSquared(item.Position, gap.Position) / 1000, 1f);
item.body.ApplyForce((gap.LerpedFlowForce / distance) * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
public void Extinguish(float deltaTime, float amount, Vector2 position)
{
for (int i = FireSources.Count - 1; i >= 0; i--)
{
FireSources[i].Extinguish(deltaTime, amount, position);
}
}
public void RemoveFire(FireSource fire)
{
FireSources.Remove(fire);
}
private HashSet<Hull> adjacentHulls = new HashSet<Hull>();
public IEnumerable<Hull> GetConnectedHulls(bool includingThis, int? searchDepth = null)
{
adjacentHulls.Clear();
int startStep = 0;
searchDepth = searchDepth ?? 100;
return GetAdjacentHulls(includingThis, adjacentHulls, ref startStep, searchDepth.Value);
}
private HashSet<Hull> GetAdjacentHulls(bool includingThis, HashSet<Hull> connectedHulls, ref int step, int searchDepth)
{
if (includingThis)
{
connectedHulls.Add(this);
}
if (step > searchDepth)
{
return connectedHulls;
}
foreach (Gap g in ConnectedGaps)
{
for (int i = 0; i < 2 && i < g.linkedTo.Count; i++)
{
if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull))
{
step++;
hull.GetAdjacentHulls(true, connectedHulls, ref step, searchDepth);
}
}
}
return connectedHulls;
}
/// <summary>
/// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls.
/// Uses a greedy algo and may not use the most optimal path. Returns float.MaxValue if no path is found.
/// </summary>
public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance)
{
return GetApproximateHullDistance(startPos, endPos, new HashSet<Hull>(), targetHull, 0.0f, maxDistance);
}
private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet<Hull> connectedHulls, Hull target, float distance, float maxDistance)
{
if (distance >= maxDistance) return float.MaxValue;
if (this == target)
{
return distance + Vector2.Distance(startPos, endPos);
}
connectedHulls.Add(this);
foreach (Gap g in ConnectedGaps)
{
if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken)
{
//gap blocked if the door is not open or the predicted state is not open
if (!g.ConnectedDoor.IsOpen || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
{
if (g.ConnectedDoor.OpenState < 0.1f) continue;
}
}
else if (g.Open <= 0.0f)
{
continue;
}
for (int i = 0; i < 2 && i < g.linkedTo.Count; i++)
{
if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull))
{
float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position), maxDistance);
if (dist < float.MaxValue) { return dist; }
}
}
}
return float.MaxValue;
}
//returns the water block which contains the point (or null if it isn't inside any)
public static Hull FindHull(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
{
if (EntityGrids == null) return null;
if (guess != null)
{
if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess;
}
foreach (EntityGrid entityGrid in EntityGrids)
{
if (entityGrid.Submarine != null && !entityGrid.Submarine.Loading)
{
System.Diagnostics.Debug.Assert(!entityGrid.Submarine.Removed);
Rectangle borders = entityGrid.Submarine.Borders;
if (useWorldCoordinates)
{
Vector2 worldPos = entityGrid.Submarine.WorldPosition;
borders.Location += new Point((int)worldPos.X, (int)worldPos.Y);
}
else
{
borders.Location += new Point((int)entityGrid.Submarine.HiddenSubPosition.X, (int)entityGrid.Submarine.HiddenSubPosition.Y);
}
const float padding = 128.0f;
if (position.X < borders.X - padding || position.X > borders.Right + padding ||
position.Y > borders.Y + padding || position.Y < borders.Y - borders.Height - padding)
{
continue;
}
}
Vector2 transformedPosition = position;
if (useWorldCoordinates && entityGrid.Submarine != null) transformedPosition -= entityGrid.Submarine.Position;
var entities = entityGrid.GetEntities(transformedPosition);
if (entities == null) continue;
foreach (Hull hull in entities)
{
if (Submarine.RectContains(hull.rect, transformedPosition, inclusive)) return hull;
}
}
return null;
}
//returns the water block which contains the point (or null if it isn't inside any)
public static Hull FindHullOld(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
{
return FindHullOld(position, hullList, guess, useWorldCoordinates, inclusive);
}
public static Hull FindHullOld(Vector2 position, List<Hull> hulls, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
{
if (guess != null && hulls.Contains(guess))
{
if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess;
}
foreach (Hull hull in hulls)
{
if (Submarine.RectContains(useWorldCoordinates ? hull.WorldRect : hull.rect, position, inclusive)) return hull;
}
return null;
}
public static void DetectItemVisibility(Character c=null)
{
if (c==null)
{
foreach (Item it in Item.ItemList)
{
it.Visible = true;
}
}
else
{
Hull h = c.CurrentHull;
hullList.ForEach(j => j.Visible = false);
List<Hull> visibleHulls;
if (h == null || c.Submarine == null)
{
visibleHulls = hullList.FindAll(j => j.CanSeeOther(null, false));
}
else
{
visibleHulls = hullList.FindAll(j => h.CanSeeOther(j, true));
}
visibleHulls.ForEach(j => j.Visible = true);
foreach (Item it in Item.ItemList)
{
if (it.CurrentHull == null || visibleHulls.Contains(it.CurrentHull)) it.Visible = true;
else it.Visible = false;
}
}
}
private bool CanSeeOther(Hull other, bool allowIndirect = true)
{
if (other == this) return true;
if (other != null && other.Submarine == Submarine)
{
bool retVal = false;
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);
retVal = otherHulls.Any(h => h == other);
if (!retVal && allowIndirect) retVal = otherHulls.Any(h => h.CanSeeOther(other, false));
if (retVal) return true;
}
}
else
{
foreach (Gap g in ConnectedGaps)
{
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);
}
return false;
}
public string CreateRoomName()
{
List<string> roomItems = new List<string>();
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != this) continue;
if (item.GetComponent<Items.Components.Reactor>() != null) roomItems.Add("reactor");
if (item.GetComponent<Items.Components.Engine>() != null) roomItems.Add("engine");
if (item.GetComponent<Items.Components.Steering>() != null) roomItems.Add("steering");
if (item.GetComponent<Items.Components.Sonar>() != null) roomItems.Add("sonar");
if (item.HasTag("ballast")) roomItems.Add("ballast");
}
if (roomItems.Contains("reactor"))
return "RoomName.ReactorRoom";
else if (roomItems.Contains("engine"))
return "RoomName.EngineRoom";
else if (roomItems.Contains("steering") && roomItems.Contains("sonar"))
return "RoomName.CommandRoom";
else if (roomItems.Contains("ballast"))
return "RoomName.Ballast";
if (ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null))
{
return "RoomName.Airlock";
}
Rectangle subRect = Submarine.CalculateDimensions();
Alignment roomPos;
if (rect.Y - rect.Height / 2 > subRect.Y + subRect.Height * 0.66f)
roomPos = Alignment.Top;
else if (rect.Y - rect.Height / 2 > subRect.Y + subRect.Height * 0.33f)
roomPos = Alignment.CenterY;
else
roomPos = Alignment.Bottom;
if (rect.Center.X < subRect.X + subRect.Width * 0.33f)
roomPos |= Alignment.Left;
else if (rect.Center.X < subRect.X + subRect.Width * 0.66f)
roomPos |= Alignment.CenterX;
else
roomPos |= Alignment.Right;
return "RoomName.Sub" + roomPos.ToString();
}
public static Hull Load(XElement element, Submarine submarine)
{
Rectangle rect = Rectangle.Empty;
if (element.Attribute("rect") != null)
{
rect = element.GetAttributeRect("rect", Rectangle.Empty);
}
else
{
//backwards compatibility
rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
int.Parse(element.Attribute("width").Value),
int.Parse(element.Attribute("height").Value));
}
var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine)
{
WaterVolume = element.GetAttributeFloat("pressure", 0.0f),
ID = (ushort)int.Parse(element.Attribute("ID").Value)
};
hull.linkedToID = new List<ushort>();
string linkedToString = element.GetAttributeString("linked", "");
if (linkedToString != "")
{
string[] linkedToIds = linkedToString.Split(',');
for (int i = 0; i < linkedToIds.Length; i++)
{
hull.linkedToID.Add((ushort)int.Parse(linkedToIds[i]));
}
}
SerializableProperty.DeserializeProperties(hull, element);
if (element.Attribute("oxygen") == null) { hull.Oxygen = hull.Volume; }
return hull;
}
public override XElement Save(XElement parentElement)
{
if (Submarine == null)
{
string errorMsg = "Error - tried to save a hull that's not a part of any submarine.\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Hull.Save:WorldHull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return null;
}
XElement element = new XElement("Hull");
element.Add
(
new XAttribute("ID", ID),
new XAttribute("rect",
(int)(rect.X - Submarine.HiddenSubPosition.X) + "," +
(int)(rect.Y - Submarine.HiddenSubPosition.Y) + "," +
rect.Width + "," + rect.Height),
new XAttribute("water", waterVolume)
);
if (linkedTo != null && linkedTo.Count > 0)
{
var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved).ToList();
element.Add(new XAttribute("linked", string.Join(",", saveableLinked.Select(l => l.ID.ToString()))));
}
SerializableProperty.SerializeProperties(this, element);
parentElement.Add(element);
return element;
}
}
}
@@ -0,0 +1,24 @@
using Microsoft.Xna.Framework;
namespace Barotrauma
{
interface IDamageable
{
Vector2 SimPosition
{
get;
}
Vector2 WorldPosition
{
get;
}
float Health
{
get;
}
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound=true);
}
}
@@ -0,0 +1,12 @@
using Microsoft.Xna.Framework;
namespace Barotrauma
{
interface ISpatialEntity
{
Vector2 Position { get; }
Vector2 WorldPosition { get; }
Vector2 SimPosition { get; }
Submarine Submarine { get; }
}
}
@@ -0,0 +1,169 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class ItemAssemblyPrefab : MapEntityPrefab
{
private string name;
public override string Name { get { return name; } }
public static readonly PrefabCollection<ItemAssemblyPrefab> Prefabs = new PrefabCollection<ItemAssemblyPrefab>();
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
{
get;
private set;
}
public Rectangle Bounds;
public ItemAssemblyPrefab(string filePath)
{
FilePath = filePath;
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null) { return; }
originalName = doc.Root.GetAttributeString("name", "");
identifier = doc.Root.GetAttributeString("identifier", null) ?? originalName.ToLowerInvariant().Replace(" ", "");
configElement = doc.Root;
Category = MapEntityCategory.ItemAssembly;
SerializableProperty.DeserializeProperties(this, configElement);
name = TextManager.Get("EntityName." + identifier, returnNull: true) ?? originalName;
Description = TextManager.Get("EntityDescription." + identifier, returnNull: true) ?? Description;
int minX = int.MaxValue, minY = int.MaxValue;
int maxX = int.MinValue, maxY = int.MinValue;
DisplayEntities = new List<Pair<MapEntityPrefab, Rectangle>>();
foreach (XElement entityElement in doc.Root.Elements())
{
string identifier = entityElement.GetAttributeString("identifier", "");
MapEntityPrefab mapEntity = List.FirstOrDefault(p => p.Identifier == identifier);
if (mapEntity == null)
{
string entityName = entityElement.GetAttributeString("name", "");
mapEntity = List.FirstOrDefault(p => p.Name == entityName);
}
Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty);
if (mapEntity != null && !entityElement.GetAttributeBool("hideinassemblypreview", false))
{
DisplayEntities.Add(new Pair<MapEntityPrefab, Rectangle>(mapEntity, 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);
}
}
Bounds = new Rectangle(minX, minY, maxX - minX, maxY - minY);
Prefabs.Add(this, false);
}
public static void Remove(string filePath)
{
Prefabs.RemoveByFile(filePath);
}
protected override void CreateInstance(Rectangle rect)
{
CreateInstance(rect.Location.ToVector2(), Submarine.MainSub);
}
public List<MapEntity> CreateInstance(Vector2 position, Submarine sub)
{
List<MapEntity> entities = MapEntity.LoadAll(sub, configElement, FilePath);
if (entities.Count == 0) return entities;
Vector2 offset = sub == null ? Vector2.Zero : sub.HiddenSubPosition;
foreach (MapEntity me in entities)
{
me.Move(position);
Item item = me as Item;
if (item == null) continue;
Wire wire = item.GetComponent<Wire>();
if (wire != null) wire.MoveNodes(position - offset);
}
MapEntity.MapLoaded(entities, true);
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen)
{
MapEntity.SelectedList.Clear();
entities.ForEach(e => MapEntity.AddSelection(e));
}
#endif
return entities;
}
public void Delete()
{
Dispose();
if (File.Exists(FilePath))
{
try
{
File.Delete(FilePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Deleting item assembly \"" + name + "\" failed.", e);
}
}
}
public static void LoadAll()
{
if (GameSettings.VerboseLogging)
{
DebugConsole.Log("Loading item assembly prefabs: ");
}
List<string> itemAssemblyFiles = new List<string>();
//find assembly files in the item assembly folder
string directoryPath = Path.Combine("Content", "Items", "Assemblies");
if (Directory.Exists(directoryPath))
{
itemAssemblyFiles.AddRange(Directory.GetFiles(directoryPath));
}
//find assembly files in selected content packages
foreach (ContentPackage cp in GameMain.Config.SelectedContentPackages)
{
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);
}
}
}
}
@@ -0,0 +1,436 @@
using FarseerPhysics;
using FarseerPhysics.Common;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Voronoi2;
namespace Barotrauma
{
static partial class CaveGenerator
{
public static List<VoronoiCell> GraphEdgesToCells(List<GraphEdge> graphEdges, Rectangle borders, float gridCellSize, out List<VoronoiCell>[,] cellGrid)
{
List<VoronoiCell> cells = new List<VoronoiCell>();
cellGrid = new List<VoronoiCell>[(int)Math.Ceiling(borders.Width / gridCellSize), (int)Math.Ceiling(borders.Height / gridCellSize)];
for (int x = 0; x < borders.Width / gridCellSize; x++)
{
for (int y = 0; y < borders.Height / gridCellSize; y++)
{
cellGrid[x, y] = new List<VoronoiCell>();
}
}
foreach (GraphEdge ge in graphEdges)
{
if (Vector2.DistanceSquared(ge.Point1, ge.Point2) < 0.001f) continue;
for (int i = 0; i < 2; i++)
{
Site site = (i == 0) ? ge.Site1 : ge.Site2;
int x = (int)(Math.Floor((site.Coord.X-borders.X) / gridCellSize));
int y = (int)(Math.Floor((site.Coord.Y-borders.Y) / gridCellSize));
x = MathHelper.Clamp(x, 0, cellGrid.GetLength(0)-1);
y = MathHelper.Clamp(y, 0, cellGrid.GetLength(1)-1);
VoronoiCell cell = cellGrid[x,y].Find(c => c.Site == site);
if (cell == null)
{
cell = new VoronoiCell(site);
cellGrid[x, y].Add(cell);
cells.Add(cell);
}
if (ge.Cell1 == null)
{
ge.Cell1 = cell;
}
else
{
ge.Cell2 = cell;
}
cell.Edges.Add(ge);
}
}
return cells;
}
private static Vector2 GetEdgeNormal(GraphEdge edge, VoronoiCell cell = null)
{
if (cell == null) cell = edge.AdjacentCell(null);
if (cell == null) return Vector2.UnitX;
CompareCCW compare = new CompareCCW(cell.Center);
if (compare.Compare(edge.Point1, edge.Point2) == -1)
{
var temp = edge.Point1;
edge.Point1 = edge.Point2;
edge.Point2 = temp;
}
Vector2 normal = Vector2.Zero;
normal = Vector2.Normalize(edge.Point2 - edge.Point1);
Vector2 diffToCell = Vector2.Normalize(cell.Center - edge.Point2);
normal = new Vector2(-normal.Y, normal.X);
if (Vector2.Dot(normal, diffToCell) < 0)
{
normal = -normal;
}
return normal;
}
public static List<VoronoiCell> GeneratePath(
List<Point> pathNodes, List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid,
int gridCellSize, Rectangle limits, float wanderAmount = 0.3f, bool mirror = false)
{
var targetCells = new List<VoronoiCell>();
for (int i = 0; i < pathNodes.Count; i++)
{
//a search depth of 2 is large enough to find a cell in almost all maps, but in case it fails, we increase the depth
int searchDepth = 2;
while (searchDepth < 5)
{
int cellIndex = FindCellIndex(pathNodes[i], cells, cellGrid, gridCellSize, searchDepth);
if (cellIndex > -1)
{
targetCells.Add(cells[cellIndex]);
break;
}
searchDepth++;
}
}
return GeneratePath(targetCells, cells, cellGrid, gridCellSize, limits, wanderAmount, mirror);
}
public static List<VoronoiCell> GeneratePath(
List<VoronoiCell> targetCells, List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid,
int gridCellSize, Rectangle limits, float wanderAmount = 0.3f, bool mirror = false)
{
Stopwatch sw2 = new Stopwatch();
sw2.Start();
//how heavily the path "steers" towards the endpoint
//lower values will cause the path to "wander" more, higher will make it head straight to the end
wanderAmount = MathHelper.Clamp(wanderAmount, 0.0f, 1.0f);
List<GraphEdge> allowedEdges = new List<GraphEdge>();
List<VoronoiCell> pathCells = new List<VoronoiCell>();
VoronoiCell currentCell = targetCells[0];
currentCell.CellType = CellType.Path;
pathCells.Add(currentCell);
int currentTargetIndex = 1;
int iterationsLeft = cells.Count;
do
{
int edgeIndex = 0;
allowedEdges.Clear();
foreach (GraphEdge edge in currentCell.Edges)
{
var adjacentCell = edge.AdjacentCell(currentCell);
if (limits.Contains(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y))
{
allowedEdges.Add(edge);
}
}
//steer towards target
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > wanderAmount || allowedEdges.Count == 0)
{
double smallestDist = double.PositiveInfinity;
for (int i = 0; i < currentCell.Edges.Count; i++)
{
var adjacentCell = currentCell.Edges[i].AdjacentCell(currentCell);
double dist = MathUtils.Distance(
adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y,
targetCells[currentTargetIndex].Site.Coord.X, targetCells[currentTargetIndex].Site.Coord.Y);
if (dist < smallestDist)
{
edgeIndex = i;
smallestDist = dist;
}
}
}
//choose random edge (ignoring ones where the adjacent cell is outside limits)
else
{
edgeIndex = Rand.Int(allowedEdges.Count, Rand.RandSync.Server);
if (mirror && edgeIndex > 0) edgeIndex = allowedEdges.Count - edgeIndex;
edgeIndex = currentCell.Edges.IndexOf(allowedEdges[edgeIndex]);
}
currentCell = currentCell.Edges[edgeIndex].AdjacentCell(currentCell);
currentCell.CellType = CellType.Path;
pathCells.Add(currentCell);
iterationsLeft--;
if (currentCell == targetCells[currentTargetIndex])
{
currentTargetIndex += 1;
if (currentTargetIndex >= targetCells.Count) break;
}
} while (currentCell != targetCells[targetCells.Count - 1] && iterationsLeft > 0);
Debug.WriteLine("gettooclose: " + sw2.ElapsedMilliseconds + " ms");
sw2.Restart();
return pathCells;
}
/// <summary>
/// Makes the cell rounder by subdividing the edges and offsetting them at the middle
/// </summary>
/// <param name="minEdgeLength">How small the individual subdivided edges can be (smaller values produce rounder shapes, but require more geometry)</param>
public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, float roundingAmount = 0.5f, float irregularity = 0.1f)
{
List<GraphEdge> tempEdges = new List<GraphEdge>();
foreach (GraphEdge edge in cell.Edges)
{
if (!edge.IsSolid)
{
tempEdges.Add(edge);
continue;
}
List<Vector2> edgePoints = new List<Vector2>();
Vector2 edgeNormal = GetEdgeNormal(edge, cell);
float edgeLength = Vector2.Distance(edge.Point1, edge.Point2);
int pointCount = (int)Math.Max(Math.Ceiling(edgeLength / minEdgeLength), 1);
Vector2 edgeDir = (edge.Point2 - edge.Point1);
for (int i = 0; i <= pointCount; i++)
{
if (i == 0)
{
edgePoints.Add(edge.Point1);
}
else if (i == pointCount)
{
edgePoints.Add(edge.Point2);
}
else
{
float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount));
float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.Server);
edgePoints.Add(
edge.Point1 +
edgeDir * (i / (float)pointCount) -
edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF);
}
}
for (int i = 0; i < pointCount; i++)
{
tempEdges.Add(new GraphEdge(edgePoints[i], edgePoints[i + 1])
{
Cell1 = edge.Cell1,
Cell2 = edge.Cell2,
IsSolid = edge.IsSolid,
Site1 = edge.Site1,
Site2 = edge.Site2,
OutsideLevel = edge.OutsideLevel
});
}
}
cell.Edges = tempEdges;
}
public static Body GeneratePolygons(List<VoronoiCell> cells, Level level, out List<Vector2[]> renderTriangles)
{
renderTriangles = new List<Vector2[]>();
List<Vector2> tempVertices = new List<Vector2>();
List<Vector2> bodyPoints = new List<Vector2>();
Body cellBody = new Body()
{
SleepingAllowed = false,
BodyType = BodyType.Static,
CollisionCategories = Physics.CollisionLevel
};
GameMain.World.Add(cellBody);
for (int n = cells.Count - 1; n >= 0; n-- )
{
VoronoiCell cell = cells[n];
bodyPoints.Clear();
tempVertices.Clear();
foreach (GraphEdge ge in cell.Edges)
{
if (Vector2.DistanceSquared(ge.Point1, ge.Point2) < 0.01f) continue;
if (!tempVertices.Any(v => Vector2.DistanceSquared(ge.Point1, v) < 1.0f))
{
tempVertices.Add(ge.Point1);
bodyPoints.Add(ge.Point1);
}
if (!tempVertices.Any(v => Vector2.DistanceSquared(ge.Point2, v) < 1.0f))
{
tempVertices.Add(ge.Point2);
bodyPoints.Add(ge.Point2);
}
}
if (tempVertices.Count < 3 || bodyPoints.Count < 2)
{
cells.RemoveAt(n);
continue;
}
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, cell.Center));
if (bodyPoints.Count < 2) continue;
if (bodyPoints.Count < 3)
{
foreach (Vector2 vertex in tempVertices)
{
if (bodyPoints.Contains(vertex)) continue;
bodyPoints.Add(vertex);
break;
}
}
for (int i = 0; i < bodyPoints.Count; i++)
{
cell.BodyVertices.Add(bodyPoints[i]);
bodyPoints[i] = ConvertUnits.ToSimUnits(bodyPoints[i]);
}
if (cell.CellType == CellType.Empty) continue;
cellBody.UserData = cell;
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(cell.Center));
for (int i = 0; i < triangles.Count; i++)
{
//don't create a triangle if the area of the triangle is too small
//(apparently Farseer doesn't like polygons with a very small area, see Shape.ComputeProperties)
Vector2 a = triangles[i][0];
Vector2 b = triangles[i][1];
Vector2 c = triangles[i][2];
float area = Math.Abs(a.X * (b.Y - c.Y) + b.X * (c.Y - a.Y) + c.X * (a.Y - b.Y)) / 2.0f;
if (area < 1.0f) continue;
Vertices bodyVertices = new Vertices(triangles[i]);
var newFixture = cellBody.CreatePolygon(bodyVertices, 5.0f);
newFixture.UserData = cell;
if (newFixture.Shape.MassData.Area < FarseerPhysics.Settings.Epsilon)
{
DebugConsole.ThrowError("Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + ")");
GameAnalyticsManager.AddErrorEventOnce(
"CaveGenerator.GeneratePolygons:InvalidTriangle",
GameAnalyticsSDK.Net.EGAErrorSeverity.Warning,
"Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + "). Seed: " + level.Seed);
}
}
cell.Body = cellBody;
}
return cellBody;
}
public static List<Vector2> CreateRandomChunk(float radius, int vertexCount, float radiusVariance)
{
Debug.Assert(radiusVariance < radius);
Debug.Assert(vertexCount >= 3);
List<Vector2> verts = new List<Vector2>();
float angleStep = MathHelper.TwoPi / vertexCount;
float angle = 0.0f;
for (int i = 0; i < vertexCount; i++)
{
verts.Add(new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) *
(radius + Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server)));
angle += angleStep;
}
return verts;
}
/// <summary>
/// find the index of the cell which the point is inside
/// (actually finds the cell whose center is closest, but it's always the correct cell assuming the point is inside the borders of the diagram)
/// </summary>
public static int FindCellIndex(Vector2 position,List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid, int gridCellSize, int searchDepth = 1, Vector2? offset = null)
{
float closestDist = float.PositiveInfinity;
VoronoiCell closestCell = null;
Vector2 gridOffset = offset == null ? Vector2.Zero : (Vector2)offset;
position -= gridOffset;
int gridPosX = (int)Math.Floor(position.X / gridCellSize);
int gridPosY = (int)Math.Floor(position.Y / gridCellSize);
for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++)
{
for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++)
{
for (int i = 0; i < cellGrid[x, y].Count; i++)
{
float dist = Vector2.DistanceSquared(cellGrid[x, y][i].Center, position);
if (dist > closestDist) continue;
closestDist = dist;
closestCell = cellGrid[x, y][i];
}
}
}
return cells.IndexOf(closestCell);
}
public static int FindCellIndex(Point position, List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid, int gridCellSize, int searchDepth = 1)
{
int closestDist = int.MaxValue;
VoronoiCell closestCell = null;
int gridPosX = position.X / gridCellSize;
int gridPosY = position.Y / gridCellSize;
for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++)
{
for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++)
{
for (int i = 0; i < cellGrid[x, y].Count; i++)
{
int dist = MathUtils.DistanceSquared(
(int)cellGrid[x, y][i].Site.Coord.X, (int)cellGrid[x, y][i].Site.Coord.Y,
position.X, position.Y);
if (dist > closestDist) continue;
closestDist = dist;
closestCell = cellGrid[x, y][i];
}
}
}
return cells.IndexOf(closestCell);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,537 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class Biome
{
public readonly string Identifier;
public readonly string DisplayName;
public readonly string Description;
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", "");
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);
string allowedZonesStr = element.GetAttributeString("AllowedZones", "1,2,3,4,5,6,7,8,9");
string[] zoneIndices = allowedZonesStr.Split(',');
for (int i = 0; i < zoneIndices.Length; i++)
{
int zoneIndex = -1;
if (!int.TryParse(zoneIndices[i].Trim(), out zoneIndex))
{
DebugConsole.ThrowError("Error in biome config \"" + Identifier + "\" - \"" + zoneIndices[i] + "\" is not a valid zone index.");
continue;
}
AllowedZones.Add(zoneIndex);
}
}
}
class LevelGenerationParams : ISerializableEntity
{
public static List<LevelGenerationParams> LevelParams
{
get { return levelParams; }
}
private static List<LevelGenerationParams> levelParams;
private static List<Biome> biomes;
public string Name
{
get;
private set;
}
private int minWidth, maxWidth, height;
private Point voronoiSiteInterval;
//how much the sites are "scattered" on x- and y-axis
//if Vector2.Zero, the sites will just be placed in a regular grid pattern
private Point voronoiSiteVariance;
//how far apart the nodes of the main path can be
//x = min interval, y = max interval
private Point mainPathNodeIntervalRange;
private int smallTunnelCount;
//x = min length, y = max length
private Point smallTunnelLengthRange;
//how large portion of the bottom of the level should be "carved out"
//if 0.0f, the bottom will be completely solid (making the abyss unreachable)
//if 1.0f, the bottom will be completely open
private float bottomHoleProbability;
//the y-position of the ocean floor (= the position from which the bottom formations extend upwards)
private int seaFloorBaseDepth;
//how much random variance there can be in the height of the formations
private int seaFloorVariance;
private int cellSubdivisionLength;
private float cellRoundingAmount;
private float cellIrregularity;
private int mountainCountMin, mountainCountMax;
private int mountainHeightMin, mountainHeightMax;
private int ruinCount;
private float waterParticleScale;
//which biomes can this type of level appear in
private List<Biome> allowedBiomes = new List<Biome>();
public IEnumerable<Biome> AllowedBiomes
{
get { return allowedBiomes; }
}
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
set;
}
[Serialize("27,30,36", true), Editable]
public Color AmbientLightColor
{
get;
set;
}
[Serialize("20,40,50", true), Editable()]
public Color BackgroundTextureColor
{
get;
set;
}
[Serialize("20,40,50", true), Editable]
public Color BackgroundColor
{
get;
set;
}
[Serialize("255,255,255", true), Editable]
public Color WallColor
{
get;
set;
}
[Serialize(1000, true, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)]
public int LevelObjectAmount
{
get;
set;
}
[Serialize(100000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
public int MinWidth
{
get { return minWidth; }
set { minWidth = Math.Max(value, 2000); }
}
[Serialize(100000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
public int MaxWidth
{
get { return maxWidth; }
set { maxWidth = Math.Max(value, 2000); }
}
[Serialize(50000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
public int Height
{
get { return height; }
set { height = Math.Max(value, 2000); }
}
[Editable, Serialize("3000, 3000", true, 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
{
get { return voronoiSiteInterval; }
set
{
voronoiSiteInterval.X = MathHelper.Clamp(value.X, 100, MinWidth / 2);
voronoiSiteInterval.Y = MathHelper.Clamp(value.Y, 100, height / 2);
}
}
[Editable, Serialize("700,700", true, 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
{
get { return voronoiSiteVariance; }
set
{
voronoiSiteVariance = new Point(
MathHelper.Clamp(value.X, 0, voronoiSiteInterval.X),
MathHelper.Clamp(value.Y, 0, voronoiSiteInterval.Y));
}
}
[Editable(MinValueInt = 100, MaxValueInt = 10000), Serialize(1000, true, 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
{
get { return cellSubdivisionLength; }
set
{
cellSubdivisionLength = Math.Max(value, 10);
}
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, true, 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
{
get { return cellRoundingAmount; }
set
{
cellRoundingAmount = MathHelper.Clamp(value, 0.0f, 1.0f);
}
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, true, 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
{
get { return cellIrregularity; }
set
{
cellIrregularity = MathHelper.Clamp(value, 0.0f, 1.0f);
}
}
[Editable, 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.")]
public Point MainPathNodeIntervalRange
{
get { return mainPathNodeIntervalRange; }
set
{
mainPathNodeIntervalRange.X = MathHelper.Clamp(value.X, 100, MinWidth / 2);
mainPathNodeIntervalRange.Y = MathHelper.Clamp(value.Y, mainPathNodeIntervalRange.X, MinWidth / 2);
}
}
[Editable, Serialize(5, true, description: "The number of small tunnels placed along the main path.")]
public int SmallTunnelCount
{
get { return smallTunnelCount; }
set { smallTunnelCount = MathHelper.Clamp(value, 0, 100); }
}
[Editable, Serialize("5000, 10000", true, description: "The minimum and maximum length of small tunnels placed along the main path.")]
public Point SmallTunnelLengthRange
{
get { return smallTunnelLengthRange; }
set
{
smallTunnelLengthRange.X = MathHelper.Clamp(value.X, 100, MinWidth);
smallTunnelLengthRange.Y = MathHelper.Clamp(value.Y, smallTunnelLengthRange.X, MinWidth);
}
}
[Serialize(100, true), Editable(MinValueInt = 0, MaxValueInt = 10000)]
public int ItemCount
{
get;
set;
}
[Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int FloatingIceChunkCount
{
get;
set;
}
[Serialize(300000, true, 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)]
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)]
public int MountainCountMin
{
get { return mountainCountMin; }
set
{
mountainCountMin = Math.Max(value, 0);
}
}
[Serialize(0, true, description: "The maximum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int MountainCountMax
{
get { return mountainCountMax; }
set
{
mountainCountMax = Math.Max(value, 0);
}
}
[Serialize(1000, true, description: "The minimum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
public int MountainHeightMin
{
get { return mountainHeightMin; }
set
{
mountainHeightMin = Math.Max(value, 0);
}
}
[Serialize(5000, true, description: "The maximum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
public int MountainHeightMax
{
get { return mountainHeightMax; }
set
{
mountainHeightMax = Math.Max(value, 0);
}
}
[Serialize(1, true, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 50)]
public int RuinCount
{
get { return ruinCount; }
set { ruinCount = MathHelper.Clamp(value, 0, 10); }
}
[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()]
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]
public float WaterParticleScale
{
get { return waterParticleScale; }
private set { waterParticleScale = Math.Max(value, 0.01f); }
}
public Sprite BackgroundSprite { get; private set; }
public Sprite BackgroundTopSprite { get; private set; }
public Sprite WallSprite { get; private set; }
public Sprite WallSpriteSpecular { get; private set; }
public Sprite WallEdgeSprite { get; private set; }
public Sprite WallEdgeSpriteSpecular { get; private set; }
public Sprite WaterParticles { get; private set; }
public static List<Biome> GetBiomes()
{
return biomes;
}
public static LevelGenerationParams GetRandom(string seed, Biome biome = null)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
if (levelParams == null || !levelParams.Any())
{
DebugConsole.ThrowError("Level generation presets not found - using default presets");
return new LevelGenerationParams(null);
}
if (biome == null)
{
return levelParams.GetRandom(lp => lp.allowedBiomes.Count > 0, Rand.RandSync.Server);
}
var matchingLevelParams = levelParams.FindAll(lp => lp.allowedBiomes.Contains(biome));
if (matchingLevelParams.Count == 0)
{
DebugConsole.ThrowError("Level generation presets not found for the biome \"" + biome.Identifier + "\"!");
return new LevelGenerationParams(null);
}
return matchingLevelParams[Rand.Range(0, matchingLevelParams.Count, Rand.RandSync.Server)];
}
private LevelGenerationParams(XElement element)
{
Name = element == null ? "default" : element.Name.ToString();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
string biomeStr = element.GetAttributeString("biomes", "");
if (string.IsNullOrWhiteSpace(biomeStr))
{
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; }
Biome matchingBiome = biomes.Find(b => b.Identifier.Equals(biomeName, StringComparison.OrdinalIgnoreCase));
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 \"" + Name + "\".", Color.Orange);
}
}
allowedBiomes.Add(matchingBiome);
}
}
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "background":
BackgroundSprite = new Sprite(subElement);
break;
case "backgroundtop":
BackgroundTopSprite = new Sprite(subElement);
break;
case "wall":
WallSprite = new Sprite(subElement);
break;
case "wallspecular":
WallSpriteSpecular = new Sprite(subElement);
break;
case "walledge":
WallEdgeSprite = new Sprite(subElement);
break;
case "walledgespecular":
WallEdgeSpriteSpecular = new Sprite(subElement);
break;
case "waterparticles":
WaterParticles = new Sprite(subElement);
break;
}
}
}
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>();
List<XElement> levelParamElements = new List<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();
levelParamElements.Clear();
DebugConsole.NewMessage($"Overriding the level generation parameters and biomes with '{file.Path}'", Color.Yellow);
}
else if (biomeElements.Any() || levelParamElements.Any())
{
DebugConsole.ThrowError($"Error in '{file.Path}': Another level generation parameter file already loaded! Use <override></override> tags to override it.");
break;
}
foreach (XElement element in mainElement.Elements())
{
if (element.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
{
levelParamElements.Clear();
DebugConsole.NewMessage($"Overriding the level generation parameters with '{file.Path}'", Color.Yellow);
levelParamElements.AddRange(element.Elements());
}
}
else if (element.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase))
{
biomeElements.AddRange(element.Elements());
}
else
{
levelParamElements.Add(element);
}
}
}
foreach (XElement biomeElement in biomeElements)
{
biomes.Add(new Biome(biomeElement));
}
foreach (XElement levelParamElement in levelParamElements)
{
levelParams.Add(new LevelGenerationParams(levelParamElement));
}
}
}
}
@@ -0,0 +1,133 @@
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class LevelObject
{
public readonly LevelObjectPrefab Prefab;
public Vector3 Position;
public float NetworkUpdateTimer;
public float Scale;
public float Rotation;
private int spriteIndex;
public LevelObjectPrefab ActivePrefab;
public PhysicsBody PhysicsBody
{
get;
private set;
}
public List<LevelTrigger> Triggers
{
get;
private set;
}
public bool NeedsNetworkSyncing
{
get { return Triggers.Any(t => t.NeedsNetworkSyncing); }
set { Triggers.ForEach(t => t.NeedsNetworkSyncing = false); }
}
public Sprite Sprite
{
get { return spriteIndex < 0 || Prefab.Sprites.Count == 0 ? null : Prefab.Sprites[spriteIndex % Prefab.Sprites.Count]; }
}
public Sprite SpecularSprite
{
get { return spriteIndex < 0 || Prefab.SpecularSprites.Count == 0 ? null : Prefab.SpecularSprites[spriteIndex % Prefab.SpecularSprites.Count]; }
}
public LevelObject(LevelObjectPrefab prefab, Vector3 position, float scale, float rotation = 0.0f)
{
Triggers = new List<LevelTrigger>();
ActivePrefab = Prefab = prefab;
Position = position;
Scale = scale;
Rotation = rotation;
spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.Server) : -1;
if (prefab.PhysicsBodyElement != null)
{
PhysicsBody = new PhysicsBody(prefab.PhysicsBodyElement, ConvertUnits.ToSimUnits(new Vector2(position.X, position.Y)), Scale);
}
foreach (XElement triggerElement in prefab.LevelTriggerElements)
{
Vector2 triggerPosition = triggerElement.GetAttributeVector2("position", Vector2.Zero) * scale;
if (rotation != 0.0f)
{
var ca = (float)Math.Cos(rotation);
var sa = (float)Math.Sin(rotation);
triggerPosition = new Vector2(
ca * triggerPosition.X + sa * triggerPosition.Y,
-sa * triggerPosition.X + ca * triggerPosition.Y);
}
var newTrigger = new LevelTrigger(triggerElement, new Vector2(position.X, position.Y) + triggerPosition, -rotation, scale, prefab.Name);
int parentTriggerIndex = prefab.LevelTriggerElements.IndexOf(triggerElement.Parent);
if (parentTriggerIndex > -1) newTrigger.ParentTrigger = Triggers[parentTriggerIndex];
Triggers.Add(newTrigger);
}
InitProjSpecific();
}
partial void InitProjSpecific();
public Vector2 LocalToWorld(Vector2 localPosition, float swingState = 0.0f)
{
Vector2 emitterPos = localPosition * Scale;
if (Rotation != 0.0f || Prefab.SwingAmountRad != 0.0f)
{
float rot = Rotation + swingState * Prefab.SwingAmountRad;
var ca = (float)Math.Cos(rot);
var sa = (float)Math.Sin(rot);
emitterPos = new Vector2(
ca * emitterPos.X + sa * emitterPos.Y,
-sa * emitterPos.X + ca * emitterPos.Y);
}
return new Vector2(Position.X, Position.Y) + emitterPos;
}
public void Remove()
{
RemoveProjSpecific();
}
partial void RemoveProjSpecific();
public override string ToString()
{
return "LevelObject (" + ActivePrefab.Name + ")";
}
public void ServerWrite(IWriteMessage msg, Client c)
{
for (int j = 0; j < Triggers.Count; j++)
{
if (!Triggers[j].UseNetworkSyncing) continue;
Triggers[j].ServerWrite(msg, c);
}
}
}
}
@@ -0,0 +1,416 @@
#if CLIENT
using Barotrauma.Particles;
#endif
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Voronoi2;
namespace Barotrauma
{
partial class LevelObjectManager : Entity, IServerSerializable
{
const int GridSize = 2000;
private List<LevelObject> objects;
private List<LevelObject>[,] objectGrid;
public LevelObjectManager() : base(null)
{
}
class SpawnPosition
{
public readonly GraphEdge GraphEdge;
public readonly Vector2 Normal;
public readonly LevelObjectPrefab.SpawnPosType SpawnPosType;
public readonly Alignment Alignment;
public readonly float Length;
public SpawnPosition(GraphEdge graphEdge, Vector2 normal, LevelObjectPrefab.SpawnPosType spawnPosType, Alignment alignment)
{
GraphEdge = graphEdge;
Normal = normal;
SpawnPosType = spawnPosType;
Alignment = alignment;
Length = Vector2.Distance(graphEdge.Point1, graphEdge.Point2);
}
public float GetSpawnProbability(LevelObjectPrefab prefab)
{
if (prefab.ClusteringAmount <= 0.0f) return Length;
float noise = (float)(
PerlinNoise.CalculatePerlin(GraphEdge.Point1.X / 10000.0f, GraphEdge.Point1.Y / 10000.0f, prefab.ClusteringGroup) +
PerlinNoise.CalculatePerlin(GraphEdge.Point1.X / 20000.0f, GraphEdge.Point1.Y / 20000.0f, prefab.ClusteringGroup));
return Length * (float)Math.Pow(noise, prefab.ClusteringAmount);
}
}
public void PlaceObjects(Level level, int amount)
{
objectGrid = new List<LevelObject>[
level.Size.X / GridSize,
(level.Size.Y - level.BottomPos) / GridSize];
List<SpawnPosition> availableSpawnPositions = new List<SpawnPosition>();
var levelCells = level.GetAllCells();
availableSpawnPositions.AddRange(GetAvailableSpawnPositions(levelCells, LevelObjectPrefab.SpawnPosType.Wall));
availableSpawnPositions.AddRange(GetAvailableSpawnPositions(level.SeaFloor.Cells, LevelObjectPrefab.SpawnPosType.SeaFloor));
foreach (RuinGeneration.Ruin ruin in level.Ruins)
{
foreach (var ruinShape in ruin.RuinShapes)
{
foreach (var wall in ruinShape.Walls)
{
availableSpawnPositions.Add(new SpawnPosition(
new GraphEdge(wall.A, wall.B),
(wall.A + wall.B) / 2.0f - ruinShape.Center,
LevelObjectPrefab.SpawnPosType.RuinWall,
ruinShape.GetLineAlignment(wall)));
}
}
}
foreach (var posOfInterest in level.PositionsOfInterest)
{
if (posOfInterest.PositionType != Level.PositionType.MainPath) continue;
availableSpawnPositions.Add(new SpawnPosition(
new GraphEdge(posOfInterest.Position.ToVector2(), posOfInterest.Position.ToVector2() + Vector2.UnitX),
Vector2.UnitY,
LevelObjectPrefab.SpawnPosType.MainPath,
Alignment.Top));
}
objects = new List<LevelObject>();
for (int i = 0; i < amount; i++)
{
//get a random prefab and find a place to spawn it
LevelObjectPrefab prefab = GetRandomPrefab(level.GenerationParams.Name);
SpawnPosition spawnPosition = FindObjectPosition(availableSpawnPositions, level, prefab);
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) continue;
float rotation = 0.0f;
if (prefab.AlignWithSurface && spawnPosition != null)
{
rotation = MathUtils.VectorToAngle(new Vector2(spawnPosition.Normal.Y, spawnPosition.Normal.X));
}
rotation += Rand.Range(prefab.RandomRotationRad.X, prefab.RandomRotationRad.Y, Rand.RandSync.Server);
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));
}
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);
}
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);
AddObject(newObject, level);
foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects)
{
int childCount = Rand.Range(child.MinCount, child.MaxCount, Rand.RandSync.Server);
for (int j = 0; j < childCount; j++)
{
var matchingPrefabs = LevelObjectPrefab.List.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));
if (childPrefab == null) continue;
Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server) * 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));
AddObject(childObject, level);
}
}
}
}
private void AddObject(LevelObject newObject, Level level)
{
foreach (LevelTrigger trigger in newObject.Triggers)
{
trigger.OnTriggered += (levelTrigger, obj) =>
{
OnObjectTriggered(newObject, levelTrigger, obj);
};
}
var spriteCorners = new List<Vector2>
{
Vector2.Zero, Vector2.Zero, Vector2.Zero, Vector2.Zero
};
Sprite sprite = newObject.Sprite ?? newObject.Prefab.DeformableSprite?.Sprite;
//calculate the positions of the corners of the rotated sprite
if (sprite != null)
{
Vector2 halfSize = sprite.size * newObject.Scale / 2;
spriteCorners[0] = -halfSize;
spriteCorners[1] = new Vector2(-halfSize.X, halfSize.Y);
spriteCorners[2] = halfSize;
spriteCorners[3] = new Vector2(halfSize.X, -halfSize.Y);
Vector2 pivotOffset = sprite.Origin * newObject.Scale - halfSize;
pivotOffset.X = -pivotOffset.X;
pivotOffset = new Vector2(
(float)(pivotOffset.X * Math.Cos(-newObject.Rotation) - pivotOffset.Y * Math.Sin(-newObject.Rotation)),
(float)(pivotOffset.X * Math.Sin(-newObject.Rotation) + pivotOffset.Y * Math.Cos(-newObject.Rotation)));
for (int j = 0; j < 4; j++)
{
spriteCorners[j] = new Vector2(
(float)(spriteCorners[j].X * Math.Cos(-newObject.Rotation) - spriteCorners[j].Y * Math.Sin(-newObject.Rotation)),
(float)(spriteCorners[j].X * Math.Sin(-newObject.Rotation) + spriteCorners[j].Y * Math.Cos(-newObject.Rotation)));
spriteCorners[j] += new Vector2(newObject.Position.X, newObject.Position.Y) + pivotOffset;
}
}
float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z;
float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z;
float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z - level.BottomPos;
float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z - level.BottomPos;
foreach (LevelTrigger trigger in newObject.Triggers)
{
if (trigger.PhysicsBody == null) continue;
for (int i = 0; i < trigger.PhysicsBody.FarseerBody.FixtureList.Count; i++)
{
trigger.PhysicsBody.FarseerBody.GetTransform(out FarseerPhysics.Common.Transform transform);
trigger.PhysicsBody.FarseerBody.FixtureList[i].Shape.ComputeAABB(out FarseerPhysics.Collision.AABB aabb, ref transform, i);
minX = Math.Min(minX, ConvertUnits.ToDisplayUnits(aabb.LowerBound.X));
maxX = Math.Max(maxX, ConvertUnits.ToDisplayUnits(aabb.UpperBound.X));
minY = Math.Min(minY, ConvertUnits.ToDisplayUnits(aabb.LowerBound.Y) - level.BottomPos);
maxY = Math.Max(maxY, ConvertUnits.ToDisplayUnits(aabb.UpperBound.Y) - level.BottomPos);
}
}
#if CLIENT
if (newObject.ParticleEmitters != null)
{
foreach (ParticleEmitter emitter in newObject.ParticleEmitters)
{
Rectangle particleBounds = emitter.CalculateParticleBounds(new Vector2(newObject.Position.X, newObject.Position.Y));
minX = Math.Min(minX, particleBounds.X);
maxX = Math.Max(maxX, particleBounds.Right);
minY = Math.Min(minY, particleBounds.Y - level.BottomPos);
maxY = Math.Max(maxY, particleBounds.Bottom - level.BottomPos);
}
}
#endif
objects.Add(newObject);
newObject.Position.Z += (minX + minY) % 100.0f * 0.00001f;
int xStart = (int)Math.Floor(minX / GridSize);
int xEnd = (int)Math.Floor(maxX / GridSize);
if (xEnd < 0 || xStart >= objectGrid.GetLength(0)) return;
int yStart = (int)Math.Floor(minY / GridSize);
int yEnd = (int)Math.Floor(maxY / GridSize);
if (yEnd < 0 || yStart >= objectGrid.GetLength(1)) return;
xStart = Math.Max(xStart, 0);
xEnd = Math.Min(xEnd, objectGrid.GetLength(0) - 1);
yStart = Math.Max(yStart, 0);
yEnd = Math.Min(yEnd, objectGrid.GetLength(1) - 1);
for (int x = xStart; x <= xEnd; x++)
{
for (int y = yStart; y <= yEnd; y++)
{
if (objectGrid[x, y] == null) objectGrid[x, y] = new List<LevelObject>();
objectGrid[x, y].Add(newObject);
}
}
}
public Microsoft.Xna.Framework.Point GetGridIndices(Vector2 worldPosition)
{
return new Microsoft.Xna.Framework.Point(
(int)Math.Floor(worldPosition.X / GridSize),
(int)Math.Floor((worldPosition.Y - Level.Loaded.BottomPos) / GridSize));
}
public IEnumerable<LevelObject> GetAllObjects()
{
return objects;
}
private readonly static List<LevelObject> objectsInRange = new List<LevelObject>();
public IEnumerable<LevelObject> GetAllObjects(Vector2 worldPosition, float radius)
{
var minIndices = GetGridIndices(worldPosition - Vector2.One * radius);
if (minIndices.X >= objectGrid.GetLength(0) || minIndices.Y >= objectGrid.GetLength(1)) return Enumerable.Empty<LevelObject>();
var maxIndices = GetGridIndices(worldPosition + Vector2.One * radius);
if (maxIndices.X < 0 || maxIndices.Y < 0) return Enumerable.Empty<LevelObject>();
minIndices.X = Math.Max(0, minIndices.X);
minIndices.Y = Math.Max(0, minIndices.Y);
maxIndices.X = Math.Min(objectGrid.GetLength(0) - 1, maxIndices.X);
maxIndices.Y = Math.Min(objectGrid.GetLength(1) - 1, maxIndices.Y);
objectsInRange.Clear();
for (int x = minIndices.X; x <= maxIndices.X; x++)
{
for (int y = minIndices.Y; y <= maxIndices.Y; y++)
{
if (objectGrid[x, y] == null) continue;
foreach (LevelObject obj in objectGrid[x, y])
{
if (!objectsInRange.Contains(obj)) objectsInRange.Add(obj);
}
}
}
return objectsInRange;
}
private List<SpawnPosition> GetAvailableSpawnPositions(IEnumerable<VoronoiCell> cells, LevelObjectPrefab.SpawnPosType spawnPosType)
{
List<SpawnPosition> availableSpawnPositions = new List<SpawnPosition>();
foreach (var cell in cells)
{
foreach (var edge in cell.Edges)
{
if (!edge.IsSolid || edge.OutsideLevel) continue;
Vector2 normal = edge.GetNormal(cell);
Alignment edgeAlignment = 0;
if (normal.Y < -0.5f)
edgeAlignment |= Alignment.Bottom;
else if (normal.Y > 0.5f)
edgeAlignment |= Alignment.Top;
else if (normal.X < -0.5f)
edgeAlignment |= Alignment.Left;
else if(normal.X > 0.5f)
edgeAlignment |= Alignment.Right;
availableSpawnPositions.Add(new SpawnPosition(edge, normal, spawnPosType, edgeAlignment));
}
}
return availableSpawnPositions;
}
private SpawnPosition FindObjectPosition(List<SpawnPosition> availableSpawnPositions, Level level, LevelObjectPrefab prefab)
{
if (prefab.SpawnPos == LevelObjectPrefab.SpawnPosType.None) return null;
var suitableSpawnPositions = availableSpawnPositions.Where(sp =>
prefab.SpawnPos.HasFlag(sp.SpawnPosType) && sp.Length >= prefab.MinSurfaceWidth && prefab.Alignment.HasFlag(sp.Alignment)).ToList();
return ToolBox.SelectWeightedRandom(suitableSpawnPositions, suitableSpawnPositions.Select(sp => sp.GetSpawnProbability(prefab)).ToList(), Rand.RandSync.Server);
}
public void Update(float deltaTime)
{
foreach (LevelObject obj in objects)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
obj.NetworkUpdateTimer -= deltaTime;
if (obj.NeedsNetworkSyncing && obj.NetworkUpdateTimer <= 0.0f)
{
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { obj });
obj.NeedsNetworkSyncing = false;
obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval;
}
}
obj.ActivePrefab = obj.Prefab;
for (int i = 0; i < obj.Triggers.Count; i++)
{
obj.Triggers[i].Update(deltaTime);
if (obj.Triggers[i].IsTriggered && obj.Prefab.OverrideProperties[i] != null)
{
obj.ActivePrefab = obj.Prefab.OverrideProperties[i];
}
}
if (obj.PhysicsBody != null)
{
if (obj.Prefab.PhysicsBodyTriggerIndex > -1) obj.PhysicsBody.Enabled = obj.Triggers[obj.Prefab.PhysicsBodyTriggerIndex].IsTriggered;
obj.Position = new Vector3(obj.PhysicsBody.Position, obj.Position.Z);
obj.Rotation = obj.PhysicsBody.Rotation;
}
}
UpdateProjSpecific(deltaTime);
}
partial void UpdateProjSpecific(float deltaTime);
private void OnObjectTriggered(LevelObject triggeredObject, LevelTrigger trigger, Entity triggerer)
{
if (trigger.TriggerOthersDistance <= 0.0f) return;
foreach (LevelObject obj in objects)
{
if (obj == triggeredObject) continue;
foreach (LevelTrigger otherTrigger in obj.Triggers)
{
otherTrigger.OtherTriggered(triggeredObject, trigger);
}
}
}
private LevelObjectPrefab GetRandomPrefab(string levelType)
{
return ToolBox.SelectWeightedRandom(
LevelObjectPrefab.List,
LevelObjectPrefab.List.Select(p => p.GetCommonness(levelType)).ToList(), Rand.RandSync.Server);
}
public override void Remove()
{
if (objects != null)
{
foreach (LevelObject obj in objects)
{
obj.Remove();
}
objects.Clear();
}
RemoveProjSpecific();
base.Remove();
}
partial void RemoveProjSpecific();
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
LevelObject obj = extraData[0] as LevelObject;
msg.WriteRangedInteger(objects.IndexOf(obj), 0, objects.Count);
obj.ServerWrite(msg, c);
}
}
}
@@ -0,0 +1,397 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class LevelObjectPrefab : ISerializableEntity
{
private static List<LevelObjectPrefab> list = new List<LevelObjectPrefab>();
public static List<LevelObjectPrefab> List
{
get { return list; }
}
public class ChildObject
{
public List<string> AllowedNames;
public int MinCount, MaxCount;
public ChildObject()
{
AllowedNames = new List<string>();
MinCount = 1;
MaxCount = 1;
}
public ChildObject(XElement element)
{
AllowedNames = element.GetAttributeStringArray("names", new string[0]).ToList();
MinCount = element.GetAttributeInt("mincount", 1);
MaxCount = Math.Max(element.GetAttributeInt("maxcount", 1), MinCount);
}
}
[Flags]
public enum SpawnPosType
{
None = 0,
Wall = 1,
RuinWall = 2,
SeaFloor = 4,
MainPath = 8
}
public List<Sprite> Sprites
{
get;
private set;
} = new List<Sprite>();
public List<Sprite> SpecularSprites
{
get;
private set;
} = new List<Sprite>();
public DeformableSprite DeformableSprite
{
get;
private set;
}
[Serialize(1.0f, false), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
public float MinSize
{
get;
private set;
}
[Serialize(1.0f, false), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
public float MaxSize
{
get;
private set;
}
/// <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]
public Alignment Alignment
{
get;
private set;
}
[Serialize(SpawnPosType.Wall, false), Editable()]
public SpawnPosType SpawnPos
{
get;
private set;
}
public XElement Config
{
get;
private set;
}
public readonly List<XElement> 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 XElement PhysicsBodyElement
{
get;
private set;
}
public int PhysicsBodyTriggerIndex
{
get;
private set;
}
[Serialize("0.0,1.0", true), Editable]
public Vector2 DepthRange
{
get;
private set;
}
[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.")]
/// <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.
/// </summary>
public float ClusteringAmount
{
get;
private set;
}
[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 " +
" 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>
/// 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>
public float ClusteringGroup
{
get;
private set;
}
[Editable, Serialize(false, true, description: "Should the object be rotated to align it with the wall surface it spawns on.")]
public bool AlignWithSurface
{
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)]
/// <summary>
/// Minimum length of a graph edge the object can spawn on.
/// </summary>
public float MinSurfaceWidth
{
get;
private set;
}
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).")]
public Vector2 RandomRotation
{
get { return new Vector2(MathHelper.ToDegrees(randomRotation.X), MathHelper.ToDegrees(randomRotation.Y)); }
private set
{
randomRotation = new Vector2(MathHelper.ToRadians(value.X), MathHelper.ToRadians(value.Y));
}
}
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)]
public float SwingAmount
{
get { return MathHelper.ToDegrees(swingAmount); }
private set
{
swingAmount = MathHelper.ToRadians(value);
}
}
public float SwingAmountRad => swingAmount;
[Serialize(0.0f, true, 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%.")]
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)]
public float ScaleOscillationFrequency
{
get;
private set;
}
[Editable, Serialize(1.0f, true, 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.")]
public float Commonness
{
get;
private set;
}
[Serialize(0.0f, true, description: "How much the object disrupts submarine's sonar."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float SonarDisruption
{
get;
private set;
}
public string Name
{
get;
set;
}
public List<ChildObject> ChildObjects
{
get;
private set;
}
public Dictionary<string, SerializableProperty> SerializableProperties
{
get; private set;
}
/// <summary>
/// A list of prefabs whose properties override this one's properties when a trigger is active.
/// E.g. if a trigger in the index 1 of the trigger list is active, the properties in index 1 in this list are used (unless it's null)
/// </summary>
public List<LevelObjectPrefab> OverrideProperties
{
get;
private set;
}
public override string ToString()
{
return "LevelObjectPrefab (" + Name + ")";
}
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.NewMessage($"Loading additional level object prefabs from file '{configPath}'");
}
foreach (XElement element in mainElement.Elements())
{
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)
{
ChildObjects = new List<ChildObject>();
LevelTriggerElements = new List<XElement>();
OverrideProperties = new List<LevelObjectPrefab>();
OverrideCommonness = new Dictionary<string, float>();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (element != null)
{
Config = element;
Name = element.Name.ToString();
LoadElements(element, -1);
InitProjSpecific(element);
}
//use the maximum width of the sprite as the minimum surface width if no value is given
if (element != null && !element.Attributes("minsurfacewidth").Any())
{
if (Sprites.Any()) MinSurfaceWidth = Sprites[0].size.X * MaxSize;
if (DeformableSprite != null) MinSurfaceWidth = Math.Max(MinSurfaceWidth, DeformableSprite.Size.X * MaxSize);
}
}
private void LoadElements(XElement element, int parentTriggerIndex)
{
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
Sprites.Add( new Sprite(subElement, lazyLoad: true));
break;
case "specularsprite":
SpecularSprites.Add(new Sprite(subElement, lazyLoad: true));
break;
case "deformablesprite":
DeformableSprite = new DeformableSprite(subElement, lazyLoad: true);
break;
case "overridecommonness":
string levelType = subElement.GetAttributeString("leveltype", "");
if (!OverrideCommonness.ContainsKey(levelType))
{
OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f));
}
break;
case "leveltrigger":
case "trigger":
OverrideProperties.Add(null);
LevelTriggerElements.Add(subElement);
LoadElements(subElement, LevelTriggerElements.Count - 1);
break;
case "childobject":
ChildObjects.Add(new ChildObject(subElement));
break;
case "overrideproperties":
var propertyOverride = new LevelObjectPrefab(subElement);
OverrideProperties[OverrideProperties.Count - 1] = propertyOverride;
if (!propertyOverride.Sprites.Any() && propertyOverride.DeformableSprite == null)
{
propertyOverride.Sprites = Sprites;
propertyOverride.DeformableSprite = DeformableSprite;
}
break;
case "body":
case "physicsbody":
PhysicsBodyElement = subElement;
PhysicsBodyTriggerIndex = parentTriggerIndex;
break;
}
}
}
partial void InitProjSpecific(XElement element);
public float GetCommonness(string levelType)
{
if (!OverrideCommonness.TryGetValue(levelType, out float commonness))
{
return Commonness;
}
return commonness;
}
}
}
@@ -0,0 +1,612 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class LevelTrigger
{
[Flags]
enum TriggererType
{
None = 0,
Human = 1,
Creature = 2,
Character = Human | Creature,
Submarine = 4,
Item = 8,
OtherTrigger = 16
}
public enum TriggerForceMode
{
Force, //default, apply a force to the object over time
Acceleration, //apply an acceleration to the object, ignoring it's mass
Impulse, //apply an instant force, ignoring deltaTime
LimitVelocity //clamp the velocity of the triggerer to some value
}
public Action<LevelTrigger, Entity> OnTriggered;
private PhysicsBody physicsBody;
/// <summary>
/// Effects applied to entities that are inside the trigger
/// </summary>
private List<StatusEffect> statusEffects = new List<StatusEffect>();
/// <summary>
/// Attacks applied to entities that are inside the trigger
/// </summary>
private List<Attack> attacks = new List<Attack>();
private float cameraShake;
private Vector2 unrotatedForce;
private float forceFluctuationTimer, currentForceFluctuation = 1.0f;
private HashSet<Entity> triggerers = new HashSet<Entity>();
private TriggererType triggeredBy;
private float randomTriggerInterval;
private float randomTriggerProbability;
private float randomTriggerTimer;
private float triggeredTimer;
//how far away this trigger can activate other triggers from
private float triggerOthersDistance;
private HashSet<string> tags = new HashSet<string>();
//other triggers have to have at least one of these tags to trigger this one
private HashSet<string> allowedOtherTriggerTags = new HashSet<string>();
/// <summary>
/// How long the trigger stays in the triggered state after triggerers have left
/// </summary>
private float stayTriggeredDelay;
public LevelTrigger ParentTrigger;
public Dictionary<Entity, Vector2> TriggererPosition
{
get;
private set;
}
private Vector2 worldPosition;
public Vector2 WorldPosition
{
get { return worldPosition; }
set
{
worldPosition = value;
physicsBody?.SetTransform(ConvertUnits.ToSimUnits(value), physicsBody.Rotation);
}
}
public float Rotation
{
get { return physicsBody == null ? 0.0f : physicsBody.Rotation; }
set
{
if (physicsBody == null) return;
physicsBody.SetTransform(physicsBody.Position, value);
CalculateDirectionalForce();
}
}
public PhysicsBody PhysicsBody
{
get { return physicsBody; }
}
public float TriggerOthersDistance
{
get { return triggerOthersDistance; }
}
public IEnumerable<Entity> Triggerers
{
get { return triggerers.AsEnumerable(); }
}
public bool IsTriggered
{
get
{
return (triggerers.Count > 0 || triggeredTimer > 0.0f) &&
(ParentTrigger == null || ParentTrigger.IsTriggered);
}
}
public Vector2 Force
{
get;
private set;
}
/// <summary>
/// does the force diminish by distance
/// </summary>
public bool ForceFalloff
{
get;
private set;
}
public float ForceFluctuationInterval
{
get;
private set;
}
public float ForceFluctuationStrength
{
get;
private set;
}
private TriggerForceMode forceMode;
public TriggerForceMode ForceMode
{
get { return forceMode; }
}
/// <summary>
/// Stop applying forces to objects if they're moving faster than this
/// </summary>
public float ForceVelocityLimit
{
get;
private set;
}
public float ColliderRadius
{
get;
private set;
}
public bool UseNetworkSyncing
{
get;
private set;
}
public bool NeedsNetworkSyncing
{
get;
set;
}
public LevelTrigger(XElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "")
{
TriggererPosition = new Dictionary<Entity, Vector2>();
worldPosition = position;
if (element.Attributes("radius").Any() || element.Attributes("width").Any() || element.Attributes("height").Any())
{
physicsBody = new PhysicsBody(element, scale)
{
CollisionCategories = Physics.CollisionLevel,
CollidesWith = Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionProjectile | Physics.CollisionWall
};
physicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision;
physicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation;
physicsBody.FarseerBody.SetIsSensor(true);
physicsBody.FarseerBody.BodyType = BodyType.Static;
physicsBody.FarseerBody.BodyType = BodyType.Kinematic;
ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f));
physicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation);
}
cameraShake = element.GetAttributeFloat("camerashake", 0.0f);
stayTriggeredDelay = element.GetAttributeFloat("staytriggereddelay", 0.0f);
randomTriggerInterval = element.GetAttributeFloat("randomtriggerinterval", 0.0f);
randomTriggerProbability = element.GetAttributeFloat("randomtriggerprobability", 0.0f);
UseNetworkSyncing = element.GetAttributeBool("networksyncing", false);
unrotatedForce =
element.Attribute("force") != null && element.Attribute("force").Value.Contains(',') ?
element.GetAttributeVector2("force", Vector2.Zero) :
new Vector2(element.GetAttributeFloat("force", 0.0f), 0.0f);
ForceFluctuationInterval = element.GetAttributeFloat("forcefluctuationinterval", 0.01f);
ForceFluctuationStrength = Math.Max(element.GetAttributeFloat("forcefluctuationstrength", 0.0f), 0.0f);
ForceFalloff = element.GetAttributeBool("forcefalloff", true);
ForceVelocityLimit = ConvertUnits.ToSimUnits(element.GetAttributeFloat("forcevelocitylimit", float.MaxValue));
string forceModeStr = element.GetAttributeString("forcemode", "Force");
if (!Enum.TryParse(forceModeStr, out forceMode))
{
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + forceModeStr + "\" is not a valid force mode.");
}
CalculateDirectionalForce();
string triggeredByStr = element.GetAttributeString("triggeredby", "Character");
if (!Enum.TryParse(triggeredByStr, out triggeredBy))
{
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type.");
}
UpdateCollisionCategories();
triggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f);
var tagsArray = element.GetAttributeStringArray("tags", new string[0]);
foreach (string tag in tagsArray)
{
tags.Add(tag.ToLower());
}
if (triggeredBy.HasFlag(TriggererType.OtherTrigger))
{
var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", new string[0]);
foreach (string tag in otherTagsArray)
{
allowedOtherTriggerTags.Add(tag.ToLower());
}
}
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
statusEffects.Add(StatusEffect.Load(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in "+ parentDebugName));
break;
case "attack":
case "damage":
var attack = new Attack(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in " + parentDebugName);
var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step);
attack.Afflictions.Clear();
foreach (Affliction affliction in multipliedAfflictions)
{
attack.Afflictions.Add(affliction, null);
}
attacks.Add(attack);
break;
}
}
forceFluctuationTimer = Rand.Range(0.0f, ForceFluctuationInterval);
randomTriggerTimer = Rand.Range(0.0f, randomTriggerInterval);
}
private void UpdateCollisionCategories()
{
if (physicsBody == null) return;
var collidesWith = Physics.CollisionNone;
if (triggeredBy.HasFlag(TriggererType.Character) || triggeredBy.HasFlag(TriggererType.Creature)) collidesWith |= Physics.CollisionCharacter;
if (triggeredBy.HasFlag(TriggererType.Item)) collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile;
if (triggeredBy.HasFlag(TriggererType.Submarine)) collidesWith |= Physics.CollisionWall;
physicsBody.CollidesWith = collidesWith;
}
private void CalculateDirectionalForce()
{
var ca = (float)Math.Cos(-Rotation);
var sa = (float)Math.Sin(-Rotation);
Force = new Vector2(
ca * unrotatedForce.X + sa * unrotatedForce.Y,
-sa * unrotatedForce.X + ca * unrotatedForce.Y);
}
private bool PhysicsBody_OnCollision(Fixture fixtureA, Fixture fixtureB, FarseerPhysics.Dynamics.Contacts.Contact contact)
{
Entity entity = GetEntity(fixtureB);
if (entity == null) return false;
if (entity is Character character)
{
if (character.CurrentHull != null) return false;
if (character.IsHuman)
{
if (!triggeredBy.HasFlag(TriggererType.Human)) return false;
}
else
{
if (!triggeredBy.HasFlag(TriggererType.Creature)) return false;
}
}
else if (entity is Item item)
{
if (item.CurrentHull != null) return false;
if (!triggeredBy.HasFlag(TriggererType.Item)) return false;
}
else if (entity is Submarine)
{
if (!triggeredBy.HasFlag(TriggererType.Submarine)) return false;
}
if (!triggerers.Contains(entity))
{
if (!IsTriggered)
{
OnTriggered?.Invoke(this, entity);
}
TriggererPosition[entity] = entity.WorldPosition;
triggerers.Add(entity);
}
return true;
}
private void PhysicsBody_OnSeparation(Fixture fixtureA, Fixture fixtureB, Contact contact)
{
Entity entity = GetEntity(fixtureB);
if (entity == null) return;
if (entity is Character character &&
(!character.Enabled || character.Removed) &&
triggerers.Contains(entity))
{
TriggererPosition.Remove(entity);
triggerers.Remove(entity);
return;
}
//check if there are any other contacts with the entity
//(the OnSeparation callback happens when two fixtures separate,
//e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body)
ContactEdge contactEdge = fixtureA.Body.ContactList;
while (contactEdge != null)
{
if (contactEdge.Contact != null &&
contactEdge.Contact.IsTouching)
{
var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ?
contactEdge.Contact.FixtureB :
contactEdge.Contact.FixtureA);
if (otherEntity == entity) return;
}
contactEdge = contactEdge.Next;
}
if (triggerers.Contains(entity))
{
TriggererPosition.Remove(entity);
triggerers.Remove(entity);
}
}
private Entity GetEntity(Fixture fixture)
{
if (fixture.Body == null || fixture.Body.UserData == null) return null;
if (fixture.Body.UserData is Entity entity) return entity;
if (fixture.Body.UserData is Limb limb) return limb.character;
if (fixture.Body.UserData is SubmarineBody subBody) return subBody.Submarine;
return null;
}
/// <summary>
/// Another trigger was triggered, check if this one should react to it
/// </summary>
public void OtherTriggered(LevelObject levelObject, LevelTrigger otherTrigger)
{
if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) return;
//check if the other trigger has appropriate tags
if (allowedOtherTriggerTags.Count > 0)
{
if (!allowedOtherTriggerTags.Any(t => otherTrigger.tags.Contains(t))) return;
}
if (Vector2.DistanceSquared(WorldPosition, otherTrigger.WorldPosition) <= otherTrigger.triggerOthersDistance * otherTrigger.triggerOthersDistance)
{
bool wasAlreadyTriggered = IsTriggered;
triggeredTimer = stayTriggeredDelay;
if (!wasAlreadyTriggered)
{
OnTriggered?.Invoke(this, null);
}
}
}
public void Update(float deltaTime)
{
if (ParentTrigger != null && !ParentTrigger.IsTriggered) return;
triggerers.RemoveWhere(t => t.Removed);
bool isNotClient = true;
#if CLIENT
isNotClient = GameMain.Client == null;
#endif
if (!UseNetworkSyncing || isNotClient)
{
if (ForceFluctuationStrength > 0.0f)
{
//no need for force fluctuation (or network updates) if the trigger limits velocity and there are no triggerers
if (forceMode != TriggerForceMode.LimitVelocity || triggerers.Any())
{
forceFluctuationTimer += deltaTime;
if (forceFluctuationTimer > ForceFluctuationInterval)
{
NeedsNetworkSyncing = true;
currentForceFluctuation = Rand.Range(1.0f - ForceFluctuationStrength, 1.0f);
forceFluctuationTimer = 0.0f;
}
}
}
if (randomTriggerProbability > 0.0f)
{
randomTriggerTimer += deltaTime;
if (randomTriggerTimer > randomTriggerInterval)
{
if (Rand.Range(0.0f, 1.0f) < randomTriggerProbability)
{
NeedsNetworkSyncing = true;
triggeredTimer = stayTriggeredDelay;
}
randomTriggerTimer = 0.0f;
}
}
}
if (stayTriggeredDelay > 0.0f)
{
if (triggerers.Count == 0)
{
triggeredTimer -= deltaTime;
}
else
{
triggeredTimer = stayTriggeredDelay;
}
}
foreach (Entity triggerer in triggerers)
{
foreach (StatusEffect effect in statusEffects)
{
if (triggerer is Character)
{
effect.Apply(effect.type, deltaTime, triggerer, (Character)triggerer);
}
else if (triggerer is Item)
{
effect.Apply(effect.type, deltaTime, triggerer, ((Item)triggerer).AllPropertyObjects);
}
}
if (triggerer is IDamageable damageable)
{
foreach (Attack attack in attacks)
{
attack.DoDamage(null, damageable, WorldPosition, deltaTime, false);
}
}
else if (triggerer is Submarine submarine)
{
foreach (Attack attack in attacks)
{
float structureDamage = attack.GetStructureDamage(deltaTime);
if (structureDamage > 0.0f)
{
Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage);
}
}
}
if (Force.LengthSquared() > 0.01f)
{
if (triggerer is Character character)
{
ApplyForce(character.AnimController.Collider, deltaTime);
foreach (Limb limb in character.AnimController.Limbs)
{
ApplyForce(limb.body, deltaTime);
}
}
else if (triggerer is Submarine submarine)
{
ApplyForce(submarine.SubBody.Body, deltaTime);
}
}
if (triggerer == Character.Controlled || triggerer == Character.Controlled?.Submarine)
{
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, cameraShake);
}
}
}
private void ApplyForce(PhysicsBody body, float deltaTime)
{
float distFactor = 1.0f;
if (ForceFalloff)
{
distFactor = 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(body.SimPosition, PhysicsBody.SimPosition)) / ColliderRadius;
if (distFactor < 0.0f) return;
}
switch (ForceMode)
{
case TriggerForceMode.Force:
if (ForceVelocityLimit < 1000.0f)
body.ApplyForce(Force * currentForceFluctuation * distFactor, ForceVelocityLimit);
else
body.ApplyForce(Force * currentForceFluctuation * distFactor, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
break;
case TriggerForceMode.Acceleration:
if (ForceVelocityLimit < 1000.0f)
body.ApplyForce(Force * body.Mass * currentForceFluctuation * distFactor, ForceVelocityLimit);
else
body.ApplyForce(Force * body.Mass * currentForceFluctuation * distFactor, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
break;
case TriggerForceMode.Impulse:
if (ForceVelocityLimit < 1000.0f)
body.ApplyLinearImpulse(Force * currentForceFluctuation * distFactor, maxVelocity: ForceVelocityLimit);
else
body.ApplyLinearImpulse(Force * currentForceFluctuation * distFactor, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
break;
case TriggerForceMode.LimitVelocity:
float maxVel = ForceVelocityLimit * currentForceFluctuation * distFactor;
if (body.LinearVelocity.LengthSquared() > maxVel * maxVel)
{
body.ApplyForce(
Vector2.Normalize(-body.LinearVelocity) *
Force.Length() * body.Mass * currentForceFluctuation * distFactor,
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
break;
}
}
public Vector2 GetWaterFlowVelocity(Vector2 viewPosition)
{
Vector2 baseVel = GetWaterFlowVelocity();
if (baseVel.LengthSquared() < 0.1f) return Vector2.Zero;
float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f));
float dist = Vector2.Distance(viewPosition, WorldPosition);
if (dist > triggerSize) return Vector2.Zero;
return baseVel * (1.0f - dist / triggerSize);
}
public Vector2 GetWaterFlowVelocity()
{
if (Force == Vector2.Zero) return Vector2.Zero;
Vector2 vel = Force;
if (ForceMode == TriggerForceMode.Acceleration)
{
vel *= 1000.0f;
}
else if (ForceMode == TriggerForceMode.Impulse)
{
vel /= (float)Timing.Step;
}
return vel.ClampLength(ConvertUnits.ToDisplayUnits(ForceVelocityLimit)) * currentForceFluctuation;
}
public void ServerWrite(IWriteMessage msg, Client c)
{
if (ForceFluctuationStrength > 0.0f)
{
msg.WriteRangedSingle(MathHelper.Clamp(currentForceFluctuation, 0.0f, 1.0f), 0.0f, 1.0f, 8);
}
if (stayTriggeredDelay > 0.0f)
{
msg.WriteRangedSingle(MathHelper.Clamp(triggeredTimer, 0.0f, stayTriggeredDelay), 0.0f, stayTriggeredDelay, 16);
}
}
}
}
@@ -0,0 +1,153 @@
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Voronoi2;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
#endif
namespace Barotrauma
{
partial class LevelWall : IDisposable
{
private List<VoronoiCell> cells;
public List<VoronoiCell> Cells
{
get { return cells; }
}
private Body body;
public Body Body
{
get { return body; }
}
private float moveState;
private float moveLength;
private Vector2 moveAmount;
public Vector2 MoveAmount
{
get { return moveAmount; }
set
{
moveAmount = value;
moveLength = moveAmount.Length();
}
}
public float MoveSpeed;
private Vector2? originalPos;
public float MoveState
{
get { return moveState; }
set { moveState = MathHelper.Clamp(value, 0.0f, MathHelper.TwoPi); }
}
public LevelWall(List<Vector2> vertices, Color color, Level level, bool giftWrap = false)
{
if (giftWrap)
{
vertices = MathUtils.GiftWrap(vertices);
}
VoronoiCell wallCell = new VoronoiCell(vertices.ToArray());
for (int i = 0; i < wallCell.Edges.Count; i++)
{
wallCell.Edges[i].Cell1 = wallCell;
wallCell.Edges[i].IsSolid = true;
}
cells = new List<VoronoiCell>() { wallCell };
body = CaveGenerator.GeneratePolygons(cells, level, out List<Vector2[]> triangles);
#if CLIENT
List<VertexPositionTexture> bodyVertices = CaveGenerator.GenerateRenderVerticeList(triangles);
SetBodyVertices(bodyVertices.ToArray(), color);
SetWallVertices(CaveGenerator.GenerateWallShapes(cells, level), color);
#endif
}
public LevelWall(List<Vector2> edgePositions, Vector2 extendAmount, Color color, Level level)
{
cells = new List<VoronoiCell>();
for (int i = 0; i < edgePositions.Count - 1; i++)
{
Vector2[] vertices = new Vector2[4];
vertices[0] = edgePositions[i];
vertices[1] = edgePositions[i + 1];
vertices[2] = vertices[0] + extendAmount;
vertices[3] = vertices[1] + extendAmount;
VoronoiCell wallCell = new VoronoiCell(vertices);
wallCell.CellType = CellType.Edge;
wallCell.Edges[0].Cell1 = wallCell;
wallCell.Edges[1].Cell1 = wallCell;
wallCell.Edges[2].Cell1 = wallCell;
wallCell.Edges[3].Cell1 = wallCell;
wallCell.Edges[0].IsSolid = true;
if (i > 1)
{
wallCell.Edges[3].Cell2 = cells[i - 1];
cells[i - 1].Edges[1].Cell2 = wallCell;
}
cells.Add(wallCell);
}
body = CaveGenerator.GeneratePolygons(cells, level, out List<Vector2[]> triangles);
body.CollisionCategories = Physics.CollisionLevel;
#if CLIENT
List<VertexPositionTexture> bodyVertices = CaveGenerator.GenerateRenderVerticeList(triangles);
SetBodyVertices(bodyVertices.ToArray(), color);
SetWallVertices(CaveGenerator.GenerateWallShapes(cells, level), color);
#endif
}
public void Update(float deltaTime)
{
if (body.BodyType == BodyType.Static) return;
Vector2 bodyPos = ConvertUnits.ToDisplayUnits(body.Position);
Cells.ForEach(c => c.Translation = bodyPos);
if (!originalPos.HasValue) originalPos = bodyPos;
if (moveLength > 0.0f && MoveSpeed > 0.0f)
{
moveState += MoveSpeed / moveLength * deltaTime;
moveState %= MathHelper.TwoPi;
Vector2 targetPos = ConvertUnits.ToSimUnits(originalPos.Value + moveAmount * (float)Math.Sin(moveState));
body.ApplyForce((targetPos - body.Position).ClampLength(1.0f) * body.Mass);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
#if CLIENT
if (wallVertices != null)
{
wallVertices.Dispose();
wallVertices = null;
}
if (bodyVertices != null)
{
BodyVertices.Dispose();
bodyVertices = null;
}
#endif
}
}
}
@@ -0,0 +1,190 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.RuinGeneration
{
/// <summary>
/// nodes of a binary tree used for generating underwater "dungeons"
/// </summary>
class BTRoom : RuinShape
{
private BTRoom[] subRooms;
public BTRoom Parent
{
get;
private set;
}
public Corridor Corridor
{
get;
set;
}
public BTRoom[] SubRooms
{
get { return subRooms; }
}
public BTRoom Adjacent
{
get;
private set;
}
public BTRoom(Rectangle rect)
{
this.rect = rect;
}
public void Split(float minDivRatio, float verticalProbability = 0.5f, int minWidth = 200, int minHeight = 200)
{
bool verticalSplit = Rand.Range(0.0f, rect.Height / (float)rect.Width, Rand.RandSync.Server) < verticalProbability;
if (rect.Width * minDivRatio < minWidth && rect.Height * minDivRatio < minHeight)
{
minDivRatio = 0.5f;
}
else if (rect.Width * minDivRatio < minWidth)
{
verticalSplit = false;
}
else if (rect.Height * minDivRatio < minHeight)
{
verticalSplit = true;
}
subRooms = new BTRoom[2];
if (verticalSplit)
{
SplitVertical(minDivRatio);
}
else
{
SplitHorizontal(minDivRatio);
}
subRooms[0].Parent = this;
subRooms[1].Parent = this;
subRooms[0].Adjacent = subRooms[1];
subRooms[1].Adjacent = subRooms[0];
}
private void SplitHorizontal(float minDivRatio)
{
float div = Rand.Range(minDivRatio, 1.0f - minDivRatio, Rand.RandSync.Server);
subRooms[0] = new BTRoom(new Rectangle(rect.X, rect.Y, rect.Width, (int)(rect.Height * div)));
subRooms[1] = new BTRoom(new Rectangle(rect.X, rect.Y + subRooms[0].rect.Height, rect.Width, rect.Height - subRooms[0].rect.Height));
}
private void SplitVertical(float minDivRatio)
{
float div = Rand.Range(minDivRatio, 1.0f - minDivRatio, Rand.RandSync.Server);
subRooms[0] = new BTRoom(new Rectangle(rect.X, rect.Y, (int)(rect.Width * div), rect.Height));
subRooms[1] = new BTRoom(new Rectangle(rect.X + subRooms[0].rect.Width, rect.Y, rect.Width - subRooms[0].rect.Width, rect.Height));
}
public override void CreateWalls()
{
Walls = new List<Line>
{
new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.Right, Rect.Y)),
new Line(new Vector2(Rect.X, Rect.Bottom), new Vector2(Rect.Right, Rect.Bottom)),
new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.X, Rect.Bottom)),
new Line(new Vector2(Rect.Right, Rect.Y), new Vector2(Rect.Right, Rect.Bottom))
};
}
public void Scale(Vector2 scale)
{
rect.Inflate((scale.X - 1.0f) * 0.5f * rect.Width, (scale.Y - 1.0f) * 0.5f * rect.Height);
}
public List<BTRoom> GetLeaves()
{
return GetLeaves(new List<BTRoom>());
}
private List<BTRoom> GetLeaves(List<BTRoom> leaves)
{
if (subRooms == null)
{
leaves.Add(this);
}
else
{
subRooms[0].GetLeaves(leaves);
subRooms[1].GetLeaves(leaves);
}
return leaves;
}
public void GenerateCorridors(int minWidth, int maxWidth, List<Corridor> corridors)
{
if (Adjacent != null && Corridor == null)
{
Corridor = new Corridor(this, Rand.Range(minWidth, maxWidth, Rand.RandSync.Server), corridors);
}
if (subRooms != null)
{
subRooms[0].GenerateCorridors(minWidth, maxWidth, corridors);
subRooms[1].GenerateCorridors(minWidth, maxWidth, corridors);
}
}
public static void CalculateDistancesFromEntrance(BTRoom entrance, List<BTRoom> rooms, List<Corridor> corridors)
{
entrance.CalculateDistanceFromEntrance(0, rooms, new List<Corridor>(corridors));
}
private void CalculateDistanceFromEntrance(int currentDist, List<BTRoom> rooms, List<Corridor> corridors)
{
DistanceFromEntrance = DistanceFromEntrance == 0 ? currentDist : Math.Min(currentDist, DistanceFromEntrance);
currentDist++;
var roomRect = Rect;
roomRect.Inflate(5, 5);
foreach (var corridor in corridors)
{
var corridorRect = corridor.Rect;
corridorRect.Inflate(5, 5);
if (!corridorRect.Intersects(roomRect)) continue;
corridor.DistanceFromEntrance = corridor.DistanceFromEntrance == 0 ?
DistanceFromEntrance + 1 :
Math.Min(corridor.DistanceFromEntrance, DistanceFromEntrance + 1);
List<BTRoom> connectedRooms = new List<BTRoom>();
foreach (var otherRoom in rooms)
{
if (otherRoom == this) continue;
if (otherRoom.DistanceFromEntrance > 0 && otherRoom.DistanceFromEntrance < currentDist) continue;
var otherRoomRect = otherRoom.Rect;
otherRoomRect.Inflate(5, 5);
if (corridorRect.Intersects(otherRoomRect)) { connectedRooms.Add(otherRoom); }
}
connectedRooms.Sort((r1, r2) =>
{
return
(Math.Abs(r1.Rect.Center.X - Rect.Center.X) + Math.Abs(r1.Rect.Center.Y - Rect.Center.Y)) -
(Math.Abs(r2.Rect.Center.X - Rect.Center.X) + Math.Abs(r2.Rect.Center.Y - Rect.Center.Y));
});
for (int i = 0; i < connectedRooms.Count; i++)
{
connectedRooms[i].CalculateDistanceFromEntrance(currentDist + 1 + i, rooms, corridors);
}
}
}
}
}
@@ -0,0 +1,210 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma.RuinGeneration
{
class Corridor : RuinShape
{
private readonly bool isHorizontal;
public bool IsHorizontal
{
get { return isHorizontal; }
}
public BTRoom[] ConnectedRooms
{
get;
private set;
}
public Corridor(Rectangle rect)
{
this.rect = rect;
isHorizontal = rect.Width > rect.Height;
}
public Corridor(BTRoom room, int width, List<Corridor> corridors)
{
System.Diagnostics.Debug.Assert(room.Adjacent != null);
ConnectedRooms = new BTRoom[2];
ConnectedRooms[0] = room;
ConnectedRooms[1] = room.Adjacent;
Rectangle room1, room2;
room1 = room.Rect;
room2 = room.Adjacent.Rect;
isHorizontal = (room1.Right <= room2.X || room2.Right <= room1.X);
//use the leaves as starting points for the corridor
if (room.SubRooms != null)
{
var leaves1 = room.GetLeaves();
var leaves2 = room.Adjacent.GetLeaves();
var suitableLeaves = GetSuitableLeafRooms(leaves1, leaves2, width, isHorizontal);
if (suitableLeaves == null || suitableLeaves.Length < 2)
{
// No suitable leaves found due to intersections
//DebugConsole.ThrowError("Error while generating ruins. Could not find a suitable position for a corridor. The width of the corridors may be too large compared to the sizes of the rooms.");
return;
}
else
{
ConnectedRooms[0] = suitableLeaves[0];
ConnectedRooms[1] = suitableLeaves[1];
}
}
else
{
rect = CalculateRectangle(room1, room2, width, isHorizontal);
if (rect.Width <= 0 || rect.Height <= 0)
{
DebugConsole.ThrowError("Error while generating ruins. Attempted to create a corridor with a width or height of <= 0");
return;
}
}
room.Corridor = this;
room.Adjacent.Corridor = this;
for (int i = corridors.Count - 1; i >= 0; i--)
{
var corridor = corridors[i];
if (corridor.rect.Intersects(this.rect))
{
if (isHorizontal && corridor.isHorizontal)
{
if (this.rect.Width < corridor.rect.Width)
return;
else
corridors.RemoveAt(i);
}
else if (!isHorizontal && !corridor.isHorizontal)
{
if (this.rect.Height < corridor.rect.Height)
return;
else
corridors.RemoveAt(i);
}
}
}
corridors.Add(this);
}
public override void CreateWalls()
{
Walls = new List<Line>();
if (IsHorizontal)
{
Walls.Add(new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.Right, Rect.Y)));
Walls.Add(new Line(new Vector2(Rect.X, Rect.Bottom), new Vector2(Rect.Right, Rect.Bottom)));
}
else
{
Walls.Add(new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.X, Rect.Bottom)));
Walls.Add(new Line(new Vector2(Rect.Right, Rect.Y), new Vector2(Rect.Right, Rect.Bottom)));
}
}
/// <summary>
/// Find two rooms which have two face-two-face walls that we can place a corridor in between
/// </summary>
/// <returns></returns>
private BTRoom[] GetSuitableLeafRooms(List<BTRoom> leaves1, List<BTRoom> leaves2, int width, bool isHorizontal)
{
int iOffset = Rand.Int(leaves1.Count, Rand.RandSync.Server);
int jOffset = Rand.Int(leaves2.Count, Rand.RandSync.Server);
for (int iCount = 0; iCount < leaves1.Count; iCount++)
{
int i = (iCount + iOffset) % leaves1.Count;
for (int jCount = 0; jCount < leaves2.Count; jCount++)
{
int j = (jCount + jOffset) % leaves2.Count;
if (isHorizontal)
{
if (leaves1[i].Rect.Y > leaves2[j].Rect.Bottom - width) continue;
if (leaves1[i].Rect.Bottom < leaves2[j].Rect.Y + width) continue;
}
else
{
if (leaves1[i].Rect.X > leaves2[j].Rect.Right - width) continue;
if (leaves1[i].Rect.Right < leaves2[j].Rect.X + width) continue;
}
// Check if the given corridor rect would intersect over a third room
if (CheckForIntersection(leaves1[i], leaves2[j], leaves1, leaves2, width, isHorizontal)) continue;
return new BTRoom[] { leaves1[i], leaves2[j] };
}
}
return null;
}
private bool CheckForIntersection(BTRoom potential1, BTRoom potential2, List<BTRoom> leaves1, List<BTRoom> leaves2, int width, bool isHorizontal)
{
Rectangle potentialCorridorRectangle = CalculateRectangle(potential1.Rect, potential2.Rect, width, isHorizontal);
if (potentialCorridorRectangle.Width <= 0 || potentialCorridorRectangle.Height <= 0) return true; // Invalid rectangle
for (int i = 0; i < leaves1.Count; i++)
{
if (leaves1[i] == potential1) continue;
if (potentialCorridorRectangle.Intersects(leaves1[i].Rect)) return true;
}
for (int i = 0; i < leaves2.Count; i++)
{
if (leaves2[i] == potential2) continue;
if (potentialCorridorRectangle.Intersects(leaves2[i].Rect)) return true;
}
rect = potentialCorridorRectangle; // Save the rectangle that passes the test
return false;
}
private Rectangle CalculateRectangle(Rectangle rect1, Rectangle rect2, int width, bool isHorizontal)
{
if (isHorizontal)
{
int left = Math.Min(rect1.Right, rect2.Right);
int right = Math.Max(rect1.X, rect2.X);
int top = Math.Max(rect1.Y, rect2.Y);
//int bottom = Math.Min(room1.Bottom, room2.Bottom);
int yPos = top;//Rand.Range(top, bottom - width, Rand.RandSync.Server);
return new Rectangle(left, yPos, right - left, width);
}
else if (rect1.Y > rect2.Bottom || rect2.Y > rect1.Bottom)
{
int left = Math.Max(rect1.X, rect2.X);
int right = Math.Min(rect1.Right, rect2.Right);
int top = Math.Min(rect1.Bottom, rect2.Bottom);
int bottom = Math.Max(rect1.Y, rect2.Y);
int xPos = Rand.Range(left, right - width, Rand.RandSync.Server);
return new Rectangle(xPos, top, width, bottom - top);
}
else
{
DebugConsole.ThrowError("wat");
return new Rectangle();
}
}
}
}
@@ -0,0 +1,497 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
namespace Barotrauma.RuinGeneration
{
[Flags]
enum RuinEntityType
{
Wall, Back, Door, Hatch, Prop
}
class RuinGenerationParams : ISerializableEntity
{
public static List<RuinGenerationParams> List
{
get
{
if (paramsList == null)
{
LoadAll();
}
return paramsList;
}
}
private static List<RuinGenerationParams> paramsList;
private string filePath;
private List<RuinRoom> roomTypeList;
public string Name => "RuinGenerationParams";
[Serialize("5000,5000", false), Editable]
public Point SizeMin
{
get;
set;
}
[Serialize("8000,8000", false), Editable]
public Point SizeMax
{
get;
set;
}
[Serialize(3, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the minimum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)]
public int RoomDivisionIterationsMin
{
get;
set;
}
[Serialize(4, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the maximum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)]
public int RoomDivisionIterationsMax
{
get;
set;
}
[Serialize(0.5f, false, description: "The probability for the split algorithm to split the area vertically. High values tend to create tall, vertical rooms, and low values wide, horizontal rooms."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.9f)]
public float VerticalSplitProbability
{
get;
set;
}
[Serialize(400, false, description: "The splitting algorithm attempts to keep the width of the split areas larger than this. If the width of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable]
public int MinSplitWidth
{
get;
set;
}
[Serialize(400, false, description: "The splitting algorithm attempts to keep the height of the split areas larger than this. If the height of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable]
public int MinSplitHeight
{
get;
set;
}
[Serialize("0.5,0.9", false, description: "The minimum and maximum width of a room relative to the areas created by the split algorithm."), Editable]
public Vector2 RoomWidthRange
{
get;
set;
}
[Serialize("0.5,0.9", false, description: "The minimum and maximum height of a room relative to the areas created by the split algorithm."), Editable]
public Vector2 RoomHeightRange
{
get;
set;
}
[Serialize("200,256", false, description: "The minimum and maximum width of the corridors between rooms."), Editable]
public Point CorridorWidthRange
{
get;
set;
}
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
} = new Dictionary<string, SerializableProperty>();
public IEnumerable<RuinRoom> RoomTypeList
{
get { return roomTypeList; }
}
private RuinGenerationParams(XElement element)
{
roomTypeList = new List<RuinRoom>();
if (element != null)
{
foreach (XElement subElement in element.Elements())
{
roomTypeList.Add(new RuinRoom(subElement));
}
}
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public static RuinGenerationParams GetRandom()
{
if (paramsList == null) { LoadAll(); }
if (paramsList.Count == 0)
{
DebugConsole.ThrowError("No ruin configuration files found in any content package.");
return new RuinGenerationParams(null);
}
return paramsList[Rand.Int(paramsList.Count, Rand.RandSync.Server)];
}
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 == null) { continue; }
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
paramsList.Clear();
DebugConsole.NewMessage($"Overriding all ruin configuration parameters using the file {configFile.Path}.", Color.Yellow);
}
else if (paramsList.Any())
{
DebugConsole.NewMessage($"Adding additional ruin configuration parameters from file '{configFile.Path}'");
}
var newParams = new RuinGenerationParams(mainElement)
{
filePath = configFile.Path
};
paramsList.Add(newParams);
}
}
public static void ClearAll()
{
paramsList?.Clear();
paramsList = null;
}
public static void SaveAll()
{
XmlWriterSettings settings = new XmlWriterSettings
{
Indent = true,
NewLineOnAttributes = true
};
foreach (RuinGenerationParams generationParams in List)
{
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig))
{
if (configFile.Path != generationParams.filePath) continue;
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
if (doc == null) { continue; }
SerializableProperty.SerializeProperties(generationParams, doc.Root);
using (var writer = XmlWriter.Create(configFile.Path, settings))
{
doc.WriteTo(writer);
writer.Flush();
}
}
}
}
}
class RuinRoom : ISerializableEntity
{
public enum RoomPlacement
{
Any,
First,
Last
}
public string Name
{
get;
private set;
}
[Serialize(1.0f, false), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float Commonness { get; private set; }
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
} = new Dictionary<string, SerializableProperty>();
[Serialize(RoomPlacement.Any, false), Editable]
public RoomPlacement Placement
{
get;
set;
}
[Serialize(0, false), Editable]
public int PlacementOffset
{
get;
set;
}
[Serialize(false, false), Editable]
public bool IsCorridor
{
get;
set;
}
[Serialize(1.0f, false), Editable]
public float MinWaterAmount
{
get;
set;
}
[Serialize(1.0f, false), Editable]
public float MaxWaterAmount
{
get;
set;
}
private List<RuinEntityConfig> entityList = new List<RuinEntityConfig>();
public RuinRoom(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
Name = element.GetAttributeString("name", "");
if (element != null)
{
int groupIndex = 0;
LoadEntities(element, ref groupIndex);
}
void LoadEntities(XElement element2, ref int groupIndex)
{
foreach (XElement subElement in element2.Elements())
{
if (subElement.Name.ToString().Equals("chooseone", StringComparison.OrdinalIgnoreCase))
{
groupIndex++;
LoadEntities(subElement, ref groupIndex);
}
else
{
entityList.Add(new RuinEntityConfig(subElement) { SingleGroupIndex = groupIndex });
}
}
}
}
public RuinEntityConfig GetRandomEntity(RuinEntityType type, Alignment alignment)
{
var matchingEntities = entityList.FindAll(rs =>
rs.Type == type &&
rs.Alignment.HasFlag(alignment));
if (!matchingEntities.Any()) return null;
return ToolBox.SelectWeightedRandom(
matchingEntities,
matchingEntities.Select(s => s.Commonness).ToList(),
Rand.RandSync.Server);
}
public List<RuinEntityConfig> GetPropList(RuinShape room, Rand.RandSync randSync)
{
Dictionary<int, List<RuinEntityConfig>> propGroups = new Dictionary<int, List<RuinEntityConfig>>();
foreach (RuinEntityConfig entityConfig in entityList)
{
if (entityConfig.Type != RuinEntityType.Prop) { continue; }
if (room.Rect.Width < entityConfig.MinRoomSize.X || room.Rect.Height < entityConfig.MinRoomSize.Y) { continue; }
if (room.Rect.Width > entityConfig.MaxRoomSize.X || room.Rect.Height > entityConfig.MaxRoomSize.Y) { continue; }
if (!propGroups.ContainsKey(entityConfig.SingleGroupIndex))
{
propGroups[entityConfig.SingleGroupIndex] = new List<RuinEntityConfig>();
}
propGroups[entityConfig.SingleGroupIndex].Add(entityConfig);
}
List<RuinEntityConfig> props = new List<RuinEntityConfig>();
foreach (KeyValuePair<int, List<RuinEntityConfig>> propGroup in propGroups)
{
if (propGroup.Key == 0)
{
props.AddRange(propGroup.Value);
}
else
{
props.Add(propGroup.Value[Rand.Int(propGroup.Value.Count, randSync)]);
}
}
return props;
}
}
class RuinEntityConfig : ISerializableEntity
{
public readonly MapEntityPrefab Prefab;
public enum RelativePlacement
{
SameRoom,
NextRoom,
NextCorridor,
PreviousRoom,
PreviousCorridor,
FirstRoom,
FirstCorridor,
LastRoom,
LastCorridor
}
public class EntityConnection
{
//which type of room to search for the item to connect to
//sameroom, nextroom, previousroom, firstroom and lastroom are also valid
public string RoomName
{
get;
private set;
}
public string TargetEntityIdentifier
{
get;
private set;
}
//Identifier of the item to run the wire from. Only needed in item assemblies to determine which item in the assembly to use.
public string SourceEntityIdentifier
{
get;
private set;
}
//if set, the connection is done by running a wire from
//(Pair.First = the name of the connection in this item) to (Pair.Second = the name of the connection in the target item)
public Pair<string, string> WireConnection
{
get;
private set;
}
public EntityConnection(XElement element)
{
RoomName = element.GetAttributeString("roomname", "");
TargetEntityIdentifier = element.GetAttributeString("targetentity", "");
SourceEntityIdentifier = element.GetAttributeString("sourceentity", "");
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("wire", StringComparison.OrdinalIgnoreCase))
{
WireConnection = new Pair<string, string>(
subElement.GetAttributeString("from", ""),
subElement.GetAttributeString("to", ""));
}
}
}
}
[Serialize(Alignment.Bottom, false), Editable]
public Alignment Alignment { get; private set; }
[Serialize("0,0", false, description: "Minimum offset from the anchor position, relative to the size of the room." +
" For example, a value of { -0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-left corner of the room and bottom-center."), Editable]
public Vector2 MinOffset { get; private set; }
[Serialize("0,0", false, description: "Maximum offset from the anchor position, relative to the size of the room." +
" For example, a value of { 0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-right corner of the room and bottom-center."), Editable]
public Vector2 MaxOffset { get; private set; }
[Serialize(RuinEntityType.Prop, false), Editable]
public RuinEntityType Type { get; private set; }
[Serialize(false, false), Editable]
public bool Expand { get; private set; }
[Serialize(RelativePlacement.SameRoom, false), Editable]
public RelativePlacement PlacementRelativeToParent { get; private set; }
[Serialize(1.0f, false), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float Commonness { get; private set; }
[Serialize(1, false)]
public int MinAmount { get; private set; }
[Serialize(1, false)]
public int MaxAmount { get; private set; }
[Serialize("0,0", false)]
public Point MinRoomSize { get; private set; }
[Serialize("100000,100000", false)]
public Point MaxRoomSize { get; private set; }
[Serialize("", false)]
public string TargetContainer { get; private set; }
public List<EntityConnection> EntityConnections { get; private set; } = new List<EntityConnection>();
public int SingleGroupIndex;
private readonly List<RuinEntityConfig> childEntities = new List<RuinEntityConfig>();
public IEnumerable<RuinEntityConfig> ChildEntities
{
get { return childEntities; }
}
public string Name => Prefab == null ? "null" : Prefab.Name;
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
} = new Dictionary<string, SerializableProperty>();
public RuinEntityConfig(XElement element)
{
string name = element.GetAttributeString("prefab", "");
Prefab = MapEntityPrefab.Find(name: null, identifier: name);
if (Prefab == null)
{
DebugConsole.ThrowError("Loading ruin entity config failed - map entity prefab \"" + name + "\" not found.");
return;
}
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
int gIndex = 0;
LoadChildren(element, ref gIndex);
void LoadChildren(XElement element2, ref int groupIndex)
{
foreach (XElement subElement in element2.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "connection":
case "entityconnection":
EntityConnections.Add(new EntityConnection(subElement));
break;
case "chooseone":
groupIndex++;
LoadChildren(subElement, ref groupIndex);
break;
default:
childEntities.Add(new RuinEntityConfig(subElement) { SingleGroupIndex = groupIndex });
break;
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,393 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class LinkedSubmarinePrefab : MapEntityPrefab
{
//public static readonly PrefabCollection<LinkedSubmarinePrefab> Prefabs = new PrefabCollection<LinkedSubmarinePrefab>();
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
//Prefabs.Remove(this);
}
public readonly Submarine mainSub;
public LinkedSubmarinePrefab(Submarine submarine)
{
this.mainSub = submarine;
}
protected override void CreateInstance(Rectangle rect)
{
System.Diagnostics.Debug.Assert(Submarine.MainSub != null);
LinkedSubmarine.CreateDummy(Submarine.MainSub, mainSub.FilePath, rect.Location.ToVector2());
}
}
partial class LinkedSubmarine : MapEntity
{
private List<Vector2> wallVertices;
private string filePath;
private bool loadSub;
private Submarine sub;
private ushort originalMyPortID;
//the ID of the docking port the sub was docked to in the original sub file
//(needed when replacing a lost sub)
private ushort originalLinkedToID;
private DockingPort originalLinkedPort;
private bool purchasedLostShuttles;
public Submarine Sub
{
get
{
return sub;
}
}
private XElement saveElement;
public override bool Linkable
{
get
{
return true;
}
}
public LinkedSubmarine(Submarine submarine)
: base(null, submarine)
{
linkedToID = new List<ushort>();
InsertToList();
DebugConsole.Log("Created linked submarine (" + ID + ")");
}
public static LinkedSubmarine CreateDummy(Submarine mainSub, Submarine linkedSub)
{
LinkedSubmarine sl = new LinkedSubmarine(mainSub)
{
sub = linkedSub
};
return sl;
}
public static LinkedSubmarine CreateDummy(Submarine mainSub, string filePath, Vector2 position)
{
XDocument doc = Submarine.OpenFile(filePath);
if (doc == null || doc.Root == null) return null;
LinkedSubmarine sl = CreateDummy(mainSub, doc.Root, position);
sl.filePath = filePath;
return sl;
}
public static LinkedSubmarine CreateDummy(Submarine mainSub, XElement element, Vector2 position)
{
LinkedSubmarine sl = new LinkedSubmarine(mainSub);
sl.GenerateWallVertices(element);
sl.Rect = new Rectangle(
(int)sl.wallVertices.Min(v => v.X + position.X),
(int)sl.wallVertices.Max(v => v.Y + position.Y),
(int)sl.wallVertices.Max(v => v.X + position.X),
(int)sl.wallVertices.Min(v => v.Y + position.Y));
sl.rect = new Rectangle((int)position.X, (int)position.Y, 1, 1);
return sl;
}
public override bool IsMouseOn(Vector2 position)
{
return Vector2.Distance(position, WorldPosition) < 50.0f;
}
public override MapEntity Clone()
{
return CreateDummy(Submarine, filePath, Position);
}
private void GenerateWallVertices(XElement rootElement)
{
List<Vector2> points = new List<Vector2>();
var wallPrefabs = StructurePrefab.Prefabs.Where(mp => mp.Body);
foreach (XElement element in rootElement.Elements())
{
if (element.Name != "Structure") { continue; }
string name = element.GetAttributeString("name", "");
string identifier = element.GetAttributeString("identifier", "");
StructurePrefab prefab = Structure.FindPrefab(name, identifier);
if (prefab == null) { continue; }
var rect = element.GetAttributeVector4("rect", Vector4.Zero);
points.Add(new Vector2(rect.X, rect.Y));
points.Add(new Vector2(rect.X + rect.Z, rect.Y));
points.Add(new Vector2(rect.X, rect.Y - rect.W));
points.Add(new Vector2(rect.X + rect.Z, rect.Y - rect.W));
}
wallVertices = MathUtils.GiftWrap(points);
}
public static LinkedSubmarine Load(XElement element, Submarine submarine)
{
Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero);
LinkedSubmarine linkedSub = null;
if (Screen.Selected == GameMain.SubEditorScreen)
{
linkedSub = CreateDummy(submarine, element, pos);
linkedSub.saveElement = element;
linkedSub.purchasedLostShuttles = false;
}
else
{
linkedSub = new LinkedSubmarine(submarine)
{
saveElement = element
};
linkedSub.purchasedLostShuttles = GameMain.GameSession.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles;
string levelSeed = element.GetAttributeString("location", "");
if (!string.IsNullOrWhiteSpace(levelSeed) &&
GameMain.GameSession.Level != null &&
GameMain.GameSession.Level.Seed != levelSeed &&
!linkedSub.purchasedLostShuttles)
{
linkedSub.loadSub = false;
}
else
{
linkedSub.loadSub = true;
linkedSub.rect.Location = MathUtils.ToPoint(pos);
}
}
linkedSub.filePath = element.GetAttributeString("filepath", "");
int[] linkedToIds = element.GetAttributeIntArray("linkedto", new int[0]);
for (int i = 0; i < linkedToIds.Length; i++)
{
linkedSub.linkedToID.Add((ushort)linkedToIds[i]);
if (Screen.Selected == GameMain.SubEditorScreen)
{
if (FindEntityByID((ushort)linkedToIds[i]) is MapEntity linked)
{
linkedSub.linkedTo.Add(linked);
}
}
}
linkedSub.originalLinkedToID = (ushort)element.GetAttributeInt("originallinkedto", 0);
linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0);
return linkedSub.loadSub ? linkedSub : null;
}
public override void OnMapLoaded()
{
if (!loadSub) { return; }
sub = Submarine.Load(saveElement, false);
Vector2 worldPos = saveElement.GetAttributeVector2("worldpos", Vector2.Zero);
if (worldPos != Vector2.Zero)
{
sub.SetPosition(worldPos);
}
else
{
sub.SetPosition(WorldPosition);
}
DockingPort linkedPort = null;
DockingPort myPort = null;
MapEntity linkedItem = linkedTo.FirstOrDefault(lt => (lt is Item) && ((Item)lt).GetComponent<DockingPort>() != null);
if (linkedItem == null)
{
linkedPort = DockingPort.List.FirstOrDefault(dp => dp.DockingTarget != null && dp.DockingTarget.Item.Submarine == sub);
}
else
{
linkedPort = ((Item)linkedItem).GetComponent<DockingPort>();
}
if (linkedPort == null)
{
if (purchasedLostShuttles)
{
linkedPort = (FindEntityByID(originalLinkedToID) as Item)?.GetComponent<DockingPort>();
}
if (linkedPort == null) { return; }
}
originalLinkedPort = linkedPort;
myPort = (FindEntityByID(originalMyPortID) as Item)?.GetComponent<DockingPort>();
if (myPort == null)
{
float closestDistance = 0.0f;
foreach (DockingPort port in DockingPort.List)
{
if (port.Item.Submarine != sub || port.IsHorizontal != linkedPort.IsHorizontal) { continue; }
float dist = Vector2.Distance(port.Item.WorldPosition, linkedPort.Item.WorldPosition);
if (myPort == null || dist < closestDistance)
{
myPort = port;
closestDistance = dist;
}
}
}
if (myPort != null)
{
originalMyPortID = myPort.Item.ID;
myPort.Undock();
//something else is already docked to the port this sub should be docked to
//may happen if a shuttle is lost, another vehicle docked to where the shuttle used to be,
//and the shuttle is then restored in the campaign mode
//or if the user connects multiple subs to the same docking ports in the sub editor
if (linkedPort.Docked && linkedPort.DockingTarget != null && linkedPort.DockingTarget != myPort)
{
//just spawn below the main sub
sub.SetPosition(
linkedPort.Item.Submarine.WorldPosition -
new Vector2(0, linkedPort.Item.Submarine.GetDockedBorders().Height / 2 + sub.GetDockedBorders().Height / 2));
}
else
{
Vector2 portDiff = myPort.Item.WorldPosition - sub.WorldPosition;
Vector2 offset = (myPort.IsHorizontal ?
Vector2.UnitX * Math.Sign(linkedPort.Item.WorldPosition.X - myPort.Item.WorldPosition.X) :
Vector2.UnitY * Math.Sign(linkedPort.Item.WorldPosition.Y - myPort.Item.WorldPosition.Y));
offset *= myPort.DockedDistance;
sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset);
myPort.Dock(linkedPort);
myPort.Lock(true);
}
}
if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles)
{
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine != sub) { continue; }
for (int i = 0; i < wall.SectionCount; i++)
{
wall.AddDamage(i, -wall.Prefab.Health);
}
}
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine != sub) { continue; }
hull.WaterVolume = 0.0f;
hull.OxygenPercentage = 100.0f;
}
}
sub.SetPosition(sub.WorldPosition - Submarine.WorldPosition);
sub.Submarine = Submarine;
}
public override XElement Save(XElement parentElement)
{
XElement saveElement = null;
if (sub == null)
{
if (this.saveElement == null)
{
var doc = Submarine.OpenFile(filePath);
saveElement = doc.Root;
saveElement.Name = "LinkedSubmarine";
saveElement.Add(new XAttribute("filepath", filePath));
}
else
{
saveElement = this.saveElement;
}
if (saveElement.Attribute("pos") != null) saveElement.Attribute("pos").Remove();
saveElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition)));
var linkedPort = linkedTo.FirstOrDefault(lt => (lt is Item) && ((Item)lt).GetComponent<DockingPort>() != null);
if (linkedPort != null)
{
saveElement.Attribute("linkedto")?.Remove();
saveElement.Add(new XAttribute("linkedto", linkedPort.ID));
}
}
else
{
saveElement = new XElement("LinkedSubmarine");
sub.SaveToXElement(saveElement);
}
saveElement.Attribute("originallinkedto")?.Remove();
saveElement.Add(new XAttribute("originallinkedto", originalLinkedPort != null ? originalLinkedPort.Item.ID : originalLinkedToID));
saveElement.Attribute("originalmyport")?.Remove();
saveElement.Add(new XAttribute("originalmyport", originalMyPortID));
if (sub != null)
{
bool leaveBehind = false;
if (!sub.DockedTo.Contains(Submarine.MainSub))
{
System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndPosition || Submarine.MainSub.AtStartPosition);
if (Submarine.MainSub.AtEndPosition)
{
leaveBehind = sub.AtEndPosition != Submarine.MainSub.AtEndPosition;
}
else
{
leaveBehind = sub.AtStartPosition != Submarine.MainSub.AtStartPosition;
}
}
if (leaveBehind)
{
saveElement.SetAttributeValue("location", Level.Loaded.Seed);
saveElement.SetAttributeValue("worldpos", XMLExtensions.Vector2ToString(sub.SubBody.Position));
}
else
{
if (saveElement.Attribute("location") != null) saveElement.Attribute("location").Remove();
if (saveElement.Attribute("worldpos") != null) saveElement.Attribute("worldpos").Remove();
}
saveElement.SetAttributeValue("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition));
}
parentElement.Add(saveElement);
return saveElement;
}
}
}
@@ -0,0 +1,151 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Location
{
public List<LocationConnection> Connections;
private string baseName;
private int nameFormatIndex;
public bool Discovered;
public int TypeChangeTimer;
public string BaseName { get => baseName; }
public string Name { get; private set; }
public Vector2 MapPosition { get; private set; }
public LocationType Type { get; private set; }
public int PortraitId { get; private set; }
public int MissionsCompleted;
private List<Mission> availableMissions = new List<Mission>();
public IEnumerable<Mission> AvailableMissions
{
get
{
CheckMissionCompleted();
for (int i = availableMissions.Count; i < Connections.Count * 2; i++)
{
int seed = (ToolBox.StringToInt(BaseName) + MissionsCompleted * 10 + i) % int.MaxValue;
MTRandom rand = new MTRandom(seed);
LocationConnection connection = Connections[(MissionsCompleted + i) % Connections.Count];
Location destination = connection.OtherLocation(this);
var mission = Mission.LoadRandom(new Location[] { this, destination }, rand, true, MissionType.All, true);
if (mission == null) { continue; }
if (availableMissions.Any(m => m.Prefab == mission.Prefab)) { continue; }
if (GameSettings.VerboseLogging && mission != null)
{
DebugConsole.NewMessage("Generated a new mission for a location (location: " + Name + ", seed: " + seed.ToString("X") + ", missions completed: " + MissionsCompleted + ", type: " + mission.Name + ")", Color.White);
}
availableMissions.Add(mission);
}
return availableMissions;
}
}
public Mission SelectedMission
{
get;
set;
}
public int SelectedMissionIndex
{
get
{
if (SelectedMission == null) { return -1; }
return availableMissions.IndexOf(SelectedMission);
}
set
{
if (value < 0 || value >= AvailableMissions.Count())
{
SelectedMission = null;
return;
}
SelectedMission = availableMissions[value];
}
}
public Location(Vector2 mapPosition, int? zone, Random rand)
{
this.Type = LocationType.Random(rand, zone);
this.Name = RandomName(Type, rand);
this.MapPosition = mapPosition;
PortraitId = ToolBox.StringToInt(Name);
Connections = new List<LocationConnection>();
}
public static Location CreateRandom(Vector2 position, int? zone , Random rand)
{
return new Location(position, zone, rand);
}
public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
{
System.Diagnostics.Debug.Assert(Connections.Contains(connection));
return AvailableMissions.Where(m => m.Locations[1] == connection.OtherLocation(this));
}
public void ChangeType(LocationType newType)
{
if (newType == Type) { return; }
//clear missions from this and adjacent locations (they may be invalid now)
availableMissions.Clear();
foreach (LocationConnection connection in Connections)
{
connection.OtherLocation(this)?.availableMissions.Clear();
}
DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType);
Type = newType;
Name = Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
}
public void CheckMissionCompleted()
{
foreach (Mission mission in availableMissions)
{
if (mission.Completed)
{
DebugConsole.Log("Mission \"" + mission.Name + "\" completed in \"" + Name + "\".");
MissionsCompleted++;
}
}
availableMissions.RemoveAll(m => m.Completed);
}
private string RandomName(LocationType type, Random rand)
{
baseName = type.GetRandomName(rand);
nameFormatIndex = rand.Next() % type.NameFormats.Count;
return type.NameFormats[nameFormatIndex].Replace("[name]", baseName);
}
public void Remove()
{
RemoveProjSpecific();
}
partial void RemoveProjSpecific();
}
}
@@ -0,0 +1,57 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
namespace Barotrauma
{
class LocationConnection
{
public Biome Biome;
public float Difficulty;
public List<Vector2[]> CrackSegments;
public bool Passed;
public Level Level { get; set; }
public Vector2 CenterPos
{
get
{
return (Locations[0].MapPosition + Locations[1].MapPosition) / 2.0f;
}
}
public Location[] Locations { get; private set; }
public float Length
{
get;
private set;
}
public LocationConnection(Location location1, Location location2)
{
Locations = new Location[] { location1, location2 };
Length = Vector2.Distance(location1.MapPosition, location2.MapPosition);
}
public Location OtherLocation(Location location)
{
if (Locations[0] == location)
{
return Locations[1];
}
else if (Locations[1] == location)
{
return Locations[0];
}
else
{
return null;
}
}
}
}
@@ -0,0 +1,244 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class LocationType
{
public static readonly List<LocationType> List = new List<LocationType>();
private List<string> nameFormats;
private List<string> names;
private Sprite symbolSprite;
private readonly List<Sprite> portraits = new List<Sprite>();
//<name, commonness>
private List<Tuple<JobPrefab, float>> hireableJobs;
private float totalHireableWeight;
public Dictionary<int, float> CommonnessPerZone = new Dictionary<int, float>();
public readonly string Identifier;
public readonly string Name;
public readonly List<LocationTypeChange> CanChangeTo = new List<LocationTypeChange>();
public bool UseInMainMenu
{
get;
private set;
}
public List<string> NameFormats
{
get { return nameFormats; }
}
public bool HasHireableCharacters
{
get { return hireableJobs.Any(); }
}
public Sprite Sprite
{
get { return symbolSprite; }
}
public Color SpriteColor
{
get;
private set;
}
public override string ToString()
{
return $"LocationType (" + Identifier + ")";
}
private LocationType(XElement element)
{
Identifier = element.GetAttributeString("identifier", element.Name.ToString());
Name = TextManager.Get("LocationName." + Identifier);
nameFormats = TextManager.GetAll("LocationNameFormat." + Identifier);
UseInMainMenu = element.GetAttributeBool("useinmainmenu", false);
string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt");
try
{
names = File.ReadAllLines(nameFile).ToList();
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to read name file for location type \"" + Identifier + "\"!", e);
names = new List<string>() { "Name file not found" };
}
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", new string[] { "" });
foreach (string commonnessPerZoneStr in commonnessPerZoneStrs)
{
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
if (splitCommonnessPerZone.Length != 2 ||
!int.TryParse(splitCommonnessPerZone[0].Trim(), out int zoneIndex) ||
!float.TryParse(splitCommonnessPerZone[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float zoneCommonness))
{
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone0index: zone0commonness, zone1index: zone1commonness\"");
break;
}
CommonnessPerZone[zoneIndex] = zoneCommonness;
}
hireableJobs = new List<Tuple<JobPrefab, float>>();
foreach (XElement 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;
}
float jobCommonness = subElement.GetAttributeFloat("commonness", 1.0f);
totalHireableWeight += jobCommonness;
Tuple<JobPrefab, float> hireableJob = new Tuple<JobPrefab, float>(jobPrefab, jobCommonness);
hireableJobs.Add(hireableJob);
break;
case "symbol":
symbolSprite = new Sprite(subElement, lazyLoad: true);
SpriteColor = subElement.GetAttributeColor("color", Color.White);
break;
case "changeto":
CanChangeTo.Add(new LocationTypeChange(Identifier, subElement));
break;
case "portrait":
var portrait = new Sprite(subElement, lazyLoad: true);
if (portrait != null)
{
portraits.Add(portrait);
}
break;
}
}
}
public JobPrefab GetRandomHireable()
{
float randFloat = Rand.Range(0.0f, totalHireableWeight, Rand.RandSync.Server);
foreach (Tuple<JobPrefab, float> hireable in hireableJobs)
{
if (randFloat < hireable.Item2) return hireable.Item1;
randFloat -= hireable.Item2;
}
return null;
}
public Sprite GetPortrait(int portraitId)
{
if (portraits.Count == 0) { return null; }
return portraits[Math.Abs(portraitId) % portraits.Count];
}
public string GetRandomName(Random rand)
{
return names[rand.Next() % names.Count];
}
public static LocationType Random(Random rand, int? zone = null)
{
Debug.Assert(List.Count > 0, "LocationType.list.Count == 0, you probably need to initialize LocationTypes");
List<LocationType> allowedLocationTypes = zone.HasValue ? List.FindAll(lt => lt.CommonnessPerZone.ContainsKey(zone.Value)) : List;
if (allowedLocationTypes.Count == 0)
{
DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!");
}
if (zone.HasValue)
{
return ToolBox.SelectWeightedRandom(
allowedLocationTypes,
allowedLocationTypes.Select(a => a.CommonnessPerZone[zone.Value]).ToList(),
rand);
}
else
{
return allowedLocationTypes[rand.Next() % allowedLocationTypes.Count];
}
}
public static void Init()
{
List.Clear();
var locationTypeFiles = GameMain.Instance.GetFilesOfType(ContentType.LocationTypes);
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);
}
}
}
}
}
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class LocationTypeChange
{
public readonly string ChangeToType;
public readonly float Probability;
public readonly int RequiredDuration;
public List<string> Messages = new List<string>();
//the change can't happen if there's a location of the given type next to this one
public readonly List<string> DisallowedAdjacentLocations;
//the change can only happen if there's at least one of the given types of locations next to this one
public readonly List<string> RequiredAdjacentLocations;
public LocationTypeChange(string currentType, XElement element)
{
ChangeToType = element.GetAttributeString("type", "");
Probability = element.GetAttributeFloat("probability", 1.0f);
RequiredDuration = element.GetAttributeInt("requiredduration", 0);
DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList();
RequiredAdjacentLocations = element.GetAttributeStringArray("requiredadjacentlocations", new string[0]).ToList();
string messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType);
Messages = TextManager.GetAll(messageTag);
if (Messages == null)
{
DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType);
}
}
}
}
@@ -0,0 +1,631 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Voronoi2;
namespace Barotrauma
{
partial class Map
{
private MapGenerationParams generationParams;
private readonly int size;
private List<LocationConnection> connections;
public Action<Location, LocationConnection> OnLocationSelected;
//from -> to
public Action<Location, Location> OnLocationChanged;
public Action<LocationConnection, Mission> OnMissionSelected;
public Location CurrentLocation { get; private set; }
public int CurrentLocationIndex
{
get { return Locations.IndexOf(CurrentLocation); }
}
public Location SelectedLocation { get; private set; }
public int SelectedLocationIndex
{
get { return Locations.IndexOf(SelectedLocation); }
}
public int SelectedMissionIndex
{
get { return SelectedConnection == null ? -1 : CurrentLocation.SelectedMissionIndex; }
}
public LocationConnection SelectedConnection { get; private set; }
public string Seed { get; private set; }
public List<Location> Locations { get; private set; }
public Map(string seed)
{
generationParams = MapGenerationParams.Instance;
this.Seed = seed;
this.size = generationParams.Size;
Locations = new List<Location>();
connections = new List<LocationConnection>();
Rand.SetSyncedSeed(ToolBox.StringToInt(this.Seed));
Generate();
//start from the colony furthest away from the center
float largestDist = 0.0f;
Vector2 center = new Vector2(size, size) / 2;
foreach (Location location in Locations)
{
if (location.Type.Identifier != "City") continue;
float dist = Vector2.DistanceSquared(center, location.MapPosition);
if (dist > largestDist)
{
largestDist = dist;
CurrentLocation = location;
}
}
CurrentLocation.Discovered = true;
foreach (LocationConnection connection in connections)
{
connection.Level = Level.CreateRandom(connection);
}
InitProjectSpecific();
}
partial void InitProjectSpecific();
public float[,] Noise;
private void GenerateNoiseMap(int octaves, float persistence)
{
float z = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server);
Noise = new float[generationParams.NoiseResolution, generationParams.NoiseResolution];
float min = float.MaxValue, max = 0.0f;
for (int x = 0; x < generationParams.NoiseResolution; x++)
{
for (int y = 0; y < generationParams.NoiseResolution; y++)
{
Noise[x, y] = (float)PerlinNoise.OctavePerlin(
(double)x / generationParams.NoiseResolution,
(double)y / generationParams.NoiseResolution,
z, generationParams.NoiseFrequency, octaves, persistence);
min = Math.Min(Noise[x, y], min);
max = Math.Max(Noise[x, y], max);
}
}
float radius = generationParams.NoiseResolution / 2;
Vector2 center = Vector2.One * radius;
float range = max - min;
float centerDarkenRadius = radius * generationParams.CenterDarkenRadius;
float edgeDarkenRadius = radius * generationParams.EdgeDarkenRadius;
for (int x = 0; x < generationParams.NoiseResolution; x++)
{
for (int y = 0; y < generationParams.NoiseResolution; y++)
{
//normalize the noise to 0-1 range
Noise[x, y] = (Noise[x, y] - min) / range;
float dist = Vector2.Distance(center, new Vector2(x, y));
if (dist < centerDarkenRadius)
{
float angle = (float)Math.Atan2(y - center.Y, x - center.X);
float phase = angle * generationParams.CenterDarkenWaveFrequency + Noise[x, y] * generationParams.CenterDarkenWavePhaseNoise;
float currDarkenRadius = centerDarkenRadius * (0.6f + (float)Math.Sin(phase) * 0.4f);
if (dist < currDarkenRadius)
{
float darkenAmount = 1.0f - (dist / currDarkenRadius);
Noise[x, y] = MathHelper.Lerp(Noise[x, y], Noise[x, y] * (1.0f - generationParams.CenterDarkenStrength), darkenAmount);
}
}
if (dist > edgeDarkenRadius)
{
float darkenAmount = Math.Min((dist - edgeDarkenRadius) / (radius - edgeDarkenRadius), 1.0f);
Noise[x, y] = MathHelper.Lerp(Noise[x, y], 1.0f - generationParams.EdgeDarkenStrength, darkenAmount);
}
}
}
}
partial void GenerateNoiseMapProjSpecific();
private void Generate()
{
connections.Clear();
Locations.Clear();
GenerateNoiseMap(generationParams.NoiseOctaves, generationParams.NoisePersistence);
List<Vector2> sites = new List<Vector2>();
float mapRadius = size / 2;
Vector2 mapCenter = new Vector2(mapRadius, mapRadius);
float locationRadius = mapRadius * generationParams.LocationRadius;
for (float x = mapCenter.X - locationRadius; x < mapCenter.X + locationRadius; x += generationParams.VoronoiSiteInterval)
{
for (float y = mapCenter.Y - locationRadius; y < mapCenter.Y + locationRadius; y += generationParams.VoronoiSiteInterval)
{
float noiseVal = Noise[(int)(x / size * generationParams.NoiseResolution), (int)(y / size * generationParams.NoiseResolution)];
if (Rand.Range(generationParams.VoronoiSitePlacementMinVal, 1.0f, Rand.RandSync.Server) <
noiseVal * generationParams.VoronoiSitePlacementProbability)
{
sites.Add(new Vector2(x, y));
}
}
}
Voronoi voronoi = new Voronoi(0.5f);
List<GraphEdge> edges = voronoi.MakeVoronoiGraph(sites, size, size);
float zoneRadius = size / 2 / generationParams.DifficultyZones;
sites.Clear();
foreach (GraphEdge edge in edges)
{
if (edge.Point1 == edge.Point2) continue;
if (Vector2.DistanceSquared(edge.Point1, mapCenter) >= locationRadius * locationRadius ||
Vector2.DistanceSquared(edge.Point2, mapCenter) >= locationRadius * locationRadius) continue;
Location[] newLocations = new Location[2];
newLocations[0] = Locations.Find(l => l.MapPosition == edge.Point1 || l.MapPosition == edge.Point2);
newLocations[1] = Locations.Find(l => l != newLocations[0] && (l.MapPosition == edge.Point1 || l.MapPosition == edge.Point2));
for (int i = 0; i < 2; i++)
{
if (newLocations[i] != null) continue;
Vector2[] points = new Vector2[] { edge.Point1, edge.Point2 };
int positionIndex = Rand.Int(1, Rand.RandSync.Server);
Vector2 position = points[positionIndex];
if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) position = points[1 - positionIndex];
int zone = MathHelper.Clamp(generationParams.DifficultyZones - (int)Math.Floor(Vector2.Distance(position, mapCenter) / zoneRadius), 1, generationParams.DifficultyZones);
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server));
Locations.Add(newLocations[i]);
}
var newConnection = new LocationConnection(newLocations[0], newLocations[1]);
float centerDist = Vector2.Distance(newConnection.CenterPos, mapCenter);
newConnection.Difficulty = MathHelper.Clamp(((1.0f - centerDist / mapRadius) * 100) + Rand.Range(-10.0f, 10.0f, Rand.RandSync.Server), 0, 100);
connections.Add(newConnection);
}
//remove connections that are too short
float minConnectionDistanceSqr = generationParams.MinConnectionDistance * generationParams.MinConnectionDistance;
for (int i = connections.Count - 1; i >= 0; i--)
{
LocationConnection connection = connections[i];
if (Vector2.DistanceSquared(connection.Locations[0].MapPosition, connection.Locations[1].MapPosition) > minConnectionDistanceSqr)
{
continue;
}
//locations.Remove(connection.Locations[0]);
connections.Remove(connection);
foreach (LocationConnection connection2 in connections)
{
if (connection2.Locations[0] == connection.Locations[0]) connection2.Locations[0] = connection.Locations[1];
if (connection2.Locations[1] == connection.Locations[0]) connection2.Locations[1] = connection.Locations[1];
}
}
HashSet<Location> connectedLocations = new HashSet<Location>();
foreach (LocationConnection connection in connections)
{
connection.Locations[0].Connections.Add(connection);
connection.Locations[1].Connections.Add(connection);
connectedLocations.Add(connection.Locations[0]);
connectedLocations.Add(connection.Locations[1]);
}
//remove orphans
Locations.RemoveAll(c => !connectedLocations.Contains(c));
//remove locations that are too close to each other
float minLocationDistanceSqr = generationParams.MinLocationDistance * generationParams.MinLocationDistance;
for (int i = Locations.Count - 1; i >= 0; i--)
{
for (int j = Locations.Count - 1; j > i; j--)
{
float dist = Vector2.DistanceSquared(Locations[i].MapPosition, Locations[j].MapPosition);
if (dist > minLocationDistanceSqr)
{
continue;
}
//move connections from Locations[j] to Locations[i]
foreach (LocationConnection connection in Locations[j].Connections)
{
if (connection.Locations[0] == Locations[j])
{
connection.Locations[0] = Locations[i];
}
else
{
connection.Locations[1] = Locations[i];
}
Locations[i].Connections.Add(connection);
}
Locations.RemoveAt(j);
}
}
for (int i = connections.Count - 1; i >= 0; i--)
{
i = Math.Min(i, connections.Count - 1);
LocationConnection connection = connections[i];
for (int n = Math.Min(i - 1, connections.Count - 1); n >= 0; n--)
{
if (connection.Locations.Contains(connections[n].Locations[0])
&& connection.Locations.Contains(connections[n].Locations[1]))
{
connections.RemoveAt(n);
}
}
}
foreach (LocationConnection connection in connections)
{
float centerDist = Vector2.Distance(connection.CenterPos, mapCenter);
connection.Difficulty = MathHelper.Clamp(((1.0f - centerDist / mapRadius) * 100) + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 0, 100);
}
AssignBiomes();
GenerateNoiseMapProjSpecific();
}
private void AssignBiomes()
{
float locationRadius = size * 0.5f * generationParams.LocationRadius;
var biomes = LevelGenerationParams.GetBiomes();
Vector2 centerPos = new Vector2(size, size) / 2;
for (int i = 0; i < generationParams.DifficultyZones; i++)
{
List<Biome> allowedBiomes = biomes.FindAll(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i));
float zoneRadius = locationRadius * ((i + 1.0f) / generationParams.DifficultyZones);
foreach (LocationConnection connection in connections)
{
if (connection.Biome != null) continue;
if (i == generationParams.DifficultyZones - 1 ||
Vector2.Distance(connection.Locations[0].MapPosition, centerPos) < zoneRadius ||
Vector2.Distance(connection.Locations[1].MapPosition, centerPos) < zoneRadius)
{
connection.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.Server)];
}
}
}
}
private void ExpandBiomes(List<LocationConnection> seeds)
{
List<LocationConnection> nextSeeds = new List<LocationConnection>();
foreach (LocationConnection connection in seeds)
{
foreach (Location location in connection.Locations)
{
foreach (LocationConnection otherConnection in location.Connections)
{
if (otherConnection == connection) continue;
if (otherConnection.Biome != null) continue; //already assigned
otherConnection.Biome = connection.Biome;
nextSeeds.Add(otherConnection);
}
}
}
if (nextSeeds.Count > 0)
{
ExpandBiomes(nextSeeds);
}
}
public void MoveToNextLocation()
{
Location prevLocation = CurrentLocation;
SelectedConnection.Passed = true;
CurrentLocation = SelectedLocation;
CurrentLocation.Discovered = true;
SelectedLocation = null;
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
}
public void SetLocation(int index)
{
if (index == -1)
{
CurrentLocation = null;
return;
}
if (index < 0 || index >= Locations.Count)
{
DebugConsole.ThrowError("Location index out of bounds");
return;
}
Location prevLocation = CurrentLocation;
CurrentLocation = Locations[index];
CurrentLocation.Discovered = true;
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
}
public void SelectLocation(int index)
{
if (index == -1)
{
SelectedLocation = null;
SelectedConnection = null;
OnLocationSelected?.Invoke(null, null);
return;
}
if (index < 0 || index >= Locations.Count)
{
DebugConsole.ThrowError("Location index out of bounds");
return;
}
SelectedLocation = Locations[index];
SelectedConnection = connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
}
public void SelectLocation(Location location)
{
if (!Locations.Contains(location))
{
string errorMsg = "Failed to select a location. " + (location?.Name ?? "null") + " not found in the map.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Map.SelectLocation:LocationNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
SelectedLocation = location;
SelectedConnection = connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
}
public void SelectMission(int missionIndex)
{
if (SelectedConnection == null) { return; }
if (CurrentLocation == null)
{
string errorMsg = "Failed to select a mission (current location not set).";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Map.SelectMission:CurrentLocationNotSet", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
CurrentLocation.SelectedMissionIndex = missionIndex;
//the destination must be the same as the destination of the mission
if (CurrentLocation.SelectedMission != null &&
CurrentLocation.SelectedMission.Locations[1] != SelectedLocation)
{
SelectLocation(CurrentLocation.SelectedMission.Locations[1]);
}
OnMissionSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMission);
}
public void SelectRandomLocation(bool preferUndiscovered)
{
List<Location> nextLocations = CurrentLocation.Connections.Select(c => c.OtherLocation(CurrentLocation)).ToList();
List<Location> undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered);
if (undiscoveredLocations.Count > 0 && preferUndiscovered)
{
SelectLocation(undiscoveredLocations[Rand.Int(undiscoveredLocations.Count, Rand.RandSync.Unsynced)]);
}
else
{
SelectLocation(nextLocations[Rand.Int(nextLocations.Count, Rand.RandSync.Unsynced)]);
}
}
public void ProgressWorld()
{
foreach (Location location in Locations)
{
if (!location.Discovered) continue;
//find which types of locations this one can change to
List<LocationTypeChange> allowedTypeChanges = new List<LocationTypeChange>();
List<LocationTypeChange> readyTypeChanges = new List<LocationTypeChange>();
foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
{
//check if there are any adjacent locations that would prevent the change
bool disallowedFound = false;
foreach (string disallowedLocationName in typeChange.DisallowedAdjacentLocations)
{
if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(disallowedLocationName, StringComparison.OrdinalIgnoreCase)))
{
disallowedFound = true;
break;
}
}
if (disallowedFound) continue;
//check that there's a required adjacent location present
bool requiredFound = false;
foreach (string requiredLocationName in typeChange.RequiredAdjacentLocations)
{
if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(requiredLocationName, StringComparison.OrdinalIgnoreCase)))
{
requiredFound = true;
break;
}
}
if (!requiredFound && typeChange.RequiredAdjacentLocations.Count > 0) continue;
allowedTypeChanges.Add(typeChange);
if (location.TypeChangeTimer >= typeChange.RequiredDuration)
{
readyTypeChanges.Add(typeChange);
}
}
//select a random type change
if (Rand.Range(0.0f, 1.0f) < readyTypeChanges.Sum(t => t.Probability))
{
var selectedTypeChange =
ToolBox.SelectWeightedRandom(readyTypeChanges, readyTypeChanges.Select(t => t.Probability).ToList(), Rand.RandSync.Unsynced);
if (selectedTypeChange != null)
{
string prevName = location.Name;
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(selectedTypeChange.ChangeToType, StringComparison.OrdinalIgnoreCase)));
ChangeLocationType(location, prevName, selectedTypeChange);
location.TypeChangeTimer = -1;
break;
}
}
if (allowedTypeChanges.Count > 0)
{
location.TypeChangeTimer++;
}
else
{
location.TypeChangeTimer = 0;
}
}
}
partial void ChangeLocationType(Location location, string prevName, LocationTypeChange change);
partial void ClearAnimQueue();
public static Map LoadNew(XElement element)
{
string mapSeed = element.GetAttributeString("seed", "a");
Map map = new Map(mapSeed);
map.Load(element, false);
return map;
}
public void Load(XElement element, bool showNotifications)
{
ClearAnimQueue();
SetLocation(element.GetAttributeInt("currentlocation", 0));
if (!Version.TryParse(element.GetAttributeString("version", ""), out _))
{
DebugConsole.ThrowError("Incompatible map save file, loading the game failed.");
return;
}
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "location":
string locationType = subElement.GetAttributeString("type", "");
Location location = Locations[subElement.GetAttributeInt("i", 0)];
int typeChangeTimer = subElement.GetAttributeInt("changetimer", 0);
int missionsCompleted = subElement.GetAttributeInt("missionscompleted", 0);
string prevLocationName = location.Name;
LocationType prevLocationType = location.Type;
location.Discovered = true;
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)));
location.TypeChangeTimer = typeChangeTimer;
location.MissionsCompleted = missionsCompleted;
if (showNotifications && prevLocationType != location.Type)
{
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase));
if (change != null)
{
ChangeLocationType(location, prevLocationName, change);
}
}
break;
case "connection":
int connectionIndex = subElement.GetAttributeInt("i", 0);
connections[connectionIndex].Passed = true;
break;
}
}
}
public void Save(XElement element)
{
XElement mapElement = new XElement("map");
mapElement.Add(new XAttribute("version", GameMain.Version.ToString()));
mapElement.Add(new XAttribute("currentlocation", CurrentLocationIndex));
mapElement.Add(new XAttribute("seed", Seed));
for (int i = 0; i < Locations.Count; i++)
{
var location = Locations[i];
if (!location.Discovered) continue;
var locationElement = new XElement("location", new XAttribute("i", i));
locationElement.Add(new XAttribute("type", location.Type.Identifier));
if (location.TypeChangeTimer > 0)
{
locationElement.Add(new XAttribute("changetimer", location.TypeChangeTimer));
}
location.CheckMissionCompleted();
if (location.MissionsCompleted > 0)
{
locationElement.Add(new XAttribute("missionscompleted", location.MissionsCompleted));
}
mapElement.Add(locationElement);
}
for (int i = 0; i < connections.Count; i++)
{
var connection = connections[i];
if (!connection.Passed) continue;
var connectionElement = new XElement("connection",
new XAttribute("i", i),
new XAttribute("passed", connection.Passed));
mapElement.Add(connectionElement);
}
element.Add(mapElement);
}
public void Remove()
{
foreach (Location location in Locations)
{
location.Remove();
}
RemoveProjSpecific();
}
partial void RemoveProjSpecific();
}
}
@@ -0,0 +1,276 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
namespace Barotrauma
{
class MapGenerationParams : ISerializableEntity
{
private static MapGenerationParams instance;
private static string loadedFile;
public static MapGenerationParams Instance
{
get
{
return instance;
}
}
#if DEBUG
[Serialize(false, true), Editable]
public bool ShowNoiseMap { get; set; }
[Serialize(true, true), Editable]
public bool ShowLocations { get; set; }
[Serialize(true, true), Editable]
public bool ShowLevelTypeNames { get; set; }
[Serialize(true, true), Editable]
public bool ShowOverlay { get; set; }
#else
public readonly bool ShowLocations = true;
public readonly bool ShowLevelTypeNames = false;
public readonly bool ShowOverlay = true;
#endif
[Serialize(6, true)]
public int DifficultyZones { get; set; } //Number of difficulty zones
[Serialize(2000, true)]
public int Size { 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)]
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)]
public float LargeLevelConnectionLength { get; set; }
[Serialize(1024, true)]
public int NoiseResolution { get; set; } //Resolution of the noisemap overlay
[Serialize(10.0f, true), Editable(0.0f, 1000.0f)]
public float NoiseFrequency { get; set; }
[Serialize(8, true), Editable(1, 100)]
public int NoiseOctaves { get; set; }
[Serialize(0.5f, true), Editable(0.0f, 1.0f)]
public float NoisePersistence { get; set; }
[Serialize("200,200", true), Editable]
public Vector2 TileSpriteSize { get; set; }
[Serialize("280,80", true), Editable]
public Vector2 TileSpriteSpacing { get; set; }
[Serialize(1.0f, true, description: "How dark the center of the map is (1.0f = black)."), Editable(0.0f, 1.0f)]
public float CenterDarkenStrength { get; set; }
[Serialize(0.9f, true, description: "How close to the center the darkening starts (0.8f = 20% from the edge)."), Editable(0.0f, 1.0f)]
public float CenterDarkenRadius { get; set; }
[Serialize(5, true, description: "The edge of the dark center area is wave-shaped, and the frequency is determined by this value." +
" I.e. how many points does the star-shaped dark area in the center have."), Editable(0, 1000)]
public int CenterDarkenWaveFrequency { get; set; }
[Serialize(15.0f, true, description: "How heavily the noise map affects the phase of the edge wave (higher value = more irregular shape)."), Editable(0, 1000.0f)]
public float CenterDarkenWavePhaseNoise { get; set; }
[Serialize(0.8f, true, description: "How dark the edges of the map are (1.0f = black)."), Editable(0.0f, 1.0f)]
public float EdgeDarkenStrength { get; set; }
[Serialize(0.9f, true, description: "How far from the center the darkening starts (0.95f = 5% from the edge)."), Editable(0.0f, 1.0f)]
public float EdgeDarkenRadius { get; set; }
[Serialize(0.9f, true, description: "How far from the center locations can be placed."), Editable(0.0f, 1.0f)]
public float LocationRadius { get; set; }
[Serialize(20.0f, true, 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(1.0f, 100.0f)]
public float VoronoiSiteInterval { get; set; }
[Serialize(0.3f, true, description: "How likely it is for a site to be placed at a given spot (e.g. 20% probability for a site to be placed every 5 units of the map). " +
"Multiplied with the noise value in the spot, meaning that sites are less likely to appear in dark spots."), Editable(0.01f, 1.0f)]
public float VoronoiSitePlacementProbability { get; set; }
[Serialize(0.1f, true, description: "Probability * noise ^ 2 must be higher than this for a site to be placed. " +
"= How bright the noise map must be at a given spot for a location to be placed there"), Editable(0.01f, 1.0f)]
public float VoronoiSitePlacementMinVal { get; set; }
[Serialize(10.0f, true, 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)]
public float MinLocationDistance { get; set; }
[Serialize(0.2f, true, description: "Affects how many iterations are done when generating the jagged shape of the connections (iterations = Sqrt(connectionLength * multiplier))."), Editable(0.0f, 10.0f)]
public float ConnectionIterationMultiplier { get; set; }
[Serialize(0.5f, true, description: "How large the \"bends\" in the connections are (displacement = connectionLength * multiplier)."), Editable(0.0f, 10.0f)]
public float ConnectionDisplacementMultiplier { get; set; }
[Serialize(0.1f, true, description: "ConnectionIterationMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f)]
public float ConnectionIndicatorIterationMultiplier { get; set; }
[Serialize(0.1f, true, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f)]
public float ConnectionIndicatorDisplacementMultiplier { get; set; }
public Sprite ConnectionSprite { get; private set; }
#if CLIENT
[Serialize(15.0f, true, description: "Size of the location icons in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)]
public float LocationIconSize { get; set; }
[Serialize("150,150,150,255", true, description: "The color used to display the low-difficulty connections on the map."), Editable()]
public Color LowDifficultyColor { get; set; }
[Serialize("210,143,83,255", true, description: "The color used to display the medium-difficulty connections on the map."), Editable()]
public Color MediumDifficultyColor { get; set; }
[Serialize("216,154,138", true, description: "The color used to display the high-difficulty connections on the map."), Editable()]
public Color HighDifficultyColor { get; set; }
public SpriteSheet DecorativeMapSprite { get; private set; }
public SpriteSheet DecorativeGraphSprite { get; private set; }
public SpriteSheet DecorativeLineTop { get; private set; }
public SpriteSheet DecorativeLineBottom { get; private set; }
public SpriteSheet DecorativeLineCorner { get; private set; }
public SpriteSheet ReticleLarge { get; private set; }
public SpriteSheet ReticleMedium { get; private set; }
public SpriteSheet ReticleSmall { get; private set; }
public Sprite MapCircle { get; private set; }
public Sprite LocationIndicator { get; private set; }
#endif
public List<Sprite> BackgroundTileSprites { get; private set; }
public string Name
{
get { return GetType().ToString(); }
}
public Dictionary<string, SerializableProperty> SerializableProperties
{
get; private set;
}
public static void Init()
{
var files = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, 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; }
instance?.ConnectionSprite?.Remove();
instance?.BackgroundTileSprites.ForEach(s => s.Remove());
#if CLIENT
instance?.MapCircle?.Remove();
instance?.LocationIndicator?.Remove();
instance?.DecorativeMapSprite?.Remove();
instance?.DecorativeGraphSprite?.Remove();
instance?.DecorativeLineTop?.Remove();
instance?.DecorativeLineBottom?.Remove();
instance?.DecorativeLineCorner?.Remove();
instance?.ReticleLarge?.Remove();
instance?.ReticleMedium?.Remove();
instance?.ReticleSmall?.Remove();
#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)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
BackgroundTileSprites = new List<Sprite>();
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "connectionsprite":
ConnectionSprite = new Sprite(subElement);
break;
case "backgroundtile":
BackgroundTileSprites.Add(new Sprite(subElement));
break;
#if CLIENT
case "mapcircle":
MapCircle = new Sprite(subElement);
break;
case "locationindicator":
LocationIndicator = new Sprite(subElement);
break;
case "decorativemapsprite":
DecorativeMapSprite = new SpriteSheet(subElement);
break;
case "decorativegraphsprite":
DecorativeGraphSprite = new SpriteSheet(subElement);
break;
case "decorativelinetop":
DecorativeLineTop = new SpriteSheet(subElement);
break;
case "decorativelinebottom":
DecorativeLineBottom = new SpriteSheet(subElement);
break;
case "decorativelinecorner":
DecorativeLineCorner = new SpriteSheet(subElement);
break;
case "reticlelarge":
ReticleLarge = new SpriteSheet(subElement);
break;
case "reticlemedium":
ReticleMedium = new SpriteSheet(subElement);
break;
case "reticlesmall":
ReticleSmall = new SpriteSheet(subElement);
break;
#endif
}
}
}
}
}
@@ -0,0 +1,593 @@
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
abstract partial class MapEntity : Entity
{
public static List<MapEntity> mapEntityList = new List<MapEntity>();
public readonly MapEntityPrefab prefab;
protected List<ushort> linkedToID;
//observable collection because some entities may need to be notified when the collection is modified
public readonly ObservableCollection<MapEntity> linkedTo = new ObservableCollection<MapEntity>();
private bool flippedX, flippedY;
public bool FlippedX { get { return flippedX; } }
public bool FlippedY { get { return flippedY; } }
public bool ShouldBeSaved = true;
//the position and dimensions of the entity
protected Rectangle rect;
public bool ExternalHighlight = false;
//is the mouse inside the rect
private bool isHighlighted;
public event Action<Rectangle> Resized;
public bool IsHighlighted
{
get { return isHighlighted || ExternalHighlight; }
set { isHighlighted = value; }
}
public virtual Rectangle Rect
{
get { return rect; }
set { rect = value; }
}
public Rectangle WorldRect
{
get { return Submarine == null ? rect : new Rectangle((int)(Submarine.Position.X + rect.X), (int)(Submarine.Position.Y + rect.Y), rect.Width, rect.Height); }
}
public virtual Sprite Sprite
{
get { return null; }
}
public virtual bool DrawBelowWater
{
get
{
return Sprite != null && SpriteDepth > 0.5f;
}
}
public virtual bool DrawOverWater
{
get
{
return !DrawBelowWater;
}
}
public virtual bool Linkable
{
get { return false; }
}
public List<string> AllowedLinks => prefab == null ? new List<string>() : prefab.AllowedLinks;
public bool ResizeHorizontal
{
get { return prefab != null && prefab.ResizeHorizontal; }
}
public bool ResizeVertical
{
get { return prefab != null && prefab.ResizeVertical; }
}
//for upgrading the dimensions of the entity from xml
[Serialize(0, false)]
public int RectWidth
{
get { return rect.Width; }
set
{
if (value <= 0) { return; }
Rect = new Rectangle(rect.X, rect.Y, value, rect.Height);
}
}
//for upgrading the dimensions of the entity from xml
[Serialize(0, false)]
public int RectHeight
{
get { return rect.Height; }
set
{
if (value <= 0) { return; }
Rect = new Rectangle(rect.X, rect.Y, rect.Width, value);
}
}
public override Vector2 Position
{
get
{
Vector2 rectPos = new Vector2(
rect.X + rect.Width / 2.0f,
rect.Y - rect.Height / 2.0f);
//if (MoveWithLevel) rectPos += Level.Loaded.Position;
return rectPos;
}
}
public override Vector2 SimPosition
{
get
{
return ConvertUnits.ToSimUnits(Position);
}
}
public float SoundRange
{
get
{
if (aiTarget == null) return 0.0f;
return aiTarget.SoundRange;
}
set
{
if (aiTarget == null) return;
aiTarget.SoundRange = value;
}
}
public float SightRange
{
get
{
if (aiTarget == null) return 0.0f;
return aiTarget.SightRange;
}
set
{
if (aiTarget == null) return;
aiTarget.SightRange = value;
}
}
public RuinGeneration.Ruin ParentRuin
{
get;
set;
}
public virtual string Name
{
get { return ""; }
}
// Quick undo/redo for size and movement only. TODO: Remove if we do a more general implementation.
private Memento<Rectangle> rectMemento;
public MapEntity(MapEntityPrefab prefab, Submarine submarine) : base(submarine)
{
this.prefab = prefab;
Scale = prefab != null ? prefab.Scale : 1;
}
public virtual void Move(Vector2 amount)
{
rect.X += (int)amount.X;
rect.Y += (int)amount.Y;
}
public virtual bool IsMouseOn(Vector2 position)
{
return (Submarine.RectContains(WorldRect, position));
}
public abstract MapEntity Clone();
public static List<MapEntity> Clone(List<MapEntity> entitiesToClone)
{
List<MapEntity> clones = new List<MapEntity>();
foreach (MapEntity e in entitiesToClone)
{
Debug.Assert(e != null);
try
{
clones.Add(e.Clone());
}
catch (Exception ex)
{
DebugConsole.ThrowError("Cloning entity \"" + e.Name + "\" failed.", ex);
GameAnalyticsManager.AddErrorEventOnce(
"MapEntity.Clone:" + e.Name,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Cloning entity \"" + e.Name + "\" failed (" + ex.Message + ").\n" + ex.StackTrace);
return clones;
}
Debug.Assert(clones.Last() != null);
}
Debug.Assert(clones.Count == entitiesToClone.Count);
//clone links between the entities
for (int i = 0; i < clones.Count; i++)
{
if (entitiesToClone[i].linkedTo == null) { continue; }
foreach (MapEntity linked in entitiesToClone[i].linkedTo)
{
if (!entitiesToClone.Contains(linked)) { continue; }
clones[i].linkedTo.Add(clones[entitiesToClone.IndexOf(linked)]);
}
}
//connect clone wires to the clone items and refresh links between doors and gaps
for (int i = 0; i < clones.Count; i++)
{
var cloneItem = clones[i] as Item;
if (cloneItem == null) { continue; }
var door = cloneItem.GetComponent<Door>();
if (door != null) { door.RefreshLinkedGap(); }
var cloneWire = cloneItem.GetComponent<Wire>();
if (cloneWire == null) continue;
var originalWire = ((Item)entitiesToClone[i]).GetComponent<Wire>();
cloneWire.SetNodes(originalWire.GetNodes());
for (int n = 0; n < 2; n++)
{
if (originalWire.Connections[n] == null) { continue; }
var connectedItem = originalWire.Connections[n].Item;
if (connectedItem == null) continue;
//index of the item the wire is connected to
int itemIndex = entitiesToClone.IndexOf(connectedItem);
if (itemIndex < 0)
{
DebugConsole.ThrowError("Error while cloning wires - item \"" + connectedItem.Name + "\" was not found in entities to clone.");
GameAnalyticsManager.AddErrorEventOnce("MapEntity.Clone:ConnectedNotFound" + connectedItem.ID,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Error while cloning wires - item \"" + connectedItem.Name + "\" was not found in entities to clone.");
continue;
}
//index of the connection in the connectionpanel of the target item
int connectionIndex = connectedItem.Connections.IndexOf(originalWire.Connections[n]);
if (connectionIndex < 0)
{
DebugConsole.ThrowError("Error while cloning wires - connection \"" + originalWire.Connections[n].Name + "\" was not found in connected item \"" + connectedItem.Name + "\".");
GameAnalyticsManager.AddErrorEventOnce("MapEntity.Clone:ConnectionNotFound" + connectedItem.ID,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Error while cloning wires - connection \"" + originalWire.Connections[n].Name + "\" was not found in connected item \"" + connectedItem.Name + "\".");
continue;
}
(clones[itemIndex] as Item).Connections[connectionIndex].TryAddLink(cloneWire);
cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], false);
}
}
return clones;
}
protected void InsertToList()
{
int i = 0;
if (Sprite == null)
{
mapEntityList.Add(this);
return;
}
while (i < mapEntityList.Count)
{
i++;
Sprite existingSprite = mapEntityList[i - 1].Sprite;
if (existingSprite == null) continue;
#if CLIENT
if (existingSprite.Texture == this.Sprite.Texture) break;
#endif
}
mapEntityList.Insert(i, this);
}
/// <summary>
/// Remove the entity from the entity list without removing links to other entities
/// </summary>
public virtual void ShallowRemove()
{
base.Remove();
mapEntityList.Remove(this);
if (aiTarget != null) aiTarget.Remove();
}
public override void Remove()
{
base.Remove();
mapEntityList.Remove(this);
#if CLIENT
if (selectedList.Contains(this))
{
selectedList = selectedList.FindAll(e => e != this);
}
#endif
if (aiTarget != null) aiTarget.Remove();
if (linkedTo != null)
{
for (int i = linkedTo.Count - 1; i >= 0; i--)
{
linkedTo[i].RemoveLinked(this);
}
linkedTo.Clear();
}
}
/// <summary>
/// Call Update() on every object in Entity.list
/// </summary>
public static void UpdateAll(float deltaTime, Camera cam)
{
foreach (Hull hull in Hull.hullList)
{
hull.Update(deltaTime, cam);
}
foreach (Structure structure in Structure.WallList)
{
structure.Update(deltaTime, cam);
}
//update gaps in random order, because otherwise in rooms with multiple gaps
//the water/air will always tend to flow through the first gap in the list,
//which may lead to weird behavior like water draining down only through
//one gap in a room even if there are several
foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue)))
{
gap.Update(deltaTime, cam);
}
Powered.UpdatePower(deltaTime);
foreach (Item item in Item.ItemList)
{
item.Update(deltaTime, cam);
}
UpdateAllProjSpecific(deltaTime);
Spawner?.Update();
}
static partial void UpdateAllProjSpecific(float deltaTime);
public virtual void Update(float deltaTime, Camera cam) { }
/// <summary>
/// Flip the entity horizontally
/// </summary>
/// <param name="relativeToSub">Should the entity be flipped across the y-axis of the sub it's inside</param>
public virtual void FlipX(bool relativeToSub)
{
flippedX = !flippedX;
if (!relativeToSub || Submarine == null) return;
Vector2 relative = WorldPosition - Submarine.WorldPosition;
relative.Y = 0.0f;
Move(-relative * 2.0f);
}
/// <summary>
/// Flip the entity vertically
/// </summary>
/// <param name="relativeToSub">Should the entity be flipped across the x-axis of the sub it's inside</param>
public virtual void FlipY(bool relativeToSub)
{
flippedY = !flippedY;
if (!relativeToSub || Submarine == null) return;
Vector2 relative = WorldPosition - Submarine.WorldPosition;
relative.X = 0.0f;
Move(-relative * 2.0f);
}
public static List<MapEntity> LoadAll(Submarine submarine, XElement parentElement, string filePath)
{
List<MapEntity> entities = new List<MapEntity>();
foreach (XElement element in parentElement.Elements())
{
string typeName = element.Name.ToString();
Type t;
try
{
t = Type.GetType("Barotrauma." + typeName, true, true);
if (t == null)
{
DebugConsole.ThrowError("Error in " + filePath + "! Could not find a entity of the type \"" + typeName + "\".");
continue;
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Error in " + filePath + "! Could not find a entity of the type \"" + typeName + "\".", e);
continue;
}
try
{
MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine) });
if (loadMethod == null)
{
DebugConsole.ThrowError("Could not find the method \"Load\" in " + t + ".");
}
else if (!loadMethod.ReturnType.IsSubclassOf(typeof(MapEntity)))
{
DebugConsole.ThrowError("Error loading entity of the type \"" + t.ToString() + "\" - load method does not return a valid map entity.");
}
else
{
object newEntity = loadMethod.Invoke(t, new object[] { element, submarine });
if (newEntity != null) entities.Add((MapEntity)newEntity);
}
}
catch (TargetInvocationException e)
{
DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e.InnerException);
}
catch (Exception e)
{
DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e);
}
}
return entities;
}
/// <summary>
/// Update the linkedTo-lists of the entities based on the linkedToID-lists
/// Has to be done after all the entities have been loaded (an entity can't
/// be linked to some other entity that hasn't been loaded yet)
/// </summary>
private bool mapLoadedCalled;
public static void MapLoaded(List<MapEntity> entities, bool updateHulls)
{
foreach (MapEntity e in entities)
{
if (e.mapLoadedCalled) continue;
if (e.linkedToID == null) continue;
if (e.linkedToID.Count == 0) continue;
e.linkedTo.Clear();
foreach (ushort i in e.linkedToID)
{
if (FindEntityByID(i) is MapEntity linked) e.linkedTo.Add(linked);
}
}
List<LinkedSubmarine> linkedSubs = new List<LinkedSubmarine>();
for (int i = 0; i < entities.Count; i++)
{
if (entities[i].mapLoadedCalled) continue;
if (entities[i] is LinkedSubmarine)
{
linkedSubs.Add((LinkedSubmarine)entities[i]);
continue;
}
entities[i].OnMapLoaded();
}
if (updateHulls)
{
Item.UpdateHulls();
Gap.UpdateHulls();
}
entities.ForEach(e => e.mapLoadedCalled = true);
foreach (LinkedSubmarine linkedSub in linkedSubs)
{
linkedSub.OnMapLoaded();
}
}
public virtual void OnMapLoaded() { }
public virtual XElement Save(XElement parentElement)
{
DebugConsole.ThrowError("Saving entity " + GetType() + " failed.");
return null;
}
public void RemoveLinked(MapEntity e)
{
if (linkedTo == null) return;
if (linkedTo.Contains(e)) linkedTo.Remove(e);
}
/// <summary>
/// Gets all linked entities of specific type.
/// </summary>
public HashSet<T> GetLinkedEntities<T>(HashSet<T> list = null, int? maxDepth = null, Func<T, bool> filter = null) where T : MapEntity
{
list = list ?? new HashSet<T>();
int startDepth = 0;
GetLinkedEntitiesRecursive<T>(this, list, ref startDepth, maxDepth, filter);
return list;
}
/// <summary>
/// Gets all linked entities of specific type.
/// </summary>
private static void GetLinkedEntitiesRecursive<T>(MapEntity mapEntity, HashSet<T> linkedTargets, ref int depth, int? maxDepth = null, Func<T, bool> filter = null)
where T : MapEntity
{
if (depth > maxDepth) { return; }
foreach (var linkedEntity in mapEntity.linkedTo)
{
if (linkedEntity is T linkedTarget)
{
if (!linkedTargets.Contains(linkedTarget) && (filter == null || filter(linkedTarget)))
{
linkedTargets.Add(linkedTarget);
depth++;
GetLinkedEntitiesRecursive(linkedEntity, linkedTargets, ref depth, maxDepth, filter);
}
}
}
}
#region Serialized properties
// We could use NaN or nullables, but in this case the first is not preferable, because it needs to be checked every time the value is used.
// Nullable on the other requires boxing that we don't want to do too often, since it generates garbage.
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)]
public float SpriteDepth
{
get
{
if (SpriteDepthOverrideIsSet) { return _spriteOverrideDepth; }
return Sprite != null ? Sprite.Depth : 0;
}
set
{
if (!float.IsNaN(value))
{
_spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999f);
if (this is Item) { _spriteOverrideDepth = Math.Min(_spriteOverrideDepth, 0.9f); }
SpriteDepthOverrideIsSet = true;
}
}
}
[Serialize(1f, true), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)]
public virtual float Scale { get; set; } = 1;
#endregion
}
}
@@ -0,0 +1,309 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
[Flags]
enum MapEntityCategory
{
Structure = 1, Decorative = 2, Machine = 4, Equipment = 8, Electrical = 16, Material = 32, Misc = 64, Alien = 128, ItemAssembly = 256, Legacy = 512
}
abstract partial class MapEntityPrefab : IPrefab, IDisposable
{
public static IEnumerable<MapEntityPrefab> List
{
get
{
foreach (var ep in CoreEntityPrefab.Prefabs)
{
yield return ep;
}
foreach (var ep in StructurePrefab.Prefabs)
{
yield return ep;
}
foreach (var ep in ItemPrefab.Prefabs)
{
yield return ep;
}
foreach (var ep in ItemAssemblyPrefab.Prefabs)
{
yield return ep;
}
}
}
protected string originalName;
protected string identifier;
public Sprite sprite;
//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, false)]
public bool HideInMenus { 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;
}
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)
{
placePosition = Vector2.Zero;
return true;
}
else
{
return false;
}
}
/// <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>
public static MapEntityPrefab Find(string name, string identifier = null, bool showErrorMessages = true)
{
if (name != null)
{
name = name.ToLowerInvariant();
}
foreach (MapEntityPrefab prefab in List)
{
if (identifier != null)
{
if (prefab.identifier != identifier)
{
continue;
}
else
{
if (string.IsNullOrEmpty(name)) { return prefab; }
}
}
if (!string.IsNullOrEmpty(name))
{
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;
}
}
}
if (showErrorMessages)
{
DebugConsole.ThrowError("Failed to find a matching MapEntityPrefab (name: \"" + name + "\", identifier: \"" + identifier + "\").\n" + Environment.StackTrace);
}
return null;
}
/// <summary>
/// Find a matching map entity prefab
/// </summary>
/// <param name="predicate">A predicate that returns true on the desired prefab.</param>
public static MapEntityPrefab Find(Predicate<MapEntityPrefab> predicate)
{
return List.FirstOrDefault(p => predicate(p));
}
/// <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(IEnumerable<string> allowedNames, StringComparison comparisonType) => allowedNames.Any(n => NameMatches(n, comparisonType));
public bool IsLinkAllowed(MapEntityPrefab target)
{
if (target == null) { return false; }
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;
}
}
}
@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.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()
{
if (!File.Exists(cachePath)) { return; }
string[] lines = File.ReadAllLines(cachePath);
if (lines.Length <= 0 || lines[0] != GameMain.Version.ToString()) { return; }
foreach (string line in lines.Skip(1))
{
if (string.IsNullOrWhiteSpace(line)) { continue; }
string[] parts = line.Split('|');
if (parts.Length < 3) { continue; }
string path = parts[0].CleanUpPath();
string hashStr = parts[1];
long timeLong = long.Parse(parts[2]);
Md5Hash hash = new Md5Hash(hashStr);
DateTime time = DateTime.FromBinary(timeLong);
if (File.GetLastWriteTime(path) == time && !cache.ContainsKey(path))
{
cache.Add(path, new Tuple<Md5Hash, long>(hash, timeLong));
}
}
}
public static void SaveCache()
{
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);
}
private bool LoadFromCache(string filename)
{
if (!string.IsNullOrWhiteSpace(filename))
{
filename = filename.CleanUpPath();
lock (cache)
{
if (cache.ContainsKey(filename))
{
Hash = cache[filename].Item1.Hash;
ShortHash = cache[filename].Item1.ShortHash;
return true;
}
}
}
return false;
}
public void SaveToCache(string filename, long? time = null)
{
if (string.IsNullOrWhiteSpace(filename)) { return; }
lock (cache)
{
filename = filename.CleanUpPath();
Tuple<Md5Hash, long> cacheVal = new Tuple<Md5Hash, long>(this, time ?? File.GetLastWriteTime(filename).ToBinary());
if (cache.ContainsKey(filename))
{
cache[filename] = cacheVal;
}
else
{
cache.Add(filename, cacheVal);
}
SaveCache();
}
}
public static Md5Hash FetchFromCache(string filename)
{
Md5Hash newHash = new Md5Hash();
if (newHash.LoadFromCache(filename)) { return newHash; }
return null;
}
public string Hash { get; private set; }
public string ShortHash { get; private set; }
private Md5Hash()
{
this.Hash = null;
ShortHash = null;
}
public Md5Hash(string md5Hash)
{
this.Hash = md5Hash;
ShortHash = GetShortHash(md5Hash);
}
public Md5Hash(byte[] bytes)
{
Hash = CalculateHash(bytes);
ShortHash = GetShortHash(Hash);
}
public Md5Hash(FileStream fileStream, string filename = null, bool tryLoadFromCache = true)
{
if (tryLoadFromCache)
{
if (LoadFromCache(filename)) { return; }
}
Hash = CalculateHash(fileStream);
ShortHash = GetShortHash(Hash);
SaveToCache(filename);
}
public Md5Hash(XDocument doc, string filename = null, bool tryLoadFromCache = true)
{
if (tryLoadFromCache)
{
if (LoadFromCache(filename)) { return; }
}
if (doc == null) { return; }
string docString = removeWhitespaceRegex.Replace(doc.ToString(), "");
byte[] inputBytes = Encoding.ASCII.GetBytes(docString);
Hash = CalculateHash(inputBytes);
ShortHash = GetShortHash(Hash);
SaveToCache(filename);
}
public override string ToString()
{
return Hash;
}
private string CalculateHash(FileStream stream)
{
using (MD5 md5 = MD5.Create())
{
byte[] byteHash = md5.ComputeHash(stream);
// step 2, convert byte array to hex string
StringBuilder sb = new StringBuilder();
for (int i = 0; i < byteHash.Length; i++)
{
sb.Append(byteHash[i].ToString("X2"));
}
return sb.ToString();
}
}
private string CalculateHash(byte[] bytes)
{
using (MD5 md5 = MD5.Create())
{
byte[] byteHash = md5.ComputeHash(bytes);
// step 2, convert byte array to hex string
StringBuilder sb = new StringBuilder();
for (int i = 0; i < byteHash.Length; i++)
{
sb.Append(byteHash[i].ToString("X2"));
}
return sb.ToString();
}
}
public static string GetShortHash(string fullHash)
{
if (string.IsNullOrEmpty(fullHash)) { return ""; }
return fullHash.Length < 7 ? fullHash : fullHash.Substring(0, 7);
}
public static bool RemoveFromCache(string filename)
{
if (!string.IsNullOrWhiteSpace(filename))
{
filename = filename.CleanUpPath();
lock (cache)
{
if (cache.ContainsKey(filename))
{
cache.Remove(filename);
return true;
}
}
}
return false;
}
}
}
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Linq;
namespace Barotrauma
{
class PriceInfo
{
public readonly int BuyPrice;
//minimum number of items available at a given store
public readonly int MinAvailableAmount;
//maximum number of items available at a given store
public readonly int MaxAvailableAmount;
public PriceInfo (XElement element)
{
BuyPrice = element.GetAttributeInt("buyprice", 0);
MinAvailableAmount = element.GetAttributeInt("minamount", 0);
MaxAvailableAmount = element.GetAttributeInt("maxamount", 0);
}
}
}
@@ -0,0 +1,117 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class RoundEndCinematic
{
public bool Running
{
get;
private set;
}
public Camera AssignedCamera;
private float duration;
private CoroutineHandle updateCoroutine;
public RoundEndCinematic(Submarine submarine, Camera cam, float duration = 10.0f)
: this(new List<Submarine>() { submarine }, cam, duration)
{
}
public RoundEndCinematic(List<Submarine> submarines, Camera cam, float duration)
{
if (!submarines.Any(s => s != null)) return;
this.duration = duration;
AssignedCamera = cam;
Running = true;
updateCoroutine = CoroutineManager.StartCoroutine(Update(submarines, cam));
}
public void Stop()
{
CoroutineManager.StopCoroutines(updateCoroutine);
Running = false;
#if CLIENT
GUI.ScreenOverlayColor = Color.TransparentBlack;
#endif
}
private IEnumerable<object> Update(List<Submarine> subs, Camera cam)
{
if (!subs.Any()) yield return CoroutineStatus.Success;
Character.Controlled = null;
cam.TargetPos = Vector2.Zero;
#if CLIENT
GameMain.LightManager.LosEnabled = false;
#endif
Level.Loaded.TopBarrier.Enabled = false;
cam.TargetPos = Vector2.Zero;
float timer = 0.0f;
float initialZoom = cam.Zoom;
Vector2 initialCameraPos = cam.Position;
while (timer < duration)
{
if (Screen.Selected != GameMain.GameScreen)
{
yield return new WaitForSeconds(0.1f);
#if CLIENT
GUI.ScreenOverlayColor = Color.TransparentBlack;
#endif
Running = false;
yield return CoroutineStatus.Success;
}
Vector2 minPos = new Vector2(
subs.Min(s => s.WorldPosition.X - s.Borders.Width / 2),
subs.Min(s => s.WorldPosition.Y - s.Borders.Height / 2));
Vector2 maxPos = new Vector2(
subs.Min(s => s.WorldPosition.X + s.Borders.Width / 2),
subs.Min(s => s.WorldPosition.Y + s.Borders.Height / 2));
Vector2 cameraPos = new Vector2(
MathHelper.SmoothStep(minPos.X, maxPos.X, timer / duration),
(minPos.Y + maxPos.Y) / 2.0f);
cam.Translate(cameraPos - cam.Position);
foreach (Submarine sub in subs)
{
sub.PhysicsBody?.ResetDynamics();
}
#if CLIENT
cam.Zoom = MathHelper.SmoothStep(initialZoom, 0.5f, timer / duration);
if (timer / duration > 0.9f)
{
GUI.ScreenOverlayColor = Color.Lerp(Color.TransparentBlack, Color.Black, ((timer / duration) - 0.9f) * 10.0f);
}
#endif
timer += CoroutineManager.UnscaledDeltaTime;
yield return CoroutineStatus.Running;
}
Running = false;
yield return new WaitForSeconds(0.1f);
#if CLIENT
GUI.ScreenOverlayColor = Color.TransparentBlack;
#endif
yield return CoroutineStatus.Success;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,354 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Xml.Linq;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
#endif
namespace Barotrauma
{
partial class StructurePrefab : MapEntityPrefab
{
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);
}
private string name;
public override string Name
{
get { return name; }
}
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(100.0f, false)]
public float Health
{
get { return health; }
set { health = Math.Max(value, 0.0f); }
}
[Serialize(false, false)]
public bool CastShadow
{
get;
private set;
}
/// <summary>
/// If null, the orientation is determined automatically based on the dimensions of the structure instances
/// </summary>
public bool? IsHorizontal
{
get;
private set;
}
[Serialize(Direction.None, false)]
public Direction StairDirection
{
get;
private set;
}
[Serialize(45.0f, false)]
public float StairAngle
{
get;
private set;
}
[Serialize(false, false)]
public bool NoAITarget
{
get;
private set;
}
public bool CanSpriteFlipX
{
get { return canSpriteFlipX; }
}
public bool CanSpriteFlipY
{
get { return canSpriteFlipY; }
}
[Serialize("0,0", true)]
public Vector2 Size
{
get { return size; }
private set { size = value; }
}
public Vector2 ScaledSize => size * Scale;
protected Vector2 textureScale = Vector2.One;
[Editable(DecimalCount = 3), Serialize("1.0, 1.0", true)]
public Vector2 TextureScale
{
get { return textureScale; }
set
{
textureScale = new Vector2(
MathHelper.Clamp(value.X, 0.01f, 10),
MathHelper.Clamp(value.Y, 0.01f, 10));
}
}
public Sprite BackgroundSprite
{
get;
private set;
}
public static void LoadAll(IEnumerable<ContentFile> files)
{
foreach (ContentFile file in files)
{
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())
{
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);
}
}
}
}
public static void RemoveByFile(string filePath)
{
Prefabs.RemoveByFile(filePath);
}
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", "");
if (string.IsNullOrEmpty(sp.name))
{
sp.name = TextManager.Get("EntityName." + sp.identifier, returnNull: true) ?? $"Not defined ({sp.identifier})";
}
sp.Tags = new HashSet<string>();
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());
}
if (element.Attribute("ishorizontal") != null)
{
sp.IsHorizontal = element.GetAttributeBool("ishorizontal", false);
}
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString())
{
case "sprite":
sp.sprite = new Sprite(subElement, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null)
{
DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + sp.name + "\"!");
}
#if CLIENT
if (subElement.GetAttributeBool("fliphorizontal", false))
sp.sprite.effects = SpriteEffects.FlipHorizontally;
if (subElement.GetAttributeBool("flipvertical", false))
sp.sprite.effects = SpriteEffects.FlipVertically;
#endif
sp.canSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
sp.canSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
if (subElement.Attribute("name") == null && !string.IsNullOrWhiteSpace(sp.Name))
{
sp.sprite.Name = sp.Name;
}
sp.sprite.EntityID = sp.identifier;
break;
case "backgroundsprite":
sp.BackgroundSprite = new Sprite(subElement, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null && sp.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));
}
#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);
#endif
break;
}
}
if (!Enum.TryParse(element.GetAttributeString("category", "Structure"), true, out MapEntityCategory category))
{
category = MapEntityCategory.Structure;
}
sp.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();
string nonTranslatedName = element.GetAttributeString("name", null) ?? element.Name.ToString();
sp.Aliases.Add(nonTranslatedName.ToLowerInvariant());
SerializableProperty.DeserializeProperties(sp, element);
if (sp.Body)
{
sp.Tags.Add("wall");
}
if (string.IsNullOrEmpty(sp.Description))
{
sp.Description = TextManager.Get("EntityDescription." + sp.identifier, returnNull: true) ?? string.Empty;
}
//backwards compatibility
if (element.Attribute("size") == null)
{
sp.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;
}
else
{
sp.size.X = element.GetAttributeFloat("width", 0.0f);
sp.size.Y = element.GetAttributeFloat("height", 0.0f);
}
}
if (string.IsNullOrEmpty(sp.identifier))
{
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.");
}
Prefabs.Add(sp, allowOverride);
return sp;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,847 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Collision;
using FarseerPhysics.Common;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Voronoi2;
namespace Barotrauma
{
partial class SubmarineBody
{
public const float NeutralBallastPercentage = 0.07f;
const float HorizontalDrag = 0.01f;
const float VerticalDrag = 0.05f;
const float MaxDrag = 0.1f;
public const float DamageDepth = -30000.0f;
private const float ImpactDamageMultiplier = 10.0f;
//limbs with a mass smaller than this won't cause an impact when they hit the sub
private const float MinImpactLimbMass = 10.0f;
//impacts smaller than this are ignored
private const float MinCollisionImpact = 3.0f;
//impacts are clamped below this value
private const float MaxCollisionImpact = 5.0f;
private const float Friction = 0.2f, Restitution = 0.0f;
public List<Vector2> HullVertices
{
get;
private set;
}
private float depthDamageTimer;
private readonly Submarine submarine;
public readonly PhysicsBody Body;
private readonly List<PosInfo> positionBuffer = new List<PosInfo>();
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
struct Impact
{
public Fixture Target;
public Vector2 Velocity;
public Vector2 ImpactPos;
public Vector2 Normal;
public Impact(Fixture f1, Fixture f2, Contact contact)
{
Target = f2;
contact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> points);
if (contact.FixtureA.Body == f1.Body) { contactNormal = -contactNormal; }
ImpactPos = points[0];
Normal = contactNormal;
Velocity = f1.Body.LinearVelocity - f2.Body.LinearVelocity;
}
}
public Rectangle Borders
{
get;
private set;
}
public Vector2 Velocity
{
get { return Body.LinearVelocity; }
set
{
if (!MathUtils.IsValid(value)) return;
Body.LinearVelocity = value;
}
}
public Vector2 Position
{
get { return ConvertUnits.ToDisplayUnits(Body.SimPosition); }
}
public List<PosInfo> PositionBuffer
{
get { return positionBuffer; }
}
public bool AtDamageDepth
{
get { return Position.Y < DamageDepth; }
}
public Submarine Submarine
{
get { return submarine; }
}
public SubmarineBody(Submarine sub, bool showWarningMessages = true)
{
this.submarine = sub;
Body farseerBody = null;
if (!Hull.hullList.Any())
{
farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f);
if (showWarningMessages)
{
DebugConsole.ThrowError("WARNING: no hulls found, generating a physics body for the submarine failed.");
}
}
else
{
List<Vector2> convexHull = GenerateConvexHull();
for (int i = 0; i < convexHull.Count; i++)
{
convexHull[i] = ConvertUnits.ToSimUnits(convexHull[i]);
}
HullVertices = convexHull;
Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero;
farseerBody = GameMain.World.CreateBody();
farseerBody.UserData = this;
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine != submarine) continue;
Rectangle rect = wall.Rect;
farseerBody.CreateRectangle(
ConvertUnits.ToSimUnits(wall.BodyWidth),
ConvertUnits.ToSimUnits(wall.BodyHeight),
50.0f,
-wall.BodyRotation,
ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset)).UserData = wall;
minExtents.X = Math.Min(rect.X, minExtents.X);
minExtents.Y = Math.Min(rect.Y - rect.Height, minExtents.Y);
maxExtents.X = Math.Max(rect.Right, maxExtents.X);
maxExtents.Y = Math.Max(rect.Y, maxExtents.Y);
}
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine != submarine) continue;
Rectangle rect = hull.Rect;
farseerBody.CreateRectangle(
ConvertUnits.ToSimUnits(rect.Width),
ConvertUnits.ToSimUnits(rect.Height),
100.0f,
ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2))).UserData = hull;
minExtents.X = Math.Min(rect.X, minExtents.X);
minExtents.Y = Math.Min(rect.Y - rect.Height, minExtents.Y);
maxExtents.X = Math.Max(rect.Right, maxExtents.X);
maxExtents.Y = Math.Max(rect.Y, maxExtents.Y);
}
foreach (Item item in Item.ItemList)
{
if (item.StaticBodyConfig == null || item.Submarine != submarine) continue;
float radius = item.StaticBodyConfig.GetAttributeFloat("radius", 0.0f) * item.Scale;
float width = item.StaticBodyConfig.GetAttributeFloat("width", 0.0f) * item.Scale;
float height = item.StaticBodyConfig.GetAttributeFloat("height", 0.0f) * item.Scale;
Vector2 simPos = ConvertUnits.ToSimUnits(item.Position);
float simRadius = ConvertUnits.ToSimUnits(radius);
float simWidth = ConvertUnits.ToSimUnits(width);
float simHeight = ConvertUnits.ToSimUnits(height);
if (width > 0.0f && height > 0.0f)
{
farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos).UserData = item;
minExtents.X = Math.Min(item.Position.X - width / 2, minExtents.X);
minExtents.Y = Math.Min(item.Position.Y - height / 2, minExtents.Y);
maxExtents.X = Math.Max(item.Position.X + width / 2, maxExtents.X);
maxExtents.Y = Math.Max(item.Position.Y + height / 2, maxExtents.Y);
}
else if (radius > 0.0f && width > 0.0f)
{
farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos).UserData = item;
farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2).UserData = item;
farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2).UserData = item;
minExtents.X = Math.Min(item.Position.X - width / 2 - radius, minExtents.X);
minExtents.Y = Math.Min(item.Position.Y - radius, minExtents.Y);
maxExtents.X = Math.Max(item.Position.X + width / 2 + radius, maxExtents.X);
maxExtents.Y = Math.Max(item.Position.Y + radius, maxExtents.Y);
}
else if (radius > 0.0f && height > 0.0f)
{
farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos).UserData = item;
farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2).UserData = item;
farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2).UserData = item;
minExtents.X = Math.Min(item.Position.X - radius, minExtents.X);
minExtents.Y = Math.Min(item.Position.Y - height / 2 - radius, minExtents.Y);
maxExtents.X = Math.Max(item.Position.X + radius, maxExtents.X);
maxExtents.Y = Math.Max(item.Position.Y + height / 2 + radius, maxExtents.Y);
}
else if (radius > 0.0f)
{
farseerBody.CreateCircle(simRadius, 5.0f, simPos).UserData = item;
minExtents.X = Math.Min(item.Position.X - radius, minExtents.X);
minExtents.Y = Math.Min(item.Position.Y - radius, minExtents.Y);
maxExtents.X = Math.Max(item.Position.X + radius, maxExtents.X);
maxExtents.Y = Math.Max(item.Position.Y + radius, maxExtents.Y);
}
}
Borders = new Rectangle((int)minExtents.X, (int)maxExtents.Y, (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y));
}
farseerBody.BodyType = BodyType.Dynamic;
farseerBody.CollisionCategories = Physics.CollisionWall;
farseerBody.CollidesWith =
Physics.CollisionItem |
Physics.CollisionLevel |
Physics.CollisionCharacter |
Physics.CollisionProjectile |
Physics.CollisionWall;
farseerBody.Restitution = Restitution;
farseerBody.Friction = Friction;
farseerBody.FixedRotation = true;
farseerBody.Awake = true;
farseerBody.SleepingAllowed = false;
farseerBody.IgnoreGravity = true;
farseerBody.OnCollision += OnCollision;
farseerBody.UserData = submarine;
Body = new PhysicsBody(farseerBody);
}
private List<Vector2> GenerateConvexHull()
{
List<Structure> subWalls = Structure.WallList.FindAll(wall => wall.Submarine == submarine);
if (subWalls.Count == 0)
{
return new List<Vector2> { new Vector2(-1.0f, 1.0f), new Vector2(1.0f, 1.0f), new Vector2(0.0f, -1.0f) };
}
List<Vector2> points = new List<Vector2>();
foreach (Structure wall in subWalls)
{
points.Add(new Vector2(wall.Rect.X, wall.Rect.Y));
points.Add(new Vector2(wall.Rect.X + wall.Rect.Width, wall.Rect.Y));
points.Add(new Vector2(wall.Rect.X, wall.Rect.Y - wall.Rect.Height));
points.Add(new Vector2(wall.Rect.X + wall.Rect.Width, wall.Rect.Y - wall.Rect.Height));
}
List<Vector2> hullPoints = MathUtils.GiftWrap(points);
return hullPoints;
}
public void Update(float deltaTime)
{
while (impactQueue.Count > 0)
{
var impact = impactQueue.Dequeue();
if (impact.Target.UserData is VoronoiCell cell)
{
HandleLevelCollision(impact);
}
else if (impact.Target.Body.UserData is Structure)
{
HandleLevelCollision(impact);
}
else if (impact.Target.Body.UserData is Submarine otherSub)
{
HandleSubCollision(impact, otherSub);
}
else if (impact.Target.Body.UserData is Limb limb)
{
HandleLimbCollision(impact, limb);
}
}
//-------------------------
if (Body.FarseerBody.BodyType == BodyType.Static) { return; }
ClientUpdatePosition(deltaTime);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
Vector2 totalForce = CalculateBuoyancy();
//-------------------------
//if outside left or right edge of the level
if (Position.X < 0 || Position.X > Level.Loaded.Size.X)
{
Rectangle worldBorders = Borders;
worldBorders.Location += MathUtils.ToPoint(Position);
//push the sub back below the upper "barrier" of the level
if (worldBorders.Y > Level.Loaded.Size.Y)
{
Body.LinearVelocity = new Vector2(
Body.LinearVelocity.X,
Math.Min(Body.LinearVelocity.Y, ConvertUnits.ToSimUnits(Level.Loaded.Size.Y - worldBorders.Y)));
}
else if (worldBorders.Y - worldBorders.Height < Level.Loaded.BottomPos)
{
Body.LinearVelocity = new Vector2(
Body.LinearVelocity.X,
Math.Max(Body.LinearVelocity.Y, ConvertUnits.ToSimUnits(Level.Loaded.BottomPos - (worldBorders.Y - worldBorders.Height))));
}
if (Position.X < 0)
{
float force = Math.Abs(Position.X * 0.5f);
totalForce += Vector2.UnitX * force;
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(force * 0.0001f, 5.0f));
}
}
else
{
float force = (Position.X - Level.Loaded.Size.X) * 0.5f;
totalForce -= Vector2.UnitX * force;
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(force * 0.0001f, 5.0f));
}
}
}
//-------------------------
if (Body.LinearVelocity.LengthSquared() > 0.0001f)
{
//TODO: sync current drag with clients?
float attachedMass = 0.0f;
JointEdge jointEdge = Body.FarseerBody.JointList;
while (jointEdge != null)
{
Body otherBody = jointEdge.Joint.BodyA == Body.FarseerBody ? jointEdge.Joint.BodyB : jointEdge.Joint.BodyA;
Character character = (otherBody.UserData as Limb)?.character;
if (character != null) attachedMass += character.Mass;
jointEdge = jointEdge.Next;
}
float horizontalDragCoefficient = MathHelper.Clamp(HorizontalDrag + attachedMass / 5000.0f, 0.0f, MaxDrag);
totalForce.X -= Math.Sign(Body.LinearVelocity.X) * Body.LinearVelocity.X * Body.LinearVelocity.X * horizontalDragCoefficient * Body.Mass;
float verticalDragCoefficient = MathHelper.Clamp(VerticalDrag + attachedMass / 5000.0f, 0.0f, MaxDrag);
totalForce.Y -= Math.Sign(Body.LinearVelocity.Y) * Body.LinearVelocity.Y * Body.LinearVelocity.Y * verticalDragCoefficient * Body.Mass;
}
ApplyForce(totalForce);
UpdateDepthDamage(deltaTime);
}
partial void ClientUpdatePosition(float deltaTime);
/// <summary>
/// Moves away any character that is inside the bounding box of the sub (but not inside the sub)
/// </summary>
/// <param name="subTranslation">The translation that was applied to the sub before doing the displacement
/// (used for determining where to push the characters)</param>
private void DisplaceCharacters(Vector2 subTranslation)
{
Rectangle worldBorders = Borders;
worldBorders.Location += MathUtils.ToPoint(ConvertUnits.ToDisplayUnits(Body.SimPosition));
Vector2 translateDir = Vector2.Normalize(subTranslation);
if (!MathUtils.IsValid(translateDir)) translateDir = Vector2.UnitY;
foreach (Character c in Character.CharacterList)
{
if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine) continue;
foreach (Limb limb in c.AnimController.Limbs)
{
//if the character isn't inside the bounding box, continue
if (!Submarine.RectContains(worldBorders, limb.WorldPosition)) continue;
//cast a line from the position of the character to the same direction as the translation of the sub
//and see where it intersects with the bounding box
if (!MathUtils.GetLineRectangleIntersection(limb.WorldPosition,
limb.WorldPosition + translateDir * 100000.0f, worldBorders, out Vector2 intersection))
{
//should never happen when casting a line out from inside the bounding box
Debug.Assert(false);
continue;
}
//"+ translatedir" in order to move the character slightly away from the wall
c.AnimController.SetPosition(ConvertUnits.ToSimUnits(c.WorldPosition + (intersection - limb.WorldPosition)) + translateDir);
return;
}
}
}
private Vector2 CalculateBuoyancy()
{
float waterVolume = 0.0f;
float volume = 0.0f;
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine != submarine) continue;
waterVolume += hull.WaterVolume;
volume += hull.Volume;
}
float waterPercentage = volume <= 0.0f ? 0.0f : waterVolume / volume;
float buoyancy = NeutralBallastPercentage - waterPercentage;
if (buoyancy > 0.0f)
buoyancy *= 2.0f;
else
buoyancy = Math.Max(buoyancy, -0.5f);
return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f);
}
public void ApplyForce(Vector2 force)
{
Body.ApplyForce(force, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
public void SetPosition(Vector2 position)
{
Body.SetTransform(ConvertUnits.ToSimUnits(position), 0.0f);
}
private void UpdateDepthDamage(float deltaTime)
{
if (Position.Y > DamageDepth) { return; }
float depth = DamageDepth - Position.Y;
depthDamageTimer -= deltaTime;
if (depthDamageTimer > 0.0f) return;
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine != submarine) continue;
if (wall.Health < depth * 0.01f)
{
Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, depth * 0.01f);
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(depth * 0.001f, 50.0f));
}
}
}
depthDamageTimer = 10.0f;
}
public void FlipX()
{
List<Vector2> convexHull = GenerateConvexHull();
for (int i = 0; i < convexHull.Count; i++)
{
convexHull[i] = ConvertUnits.ToSimUnits(convexHull[i]);
}
HullVertices = convexHull;
}
public bool OnCollision(Fixture f1, Fixture f2, Contact contact)
{
if (f2.Body.UserData is Limb limb)
{
bool collision = CheckCharacterCollision(contact, limb.character);
if (collision)
{
lock (impactQueue)
{
impactQueue.Enqueue(new Impact(f1, f2, contact));
}
}
return collision;
}
if (f2.Body.UserData is Character character)
{
return CheckCharacterCollision(contact, character);
}
lock (impactQueue)
{
impactQueue.Enqueue(new Impact(f1, f2, contact));
}
return true;
}
private bool CheckCharacterCollision(Contact contact, Character character)
{
//characters that can't enter the sub always collide regardless of gaps
if (!character.AnimController.CanEnterSubmarine) { return true; }
if (character.Submarine != null) { return false; }
contact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> points);
Vector2 normalizedVel = character.AnimController.Collider.LinearVelocity == Vector2.Zero ?
Vector2.Zero : Vector2.Normalize(character.AnimController.Collider.LinearVelocity);
Vector2 targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal);
Hull newHull = Hull.FindHull(targetPos, null);
if (newHull == null)
{
targetPos = ConvertUnits.ToDisplayUnits(points[0] + normalizedVel);
newHull = Hull.FindHull(targetPos, null);
}
var gaps = newHull?.ConnectedGaps ?? Gap.GapList.Where(g => g.Submarine == submarine);
targetPos = character.WorldPosition;
Gap adjacentGap = Gap.FindAdjacent(gaps, targetPos, 500.0f);
if (adjacentGap == null) { return true; }
if (newHull != null)
{
CoroutineManager.Invoke(() =>
character.AnimController.FindHull(newHull.WorldPosition, true));
}
return false;
}
private void HandleLimbCollision(Impact collision, Limb limb)
{
if (limb?.body?.FarseerBody == null || limb.character == null) { return; }
if (limb.Mass > MinImpactLimbMass)
{
Vector2 normal =
Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ?
Vector2.UnitY :
Vector2.Normalize(Body.SimPosition - limb.SimPosition);
float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(limb.Mass / 100.0f, 1);
ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false);
foreach (Submarine dockedSub in submarine.DockedTo)
{
dockedSub.SubBody.ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false);
}
}
//find all contacts between the limb and level walls
List<Contact> levelContacts = new List<Contact>();
ContactEdge contactEdge = limb.body.FarseerBody.ContactList;
while (contactEdge?.Contact != null)
{
if (contactEdge.Contact.Enabled &&
contactEdge.Contact.IsTouching &&
contactEdge.Other?.UserData is VoronoiCell)
{
levelContacts.Add(contactEdge.Contact);
}
contactEdge = contactEdge.Next;
}
if (levelContacts.Count == 0) { return; }
//if the limb is in contact with the level, apply an artifical impact to prevent the sub from bouncing on top of it
//not a very realistic way to handle the collisions (makes it seem as if the characters were made of reinforced concrete),
//but more realistic than bouncing and prevents using characters as "bumpers" that prevent all collision damage
Vector2 avgContactNormal = Vector2.Zero;
foreach (Contact levelContact in levelContacts)
{
levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> temp);
//if the contact normal is pointing from the limb towards the level cell it's touching, flip the normal
VoronoiCell cell = levelContact.FixtureB.UserData is VoronoiCell ?
((VoronoiCell)levelContact.FixtureB.UserData) : ((VoronoiCell)levelContact.FixtureA.UserData);
var cellDiff = ConvertUnits.ToDisplayUnits(limb.body.SimPosition) - cell.Center;
if (Vector2.Dot(contactNormal, cellDiff) < 0)
{
contactNormal = -contactNormal;
}
avgContactNormal += contactNormal;
//apply impacts at the positions where this sub is touching the limb
ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContacts.Count, contactNormal, collision.ImpactPos, applyDamage: false);
}
avgContactNormal /= levelContacts.Count;
float contactDot = Vector2.Dot(Body.LinearVelocity, -avgContactNormal);
if (contactDot > 0.001f)
{
Vector2 velChange = Vector2.Normalize(Body.LinearVelocity) * contactDot;
if (!MathUtils.IsValid(velChange))
{
GameAnalyticsManager.AddErrorEventOnce(
"SubmarineBody.HandleLimbCollision:" + submarine.ID,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Invalid velocity change in SubmarineBody.HandleLimbCollision (submarine velocity: " + Body.LinearVelocity
+ ", avgContactNormal: " + avgContactNormal
+ ", contactDot: " + contactDot
+ ", velChange: " + velChange + ")");
return;
}
Body.LinearVelocity -= velChange;
float damageAmount = contactDot * Body.Mass / limb.character.Mass;
limb.character.LastDamageSource = submarine;
limb.character.DamageLimb(ConvertUnits.ToDisplayUnits(collision.ImpactPos), limb,
new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageAmount) }, 0.0f, true, 0.0f);
if (limb.character.IsDead)
{
foreach (LimbJoint limbJoint in limb.character.AnimController.LimbJoints)
{
if (limbJoint.IsSevered || (limbJoint.LimbA != limb && limbJoint.LimbB != limb)) continue;
limb.character.AnimController.SeverLimbJoint(limbJoint);
}
}
}
}
private void HandleLevelCollision(Impact impact)
{
float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal);
ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
foreach (Submarine dockedSub in submarine.DockedTo)
{
dockedSub.SubBody.ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
}
#if CLIENT
int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50);
for (int i = 0; i < particleAmount; i++)
{
GameMain.ParticleManager.CreateParticle("iceshards",
ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)),
Rand.Vector(Rand.Range(50.0f, 500.0f)) + impact.Velocity);
}
#endif
}
private void HandleSubCollision(Impact impact, Submarine otherSub)
{
Debug.Assert(otherSub != submarine);
Vector2 normal = impact.Normal;
if (impact.Target.Body == otherSub.SubBody.Body.FarseerBody)
{
normal = -normal;
}
float thisMass = Body.Mass + submarine.DockedTo.Sum(s => s.PhysicsBody.Mass);
float otherMass = otherSub.PhysicsBody.Mass + otherSub.DockedTo.Sum(s => s.PhysicsBody.Mass);
float massRatio = otherMass / (thisMass + otherMass);
float impulse = (Vector2.Dot(impact.Velocity, normal) / 2.0f) * massRatio;
//apply impact to this sub (the other sub takes care of this in its own collision callback)
ApplyImpact(impulse, normal, impact.ImpactPos);
foreach (Submarine dockedSub in submarine.DockedTo)
{
dockedSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
}
//find all contacts between this sub and level walls
List<Contact> levelContacts = new List<Contact>();
ContactEdge contactEdge = Body.FarseerBody.ContactList;
while (contactEdge.Next != null)
{
if (contactEdge.Contact.Enabled &&
contactEdge.Other.UserData is VoronoiCell &&
contactEdge.Contact.IsTouching)
{
levelContacts.Add(contactEdge.Contact);
}
contactEdge = contactEdge.Next;
}
if (levelContacts.Count == 0) return;
//if this sub is in contact with the level, apply artifical impacts
//to both subs to prevent the other sub from bouncing on top of this one
//and to fake the other sub "crushing" this one against a wall
Vector2 avgContactNormal = Vector2.Zero;
foreach (Contact levelContact in levelContacts)
{
levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> temp);
//if the contact normal is pointing from the sub towards the level cell we collided with, flip the normal
VoronoiCell cell = levelContact.FixtureB.UserData is VoronoiCell ?
((VoronoiCell)levelContact.FixtureB.UserData) : ((VoronoiCell)levelContact.FixtureA.UserData);
var cellDiff = ConvertUnits.ToDisplayUnits(Body.SimPosition) - cell.Center;
if (Vector2.Dot(contactNormal, cellDiff) < 0)
{
contactNormal = -contactNormal;
}
avgContactNormal += contactNormal;
//apply impacts at the positions where this sub is touching the level
ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContacts.Count, contactNormal, impact.ImpactPos);
}
avgContactNormal /= levelContacts.Count;
//apply an impact to the other sub
float contactDot = Vector2.Dot(otherSub.PhysicsBody.LinearVelocity, -avgContactNormal);
if (contactDot > 0.0f)
{
if (otherSub.PhysicsBody.LinearVelocity.LengthSquared() > 0.0001f)
{
otherSub.PhysicsBody.LinearVelocity -= Vector2.Normalize(otherSub.PhysicsBody.LinearVelocity) * contactDot;
}
impulse = Vector2.Dot(otherSub.Velocity, normal);
otherSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
foreach (Submarine dockedSub in otherSub.DockedTo)
{
dockedSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
}
}
}
private void ApplyImpact(float impact, Vector2 direction, Vector2 impactPos, bool applyDamage = true)
{
if (impact < MinCollisionImpact) { return; }
Vector2 impulse = direction * impact * 0.5f;
impulse = impulse.ClampLength(MaxCollisionImpact);
if (!MathUtils.IsValid(impulse))
{
string errorMsg =
"Invalid impulse in SubmarineBody.ApplyImpact: " + impulse +
". Direction: " + direction + ", body position: " + Body.SimPosition + ", impact: " + impact + ".";
if (GameMain.NetworkMember != null)
{
errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server.";
}
if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"SubmarineBody.ApplyImpact:InvalidImpulse",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg);
return;
}
#if CLIENT
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
GameMain.GameScreen.Cam.Shake = impact * 2.0f;
if (!submarine.IsOutpost && !submarine.DockedTo.Any(s => s.IsOutpost))
{
float angularVelocity =
(impactPos.X - Body.SimPosition.X) / ConvertUnits.ToSimUnits(submarine.Borders.Width / 2) * impulse.Y
- (impactPos.Y - Body.SimPosition.Y) / ConvertUnits.ToSimUnits(submarine.Borders.Height / 2) * impulse.X;
GameMain.GameScreen.Cam.AngularVelocity = MathHelper.Clamp(angularVelocity * 0.1f, -1.0f, 1.0f);
}
}
#endif
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != submarine) { continue; }
foreach (Limb limb in c.AnimController.Limbs)
{
limb.body.ApplyLinearImpulse(limb.Mass * impulse, 10.0f);
}
c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f);
bool holdingOntoSomething = false;
if (c.SelectedConstruction != null)
{
var controller = c.SelectedConstruction.GetComponent<Items.Components.Controller>();
holdingOntoSomething = controller != null && controller.LimbPositions.Any();
}
//stun for up to 1 second if the impact equal or higher to the maximum impact
if (impact >= MaxCollisionImpact && !holdingOntoSomething)
{
c.SetStun(Math.Min(impulse.Length() * 0.2f, 1.0f));
}
}
foreach (Item item in Item.ItemList)
{
if (item.Submarine != submarine || item.CurrentHull == null ||
item.body == null || !item.body.Enabled) continue;
item.body.ApplyLinearImpulse(item.body.Mass * impulse, 10.0f);
}
var damagedStructures = Explosion.RangedStructureDamage(
ConvertUnits.ToDisplayUnits(impactPos),
impact * 50.0f,
applyDamage ? impact * ImpactDamageMultiplier : 0.0f);
#if CLIENT
//play a damage sound for the structure that took the most damage
float maxDamage = 0.0f;
Structure maxDamageStructure = null;
foreach (KeyValuePair<Structure, float> structureDamage in damagedStructures)
{
if (maxDamageStructure == null || structureDamage.Value > maxDamage)
{
maxDamage = structureDamage.Value;
maxDamageStructure = structureDamage.Key;
}
}
if (maxDamageStructure != null)
{
SoundPlayer.PlayDamageSound(
"StructureBlunt",
impact * 10.0f,
ConvertUnits.ToDisplayUnits(impactPos),
MathHelper.Lerp(2000.0f, 10000.0f, (impact - MinCollisionImpact) / 2.0f),
maxDamageStructure.Tags);
}
#endif
}
}
}
@@ -0,0 +1,723 @@
using Barotrauma.Items.Components;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
public enum SpawnType { Path, Human, Enemy, Cargo };
partial class WayPoint : MapEntity
{
public static List<WayPoint> WayPointList = new List<WayPoint>();
public static bool ShowWayPoints = true, ShowSpawnPoints = true;
protected SpawnType spawnType;
//characters spawning at the waypoint will be given an ID card with these tags
private string idCardDesc;
private string[] idCardTags;
//only characters with this job will be spawned at the waypoint
private JobPrefab assignedJob;
private Hull currentHull;
private ushort ladderId;
public Ladder Ladders;
public Structure Stairs;
public bool isObstructed;
private ushort gapId;
public Gap ConnectedGap
{
get;
private set;
}
public Door ConnectedDoor
{
get { return ConnectedGap?.ConnectedDoor; }
}
public Hull CurrentHull
{
get { return currentHull; }
}
public SpawnType SpawnType
{
get { return spawnType; }
set { spawnType = value; }
}
public override string Name
{
get
{
return spawnType == SpawnType.Path ? "WayPoint" : "SpawnPoint";
}
}
public string IdCardDesc
{
get { return idCardDesc; }
private set { idCardDesc = value; }
}
public string[] IdCardTags
{
get { return idCardTags; }
private set
{
idCardTags = value;
for (int i = 0; i < idCardTags.Length; i++)
{
idCardTags[i] = idCardTags[i].Trim().ToLowerInvariant();
}
}
}
public JobPrefab AssignedJob
{
get { return assignedJob; }
}
public WayPoint(Vector2 position, SpawnType spawnType, Submarine submarine, Gap gap = null)
: this(new Rectangle((int)position.X - 3, (int)position.Y + 3, 6, 6), submarine)
{
this.spawnType = spawnType;
ConnectedGap = gap;
}
public WayPoint(MapEntityPrefab prefab, Rectangle rectangle)
: this (rectangle, Submarine.MainSub)
{
if (prefab.Identifier.Contains("spawn"))
{
spawnType = SpawnType.Human;
}
else
{
SpawnType = SpawnType.Path;
}
}
public WayPoint(Rectangle newRect, Submarine submarine)
: this (MapEntityPrefab.Find(null, "waypoint"), newRect, submarine)
{
}
public WayPoint(MapEntityPrefab prefab, Rectangle newRect, Submarine submarine)
: base (prefab, submarine)
{
rect = newRect;
idCardTags = new string[0];
#if CLIENT
if (iconSprites == null)
{
iconSprites = new Dictionary<SpawnType, Sprite>()
{
{ SpawnType.Path, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,0,128,128)) },
{ SpawnType.Human, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,0,128,128)) },
{ SpawnType.Enemy, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256,0,128,128)) },
{ SpawnType.Cargo, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) }
};
}
#endif
InsertToList();
WayPointList.Add(this);
DebugConsole.Log("Created waypoint (" + ID + ")");
currentHull = Hull.FindHull(WorldPosition);
}
public override MapEntity Clone()
{
var clone = new WayPoint(rect, Submarine)
{
idCardDesc = idCardDesc,
idCardTags = idCardTags,
spawnType = spawnType,
assignedJob = assignedJob
};
return clone;
}
public static bool GenerateSubWaypoints(Submarine submarine)
{
if (!Hull.hullList.Any())
{
DebugConsole.ThrowError("Couldn't generate waypoints: no hulls found.");
return false;
}
List<WayPoint> existingWaypoints = WayPointList.FindAll(wp => wp.spawnType == SpawnType.Path);
foreach (WayPoint wayPoint in existingWaypoints)
{
wayPoint.Remove();
}
//find all open doors and temporarily activate their bodies to prevent visibility checks
//from ignoring the doors and generating waypoint connections that go straight through the door
List<Door> openDoors = new List<Door>();
foreach (Item item in Item.ItemList)
{
var door = item.GetComponent<Door>();
if (door != null && !door.Body.Enabled)
{
openDoors.Add(door);
door.Body.Enabled = true;
}
}
float minDist = 150.0f;
float heightFromFloor = 110.0f;
foreach (Hull hull in Hull.hullList)
{
if (hull.Rect.Height < 150) continue;
WayPoint prevWaypoint = null;
if (hull.Rect.Width < minDist * 3.0f)
{
new WayPoint(
new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine);
continue;
}
for (float x = hull.Rect.X + minDist; x <= hull.Rect.Right - minDist; x += minDist)
{
var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine);
if (prevWaypoint != null) wayPoint.ConnectTo(prevWaypoint);
prevWaypoint = wayPoint;
}
}
float outSideWaypointInterval = 200.0f;
int outsideWaypointDist = 100;
Rectangle borders = Hull.GetBorders();
borders.X -= outsideWaypointDist;
borders.Y += outsideWaypointDist;
borders.Width += outsideWaypointDist * 2;
borders.Height += outsideWaypointDist * 2;
borders.Location -= MathUtils.ToPoint(submarine.HiddenSubPosition);
if (borders.Width <= outSideWaypointInterval*2)
{
borders.Inflate(outSideWaypointInterval*2 - borders.Width, 0);
}
if (borders.Height <= outSideWaypointInterval * 2)
{
int inflateAmount = (int)(outSideWaypointInterval * 2) - borders.Height;
borders.Y += inflateAmount / 2;
borders.Height += inflateAmount;
}
WayPoint[,] cornerWaypoint = new WayPoint[2, 2];
for (int i = 0; i < 2; i++)
{
for (float x = borders.X + outSideWaypointInterval; x < borders.Right - outSideWaypointInterval; x += outSideWaypointInterval)
{
var wayPoint = new WayPoint(
new Vector2(x, borders.Y - borders.Height * i) + submarine.HiddenSubPosition,
SpawnType.Path, submarine);
if (x == borders.X + outSideWaypointInterval)
{
cornerWaypoint[i, 0] = wayPoint;
}
else
{
wayPoint.ConnectTo(WayPointList[WayPointList.Count - 2]);
}
}
cornerWaypoint[i, 1] = WayPointList[WayPointList.Count - 1];
}
for (int i = 0; i < 2; i++)
{
WayPoint wayPoint = null;
for (float y = borders.Y - borders.Height; y < borders.Y; y += outSideWaypointInterval)
{
wayPoint = new WayPoint(
new Vector2(borders.X + borders.Width * i, y) + submarine.HiddenSubPosition,
SpawnType.Path, submarine);
if (y == borders.Y - borders.Height)
{
wayPoint.ConnectTo(cornerWaypoint[1, i]);
}
else
{
wayPoint.ConnectTo(WayPoint.WayPointList[WayPointList.Count - 2]);
}
}
wayPoint.ConnectTo(cornerWaypoint[0, i]);
}
List<Structure> stairList = new List<Structure>();
foreach (MapEntity me in mapEntityList)
{
Structure stairs = me as Structure;
if (stairs == null) continue;
if (stairs.StairDirection != Direction.None) stairList.Add(stairs);
}
foreach (Structure stairs in stairList)
{
WayPoint[] stairPoints = new WayPoint[3];
stairPoints[0] = new WayPoint(
new Vector2(stairs.Rect.X - 32.0f,
stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? 80 : stairs.Rect.Height) + heightFromFloor), SpawnType.Path, submarine);
stairPoints[1] = new WayPoint(
new Vector2(stairs.Rect.Right + 32.0f,
stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? stairs.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine);
for (int i = 0; i < 2; i++ )
{
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = stairPoints[i].FindClosest(dir, true, new Vector2(-30.0f, 30f));
if (closest == null) continue;
stairPoints[i].ConnectTo(closest);
}
}
stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position)/2, SpawnType.Path, submarine);
stairPoints[0].ConnectTo(stairPoints[2]);
stairPoints[2].ConnectTo(stairPoints[1]);
}
foreach (Item item in Item.ItemList)
{
var ladders = item.GetComponent<Ladder>();
if (ladders == null) continue;
List<WayPoint> ladderPoints = new List<WayPoint>();
ladderPoints.Add(new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height + heightFromFloor), SpawnType.Path, submarine));
WayPoint prevPoint = ladderPoints[0];
Vector2 prevPos = prevPoint.SimPosition;
List<Body> ignoredBodies = new List<Body>();
for (float y = ladderPoints[0].Position.Y + 100.0f; y < item.Rect.Y - 1.0f; y += 100.0f)
{
//first check if there's a door in the way
//(we need to create a waypoint linked to the door for NPCs to open it)
Body pickedBody = Submarine.PickBody(
ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)),
prevPos, ignoredBodies, Physics.CollisionWall, false,
(Fixture f) => f.Body.UserData is Item && ((Item)f.Body.UserData).GetComponent<Door>() != null);
Door pickedDoor = null;
if (pickedBody != null)
{
pickedDoor = (pickedBody?.UserData as Item).GetComponent<Door>();
}
else
{
//no door, check for walls
pickedBody = Submarine.PickBody(
ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)), prevPos, ignoredBodies, null, false);
}
if (pickedBody == null)
{
prevPos = Submarine.LastPickedPosition;
continue;
}
else
{
ignoredBodies.Add(pickedBody);
}
if (pickedDoor != null)
{
WayPoint newPoint = new WayPoint(pickedDoor.Item.Position, SpawnType.Path, submarine);
ladderPoints.Add(newPoint);
newPoint.ConnectedGap = pickedDoor.LinkedGap;
newPoint.ConnectTo(prevPoint);
prevPoint = newPoint;
prevPos = new Vector2(prevPos.X, ConvertUnits.ToSimUnits(pickedDoor.Item.Position.Y - pickedDoor.Item.Rect.Height));
}
else
{
WayPoint newPoint = new WayPoint(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.UnitY * heightFromFloor, SpawnType.Path, submarine);
ladderPoints.Add(newPoint);
newPoint.ConnectTo(prevPoint);
prevPoint = newPoint;
prevPos = ConvertUnits.ToSimUnits(newPoint.Position);
}
}
if (prevPoint.rect.Y < item.Rect.Y - 10.0f)
{
WayPoint newPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine);
ladderPoints.Add(newPoint);
newPoint.ConnectTo(prevPoint);
}
//connect ladder waypoints to hull points at the right and left side
foreach (WayPoint ladderPoint in ladderPoints)
{
ladderPoint.Ladders = ladders;
//don't connect if the waypoint is at a gap (= at the boundary of hulls and/or at a hatch)
if (ladderPoint.ConnectedGap != null) continue;
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = ladderPoint.FindClosest(dir, true, new Vector2(-150.0f, 10f));
if (closest == null) continue;
ladderPoint.ConnectTo(closest);
}
}
}
foreach (Gap gap in Gap.GapList)
{
if (!gap.IsHorizontal) continue;
//too small to walk through
if (gap.Rect.Height < 150.0f) continue;
var wayPoint = new WayPoint(
new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor), SpawnType.Path, submarine, gap);
for (int dir = -1; dir <= 1; dir += 2)
{
float tolerance = gap.IsRoomToRoom ? 50.0f : outSideWaypointInterval / 2.0f;
WayPoint closest = wayPoint.FindClosest(
dir, true, new Vector2(-tolerance, tolerance),
gap.ConnectedDoor?.Body.FarseerBody);
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
}
}
foreach (Gap gap in Gap.GapList)
{
if (gap.IsHorizontal || gap.IsRoomToRoom || !gap.linkedTo.Any(l => l is Hull)) { continue; }
//too small to walk through
if (gap.Rect.Width < 100.0f) { continue; }
var wayPoint = new WayPoint(
new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2), SpawnType.Path, submarine, gap);
float tolerance = outSideWaypointInterval / 2.0f;
Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull);
int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y);
WayPoint closest = wayPoint.FindClosest(
dir, false, new Vector2(-tolerance, tolerance),
gap.ConnectedDoor?.Body.FarseerBody);
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
}
var orphans = WayPointList.FindAll(w => w.spawnType == SpawnType.Path && !w.linkedTo.Any());
foreach (WayPoint wp in orphans)
{
wp.Remove();
}
//re-disable the bodies of the doors that are supposed to be open
foreach (Door door in openDoors)
{
door.Body.Enabled = false;
}
return true;
}
private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null)
{
if (dir != -1 && dir != 1) return null;
float closestDist = 0.0f;
WayPoint closest = null;
foreach (WayPoint wp in WayPointList)
{
if (wp.SpawnType != SpawnType.Path || wp == this) continue;
float diff = 0.0f;
if (horizontalSearch)
{
if ((wp.Position.Y - Position.Y) < tolerance.X || (wp.Position.Y - Position.Y) > tolerance.Y) continue;
diff = wp.Position.X - Position.X;
}
else
{
if ((wp.Position.X - Position.X) < tolerance.X || (wp.Position.X - Position.X) > tolerance.Y) continue;
diff = wp.Position.Y - Position.Y;
}
if (Math.Sign(diff) != dir) continue;
float dist = Vector2.Distance(wp.Position, Position);
if (closest == null || dist < closestDist)
{
var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, true, true, false);
if (body != null && body != ignoredBody && !(body.UserData is Submarine))
{
if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue;
}
closestDist = dist;
closest = wp;
}
}
return closest;
}
private void ConnectTo(WayPoint wayPoint2)
{
System.Diagnostics.Debug.Assert(this != wayPoint2);
if (!linkedTo.Contains(wayPoint2)) linkedTo.Add(wayPoint2);
if (!wayPoint2.linkedTo.Contains(this)) wayPoint2.linkedTo.Add(this);
}
public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, Job assignedJob = null, Submarine sub = null, bool useSyncedRand = false)
{
List<WayPoint> wayPoints = new List<WayPoint>();
foreach (WayPoint wp in WayPointList)
{
if (sub != null && wp.Submarine != sub) continue;
if (wp.spawnType != spawnType) continue;
if (assignedJob != null && wp.assignedJob != assignedJob.Prefab) continue;
wayPoints.Add(wp);
}
if (!wayPoints.Any()) return null;
return wayPoints[Rand.Int(wayPoints.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))];
}
public static WayPoint[] SelectCrewSpawnPoints(List<CharacterInfo> crew, Submarine submarine)
{
List<WayPoint> subWayPoints = WayPointList.FindAll(wp => wp.Submarine == submarine);
List<WayPoint> unassignedWayPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human);
WayPoint[] assignedWayPoints = new WayPoint[crew.Count];
for (int i = 0; i < crew.Count; i++ )
{
//try to give the crew member a spawnpoint that hasn't been assigned to anyone and matches their job
for (int n = 0; n < unassignedWayPoints.Count; n++)
{
if (crew[i].Job.Prefab != unassignedWayPoints[n].assignedJob) continue;
assignedWayPoints[i] = unassignedWayPoints[n];
unassignedWayPoints.RemoveAt(n);
break;
}
}
//go through the crewmembers that don't have a spawnpoint yet (if any)
for (int i = 0; i < crew.Count; i++)
{
if (assignedWayPoints[i] != null) continue;
//try to assign a spawnpoint that matches the job, even if the spawnpoint is already assigned to someone else
foreach (WayPoint wp in subWayPoints)
{
if (wp.spawnType != SpawnType.Human || wp.assignedJob != crew[i].Job.Prefab) continue;
assignedWayPoints[i] = wp;
break;
}
if (assignedWayPoints[i] != null) continue;
//try to assign a spawnpoint that isn't meant for any specific job
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)];
}
if (assignedWayPoints[i] != null) continue;
//everything else failed -> just give a random spawnpoint inside the sub
assignedWayPoints[i] = GetRandom(SpawnType.Human, null, submarine, true);
}
for (int i = 0; i < assignedWayPoints.Length; i++)
{
if (assignedWayPoints[i] == null)
{
DebugConsole.ThrowError("Couldn't find a waypoint for " + crew[i].Name + "!");
assignedWayPoints[i] = WayPointList[0];
}
}
return assignedWayPoints;
}
public override void OnMapLoaded()
{
currentHull = Hull.FindHull(WorldPosition, currentHull);
if (gapId > 0) ConnectedGap = FindEntityByID(gapId) as Gap;
if (ladderId > 0)
{
var ladderItem = FindEntityByID(ladderId) as Item;
if (ladderItem != null) Ladders = ladderItem.GetComponent<Ladder>();
}
Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - Vector2.UnitY * 2.0f, null, Physics.CollisionStairs);
if (pickedBody != null && pickedBody.UserData is Structure)
{
Structure structure = (Structure)pickedBody.UserData;
if (structure != null && structure.StairDirection != Direction.None)
{
Stairs = structure;
}
}
}
public static WayPoint Load(XElement element, Submarine submarine)
{
Rectangle rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
int.Parse(element.Attribute("y").Value),
(int)Submarine.GridSize.X, (int)Submarine.GridSize.Y);
Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType);
WayPoint w = new WayPoint(MapEntityPrefab.Find(null, spawnType == SpawnType.Path ? "waypoint" : "spawnpoint"), rect, submarine)
{
ID = (ushort)int.Parse(element.Attribute("ID").Value)
};
w.spawnType = spawnType;
string idCardDescString = element.GetAttributeString("idcarddesc", "");
if (!string.IsNullOrWhiteSpace(idCardDescString))
{
w.IdCardDesc = idCardDescString;
}
string idCardTagString = element.GetAttributeString("idcardtags", "");
if (!string.IsNullOrWhiteSpace(idCardTagString))
{
w.IdCardTags = idCardTagString.Split(',');
}
string jobIdentifier = element.GetAttributeString("job", "").ToLowerInvariant();
if (!string.IsNullOrWhiteSpace(jobIdentifier))
{
w.assignedJob =
JobPrefab.Get(jobIdentifier) ??
JobPrefab.Prefabs.Find(jp => jp.Name.Equals(jobIdentifier, StringComparison.OrdinalIgnoreCase));
}
w.ladderId = (ushort)element.GetAttributeInt("ladders", 0);
w.gapId = (ushort)element.GetAttributeInt("gap", 0);
w.linkedToID = new List<ushort>();
int i = 0;
while (element.Attribute("linkedto" + i) != null)
{
w.linkedToID.Add((ushort)int.Parse(element.Attribute("linkedto" + i).Value));
i += 1;
}
return w;
}
public override XElement Save(XElement parentElement)
{
if (!ShouldBeSaved) return null;
XElement element = new XElement("WayPoint");
element.Add(new XAttribute("ID", ID),
new XAttribute("x", (int)(rect.X - Submarine.HiddenSubPosition.X)),
new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)),
new XAttribute("spawn", spawnType));
if (!string.IsNullOrWhiteSpace(idCardDesc)) element.Add(new XAttribute("idcarddesc", idCardDesc));
if (idCardTags.Length > 0)
{
element.Add(new XAttribute("idcardtags", string.Join(",", idCardTags)));
}
if (assignedJob != null) element.Add(new XAttribute("job", assignedJob.Identifier));
if (ConnectedGap != null) element.Add(new XAttribute("gap", ConnectedGap.ID));
if (Ladders != null) element.Add(new XAttribute("ladders", Ladders.Item.ID));
parentElement.Add(element);
if (linkedTo != null)
{
int i = 0;
foreach (MapEntity e in linkedTo)
{
if (!e.ShouldBeSaved) continue;
element.Add(new XAttribute("linkedto" + i, e.ID));
i += 1;
}
}
return element;
}
public override void ShallowRemove()
{
base.ShallowRemove();
WayPointList.Remove(this);
}
public override void Remove()
{
base.Remove();
WayPointList.Remove(this);
}
}
}