Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Map/Hull.cs
2 using FarseerPhysics;
3 using FarseerPhysics.Dynamics;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 using System.Xml.Linq;
13 
14 namespace Barotrauma
15 {
16  partial class BackgroundSection
17  {
18  public Rectangle Rect;
19  public ushort Index;
20  public ushort RowIndex;
21 
22  private Vector4 colorVector4;
23  private Color color;
24 
25  public readonly Vector2 Noise;
26  public readonly Color DirtColor;
27 
28 #if CLIENT
30 #endif
31 
32  public float ColorStrength
33  {
34  get;
35  protected set;
36  }
37 
38  public Color Color
39  {
40  get { return color; }
41  protected set
42  {
43  color = value;
44  colorVector4 = new Vector4(value.R / 255.0f, value.G / 255.0f, value.B / 255.0f, value.A / 255.0f);
45  }
46  }
47 
48  public BackgroundSection(Rectangle rect, ushort index, ushort rowIndex)
49  {
50  Rect = rect;
51  Index = index;
52  ColorStrength = 0.0f;
53  RowIndex = rowIndex;
54 
55  Noise = new Vector2(
56  PerlinNoise.GetPerlin(Rect.X / 1000.0f, Rect.Y / 1000.0f),
57  PerlinNoise.GetPerlin(Rect.Y / 1000.0f + 0.5f, Rect.X / 1000.0f + 0.5f));
58 
59  Color = DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X);
60 #if CLIENT
61  GrimeSprite = DecalManager.GrimeSprites[$"{nameof(GrimeSprite)}{index % DecalManager.GrimeSpriteCount}"].Sprite;
62 #endif
63  }
64 
65  public BackgroundSection(Rectangle rect, ushort index, float colorStrength, Color color, ushort rowIndex)
66  {
67  System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0);
68 
69  Rect = rect;
70  Index = index;
71  ColorStrength = colorStrength;
72  Color = color;
73  RowIndex = rowIndex;
74 
75  Noise = new Vector2(
76  PerlinNoise.GetPerlin(Rect.X / 1000.0f, Rect.Y / 1000.0f),
77  PerlinNoise.GetPerlin(Rect.Y / 1000.0f + 0.5f, Rect.X / 1000.0f + 0.5f));
78 
79  DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X);
80 #if CLIENT
81  GrimeSprite = DecalManager.GrimeSprites[$"{nameof(GrimeSprite)}{index % DecalManager.GrimeSpriteCount}"].Sprite;
82 #endif
83  }
84 
85  public bool SetColor(Color color)
86  {
87  if (Color == color) { return false; }
88  Color = color;
89  return true;
90  }
91 
92  public float SetColorStrength(float colorStrength)
93  {
94  if (ColorStrength == colorStrength) { return -1f; }
95  float previous = ColorStrength;
96  ColorStrength = colorStrength;
97  return previous;
98  }
99 
100  public bool LerpColor(Color to, float amount)
101  {
102  if (Color == to) { return false; }
103  colorVector4 = Vector4.Lerp(colorVector4, to.ToVector4(), amount);
104  color = new Color(colorVector4);
105  return true;
106  }
107 
109  {
110  return Color * ColorStrength;
111  }
112  }
113 
115  {
116  public readonly static List<Hull> HullList = new List<Hull>();
117  public readonly static List<EntityGrid> EntityGrids = new List<EntityGrid>();
118 
119  public static bool ShowHulls = true;
120 
121  public static bool EditWater, EditFire;
122  public const float OxygenDistributionSpeed = 30000.0f;
123  public const float OxygenDeteriorationSpeed = 0.3f;
124  public const float OxygenConsumptionSpeed = 700.0f;
125 
126  public const int WaveWidth = 32;
127  public static float WaveStiffness = 0.01f;
128  public static float WaveSpread = 0.02f;
129  public static float WaveDampening = 0.02f;
130 
131  //how much excess water the room can contain, relative to the volume of the room.
132  //needed to make it possible for pressure to "push" water up through U-shaped hull configurations
133  public const float MaxCompress = 1.05f;
134 
135  public const int BackgroundSectionSize = 16;
136 
137  public const int BackgroundSectionsPerNetworkEvent = 16;
138 
139  public readonly Dictionary<Identifier, SerializableProperty> properties;
140  public Dictionary<Identifier, SerializableProperty> SerializableProperties
141  {
142  get { return properties; }
143  }
144 
148  public const float PressureBuildUpSpeed = 15.0f;
149 
153  public const float PressureDropSpeed = 10.0f;
154 
155  private float lethalPressure;
156 
157  private float surface;
158  private float waterVolume;
159  private float pressure;
160 
161  private float oxygen;
162 
163  private bool update;
164 
165  public bool Visible = true;
166 
167  private float[] waveY; //displacement from the surface of the water
168  private float[] waveVel; //velocity of the point
169 
170  private float[] leftDelta;
171  private float[] rightDelta;
172 
173  public const int MaxDecalsPerHull = 10;
174 
175  private readonly List<Decal> decals = new List<Decal>();
176 
177 
178  public readonly List<Gap> ConnectedGaps = new List<Gap>();
179 
180  public override string Name => "Hull";
181 
183  {
184  get;
185  private set;
186  }
187 
188  private readonly HashSet<Identifier> moduleTags = new HashSet<Identifier>();
189 
193  public IEnumerable<Identifier> OutpostModuleTags => moduleTags;
194 
195  private string roomName;
196  [Editable, Serialize("", IsPropertySaveable.Yes, translationTextTag: "RoomName.")]
197  public string RoomName
198  {
199  get { return roomName; }
200  set
201  {
202  if (roomName == value) { return; }
203  roomName = value;
204  DisplayName = TextManager.Get(roomName).Fallback(roomName);
205  if (!IsWetRoom && ForceAsWetRoom)
206  {
207  IsWetRoom = true;
208  }
209  }
210  }
211 
212  public Color? OriginalAmbientLight = null;
213 
214  private Color ambientLight;
215 
216  [Editable, Serialize("0,0,0,0", IsPropertySaveable.Yes)]
217  public Color AmbientLight
218  {
219  get { return ambientLight; }
220  set
221  {
222  ambientLight = value;
223 #if CLIENT
224  lastAmbientLightEditTime = Timing.TotalTime;
225 #endif
226  }
227  }
228 
229  public override Rectangle Rect
230  {
231  get
232  {
233  return base.Rect;
234  }
235  set
236  {
237  float prevOxygenPercentage = OxygenPercentage;
238 
239  if (value.Width != rect.Width)
240  {
241  int arraySize = (int)Math.Ceiling((float)value.Width / WaveWidth + 1);
242  waveY = new float[arraySize];
243  waveVel = new float[arraySize];
244  leftDelta = new float[arraySize];
245  rightDelta = new float[arraySize];
246  }
247 
248  base.Rect = value;
249 
250  if (Submarine == null || !Submarine.Loading)
251  {
252  Item.UpdateHulls();
253  Gap.UpdateHulls();
254  }
255 
256  OxygenPercentage = prevOxygenPercentage;
257  surface = rect.Y - rect.Height + WaterVolume / rect.Width;
258 #if CLIENT
259  drawSurface = surface;
260 #endif
261  Pressure = surface;
262 
263  CreateBackgroundSections();
264  }
265  }
266 
267  public override bool Linkable
268  {
269  get { return true; }
270  }
271 
272  public float LethalPressure
273  {
274  get { return lethalPressure; }
275  set { lethalPressure = MathHelper.Clamp(value, 0.0f, 100.0f); }
276  }
277 
278  public Vector2 Size
279  {
280  get { return new Vector2(rect.Width, rect.Height); }
281  }
282 
283  public float CeilingHeight
284  {
285  get;
286  private set;
287  }
288 
289  public float Surface
290  {
291  get { return surface; }
292  }
293 
294  public float WorldSurface
295  {
296  get { return Submarine == null ? surface : surface + Submarine.Position.Y; }
297  }
298 
299  public float WaterVolume
300  {
301  get { return waterVolume; }
302  set
303  {
304  if (!MathUtils.IsValid(value)) { return; }
305  waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress);
306  if (waterVolume < Volume) { Pressure = rect.Y - rect.Height + waterVolume / rect.Width; }
307  if (waterVolume > 0.0f)
308  {
309  update = true;
310  }
311  }
312  }
313 
314  [Serialize(100000.0f, IsPropertySaveable.Yes)]
315  public float Oxygen
316  {
317  get { return oxygen; }
318  set
319  {
320  if (!MathUtils.IsValid(value)) return;
321  oxygen = MathHelper.Clamp(value, 0.0f, Volume);
322  }
323  }
324 
325  public bool IsAirlock { get; private set; }
326 
327  private bool ForceAsWetRoom =>
328  roomName != null && (
329  roomName.Contains("ballast", StringComparison.OrdinalIgnoreCase) ||
330  roomName.Contains("bilge", StringComparison.OrdinalIgnoreCase) ||
331  roomName.Contains("airlock", StringComparison.OrdinalIgnoreCase) ||
332  roomName.Contains("dockingport", StringComparison.OrdinalIgnoreCase));
333 
334  private bool isWetRoom;
335  [Editable, Serialize(false, IsPropertySaveable.Yes, description: "It's normal for this hull to be filled with water. If the room name contains 'ballast', 'bilge', or 'airlock', you can't disable this setting.")]
336  public bool IsWetRoom
337  {
338  get { return isWetRoom; }
339  set
340  {
341  isWetRoom = value;
342  if (ForceAsWetRoom)
343  {
344  isWetRoom = true;
345  }
346  }
347  }
348 
349  private bool avoidStaying;
350  [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Bots avoid staying here, but they are still allowed to access the room when needed and go through it. Forced true for wet rooms.")]
351  public bool AvoidStaying
352  {
353  get { return avoidStaying || IsWetRoom; }
354  set
355  {
356  avoidStaying = value;
357  if (IsWetRoom)
358  {
359  avoidStaying = true;
360  }
361  }
362  }
363 
364  public float WaterPercentage => MathUtils.Percentage(WaterVolume, Volume);
365 
366  public float OxygenPercentage
367  {
368  get { return Volume <= 0.0f ? 100.0f : oxygen / Volume * 100.0f; }
369  set { Oxygen = (value / 100.0f) * Volume; }
370  }
371 
372  public float Volume
373  {
374  get { return rect.Width * rect.Height; }
375  }
376 
377  public float Pressure
378  {
379  get { return pressure; }
380  set { pressure = value; }
381  }
382 
383  public float[] WaveY
384  {
385  get { return waveY; }
386  }
387 
388  public float[] WaveVel
389  {
390  get { return waveVel; }
391  }
392 
393  // sections of a decorative background that can be painted
394  public List<BackgroundSection> BackgroundSections
395  {
396  get;
397  private set;
398  }
399 
400  private readonly HashSet<int> pendingSectionUpdates = new HashSet<int>();
401 
402  public int xBackgroundMax, yBackgroundMax;
403 
405  {
406  get
407  {
408  return BackgroundSections != null;
409  }
410  }
411 
412  private const int sectorWidth = 4;
413  private const int sectorHeight = 4;
414 
415  private const float minColorStrength = 0.0f;
416  private const float maxColorStrength = 0.7f;
417 
418  private bool networkUpdatePending;
419  private float networkUpdateTimer;
420 
424  public Color AveragePaintedColor { get; private set; }
425 
429  public bool IsRed => ColorExtensions.IsRedDominant(AveragePaintedColor, minimumAlpha: 100);
430 
434  public bool IsGreen => ColorExtensions.IsGreenDominant(AveragePaintedColor, minimumAlpha: 100);
435 
439  public bool IsBlue => ColorExtensions.IsBlueDominant(AveragePaintedColor, minimumAlpha: 100);
440 
441  public List<FireSource> FireSources { get; private set; }
442 
443  public List<DummyFireSource> FakeFireSources { get; private set; }
444 
448  public int FireCount => FireSources?.Count ?? 0;
449 
450  public BallastFloraBehavior BallastFlora { get; set; }
451 
452  public Hull(Rectangle rectangle)
453  : this (rectangle, Submarine.MainSub)
454  {
455 #if CLIENT
457  {
458  SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity> { this }, false));
459  }
460 #endif
461  }
462 
463  public Hull(Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID)
464  : base (CoreEntityPrefab.HullPrefab, submarine, id)
465  {
466  rect = rectangle;
467 
468  if (BackgroundSections == null) { CreateBackgroundSections(); }
469 
470  OxygenPercentage = 100.0f;
471 
472  FireSources = new List<FireSource>();
473  FakeFireSources = new List<DummyFireSource>();
474 
476 
477  int arraySize = (int)Math.Ceiling((float)rectangle.Width / WaveWidth + 1);
478  waveY = new float[arraySize];
479  waveVel = new float[arraySize];
480  leftDelta = new float[arraySize];
481  rightDelta = new float[arraySize];
482 
483  surface = rect.Y - rect.Height;
484 
485  if (submarine?.Info != null && !submarine.Info.IsWreck)
486  {
487  aiTarget = new AITarget(this)
488  {
489  MinSightRange = 1000,
490  MaxSightRange = 5000,
491  SoundRange = 0
492  };
493  }
494 
495  HullList.Add(this);
496 
497  if (submarine == null || !submarine.Loading)
498  {
499  Item.UpdateHulls();
500  Gap.UpdateHulls();
501  }
502 
503  CreateBackgroundSections();
504 
505  WaterVolume = 0.0f;
506 
507  InsertToList();
508 
509  DebugConsole.Log("Created hull (" + ID + ")");
510  }
511 
512  public static Rectangle GetBorders()
513  {
514  if (!HullList.Any()) return Rectangle.Empty;
515 
516  Rectangle rect = HullList[0].rect;
517 
518  foreach (Hull hull in HullList)
519  {
520  if (hull.Rect.X < rect.X)
521  {
522  rect.Width += rect.X - hull.rect.X;
523  rect.X = hull.rect.X;
524 
525  }
526  if (hull.rect.Right > rect.Right) rect.Width = hull.rect.Right - rect.X;
527 
528  if (hull.rect.Y > rect.Y)
529  {
530  rect.Height += hull.rect.Y - rect.Y;
531 
532  rect.Y = hull.rect.Y;
533  }
534  if (hull.rect.Y - hull.rect.Height < rect.Y - rect.Height) rect.Height = rect.Y - (hull.rect.Y - hull.rect.Height);
535  }
536 
537  return rect;
538  }
539 
540  public override MapEntity Clone()
541  {
542  var clone = new Hull(rect, Submarine);
543  foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
544  {
545  if (!property.Value.Attributes.OfType<Editable>().Any()) { continue; }
546  clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
547  }
548 #if CLIENT
549  clone.lastAmbientLightEditTime = 0.0;
550 #endif
551  return clone;
552  }
553 
554  public static EntityGrid GenerateEntityGrid(Rectangle worldRect)
555  {
556  var newGrid = new EntityGrid(worldRect, 200.0f);
557  EntityGrids.Add(newGrid);
558  return newGrid;
559  }
560 
561  public static EntityGrid GenerateEntityGrid(Submarine submarine)
562  {
563  var newGrid = new EntityGrid(submarine, 200.0f);
564  EntityGrids.Add(newGrid);
565  foreach (Hull hull in HullList)
566  {
567  if (hull.Submarine == submarine && !hull.IdFreed) { newGrid.InsertEntity(hull); }
568  }
569  return newGrid;
570  }
571 
572  public void SetModuleTags(IEnumerable<Identifier> tags)
573  {
574  moduleTags.Clear();
575  foreach (Identifier tag in tags)
576  {
577  moduleTags.Add(tag);
578  }
579  }
580 
581  public override void OnMapLoaded()
582  {
583  CeilingHeight = Rect.Height;
584 
585  Body lowerPickedBody = Submarine.PickBody(SimPosition, SimPosition - new Vector2(0.0f, ConvertUnits.ToSimUnits(rect.Height / 2.0f + 0.1f)), null, Physics.CollisionWall);
586  if (lowerPickedBody != null)
587  {
588  Vector2 lowerPickedPos = Submarine.LastPickedPosition;
589 
590  if (Submarine.PickBody(SimPosition, SimPosition + new Vector2(0.0f, ConvertUnits.ToSimUnits(rect.Height / 2.0f + 0.1f)), null, Physics.CollisionWall) != null)
591  {
592  Vector2 upperPickedPos = Submarine.LastPickedPosition;
593 
594  CeilingHeight = ConvertUnits.ToDisplayUnits(upperPickedPos.Y - lowerPickedPos.Y);
595  }
596  }
597  Pressure = rect.Y - rect.Height + waterVolume / rect.Width;
598 
599  DetermineIsAirlock();
600 
601  BallastFlora?.OnMapLoaded();
602 #if CLIENT
603  lastAmbientLightEditTime = 0.0;
604 #endif
605  }
606 
607  public void AddToGrid(Submarine submarine)
608  {
609  foreach (EntityGrid grid in EntityGrids)
610  {
611  if (grid.Submarine != submarine) continue;
612 
613  rect.Location -= MathUtils.ToPoint(submarine.HiddenSubPosition);
614 
615  grid.InsertEntity(this);
616 
617  rect.Location += MathUtils.ToPoint(submarine.HiddenSubPosition);
618  return;
619  }
620  }
621 
622  public int GetWaveIndex(Vector2 position)
623  {
624  return GetWaveIndex(position.X);
625  }
626 
627  public int GetWaveIndex(float xPos)
628  {
629  int index = (int)(xPos - rect.X) / WaveWidth;
630  index = (int)MathHelper.Clamp(index, 0, waveY.Length - 1);
631  return index;
632  }
633 
634  public override void Move(Vector2 amount, bool ignoreContacts = true)
635  {
636  if (!MathUtils.IsValid(amount))
637  {
638  DebugConsole.ThrowError($"Attempted to move a hull by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}");
639  return;
640  }
641 
642  rect.X += (int)amount.X;
643  rect.Y += (int)amount.Y;
644 
645  if (Submarine == null || !Submarine.Loading)
646  {
647  Item.UpdateHulls();
648  Gap.UpdateHulls();
649  }
650 
651  surface = rect.Y - rect.Height + WaterVolume / rect.Width;
652 #if CLIENT
653  drawSurface = surface;
654 #endif
655  Pressure = surface;
656  }
657 
658  public override void ShallowRemove()
659  {
660  base.Remove();
661  HullList.Remove(this);
662 
663  if (Submarine == null || (!Submarine.Loading && !Submarine.Unloading))
664  {
665  Item.UpdateHulls();
666  Gap.UpdateHulls();
667  }
668 
669  List<FireSource> fireSourcesToRemove = new List<FireSource>(FireSources);
670  fireSourcesToRemove.AddRange(FakeFireSources);
671  foreach (FireSource fireSource in fireSourcesToRemove)
672  {
673  fireSource.Remove();
674  }
675  FireSources.Clear();
676  FakeFireSources.Clear();
677 
678  if (EntityGrids != null)
679  {
680  foreach (EntityGrid entityGrid in EntityGrids)
681  {
682  entityGrid.RemoveEntity(this);
683  }
684  }
685  }
686 
687  public override void Remove()
688  {
689  base.Remove();
690  HullList.Remove(this);
691  BallastFlora?.Remove();
692 
693  if (Submarine != null && !Submarine.Loading && !Submarine.Unloading)
694  {
695  Item.UpdateHulls();
696  Gap.UpdateHulls();
697  }
698 
699  BackgroundSections?.Clear();
700 
701  List<FireSource> fireSourcesToRemove = new List<FireSource>(FireSources);
702  foreach (FireSource fireSource in fireSourcesToRemove)
703  {
704  fireSource.Remove();
705  }
706  FireSources.Clear();
707 
708  if (EntityGrids != null)
709  {
710  foreach (EntityGrid entityGrid in EntityGrids)
711  {
712  entityGrid.RemoveEntity(this);
713  }
714  }
715  }
716 
717  public void AddFireSource(FireSource fireSource)
718  {
719  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
720  {
721  //clients aren't allowed to create fire sources in hulls whose IDs have been freed (dynamic hulls between docking ports), because they can't be synced
722  if (IdFreed) { return; }
723  }
724  if (fireSource is DummyFireSource dummyFire)
725  {
726  FakeFireSources.Add(dummyFire);
727  }
728  else
729  {
730  FireSources.Add(fireSource);
731  }
732  }
733 
734  public Decal AddDecal(UInt32 decalId, Vector2 worldPosition, float scale, bool isNetworkEvent, int? spriteIndex = null)
735  {
736  //clients are only allowed to create decals when the server says so
737  if (!isNetworkEvent && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
738  {
739  return null;
740  }
741 
742  var decal = DecalManager.Prefabs.Find(p => p.UintIdentifier == decalId);
743  if (decal == null)
744  {
745  DebugConsole.ThrowError($"Could not find a decal prefab with the UInt identifier {decalId}!");
746  return null;
747  }
748  return AddDecal(decal.Name, worldPosition, scale, isNetworkEvent, spriteIndex);
749  }
750 
751 
752  public Decal AddDecal(string decalName, Vector2 worldPosition, float scale, bool isNetworkEvent, int? spriteIndex = null)
753  {
754  //clients are only allowed to create decals when the server says so
755  if (!isNetworkEvent && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
756  {
757  return null;
758  }
759 
760  if (decals.Count >= MaxDecalsPerHull) { return null; }
761 
762  var decal = DecalManager.CreateDecal(decalName, scale, worldPosition, this, spriteIndex);
763  if (decal != null)
764  {
765  if (GameMain.NetworkMember is { IsServer: true })
766  {
767  GameMain.NetworkMember.CreateEntityEvent(this, new DecalEventData());
768  }
769  decals.Add(decal);
770  }
771 
772  return decal;
773  }
774 
775  #region Shared network write
776  private void SharedStatusWrite(IWriteMessage msg)
777  {
778  msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8);
779 
780  msg.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16);
781  for (int i = 0; i < Math.Min(FireSources.Count, 16); i++)
782  {
783  var fireSource = FireSources[i];
784  Vector2 normalizedPos = new Vector2(
785  (fireSource.Position.X - rect.X) / rect.Width,
786  (fireSource.Position.Y - (rect.Y - rect.Height)) / rect.Height);
787 
788  msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.X, 0.0f, 1.0f), 0.0f, 1.0f, 8);
789  msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.Y, 0.0f, 1.0f), 0.0f, 1.0f, 8);
790  msg.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8);
791  }
792  }
793 
794  private void SharedBackgroundSectionsWrite(IWriteMessage msg, in BackgroundSectionsEventData backgroundSectionsEventData)
795  {
796  int sectorToUpdate = backgroundSectionsEventData.SectorStartIndex;
797  int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent;
798  int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1);
799  msg.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1);
800  for (int i = start; i < end; i++)
801  {
802  msg.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8);
803  msg.WriteUInt32(BackgroundSections[i].Color.PackedValue);
804  }
805  }
806  #endregion
807 
808  #region Shared network read
809  public readonly struct NetworkFireSource
810  {
811  public readonly Vector2 Position;
812  public readonly float Size;
813 
814  public NetworkFireSource(Hull hull, Vector2 normalizedPosition, float normalizedSize)
815  {
816  Position = hull.Rect.Location.ToVector2()
817  + new Vector2(0, -hull.Rect.Height)
818  + normalizedPosition * hull.Rect.Size.ToVector2();
819  Size = normalizedSize * hull.Rect.Width;
820  }
821  }
822 
823  private void SharedStatusRead(IReadMessage msg, out float newWaterVolume, out NetworkFireSource[] newFireSources)
824  {
825  newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume;
826 
827  int fireSourceCount = msg.ReadRangedInteger(0, 16);
828  newFireSources = new NetworkFireSource[fireSourceCount];
829  for (int i = 0; i < fireSourceCount; i++)
830  {
831  float x = MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f);
832  float y = MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f);
833  float size = msg.ReadRangedSingle(0.0f, 1.0f, 8);
834  newFireSources[i] = new NetworkFireSource(this, new Vector2(x, y), size);
835  }
836  }
837 
838  private readonly struct BackgroundSectionNetworkUpdate
839  {
840  public readonly int SectionIndex;
841  public readonly Color Color;
842  public readonly float ColorStrength;
843  public BackgroundSectionNetworkUpdate(int sectionIndex, Color color, float colorStrength)
844  {
845  SectionIndex = sectionIndex;
846  Color = color;
847  ColorStrength = colorStrength;
848  }
849  }
850 
851  private void SharedBackgroundSectionRead(IReadMessage msg, Action<BackgroundSectionNetworkUpdate> action, out int sectorToUpdate)
852  {
853  sectorToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1);
854  int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent;
855  int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1);
856  for (int i = start; i < end; i++)
857  {
858  float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8);
859  Color color = new Color(msg.ReadUInt32());
860 
861  action(new BackgroundSectionNetworkUpdate(i, color, colorStrength));
862  }
863  }
864  #endregion
865 
866  public override void Update(float deltaTime, Camera cam)
867  {
868  BallastFlora?.Update(deltaTime);
869 
870  UpdateProjSpecific(deltaTime, cam);
871 
872  Oxygen -= OxygenDeteriorationSpeed * deltaTime;
873 
874  if (FakeFireSources.Count > 0)
875  {
876  if ((Character.Controlled?.CharacterHealth?.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
877  {
878  for (int i = FakeFireSources.Count - 1; i >= 0; i--)
879  {
880  if (FakeFireSources[i].CausedByPsychosis)
881  {
882  FakeFireSources[i].Remove();
883  }
884  }
885  }
887  }
888 
889  FireSource.UpdateAll(FireSources, deltaTime);
890 
891  foreach (Decal decal in decals)
892  {
893  decal.Update(deltaTime);
894  }
895  //clients don't remove decals unless the server says so
896  if (GameMain.NetworkMember is not { IsClient: true })
897  {
898  for (int i = decals.Count - 1; i >= 0; i--)
899  {
900  var decal = decals[i];
901  if (decal.FadeTimer >= decal.LifeTime || decal.BaseAlpha <= 0.001f)
902  {
903  decals.RemoveAt(i);
904  #if SERVER
905  decalUpdatePending = true;
906  #endif
907  }
908  }
909  }
910 
911  if (aiTarget != null)
912  {
913  aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : MathHelper.Lerp(aiTarget.MinSightRange, aiTarget.MaxSightRange, Submarine.Velocity.Length() / 10);
914  aiTarget.SoundRange -= deltaTime * 1000.0f;
915  }
916 
917  if (!update)
918  {
919  lethalPressure = 0.0f;
920  return;
921  }
922 
923  float waterDepth = WaterVolume / rect.Width;
924  if (waterDepth < 1.0f)
925  {
926  //if there's only a minuscule amount of water, consider the surface to be at the bottom of the hull
927  //otherwise unnoticeable amounts of water can for example cause magnesium to explode
928  waterDepth = 0.0f;
929  }
930 
931  surface = Math.Max(MathHelper.Lerp(
932  surface,
933  rect.Y - rect.Height + waterDepth,
934  deltaTime * 10.0f), rect.Y - rect.Height);
935 
936  for (int i = 0; i < waveY.Length; i++)
937  {
938  //apply velocity
939  waveY[i] = waveY[i] + waveVel[i];
940 
941  //if the wave attempts to go "through" the top of the hull, make it bounce back
942  if (surface + waveY[i] > rect.Y)
943  {
944  float excess = (surface + waveY[i]) - rect.Y;
945  waveY[i] -= excess;
946  waveVel[i] = waveVel[i] * -0.5f;
947  }
948  //if the wave attempts to go "through" the bottom of the hull, make it bounce back
949  else if (surface + waveY[i] < rect.Y - rect.Height)
950  {
951  float excess = (surface + waveY[i]) - (rect.Y - rect.Height);
952  waveY[i] -= excess;
953  waveVel[i] = waveVel[i] * -0.5f;
954  }
955 
956  //acceleration
957  float a = -WaveStiffness * waveY[i] - waveVel[i] * WaveDampening;
958  waveVel[i] = waveVel[i] + a;
959  }
960 
961  //apply spread (two iterations)
962  for (int j = 0; j < 2; j++)
963  {
964  for (int i = 1; i < waveY.Length - 1; i++)
965  {
966  leftDelta[i] = WaveSpread * (waveY[i] - waveY[i - 1]);
967  waveVel[i - 1] += leftDelta[i];
968 
969  rightDelta[i] = WaveSpread * (waveY[i] - waveY[i + 1]);
970  waveVel[i + 1] += rightDelta[i];
971  }
972  }
973 
974  //make waves propagate through horizontal gaps
975  foreach (Gap gap in ConnectedGaps)
976  {
977  if (this != gap.linkedTo.FirstOrDefault() as Hull)
978  {
979  //let the first linked hull handle the water propagation
980  continue;
981  }
982 
983  if (!gap.IsRoomToRoom || !gap.IsHorizontal || gap.Open <= 0.0f) { continue; }
984  if (surface > gap.Rect.Y || surface < gap.Rect.Y - gap.Rect.Height) { continue; }
985 
986  // ReSharper refuses to compile this if it's using "as Hull" since "as" means it can be null and you can't compare null to true or false
987  Hull hull2 = this == gap.linkedTo[0] ? (Hull)gap.linkedTo[1] : (Hull)gap.linkedTo[0];
988  float otherSurfaceY = hull2.surface;
989  if (otherSurfaceY > gap.Rect.Y || otherSurfaceY < gap.Rect.Y - gap.Rect.Height) { continue; }
990 
991  float surfaceDiff = (surface - otherSurfaceY) * gap.Open;
992  for (int j = 0; j < 2; j++)
993  {
994  rightDelta[waveY.Length - 1] = WaveSpread * (hull2.waveY[0] - waveY[waveY.Length - 1] - surfaceDiff) * 0.5f;
995  waveVel[waveY.Length - 1] += rightDelta[waveY.Length - 1];
996  waveY[waveY.Length - 1] += rightDelta[waveY.Length - 1];
997 
998  hull2.leftDelta[0] = WaveSpread * (waveY[waveY.Length - 1] - hull2.waveY[0] + surfaceDiff) * 0.5f;
999  hull2.waveVel[0] += hull2.leftDelta[0];
1000  hull2.waveY[0] += hull2.leftDelta[0];
1001  }
1002 
1003  if (surfaceDiff < 32.0f)
1004  {
1005  //update surfaces to the same level
1006  hull2.waveY[0] = surfaceDiff * 0.5f;
1007  waveY[waveY.Length - 1] = -surfaceDiff * 0.5f;
1008  }
1009  }
1010 
1011 
1012  //apply spread (two iterations)
1013  for (int j = 0; j < 2; j++)
1014  {
1015  for (int i = 1; i < waveY.Length - 1; i++)
1016  {
1017  waveY[i - 1] += leftDelta[i];
1018  waveY[i + 1] += rightDelta[i];
1019  }
1020  }
1021 
1022  if (waterVolume < Volume)
1023  {
1024  //pressure drop speed is inversely proportionate to water percentage
1025  //= pressure drops very fast if the hull is nowhere near full
1026  float waterVolumeFactor = Math.Max((100.0f - WaterPercentage) / 10.0f, 1.0f);
1027  LethalPressure -=
1028  PressureDropSpeed * waterVolumeFactor * deltaTime;
1029  if (WaterVolume <= 0.0f)
1030  {
1031 #if CLIENT
1032  //wait for the surface to be lerped back to bottom and the waves to settle until disabling update
1033  if (drawSurface > rect.Y - rect.Height + 1) { return; }
1034 #endif
1035  for (int i = 1; i < waveY.Length - 1; i++)
1036  {
1037  if (waveY[i] > 0.1f) { return; }
1038  }
1039 
1040  update = false;
1041  }
1042  }
1043  }
1044 
1045  partial void UpdateProjSpecific(float deltaTime, Camera cam);
1046 
1047  public void ApplyFlowForces(float deltaTime, Item item)
1048  {
1049  if (item.body.Mass <= 0.0f)
1050  {
1051  return;
1052  }
1053  foreach (var gap in ConnectedGaps.Where(gap => gap.Open > 0))
1054  {
1055  var distance = MathHelper.Max(Vector2.DistanceSquared(item.Position, gap.Position) / 1000, 1f);
1056  Vector2 force = (gap.LerpedFlowForce / distance) * deltaTime;
1057  if (force.LengthSquared() > 0.01f)
1058  {
1059  item.body.ApplyForce(force);
1060  }
1061  }
1062  }
1063 
1064  public void Extinguish(float deltaTime, float amount, Vector2 position, bool extinguishRealFires = true, bool extinguishFakeFires = true)
1065  {
1066  if (extinguishRealFires)
1067  {
1068  for (int i = FireSources.Count - 1; i >= 0; i--)
1069  {
1070  FireSources[i].Extinguish(deltaTime, amount, position);
1071  }
1072  }
1073  if (extinguishFakeFires)
1074  {
1075  for (int i = FakeFireSources.Count - 1; i >= 0; i--)
1076  {
1077  FakeFireSources[i].Extinguish(deltaTime, amount, position);
1078  }
1079  }
1080  }
1081 
1082  public void RemoveFire(FireSource fire)
1083  {
1084  FireSources.Remove(fire);
1085  if (fire is DummyFireSource dummyFire)
1086  {
1087  FakeFireSources.Remove(dummyFire);
1088  }
1089  }
1090 
1091  private readonly HashSet<Hull> adjacentHulls = new HashSet<Hull>();
1092  public IEnumerable<Hull> GetConnectedHulls(bool includingThis, int? searchDepth = null, bool ignoreClosedGaps = false)
1093  {
1094  adjacentHulls.Clear();
1095  int startStep = 0;
1096  searchDepth ??= 100;
1097  GetAdjacentHulls(adjacentHulls, ref startStep, searchDepth.Value, ignoreClosedGaps);
1098  if (!includingThis) { adjacentHulls.Remove(this); }
1099  return adjacentHulls;
1100  }
1101 
1102  private void GetAdjacentHulls(HashSet<Hull> connectedHulls, ref int step, int searchDepth, bool ignoreClosedGaps = false)
1103  {
1104  connectedHulls.Add(this);
1105  if (step > searchDepth) { return; }
1106  foreach (Gap g in ConnectedGaps)
1107  {
1108  if (ignoreClosedGaps && g.Open <= 0.0f) { continue; }
1109  for (int i = 0; i < 2 && i < g.linkedTo.Count; i++)
1110  {
1111  if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull))
1112  {
1113  step++;
1114  hull.GetAdjacentHulls(connectedHulls, ref step, searchDepth, ignoreClosedGaps);
1115  }
1116  }
1117  }
1118  }
1119 
1124  public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance, float distanceMultiplierPerClosedDoor = 0)
1125  {
1126  return GetApproximateHullDistance(startPos, endPos, new HashSet<Hull>(), targetHull, 0.0f, maxDistance, distanceMultiplierPerClosedDoor);
1127  }
1128 
1129  private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet<Hull> connectedHulls, Hull target, float distance, float maxDistance, float distanceMultiplierFromDoors = 0)
1130  {
1131  if (distance >= maxDistance) { return float.MaxValue; }
1132  if (this == target)
1133  {
1134  return distance + Vector2.Distance(startPos, endPos);
1135  }
1136 
1137  connectedHulls.Add(this);
1138 
1139  foreach (Gap g in ConnectedGaps)
1140  {
1141  float distanceMultiplier = 1;
1142  if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken)
1143  {
1144  //gap blocked if the door is closed, and we haven't made any predictions of it opening client-side
1145  if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.PredictedState.HasValue) ||
1146  //OR we've predicted that the door is closed client-side
1147  (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
1148  {
1149  if (g.ConnectedDoor.OpenState < 0.1f)
1150  {
1151  if (distanceMultiplierFromDoors <= 0) { continue; }
1152  distanceMultiplier *= distanceMultiplierFromDoors;
1153  }
1154  }
1155  }
1156  else if (g.Open <= 0.0f)
1157  {
1158  continue;
1159  }
1160 
1161  for (int i = 0; i < 2 && i < g.linkedTo.Count; i++)
1162  {
1163  if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull))
1164  {
1165  float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position) * distanceMultiplier, maxDistance);
1166  if (dist < float.MaxValue)
1167  {
1168  return dist;
1169  }
1170  }
1171  }
1172  }
1173 
1174  return float.MaxValue;
1175  }
1176 
1184  public static Hull FindHull(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
1185  {
1186  if (EntityGrids == null)
1187  {
1188  return null;
1189  }
1190 
1191  if (guess != null)
1192  {
1193  if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive))
1194  {
1195  return guess;
1196  }
1197  }
1198 
1199  foreach (EntityGrid entityGrid in EntityGrids)
1200  {
1201  if (entityGrid.Submarine != null && !entityGrid.Submarine.Loading)
1202  {
1203  System.Diagnostics.Debug.Assert(!entityGrid.Submarine.Removed);
1204  Rectangle borders = entityGrid.Submarine.Borders;
1205  if (useWorldCoordinates)
1206  {
1207  Vector2 worldPos = entityGrid.Submarine.WorldPosition;
1208  borders.Location += new Point((int)worldPos.X, (int)worldPos.Y);
1209  }
1210  else
1211  {
1212  borders.Location += new Point((int)entityGrid.Submarine.HiddenSubPosition.X, (int)entityGrid.Submarine.HiddenSubPosition.Y);
1213  }
1214 
1215  const float padding = 128.0f;
1216  if (position.X < borders.X - padding || position.X > borders.Right + padding ||
1217  position.Y > borders.Y + padding || position.Y < borders.Y - borders.Height - padding)
1218  {
1219  continue;
1220  }
1221  }
1222  Vector2 transformedPosition = position;
1223  if (useWorldCoordinates && entityGrid.Submarine != null)
1224  {
1225  transformedPosition -= entityGrid.Submarine.Position;
1226  }
1227  var entities = entityGrid.GetEntities(transformedPosition);
1228  if (entities == null) { continue; }
1229  foreach (Hull hull in entities)
1230  {
1231  if (Submarine.RectContains(hull.rect, transformedPosition, inclusive))
1232  {
1233  return hull;
1234  }
1235  }
1236  }
1237 
1238  return null;
1239  }
1240 
1246  public static Hull FindHullUnoptimized(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
1247  {
1248  if (guess != null && HullList.Contains(guess))
1249  {
1250  if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess;
1251  }
1252 
1253  foreach (Hull hull in HullList)
1254  {
1255  if (Submarine.RectContains(useWorldCoordinates ? hull.WorldRect : hull.rect, position, inclusive)) return hull;
1256  }
1257 
1258  return null;
1259  }
1260 
1261  public static void DetectItemVisibility(Character c=null)
1262  {
1263  if (c==null)
1264  {
1265  foreach (Item it in Item.ItemList)
1266  {
1267  it.Visible = true;
1268  }
1269  }
1270  else
1271  {
1272  Hull h = c.CurrentHull;
1273  HullList.ForEach(j => j.Visible = false);
1274  List<Hull> visibleHulls;
1275  if (h == null || c.Submarine == null)
1276  {
1277  visibleHulls = HullList.FindAll(j => j.CanSeeOther(null, false));
1278  }
1279  else
1280  {
1281  visibleHulls = HullList.FindAll(j => h.CanSeeOther(j, true));
1282  }
1283  visibleHulls.ForEach(j => j.Visible = true);
1284  foreach (Item it in Item.ItemList)
1285  {
1286  if (it.CurrentHull == null || visibleHulls.Contains(it.CurrentHull)) it.Visible = true;
1287  else it.Visible = false;
1288  }
1289  }
1290  }
1291 
1292  private bool CanSeeOther(Hull other, bool allowIndirect = true)
1293  {
1294  if (other == this) return true;
1295 
1296  if (other != null && other.Submarine == Submarine)
1297  {
1298  bool retVal = false;
1299  foreach (Gap g in ConnectedGaps)
1300  {
1301  if (g.ConnectedWall != null && g.ConnectedWall.CastShadow) continue;
1302  List<Hull> otherHulls = HullList.FindAll(h => h.ConnectedGaps.Contains(g) && h != this);
1303  retVal = otherHulls.Any(h => h == other);
1304  if (!retVal && allowIndirect) retVal = otherHulls.Any(h => h.CanSeeOther(other, false));
1305  if (retVal) return true;
1306  }
1307  }
1308  else
1309  {
1310  foreach (Gap g in ConnectedGaps)
1311  {
1312  if (g.ConnectedDoor != null && !HullList.Any(h => h.ConnectedGaps.Contains(g) && h != this)) return true;
1313  }
1314  List<MapEntity> structures = MapEntityList.FindAll(me => me is Structure && me.Rect.Intersects(Rect));
1315  return structures.Any(st => !(st as Structure).CastShadow);
1316  }
1317  return false;
1318  }
1319 
1320  public string CreateRoomName()
1321  {
1322  List<string> roomItems = new List<string>();
1323  foreach (Item item in Item.ItemList)
1324  {
1325  if (item.CurrentHull != this) continue;
1326  if (item.GetComponent<Items.Components.Reactor>() != null) roomItems.Add("reactor");
1327  if (item.GetComponent<Items.Components.Engine>() != null) roomItems.Add("engine");
1328  if (item.GetComponent<Items.Components.Steering>() != null) roomItems.Add("steering");
1329  if (item.GetComponent<Items.Components.Sonar>() != null) roomItems.Add("sonar");
1330  if (item.HasTag(Tags.Ballast)) roomItems.Add("ballast");
1331  }
1332 
1333  if (roomItems.Contains("reactor"))
1334  return "RoomName.ReactorRoom";
1335  else if (roomItems.Contains("engine"))
1336  return "RoomName.EngineRoom";
1337  else if (roomItems.Contains("steering") && roomItems.Contains("sonar"))
1338  return "RoomName.CommandRoom";
1339  else if (roomItems.Contains("ballast"))
1340  return "RoomName.Ballast";
1341 
1342  var moduleFlags = Submarine?.Info?.OutpostModuleInfo?.ModuleFlags ?? this.moduleTags;
1343 
1344  if (moduleFlags != null && moduleFlags.Any() &&
1345  (Submarine.Info.Type == SubmarineType.OutpostModule || Submarine.Info.Type == SubmarineType.Outpost))
1346  {
1347  if (moduleFlags.Contains(Tags.Airlock) &&
1348  ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null))
1349  {
1350  return "RoomName.Airlock";
1351  }
1352  }
1353  else
1354  {
1355  if (ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null))
1356  {
1357  return "RoomName.Airlock";
1358  }
1359  }
1360 
1361  Rectangle subRect = Submarine.Borders;
1362 
1363  Alignment roomPos;
1364  if (rect.Y - rect.Height / 2 > subRect.Y + subRect.Height * 0.66f)
1365  roomPos = Alignment.Top;
1366  else if (rect.Y - rect.Height / 2 > subRect.Y + subRect.Height * 0.33f)
1367  roomPos = Alignment.CenterY;
1368  else
1369  roomPos = Alignment.Bottom;
1370 
1371  if (rect.Center.X < subRect.X + subRect.Width * 0.33f)
1372  roomPos |= Alignment.Left;
1373  else if (rect.Center.X < subRect.X + subRect.Width * 0.66f)
1374  roomPos |= Alignment.CenterX;
1375  else
1376  roomPos |= Alignment.Right;
1377 
1378  return "RoomName.Sub" + roomPos.ToString();
1379  }
1380 
1384  private void DetermineIsAirlock()
1385  {
1386  if (RoomName != null && RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase))
1387  {
1388  IsAirlock = true;
1389  return;
1390  }
1391  else
1392  {
1393  var airlockTag = "airlock".ToIdentifier();
1394  foreach (Item item in Item.ItemList)
1395  {
1396  if (item.CurrentHull != this && item.HasTag(airlockTag))
1397  {
1398  IsAirlock = true;
1399  return;
1400  }
1401  }
1402  }
1403  IsAirlock = false;
1404  }
1405 
1410  public bool LeadsOutside(Character character)
1411  {
1412  foreach (var gap in ConnectedGaps)
1413  {
1414  if (gap.ConnectedDoor == null) { continue; }
1415  if (gap.IsRoomToRoom) { continue; }
1416  if (!gap.ConnectedDoor.CanBeTraversed && (character == null || !gap.ConnectedDoor.HasAccess(character))) { continue; }
1417  return true;
1418  }
1419  return false;
1420  }
1421 
1422 #region BackgroundSections
1423  private void CreateBackgroundSections()
1424  {
1425  int sectionWidth, sectionHeight;
1426 
1427  sectionWidth = sectionHeight = BackgroundSectionSize;
1428 
1429  xBackgroundMax = rect.Width / sectionWidth;
1430  yBackgroundMax = rect.Height / sectionHeight;
1431 
1432  BackgroundSections = new List<BackgroundSection>(xBackgroundMax * yBackgroundMax);
1433 
1434  int sections = xBackgroundMax * yBackgroundMax;
1435  float xSectors = xBackgroundMax / (float)sectorWidth;
1436 
1437  for (int y = 0; y < yBackgroundMax; y++)
1438  {
1439  for (int x = 0; x < xBackgroundMax; x++)
1440  {
1441  ushort index = (ushort)BackgroundSections.Count;
1442  int sector = (int)Math.Floor(index / (float)sectorWidth - xSectors * y) + y / sectorHeight * (int)Math.Ceiling(xSectors);
1443  BackgroundSections.Add(new BackgroundSection(new Rectangle(x * sectionWidth, y * -sectionHeight, sectionWidth, sectionHeight), index, (ushort)y));
1444  }
1445  }
1446 
1447 #if CLIENT
1448  minimumPaintAmountToDraw = maxColorStrength / BackgroundSections.Count;
1449 #endif
1450  }
1451 
1452  public static Hull GetCleanTarget(Vector2 worldPosition)
1453  {
1454  foreach (Hull hull in HullList)
1455  {
1456  Rectangle worldRect = hull.WorldRect;
1457  if (worldPosition.X < worldRect.X || worldPosition.X > worldRect.Right) { continue; }
1458  if (worldPosition.Y > worldRect.Y || worldPosition.Y < worldRect.Y - worldRect.Height) { continue; }
1459  return hull;
1460  }
1461  return null;
1462  }
1463 
1464  public BackgroundSection GetBackgroundSection(Vector2 worldPosition)
1465  {
1466  if (!SupportsPaintedColors) { return null; }
1467 
1468  Vector2 subOffset = Submarine == null ? Vector2.Zero : Submarine.Position;
1469  Vector2 relativePosition = new Vector2(worldPosition.X - subOffset.X - rect.X, worldPosition.Y - subOffset.Y - rect.Y);
1470 
1471  int xIndex = (int)Math.Floor(relativePosition.X / BackgroundSectionSize);
1472  if (xIndex < 0 || xIndex >= xBackgroundMax) { return null; }
1473  int yIndex = (int)Math.Floor(-relativePosition.Y / BackgroundSectionSize);
1474  if (yIndex < 0 || yIndex >= yBackgroundMax) { return null; }
1475 
1476  return BackgroundSections[xIndex + yIndex * xBackgroundMax];
1477  }
1478 
1479  public IEnumerable<BackgroundSection> GetBackgroundSectionsViaContaining(Rectangle rectArea)
1480  {
1481  if (BackgroundSections == null || BackgroundSections.Count == 0)
1482  {
1483  yield break;
1484  }
1485  else
1486  {
1487  int xMin = Math.Max(rectArea.X / BackgroundSectionSize, 0);
1488  if (xMin >= xBackgroundMax) { yield break; }
1489  int xMax = Math.Min(rectArea.Right / BackgroundSectionSize, xBackgroundMax - 1);
1490  if (xMax < 0) { yield break; }
1491 
1492  int yMin = Math.Max(-rectArea.Bottom / BackgroundSectionSize, 0);
1493  if (yMin >= yBackgroundMax) { yield break; }
1494  int yMax = Math.Min(-rectArea.Y / BackgroundSectionSize, yBackgroundMax - 1);
1495  if (yMax < 0) { yield break; }
1496 
1497  for (int x = xMin; x <= xMax; x++)
1498  {
1499  for (int y = yMin; y <= yMax; y++)
1500  {
1501  yield return BackgroundSections[x + y * xBackgroundMax];
1502  }
1503  }
1504  }
1505  }
1506 
1507  public bool DoesSectionMatch(int index, int row)
1508  {
1509  return index >= 0 && row >= 0 && BackgroundSections.Count > index && BackgroundSections[index] != null && BackgroundSections[index].RowIndex == row;
1510  }
1511 
1512  public void IncreaseSectionColorOrStrength(BackgroundSection section, Color? color, float? strength, bool requiresUpdate, bool isCleaning)
1513  {
1514  bool sectionUpdated = isCleaning;
1515  if (color != null)
1516  {
1517  if (section.Color != color.Value && strength.HasValue)
1518  {
1519  //already painted with a different color -> interpolate towards the new one
1520 
1521  //an ad-hoc formula that makes the color changes faster when the current strength is low
1522  //(-> a barely dirty wall gets recolored almost immediately, while a more heavily colored one takes a while)
1523  float changeSpeed = strength.Value / Math.Max(section.ColorStrength * section.ColorStrength, 0.001f) * 0.1f;
1524  if (section.LerpColor(color.Value, changeSpeed)) { sectionUpdated = true; }
1525  }
1526  else
1527  {
1528  if (section.SetColor(color.Value)) { sectionUpdated = true; }
1529  }
1530  }
1531 
1532  if (strength != null)
1533  {
1534  float previous = section.SetColorStrength(Math.Max(minColorStrength, Math.Min(maxColorStrength, section.ColorStrength + strength.Value)));
1535  if (previous != -1f)
1536  {
1537 #if CLIENT
1538  paintAmount = Math.Max(0, paintAmount + (section.ColorStrength - previous) / BackgroundSections.Count);
1539 #endif
1540  sectionUpdated = true;
1541  }
1542  RefreshAveragePaintedColor();
1543  }
1544 
1545  if (sectionUpdated && GameMain.NetworkMember != null && requiresUpdate)
1546  {
1547  networkUpdatePending = true;
1548  pendingSectionUpdates.Add((int)Math.Floor(section.Index / (float)BackgroundSectionsPerNetworkEvent));
1549 #if CLIENT
1550  serverUpdateDelay = 0.5f;
1551 #endif
1552  }
1553  }
1554 
1555  private void RefreshAveragePaintedColor()
1556  {
1557  Vector4 avgColor = Vector4.Zero;
1558  foreach (var anySection in BackgroundSections)
1559  {
1560  avgColor += anySection.Color.ToVector4();
1561  }
1562  avgColor /= BackgroundSections.Count;
1563  AveragePaintedColor = new Color(avgColor);
1564  }
1565 
1566  public void SetSectionColorOrStrength(BackgroundSection section, Color? color, float? strength)
1567  {
1568  if (color != null)
1569  {
1570  section.SetColor(color.Value);
1571  }
1572 
1573  if (strength != null)
1574  {
1575  float previous = section.SetColorStrength(Math.Max(minColorStrength, Math.Min(maxColorStrength, section.ColorStrength + strength.Value)));
1576  if (previous != -1f)
1577  {
1578 #if CLIENT
1579  paintAmount = Math.Max(0, paintAmount + (section.ColorStrength - previous) / BackgroundSections.Count);
1580 #endif
1581  }
1582  }
1583  }
1584 
1585  public void CleanSection(BackgroundSection section, float cleanVal, bool updateRequired)
1586  {
1587  bool decalsCleaned = false;
1588  foreach (Decal decal in decals)
1589  {
1590  if (decal.AffectsSection(section))
1591  {
1592  decal.Clean(cleanVal);
1593  decalsCleaned = true;
1594 #if SERVER
1595  decalUpdatePending = true;
1596 #elif CLIENT
1597  pendingDecalUpdates.Add(decal);
1598  networkUpdatePending = true;
1599 #endif
1600  }
1601  }
1602 
1603  if (section.ColorStrength == 0 && !decalsCleaned) { return; }
1604  IncreaseSectionColorOrStrength(section, null, cleanVal, updateRequired, true);
1605  }
1606 #endregion
1607 
1608  public static Hull Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
1609  {
1610  Rectangle rect;
1611  if (element.GetAttribute("rect") != null)
1612  {
1613  rect = element.GetAttributeRect("rect", Rectangle.Empty);
1614  }
1615  else
1616  {
1617  //backwards compatibility
1618  rect = new Rectangle(
1619  int.Parse(element.GetAttribute("x").Value),
1620  int.Parse(element.GetAttribute("y").Value),
1621  int.Parse(element.GetAttribute("width").Value),
1622  int.Parse(element.GetAttribute("height").Value));
1623  }
1624 
1625  var hull = new Hull(rect, submarine, idRemap.GetOffsetId(element))
1626  {
1627  WaterVolume = element.GetAttributeFloat("water", 0.0f)
1628  };
1629  hull.linkedToID = new List<ushort>();
1630 
1631  hull.ParseLinks(element, idRemap);
1632 
1633  string originalAmbientLight = element.GetAttributeString("originalambientlight", null);
1634  if (!string.IsNullOrWhiteSpace(originalAmbientLight))
1635  {
1636  hull.OriginalAmbientLight = XMLExtensions.ParseColor(originalAmbientLight, false);
1637  }
1638 
1639  foreach (var subElement in element.Elements())
1640  {
1641  switch (subElement.Name.ToString().ToLowerInvariant())
1642  {
1643  case "decal":
1644  string id = subElement.GetAttributeString("id", "");
1645  Vector2 pos = subElement.GetAttributeVector2("pos", Vector2.Zero);
1646  float scale = subElement.GetAttributeFloat("scale", 1.0f);
1647  float timer = subElement.GetAttributeFloat("timer", 1.0f);
1648  float baseAlpha = subElement.GetAttributeFloat("alpha", 1.0f);
1649  var decal = hull.AddDecal(id, pos + hull.WorldRect.Location.ToVector2(), scale, true);
1650  if (decal != null)
1651  {
1652  decal.FadeTimer = timer;
1653  decal.BaseAlpha = baseAlpha;
1654  }
1655  break;
1656  case "ballastflorabehavior":
1657  Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
1658  BallastFloraPrefab prefab = BallastFloraPrefab.Find(identifier);
1659  if (prefab != null)
1660  {
1661  hull.BallastFlora = new BallastFloraBehavior(hull, prefab, Vector2.Zero);
1662  hull.BallastFlora.LoadSave(subElement, idRemap);
1663  }
1664  break;
1665  }
1666  }
1667 
1668  string backgroundSectionStr = element.GetAttributeString("backgroundsections", "");
1669  if (!string.IsNullOrEmpty(backgroundSectionStr))
1670  {
1671  string[] backgroundSectionStrSplit = backgroundSectionStr.Split(';');
1672  foreach (string str in backgroundSectionStrSplit)
1673  {
1674  string[] backgroundSectionData = str.Split(':');
1675  if (backgroundSectionData.Length != 3) { continue; }
1676  Color color = XMLExtensions.ParseColor(backgroundSectionData[1]);
1677  if (int.TryParse(backgroundSectionData[0], out int index) &&
1678  float.TryParse(backgroundSectionData[2], NumberStyles.Any, CultureInfo.InvariantCulture, out float strength))
1679  {
1680  hull.SetSectionColorOrStrength(hull.BackgroundSections[index], color, strength);
1681  }
1682  }
1683  }
1684  hull.RefreshAveragePaintedColor();
1685 
1687  if (element.GetAttribute("oxygen") == null) { hull.Oxygen = hull.Volume; }
1688 
1689  return hull;
1690  }
1691 
1692  public override XElement Save(XElement parentElement)
1693  {
1694  if (Submarine == null)
1695  {
1696  string errorMsg = "Error - tried to save a hull that's not a part of any submarine.\n" + Environment.StackTrace.CleanupStackTrace();
1697  DebugConsole.ThrowError(errorMsg);
1698  GameAnalyticsManager.AddErrorEventOnce("Hull.Save:WorldHull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1699  return null;
1700  }
1701 
1702  XElement element = new XElement("Hull");
1703  element.Add
1704  (
1705  new XAttribute("ID", ID),
1706  new XAttribute("rect",
1707  (int)(rect.X - Submarine.HiddenSubPosition.X) + "," +
1708  (int)(rect.Y - Submarine.HiddenSubPosition.Y) + "," +
1709  rect.Width + "," + rect.Height),
1710  new XAttribute("water", waterVolume)
1711  );
1712 
1713  if (linkedTo != null && linkedTo.Count > 0)
1714  {
1715  var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved && (l.Removed == Removed)).ToList();
1716  element.Add(new XAttribute("linked", string.Join(",", saveableLinked.Select(l => l.ID.ToString()))));
1717  }
1718 
1719  if (OriginalAmbientLight != null)
1720  {
1721  element.Add(new XAttribute("originalambientlight", XMLExtensions.ColorToString(OriginalAmbientLight.Value)));
1722  }
1723 
1724  if (BackgroundSections != null && BackgroundSections.Count > 0)
1725  {
1726  element.Add(
1727  new XAttribute(
1728  "backgroundsections",
1729  string.Join(';', BackgroundSections.Where(b => b.ColorStrength > 0.01f).Select(b => b.Index + ":" + XMLExtensions.ColorToString(b.Color) + ":" + b.ColorStrength.ToString("G", CultureInfo.InvariantCulture)))));
1730  }
1731 
1732  foreach (Decal decal in decals)
1733  {
1734  element.Add(
1735  new XElement("decal",
1736  new XAttribute("id", decal.Prefab.Identifier),
1737  new XAttribute("pos", XMLExtensions.Vector2ToString(decal.NonClampedPosition)),
1738  new XAttribute("scale", decal.Scale.ToString("G", CultureInfo.InvariantCulture)),
1739  new XAttribute("timer", decal.FadeTimer.ToString("G", CultureInfo.InvariantCulture)),
1740  new XAttribute("alpha", decal.BaseAlpha.ToString("G", CultureInfo.InvariantCulture))
1741  ));
1742  }
1743 
1744  BallastFlora?.Save(element);
1745 
1747  parentElement.Add(element);
1748  return element;
1749  }
1750 
1751  public override string ToString()
1752  {
1753  return $"{base.ToString()} ({DisplayName ?? "unnamed"}, {(Submarine?.Info?.Name ?? "no sub")})";
1754  }
1755  }
1756 }
virtual float Strength
Definition: Affliction.cs:31
BackgroundSection(Rectangle rect, ushort index, ushort rowIndex)
BackgroundSection(Rectangle rect, ushort index, float colorStrength, Color color, ushort rowIndex)
static BallastFloraPrefab Find(Identifier identifier)
Affliction GetAffliction(string identifier, bool allowLimbAfflictions=true)
string? GetAttributeString(string key, string? def)
float GetAttributeFloat(string key, float def)
Rectangle GetAttributeRect(string key, in Rectangle def)
XAttribute? GetAttribute(string name)
bool AffectsSection(BackgroundSection section)
readonly Submarine Submarine
Definition: EntityGrid.cs:16
void InsertEntity(MapEntity entity)
Definition: EntityGrid.cs:72
List< MapEntity > GetEntities(Vector2 position)
Definition: EntityGrid.cs:124
void RemoveEntity(MapEntity entity)
Definition: EntityGrid.cs:95
Submarine Submarine
Definition: Entity.cs:53
const ushort NullEntityID
Definition: Entity.cs:14
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
AITarget aiTarget
Definition: Entity.cs:33
static void UpdateAll(List< FireSource > fireSources, float deltaTime)
static NetworkMember NetworkMember
Definition: GameMain.cs:190
Hull(Rectangle rectangle, Submarine submarine, ushort id=Entity.NullEntityID)
void AddFireSource(FireSource fireSource)
const float PressureBuildUpSpeed
How fast the pressure in the hull builds up when there's a gap leading outside
static void DetectItemVisibility(Character c=null)
void IncreaseSectionColorOrStrength(BackgroundSection section, Color? color, float? strength, bool requiresUpdate, bool isCleaning)
bool IsGreen
Returns true if the green component of the background color is twice as bright as the red and blue....
readonly Dictionary< Identifier, SerializableProperty > properties
IEnumerable< Identifier > OutpostModuleTags
Inherited flags from outpost generation.
override XElement Save(XElement parentElement)
BackgroundSection GetBackgroundSection(Vector2 worldPosition)
IEnumerable< BackgroundSection > GetBackgroundSectionsViaContaining(Rectangle rectArea)
override void ShallowRemove()
Remove the entity from the entity list without removing links to other entities
const float PressureDropSpeed
How fast the pressure in the hull goes back to normal when it's no longer full of water
void SetModuleTags(IEnumerable< Identifier > tags)
void Extinguish(float deltaTime, float amount, Vector2 position, bool extinguishRealFires=true, bool extinguishFakeFires=true)
float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance, float distanceMultiplierPerClosedDoor=0)
Approximate distance from this hull to the target hull, moving through open gaps without passing thro...
static Hull GetCleanTarget(Vector2 worldPosition)
Dictionary< Identifier, SerializableProperty > SerializableProperties
static EntityGrid GenerateEntityGrid(Submarine submarine)
Decal AddDecal(UInt32 decalId, Vector2 worldPosition, float scale, bool isNetworkEvent, int? spriteIndex=null)
List< DummyFireSource > FakeFireSources
bool IsBlue
Returns true if the blue component of the background color is twice as bright as the red and green....
static readonly List< EntityGrid > EntityGrids
static EntityGrid GenerateEntityGrid(Rectangle worldRect)
static Hull FindHull(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any)
bool LeadsOutside(Character character)
Does this hull have any doors leading outside?
void SetSectionColorOrStrength(BackgroundSection section, Color? color, float? strength)
static readonly List< Hull > HullList
static Hull FindHullUnoptimized(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any). The difference to FindHul...
override void Move(Vector2 amount, bool ignoreContacts=true)
void AddToGrid(Submarine submarine)
bool DoesSectionMatch(int index, int row)
Color AveragePaintedColor
Average color of the background sections
int FireCount
Can be used by conditionals
IEnumerable< Hull > GetConnectedHulls(bool includingThis, int? searchDepth=null, bool ignoreClosedGaps=false)
bool IsRed
Returns true if the red component of the background color is twice as bright as the blue and green....
void ApplyFlowForces(float deltaTime, Item item)
override void Update(float deltaTime, Camera cam)
static Hull Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
Decal AddDecal(string decalName, Vector2 worldPosition, float scale, bool isNetworkEvent, int? spriteIndex=null)
List< BackgroundSection > BackgroundSections
void CleanSection(BackgroundSection section, float cleanVal, bool updateRequired)
ushort GetOffsetId(XElement element)
Definition: IdRemap.cs:86
static void UpdateHulls()
goes through every item and re-checks which hull they are in
static readonly List< Item > ItemList
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
static readonly List< MapEntity > MapEntityList
IEnumerable< Identifier > ModuleFlags
readonly Identifier Identifier
Definition: Prefab.cs:34
static Dictionary< Identifier, SerializableProperty > DeserializeProperties(object obj, XElement element=null)
static Dictionary< Identifier, SerializableProperty > GetProperties(object obj)
static void SerializeProperties(ISerializableEntity obj, XElement element, bool saveIfDefault=false, bool ignoreEditable=false)
static void StoreCommand(Command command)
static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive=false)
Rectangle? Borders
Extents of the solid items/structures (ones with a physics body) and hulls
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
int ReadRangedInteger(int min, int max)
Single ReadRangedSingle(Single min, Single max, int bitCount)
Interface for entities that the server can send events to the clients
void WriteRangedInteger(int val, int min, int max)
void WriteRangedSingle(Single val, Single min, Single max, int bitCount)
NetworkFireSource(Hull hull, Vector2 normalizedPosition, float normalizedSize)