Client LuaCsForBarotrauma
OutpostGenerator.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Linq;
8 
9 namespace Barotrauma
10 {
11  static class OutpostGenerator
12  {
13  class PlacedModule
14  {
18  public readonly SubmarineInfo Info;
22  public readonly PlacedModule PreviousModule;
26  public readonly OutpostModuleInfo.GapPosition ThisGapPosition = 0;
27 
28  public OutpostModuleInfo.GapPosition UsedGapPositions = 0;
29 
30  public readonly HashSet<Identifier> FulfilledModuleTypes = new HashSet<Identifier>();
31 
32  public Vector2 Offset;
33 
34  public Vector2 MoveOffset;
35 
36  public Gap ThisGap, PreviousGap;
37 
38  public Rectangle Bounds;
39  public Rectangle HullBounds;
40 
41  public PlacedModule(SubmarineInfo thisModule, PlacedModule previousModule, OutpostModuleInfo.GapPosition thisGapPosition)
42  {
43  Info = thisModule;
44  PreviousModule = previousModule;
45  ThisGapPosition = thisGapPosition;
46  UsedGapPositions = thisGapPosition;
47  if (PreviousModule != null)
48  {
49  previousModule.UsedGapPositions |= GetOpposingGapPosition(thisGapPosition);
50  }
51  }
52 
53  public override string ToString()
54  {
55  return $"OutpostGenerator.PlacedModule ({Info.Name})";
56  }
57  }
58 
59  public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false, bool allowInvalidOutpost = false)
60  {
61  return Generate(generationParams, locationType, location: null, onlyEntrance, allowInvalidOutpost);
62  }
63 
64  public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
65  {
66  return Generate(generationParams, location.Type, location, onlyEntrance, allowInvalidOutpost);
67  }
68 
69  private static SubmarineInfo usedForceOutpostModule;
70 
71  private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
72  {
73  var outpostModuleFiles = ContentPackageManager.EnabledPackages.All
74  .SelectMany(p => p.GetFiles<OutpostModuleFile>())
75  .OrderBy(f => f.UintIdentifier).ToArray();
76  var uintIdDupes = outpostModuleFiles.Where(f1 =>
77  outpostModuleFiles.Any(f2 => f1 != f2 && f1.UintIdentifier == f2.UintIdentifier)).ToArray();
78  if (uintIdDupes.Any())
79  {
80  throw new Exception($"OutpostModuleFile UintIdentifier duplicates found: {uintIdDupes.Select(f => f.Path)}");
81  }
82  if (location != null)
83  {
84  if (location.IsCriticallyRadiated() && OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.Identifier == generationParams.ReplaceInRadiation) is { } newParams)
85  {
86  generationParams = newParams;
87  }
88 
89  locationType = location.GetLocationType();
90  }
91 
92  Submarine sub = null;
93  if (generationParams.OutpostTag.IsEmpty)
94  {
95  var forceOutpostModule = GameMain.GameSession?.ForceOutpostModule;
96  sub = GenerateFromModules(generationParams, outpostModuleFiles, sub, locationType, location, onlyEntrance, allowInvalidOutpost);
97  if (sub != null)
98  {
99  return sub;
100  }
101  else if (forceOutpostModule != null)
102  {
103  //failed to force the module, abort
104  return null;
105  }
106  }
107 
108  var outpostFiles = ContentPackageManager.EnabledPackages.All
109  .SelectMany(p => p.GetFiles<OutpostFile>())
110  .Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path))
111  .OrderBy(f => f.UintIdentifier).ToList();
112 
113  List<SubmarineInfo> outpostInfos = new List<SubmarineInfo>();
114  foreach (var outpostFile in outpostFiles)
115  {
116  outpostInfos.Add(new SubmarineInfo(outpostFile.Path.Value));
117  }
118  if (!generationParams.OutpostTag.IsEmpty)
119  {
120  if (outpostInfos.Any(o => o.OutpostTags.Contains(generationParams.OutpostTag)))
121  {
122  outpostInfos = outpostInfos.FindAll(o => o.OutpostTags.Contains(generationParams.OutpostTag));
123  }
124  else
125  {
126  DebugConsole.ThrowError($"Could not find any outposts with the tag {generationParams.OutpostTag}. Choosing a random one instead...");
127  }
128  }
129  if (!outpostInfos.Any())
130  {
131  throw new Exception("Failed to generate an outpost. Could not generate an outpost from the available outpost modules and there are no pre-built outposts available.");
132  }
133  var prebuiltOutpostInfo = outpostInfos.GetRandom(Rand.RandSync.ServerAndClient);
134 
135  if (GameMain.NetworkMember?.ServerSettings is { } serverSettings &&
136  serverSettings.SelectedOutpostName != "Random")
137  {
138  var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
139  if (matchingOutpost != null)
140  {
141  prebuiltOutpostInfo = matchingOutpost;
142  }
143  }
144 
145  prebuiltOutpostInfo.Type = SubmarineType.Outpost;
146  sub = new Submarine(prebuiltOutpostInfo);
147  sub.Info.OutpostGenerationParams = generationParams;
148  location?.RemoveTakenItems();
149  EnableFactionSpecificEntities(sub, location);
150  return sub;
151  }
152 
153  private static Submarine GenerateFromModules(OutpostGenerationParams generationParams, OutpostModuleFile[] outpostModuleFiles, Submarine sub, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
154  {
155  //load the infos of the outpost module files
156  List<SubmarineInfo> outpostModules = new List<SubmarineInfo>();
157  foreach (var outpostModuleFile in outpostModuleFiles)
158  {
159  var subInfo = new SubmarineInfo(outpostModuleFile.Path.Value);
160  if (subInfo.OutpostModuleInfo != null)
161  {
162  if (generationParams is RuinGeneration.RuinGenerationParams)
163  {
164  //if the module doesn't have the ruin flag or any other flag used in the generation params, don't use it in ruins
165  if (!subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()) &&
166  !generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Identifier)))
167  {
168  continue;
169  }
170  }
171  else if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()))
172  {
173  continue;
174  }
175  outpostModules.Add(subInfo);
176  }
177  }
178 
179  List<PlacedModule> selectedModules = new List<PlacedModule>();
180  bool generationFailed = false;
181  int remainingTries = 5;
182  while (remainingTries > -1 && outpostModules.Any())
183  {
184  if (sub != null)
185  {
186 #if SERVER
187  int eventCount = GameMain.Server.EntityEventManager.Events.Count();
188  int uniqueEventCount = GameMain.Server.EntityEventManager.UniqueEvents.Count();
189 #endif
190  HashSet<Submarine> connectedSubs = new HashSet<Submarine>() { sub };
191  foreach (Submarine otherSub in Submarine.Loaded)
192  {
193  //remove linked subs too
194  if (otherSub.Submarine == sub) { connectedSubs.Add(otherSub); }
195  }
196  List<MapEntity> entities = MapEntity.MapEntityList.FindAll(e => connectedSubs.Contains(e.Submarine));
197  entities.ForEach(e => e.Remove());
198  foreach (Submarine otherSub in connectedSubs)
199  {
200  otherSub.Remove();
201  }
202 #if SERVER
203  //remove any events created during the removal of the entities
204  GameMain.Server.EntityEventManager.Events.RemoveRange(eventCount, GameMain.Server.EntityEventManager.Events.Count - eventCount);
205  GameMain.Server.EntityEventManager.UniqueEvents.RemoveRange(uniqueEventCount, GameMain.Server.EntityEventManager.UniqueEvents.Count - uniqueEventCount);
206 #endif
207  if (remainingTries <= 0)
208  {
209  generationFailed = true;
210  break;
211  }
212  }
213 
214  selectedModules.Clear();
215  //select which module types the outpost should consist of
216  List<Identifier> pendingModuleFlags = new List<Identifier>();
217  if (generationParams.ModuleCounts.Any())
218  {
219  pendingModuleFlags = onlyEntrance ?
220  generationParams.ModuleCounts[0].Identifier.ToEnumerable().ToList() :
221  SelectModules(outpostModules, location, generationParams);
222  }
223 
224  foreach (Identifier flag in pendingModuleFlags)
225  {
226  if (flag == "none") { continue; }
227  int pendingCount = pendingModuleFlags.Count(f => f == flag);
228  int availableModuleCount =
229  outpostModules
230  .Where(m => m.OutpostModuleInfo.ModuleFlags.Any(f => f == flag))
231  .Select(m => m.OutpostModuleInfo.MaxCount)
232  .DefaultIfEmpty(0)
233  .Sum();
234 
235  if (availableModuleCount < pendingCount)
236  {
237  DebugConsole.ThrowError($"Error in outpost generation parameters. Trying to place {pendingCount} modules of the type \"{flag}\", but there aren't enough suitable modules available. You may need to increase the \"max count\" value of some of the modules in the sub editor or decrease the number of modules in the outpost.");
238  for (int i = 0; i < (pendingCount - availableModuleCount); i++)
239  {
240  pendingModuleFlags.Remove(flag);
241  }
242  }
243  }
244 
245  //the first module is spawned separately, remove it from the list of pending modules
246  Identifier initialModuleFlag = pendingModuleFlags.FirstOrDefault().IfEmpty("airlock".ToIdentifier());
247  pendingModuleFlags.Remove(initialModuleFlag);
248 
249  bool hasForceOutpostWithInitialFlag = GameMain.GameSession?.ForceOutpostModule != null && GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.ModuleFlags.Contains(initialModuleFlag);
250  var initialModule = hasForceOutpostWithInitialFlag ? GameMain.GameSession.ForceOutpostModule : GetRandomModule(outpostModules, initialModuleFlag, locationType);
251 
252  if (hasForceOutpostWithInitialFlag)
253  {
254  DebugConsole.NewMessage($"Using Force outpost module as initial in Outpost generation: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}", Color.Yellow);
255  usedForceOutpostModule = GameMain.GameSession.ForceOutpostModule;
256  GameMain.GameSession.ForceOutpostModule = null;
257  }
258 
259  if (initialModule == null)
260  {
261  throw new Exception("Failed to generate an outpost (no airlock modules found).");
262  }
263  foreach (Identifier initialFlag in initialModule.OutpostModuleInfo.ModuleFlags)
264  {
265  if (pendingModuleFlags.Contains("initialFlag".ToIdentifier())) { pendingModuleFlags.Remove(initialFlag); }
266  }
267 
268  if (remainingTries == 1)
269  {
270  //generation has failed and only one attempt left, try removing duplicate modules
271  pendingModuleFlags = pendingModuleFlags.Distinct().ToList();
272  }
273 
274  selectedModules.Add(new PlacedModule(initialModule, null, OutpostModuleInfo.GapPosition.None));
275  selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag);
276 
277  AppendToModule(
278  selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags,
279  selectedModules,
280  locationType,
281  allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams,
282  allowDifferentLocationType: remainingTries == 1);
283 
284  if (GameMain.GameSession?.ForceOutpostModule != null)
285  {
286  if (remainingTries > 0)
287  {
288  remainingTries--;
289  continue;
290  }
291  DebugConsole.ThrowError($"Could not place force outpost module: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}");
292  GameMain.GameSession.ForceOutpostModule = null;
293  return null;
294  }
295 
296  if (pendingModuleFlags.Any(flag => flag != "none"))
297  {
298  if (!allowInvalidOutpost)
299  {
300  remainingTries--;
301  if (remainingTries <= 0)
302  {
303  DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
304  }
305  continue;
306  }
307  else
308  {
309  DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed.");
310  }
311  }
312 
313  var outpostInfo = new SubmarineInfo()
314  {
315  Type = SubmarineType.Outpost
316  };
317  generationFailed = false;
318  outpostInfo.OutpostGenerationParams = generationParams;
319  sub = new Submarine(outpostInfo, loadEntities: loadEntities);
320  sub.Info.OutpostGenerationParams = generationParams;
321  if (!generationFailed)
322  {
323  foreach (Hull hull in Hull.HullList)
324  {
325  if (hull.Submarine != sub) { continue; }
326  if (string.IsNullOrEmpty(hull.RoomName))
327  {
328  hull.RoomName = hull.CreateRoomName();
329  }
330  }
331  if (Level.IsLoadedOutpost)
332  {
333  location?.RemoveTakenItems();
334  }
335  foreach (WayPoint wp in WayPoint.WayPointList)
336  {
337  if (wp.CurrentHull == null && wp.Submarine == sub)
338  {
339  wp.FindHull();
340  }
341  }
342  EnableFactionSpecificEntities(sub, location);
343  return sub;
344  }
345  remainingTries--;
346  }
347 
348  DebugConsole.AddSafeError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
349  return null;
350 
351  List<MapEntity> loadEntities(Submarine sub)
352  {
353  Dictionary<PlacedModule, List<MapEntity>> entities = new Dictionary<PlacedModule, List<MapEntity>>();
354  int idOffset = sub.IdOffset;
355  for (int i = 0; i < selectedModules.Count; i++)
356  {
357  var selectedModule = selectedModules[i];
358  sub.Info.GameVersion = selectedModule.Info.GameVersion;
359  var moduleEntities = MapEntity.LoadAll(sub, selectedModule.Info.SubmarineElement, selectedModule.Info.FilePath, idOffset);
360 
361  if (usedForceOutpostModule != null && usedForceOutpostModule == selectedModule.Info)
362  {
363  sub.ForcedOutpostModuleWayPoints = moduleEntities.OfType<WayPoint>().ToList();
364  }
365 
366  MapEntity.InitializeLoadedLinks(moduleEntities);
367 
368  foreach (MapEntity entity in moduleEntities.ToList())
369  {
370  entity.OriginalModuleIndex = i;
371  if (entity is not Item item) { continue; }
372  var door = item.GetComponent<Door>();
373  if (door != null)
374  {
375  door.RefreshLinkedGap();
376  if (!moduleEntities.Contains(door.LinkedGap)) { moduleEntities.Add(door.LinkedGap); }
377  }
378  item.GetComponent<ConnectionPanel>()?.InitializeLinks();
379  item.GetComponent<ItemContainer>()?.OnMapLoaded();
380  }
381  idOffset = moduleEntities.Max(e => e.ID) + 1;
382 
383  var wallEntities = moduleEntities.Where(e => e is Structure s && s.HasBody).Cast<Structure>();
384  var hullEntities = moduleEntities.Where(e => e is Hull).Cast<Hull>();
385 
386  // Tell the hulls what tags the module has, used to spawn NPCs on specific rooms
387  foreach (Hull hull in hullEntities)
388  {
389  hull.SetModuleTags(selectedModule.Info.OutpostModuleInfo.ModuleFlags);
390  }
391 
392  if (Screen.Selected is { IsEditor: false })
393  {
394  foreach (Identifier layer in selectedModule.Info.LayersHiddenByDefault)
395  {
396  Submarine.SetLayerEnabled(layer, enabled: false, entities: moduleEntities);
397  }
398  }
399 
400  if (!hullEntities.Any())
401  {
402  selectedModule.HullBounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint());
403  }
404  else
405  {
406  Point min = new Point(hullEntities.Min(e => e.WorldRect.X), hullEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height));
407  Point max = new Point(hullEntities.Max(e => e.WorldRect.Right), hullEntities.Max(e => e.WorldRect.Y));
408  selectedModule.HullBounds = new Rectangle(min, max - min);
409  }
410 
411  if (!wallEntities.Any())
412  {
413  selectedModule.Bounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint());
414  }
415  else
416  {
417  Point min = new Point(wallEntities.Min(e => e.WorldRect.X), wallEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height));
418  Point max = new Point(wallEntities.Max(e => e.WorldRect.Right), wallEntities.Max(e => e.WorldRect.Y));
419  selectedModule.Bounds = new Rectangle(min, max - min);
420  }
421 
422  if (selectedModule.PreviousModule != null)
423  {
424  selectedModule.PreviousGap = GetGap(entities[selectedModule.PreviousModule], GetOpposingGapPosition(selectedModule.ThisGapPosition));
425  if (selectedModule.PreviousGap == null)
426  {
427  DebugConsole.ThrowError($"Error during outpost generation: {GetOpposingGapPosition(selectedModule.ThisGapPosition)} gap not found in module {selectedModule.PreviousModule.Info.Name}.");
428  generationFailed = true;
429  return new List<MapEntity>();
430  }
431  selectedModule.ThisGap = GetGap(moduleEntities, selectedModule.ThisGapPosition);
432  if (selectedModule.ThisGap == null)
433  {
434  DebugConsole.ThrowError($"Error during outpost generation: {selectedModule.ThisGapPosition} gap not found in module {selectedModule.Info.Name}.");
435  generationFailed = true;
436  return new List<MapEntity>();
437  }
438 
439  Vector2 moveDir = GetMoveDir(selectedModule.ThisGapPosition);
440  selectedModule.Offset =
441  (selectedModule.PreviousGap.WorldPosition + selectedModule.PreviousModule.Offset) -
442  selectedModule.ThisGap.WorldPosition;
443  if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null)
444  {
445  selectedModule.Offset += moveDir * generationParams.MinHallwayLength;
446  }
447  }
448  entities[selectedModule] = moduleEntities;
449  }
450 
451  bool overlapsFound = true;
452  int iteration = 0;
453  while (overlapsFound)
454  {
455  overlapsFound = false;
456  foreach (PlacedModule placedModule in selectedModules)
457  {
458  if (placedModule.PreviousModule == null) { continue; }
459 
460  List<PlacedModule> subsequentModules = new List<PlacedModule>();
461  GetSubsequentModules(placedModule, selectedModules, ref subsequentModules);
462  List<PlacedModule> otherModules = selectedModules.Except(subsequentModules).ToList();
463 
464  int remainingTries = 10;
465  while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingTries > 0)
466  {
467  overlapsFound = true;
468  if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, out Dictionary<PlacedModule, Vector2> solution))
469  {
470  foreach (KeyValuePair<PlacedModule, Vector2> kvp in solution)
471  {
472  kvp.Key.Offset += kvp.Value;
473  }
474  }
475  else
476  {
477  break;
478  }
479  remainingTries--;
480  }
481  }
482  iteration++;
483  if (iteration > 10)
484  {
485  generationFailed = true;
486  break;
487  }
488  }
489 
490  List<MapEntity> allEntities = new List<MapEntity>();
491  foreach (List<MapEntity> entityList in entities.Values)
492  {
493  allEntities.AddRange(entityList);
494  }
495 
496  if (!generationFailed)
497  {
498  foreach (PlacedModule module in selectedModules)
499  {
500  Submarine.RepositionEntities(module.Offset + sub.HiddenSubPosition, entities[module]);
501  }
502  Gap.UpdateHulls();
503  allEntities.AddRange(GenerateHallways(sub, locationType, selectedModules, outpostModules, entities, generationParams is RuinGeneration.RuinGenerationParams));
504  LinkOxygenGenerators(allEntities);
505  if (generationParams.LockUnusedDoors)
506  {
507  LockUnusedDoors(selectedModules, entities, generationParams.RemoveUnusedGaps);
508  }
509  if (generationParams.DrawBehindSubs)
510  {
511  foreach (var entity in allEntities)
512  {
513  if (entity is Structure structure)
514  {
515  //eww
516  structure.SpriteDepth = MathHelper.Lerp(0.999f, 0.9999f, structure.SpriteDepth);
517 #if CLIENT
518  foreach (var light in structure.Lights)
519  {
520  light.IsBackground = true;
521  }
522 #endif
523  }
524  }
525  }
526  AlignLadders(selectedModules, entities);
527  if (generationParams.MaxWaterPercentage > 0.0f)
528  {
529  foreach (var entity in allEntities)
530  {
531  if (entity is Hull hull)
532  {
533  float diff = generationParams.MaxWaterPercentage - generationParams.MinWaterPercentage;
534  if (diff < 0.01f)
535  {
536  // Overfill the hulls to get rid of air pockets in the vertical hallways. Airpockets make it impossible to swim up the hallways.
537  hull.WaterVolume = hull.Volume * 2;
538  }
539  else
540  {
541  hull.WaterVolume = hull.Volume * Rand.Range(generationParams.MinWaterPercentage, generationParams.MaxWaterPercentage, Rand.RandSync.ServerAndClient) * 0.01f;
542  }
543  }
544  }
545  }
546  }
547 
548  return allEntities;
549  }
550  }
551 
555  private static List<Identifier> SelectModules(IEnumerable<SubmarineInfo> modules, Location location, OutpostGenerationParams generationParams)
556  {
557  int totalModuleCount = generationParams.TotalModuleCount;
558  int totalModuleCountExcludingOptional = totalModuleCount - generationParams.ModuleCounts.Count(m => m.Probability < 1.0f);
559  var pendingModuleFlags = new List<Identifier>();
560  bool availableModulesFound = true;
561 
562  Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Identifier;
563  pendingModuleFlags.Add(initialModuleFlag);
564  while (pendingModuleFlags.Count < totalModuleCountExcludingOptional && availableModulesFound)
565  {
566  availableModulesFound = false;
567  foreach (var moduleCount in generationParams.ModuleCounts)
568  {
569  float? difficulty = Level.ForcedDifficulty ?? location?.LevelData?.Difficulty;
570  if (difficulty.HasValue)
571  {
572  if (difficulty.Value < moduleCount.MinDifficulty || difficulty.Value > moduleCount.MaxDifficulty)
573  {
574  continue;
575  }
576  }
577 
578  //if this is a module that we're trying to force into the outpost,
579  //ignore probability and faction requirements
580  if (GameMain.GameSession?.ForceOutpostModule == null ||
581  !GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.ModuleFlags.Contains(moduleCount.Identifier))
582  {
583  if (moduleCount.Probability < 1.0f &&
584  Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > moduleCount.Probability)
585  {
586  continue;
587  }
588  if (!moduleCount.RequiredFaction.IsEmpty &&
589  location?.Faction?.Prefab.Identifier != moduleCount.RequiredFaction &&
590  location?.SecondaryFaction?.Prefab.Identifier != moduleCount.RequiredFaction)
591  {
592  continue;
593  }
594  }
595  if (pendingModuleFlags.Count(m => m == moduleCount.Identifier) >= generationParams.GetModuleCount(moduleCount.Identifier))
596  {
597  continue;
598  }
599  if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleCount.Identifier)))
600  {
601  DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleCount.Identifier}\" found).");
602  continue;
603  }
604  availableModulesFound = true;
605  pendingModuleFlags.Add(moduleCount.Identifier);
606  }
607  }
608  pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f).Order).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient));
609  while (pendingModuleFlags.Count < totalModuleCount && generationParams.AppendToReachTotalModuleCount)
610  {
611  //don't place "none" modules at the end because
612  // a. "filler rooms" at the end of a hallway are pointless
613  // b. placing the unnecessary filler rooms first give more options for the placement of the more important modules
614  pendingModuleFlags.Insert(Rand.Int(pendingModuleFlags.Count - 1, Rand.RandSync.ServerAndClient), "none".ToIdentifier());
615  }
616 
617  //make sure the initial module is inserted first
618  pendingModuleFlags.Remove(initialModuleFlag);
619  pendingModuleFlags.Insert(0, initialModuleFlag);
620 
621  if (pendingModuleFlags.Count > totalModuleCount)
622  {
623  DebugConsole.ThrowError($"Error during outpost generation. {pendingModuleFlags.Count} modules set to be used the outpost, but total module count is only {totalModuleCount}. Leaving out some of the modules...");
624  int removeCount = pendingModuleFlags.Count - totalModuleCount;
625  for (int i = 0; i < removeCount; i++)
626  {
627  pendingModuleFlags.Remove(pendingModuleFlags.Last());
628  }
629  }
630 
631  return pendingModuleFlags;
632  }
633 
646  private static bool AppendToModule(PlacedModule currentModule,
647  List<SubmarineInfo> availableModules,
648  List<Identifier> pendingModuleFlags,
649  List<PlacedModule> selectedModules,
650  LocationType locationType,
651  bool tryReplacingCurrentModule = true,
652  bool allowExtendBelowInitialModule = false,
653  bool allowDifferentLocationType = false)
654  {
655  if (pendingModuleFlags.Count == 0) { return true; }
656 
657  List<PlacedModule> placedModules = new List<PlacedModule>();
658  foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient))
659  {
660  if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
661  if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; }
662 
663  PlacedModule newModule = null;
664  //try appending to the current module if possible
665  if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition))
666  {
667  newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType);
668  }
669 
670  if (newModule != null)
671  {
672  placedModules.Add(newModule);
673  }
674  else
675  {
676  //couldn't append to current module, try one of the other placed modules
677  foreach (PlacedModule otherModule in selectedModules)
678  {
679  if (otherModule == currentModule) { continue; }
680  foreach (OutpostModuleInfo.GapPosition otherGapPosition in
681  GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g)))
682  {
683  if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; }
684  newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType);
685  if (newModule != null)
686  {
687  placedModules.Add(newModule);
688  break;
689  }
690  }
691  if (newModule != null) { break; }
692  }
693  }
694  if (pendingModuleFlags.Count == 0) { return true; }
695  }
696 
697  //couldn't place a module anywhere, we're probably fucked!
698  if (placedModules.Count == 0 && tryReplacingCurrentModule && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule))
699  {
700  //try to replace the previously placed module with something else that we can append to
701  for (int i = 0; i < 10; i++)
702  {
703  selectedModules.Remove(currentModule);
704  assertAllPreviousModulesPresent();
705  //readd the module types that the previous module was supposed to fulfill to the pending module types
706  pendingModuleFlags.AddRange(currentModule.FulfilledModuleTypes);
707  if (!availableModules.Contains(currentModule.Info)) { availableModules.Add(currentModule.Info); }
708  //retry
709  currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType: true);
710  assertAllPreviousModulesPresent();
711  if (currentModule == null) { break; }
712  if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, tryReplacingCurrentModule: false, allowExtendBelowInitialModule, allowDifferentLocationType))
713  {
714  assertAllPreviousModulesPresent();
715  return true;
716  }
717  }
718  return false;
719  }
720 
721  foreach (PlacedModule placedModule in placedModules)
722  {
723  AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, tryReplacingCurrentModule: true, allowExtendBelowInitialModule, allowDifferentLocationType);
724  }
725  return placedModules.Count > 0;
726 
727  void assertAllPreviousModulesPresent()
728  {
729  System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule)));
730  }
731 
732  static bool DisallowBelowAirlock(bool allowExtendBelowInitialModule, OutpostModuleInfo.GapPosition gapPosition, PlacedModule currentModule)
733  {
734  if (!allowExtendBelowInitialModule)
735  {
736  //don't continue downwards if it'd extend below the airlock
737  if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { return true; }
738  }
739  return false;
740  }
741  }
742 
751  private static PlacedModule AppendModule(
752  PlacedModule currentModule,
753  OutpostModuleInfo.GapPosition gapPosition,
754  List<SubmarineInfo> availableModules,
755  List<Identifier> pendingModuleFlags,
756  List<PlacedModule> selectedModules,
757  LocationType locationType,
758  bool allowDifferentLocationType)
759  {
760  if (pendingModuleFlags.Count == 0) { return null; }
761 
762  Identifier flagToPlace = "none".ToIdentifier();
763  SubmarineInfo nextModule = null;
764  foreach (Identifier moduleFlag in pendingModuleFlags.OrderByDescending(f => currentModule?.Info?.OutpostModuleInfo.AllowAttachToModules.Contains(f) ?? false))
765  {
766  flagToPlace = moduleFlag;
767  nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType, allowDifferentLocationType);
768  if (nextModule != null) { break; }
769  }
770 
771  if (nextModule != null)
772  {
773  var newModule = new PlacedModule(nextModule, currentModule, gapPosition)
774  {
775  Offset = currentModule.Offset + GetMoveDir(gapPosition),
776  };
777  foreach (Identifier moduleFlag in nextModule.OutpostModuleInfo.ModuleFlags)
778  {
779  if (!pendingModuleFlags.Contains(moduleFlag)) { continue; }
780  if (moduleFlag != "none" || flagToPlace == "none")
781  {
782  newModule.FulfilledModuleTypes.Add(moduleFlag);
783  pendingModuleFlags.Remove(moduleFlag);
784  }
785  }
786  selectedModules.Add(newModule);
787  if (selectedModules.Count(m => m.Info == nextModule) >= nextModule.OutpostModuleInfo.MaxCount)
788  {
789  availableModules.Remove(nextModule);
790  }
791  return newModule;
792  }
793  return null;
794  }
795 
799  private static bool FindOverlap(IEnumerable<PlacedModule> modules1, IEnumerable<PlacedModule> modules2, out PlacedModule module1, out PlacedModule module2)
800  {
801  module1 = null;
802  module2 = null;
803  foreach (PlacedModule module in modules1)
804  {
805  foreach (PlacedModule otherModule in modules2)
806  {
807  if (module == otherModule) { continue; }
808  if (module.PreviousModule == otherModule && module.PreviousGap.ConnectedDoor == null && module.ThisGap.ConnectedDoor == null) { continue; }
809  if (ModulesOverlap(module, otherModule))
810  {
811  module1 = module;
812  module2 = otherModule;
813  return true;
814  }
815  }
816  }
817  return false;
818  }
819 
823  private static bool ModulesOverlap(PlacedModule module1, PlacedModule module2)
824  {
825  Rectangle bounds1 = module1.Bounds;
826  bounds1.Location += (module1.Offset + module1.MoveOffset).ToPoint();
827  Rectangle bounds2 = module2.Bounds;
828  bounds2.Location += (module2.Offset + module2.MoveOffset).ToPoint();
829 
830  //more tolerance on adjacent modules to prevent generating an unnecessary, small hallway between them
831  if (module1.PreviousModule == module2 || module2.PreviousModule == module1)
832  {
833  bounds1.Inflate(-16, -16);
834  bounds2.Inflate(-16, -16);
835  }
836 
837  Rectangle hullBounds1 = module1.HullBounds;
838  hullBounds1.Location += (module1.Offset + module1.MoveOffset).ToPoint();
839  Rectangle hullBounds2 = module2.HullBounds;
840  hullBounds2.Location += (module2.Offset + module2.MoveOffset).ToPoint();
841 
842  hullBounds1.Inflate(-32, -32);
843  hullBounds2.Inflate(-32, -32);
844 
845  return hullBounds1.Intersects(hullBounds2) || hullBounds1.Intersects(bounds2) || hullBounds2.Intersects(bounds1);
846  }
847 
851  private static bool ModuleOverlapsWithModuleConnections(IEnumerable<PlacedModule> modules)
852  {
853  foreach (PlacedModule module in modules)
854  {
855  Rectangle rect = module.Bounds;
856  rect.Location += (module.Offset + module.MoveOffset).ToPoint();
857  rect.Y += module.Bounds.Height;
858 
859  Vector2? selfGapPos1 = null;
860  Vector2? selfGapPos2 = null;
861  if (module.PreviousModule != null)
862  {
863  selfGapPos1 = module.Offset + module.ThisGap.Position + module.MoveOffset;
864  selfGapPos2 = module.PreviousModule.Offset + module.PreviousGap.Position + module.PreviousModule.MoveOffset;
865  }
866 
867  foreach (PlacedModule otherModule in modules)
868  {
869  if (otherModule == module || otherModule.PreviousModule == null || otherModule.PreviousModule == module) { continue; }
870 
871  //cast at both edges of the gap and see if it overlaps with anything
872  for (int i = -1; i <= 1; i += 2)
873  {
874  Vector2 gapEdgeOffset =
875  otherModule.ThisGap.IsHorizontal ?
876  Vector2.UnitY * otherModule.ThisGap.Rect.Height / 2 * i * 0.9f :
877  Vector2.UnitX * otherModule.ThisGap.Rect.Width / 2 * i * 0.9f;
878 
879  Vector2 gapPos1 = otherModule.Offset + otherModule.ThisGap.Position + gapEdgeOffset + otherModule.MoveOffset;
880  Vector2 gapPos2 = otherModule.PreviousModule.Offset + otherModule.PreviousGap.Position + gapEdgeOffset + otherModule.PreviousModule.MoveOffset;
881  if (Submarine.RectContains(rect, gapPos1) ||
882  Submarine.RectContains(rect, gapPos2) ||
883  MathUtils.GetLineRectangleIntersection(gapPos1, gapPos2, rect, out _))
884  {
885  return true;
886  }
887 
888  //check if the connection overlaps with this module's connection
889  if (selfGapPos1.HasValue && selfGapPos2.HasValue &&
890  !gapPos1.NearlyEquals(gapPos2) && !selfGapPos1.Value.NearlyEquals(selfGapPos2.Value) &&
891  MathUtils.LineSegmentsIntersect(gapPos1, gapPos2, selfGapPos1.Value, selfGapPos2.Value))
892  {
893  return true;
894  }
895  }
896  }
897  }
898  return false;
899  }
900 
912  private static bool FindOverlapSolution(IEnumerable<PlacedModule> movableModules, PlacedModule module1, PlacedModule module2, IEnumerable<PlacedModule> allmodules, out Dictionary<PlacedModule, Vector2> solution)
913  {
914  solution = new Dictionary<PlacedModule, Vector2>();
915  foreach (PlacedModule module in movableModules)
916  {
917  solution[module] = Vector2.Zero;
918  }
919 
920  Vector2 shortestMove = new Vector2(float.MaxValue, float.MaxValue);
921  bool solutionFound = false;
922  foreach (PlacedModule module in movableModules)
923  {
924  if (module.ThisGap.ConnectedDoor == null && module.PreviousGap.ConnectedDoor == null) { continue; }
925  Vector2 moveDir = GetMoveDir(module.ThisGapPosition);
926  Vector2 moveStep = moveDir * 50.0f;
927  Vector2 currentMove = Vector2.Zero;
928  float maxMoveAmount = 2000.0f;
929 
930  List<PlacedModule> subsequentModules2 = new List<PlacedModule>();
931  GetSubsequentModules(module, movableModules, ref subsequentModules2);
932  while (currentMove.LengthSquared() < maxMoveAmount * maxMoveAmount)
933  {
934  currentMove += moveStep;
935  foreach (PlacedModule movedModule in subsequentModules2)
936  {
937  movedModule.MoveOffset = currentMove;
938  }
939  if (!ModulesOverlap(module1, module2) &&
940  !ModuleOverlapsWithModuleConnections(allmodules) &&
941  currentMove.LengthSquared() < shortestMove.LengthSquared())
942  {
943  shortestMove = currentMove;
944  foreach (PlacedModule movedModule in allmodules)
945  {
946  solution[movedModule] = subsequentModules2.Contains(movedModule) ? currentMove : Vector2.Zero;
947  solutionFound = true;
948  }
949  break;
950  }
951  }
952  foreach (PlacedModule movedModule in allmodules)
953  {
954  movedModule.MoveOffset = Vector2.Zero;
955  }
956  }
957 
958  return solutionFound;
959  }
960 
961  private static SubmarineInfo GetRandomModule(IEnumerable<SubmarineInfo> modules, Identifier moduleFlag, LocationType locationType)
962  {
963  IEnumerable<SubmarineInfo> availableModules = null;
964  if (moduleFlag.IsEmpty || moduleFlag == "none")
965  {
966  availableModules = modules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier()));
967  }
968  else
969  {
970  availableModules = modules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
971  if (moduleFlag != "hallwayhorizontal" && moduleFlag != "hallwayvertical")
972  {
973  availableModules = availableModules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayhorizontal".ToIdentifier()) && !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayvertical".ToIdentifier()));
974  }
975  }
976 
977  if (!availableModules.Any()) { return null; }
978 
979  //try to search for modules made specifically for this location type first
980  var modulesSuitableForLocationType =
981  availableModules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType));
982 
983  //if not found, search for modules suitable for any location type
984  if (!modulesSuitableForLocationType.Any())
985  {
986  modulesSuitableForLocationType = availableModules.Where(m => m.OutpostModuleInfo.IsAllowedInAnyLocationType());
987  }
988 
989  if (!modulesSuitableForLocationType.Any())
990  {
991  DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange);
992  return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
993  }
994  else
995  {
996  return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
997  }
998  }
999 
1000  private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable<SubmarineInfo> modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType)
1001  {
1002  IEnumerable<SubmarineInfo> modulesWithCorrectFlags = null;
1003  if (moduleFlag.IsEmpty || moduleFlag.Equals("none"))
1004  {
1005  modulesWithCorrectFlags = modules
1006  .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())));
1007  }
1008  else
1009  {
1010  modulesWithCorrectFlags = modules
1011  .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
1012  }
1013  modulesWithCorrectFlags = modulesWithCorrectFlags.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
1014 
1015  var suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
1016  var suitableModulesForAnyOutpost = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
1017  if (!suitableModules.Any())
1018  {
1019  //no suitable module found, see if we can find a "generic" module that's not meant for any specific type of outpost
1020  suitableModules = suitableModulesForAnyOutpost;
1021  //still not found, see if we can find something that's otherwise suitable but not meant to attach to the previous module
1022  if (!suitableModules.Any())
1023  {
1024  suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
1025  }
1026  //still not found! Try if we can find a generic module that's not meant to attach to the previous module
1027  if (!suitableModules.Any())
1028  {
1029  suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
1030  }
1031  }
1032 
1033  if (!suitableModules.Any())
1034  {
1035  if (allowDifferentLocationType && modulesWithCorrectFlags.Any())
1036  {
1037  DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange);
1038  return ToolBox.SelectWeightedRandom(modulesWithCorrectFlags.ToList(), modulesWithCorrectFlags.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
1039  }
1040  else
1041  {
1042  return null;
1043  }
1044  }
1045  else
1046  {
1047  var suitableModule = ToolBox.SelectWeightedRandom(suitableModules.ToList(), suitableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
1048 
1049  if (GameMain.GameSession?.ForceOutpostModule != null)
1050  {
1051  if (suitableModules.Any(module => module.OutpostModuleInfo.Name == GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name) ||
1052  suitableModulesForAnyOutpost.Any(module => module.OutpostModuleInfo.Name == GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name))
1053  {
1054  var forceOutpostModule = GameMain.GameSession.ForceOutpostModule;
1055  System.Diagnostics.Debug.WriteLine($"Inserting Force outpost module in Outpost generation: {forceOutpostModule.OutpostModuleInfo.Name}");
1056  GameMain.GameSession.ForceOutpostModule = null;
1057  usedForceOutpostModule = forceOutpostModule;
1058  return forceOutpostModule;
1059  }
1060  else if (GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag))
1061  {
1062  // if our force module has the same tag as the selected random one, return nothing
1063  // because we don't want another module of the same type to be hogging the only spot for that type
1064  return null;
1065  }
1066  }
1067 
1068  return suitableModule;
1069  }
1070 
1071  IEnumerable<SubmarineInfo> GetSuitableModules(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific)
1072  {
1073  IEnumerable<SubmarineInfo> suitable = modules;
1074  if (requireCorrectLocationType)
1075  {
1076  if (disallowNonLocationTypeSpecific)
1077  {
1078  //don't use OutpostModuleInfo.IsLocationTypeAllowed here - we're trying to choose a module specifically for this location type, not modules suitable for any location type
1079  suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
1080  }
1081  else
1082  {
1083  suitable = modules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType));
1084  }
1085  }
1086  if (requireAllowAttachToPrevious && prevModule != null)
1087  {
1088  suitable = suitable.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));
1089  }
1090  return suitable;
1091  }
1092  }
1093 
1097  private static void GetSubsequentModules(PlacedModule startModule, IEnumerable<PlacedModule> allModules, ref List<PlacedModule> subsequentModules)
1098  {
1099  System.Diagnostics.Debug.Assert(!subsequentModules.Contains(startModule));
1100  subsequentModules.Add(startModule);
1101  foreach (PlacedModule module in allModules)
1102  {
1103  if (module.PreviousModule == startModule)
1104  {
1105  GetSubsequentModules(module, allModules, ref subsequentModules);
1106  }
1107  }
1108  }
1109 
1110  private readonly static OutpostModuleInfo.GapPosition[] GapPositions = new[]
1111  {
1112  OutpostModuleInfo.GapPosition.Right,
1113  OutpostModuleInfo.GapPosition.Left,
1114  OutpostModuleInfo.GapPosition.Top,
1115  OutpostModuleInfo.GapPosition.Bottom
1116  };
1117 
1118  private static OutpostModuleInfo.GapPosition GetOpposingGapPosition(OutpostModuleInfo.GapPosition thisGapPosition)
1119  {
1120  return thisGapPosition switch
1121  {
1122  OutpostModuleInfo.GapPosition.Right => OutpostModuleInfo.GapPosition.Left,
1123  OutpostModuleInfo.GapPosition.Left => OutpostModuleInfo.GapPosition.Right,
1124  OutpostModuleInfo.GapPosition.Bottom => OutpostModuleInfo.GapPosition.Top,
1125  OutpostModuleInfo.GapPosition.Top => OutpostModuleInfo.GapPosition.Bottom,
1126  OutpostModuleInfo.GapPosition.None => OutpostModuleInfo.GapPosition.None,
1127  _ => throw new ArgumentException()
1128  };
1129  }
1130 
1131  private static Vector2 GetMoveDir(OutpostModuleInfo.GapPosition thisGapPosition)
1132  {
1133  return thisGapPosition switch
1134  {
1135  OutpostModuleInfo.GapPosition.Right => -Vector2.UnitX,
1136  OutpostModuleInfo.GapPosition.Left => Vector2.UnitX,
1137  OutpostModuleInfo.GapPosition.Bottom => Vector2.UnitY,
1138  OutpostModuleInfo.GapPosition.Top => -Vector2.UnitY,
1139  OutpostModuleInfo.GapPosition.None => Vector2.Zero,
1140  _ => throw new ArgumentException()
1141  };
1142  }
1143 
1144  private static Gap GetGap(IEnumerable<MapEntity> entities, OutpostModuleInfo.GapPosition gapPosition)
1145  {
1146  Gap selectedGap = null;
1147  foreach (MapEntity entity in entities)
1148  {
1149  if (!(entity is Gap gap)) { continue; }
1150  if (gap.ConnectedDoor != null && !gap.ConnectedDoor.UseBetweenOutpostModules) { continue; }
1151  switch (gapPosition)
1152  {
1153  case OutpostModuleInfo.GapPosition.Right:
1154  if (gap.IsHorizontal && (selectedGap == null || gap.WorldPosition.X > selectedGap.WorldPosition.X) &&
1155  !entities.Any(e => e is Hull && e.WorldPosition.X > gap.WorldPosition.X && gap.WorldRect.Y - gap.WorldRect.Height <= e.WorldRect.Y && gap.WorldRect.Y >= e.WorldRect.Y - e.WorldRect.Height))
1156  {
1157  selectedGap = gap;
1158  }
1159  break;
1160  case OutpostModuleInfo.GapPosition.Left:
1161  if (gap.IsHorizontal && (selectedGap == null || gap.WorldPosition.X < selectedGap.WorldPosition.X) &&
1162  !entities.Any(e => e is Hull && e.WorldPosition.X < gap.WorldPosition.X && gap.WorldRect.Y - gap.WorldRect.Height <= e.WorldRect.Y && gap.WorldRect.Y >= e.WorldRect.Y - e.WorldRect.Height))
1163  {
1164  selectedGap = gap;
1165  }
1166  break;
1167  case OutpostModuleInfo.GapPosition.Top:
1168  if (!gap.IsHorizontal && (selectedGap == null || gap.WorldPosition.Y > selectedGap.WorldPosition.Y) &&
1169  !entities.Any(e => e is Hull && e.WorldPosition.Y > gap.WorldPosition.Y && gap.WorldRect.Right >= e.WorldRect.X && gap.WorldRect.X <= e.WorldRect.Right))
1170  {
1171  selectedGap = gap;
1172  }
1173  break;
1174  case OutpostModuleInfo.GapPosition.Bottom:
1175  if (!gap.IsHorizontal && (selectedGap == null || gap.WorldPosition.Y < selectedGap.WorldPosition.Y) &&
1176  !entities.Any(e => e is Hull && e.WorldPosition.Y < gap.WorldPosition.Y && gap.WorldRect.Right >= e.WorldRect.X && gap.WorldRect.X <= e.WorldRect.Right))
1177  {
1178  selectedGap = gap;
1179  }
1180  break;
1181  }
1182  }
1183  return selectedGap;
1184  }
1185 
1186  private static bool CanAttachTo(OutpostModuleInfo from, OutpostModuleInfo to)
1187  {
1188  if (!from.AllowAttachToModules.Any() || from.AllowAttachToModules.All(s => s == "any")) { return true; }
1189  return from.AllowAttachToModules.Any(s => to.ModuleFlags.Contains(s));
1190  }
1191 
1192  private static List<MapEntity> GenerateHallways(Submarine sub, LocationType locationType, IEnumerable<PlacedModule> placedModules, IEnumerable<SubmarineInfo> availableModules, Dictionary<PlacedModule, List<MapEntity>> allEntities, bool isRuin)
1193  {
1194  //if a hallway is shorter than this, one of the doors at the ends of the hallway is removed
1195  const float MinTwoDoorHallwayLength = 32.0f;
1196 
1197  List<MapEntity> placedEntities = new List<MapEntity>();
1198  foreach (PlacedModule module in placedModules)
1199  {
1200  if (module.PreviousModule == null) { continue; }
1201 
1202  var thisJunctionBox = Powered.PoweredList.FirstOrDefault(p => p is PowerTransfer pt && IsLinked(module.ThisGap, pt))?.Item?.GetComponent<ConnectionPanel>();
1203  var previousJunctionBox = Powered.PoweredList.FirstOrDefault(p => p is PowerTransfer pt && IsLinked(module.PreviousGap, pt))?.Item?.GetComponent<ConnectionPanel>();
1204 
1205  static bool IsLinked(Gap gap, PowerTransfer junctionBox)
1206  {
1207  if (junctionBox.Item.linkedTo.Contains(gap)) { return true; }
1208  if (gap.ConnectedDoor != null && junctionBox.Item.linkedTo.Contains(gap.ConnectedDoor.Item)) { return true; }
1209  if (gap.linkedTo.Contains(junctionBox.Item)) { return true; }
1210  if (gap.ConnectedDoor != null && gap.ConnectedDoor.Item.linkedTo.Contains(junctionBox.Item)) { return true; }
1211  return false;
1212  }
1213 
1214  if (thisJunctionBox != null && previousJunctionBox != null)
1215  {
1216  for (int i = 0; i < thisJunctionBox.Connections.Count && i < previousJunctionBox.Connections.Count; i++)
1217  {
1218  var wirePrefab = MapEntityPrefab.FindByIdentifier((thisJunctionBox.Connections[i].IsPower ? "redwire" : "bluewire").ToIdentifier()) as ItemPrefab;
1219  var wire = new Item(wirePrefab, thisJunctionBox.Item.Position, sub).GetComponent<Wire>();
1220 
1221  if (!thisJunctionBox.Connections[i].TryAddLink(wire))
1222  {
1223  DebugConsole.AddWarning($"Failed to connect junction boxes between outpost modules (not enough free connections in module \"{module.Info.Name}\")");
1224  continue;
1225  }
1226  if (!previousJunctionBox.Connections[i].TryAddLink(wire))
1227  {
1228  DebugConsole.AddWarning($"Failed to connect junction boxes between outpost modules (not enough free connections in module \"{module.PreviousModule.Info.Name}\")");
1229  continue;
1230  }
1231  wire.TryConnect(thisJunctionBox.Connections[i], addNode: false);
1232  wire.TryConnect(previousJunctionBox.Connections[i], addNode: false);
1233  wire.SetNodes(new List<Vector2>());
1234  }
1235  }
1236 
1237  bool isHorizontal =
1238  module.ThisGapPosition == OutpostModuleInfo.GapPosition.Left ||
1239  module.ThisGapPosition == OutpostModuleInfo.GapPosition.Right;
1240 
1241  if (!module.ThisGap.linkedTo.Any())
1242  {
1243  DebugConsole.ThrowError($"Error during outpost generation: {module.ThisGapPosition} gap in module \"{module.Info.Name}\" was not linked to any hulls.");
1244  continue;
1245  }
1246  if (!module.PreviousGap.linkedTo.Any())
1247  {
1248  DebugConsole.ThrowError($"Error during outpost generation: {GetOpposingGapPosition(module.ThisGapPosition)} gap in module \"{module.PreviousModule.Info.Name}\" was not linked to any hulls.");
1249  continue;
1250  }
1251 
1252  MapEntity leftHull = module.ThisGap.Position.X < module.PreviousGap.Position.X ? module.ThisGap.linkedTo[0] : module.PreviousGap.linkedTo[0];
1253  MapEntity rightHull = module.ThisGap.Position.X > module.PreviousGap.Position.X ?
1254  module.ThisGap.linkedTo.Count == 1 ? module.ThisGap.linkedTo[0] : module.ThisGap.linkedTo[1] :
1255  module.PreviousGap.linkedTo.Count == 1 ? module.PreviousGap.linkedTo[0] : module.PreviousGap.linkedTo[1];
1256  MapEntity topHull = module.ThisGap.Position.Y > module.PreviousGap.Position.Y ? module.ThisGap.linkedTo[0] : module.PreviousGap.linkedTo[0];
1257  MapEntity bottomHull = module.ThisGap.Position.Y < module.PreviousGap.Position.Y ?
1258  module.ThisGap.linkedTo.Count == 1 ? module.ThisGap.linkedTo[0] : module.ThisGap.linkedTo[1] :
1259  module.PreviousGap.linkedTo.Count == 1 ? module.PreviousGap.linkedTo[0] : module.PreviousGap.linkedTo[1];
1260 
1261  float hallwayLength = isHorizontal ?
1262  rightHull.WorldRect.X - leftHull.WorldRect.Right :
1263  topHull.WorldRect.Y - topHull.RectHeight - bottomHull.WorldRect.Y;
1264 
1265  if (module.ThisGap != null && module.ThisGap.ConnectedDoor == null)
1266  {
1267  //gap in use -> remove linked entities that are marked to be removed
1268  foreach (var otherEntity in allEntities[module])
1269  {
1270  if (otherEntity is Structure structure && structure.HasBody && !structure.IsPlatform && structure.RemoveIfLinkedOutpostDoorInUse &&
1271  Submarine.RectContains(structure.WorldRect, module.ThisGap.WorldPosition))
1272  {
1273  structure.Remove();
1274  }
1275  }
1276  }
1277  if (module.PreviousGap != null && module.PreviousGap.ConnectedDoor == null)
1278  {
1279  //gap in use -> remove linked entities that are marked to be removed
1280  foreach (var otherEntity in allEntities[module.PreviousModule])
1281  {
1282  if (otherEntity is Structure structure && structure.HasBody && !structure.IsPlatform && structure.RemoveIfLinkedOutpostDoorInUse &&
1283  Submarine.RectContains(structure.WorldRect, module.PreviousGap.WorldPosition))
1284  {
1285  structure.Remove();
1286  }
1287  }
1288  }
1289 
1290  //if the hallway is very short, remove one of the doors
1291  if (hallwayLength <= MinTwoDoorHallwayLength)
1292  {
1293  if (module.ThisGap != null && module.PreviousGap != null)
1294  {
1295  var gapToRemove = module.ThisGap.ConnectedDoor == null ? module.ThisGap : module.PreviousGap;
1296  var otherGap = gapToRemove == module.ThisGap ? module.PreviousGap : module.ThisGap;
1297 
1298  gapToRemove.ConnectedDoor?.Item.linkedTo.ForEachMod(lt => (lt as Structure)?.Remove());
1299  if (gapToRemove.ConnectedDoor?.Item.Connections != null)
1300  {
1301  foreach (Connection c in gapToRemove.ConnectedDoor.Item.Connections)
1302  {
1303  c.Wires.ToArray().ForEach(w => w?.Item.Remove());
1304  }
1305  }
1306 
1307  WayPoint thisWayPoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == gapToRemove);
1308  WayPoint previousWayPoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == otherGap);
1309  if (thisWayPoint != null && previousWayPoint != null)
1310  {
1311  foreach (MapEntity me in thisWayPoint.linkedTo)
1312  {
1313  if (me is WayPoint wayPoint && !previousWayPoint.linkedTo.Contains(wayPoint))
1314  {
1315  previousWayPoint.linkedTo.Add(wayPoint);
1316  }
1317  }
1318  thisWayPoint.Remove();
1319  }
1320  else
1321  {
1322  if (thisWayPoint == null)
1323  {
1324  DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {module.ThisGapPosition.ToString().ToLower()} gap of the module \"{module.Info.Name}\".");
1325  }
1326  if (previousWayPoint == null)
1327  {
1328  DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.PreviousModule.Info.Name}\".");
1329  }
1330  }
1331 
1332  gapToRemove.ConnectedDoor?.Item.Remove();
1333  if (hallwayLength <= 1.0f) { gapToRemove?.Remove(); }
1334  }
1335  }
1336 
1337  if (hallwayLength <= 1.0f) { continue; }
1338 
1339  Identifier moduleFlag = (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier();
1340  var hallwayModules = availableModules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
1341 
1342  var suitableHallwayModules = hallwayModules.Where(m =>
1343  m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) &&
1344  m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s)));
1345  if (suitableHallwayModules.None())
1346  {
1347  suitableHallwayModules = hallwayModules.Where(m =>
1348  !m.OutpostModuleInfo.AllowAttachToModules.Any() ||
1349  m.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any"));
1350  }
1351 
1352  var hallwayInfo = GetRandomModule(suitableHallwayModules, moduleFlag, locationType);
1353  if (hallwayInfo == null)
1354  {
1355  DebugConsole.ThrowError($"Generating hallways between outpost modules failed. No {(isHorizontal ? "horizontal" : "vertical")} hallway modules suitable for use between the modules \"{module.Info.DisplayName}\" and \"{module.PreviousModule.Info.DisplayName}\".");
1356  return placedEntities;
1357  }
1358 
1359  var moduleEntities = MapEntity.LoadAll(sub, hallwayInfo.SubmarineElement, hallwayInfo.FilePath, -1);
1360 
1361  //remove items that don't fit in the hallway
1362  moduleEntities.Where(e => e is Item item && item.GetComponent<Door>() == null && (isHorizontal ? e.Rect.Width : e.Rect.Height) > hallwayLength).ForEach(e => e.Remove());
1363 
1364  //find the largest hull to use it as the center point of the hallway
1365  //and the bounds of all the hulls, used when resizing the hallway to fit between the modules
1366  Vector2 hullCenter = Vector2.Zero;
1367  Rectangle hullBounds = Rectangle.Empty;
1368  float largestHullVolume = 0.0f;
1369  foreach (MapEntity me in moduleEntities)
1370  {
1371  if (me is Hull hull)
1372  {
1373  if (hull.Volume > largestHullVolume)
1374  {
1375  largestHullVolume = hull.Volume;
1376  hullCenter = hull.WorldPosition;
1377  }
1378  hullBounds = new Rectangle(
1379  Math.Min(hullBounds.X, me.WorldRect.X),
1380  Math.Min(hullBounds.Y, me.WorldRect.Y - me.WorldRect.Height),
1381  Math.Max(hullBounds.Width, me.WorldRect.Right),
1382  Math.Max(hullBounds.Height, me.WorldRect.Y));
1383  }
1384  }
1385  hullBounds.Width -= hullBounds.X;
1386  hullBounds.Height -= hullBounds.Y;
1387 
1388  float scaleFactor = isHorizontal ?
1389  hallwayLength / (float)hullBounds.Width :
1390  hallwayLength / (float)hullBounds.Height;
1391  System.Diagnostics.Debug.Assert(scaleFactor > 0.0f);
1392 
1393  placedEntities.AddRange(moduleEntities);
1394  MapEntity.InitializeLoadedLinks(moduleEntities);
1395  Vector2 moveAmount = (module.ThisGap.Position + module.PreviousGap.Position) / 2 - hullCenter;
1396  Submarine.RepositionEntities(moveAmount, moduleEntities);
1397  hullBounds.Location += moveAmount.ToPoint();
1398 
1399  //resize/reposition entities to make the hallway fit between the modules
1400  foreach (MapEntity me in moduleEntities)
1401  {
1402  if (me is Hull)
1403  {
1404  if (hallwayLength <= MinTwoDoorHallwayLength)
1405  {
1406  //if the hallway is very short, stretch the hulls in adjacent modules and remove the hull in between
1407  if (isHorizontal)
1408  {
1409  int midX = (leftHull.Rect.Right + rightHull.Rect.X) / 2;
1410  leftHull.Rect = new Rectangle(leftHull.Rect.X, leftHull.Rect.Y, midX - leftHull.Rect.X, leftHull.Rect.Height);
1411  rightHull.Rect = new Rectangle(midX, rightHull.Rect.Y, rightHull.Rect.Right - midX, rightHull.Rect.Height);
1412  }
1413  else
1414  {
1415  int midY = (topHull.Rect.Y - topHull.Rect.Height + bottomHull.Rect.Y) / 2;
1416  topHull.Rect = new Rectangle(topHull.Rect.X, topHull.Rect.Y, topHull.Rect.Width, topHull.Rect.Y - midY);
1417  bottomHull.Rect = new Rectangle(bottomHull.Rect.X, midY, bottomHull.Rect.Width, midY - (bottomHull.Rect.Y - bottomHull.Rect.Height));
1418  }
1419  me.Remove();
1420  }
1421  else
1422  {
1423  if (isHorizontal)
1424  {
1425  //extend from the right edge of the hull on the left to the left edge of the hull on the right
1426  me.Rect = new Rectangle(leftHull.Rect.Right, me.Rect.Y, rightHull.Rect.X - leftHull.Rect.Right, me.Rect.Height);
1427  }
1428  else
1429  {
1430  //extend from the top of the hull below to the bottom of the hull above
1431  me.Rect = new Rectangle(me.Rect.X, topHull.Rect.Y - topHull.Rect.Height, me.Rect.Width, topHull.Rect.Y - topHull.Rect.Height - bottomHull.Rect.Y);
1432  }
1433  }
1434  }
1435  else if (me is Structure || (me is Item item && item.GetComponent<Door>() == null))
1436  {
1437  if (isHorizontal)
1438  {
1439  if (!me.ResizeHorizontal)
1440  {
1441  int xPos = (int)(leftHull.WorldRect.Right + (me.WorldPosition.X - hullBounds.X) * scaleFactor);
1442  me.Rect = new Rectangle(xPos - me.RectWidth / 2, me.Rect.Y, me.Rect.Width, me.Rect.Height);
1443  }
1444  else
1445  {
1446  int minX = (int)(leftHull.WorldRect.Right + (me.WorldRect.X - hullBounds.X) * scaleFactor);
1447  int maxX = (int)(leftHull.WorldRect.Right + (me.WorldRect.Right - hullBounds.X) * scaleFactor);
1448  me.Rect = new Rectangle(minX, me.Rect.Y, Math.Max(maxX - minX, 16), me.Rect.Height);
1449  }
1450  }
1451  else
1452  {
1453  if (!me.ResizeVertical)
1454  {
1455  int yPos = (int)(topHull.WorldRect.Y - topHull.RectHeight + (me.WorldPosition.Y - hullBounds.Bottom) * scaleFactor);
1456  me.Rect = new Rectangle(me.Rect.X, yPos + me.RectHeight / 2, me.Rect.Width, me.Rect.Height);
1457  }
1458  else
1459  {
1460  int minY = (int)(bottomHull.WorldRect.Y + (me.WorldRect.Y - me.RectHeight - hullBounds.Y) * scaleFactor);
1461  int maxY = (int)(bottomHull.WorldRect.Y + (me.WorldRect.Y - hullBounds.Y) * scaleFactor);
1462  me.Rect = new Rectangle(me.Rect.X, maxY, me.Rect.Width, Math.Max(maxY - minY, 16));
1463  }
1464  }
1465  }
1466  }
1467 
1468  if (hallwayLength > MinTwoDoorHallwayLength)
1469  {
1470  //connect waypoints
1471  var startWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.ThisGap);
1472  if (startWaypoint == null)
1473  {
1474  DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {module.ThisGapPosition.ToString().ToLower()} gap of the module \"{module.Info.Name}\".");
1475  continue;
1476  }
1477  var endWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.PreviousGap);
1478  if (endWaypoint == null)
1479  {
1480  DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.PreviousModule.Info.Name}\".");
1481  continue;
1482  }
1483 
1484  if (startWaypoint.WorldPosition.X > endWaypoint.WorldPosition.X)
1485  {
1486  (endWaypoint, startWaypoint) = (startWaypoint, endWaypoint);
1487  }
1488 
1489  //if the hallway is longer than 100 pixels, generate some waypoints inside it
1490  //for vertical hallways this isn't necessarily, it's done as a part of the ladder generation in AlignLadders
1491  const float distanceBetweenWaypoints = 100.0f;
1492  if (hallwayLength > distanceBetweenWaypoints)
1493  {
1494  WayPoint prevWayPoint = startWaypoint;
1495  WayPoint firstWayPoint = null;
1496  if (isHorizontal)
1497  {
1498  for (float x = leftHull.Rect.Right + distanceBetweenWaypoints / 2; x < rightHull.Rect.X - distanceBetweenWaypoints / 2; x += distanceBetweenWaypoints)
1499  {
1500  var newWayPoint = new WayPoint(new Vector2(x, hullBounds.Y + 110.0f), SpawnType.Path, sub);
1501  firstWayPoint ??= newWayPoint;
1502  prevWayPoint.linkedTo.Add(newWayPoint);
1503  newWayPoint.linkedTo.Add(prevWayPoint);
1504  prevWayPoint = newWayPoint;
1505  }
1506  }
1507  else if (startWaypoint.Ladders == null)
1508  {
1509  float bottom = bottomHull.Rect.Y;
1510  float top = topHull.Rect.Y - topHull.Rect.Height;
1511  for (float y = bottom + distanceBetweenWaypoints; y < top - distanceBetweenWaypoints; y += distanceBetweenWaypoints)
1512  {
1513  var newWayPoint = new WayPoint(new Vector2(startWaypoint.Position.X, y), SpawnType.Path, sub);
1514  firstWayPoint ??= newWayPoint;
1515  prevWayPoint.linkedTo.Add(newWayPoint);
1516  newWayPoint.linkedTo.Add(prevWayPoint);
1517  prevWayPoint = newWayPoint;
1518  }
1519  }
1520  else
1521  {
1522  startWaypoint.linkedTo.Add(endWaypoint);
1523  endWaypoint.linkedTo.Add(startWaypoint);
1524  }
1525  if (firstWayPoint != null)
1526  {
1527  firstWayPoint.linkedTo.Add(startWaypoint);
1528  startWaypoint.linkedTo.Add(firstWayPoint);
1529  }
1530  if (prevWayPoint != null)
1531  {
1532  prevWayPoint.linkedTo.Add(endWaypoint);
1533  endWaypoint.linkedTo.Add(prevWayPoint);
1534  }
1535  }
1536  else
1537  {
1538  startWaypoint.linkedTo.Add(endWaypoint);
1539  endWaypoint.linkedTo.Add(startWaypoint);
1540  }
1541  }
1542  }
1543  return placedEntities;
1544  }
1545 
1546  private static void LinkOxygenGenerators(IEnumerable<MapEntity> entities)
1547  {
1548  List<OxygenGenerator> oxygenGenerators = new List<OxygenGenerator>();
1549  List<Vent> vents = new List<Vent>();
1550  foreach (MapEntity e in entities)
1551  {
1552  if (e is Item item)
1553  {
1554  var oxygenGenerator = item.GetComponent<OxygenGenerator>();
1555  if (oxygenGenerator != null) { oxygenGenerators.Add(oxygenGenerator); }
1556  var vent = item.GetComponent<Vent>();
1557  if (vent != null) { vents.Add(vent); }
1558  }
1559  }
1560 
1561  //link every vent to the closest oxygen generator
1562  foreach (Vent vent in vents)
1563  {
1564  OxygenGenerator closestOxygenGenerator = null;
1565  float closestDist = float.MaxValue;
1566  foreach (OxygenGenerator oxygenGenerator in oxygenGenerators)
1567  {
1568  float dist = Vector2.DistanceSquared(oxygenGenerator.Item.WorldPosition, vent.Item.WorldPosition);
1569  if (dist < closestDist)
1570  {
1571  closestOxygenGenerator = oxygenGenerator;
1572  closestDist = dist;
1573  }
1574  }
1575  if (closestOxygenGenerator != null && !closestOxygenGenerator.Item.linkedTo.Contains(vent.Item))
1576  {
1577  closestOxygenGenerator.Item.linkedTo.Add(vent.Item);
1578  }
1579  }
1580  }
1581 
1582  private static void EnableFactionSpecificEntities(Submarine sub, Location location)
1583  {
1584  sub.EnableFactionSpecificEntities(location?.Faction?.Prefab.Identifier ?? Identifier.Empty);
1585  }
1586 
1587  private static void LockUnusedDoors(IEnumerable<PlacedModule> placedModules, Dictionary<PlacedModule, List<MapEntity>> entities, bool removeUnusedGaps)
1588  {
1589  foreach (PlacedModule module in placedModules)
1590  {
1591  foreach (MapEntity me in entities[module])
1592  {
1593  if (me is not Gap gap) { continue; }
1594  var door = gap.ConnectedDoor;
1595  if (door != null && !door.UseBetweenOutpostModules) { continue; }
1596  if (placedModules.Any(m => m.PreviousGap == gap || m.ThisGap == gap))
1597  {
1598  //gap in use -> remove linked entities that are marked to be removed
1599  if (gap.ConnectedDoor == null)
1600  {
1601  foreach (var otherEntity in entities[module])
1602  {
1603  if (otherEntity is Structure structure && structure.HasBody && !structure.IsPlatform && structure.RemoveIfLinkedOutpostDoorInUse &&
1604  Submarine.RectContains(structure.WorldRect, gap.WorldPosition))
1605  {
1606  RemoveLinkedEntity(otherEntity);
1607  }
1608  }
1609  }
1610  door?.Item.linkedTo.Where(lt => ShouldRemoveLinkedEntity(lt, doorInUse: true, module: module)).ForEachMod(lt => RemoveLinkedEntity(lt));
1611  continue;
1612  }
1613  if (door != null && DockingPort.List.Any(d => Submarine.RectContains(d.Item.WorldRect, door.Item.WorldPosition))) { continue; }
1614 
1615  //if the door is between two hulls of the same module, don't disable it
1616  if (gap.linkedTo.Count == 2 &&
1617  entities[module].Contains(gap.linkedTo[0]) &&
1618  entities[module].Contains(gap.linkedTo[1]))
1619  {
1620  continue;
1621  }
1622  if (door != null)
1623  {
1624  if (door.Item.linkedTo.Any(lt => lt is Structure))
1625  {
1626  //door not in use -> remove linked entities that are NOT marked to be removed
1627  door.Item.linkedTo.Where(lt => ShouldRemoveLinkedEntity(lt, doorInUse: false, module: module)).ForEachMod(lt => RemoveLinkedEntity(lt));
1628  WayPoint.WayPointList.Where(wp => wp.ConnectedDoor == door).ForEachMod(wp => wp.Remove());
1629  RemoveLinkedEntity(door.Item);
1630  continue;
1631  }
1632  else
1633  {
1634  door.Stuck = 100.0f;
1635  door.Item.NonInteractable = true;
1636  var connectionPanel = door.Item.GetComponent<ConnectionPanel>();
1637  if (connectionPanel != null) { connectionPanel.Locked = true; }
1638  }
1639  }
1640  else if (removeUnusedGaps)
1641  {
1642  gap.Remove();
1643  WayPoint.WayPointList.Where(wp => wp.ConnectedGap == gap).ForEachMod(wp => wp.Remove());
1644  }
1645  }
1646  entities[module].RemoveAll(e => e.Removed);
1647  }
1648 
1649  static bool ShouldRemoveLinkedEntity(MapEntity e, bool doorInUse, PlacedModule module)
1650  {
1651  if (e is Item { IsLadder: true } ladderItem)
1652  {
1653  int linkedToLadderCount = Door.DoorList.Count(otherDoor => otherDoor.Item.linkedTo.Contains(ladderItem));
1654  if (linkedToLadderCount > 1)
1655  {
1656  //if there's multiple doors linked to the ladder, never remove it
1657  //(the ladder is presumably not just for moving between two modules in that case, but might e.g. go through the whole module)
1658  return false;
1659  }
1660  return ladderItem.RemoveIfLinkedOutpostDoorInUse == doorInUse;
1661  }
1662 
1663  if (e is Structure structure)
1664  {
1665  return structure.RemoveIfLinkedOutpostDoorInUse == doorInUse;
1666  }
1667  else if (e is Item item)
1668  {
1669  if (item.GetComponent<PowerTransfer>() != null) { return false; }
1670  return item.RemoveIfLinkedOutpostDoorInUse == doorInUse;
1671  }
1672  return false;
1673  }
1674 
1675  static void RemoveLinkedEntity(MapEntity linked)
1676  {
1677  if (linked is Item linkedItem)
1678  {
1679  if (linkedItem.Connections != null)
1680  {
1681  foreach (Connection connection in linkedItem.Connections)
1682  {
1683  foreach (Wire w in connection.Wires.ToArray())
1684  {
1685  w?.Item.Remove();
1686  }
1687  }
1688  }
1689  //if we end up removing a ladder, remove its waypoints too
1690  if (linkedItem.GetComponent<Ladder>() is Ladder ladder)
1691  {
1692  var ladderWaypoints = WayPoint.WayPointList.FindAll(wp => wp.Ladders == ladder);
1693  foreach (var ladderWaypoint in ladderWaypoints)
1694  {
1695  //got through all waypoints linked to the ladder waypoints, and link them together
1696  //so we don't end up breaking up any paths by removing the ladder waypoints
1697  for (int i = 0; i < ladderWaypoint.linkedTo.Count; i++)
1698  {
1699  if (ladderWaypoint.linkedTo[i] is not WayPoint waypoint1 || waypoint1.Ladders == ladder) { continue; }
1700  for (int j = i + 1; j < ladderWaypoint.linkedTo.Count; j++)
1701  {
1702  if (ladderWaypoint.linkedTo[j] is not WayPoint waypoint2 || waypoint2.Ladders == ladder) { continue; }
1703  waypoint1.ConnectTo(waypoint2);
1704  }
1705  }
1706  }
1707  ladderWaypoints.ForEach(wp => wp.Remove());
1708  }
1709  }
1710  linked.Remove();
1711  }
1712  }
1713 
1714  private static void AlignLadders(IEnumerable<PlacedModule> placedModules, Dictionary<PlacedModule, List<MapEntity>> entities)
1715  {
1716  //how close ladders have to be horizontally for them to get aligned with each other
1717  float horizontalTolerance = 30.0f;
1718  foreach (PlacedModule module in placedModules)
1719  {
1720  var topModule =
1721  module.ThisGapPosition == OutpostModuleInfo.GapPosition.Top ?
1722  module.PreviousModule :
1723  placedModules.FirstOrDefault(m => m.PreviousModule == module && m.ThisGapPosition == OutpostModuleInfo.GapPosition.Bottom);
1724  if (topModule == null) { continue; }
1725 
1726  var topGap = module.ThisGapPosition == OutpostModuleInfo.GapPosition.Top ? module.ThisGap : topModule.ThisGap;
1727  var bottomGap = module.ThisGapPosition == OutpostModuleInfo.GapPosition.Top ? module.PreviousGap : topModule.PreviousGap;
1728 
1729  foreach (MapEntity me in entities[module])
1730  {
1731  var ladder = (me as Item)?.GetComponent<Ladder>();
1732  if (ladder == null) { continue; }
1733  if (ladder.Item.WorldRect.Right < topGap.WorldRect.X || ladder.Item.WorldPosition.X > topGap.WorldRect.Right) { continue; }
1734 
1735  var topLadder = entities[topModule].Find(e =>
1736  (e as Item)?.GetComponent<Ladder>() != null &&
1737  Math.Abs(e.WorldPosition.X - me.WorldPosition.X) < horizontalTolerance);
1738 
1739  int topLadderDiff = 0;
1740  int topLadderBottom = (int)(topModule.HullBounds.Y + topModule.Offset.Y + topModule.MoveOffset.Y + ladder.Item.Submarine.HiddenSubPosition.Y);
1741  if (topLadder != null)
1742  {
1743  topLadderBottom = topLadder.WorldRect.Y - topLadder.WorldRect.Height;
1744  }
1745 
1746  var newLadderRect = new Rectangle(
1747  ladder.Item.Rect.X + topLadderDiff,
1748  topLadderBottom,
1749  ladder.Item.Rect.Width,
1750  topLadderBottom - (ladder.Item.WorldRect.Y - ladder.Item.WorldRect.Height));
1751 
1752  Rectangle testOverlapRect = new Rectangle(newLadderRect.X, newLadderRect.Y + 30, newLadderRect.Width, newLadderRect.Height - 60);
1753  if (testOverlapRect.Height <= 0) { continue; }
1754 
1755  //don't extend the ladder if it'd have to go through a wall
1756  if (entities[module].Any(e => e is Structure structure && structure.HasBody && !structure.IsPlatform && Submarine.RectsOverlap(testOverlapRect, structure.Rect)))
1757  {
1758  continue;
1759  }
1760  ladder.Item.Rect = newLadderRect;
1761 
1762  if (topGap != null && bottomGap != null)
1763  {
1764  var startWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == bottomGap);
1765  var endWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == topGap);
1766  float margin = 100;
1767  if (startWaypoint != null && endWaypoint != null)
1768  {
1769  WayPoint prevWaypoint = startWaypoint;
1770  for (float y = bottomGap.Position.Y + margin; y <= topGap.Position.Y - margin; y += WayPoint.LadderWaypointInterval)
1771  {
1772  var wayPoint = new WayPoint(new Vector2(startWaypoint.Position.X, y), SpawnType.Path, ladder.Item.Submarine)
1773  {
1774  Ladders = ladder
1775  };
1776  prevWaypoint.ConnectTo(wayPoint);
1777  prevWaypoint = wayPoint;
1778  }
1779  prevWaypoint.ConnectTo(endWaypoint);
1780  }
1781  }
1782  }
1783  }
1784  }
1785 
1786  public static void PowerUpOutpost(Submarine sub)
1787  {
1788  //create a copy of the list, because EntitySpawner may not exist yet we're generating the level,
1789  //which can cause items to be removed/instantiated directly
1790  var entities = MapEntity.MapEntityList.Where(me => me.Submarine == sub).ToList();
1791 
1792  foreach (MapEntity e in entities)
1793  {
1794  if (e is not Item item) { continue; }
1795  var reactor = item.GetComponent<Reactor>();
1796  if (reactor != null)
1797  {
1798  reactor.PowerOn = true;
1799  reactor.AutoTemp = true;
1800  }
1801  }
1802 
1803  for (int i = 0; i < 600; i++)
1804  {
1805  Powered.UpdatePower((float)Timing.Step);
1806  foreach (MapEntity e in entities)
1807  {
1808  if (e is not Item item || item.GetComponent<Powered>() == null) { continue; }
1809  item.Update((float)Timing.Step, GameMain.GameScreen.Cam);
1810  }
1811  }
1812  }
1813 
1814  public static void SpawnNPCs(Location location, Submarine outpost)
1815  {
1816  if (outpost?.Info?.OutpostGenerationParams == null) { return; }
1817 
1818  List<HumanPrefab> killedCharacters = new List<HumanPrefab>();
1819  List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)> selectedCharacters
1820  = new List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)>();
1821 
1822  List<FactionPrefab> factions = new List<FactionPrefab>();
1823  if (location?.Faction != null) { factions.Add(location.Faction.Prefab); }
1824  if (location?.SecondaryFaction != null) { factions.Add(location.SecondaryFaction.Prefab); }
1825 
1826  var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(factions, outpost, Rand.RandSync.ServerAndClient);
1827  foreach (HumanPrefab humanPrefab in humanPrefabs)
1828  {
1829  if (humanPrefab is null) { continue; }
1830  var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient);
1831  if (location != null && location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier()))
1832  {
1833  killedCharacters.Add(humanPrefab);
1834  continue;
1835  }
1836  selectedCharacters.Add((humanPrefab, characterInfo));
1837  }
1838 
1839  //replace killed characters with new ones
1840  foreach (HumanPrefab killedCharacter in killedCharacters)
1841  {
1842  for (int tries = 0; tries < 100; tries++)
1843  {
1844  var characterInfo = killedCharacter.CreateCharacterInfo(Rand.RandSync.ServerAndClient);
1845  if (location != null && !location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier()))
1846  {
1847  selectedCharacters.Add((killedCharacter, characterInfo));
1848  break;
1849  }
1850  }
1851  }
1852 
1853  foreach ((var humanPrefab, var characterInfo) in selectedCharacters)
1854  {
1855  Rand.SetSyncedSeed(ToolBox.StringToInt(characterInfo.Name));
1856 
1857  ISpatialEntity gotoTarget = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, humanPrefab.GetModuleFlags(), humanPrefab.GetSpawnPointTags());
1858  if (gotoTarget == null)
1859  {
1860  gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.ServerAndClient);
1861  }
1862  characterInfo.TeamID = CharacterTeamType.FriendlyNPC;
1863  var npc = Character.Create(characterInfo.SpeciesName, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
1864  npc.AnimController.FindHull(gotoTarget.WorldPosition, setSubmarine: true);
1865  npc.TeamID = CharacterTeamType.FriendlyNPC;
1866  npc.HumanPrefab = humanPrefab;
1867  outpost.Info.AddOutpostNPCIdentifierOrTag(npc, humanPrefab.Identifier);
1868  foreach (Identifier tag in humanPrefab.GetTags())
1869  {
1870  outpost.Info.AddOutpostNPCIdentifierOrTag(npc, tag);
1871  }
1872  if (GameMain.NetworkMember?.ServerSettings != null && !GameMain.NetworkMember.ServerSettings.KillableNPCs)
1873  {
1874  npc.CharacterHealth.Unkillable = true;
1875  }
1876  humanPrefab.GiveItems(npc, outpost, gotoTarget as WayPoint, Rand.RandSync.ServerAndClient);
1877  foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true))
1878  {
1879  item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing;
1880  item.SpawnedInCurrentOutpost = true;
1881  }
1882  humanPrefab.InitializeCharacter(npc, gotoTarget);
1883  }
1884  }
1885  }
1886 }
static void UpdatePower(float deltaTime)
Update the power calculations of all devices and grids Updates grids in the order of ConnCurrConsumpt...
bool TryConnect(Connection newConnection, bool addNode=true, bool sendNetworkEvent=false)
Tries to add the given connection to this wire. Note that this only affects the wire - adding the wir...
@ Character
Characters only
@ Structure
Structures and hulls, but also items (for backwards support)!