Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Map/Map/Map.cs
2 using Microsoft.Xna.Framework;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Xml.Linq;
7 using Voronoi2;
8 
9 namespace Barotrauma
10 {
11  partial class Map
12  {
13  public bool AllowDebugTeleport;
14 
15  private readonly MapGenerationParams generationParams;
16 
17  private Location furthestDiscoveredLocation;
18 
19  public int Width { get; private set; }
20  public int Height { get; private set; }
21 
22  public Action<Location, LocationConnection> OnLocationSelected;
23  public Action<LocationConnection, IEnumerable<Mission>> OnMissionsSelected;
24 
25  public readonly struct LocationChangeInfo
26  {
27  public readonly Location PrevLocation;
28  public readonly Location NewLocation;
29 
30  public LocationChangeInfo(Location prevLocation, Location newLocation)
31  {
32  PrevLocation = prevLocation;
33  NewLocation = newLocation;
34  }
35  }
36 
40  public readonly NamedEvent<LocationChangeInfo> OnLocationChanged = new NamedEvent<LocationChangeInfo>();
41 
42  private List<Location> endLocations = new List<Location>();
43  public IReadOnlyList<Location> EndLocations { get { return endLocations; } }
44 
45  public Location StartLocation { get; private set; }
46 
47  public Location CurrentLocation { get; private set; }
48 
50  {
51  get { return Locations.IndexOf(CurrentLocation); }
52  }
53 
54  public Location SelectedLocation { get; private set; }
55 
57  {
58  get { return Locations.IndexOf(SelectedLocation); }
59  }
60 
61  public IEnumerable<int> GetSelectedMissionIndices()
62  {
63  return SelectedConnection == null ? Enumerable.Empty<int>() : CurrentLocation.GetSelectedMissionIndices();
64  }
65 
66  public LocationConnection SelectedConnection { get; private set; }
67 
68  public string Seed { get; private set; }
69 
70  public List<Location> Locations { get; private set; }
71 
72  private readonly List<Location> locationsDiscovered = new List<Location>();
73  private readonly List<Location> locationsVisited = new List<Location>();
74 
75  public List<LocationConnection> Connections { get; private set; }
76 
78 
79  private bool trackedLocationDiscoveryAndVisitOrder = true;
80 
81  public Map(CampaignSettings settings)
82  {
83  generationParams = MapGenerationParams.Instance;
84  Width = generationParams.Width;
85  Height = generationParams.Height;
86  Locations = new List<Location>();
87  Connections = new List<LocationConnection>();
88  if (generationParams.RadiationParams != null)
89  {
90  Radiation = new Radiation(this, generationParams.RadiationParams)
91  {
92  Enabled = settings.RadiationEnabled
93  };
94  }
95  }
96 
100  private Map(CampaignMode campaign, XElement element) : this(campaign.Settings)
101  {
102  Seed = element.GetAttributeString("seed", "a");
103  Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
104 
105  Width = element.GetAttributeInt("width", Width);
106  Height = element.GetAttributeInt("height", Height);
107 
108  bool lairsFound = false;
109 
110  foreach (var subElement in element.Elements())
111  {
112  switch (subElement.Name.ToString().ToLowerInvariant())
113  {
114  case "location":
115  int i = subElement.GetAttributeInt("i", 0);
116  while (Locations.Count <= i)
117  {
118  Locations.Add(null);
119  }
120  lairsFound |= subElement.GetAttributeString("type", "").Equals("lair", StringComparison.OrdinalIgnoreCase);
121  Locations[i] = new Location(campaign, subElement);
122  break;
123  case "radiation":
124  Radiation = new Radiation(this, generationParams.RadiationParams, subElement)
125  {
126  Enabled = campaign.Settings.RadiationEnabled
127  };
128  break;
129  }
130  }
131 
132  List<XElement> connectionElements = new List<XElement>();
133  foreach (var subElement in element.Elements())
134  {
135  switch (subElement.Name.ToString().ToLowerInvariant())
136  {
137  case "connection":
138  Point locationIndices = subElement.GetAttributePoint("locations", new Point(0, 1));
139  if (locationIndices.X == locationIndices.Y) { continue; }
140  var connection = new LocationConnection(Locations[locationIndices.X], Locations[locationIndices.Y])
141  {
142  Passed = subElement.GetAttributeBool("passed", false),
143  Locked = subElement.GetAttributeBool("locked", false),
144  Difficulty = subElement.GetAttributeFloat("difficulty", 0.0f)
145  };
146  Locations[locationIndices.X].Connections.Add(connection);
147  Locations[locationIndices.Y].Connections.Add(connection);
148  string biomeId = subElement.GetAttributeString("biome", "");
149  connection.Biome =
150  Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeId) ??
151  Biome.Prefabs.FirstOrDefault(b => !b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeId) ??
152  Biome.Prefabs.First();
153  connection.Difficulty = MathHelper.Clamp(connection.Difficulty, connection.Biome.MinDifficulty, connection.Biome.AdjustedMaxDifficulty);
154  connection.LevelData = new LevelData(subElement.Element("Level"), connection.Difficulty);
155  Connections.Add(connection);
156  connectionElements.Add(subElement);
157  break;
158  }
159  }
160 
161  //backwards compatibility: location biomes weren't saved (or used for anything) previously,
162  //assign them if they haven't been assigned
163  Random rand = new MTRandom(ToolBox.StringToInt(Seed));
164  if (Locations.First().Biome == null)
165  {
166  AssignBiomes(rand);
167  }
168 
169  int startLocationindex = element.GetAttributeInt("startlocation", -1);
170  if (startLocationindex >= 0 && startLocationindex < Locations.Count)
171  {
172  StartLocation = Locations[startLocationindex];
173  }
174  else
175  {
176  DebugConsole.AddWarning($"Error while loading the map. Start location index out of bounds (index: {startLocationindex}, location count: {Locations.Count}).");
177  foreach (Location location in Locations)
178  {
179  if (!location.Type.HasOutpost) { continue; }
180  if (StartLocation == null || location.MapPosition.X < StartLocation.MapPosition.X)
181  {
182  StartLocation = location;
183  }
184  }
185  }
186 
187  if (element.GetAttribute("endlocation") != null)
188  {
189  //backwards compatibility
190  int endLocationIndex = element.GetAttributeInt("endlocation", -1);
191  if (endLocationIndex >= 0 && endLocationIndex < Locations.Count)
192  {
193  endLocations.Add(Locations[endLocationIndex]);
194  }
195  else
196  {
197  DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count}).");
198  }
199  }
200  else
201  {
202  int[] endLocationindices = element.GetAttributeIntArray("endlocations", Array.Empty<int>());
203  foreach (int endLocationIndex in endLocationindices)
204  {
205  if (endLocationIndex >= 0 && endLocationIndex < Locations.Count)
206  {
207  endLocations.Add(Locations[endLocationIndex]);
208  }
209  else
210  {
211  DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count}).");
212  }
213  }
214  }
215 
216  if (!endLocations.Any())
217  {
218  DebugConsole.AddWarning($"Error while loading the map. No end location(s) found. Choosing the rightmost location as the end location...");
219  Location endLocation = null;
220  foreach (Location location in Locations)
221  {
222  if (endLocation == null || location.MapPosition.X > endLocation.MapPosition.X)
223  {
224  endLocation = location;
225  }
226  }
227  endLocations.Add(endLocation);
228  }
229 
230  System.Diagnostics.Debug.Assert(endLocations.First().Biome != null, "End location biome was null.");
231  System.Diagnostics.Debug.Assert(endLocations.First().Biome.IsEndBiome, "The biome of the end location isn't the end biome.");
232 
233  //backwards compatibility (or support for loading maps created with mods that modify the end biome setup):
234  //if there's too few end locations, create more
235  int missingOutpostCount = endLocations.First().Biome.EndBiomeLocationCount - endLocations.Count;
236 
237  Location firstEndLocation = EndLocations[0];
238  for (int i = 0; i < missingOutpostCount; i++)
239  {
240  Vector2 mapPos = new Vector2(
241  MathHelper.Lerp(firstEndLocation.MapPosition.X, Width, MathHelper.Lerp(0.2f, 0.8f, i / (float)missingOutpostCount)),
242  Height * MathHelper.Lerp(0.2f, 1.0f, (float)rand.NextDouble()));
243  var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations)
244  {
245  Biome = endLocations.First().Biome
246  };
247  newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f);
248  Locations.Add(newEndLocation);
249  endLocations.Add(newEndLocation);
250  }
251 
252  //backwards compatibility: if the map contained the now-removed lairs and has no hunting grounds, create some hunting grounds
253  if (lairsFound && !Connections.Any(c => c.LevelData.HasHuntingGrounds))
254  {
255  for (int i = 0; i < Connections.Count; i++)
256  {
257  Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * LevelData.MaxHuntingGroundsProbability;
258  connectionElements[i].SetAttributeValue("hashuntinggrounds", true);
259  }
260  }
261 
262  foreach (var endLocation in EndLocations)
263  {
264  if (endLocation.Type?.ForceLocationName is { IsEmpty: false })
265  {
266  endLocation.ForceName(endLocation.Type.ForceLocationName);
267  }
268  }
269 
270  AssignEndLocationLevelData();
271 
272  //backwards compatibility: if locations go out of bounds (map saved with different generation parameters before width/height were included in the xml)
273  float maxX = Locations.Select(l => l.MapPosition.X).Max();
274  if (maxX > Width) { Width = (int)(maxX + 10); }
275  float maxY = Locations.Select(l => l.MapPosition.Y).Max();
276  if (maxY > Height) { Height = (int)(maxY + 10); }
277 
278  InitProjectSpecific();
279  }
280 
284  public Map(CampaignMode campaign, string seed) : this(campaign.Settings)
285  {
286  Seed = seed;
287  Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
288 
289  Generate(campaign);
290 
291  if (Locations.Count == 0)
292  {
293  throw new Exception($"Generating a campaign map failed (no locations created). Width: {Width}, height: {Height}");
294  }
295 
296  FindStartLocation(l => l.Type.Identifier == "outpost");
297  //if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost
298  if (CurrentLocation == null)
299  {
300  FindStartLocation(l => l.Type.HasOutpost && l.Type.OutpostTeam == CharacterTeamType.FriendlyNPC);
301  }
302 
303  void FindStartLocation(Func<Location, bool> predicate)
304  {
305  foreach (Location location in Locations)
306  {
307  if (!predicate(location)) { continue; }
308  if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
309  {
310  CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
311  }
312  }
313  }
314 
316  var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost);
317  if (startOutpostFaction != null)
318  {
319  StartLocation.Faction = startOutpostFaction;
320  }
321  foreach (var connection in StartLocation.Connections)
322  {
323  //force locations adjacent to the start location to have an outpost
324  //non-inhabited locations seem to be confusing to new players, particularly
325  //on the first round/mission when they still don't know how transitions between levels work
326  var otherLocation = connection.OtherLocation(StartLocation);
327  if (!otherLocation.HasOutpost())
328  {
329  if (LocationType.Prefabs.TryGet("outpost".ToIdentifier(), out LocationType outpostLocationType))
330  {
331  otherLocation.ChangeType(campaign, outpostLocationType);
332  }
333  }
334 
335  if (otherLocation.HasOutpost() &&
336  otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC &&
337  otherLocation.Type.Faction.IsEmpty)
338  {
339  otherLocation.Faction = startOutpostFaction;
340  }
341  }
342 
343  System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
344 
345  int loops = campaign.CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0);
346  if (loops == 0 && (campaign.Settings.WorldHostility == WorldHostilityOption.Low || campaign.Settings.WorldHostility == WorldHostilityOption.Medium))
347  {
348  if (StartLocation != null)
349  {
351  }
352 
353  //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy
354  foreach (var locationConnection in StartLocation.Connections)
355  {
356  if (locationConnection.Difficulty > 0.0f)
357  {
358  locationConnection.Difficulty = 0.0f;
359  locationConnection.LevelData = new LevelData(locationConnection);
360  }
361  }
362  }
363 
364  if (campaign.IsSinglePlayer && campaign.Settings.TutorialEnabled && LocationType.Prefabs.TryGet("tutorialoutpost", out var tutorialOutpost))
365  {
366  CurrentLocation.ChangeType(campaign, tutorialOutpost);
367  }
371 
372  foreach (var location in Locations)
373  {
374  location.UnlockInitialMissions();
375  }
376 
377  InitProjectSpecific();
378  }
379 
380  partial void InitProjectSpecific();
381 
382  #region Generation
383 
384  private void Generate(CampaignMode campaign)
385  {
386  Connections.Clear();
387  Locations.Clear();
388 
389  List<Vector2> voronoiSites = new List<Vector2>();
390  for (float x = 10.0f; x < Width - 10.0f; x += generationParams.VoronoiSiteInterval.X)
391  {
392  for (float y = 10.0f; y < Height - 10.0f; y += generationParams.VoronoiSiteInterval.Y)
393  {
394  voronoiSites.Add(new Vector2(
395  x + generationParams.VoronoiSiteVariance.X * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient),
396  y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient)));
397  }
398  }
399 
400  Voronoi voronoi = new Voronoi(0.5f);
401  List<GraphEdge> edges = voronoi.MakeVoronoiGraph(voronoiSites, Width, Height);
402 
403  Vector2 margin = new Vector2(
404  Math.Min(10, Width * 0.1f),
405  Math.Min(10, Height * 0.2f));
406 
407  float startX = margin.X, endX = Width - margin.X;
408  float startY = margin.Y, endY = Height - margin.Y;
409 
410  if (!edges.Any())
411  {
412  throw new Exception($"Generating a campaign map failed (no edges in the voronoi graph). Width: {Width}, height: {Height}, margin: {margin}");
413  }
414 
415  voronoiSites.Clear();
416  Dictionary<int, List<Location>> locationsPerZone = new Dictionary<int, List<Location>>();
417  bool possibleStartOutpostCreated = false;
418  foreach (GraphEdge edge in edges)
419  {
420  if (edge.Point1 == edge.Point2) { continue; }
421 
422  if (edge.Point1.X < margin.X || edge.Point1.X > Width - margin.X || edge.Point1.Y < startY || edge.Point1.Y > endY)
423  {
424  continue;
425  }
426  if (edge.Point2.X < margin.X || edge.Point2.X > Width - margin.X || edge.Point2.Y < startY || edge.Point2.Y > endY)
427  {
428  continue;
429  }
430 
431  Location[] newLocations = new Location[2];
432  newLocations[0] = Locations.Find(l => l.MapPosition == edge.Point1 || l.MapPosition == edge.Point2);
433  newLocations[1] = Locations.Find(l => l != newLocations[0] && (l.MapPosition == edge.Point1 || l.MapPosition == edge.Point2));
434 
435  for (int i = 0; i < 2; i++)
436  {
437  if (newLocations[i] != null) { continue; }
438 
439  Vector2[] points = new Vector2[] { edge.Point1, edge.Point2 };
440 
441  int positionIndex = Rand.Int(1, Rand.RandSync.ServerAndClient);
442 
443  Vector2 position = points[positionIndex];
444  if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; }
445  int zone = GetZoneIndex(position.X);
446  if (!locationsPerZone.ContainsKey(zone))
447  {
448  locationsPerZone[zone] = new List<Location>();
449  }
450 
451  LocationType forceLocationType = null;
452  if (!possibleStartOutpostCreated)
453  {
454  float zoneWidth = Width / generationParams.DifficultyZones;
455  float threshold = zoneWidth * 0.1f;
456  if (position.X < threshold)
457  {
458  LocationType.Prefabs.TryGet("outpost", out forceLocationType);
459  possibleStartOutpostCreated = true;
460  }
461  }
462 
463  if (forceLocationType == null)
464  {
465  foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
466  {
467  if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount)
468  {
469  forceLocationType = locationType;
470  break;
471  }
472  }
473  }
474 
475  newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient),
476  requireOutpost: false, forceLocationType: forceLocationType, existingLocations: Locations);
477  locationsPerZone[zone].Add(newLocations[i]);
478  Locations.Add(newLocations[i]);
479  }
480 
481  var newConnection = new LocationConnection(newLocations[0], newLocations[1]);
482  Connections.Add(newConnection);
483  }
484 
485  //remove connections that are too short
486  float minConnectionDistanceSqr = generationParams.MinConnectionDistance * generationParams.MinConnectionDistance;
487  for (int i = Connections.Count - 1; i >= 0; i--)
488  {
489  LocationConnection connection = Connections[i];
490 
491  if (Vector2.DistanceSquared(connection.Locations[0].MapPosition, connection.Locations[1].MapPosition) > minConnectionDistanceSqr)
492  {
493  continue;
494  }
495 
496  //locations.Remove(connection.Locations[0]);
497  Connections.Remove(connection);
498 
499  foreach (LocationConnection connection2 in Connections)
500  {
501  if (connection2.Locations[0] == connection.Locations[0]) { connection2.Locations[0] = connection.Locations[1]; }
502  if (connection2.Locations[1] == connection.Locations[0]) { connection2.Locations[1] = connection.Locations[1]; }
503  }
504  }
505 
506  foreach (LocationConnection connection in Connections)
507  {
508  connection.Locations[0].Connections.Add(connection);
509  connection.Locations[1].Connections.Add(connection);
510  }
511 
512  //remove locations that are too close to each other
513  float minLocationDistanceSqr = generationParams.MinLocationDistance * generationParams.MinLocationDistance;
514  for (int i = Locations.Count - 1; i >= 0; i--)
515  {
516  for (int j = Locations.Count - 1; j > i; j--)
517  {
518  float dist = Vector2.DistanceSquared(Locations[i].MapPosition, Locations[j].MapPosition);
519  if (dist > minLocationDistanceSqr)
520  {
521  continue;
522  }
523  //move connections from Locations[j] to Locations[i]
524  foreach (LocationConnection connection in Locations[j].Connections)
525  {
526  if (connection.Locations[0] == Locations[j])
527  {
528  connection.Locations[0] = Locations[i];
529  }
530  else
531  {
532  connection.Locations[1] = Locations[i];
533  }
534 
535  if (connection.Locations[0] != connection.Locations[1])
536  {
537  Locations[i].Connections.Add(connection);
538  }
539  else
540  {
541  Connections.Remove(connection);
542  }
543  }
544  Locations[i].Connections.RemoveAll(c => c.OtherLocation(Locations[i]) == Locations[j]);
545  Locations.RemoveAt(j);
546  }
547  }
548 
549  //make sure the connections are in the same order on the locations and the Connections list
550  //otherwise their order will change when loading the game (as they're added to the locations in the same order they're loaded)
551  foreach (var location in Locations)
552  {
553  location.Connections.Sort((c1, c2) => Connections.IndexOf(c1).CompareTo(Connections.IndexOf(c2)));
554  }
555 
556  for (int i = Connections.Count - 1; i >= 0; i--)
557  {
558  i = Math.Min(i, Connections.Count - 1);
559  LocationConnection connection = Connections[i];
560  for (int n = Math.Min(i - 1, Connections.Count - 1); n >= 0; n--)
561  {
562  if (connection.Locations.Contains(Connections[n].Locations[0])
563  && connection.Locations.Contains(Connections[n].Locations[1]))
564  {
565  Connections.RemoveAt(n);
566  }
567  }
568  }
569 
570  List<LocationConnection>[] connectionsBetweenZones = new List<LocationConnection>[generationParams.DifficultyZones];
571  for (int i = 0; i < generationParams.DifficultyZones; i++)
572  {
573  connectionsBetweenZones[i] = new List<LocationConnection>();
574  }
575  var shuffledConnections = Connections.ToList();
576  shuffledConnections.Shuffle(Rand.RandSync.ServerAndClient);
577  foreach (var connection in shuffledConnections)
578  {
579  int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X);
580  int zone2 = GetZoneIndex(connection.Locations[1].MapPosition.X);
581  if (zone1 == zone2) { continue; }
582  if (zone1 > zone2)
583  {
584  (zone1, zone2) = (zone2, zone1);
585  }
586 
587  if (generationParams.GateCount[zone1] == 0) { continue; }
588 
589  if (!connectionsBetweenZones[zone1].Any())
590  {
591  connectionsBetweenZones[zone1].Add(connection);
592  }
593  else if (generationParams.GateCount[zone1] == 1)
594  {
595  //if there's only one connection, place it at the center of the map
596  if (Math.Abs(connection.CenterPos.Y - Height / 2) < Math.Abs(connectionsBetweenZones[zone1].First().CenterPos.Y - Height / 2))
597  {
598  connectionsBetweenZones[zone1].Clear();
599  connectionsBetweenZones[zone1].Add(connection);
600  }
601  }
602  else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1] &&
603  connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1])))
604  {
605  connectionsBetweenZones[zone1].Add(connection);
606  }
607  }
608 
609  var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList();
610  for (int i = Connections.Count - 1; i >= 0; i--)
611  {
612  int zone1 = GetZoneIndex(Connections[i].Locations[0].MapPosition.X);
613  int zone2 = GetZoneIndex(Connections[i].Locations[1].MapPosition.X);
614  if (zone1 == zone2) { continue; }
615  if (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) { continue; }
616 
617  int leftZone = Math.Min(zone1, zone2);
618  if (generationParams.GateCount[leftZone] == 0) { continue; }
619  if (!connectionsBetweenZones[leftZone].Contains(Connections[i]))
620  {
621  Connections.RemoveAt(i);
622  }
623  else
624  {
625  var leftMostLocation =
626  Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ?
627  Connections[i].Locations[0] :
628  Connections[i].Locations[1];
629  if (!AllowAsBiomeGate(leftMostLocation.Type))
630  {
631  leftMostLocation.ChangeType(
632  campaign,
633  LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => AllowAsBiomeGate(lt)),
634  createStores: false);
635  }
636  static bool AllowAsBiomeGate(LocationType lt)
637  {
638  //checking for "abandoned" is not strictly necessary here because it's now configured to not be allowed as a biome gate
639  //but might be better to keep it for backwards compatibility (previously we relied only on that check)
640  return lt.HasOutpost && lt.Identifier != "abandoned" && lt.AllowAsBiomeGate;
641  }
642 
643  leftMostLocation.IsGateBetweenBiomes = true;
644  Connections[i].Locked = true;
645 
646  if (leftMostLocation.Type.HasOutpost && campaign != null && gateFactions.Any())
647  {
648  leftMostLocation.Faction = gateFactions[connectionsBetweenZones[leftZone].IndexOf(Connections[i]) % gateFactions.Count];
649  }
650  }
651  }
652 
653  foreach (Location location in Locations)
654  {
655  for (int i = location.Connections.Count - 1; i >= 0; i--)
656  {
657  if (!Connections.Contains(location.Connections[i]))
658  {
659  location.Connections.RemoveAt(i);
660  }
661  }
662  }
663 
664  //make sure the location at the right side of the gate between biomes isn't a dead-end
665  //those may sometimes generate if all the connections of the right-side location lead to the previous biome
666  //(i.e. a situation where the adjacent locations happen to be at the left side of the border of the biomes, see see Regalis11/Barotrauma#10047)
667  for (int i = 0; i < Connections.Count; i++)
668  {
669  var connection = Connections[i];
670  if (!connection.Locked) { continue; }
671  var rightMostLocation =
672  connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ?
673  connection.Locations[0] :
674  connection.Locations[1];
675 
676  //if there's only one connection (= the connection between biomes), create a new connection to the closest location to the right
677  if (rightMostLocation.Connections.Count == 1)
678  {
679  Location closestLocation = null;
680  float closestDist = float.PositiveInfinity;
681  foreach (Location otherLocation in Locations)
682  {
683  if (otherLocation == rightMostLocation || otherLocation.MapPosition.X < rightMostLocation.MapPosition.X) { continue; }
684  float dist = Vector2.DistanceSquared(rightMostLocation.MapPosition, otherLocation.MapPosition);
685  if (dist < closestDist || closestLocation == null)
686  {
687  closestLocation = otherLocation;
688  closestDist = dist;
689  }
690  }
691 
692  var newConnection = new LocationConnection(rightMostLocation, closestLocation);
693  rightMostLocation.Connections.Add(newConnection);
694  closestLocation.Connections.Add(newConnection);
695  Connections.Add(newConnection);
696  GenerateLocationConnectionVisuals(newConnection);
697  }
698  }
699 
700  //remove orphans
701  Locations.RemoveAll(l => !Connections.Any(c => c.Locations.Contains(l)));
702 
703  AssignBiomes(new MTRandom(ToolBox.StringToInt(Seed)));
704 
705  foreach (LocationConnection connection in Connections)
706  {
707  if (connection.Locations.Any(l => l.IsGateBetweenBiomes))
708  {
709  connection.Difficulty = Math.Min(connection.Locations.Min(l => l.Biome.ActualMaxDifficulty), connection.Biome.AdjustedMaxDifficulty);
710  }
711  else
712  {
713  connection.Difficulty = CalculateDifficulty(connection.CenterPos.X, connection.Biome);
714  }
715  }
716 
717  foreach (Location location in Locations)
718  {
719  location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
720  location.TryAssignFactionBasedOnLocationType(campaign);
721  if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
722  {
723  if (location.Type.Faction.IsEmpty)
724  {
725  //no faction defined in the location type, assign a random one
726  location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
727  }
728  if (location.Type.SecondaryFaction.IsEmpty)
729  {
730  //no secondary faction defined in the location type, assign a random one
731  location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
732  }
733  }
734  location.CreateStores(force: true);
735  }
736 
737  foreach (LocationConnection connection in Connections)
738  {
739  connection.LevelData = new LevelData(connection);
740  }
741 
742  CreateEndLocation(campaign);
743 
744  float CalculateDifficulty(float mapPosition, Biome biome)
745  {
746  float settingsFactor = campaign.Settings.LevelDifficultyMultiplier;
747  float minDifficulty = 0;
748  float maxDifficulty = 100;
749  float difficulty = mapPosition / Width * 100;
750  System.Diagnostics.Debug.Assert(biome != null);
751  if (biome != null)
752  {
753  minDifficulty = biome.MinDifficulty;
754  maxDifficulty = biome.AdjustedMaxDifficulty;
755  float diff = 1 - settingsFactor;
756  difficulty *= 1 - (1f / biome.AllowedZones.Max() * diff);
757  }
758  return MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty);
759  }
760  }
761 
762  partial void GenerateAllLocationConnectionVisuals();
763 
764  partial void GenerateLocationConnectionVisuals(LocationConnection connection);
765 
766  private int GetZoneIndex(float xPos)
767  {
768  float zoneWidth = Width / generationParams.DifficultyZones;
769  return MathHelper.Clamp((int)Math.Floor(xPos / zoneWidth) + 1, 1, generationParams.DifficultyZones);
770  }
771 
772  public Biome GetBiome(Vector2 mapPos)
773  {
774  return GetBiome(mapPos.X);
775  }
776 
777  public Biome GetBiome(float xPos)
778  {
779  float zoneWidth = Width / generationParams.DifficultyZones;
780  int zoneIndex = (int)Math.Floor(xPos / zoneWidth) + 1;
781  zoneIndex = Math.Clamp(zoneIndex, 1, generationParams.DifficultyZones - 1);
782  return Biome.Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
783  }
784 
785  private void AssignBiomes(Random rand)
786  {
787  var biomes = Biome.Prefabs;
788  float zoneWidth = Width / generationParams.DifficultyZones;
789 
790  List<Biome> allowedBiomes = new List<Biome>(10);
791  for (int i = 0; i < generationParams.DifficultyZones; i++)
792  {
793  allowedBiomes.Clear();
794  allowedBiomes.AddRange(biomes.Where(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i)));
795  float zoneX = zoneWidth * (generationParams.DifficultyZones - i);
796 
797  foreach (Location location in Locations)
798  {
799  if (location.MapPosition.X < zoneX)
800  {
801  location.Biome = allowedBiomes[rand.Next() % allowedBiomes.Count];
802  }
803  }
804  }
805  foreach (LocationConnection connection in Connections)
806  {
807  if (connection.Biome != null) { continue; }
808  connection.Biome = connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ? connection.Locations[0].Biome : connection.Locations[1].Biome;
809  }
810 
811  System.Diagnostics.Debug.Assert(Locations.All(l => l.Biome != null));
812  System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null));
813  }
814 
815  private Location GetPreviousToEndLocation()
816  {
817  Location previousToEndLocation = null;
818  foreach (Location location in Locations)
819  {
820  if (!location.Biome.IsEndBiome && (previousToEndLocation == null || location.MapPosition.X > previousToEndLocation.MapPosition.X))
821  {
822  previousToEndLocation = location;
823  }
824  }
825  return previousToEndLocation;
826  }
827 
828  private void ForceLocationTypeToNone(CampaignMode campaign, Location location)
829  {
830  if (LocationType.Prefabs.TryGet("none", out LocationType locationType))
831  {
832  location.ChangeType(campaign, locationType, createStores: false);
833  }
834  location.DisallowLocationTypeChanges = true;
835  }
836 
837  private void CreateEndLocation(CampaignMode campaign)
838  {
839  float zoneWidth = Width / generationParams.DifficultyZones;
840  Vector2 endPos = new Vector2(Width - zoneWidth * 0.7f, Height / 2);
841  float closestDist = float.MaxValue;
842  var endLocation = Locations.First();
843  foreach (Location location in Locations)
844  {
845  float dist = Vector2.DistanceSquared(endPos, location.MapPosition);
846  if (location.Biome.IsEndBiome && dist < closestDist)
847  {
848  endLocation = location;
849  closestDist = dist;
850  }
851  }
852 
853  var previousToEndLocation = GetPreviousToEndLocation();
854  if (endLocation == null || previousToEndLocation == null) { return; }
855 
856  endLocations = new List<Location>() { endLocation };
857  if (endLocation.Biome.EndBiomeLocationCount > 1)
858  {
859  FindConnectedEndLocations(endLocation);
860 
861  void FindConnectedEndLocations(Location currLocation)
862  {
863  if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; }
864  foreach (var connection in currLocation.Connections)
865  {
866  if (connection.Biome != endLocation.Biome) { continue; }
867  var otherLocation = connection.OtherLocation(currLocation);
868  if (otherLocation != null && !endLocations.Contains(otherLocation))
869  {
870  if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; }
871  endLocations.Add(otherLocation);
872  FindConnectedEndLocations(otherLocation);
873  }
874  }
875  }
876  }
877 
878  ForceLocationTypeToNone(campaign, previousToEndLocation);
879 
880  //remove all locations from the end biome except the end location
881  for (int i = Locations.Count - 1; i >= 0; i--)
882  {
883  if (Locations[i].Biome.IsEndBiome)
884  {
885  for (int j = Locations[i].Connections.Count - 1; j >= 0; j--)
886  {
887  if (j >= Locations[i].Connections.Count) { continue; }
888  var connection = Locations[i].Connections[j];
889  var otherLocation = connection.OtherLocation(Locations[i]);
890  Locations[i].Connections.RemoveAt(j);
891  otherLocation?.Connections.Remove(connection);
892  Connections.Remove(connection);
893  }
894  if (!endLocations.Contains(Locations[i]))
895  {
896  Locations.RemoveAt(i);
897  }
898  }
899  }
900 
901  //removed all connections from the second-to-last location, need to reconnect it
902  if (previousToEndLocation.Connections.None())
903  {
904  Location connectTo = Locations.First();
905  foreach (Location location in Locations)
906  {
907  if (!location.Biome.IsEndBiome && location != previousToEndLocation && location.MapPosition.X > connectTo.MapPosition.X)
908  {
909  connectTo = location;
910  }
911  }
912  var newConnection = new LocationConnection(previousToEndLocation, connectTo)
913  {
914  Biome = endLocation.Biome,
915  Difficulty = 100.0f
916  };
917  newConnection.LevelData = new LevelData(newConnection);
918  Connections.Add(newConnection);
919  previousToEndLocation.Connections.Add(newConnection);
920  connectTo.Connections.Add(newConnection);
921  }
922 
923  var endConnection = new LocationConnection(previousToEndLocation, endLocation)
924  {
925  Biome = endLocation.Biome,
926  Difficulty = 100.0f
927  };
928  endConnection.LevelData = new LevelData(endConnection);
929  Connections.Add(endConnection);
930  previousToEndLocation.Connections.Add(endConnection);
931  endLocation.Connections.Add(endConnection);
932 
933  AssignEndLocationLevelData();
934  }
935 
936  private void AssignEndLocationLevelData()
937  {
938  for (int i = 0; i < endLocations.Count; i++)
939  {
940  endLocations[i].LevelData.ReassignGenerationParams(Seed);
941  var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i);
942  if (outpostParams != null)
943  {
944  endLocations[i].LevelData.ForceOutpostGenerationParams = outpostParams;
945  }
946  }
947  }
948 
949  private void ExpandBiomes(List<LocationConnection> seeds)
950  {
951  List<LocationConnection> nextSeeds = new List<LocationConnection>();
952  foreach (LocationConnection connection in seeds)
953  {
954  foreach (Location location in connection.Locations)
955  {
956  foreach (LocationConnection otherConnection in location.Connections)
957  {
958  if (otherConnection == connection) continue;
959  if (otherConnection.Biome != null) continue; //already assigned
960 
961  otherConnection.Biome = connection.Biome;
962  nextSeeds.Add(otherConnection);
963  }
964  }
965  }
966 
967  if (nextSeeds.Count > 0)
968  {
969  ExpandBiomes(nextSeeds);
970  }
971  }
972 
973 
974  #endregion Generation
975 
976  public void MoveToNextLocation()
977  {
978  if (SelectedLocation == null && Level.Loaded?.EndLocation != null)
979  {
980  //force the location at the end of the level to be selected, even if it's been deselect on the map
981  //(e.g. due to returning to an empty location the beginning of the level during the round)
983  }
984  if (SelectedConnection == null)
985  {
986  if (!endLocations.Contains(CurrentLocation))
987  {
988  DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace());
989  return;
990  }
991  }
992  if (SelectedLocation == null)
993  {
994  if (endLocations.Contains(CurrentLocation))
995  {
996  int currentEndLocationIndex = endLocations.IndexOf(CurrentLocation);
997  if (currentEndLocationIndex < endLocations.Count - 1)
998  {
999  //more end locations to go, progress to the next one
1000  SelectedLocation = endLocations[currentEndLocationIndex + 1];
1001  }
1002  else
1003  {
1004  //at the last end location, end of campaign
1006  }
1007  }
1008  else
1009  {
1010  DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace());
1011  return;
1012  }
1013  }
1014 
1015  Location prevLocation = CurrentLocation;
1016  if (SelectedConnection != null)
1017  {
1018  SelectedConnection.Passed = true;
1019  }
1020 
1024  SelectedLocation = null;
1025 
1027  OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation));
1028 
1029  if (GameMain.GameSession is { Campaign.CampaignMetadata: { } metadata })
1030  {
1031  metadata.SetValue("campaign.location.id".ToIdentifier(), CurrentLocationIndex);
1032  metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.NameIdentifier.Value);
1033  metadata.SetValue("campaign.location.biome".ToIdentifier(), CurrentLocation.Biome?.Identifier ?? "null".ToIdentifier());
1034  metadata.SetValue("campaign.location.type".ToIdentifier(), CurrentLocation.Type?.Identifier ?? "null".ToIdentifier());
1035  }
1036  }
1037 
1038  public void SetLocation(int index)
1039  {
1040  if (index == -1)
1041  {
1042  CurrentLocation = null;
1043  return;
1044  }
1045 
1046  if (index < 0 || index >= Locations.Count)
1047  {
1048  DebugConsole.ThrowError("Location index out of bounds");
1049  return;
1050  }
1051 
1052  Location prevLocation = CurrentLocation;
1053  CurrentLocation = Locations[index];
1054  Discover(CurrentLocation);
1055 
1056  CurrentLocation.CreateStores();
1057  if (prevLocation != CurrentLocation)
1058  {
1059  var connection = CurrentLocation.Connections.Find(c => c.Locations.Contains(prevLocation));
1060  if (connection != null)
1061  {
1062  connection.Passed = true;
1063  }
1064  OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation));
1065  }
1066  }
1067 
1068  public void SelectLocation(int index)
1069  {
1070  if (index == -1)
1071  {
1072  SelectedLocation = null;
1073  SelectedConnection = null;
1074 
1075  OnLocationSelected?.Invoke(null, null);
1076  return;
1077  }
1078 
1079  if (index < 0 || index >= Locations.Count)
1080  {
1081  DebugConsole.ThrowError("Location index out of bounds");
1082  return;
1083  }
1084 
1085  Location prevSelected = SelectedLocation;
1086  SelectedLocation = Locations[index];
1087  var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation();
1088  if (currentDisplayLocation == SelectedLocation)
1089  {
1090  SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1091  }
1092  else
1093  {
1094  SelectedConnection =
1095  Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ??
1096  Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1097  }
1098  if (SelectedConnection?.Locked ?? false)
1099  {
1100  string errorMsg =
1101  $"A locked connection was selected ({SelectedConnection.Locations[0].DisplayName} -> {SelectedConnection.Locations[1].DisplayName}." +
1102  $" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n"
1103  + Environment.StackTrace.CleanupStackTrace();
1104  GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1105  DebugConsole.ThrowError(errorMsg);
1106  }
1107  if (prevSelected != SelectedLocation)
1108  {
1109  OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
1110  }
1111  }
1112 
1113  public void SelectLocation(Location location)
1114  {
1115  if (!Locations.Contains(location))
1116  {
1117  string errorMsg = $"Failed to select a location. {location?.DisplayName ?? "null"} not found in the map.";
1118  DebugConsole.ThrowError(errorMsg);
1119  GameAnalyticsManager.AddErrorEventOnce("Map.SelectLocation:LocationNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1120  return;
1121  }
1122 
1123  Location prevSelected = SelectedLocation;
1124  SelectedLocation = location;
1125  SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1126  if (SelectedConnection?.Locked ?? false)
1127  {
1128  DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
1129  }
1130  if (prevSelected != SelectedLocation)
1131  {
1132  OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
1133  }
1134  }
1135 
1136  public void SelectMission(IEnumerable<int> missionIndices)
1137  {
1138  if (CurrentLocation == null)
1139  {
1140  string errorMsg = "Failed to select a mission (current location not set).";
1141  DebugConsole.ThrowError(errorMsg);
1142  GameAnalyticsManager.AddErrorEventOnce("Map.SelectMission:CurrentLocationNotSet", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1143  return;
1144  }
1145 
1146  if (!missionIndices.SequenceEqual(GetSelectedMissionIndices()))
1147  {
1148  CurrentLocation.SetSelectedMissionIndices(missionIndices);
1149  foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList())
1150  {
1151  if (selectedMission.Locations[0] != CurrentLocation ||
1152  selectedMission.Locations[1] != CurrentLocation)
1153  {
1154  if (SelectedConnection == null) { return; }
1155  //the destination must be the same as the destination of the mission
1156  if (selectedMission.Locations[1] != SelectedLocation)
1157  {
1158  CurrentLocation.DeselectMission(selectedMission);
1159  }
1160  }
1161  }
1162  OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions);
1163  }
1164  }
1165 
1166  public void SelectRandomLocation(bool preferUndiscovered)
1167  {
1168  List<Location> nextLocations = CurrentLocation.Connections.Where(c => !c.Locked).Select(c => c.OtherLocation(CurrentLocation)).ToList();
1169  List<Location> undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered);
1170 
1171  if (undiscoveredLocations.Count > 0 && preferUndiscovered)
1172  {
1173  SelectLocation(undiscoveredLocations[Rand.Int(undiscoveredLocations.Count, Rand.RandSync.Unsynced)]);
1174  }
1175  else
1176  {
1177  SelectLocation(nextLocations[Rand.Int(nextLocations.Count, Rand.RandSync.Unsynced)]);
1178  }
1179  }
1180 
1181  public void ProgressWorld(CampaignMode campaign, CampaignMode.TransitionType transitionType, float roundDuration)
1182  {
1183  //one step per 10 minutes of play time
1184  int steps = (int)Math.Floor(roundDuration / (60.0f * 10.0f));
1185  if (transitionType == CampaignMode.TransitionType.ProgressToNextLocation ||
1186  transitionType == CampaignMode.TransitionType.ProgressToNextEmptyLocation)
1187  {
1188  //at least one step when progressing to the next location, regardless of how long the round took
1189  steps = Math.Max(1, steps);
1190  }
1191  steps = Math.Min(steps, 5);
1192  for (int i = 0; i < steps; i++)
1193  {
1194  ProgressWorld(campaign);
1195  }
1196 
1197  // always update specials every step
1198  for (int i = 0; i < Math.Max(1, steps); i++)
1199  {
1200  foreach (Location location in Locations)
1201  {
1202  if (!location.Discovered) { continue; }
1203  location.UpdateSpecials();
1204  }
1205  }
1206 
1207  Radiation?.OnStep(steps);
1208  }
1209 
1210  private void ProgressWorld(CampaignMode campaign)
1211  {
1212  foreach (Location location in Locations)
1213  {
1214  location.LevelData.EventsExhausted = false;
1215  if (location.Discovered)
1216  {
1217  if (furthestDiscoveredLocation == null ||
1218  location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
1219  {
1220  furthestDiscoveredLocation = location;
1221  }
1222  }
1223  }
1224  foreach (LocationConnection connection in Connections)
1225  {
1226  connection.LevelData.EventsExhausted = false;
1227  }
1228 
1229  foreach (Location location in Locations)
1230  {
1231  if (location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
1232  {
1233  continue;
1234  }
1235 
1236  if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; }
1237 
1238  if (!ProgressLocationTypeChanges(campaign, location) && location.Discovered)
1239  {
1240  location.UpdateStores();
1241  }
1242  }
1243  }
1244 
1245  private bool ProgressLocationTypeChanges(CampaignMode campaign, Location location)
1246  {
1247  location.TimeSinceLastTypeChange++;
1248  location.LocationTypeChangeCooldown--;
1249 
1250  if (location.PendingLocationTypeChange != null)
1251  {
1252  if (location.PendingLocationTypeChange.Value.typeChange.DetermineProbability(location) <= 0.0f)
1253  {
1254  //remove pending type change if it's no longer allowed
1255  location.PendingLocationTypeChange = null;
1256  }
1257  else
1258  {
1259  location.PendingLocationTypeChange =
1260  (location.PendingLocationTypeChange.Value.typeChange,
1261  location.PendingLocationTypeChange.Value.delay - 1,
1262  location.PendingLocationTypeChange.Value.parentMission);
1263  if (location.PendingLocationTypeChange.Value.delay <= 0)
1264  {
1265  return ChangeLocationType(campaign, location, location.PendingLocationTypeChange.Value.typeChange);
1266  }
1267  }
1268  }
1269 
1270  //find which types of locations this one can change to
1271  Dictionary<LocationTypeChange, float> allowedTypeChanges = new Dictionary<LocationTypeChange, float>();
1272  foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
1273  {
1274  float probability = typeChange.DetermineProbability(location);
1275  if (probability <= 0.0f) { continue; }
1276  allowedTypeChanges.Add(typeChange, probability);
1277  }
1278 
1279  //select a random type change
1280  if (Rand.Range(0.0f, 1.0f) < allowedTypeChanges.Sum(change => change.Value))
1281  {
1282  var selectedTypeChange =
1283  ToolBox.SelectWeightedRandom(
1284  allowedTypeChanges.Keys.ToList(),
1285  allowedTypeChanges.Values.ToList(),
1286  Rand.RandSync.Unsynced);
1287  if (selectedTypeChange != null)
1288  {
1289  if (selectedTypeChange.RequiredDurationRange.X > 0)
1290  {
1291  location.PendingLocationTypeChange =
1292  (selectedTypeChange,
1293  Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
1294  null);
1295  }
1296  else
1297  {
1298  return ChangeLocationType(campaign, location, selectedTypeChange);
1299  }
1300  return false;
1301  }
1302  }
1303 
1304  foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
1305  {
1306  foreach (var requirement in typeChange.Requirements)
1307  {
1308  if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease))
1309  {
1310  if (!location.ProximityTimer.ContainsKey(requirement)) { location.ProximityTimer[requirement] = 0; }
1311  location.ProximityTimer[requirement] += 1;
1312  }
1313  else
1314  {
1315  location.ProximityTimer.Remove(requirement);
1316  }
1317  }
1318  }
1319 
1320  return false;
1321  }
1322 
1323  private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change)
1324  {
1325  LocalizedString prevName = location.DisplayName;
1326 
1327  if (!LocationType.Prefabs.TryGet(change.ChangeToType, out var newType))
1328  {
1329  DebugConsole.ThrowError($"Failed to change the type of the location \"{location.DisplayName}\". Location type \"{change.ChangeToType}\" not found.");
1330  return false;
1331  }
1332 
1333  if (location.LocationTypeChangesBlocked) { return false; }
1334 
1335  if (newType.OutpostTeam != location.Type.OutpostTeam ||
1336  newType.HasOutpost != location.Type.HasOutpost)
1337  {
1338  location.ClearMissions();
1339  }
1340  location.ChangeType(campaign, newType);
1341  ChangeLocationTypeProjSpecific(location, prevName, change);
1342  foreach (var requirement in change.Requirements)
1343  {
1344  location.ProximityTimer.Remove(requirement);
1345  }
1346  location.TimeSinceLastTypeChange = 0;
1347  location.LocationTypeChangeCooldown = change.CooldownAfterChange;
1348  location.PendingLocationTypeChange = null;
1349  return true;
1350  }
1351 
1352  public static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func<Location, bool> criteria, Func<LocationConnection, bool> connectionCriteria = null)
1353  {
1354  return GetDistanceToClosestLocationOrConnection(startLocation, maxDistance, criteria, connectionCriteria) <= maxDistance;
1355  }
1356 
1361  public static int GetDistanceToClosestLocationOrConnection(Location startLocation, int maxDistance, Func<Location, bool> criteria, Func<LocationConnection, bool> connectionCriteria = null)
1362  {
1363  int distance = 0;
1364  var locationsToTest = new List<Location>() { startLocation };
1365  var nextBatchToTest = new HashSet<Location>();
1366  var checkedLocations = new HashSet<Location>();
1367  while (locationsToTest.Any())
1368  {
1369  foreach (var location in locationsToTest)
1370  {
1371  checkedLocations.Add(location);
1372  if (criteria(location)) { return distance; }
1373  foreach (var connection in location.Connections)
1374  {
1375  if (connectionCriteria != null && connectionCriteria(connection))
1376  {
1377  return distance;
1378  }
1379  var otherLocation = connection.OtherLocation(location);
1380  if (!checkedLocations.Contains(otherLocation))
1381  {
1382  nextBatchToTest.Add(otherLocation);
1383  }
1384  }
1385  if (distance > maxDistance) { return int.MaxValue; }
1386  }
1387  distance++;
1388  locationsToTest.Clear();
1389  locationsToTest.AddRange(nextBatchToTest);
1390  nextBatchToTest.Clear();
1391  }
1392  return int.MaxValue;
1393  }
1394 
1395 
1396  partial void ChangeLocationTypeProjSpecific(Location location, LocalizedString prevName, LocationTypeChange change);
1397 
1398  partial void ClearAnimQueue();
1399 
1400  public void Discover(Location location, bool checkTalents = true)
1401  {
1402  if (location is null) { return; }
1403  if (locationsDiscovered.Contains(location)) { return; }
1404  locationsDiscovered.Add(location);
1405  if (checkTalents)
1406  {
1407  GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new Location.AbilityLocation(location)));
1408  }
1409  }
1410 
1411  public void Visit(Location location)
1412  {
1413  if (location is null) { return; }
1414  if (locationsVisited.Contains(location)) { return; }
1415  locationsVisited.Add(location);
1416  RemoveFogOfWarProjSpecific(location);
1417  }
1418 
1419  public void ClearLocationHistory()
1420  {
1421  locationsDiscovered.Clear();
1422  locationsVisited.Clear();
1423  }
1424 
1425  public int? GetDiscoveryIndex(Location location)
1426  {
1427  if (!trackedLocationDiscoveryAndVisitOrder) { return null; }
1428  if (location is null) { return -1; }
1429  return locationsDiscovered.IndexOf(location);
1430  }
1431 
1432  public int? GetVisitIndex(Location location, bool includeLocationsWithoutOutpost = false)
1433  {
1434  if (!trackedLocationDiscoveryAndVisitOrder) { return null; }
1435  if (location is null) { return -1; }
1436  int index = locationsVisited.IndexOf(location);
1437  if (includeLocationsWithoutOutpost) { return index; }
1438  int noOutpostLocations = 0;
1439  for (int i = 0; i < index; i++)
1440  {
1441  if (locationsVisited[i] is not Location l) { continue; }
1442  if (l.HasOutpost()) { continue; }
1443  noOutpostLocations++;
1444  }
1445  return index - noOutpostLocations;
1446  }
1447 
1448  public bool IsDiscovered(Location location)
1449  {
1450  if (location is null) { return false; }
1451  return locationsDiscovered.Contains(location);
1452  }
1453 
1454  public bool IsVisited(Location location)
1455  {
1456  if (location is null) { return false; }
1457  return locationsVisited.Contains(location);
1458  }
1459 
1460  partial void RemoveFogOfWarProjSpecific(Location location);
1461 
1465  public static Map Load(CampaignMode campaign, XElement element)
1466  {
1467  Map map = new Map(campaign, element);
1468  map.LoadState(campaign, element, false);
1469 #if CLIENT
1471 #endif
1472  return map;
1473  }
1474 
1478  public void LoadState(CampaignMode campaign, XElement element, bool showNotifications)
1479  {
1480  ClearAnimQueue();
1481  SetLocation(element.GetAttributeInt("currentlocation", 0));
1482 
1483  if (!Version.TryParse(element.GetAttributeString("version", ""), out Version version))
1484  {
1485  DebugConsole.ThrowError("Incompatible map save file, loading the game failed.");
1486  return;
1487  }
1488 
1489  ClearLocationHistory();
1490  foreach (var subElement in element.Elements())
1491  {
1492  switch (subElement.Name.ToString().ToLowerInvariant())
1493  {
1494  case "location":
1495  int locationIndex = subElement.GetAttributeInt("i", -1);
1496  if (locationIndex < 0 || locationIndex >= Locations.Count)
1497  {
1498  DebugConsole.AddWarning($"Error while loading the campaign map: location index out of bounds ({locationIndex})");
1499  continue;
1500  }
1501  Location location = Locations[locationIndex];
1502  location.ProximityTimer.Clear();
1503  for (int i = 0; i < location.Type.CanChangeTo.Count; i++)
1504  {
1505  for (int j = 0; j < location.Type.CanChangeTo[i].Requirements.Count; j++)
1506  {
1507  location.ProximityTimer.Add(location.Type.CanChangeTo[i].Requirements[j], subElement.GetAttributeInt("changetimer" + i + "-" + j, 0));
1508  }
1509  }
1510  location.LoadLocationTypeChange(subElement);
1511 
1512  // Backwards compatibility: if the discovery status is defined in the location element,
1513  // the game was saved using when the discovery order still wasn't being tracked
1514  if (subElement.GetAttributeBool("discovered", false))
1515  {
1516  Discover(location);
1517  Visit(location);
1518  trackedLocationDiscoveryAndVisitOrder = false;
1519  }
1520 
1521  Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
1522  LocalizedString prevLocationName = location.DisplayName;
1523  LocationType prevLocationType = location.Type;
1524  LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
1525  location.ChangeType(campaign, newLocationType);
1526 
1527  var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty);
1528  location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
1529 
1530  if (showNotifications && prevLocationType != location.Type)
1531  {
1532  var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier);
1533  if (change != null)
1534  {
1535  ChangeLocationTypeProjSpecific(location, prevLocationName, change);
1536  location.TimeSinceLastTypeChange = 0;
1537  }
1538  }
1539 
1540  var secondaryFactionIdentifier = subElement.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
1541  location.SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
1542 
1543  location.LoadStores(subElement);
1544  location.LoadMissions(subElement);
1545 
1546  break;
1547  case "connection":
1548  //the index wasn't saved previously, skip if that's the case
1549  if (subElement.Attribute("i") == null) { continue; }
1550 
1551  int connectionIndex = subElement.GetAttributeInt("i", -1);
1552  if (connectionIndex < 0 || connectionIndex >= Connections.Count)
1553  {
1554  DebugConsole.AddWarning($"Error while loading the campaign map: connection index out of bounds ({connectionIndex})");
1555  continue;
1556  }
1557  Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false);
1558  Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false);
1559  break;
1560  case "radiation":
1561  Radiation = new Radiation(this, generationParams.RadiationParams, subElement);
1562  break;
1563  case "discovered":
1564  bool trackedVisitedEmptyLocations = subElement.GetAttributeBool("trackedvisitedemptylocations", false);
1565  foreach (var childElement in subElement.GetChildElements("location"))
1566  {
1567  if (GetLocation(childElement) is Location l)
1568  {
1569  Discover(l);
1570  if (!trackedVisitedEmptyLocations)
1571  {
1572  if (!l.HasOutpost())
1573  {
1574  Visit(l);
1575  }
1576  trackedLocationDiscoveryAndVisitOrder = false;
1577  }
1578  }
1579  }
1580  break;
1581  case "visited":
1582  foreach (var childElement in subElement.GetChildElements("location"))
1583  {
1584  if (GetLocation(childElement) is Location l)
1585  {
1586  Visit(l);
1587  }
1588  }
1589  break;
1590  }
1591 
1592  Location GetLocation(XElement element)
1593  {
1594  int index = element.GetAttributeInt("i", -1);
1595  if (index < 0) { return null; }
1596  return Locations[index];
1597  }
1598 
1599  }
1600 
1601  void Discover(Location location)
1602  {
1603  this.Discover(location, checkTalents: false);
1604  if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
1605  {
1606  furthestDiscoveredLocation = location;
1607  }
1608  }
1609 
1610  foreach (Location location in Locations)
1611  {
1612  location?.InstantiateLoadedMissions(this);
1613  }
1614 
1615  //backwards compatibility:
1616  //if the save is from a version prior to the addition of faction-specific outposts, assign factions
1617  if (version < new Version(1, 0) && Locations.None(l => l.Faction != null || l.SecondaryFaction != null))
1618  {
1619  Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
1620  foreach (Location location in Locations)
1621  {
1622  if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
1623  {
1624  location.Faction = campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
1625  if (location != StartLocation)
1626  {
1627  location.SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
1628  }
1629  }
1630  }
1631  }
1632 
1633  int currentLocationConnection = element.GetAttributeInt("currentlocationconnection", -1);
1634  if (currentLocationConnection >= 0)
1635  {
1636  Connections[currentLocationConnection].Locked = false;
1637  SelectLocation(Connections[currentLocationConnection].OtherLocation(CurrentLocation));
1638  }
1639  else
1640  {
1641  //this should not be possible, you can't enter non-outpost locations (= natural formations)
1642  if (CurrentLocation != null && !CurrentLocation.Type.HasOutpost && SelectedConnection == null)
1643  {
1644  DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.DisplayName}). Loading the first adjacent connection...");
1645  SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation));
1646  }
1647  }
1648 
1649  var previousToEndLocation = GetPreviousToEndLocation();
1650  if (previousToEndLocation != null)
1651  {
1652  ForceLocationTypeToNone(campaign, previousToEndLocation);
1653  }
1654  }
1655 
1656  public void Save(XElement element)
1657  {
1658  XElement mapElement = new XElement("map");
1659 
1660  mapElement.Add(new XAttribute("version", GameMain.Version.ToString()));
1661  mapElement.Add(new XAttribute("currentlocation", CurrentLocationIndex));
1662  if (GameMain.GameSession.GameMode is CampaignMode campaign)
1663  {
1664  if (campaign.NextLevel != null && campaign.NextLevel.Type == LevelData.LevelType.LocationConnection)
1665  {
1666  mapElement.Add(new XAttribute("currentlocationconnection", Connections.IndexOf(CurrentLocation.Connections.Find(c => c.LevelData == campaign.NextLevel))));
1667  }
1668  else if (Level.Loaded != null && Level.Loaded.Type == LevelData.LevelType.LocationConnection && !CurrentLocation.Type.HasOutpost)
1669  {
1670  mapElement.Add(new XAttribute("currentlocationconnection", Connections.IndexOf(Connections.Find(c => c.LevelData == Level.Loaded.LevelData))));
1671  }
1672  }
1673  mapElement.Add(new XAttribute("width", Width));
1674  mapElement.Add(new XAttribute("height", Height));
1675  mapElement.Add(new XAttribute("selectedlocation", SelectedLocationIndex));
1676  mapElement.Add(new XAttribute("startlocation", Locations.IndexOf(StartLocation)));
1677  mapElement.Add(new XAttribute("endlocations", string.Join(',', EndLocations.Select(e => Locations.IndexOf(e)))));
1678  mapElement.Add(new XAttribute("seed", Seed));
1679 
1680  for (int i = 0; i < Locations.Count; i++)
1681  {
1682  var location = Locations[i];
1683  var locationElement = location.Save(this, mapElement);
1684  locationElement.Add(new XAttribute("i", i));
1685  }
1686 
1687  for (int i = 0; i < Connections.Count; i++)
1688  {
1689  var connection = Connections[i];
1690 
1691  var connectionElement = new XElement("connection",
1692  new XAttribute("passed", connection.Passed),
1693  new XAttribute("locked", connection.Locked),
1694  new XAttribute("difficulty", connection.Difficulty),
1695  new XAttribute("biome", connection.Biome.Identifier),
1696  new XAttribute("i", i),
1697  new XAttribute("locations", Locations.IndexOf(connection.Locations[0]) + "," + Locations.IndexOf(connection.Locations[1])));
1698  connection.LevelData.Save(connectionElement);
1699  mapElement.Add(connectionElement);
1700  }
1701 
1702  if (Radiation != null)
1703  {
1704  mapElement.Add(Radiation.Save());
1705  }
1706 
1707  if (locationsDiscovered.Any())
1708  {
1709  var discoveryElement = new XElement("discovered",
1710  new XAttribute("trackedvisitedemptylocations", true));
1711  foreach (Location location in locationsDiscovered)
1712  {
1713  int index = Locations.IndexOf(location);
1714  var locationElement = new XElement("location", new XAttribute("i", index));
1715  discoveryElement.Add(locationElement);
1716  }
1717  mapElement.Add(discoveryElement);
1718  }
1719 
1720  if (locationsVisited.Any())
1721  {
1722  var visitElement = new XElement("visited");
1723  foreach (Location location in locationsVisited)
1724  {
1725  int index = Locations.IndexOf(location);
1726  var locationElement = new XElement("location", new XAttribute("i", index));
1727  visitElement.Add(locationElement);
1728  }
1729  mapElement.Add(visitElement);
1730  }
1731 
1732  element.Add(mapElement);
1733  }
1734 
1735  public void Remove()
1736  {
1737  foreach (Location location in Locations)
1738  {
1739  location.Remove();
1740  }
1741  RemoveProjSpecific();
1742  }
1743 
1744  partial void RemoveProjSpecific();
1745  }
1746 }
static readonly PrefabCollection< Biome > Prefabs
Definition: Biome.cs:10
Biome(ContentXElement element, LevelGenerationParametersFile file)
Definition: Biome.cs:35
readonly int EndBiomeLocationCount
Definition: Biome.cs:17
Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their ControlledOutpostPercentage
Faction GetRandomSecondaryFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their SecondaryControlledOutpostPercentage
Location GetCurrentDisplayLocation()
The location that's displayed as the "current one" in the map screen. Normally the current outpost or...
Faction(CampaignMetadata? metadata, FactionPrefab prefab)
Definition: Factions.cs:20
static GameSession?? GameSession
Definition: GameMain.cs:88
static readonly Version Version
Definition: GameMain.cs:46
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
bool EventsExhausted
'Exhaustible' sets won't appear in the same level until after one world step (~10 min,...
Definition: LevelData.cs:79
void ChangeType(CampaignMode campaign, LocationType newType, bool createStores=true)
Definition: Location.cs:744
void LoadStores(XElement locationElement)
Definition: Location.cs:1196
int TimeSinceLastTypeChange
Definition: Location.cs:510
readonly List< LocationConnection > Connections
Definition: Location.cs:56
LocationType Type
Definition: Location.cs:91
XElement Save(Map map, XElement parentElement)
Definition: Location.cs:1531
void LoadLocationTypeChange(XElement locationElement)
Definition: Location.cs:687
Identifier NameIdentifier
Definition: Location.cs:60
Vector2 MapPosition
Definition: Location.cs:89
List< int > GetSelectedMissionIndices()
Definition: Location.cs:467
readonly Dictionary< LocationTypeChange.Requirement, int > ProximityTimer
Definition: Location.cs:76
void InstantiateLoadedMissions(Map map)
Definition: Location.cs:1011
LocalizedString DisplayName
Definition: Location.cs:58
LevelData LevelData
Definition: Location.cs:95
Faction SecondaryFaction
Definition: Location.cs:101
void CreateStores(bool force=false)
If true, the stores will be recreated if they already exists.
Definition: Location.cs:1309
void LoadMissions(XElement locationElement)
Definition: Location.cs:723
readonly CharacterTeamType OutpostTeam
Definition: LocationType.cs:34
readonly List< LocationTypeChange > CanChangeTo
Definition: LocationType.cs:36
static readonly PrefabCollection< LocationType > Prefabs
Definition: LocationType.cs:15
static MapGenerationParams Instance
void ProgressWorld(CampaignMode campaign, CampaignMode.TransitionType transitionType, float roundDuration)
void LoadState(CampaignMode campaign, XElement element, bool showNotifications)
Load the state of an existing map from xml (current state of locations, where the crew is now,...
readonly NamedEvent< LocationChangeInfo > OnLocationChanged
From -> To
Action< Location, LocationConnection > OnLocationSelected
IReadOnlyList< Location > EndLocations
IEnumerable< int > GetSelectedMissionIndices()
static int GetDistanceToClosestLocationOrConnection(Location startLocation, int maxDistance, Func< Location, bool > criteria, Func< LocationConnection, bool > connectionCriteria=null)
Get the shortest distance from the start location to another location that satisfies the specified cr...
static Map Load(CampaignMode campaign, XElement element)
Load a previously saved map from an xml element
void SelectMission(IEnumerable< int > missionIndices)
int? GetVisitIndex(Location location, bool includeLocationsWithoutOutpost=false)
static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func< Location, bool > criteria, Func< LocationConnection, bool > connectionCriteria=null)
Action< LocationConnection, IEnumerable< Mission > > OnMissionsSelected
void Discover(Location location, bool checkTalents=true)
Map(CampaignMode campaign, string seed)
Generate a new campaign map from the seed
List< LocationConnection > Connections
void SelectRandomLocation(bool preferUndiscovered)
readonly Identifier Identifier
Definition: Prefab.cs:34
Description of Voronoi.
Definition: Voronoi.cs:76
List< GraphEdge > MakeVoronoiGraph(List< Vector2 > sites, int width, int height)
Definition: Voronoi.cs:973
WorldHostilityOption
Definition: Enums.cs:707
AbilityEffectType
Definition: Enums.cs:125
CharacterType
Definition: Enums.cs:685
LocationChangeInfo(Location prevLocation, Location newLocation)