Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Linq;
8 using System.Xml.Linq;
9 
10 namespace Barotrauma
11 {
13  {
14  public override void Dispose() { }
15 
16  public readonly SubmarineInfo subInfo;
17 
18  public override Sprite Sprite => null;
19 
20  public override string OriginalName => Name.Value;
21 
22  public override LocalizedString Name => subInfo.Name;
23 
24  public override ImmutableHashSet<Identifier> Tags => null;
25 
26  public override ImmutableHashSet<Identifier> AllowedLinks => null;
27 
28  public override MapEntityCategory Category => MapEntityCategory.Misc;
29 
30  public override ImmutableHashSet<string> Aliases { get; }
31 
32  public LinkedSubmarinePrefab(SubmarineInfo subInfo) : base(subInfo.Name.ToIdentifier())
33  {
34  this.subInfo = subInfo;
35 
36  Aliases = Name.Value.ToEnumerable().ToImmutableHashSet();
37  }
38 
39  protected override void CreateInstance(Rectangle rect)
40  {
41  System.Diagnostics.Debug.Assert(Submarine.MainSub != null);
42  LinkedSubmarine.CreateDummy(Submarine.MainSub, subInfo.FilePath, rect.Location.ToVector2());
43  }
44  }
45 
46  partial class LinkedSubmarine : MapEntity
47  {
48  private List<Vector2> wallVertices;
49 
50  private string filePath;
51 
52  private bool loadSub;
53  public bool LoadSub => loadSub;
54  private Submarine sub;
55 
56  private ushort originalMyPortID;
57 
58  //the ID of the docking port the sub was docked to in the original sub file
59  //(needed when replacing a lost sub)
60  private ushort originalLinkedToID;
61  public ushort OriginalLinkedToID => originalLinkedToID;
62  private DockingPort originalLinkedPort;
63 
64  private bool purchasedLostShuttles;
65 
66  public Submarine Sub
67  {
68  get
69  {
70  return sub;
71  }
72  }
73 
74  private XElement saveElement;
75 
76  private Vector2? positionRelativeToMainSub;
77 
78  public override bool Linkable
79  {
80  get
81  {
82  return true;
83  }
84  }
85 
86  public int CargoCapacity { get; private set; }
87 
88  public LinkedSubmarine(Submarine submarine, ushort id = Entity.NullEntityID)
89  : base(null, submarine, id)
90  {
91  linkedToID = new List<ushort>();
92 
93  InsertToList();
94 
95  DebugConsole.Log("Created linked submarine (" + ID + ")");
96  }
97 
98  public static LinkedSubmarine CreateDummy(Submarine mainSub, Submarine linkedSub)
99  {
100  LinkedSubmarine sl = new LinkedSubmarine(mainSub)
101  {
102  sub = linkedSub
103  };
104 
105  return sl;
106  }
107 
108  public static LinkedSubmarine CreateDummy(Submarine mainSub, string filePath, Vector2 position)
109  {
110  XDocument doc = SubmarineInfo.OpenFile(filePath);
111  if (doc == null || doc.Root == null) return null;
112 
113  LinkedSubmarine sl = CreateDummy(mainSub, doc.Root, position);
114  sl.filePath = filePath;
115  sl.saveElement = doc.Root;
116  sl.saveElement.Name = "LinkedSubmarine";
117  sl.saveElement.SetAttributeValue("filepath", filePath);
118 
119  return sl;
120  }
121 
122  public static LinkedSubmarine CreateDummy(Submarine mainSub, XElement element, Vector2 position, ushort id = Entity.NullEntityID)
123  {
124  LinkedSubmarine sl = new LinkedSubmarine(mainSub, id);
125  sl.GenerateWallVertices(element);
126  sl.CargoCapacity = element.GetAttributeInt("cargocapacity", 0);
127  if (sl.wallVertices.Any())
128  {
129  sl.Rect = new Rectangle(
130  (int)sl.wallVertices.Min(v => v.X + position.X),
131  (int)sl.wallVertices.Max(v => v.Y + position.Y),
132  (int)sl.wallVertices.Max(v => v.X + position.X),
133  (int)sl.wallVertices.Min(v => v.Y + position.Y));
134 
135  int width = sl.rect.Width - sl.rect.X;
136  int height = sl.rect.Y - sl.rect.Height;
137  sl.Rect = new Rectangle((int)(position.X - width / 2), (int)(position.Y + height / 2), width, height);
138  }
139  else
140  {
141  sl.Rect = new Rectangle((int)position.X, (int)position.Y, 10, 10);
142  }
143  return sl;
144  }
145 
146  public override bool IsMouseOn(Vector2 position)
147  {
148  return Vector2.Distance(position, WorldPosition) < 50.0f;
149  }
150 
151  public override MapEntity Clone()
152  {
153  XElement cloneElement = new XElement(saveElement);
154  LinkedSubmarine sl = CreateDummy(Submarine, cloneElement, Position);
155  sl.saveElement = cloneElement;
156  sl.filePath = filePath;
157  return sl;
158  }
159 
160  private void GenerateWallVertices(XElement rootElement)
161  {
162  List<Vector2> points = new List<Vector2>();
163 
164  foreach (XElement element in rootElement.Elements())
165  {
166  if (element.Name != "Structure") { continue; }
167 
168  string name = element.GetAttributeString("name", "");
169  Identifier identifier = element.GetAttributeIdentifier("identifier", "");
170 
171  StructurePrefab prefab = Structure.FindPrefab(name, identifier);
172  if (prefab == null) { continue; }
173 
174  float scale = element.GetAttributeFloat("scale", prefab.Scale);
175 
176  var rect = element.GetAttributeVector4("rect", Vector4.Zero);
177  if (!prefab.ResizeHorizontal) { rect.Z *= scale / prefab.Scale; }
178  if (!prefab.ResizeVertical) { rect.W *= scale / prefab.Scale; }
179 
180  points.Add(new Vector2(rect.X, rect.Y));
181  points.Add(new Vector2(rect.X + rect.Z, rect.Y));
182  points.Add(new Vector2(rect.X, rect.Y - rect.W));
183  points.Add(new Vector2(rect.X + rect.Z, rect.Y - rect.W));
184  }
185 
186  wallVertices = MathUtils.GiftWrap(points);
187  }
188 
189  // LinkedSubmarine.Load() is called from MapEntity.LoadAll()
190  public static LinkedSubmarine Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
191  {
192  Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero);
193  LinkedSubmarine linkedSub;
194  idRemap.AssignMaxId(out ushort id);
196  {
197  linkedSub = CreateDummy(submarine, element, pos, id);
198  linkedSub.saveElement = new XElement(element);
199  linkedSub.purchasedLostShuttles = false;
200  }
201  else
202  {
203  string levelSeed = element.GetAttributeString("location", "");
205  linkedSub = new LinkedSubmarine(submarine, id)
206  {
207  purchasedLostShuttles =
208  (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles) ||
209  element.GetAttributeBool("purchasedlostshuttle", false),
210  saveElement = new XElement(element)
211  };
212 
213  bool levelMatches = string.IsNullOrWhiteSpace(levelSeed) || levelData == null || levelData.Seed == levelSeed;
214 
215  //don't load a sub that was left in this level if we have a submarine switch pending
216  //to make sure it gets ignored during the submarine switch and item transfer (reloading and saving it during the switch makes it not considered "left behind")
217  if ((levelMatches || linkedSub.purchasedLostShuttles) && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null)
218  {
219  linkedSub.loadSub = true;
220  linkedSub.rect.Location = MathUtils.ToPoint(pos);
221  }
222  else
223  {
224  linkedSub.loadSub = false;
225  }
226  }
227 
228  #warning TODO: revise
229  linkedSub.filePath = element.GetAttributeContentPath("filepath")?.Value ?? string.Empty;
230  int[] linkedToIds = element.GetAttributeIntArray("linkedto", Array.Empty<int>());
231  for (int i = 0; i < linkedToIds.Length; i++)
232  {
233  linkedSub.linkedToID.Add(idRemap.GetOffsetId(linkedToIds[i]));
234  }
235  linkedSub.originalLinkedToID = idRemap.GetOffsetId(element.GetAttributeInt("originallinkedto", 0));
236  linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0);
237  linkedSub.CargoCapacity = element.GetAttributeInt("cargocapacity", 0);
238 
239  return linkedSub.loadSub ? linkedSub : null;
240  }
241 
243  {
244  if (Screen.Selected != GameMain.SubEditorScreen) { return; }
245  for (int i = 0; i < linkedToID.Count; i++)
246  {
247  if (FindEntityByID(linkedToID[i]) is MapEntity linked)
248  {
249  linkedTo.Add(linked);
250  }
251  }
252  }
253 
255  {
256  if (positionRelativeToMainSub.HasValue)
257  {
258  Sub.SetPosition(Submarine.WorldPosition + positionRelativeToMainSub.Value);
259  }
260  positionRelativeToMainSub = null;
261  }
262 
263  public override void OnMapLoaded()
264  {
265  if (!loadSub) { return; }
266 
267  SubmarineInfo info = new SubmarineInfo(Submarine.Info.FilePath, "", saveElement);
268  if (!info.SubmarineElement.HasElements)
269  {
270  DebugConsole.ThrowError("Failed to load a linked submarine (empty XML element). The save file may be corrupted.");
271  return;
272  }
273  if (!info.SubmarineElement.Elements().Any(e => e.Name.ToString().Equals("hull", StringComparison.OrdinalIgnoreCase)))
274  {
275  DebugConsole.ThrowError("Failed to load a linked submarine (the submarine contains no hulls).");
276  return;
277  }
278 
279  saveElement.Attribute("purchasedlostshuttle")?.Remove();
280 
282  sub = Submarine.Load(info, false, parentRemap);
285  {
286  sub.TeamID = CharacterTeamType.FriendlyNPC;
287  }
288 
289  IdRemap childRemap = new IdRemap(saveElement, sub.IdOffset);
290 
291  Vector2 worldPos = saveElement.GetAttributeVector2("worldpos", Vector2.Zero);
292  if (worldPos != Vector2.Zero)
293  {
295  {
296  worldPos.X = GameMain.GameSession.LevelData.Size.X - worldPos.X;
297  }
298  sub.SetPosition(worldPos);
299  }
300  else
301  {
303  }
304 
305  DockingPort linkedPort = null;
306  DockingPort myPort = null;
307 
308  MapEntity linkedItem = linkedTo.FirstOrDefault(lt => (lt as Item)?.GetComponent<DockingPort>() != null);
309  if (linkedItem == null)
310  {
311  linkedPort = DockingPort.List.FirstOrDefault(dp => dp.DockingTarget != null && dp.DockingTarget.Item.Submarine == sub);
312  }
313  else
314  {
315  linkedPort = ((Item)linkedItem).GetComponent<DockingPort>();
316  }
317 
318  if (linkedPort == null)
319  {
320  if (purchasedLostShuttles)
321  {
322  linkedPort = (FindEntityByID(originalLinkedToID) as Item)?.GetComponent<DockingPort>();
323  }
324  }
325 
326  if (linkedPort == null)
327  {
328  if (worldPos == Vector2.Zero)
329  {
330  Vector2 relativePos = saveElement.GetAttributeVector2("posrelativetomainsub", Vector2.Zero);
331  if (relativePos != Vector2.Zero)
332  {
333  positionRelativeToMainSub = relativePos;
334  }
335  else
336  {
337  DebugConsole.ThrowError("Something went wrong when loading a linked submarine - the save didn't include a world position, a linked port or position relative to the main sub.");
338  }
339  }
340  else
341  {
342  sub.Submarine = Submarine;
343  }
344  return;
345  }
346 
347  originalLinkedPort = linkedPort;
348 
349  ushort originalMyId = childRemap.GetOffsetId(originalMyPortID);
350  myPort = (FindEntityByID(originalMyId) as Item)?.GetComponent<DockingPort>();
351  if (myPort == null)
352  {
353  float closestDistance = 0.0f;
354  foreach (DockingPort port in DockingPort.List)
355  {
356  if (port.Item.Submarine != sub) { continue; }
357  if (port.IsHorizontal != linkedPort.IsHorizontal) { continue; }
358  if (port.ForceDockingDirection != DockingPort.DirectionType.None && port.ForceDockingDirection == linkedPort.ForceDockingDirection) { continue; }
359  float dist = Vector2.Distance(port.Item.WorldPosition, linkedPort.Item.WorldPosition);
360  if (myPort == null || dist < closestDistance)
361  {
362  myPort = port;
363  closestDistance = dist;
364  }
365  }
366  }
367 
368  if (myPort != null)
369  {
370  originalMyPortID = myPort.Item.ID;
371 
372  myPort.Undock(applyEffects: false);
373  myPort.DockingDir = 0;
374 
375  //something else is already docked to the port this sub should be docked to
376  //may happen if a shuttle is lost, another vehicle docked to where the shuttle used to be,
377  //and the shuttle is then restored in the campaign mode
378  //or if the user connects multiple subs to the same docking ports in the sub editor
379  if (linkedPort.Docked && linkedPort.DockingTarget != null && linkedPort.DockingTarget != myPort)
380  {
381  //just spawn below the main sub
382  sub.SetPosition(
383  linkedPort.Item.Submarine.WorldPosition -
384  new Vector2(0, linkedPort.Item.Submarine.GetDockedBorders().Height / 2 + sub.GetDockedBorders().Height / 2));
385  }
386  else
387  {
388  Vector2 portDiff = myPort.Item.WorldPosition - sub.WorldPosition;
389  Vector2 offset = myPort.IsHorizontal ?
390  Vector2.UnitX * myPort.GetDir(linkedPort) :
391  Vector2.UnitY * myPort.GetDir(linkedPort);
392  offset *= myPort.DockedDistance;
393 
394  sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset);
395 
396  myPort.Dock(linkedPort);
397  myPort.Lock(isNetworkMessage: true, applyEffects: false);
398  }
399  }
400 
401  if (GameMain.GameSession?.GameMode is CampaignMode campaign &&
402  (campaign.PurchasedLostShuttles || campaign.PurchasedLostShuttlesInLatestSave))
403  {
404  foreach (Structure wall in Structure.WallList)
405  {
406  if (wall.Submarine != sub) { continue; }
407  for (int i = 0; i < wall.SectionCount; i++)
408  {
409  wall.SetDamage(i, 0, createNetworkEvent: false, createExplosionEffect: false);
410  }
411  }
412  foreach (Hull hull in Hull.HullList)
413  {
414  if (hull.Submarine != sub) { continue; }
415  hull.WaterVolume = 0.0f;
416  hull.OxygenPercentage = 100.0f;
417  hull.BallastFlora?.Kill();
418  }
419  }
420 
421  sub.SetPosition(sub.WorldPosition - Submarine.WorldPosition, forceUndockFromStaticSubmarines: false);
422  sub.Submarine = Submarine;
423  }
424 
425  public override XElement Save(XElement parentElement)
426  {
427  XElement saveElement = null;
428 
429  if (sub == null)
430  {
431  if (this.saveElement == null)
432  {
433  var doc = SubmarineInfo.OpenFile(filePath);
434  saveElement = doc.Root;
435  saveElement.Add(new XAttribute("filepath", filePath));
436  }
437  else
438  {
439  saveElement = this.saveElement;
440  }
441  saveElement.Name = "LinkedSubmarine";
442 
443  if (saveElement.Attribute("previewimage") != null)
444  {
445  saveElement.Attribute("previewimage").Remove();
446  }
447 
448  if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles)
449  {
450  saveElement.SetAttributeValue("purchasedlostshuttle", true);
451  }
452 
453  saveElement.SetAttributeValue("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition));
454 
455  }
456  else
457  {
458  saveElement = new XElement("LinkedSubmarine");
459  sub.SaveToXElement(saveElement);
460  }
461  if (linkedTo.Any() || linkedToID.Any())
462  {
463  var linkedPort =
464  linkedTo.FirstOrDefault(lt => (lt is Item item) && item.GetComponent<DockingPort>() != null) ??
465  FindEntityByID(linkedToID.First()) as MapEntity;
466  if (linkedPort != null)
467  {
468  saveElement.SetAttributeValue("linkedto", linkedPort.ID);
469  }
470  }
471 
472  saveElement.SetAttributeValue("originallinkedto", originalLinkedPort != null ? originalLinkedPort.Item.ID : originalLinkedToID);
473  saveElement.SetAttributeValue("originalmyport", originalMyPortID);
474 
475  if (sub != null)
476  {
477  bool leaveBehind = false;
478  if (sub.Submarine != null && !sub.DockedTo.Contains(sub.Submarine))
479  {
480  System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEitherExit);
482  {
483  leaveBehind = sub.AtEndExit != Submarine.MainSub.AtEndExit;
484  }
485  else
486  {
487  leaveBehind = sub.AtStartExit != Submarine.MainSub.AtStartExit;
488  }
489  }
490 
491  if (leaveBehind)
492  {
493  saveElement.SetAttributeValue("location", Level.Loaded.Seed);
494  Vector2 position = sub.SubBody.Position;
495  if (Level.Loaded.Mirrored)
496  {
497  position.X = Level.Loaded.Size.X - position.X;
498  }
499  saveElement.SetAttributeValue("worldpos", XMLExtensions.Vector2ToString(position));
500  }
501  else
502  {
503  if (saveElement.Attribute("location") != null) { saveElement.Attribute("location").Remove(); }
504  if (saveElement.Attribute("worldpos") != null) { saveElement.Attribute("worldpos").Remove(); }
505  saveElement.SetAttributeValue("posrelativetomainsub", XMLExtensions.Vector2ToString(sub.WorldPosition - Submarine.WorldPosition));
506  }
507  saveElement.SetAttributeValue("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition));
508  }
509 
510  parentElement.Add(saveElement);
511 
512  return saveElement;
513  }
514  }
515 }
string???????????? Value
Definition: ContentPath.cs:27
string? GetAttributeString(string key, string? def)
int?[] GetAttributeIntArray(string key, int[]? def)
Vector2 GetAttributeVector2(string key, in Vector2 def)
ContentPath? GetAttributeContentPath(string key)
bool GetAttributeBool(string key, bool def)
int GetAttributeInt(string key, int def)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
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
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
static GameSession?? GameSession
Definition: GameMain.cs:88
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static readonly List< Hull > HullList
ushort GetOffsetId(XElement element)
Definition: IdRemap.cs:86
void AssignMaxId(out ushort result)
Definition: IdRemap.cs:38
readonly Point Size
Definition: LevelData.cs:52
readonly string Seed
Definition: LevelData.cs:21
override XElement Save(XElement parentElement)
static LinkedSubmarine Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
static LinkedSubmarine CreateDummy(Submarine mainSub, string filePath, Vector2 position)
LinkedSubmarine(Submarine submarine, ushort id=Entity.NullEntityID)
static LinkedSubmarine CreateDummy(Submarine mainSub, Submarine linkedSub)
static LinkedSubmarine CreateDummy(Submarine mainSub, XElement element, Vector2 position, ushort id=Entity.NullEntityID)
MapEntity(MapEntityPrefab prefab, Submarine submarine, ushort id)
void SetDamage(int sectionIndex, float damage, Character attacker=null, bool createNetworkEvent=true, bool isNetworkEvent=true, bool createExplosionEffect=true, bool createWallDamageProjectiles=false)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
static Submarine Load(SubmarineInfo info, bool unloadPrevious, IdRemap linkedRemap=null)
Rectangle GetDockedBorders(bool allowDifferentTeam=true)
Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
void SetPosition(Vector2 position, List< Submarine > checkd=null, bool forceUndockFromStaticSubmarines=true)