Server LuaCsForBarotrauma
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 (forceLocationType == null)
453  {
454  foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
455  {
456  if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount)
457  {
458  forceLocationType = locationType;
459  break;
460  }
461  }
462  }
463 
464  newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient),
465  requireOutpost: false, forceLocationType: forceLocationType, existingLocations: Locations);
466  locationsPerZone[zone].Add(newLocations[i]);
467  Locations.Add(newLocations[i]);
468  }
469 
470  var newConnection = new LocationConnection(newLocations[0], newLocations[1]);
471  Connections.Add(newConnection);
472  }
473 
474  //remove connections that are too short
475  float minConnectionDistanceSqr = generationParams.MinConnectionDistance * generationParams.MinConnectionDistance;
476  for (int i = Connections.Count - 1; i >= 0; i--)
477  {
478  LocationConnection connection = Connections[i];
479 
480  if (Vector2.DistanceSquared(connection.Locations[0].MapPosition, connection.Locations[1].MapPosition) > minConnectionDistanceSqr)
481  {
482  continue;
483  }
484 
485  //locations.Remove(connection.Locations[0]);
486  Connections.Remove(connection);
487 
488  foreach (LocationConnection connection2 in Connections)
489  {
490  if (connection2.Locations[0] == connection.Locations[0]) { connection2.Locations[0] = connection.Locations[1]; }
491  if (connection2.Locations[1] == connection.Locations[0]) { connection2.Locations[1] = connection.Locations[1]; }
492  }
493  }
494 
495  foreach (LocationConnection connection in Connections)
496  {
497  connection.Locations[0].Connections.Add(connection);
498  connection.Locations[1].Connections.Add(connection);
499  }
500 
501  //remove locations that are too close to each other
502  float minLocationDistanceSqr = generationParams.MinLocationDistance * generationParams.MinLocationDistance;
503  for (int i = Locations.Count - 1; i >= 0; i--)
504  {
505  for (int j = Locations.Count - 1; j > i; j--)
506  {
507  float dist = Vector2.DistanceSquared(Locations[i].MapPosition, Locations[j].MapPosition);
508  if (dist > minLocationDistanceSqr)
509  {
510  continue;
511  }
512  //move connections from Locations[j] to Locations[i]
513  foreach (LocationConnection connection in Locations[j].Connections)
514  {
515  if (connection.Locations[0] == Locations[j])
516  {
517  connection.Locations[0] = Locations[i];
518  }
519  else
520  {
521  connection.Locations[1] = Locations[i];
522  }
523 
524  if (connection.Locations[0] != connection.Locations[1])
525  {
526  Locations[i].Connections.Add(connection);
527  }
528  else
529  {
530  Connections.Remove(connection);
531  }
532  }
533  Locations[i].Connections.RemoveAll(c => c.OtherLocation(Locations[i]) == Locations[j]);
534  Locations.RemoveAt(j);
535  }
536  }
537 
538  //make sure the connections are in the same order on the locations and the Connections list
539  //otherwise their order will change when loading the game (as they're added to the locations in the same order they're loaded)
540  foreach (var location in Locations)
541  {
542  location.Connections.Sort((c1, c2) => Connections.IndexOf(c1).CompareTo(Connections.IndexOf(c2)));
543  }
544 
545  for (int i = Connections.Count - 1; i >= 0; i--)
546  {
547  i = Math.Min(i, Connections.Count - 1);
548  LocationConnection connection = Connections[i];
549  for (int n = Math.Min(i - 1, Connections.Count - 1); n >= 0; n--)
550  {
551  if (connection.Locations.Contains(Connections[n].Locations[0])
552  && connection.Locations.Contains(Connections[n].Locations[1]))
553  {
554  Connections.RemoveAt(n);
555  }
556  }
557  }
558 
559  List<LocationConnection>[] connectionsBetweenZones = new List<LocationConnection>[generationParams.DifficultyZones];
560  for (int i = 0; i < generationParams.DifficultyZones; i++)
561  {
562  connectionsBetweenZones[i] = new List<LocationConnection>();
563  }
564  var shuffledConnections = Connections.ToList();
565  shuffledConnections.Shuffle(Rand.RandSync.ServerAndClient);
566  foreach (var connection in shuffledConnections)
567  {
568  int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X);
569  int zone2 = GetZoneIndex(connection.Locations[1].MapPosition.X);
570  if (zone1 == zone2) { continue; }
571  if (zone1 > zone2)
572  {
573  (zone1, zone2) = (zone2, zone1);
574  }
575 
576  if (generationParams.GateCount[zone1] == 0) { continue; }
577 
578  if (!connectionsBetweenZones[zone1].Any())
579  {
580  connectionsBetweenZones[zone1].Add(connection);
581  }
582  else if (generationParams.GateCount[zone1] == 1)
583  {
584  //if there's only one connection, place it at the center of the map
585  if (Math.Abs(connection.CenterPos.Y - Height / 2) < Math.Abs(connectionsBetweenZones[zone1].First().CenterPos.Y - Height / 2))
586  {
587  connectionsBetweenZones[zone1].Clear();
588  connectionsBetweenZones[zone1].Add(connection);
589  }
590  }
591  else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1] &&
592  connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1])))
593  {
594  connectionsBetweenZones[zone1].Add(connection);
595  }
596  if (connectionsBetweenZones[zone1].None())
597  {
598  DebugConsole.ThrowError($"Potential error during map generation: no connections between zones {zone1} and {zone2} found. Traversing through to the end of the map may be impossible.");
599  }
600  }
601 
602  var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList();
603  for (int i = Connections.Count - 1; i >= 0; i--)
604  {
605  int zone1 = GetZoneIndex(Connections[i].Locations[0].MapPosition.X);
606  int zone2 = GetZoneIndex(Connections[i].Locations[1].MapPosition.X);
607  if (zone1 == zone2) { continue; }
608  if (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) { continue; }
609 
610  int leftZone = Math.Min(zone1, zone2);
611  if (generationParams.GateCount[leftZone] == 0) { continue; }
612  if (!connectionsBetweenZones[leftZone].Contains(Connections[i]))
613  {
614  Connections.RemoveAt(i);
615  }
616  else
617  {
618  var leftMostLocation =
619  Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ?
620  Connections[i].Locations[0] :
621  Connections[i].Locations[1];
622  if (!AllowAsBiomeGate(leftMostLocation.Type))
623  {
624  leftMostLocation.ChangeType(
625  campaign,
626  LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => AllowAsBiomeGate(lt)),
627  createStores: false);
628  }
629  static bool AllowAsBiomeGate(LocationType lt)
630  {
631  //checking for "abandoned" is not strictly necessary here because it's now configured to not be allowed as a biome gate
632  //but might be better to keep it for backwards compatibility (previously we relied only on that check)
633  return lt.HasOutpost && lt.Identifier != "abandoned" && lt.AllowAsBiomeGate;
634  }
635 
636  leftMostLocation.IsGateBetweenBiomes = true;
637  Connections[i].Locked = true;
638 
639  if (leftMostLocation.Type.HasOutpost && campaign != null && gateFactions.Any())
640  {
641  leftMostLocation.Faction = gateFactions[connectionsBetweenZones[leftZone].IndexOf(Connections[i]) % gateFactions.Count];
642  }
643  }
644  }
645 
646  foreach (Location location in Locations)
647  {
648  for (int i = location.Connections.Count - 1; i >= 0; i--)
649  {
650  if (!Connections.Contains(location.Connections[i]))
651  {
652  location.Connections.RemoveAt(i);
653  }
654  }
655  }
656 
657  //make sure the location at the right side of the gate between biomes isn't a dead-end
658  //those may sometimes generate if all the connections of the right-side location lead to the previous biome
659  //(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)
660  for (int i = 0; i < Connections.Count; i++)
661  {
662  var connection = Connections[i];
663  if (!connection.Locked) { continue; }
664  var rightMostLocation =
665  connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ?
666  connection.Locations[0] :
667  connection.Locations[1];
668 
669  //if all of the other connected locations are to the left (= if there's no path forwards from the outpost),
670  //create a new connection to the closest location to the right
671  if (rightMostLocation.Connections.All(c => c.OtherLocation(rightMostLocation).MapPosition.X < rightMostLocation.MapPosition.X))
672  {
673  Location closestLocation = null;
674  float closestDist = float.PositiveInfinity;
675  foreach (Location otherLocation in Locations)
676  {
677  if (otherLocation == rightMostLocation || otherLocation.MapPosition.X < rightMostLocation.MapPosition.X) { continue; }
678  float dist = Vector2.DistanceSquared(rightMostLocation.MapPosition, otherLocation.MapPosition);
679  if (dist < closestDist || closestLocation == null)
680  {
681  closestLocation = otherLocation;
682  closestDist = dist;
683  }
684  }
685 
686  var newConnection = new LocationConnection(rightMostLocation, closestLocation);
687  rightMostLocation.Connections.Add(newConnection);
688  closestLocation.Connections.Add(newConnection);
689  Connections.Add(newConnection);
690  GenerateLocationConnectionVisuals(newConnection);
691  }
692  }
693 
694  //remove orphans
695  Locations.RemoveAll(l => !Connections.Any(c => c.Locations.Contains(l)));
696 
697  AssignBiomes(new MTRandom(ToolBox.StringToInt(Seed)));
698 
699  foreach (LocationConnection connection in Connections)
700  {
701  if (connection.Locations.Any(l => l.IsGateBetweenBiomes))
702  {
703  connection.Difficulty = Math.Min(connection.Locations.Min(l => l.Biome.ActualMaxDifficulty), connection.Biome.AdjustedMaxDifficulty);
704  }
705  else
706  {
707  connection.Difficulty = CalculateDifficulty(connection.CenterPos.X, connection.Biome);
708  }
709  }
710 
711  //ensure there's an outpost (a valid starting location) at the very left side of the map
712  Location startLocation = Locations.MinBy(l => l.MapPosition.X);
713  if (LocationType.Prefabs.TryGet("outpost", out LocationType startLocationType))
714  {
715  startLocation.ChangeType(campaign, startLocationType, createStores: false);
716  }
717 
718  foreach (Location location in Locations)
719  {
720  location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
721  location.TryAssignFactionBasedOnLocationType(campaign);
722  if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
723  {
724  if (location.Type.Faction.IsEmpty)
725  {
726  //no faction defined in the location type, assign a random one
727  location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
728  }
729  if (location.Type.SecondaryFaction.IsEmpty)
730  {
731  //no secondary faction defined in the location type, assign a random one
732  location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
733  }
734  }
735  location.CreateStores(force: true);
736  }
737 
738  foreach (LocationConnection connection in Connections)
739  {
740  connection.LevelData = new LevelData(connection);
741  }
742 
743  CreateEndLocation(campaign);
744 
745  float CalculateDifficulty(float mapPosition, Biome biome)
746  {
747  float settingsFactor = campaign.Settings.LevelDifficultyMultiplier;
748  float minDifficulty = 0;
749  float maxDifficulty = 100;
750  float difficulty = mapPosition / Width * 100;
751  System.Diagnostics.Debug.Assert(biome != null);
752  if (biome != null)
753  {
754  minDifficulty = biome.MinDifficulty;
755  maxDifficulty = biome.AdjustedMaxDifficulty;
756  float diff = 1 - settingsFactor;
757  difficulty *= 1 - (1f / biome.AllowedZones.Max() * diff);
758  }
759  return MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty);
760  }
761  }
762 
763  partial void GenerateAllLocationConnectionVisuals();
764 
765  partial void GenerateLocationConnectionVisuals(LocationConnection connection);
766 
767  public int GetZoneIndex(float xPos)
768  {
769  float zoneWidth = Width / generationParams.DifficultyZones;
770  return MathHelper.Clamp((int)Math.Floor(xPos / zoneWidth) + 1, 1, generationParams.DifficultyZones);
771  }
772 
773  public Biome GetBiome(Vector2 mapPos)
774  {
775  return GetBiome(mapPos.X);
776  }
777 
778  public Biome GetBiome(float xPos)
779  {
780  float zoneWidth = Width / generationParams.DifficultyZones;
781  int zoneIndex = (int)Math.Floor(xPos / zoneWidth) + 1;
782  zoneIndex = Math.Clamp(zoneIndex, 1, generationParams.DifficultyZones - 1);
783  return Biome.Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
784  }
785 
786  private void AssignBiomes(Random rand)
787  {
788  var biomes = Biome.Prefabs;
789  float zoneWidth = Width / generationParams.DifficultyZones;
790 
791  List<Biome> allowedBiomes = new List<Biome>(10);
792  for (int i = 0; i < generationParams.DifficultyZones; i++)
793  {
794  allowedBiomes.Clear();
795  allowedBiomes.AddRange(biomes.Where(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i)));
796  float zoneX = zoneWidth * (generationParams.DifficultyZones - i);
797 
798  foreach (Location location in Locations)
799  {
800  if (location.MapPosition.X < zoneX)
801  {
802  location.Biome = allowedBiomes[rand.Next() % allowedBiomes.Count];
803  }
804  }
805  }
806  foreach (LocationConnection connection in Connections)
807  {
808  if (connection.Biome != null) { continue; }
809  connection.Biome = connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ? connection.Locations[0].Biome : connection.Locations[1].Biome;
810  }
811 
812  System.Diagnostics.Debug.Assert(Locations.All(l => l.Biome != null));
813  System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null));
814  }
815 
816  private Location GetPreviousToEndLocation()
817  {
818  Location previousToEndLocation = null;
819  foreach (Location location in Locations)
820  {
821  if (!location.Biome.IsEndBiome && (previousToEndLocation == null || location.MapPosition.X > previousToEndLocation.MapPosition.X))
822  {
823  previousToEndLocation = location;
824  }
825  }
826  return previousToEndLocation;
827  }
828 
829  private void ForceLocationTypeToNone(CampaignMode campaign, Location location)
830  {
831  if (LocationType.Prefabs.TryGet("none", out LocationType locationType))
832  {
833  location.ChangeType(campaign, locationType, createStores: false);
834  }
835  location.DisallowLocationTypeChanges = true;
836  }
837 
838  private void CreateEndLocation(CampaignMode campaign)
839  {
840  float zoneWidth = Width / generationParams.DifficultyZones;
841  Vector2 endPos = new Vector2(Width - zoneWidth * 0.7f, Height / 2);
842  float closestDist = float.MaxValue;
843  var endLocation = Locations.First();
844  foreach (Location location in Locations)
845  {
846  float dist = Vector2.DistanceSquared(endPos, location.MapPosition);
847  if (location.Biome.IsEndBiome && dist < closestDist)
848  {
849  endLocation = location;
850  closestDist = dist;
851  }
852  }
853 
854  var previousToEndLocation = GetPreviousToEndLocation();
855  if (endLocation == null || previousToEndLocation == null) { return; }
856 
857  endLocations = new List<Location>() { endLocation };
858  if (endLocation.Biome.EndBiomeLocationCount > 1)
859  {
860  FindConnectedEndLocations(endLocation);
861 
862  void FindConnectedEndLocations(Location currLocation)
863  {
864  if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; }
865  foreach (var connection in currLocation.Connections)
866  {
867  if (connection.Biome != endLocation.Biome) { continue; }
868  var otherLocation = connection.OtherLocation(currLocation);
869  if (otherLocation != null && !endLocations.Contains(otherLocation))
870  {
871  if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; }
872  endLocations.Add(otherLocation);
873  FindConnectedEndLocations(otherLocation);
874  }
875  }
876  }
877  }
878 
879  ForceLocationTypeToNone(campaign, previousToEndLocation);
880 
881  //remove all locations from the end biome except the end location
882  for (int i = Locations.Count - 1; i >= 0; i--)
883  {
884  if (Locations[i].Biome.IsEndBiome)
885  {
886  for (int j = Locations[i].Connections.Count - 1; j >= 0; j--)
887  {
888  if (j >= Locations[i].Connections.Count) { continue; }
889  var connection = Locations[i].Connections[j];
890  var otherLocation = connection.OtherLocation(Locations[i]);
891  Locations[i].Connections.RemoveAt(j);
892  otherLocation?.Connections.Remove(connection);
893  Connections.Remove(connection);
894  }
895  if (!endLocations.Contains(Locations[i]))
896  {
897  Locations.RemoveAt(i);
898  }
899  }
900  }
901 
902  //removed all connections from the second-to-last location, need to reconnect it
903  if (previousToEndLocation.Connections.None())
904  {
905  Location connectTo = Locations.First();
906  foreach (Location location in Locations)
907  {
908  if (!location.Biome.IsEndBiome && location != previousToEndLocation && location.MapPosition.X > connectTo.MapPosition.X)
909  {
910  connectTo = location;
911  }
912  }
913  var newConnection = new LocationConnection(previousToEndLocation, connectTo)
914  {
915  Biome = endLocation.Biome,
916  Difficulty = 100.0f
917  };
918  newConnection.LevelData = new LevelData(newConnection);
919  Connections.Add(newConnection);
920  previousToEndLocation.Connections.Add(newConnection);
921  connectTo.Connections.Add(newConnection);
922  }
923 
924  var endConnection = new LocationConnection(previousToEndLocation, endLocation)
925  {
926  Biome = endLocation.Biome,
927  Difficulty = 100.0f
928  };
929  endConnection.LevelData = new LevelData(endConnection);
930  Connections.Add(endConnection);
931  previousToEndLocation.Connections.Add(endConnection);
932  endLocation.Connections.Add(endConnection);
933 
934  AssignEndLocationLevelData();
935  }
936 
937  private void AssignEndLocationLevelData()
938  {
939  for (int i = 0; i < endLocations.Count; i++)
940  {
941  endLocations[i].LevelData.ReassignGenerationParams(Seed);
942  var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i);
943  if (outpostParams != null)
944  {
945  endLocations[i].LevelData.ForceOutpostGenerationParams = outpostParams;
946  }
947  }
948  }
949 
950  private void ExpandBiomes(List<LocationConnection> seeds)
951  {
952  List<LocationConnection> nextSeeds = new List<LocationConnection>();
953  foreach (LocationConnection connection in seeds)
954  {
955  foreach (Location location in connection.Locations)
956  {
957  foreach (LocationConnection otherConnection in location.Connections)
958  {
959  if (otherConnection == connection) continue;
960  if (otherConnection.Biome != null) continue; //already assigned
961 
962  otherConnection.Biome = connection.Biome;
963  nextSeeds.Add(otherConnection);
964  }
965  }
966  }
967 
968  if (nextSeeds.Count > 0)
969  {
970  ExpandBiomes(nextSeeds);
971  }
972  }
973 
974 
975  #endregion Generation
976 
977  public void MoveToNextLocation()
978  {
979  if (SelectedLocation == null && Level.Loaded?.EndLocation != null)
980  {
981  //force the location at the end of the level to be selected, even if it's been deselect on the map
982  //(e.g. due to returning to an empty location the beginning of the level during the round)
984  }
985  if (SelectedConnection == null)
986  {
987  if (!endLocations.Contains(CurrentLocation))
988  {
989  DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace());
990  return;
991  }
992  }
993  if (SelectedLocation == null)
994  {
995  if (endLocations.Contains(CurrentLocation))
996  {
997  int currentEndLocationIndex = endLocations.IndexOf(CurrentLocation);
998  if (currentEndLocationIndex < endLocations.Count - 1)
999  {
1000  //more end locations to go, progress to the next one
1001  SelectedLocation = endLocations[currentEndLocationIndex + 1];
1002  }
1003  else
1004  {
1005  //at the last end location, end of campaign
1007  }
1008  }
1009  else
1010  {
1011  DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace());
1012  return;
1013  }
1014  }
1015 
1016  Location prevLocation = CurrentLocation;
1017  if (SelectedConnection != null)
1018  {
1019  SelectedConnection.Passed = true;
1020  }
1021 
1025  SelectedLocation = null;
1026 
1028  OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation));
1029 
1030  if (GameMain.GameSession is { Campaign.CampaignMetadata: { } metadata })
1031  {
1032  metadata.SetValue("campaign.location.id".ToIdentifier(), CurrentLocationIndex);
1033  metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.NameIdentifier.Value);
1034  metadata.SetValue("campaign.location.biome".ToIdentifier(), CurrentLocation.Biome?.Identifier ?? "null".ToIdentifier());
1035  metadata.SetValue("campaign.location.type".ToIdentifier(), CurrentLocation.Type?.Identifier ?? "null".ToIdentifier());
1036  }
1037  }
1038 
1039  public void SetLocation(int index)
1040  {
1041  if (index == -1)
1042  {
1043  CurrentLocation = null;
1044  return;
1045  }
1046 
1047  if (index < 0 || index >= Locations.Count)
1048  {
1049  DebugConsole.ThrowError("Location index out of bounds");
1050  return;
1051  }
1052 
1053  Location prevLocation = CurrentLocation;
1054  CurrentLocation = Locations[index];
1055  Discover(CurrentLocation);
1056 
1057  CurrentLocation.CreateStores();
1058  if (prevLocation != CurrentLocation)
1059  {
1060  var connection = CurrentLocation.Connections.Find(c => c.Locations.Contains(prevLocation));
1061  if (connection != null)
1062  {
1063  connection.Passed = true;
1064  }
1065  OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation));
1066  }
1067  }
1068 
1069  public void SelectLocation(int index)
1070  {
1071  if (index == -1)
1072  {
1073  SelectedLocation = null;
1074  SelectedConnection = null;
1075 
1076  OnLocationSelected?.Invoke(null, null);
1077  return;
1078  }
1079 
1080  if (index < 0 || index >= Locations.Count)
1081  {
1082  DebugConsole.ThrowError("Location index out of bounds");
1083  return;
1084  }
1085 
1086  Location prevSelected = SelectedLocation;
1087  SelectedLocation = Locations[index];
1088  var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation();
1089  if (currentDisplayLocation == SelectedLocation)
1090  {
1091  SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1092  }
1093  else
1094  {
1095  SelectedConnection =
1096  Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ??
1097  Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1098  }
1099  if (SelectedConnection?.Locked ?? false)
1100  {
1101  string errorMsg =
1102  $"A locked connection was selected ({SelectedConnection.Locations[0].DisplayName} -> {SelectedConnection.Locations[1].DisplayName}." +
1103  $" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n"
1104  + Environment.StackTrace.CleanupStackTrace();
1105  GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1106  DebugConsole.ThrowError(errorMsg);
1107  }
1108  if (prevSelected != SelectedLocation)
1109  {
1110  OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
1111  }
1112  }
1113 
1114  public void SelectLocation(Location location)
1115  {
1116  if (!Locations.Contains(location))
1117  {
1118  string errorMsg = $"Failed to select a location. {location?.DisplayName ?? "null"} not found in the map.";
1119  DebugConsole.ThrowError(errorMsg);
1120  GameAnalyticsManager.AddErrorEventOnce("Map.SelectLocation:LocationNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1121  return;
1122  }
1123 
1124  Location prevSelected = SelectedLocation;
1125  SelectedLocation = location;
1126  SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1127  if (SelectedConnection?.Locked ?? false)
1128  {
1129  DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
1130  }
1131  if (prevSelected != SelectedLocation)
1132  {
1133  OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
1134  }
1135  }
1136 
1137  public void SelectMission(IEnumerable<int> missionIndices)
1138  {
1139  if (CurrentLocation == null)
1140  {
1141  string errorMsg = "Failed to select a mission (current location not set).";
1142  DebugConsole.ThrowError(errorMsg);
1143  GameAnalyticsManager.AddErrorEventOnce("Map.SelectMission:CurrentLocationNotSet", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1144  return;
1145  }
1146 
1147  if (!missionIndices.SequenceEqual(GetSelectedMissionIndices()))
1148  {
1149  CurrentLocation.SetSelectedMissionIndices(missionIndices);
1150  foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList())
1151  {
1152  if (selectedMission.Locations[0] != CurrentLocation ||
1153  selectedMission.Locations[1] != CurrentLocation)
1154  {
1155  if (SelectedConnection == null) { return; }
1156  //the destination must be the same as the destination of the mission
1157  if (selectedMission.Locations[1] != SelectedLocation)
1158  {
1159  CurrentLocation.DeselectMission(selectedMission);
1160  }
1161  }
1162  }
1163  OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions);
1164  }
1165  }
1166 
1167  public void SelectRandomLocation(bool preferUndiscovered)
1168  {
1169  List<Location> nextLocations = CurrentLocation.Connections.Where(c => !c.Locked).Select(c => c.OtherLocation(CurrentLocation)).ToList();
1170  List<Location> undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered);
1171 
1172  if (undiscoveredLocations.Count > 0 && preferUndiscovered)
1173  {
1174  SelectLocation(undiscoveredLocations[Rand.Int(undiscoveredLocations.Count, Rand.RandSync.Unsynced)]);
1175  }
1176  else
1177  {
1178  SelectLocation(nextLocations[Rand.Int(nextLocations.Count, Rand.RandSync.Unsynced)]);
1179  }
1180  }
1181 
1182  public void ProgressWorld(CampaignMode campaign, CampaignMode.TransitionType transitionType, float roundDuration)
1183  {
1184  //one step per 10 minutes of play time
1185  int steps = (int)Math.Floor(roundDuration / (60.0f * 10.0f));
1186  if (transitionType == CampaignMode.TransitionType.ProgressToNextLocation ||
1187  transitionType == CampaignMode.TransitionType.ProgressToNextEmptyLocation)
1188  {
1189  //at least one step when progressing to the next location, regardless of how long the round took
1190  steps = Math.Max(1, steps);
1191  }
1192  steps = Math.Min(steps, 5);
1193  for (int i = 0; i < steps; i++)
1194  {
1195  ProgressWorld(campaign);
1196  }
1197 
1198  // always update specials every step
1199  for (int i = 0; i < Math.Max(1, steps); i++)
1200  {
1201  foreach (Location location in Locations)
1202  {
1203  if (!location.Discovered) { continue; }
1204  location.UpdateSpecials();
1205  }
1206  }
1207 
1208  Radiation?.OnStep(steps);
1209  }
1210 
1211  private void ProgressWorld(CampaignMode campaign)
1212  {
1213  foreach (Location location in Locations)
1214  {
1215  location.LevelData.EventsExhausted = false;
1216  if (location.Discovered)
1217  {
1218  if (furthestDiscoveredLocation == null ||
1219  location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
1220  {
1221  furthestDiscoveredLocation = location;
1222  }
1223  }
1224  }
1225  foreach (LocationConnection connection in Connections)
1226  {
1227  connection.LevelData.EventsExhausted = false;
1228  }
1229 
1230  foreach (Location location in Locations)
1231  {
1232  if (location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
1233  {
1234  continue;
1235  }
1236 
1237  if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; }
1238 
1239  if (!ProgressLocationTypeChanges(campaign, location) && location.Discovered)
1240  {
1241  location.UpdateStores();
1242  }
1243  }
1244  }
1245 
1246  private bool ProgressLocationTypeChanges(CampaignMode campaign, Location location)
1247  {
1248  location.TimeSinceLastTypeChange++;
1249  location.LocationTypeChangeCooldown--;
1250 
1251  if (location.PendingLocationTypeChange != null)
1252  {
1253  if (location.PendingLocationTypeChange.Value.typeChange.DetermineProbability(location) <= 0.0f)
1254  {
1255  //remove pending type change if it's no longer allowed
1256  location.PendingLocationTypeChange = null;
1257  }
1258  else
1259  {
1260  location.PendingLocationTypeChange =
1261  (location.PendingLocationTypeChange.Value.typeChange,
1262  location.PendingLocationTypeChange.Value.delay - 1,
1263  location.PendingLocationTypeChange.Value.parentMission);
1264  if (location.PendingLocationTypeChange.Value.delay <= 0)
1265  {
1266  return ChangeLocationType(campaign, location, location.PendingLocationTypeChange.Value.typeChange);
1267  }
1268  }
1269  }
1270 
1271  //find which types of locations this one can change to
1272  Dictionary<LocationTypeChange, float> allowedTypeChanges = new Dictionary<LocationTypeChange, float>();
1273  foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
1274  {
1275  float probability = typeChange.DetermineProbability(location);
1276  if (probability <= 0.0f) { continue; }
1277  allowedTypeChanges.Add(typeChange, probability);
1278  }
1279 
1280  //select a random type change
1281  if (Rand.Range(0.0f, 1.0f) < allowedTypeChanges.Sum(change => change.Value))
1282  {
1283  var selectedTypeChange =
1284  ToolBox.SelectWeightedRandom(
1285  allowedTypeChanges.Keys.ToList(),
1286  allowedTypeChanges.Values.ToList(),
1287  Rand.RandSync.Unsynced);
1288  if (selectedTypeChange != null)
1289  {
1290  if (selectedTypeChange.RequiredDurationRange.X > 0)
1291  {
1292  location.PendingLocationTypeChange =
1293  (selectedTypeChange,
1294  Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
1295  null);
1296  }
1297  else
1298  {
1299  return ChangeLocationType(campaign, location, selectedTypeChange);
1300  }
1301  return false;
1302  }
1303  }
1304 
1305  foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
1306  {
1307  foreach (var requirement in typeChange.Requirements)
1308  {
1309  if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease))
1310  {
1311  if (!location.ProximityTimer.ContainsKey(requirement)) { location.ProximityTimer[requirement] = 0; }
1312  location.ProximityTimer[requirement] += 1;
1313  }
1314  else
1315  {
1316  location.ProximityTimer.Remove(requirement);
1317  }
1318  }
1319  }
1320 
1321  return false;
1322  }
1323 
1324  private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change)
1325  {
1326  LocalizedString prevName = location.DisplayName;
1327 
1328  if (!LocationType.Prefabs.TryGet(change.ChangeToType, out var newType))
1329  {
1330  DebugConsole.ThrowError($"Failed to change the type of the location \"{location.DisplayName}\". Location type \"{change.ChangeToType}\" not found.");
1331  return false;
1332  }
1333 
1334  if (location.LocationTypeChangesBlocked) { return false; }
1335 
1336  if (newType.OutpostTeam != location.Type.OutpostTeam ||
1337  newType.HasOutpost != location.Type.HasOutpost)
1338  {
1339  location.ClearMissions();
1340  }
1341  location.ChangeType(campaign, newType);
1342  ChangeLocationTypeProjSpecific(location, prevName, change);
1343  foreach (var requirement in change.Requirements)
1344  {
1345  location.ProximityTimer.Remove(requirement);
1346  }
1347  location.TimeSinceLastTypeChange = 0;
1348  location.LocationTypeChangeCooldown = change.CooldownAfterChange;
1349  location.PendingLocationTypeChange = null;
1350  return true;
1351  }
1352 
1353  public static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func<Location, bool> criteria, Func<LocationConnection, bool> connectionCriteria = null)
1354  {
1355  return GetDistanceToClosestLocationOrConnection(startLocation, maxDistance, criteria, connectionCriteria) <= maxDistance;
1356  }
1357 
1362  public static int GetDistanceToClosestLocationOrConnection(Location startLocation, int maxDistance, Func<Location, bool> criteria, Func<LocationConnection, bool> connectionCriteria = null)
1363  {
1364  int distance = 0;
1365  var locationsToTest = new List<Location>() { startLocation };
1366  var nextBatchToTest = new HashSet<Location>();
1367  var checkedLocations = new HashSet<Location>();
1368  while (locationsToTest.Any())
1369  {
1370  foreach (var location in locationsToTest)
1371  {
1372  checkedLocations.Add(location);
1373  if (criteria(location)) { return distance; }
1374  foreach (var connection in location.Connections)
1375  {
1376  if (connectionCriteria != null && connectionCriteria(connection))
1377  {
1378  return distance;
1379  }
1380  var otherLocation = connection.OtherLocation(location);
1381  if (!checkedLocations.Contains(otherLocation))
1382  {
1383  nextBatchToTest.Add(otherLocation);
1384  }
1385  }
1386  if (distance > maxDistance) { return int.MaxValue; }
1387  }
1388  distance++;
1389  locationsToTest.Clear();
1390  locationsToTest.AddRange(nextBatchToTest);
1391  nextBatchToTest.Clear();
1392  }
1393  return int.MaxValue;
1394  }
1395 
1396 
1397  partial void ChangeLocationTypeProjSpecific(Location location, LocalizedString prevName, LocationTypeChange change);
1398 
1399  partial void ClearAnimQueue();
1400 
1401  public void Discover(Location location, bool checkTalents = true)
1402  {
1403  if (location is null) { return; }
1404  if (locationsDiscovered.Contains(location)) { return; }
1405  locationsDiscovered.Add(location);
1406  if (checkTalents)
1407  {
1408  GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new Location.AbilityLocation(location)));
1409  }
1410  }
1411 
1412  public void Visit(Location location)
1413  {
1414  if (location is null) { return; }
1415  if (locationsVisited.Contains(location)) { return; }
1416  locationsVisited.Add(location);
1417  RemoveFogOfWarProjSpecific(location);
1418  }
1419 
1420  public void ClearLocationHistory()
1421  {
1422  locationsDiscovered.Clear();
1423  locationsVisited.Clear();
1424  }
1425 
1426  public int? GetDiscoveryIndex(Location location)
1427  {
1428  if (!trackedLocationDiscoveryAndVisitOrder) { return null; }
1429  if (location is null) { return -1; }
1430  return locationsDiscovered.IndexOf(location);
1431  }
1432 
1433  public int? GetVisitIndex(Location location, bool includeLocationsWithoutOutpost = false)
1434  {
1435  if (!trackedLocationDiscoveryAndVisitOrder) { return null; }
1436  if (location is null) { return -1; }
1437  int index = locationsVisited.IndexOf(location);
1438  if (includeLocationsWithoutOutpost) { return index; }
1439  int noOutpostLocations = 0;
1440  for (int i = 0; i < index; i++)
1441  {
1442  if (locationsVisited[i] is not Location l) { continue; }
1443  if (l.HasOutpost()) { continue; }
1444  noOutpostLocations++;
1445  }
1446  return index - noOutpostLocations;
1447  }
1448 
1449  public bool IsDiscovered(Location location)
1450  {
1451  if (location is null) { return false; }
1452  return locationsDiscovered.Contains(location);
1453  }
1454 
1455  public bool IsVisited(Location location)
1456  {
1457  if (location is null) { return false; }
1458  return locationsVisited.Contains(location);
1459  }
1460 
1461  partial void RemoveFogOfWarProjSpecific(Location location);
1462 
1466  public static Map Load(CampaignMode campaign, XElement element)
1467  {
1468  Map map = new Map(campaign, element);
1469  map.LoadState(campaign, element, false);
1470 #if CLIENT
1471  map.DrawOffset = -map.CurrentLocation.MapPosition;
1472 #endif
1473  return map;
1474  }
1475 
1479  public void LoadState(CampaignMode campaign, XElement element, bool showNotifications)
1480  {
1481  ClearAnimQueue();
1482  SetLocation(element.GetAttributeInt("currentlocation", 0));
1483 
1484  if (!Version.TryParse(element.GetAttributeString("version", ""), out Version version))
1485  {
1486  DebugConsole.ThrowError("Incompatible map save file, loading the game failed.");
1487  return;
1488  }
1489 
1490  ClearLocationHistory();
1491  foreach (var subElement in element.Elements())
1492  {
1493  switch (subElement.Name.ToString().ToLowerInvariant())
1494  {
1495  case "location":
1496  int locationIndex = subElement.GetAttributeInt("i", -1);
1497  if (locationIndex < 0 || locationIndex >= Locations.Count)
1498  {
1499  DebugConsole.AddWarning($"Error while loading the campaign map: location index out of bounds ({locationIndex})");
1500  continue;
1501  }
1502  Location location = Locations[locationIndex];
1503  location.ProximityTimer.Clear();
1504  for (int i = 0; i < location.Type.CanChangeTo.Count; i++)
1505  {
1506  for (int j = 0; j < location.Type.CanChangeTo[i].Requirements.Count; j++)
1507  {
1508  location.ProximityTimer.Add(location.Type.CanChangeTo[i].Requirements[j], subElement.GetAttributeInt("changetimer" + i + "-" + j, 0));
1509  }
1510  }
1511  location.LoadLocationTypeChange(subElement);
1512 
1513  // Backwards compatibility: if the discovery status is defined in the location element,
1514  // the game was saved using when the discovery order still wasn't being tracked
1515  if (subElement.GetAttributeBool("discovered", false))
1516  {
1517  Discover(location);
1518  Visit(location);
1519  trackedLocationDiscoveryAndVisitOrder = false;
1520  }
1521 
1522  Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
1523  LocalizedString prevLocationName = location.DisplayName;
1524  LocationType prevLocationType = location.Type;
1525  LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
1526  location.ChangeType(campaign, newLocationType);
1527 
1528  var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty);
1529  location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
1530 
1531  if (showNotifications && prevLocationType != location.Type)
1532  {
1533  var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier);
1534  if (change != null)
1535  {
1536  ChangeLocationTypeProjSpecific(location, prevLocationName, change);
1537  location.TimeSinceLastTypeChange = 0;
1538  }
1539  }
1540 
1541  var secondaryFactionIdentifier = subElement.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
1542  location.SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
1543 
1544  location.LoadStores(subElement);
1545  location.LoadMissions(subElement);
1546 
1547  break;
1548  case "connection":
1549  //the index wasn't saved previously, skip if that's the case
1550  if (subElement.Attribute("i") == null) { continue; }
1551 
1552  int connectionIndex = subElement.GetAttributeInt("i", -1);
1553  if (connectionIndex < 0 || connectionIndex >= Connections.Count)
1554  {
1555  DebugConsole.AddWarning($"Error while loading the campaign map: connection index out of bounds ({connectionIndex})");
1556  continue;
1557  }
1558  Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false);
1559  Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false);
1560  break;
1561  case "radiation":
1562  Radiation = new Radiation(this, generationParams.RadiationParams, subElement);
1563  break;
1564  case "discovered":
1565  bool trackedVisitedEmptyLocations = subElement.GetAttributeBool("trackedvisitedemptylocations", false);
1566  foreach (var childElement in subElement.GetChildElements("location"))
1567  {
1568  if (GetLocation(childElement) is Location l)
1569  {
1570  Discover(l);
1571  if (!trackedVisitedEmptyLocations)
1572  {
1573  if (!l.HasOutpost())
1574  {
1575  Visit(l);
1576  }
1577  trackedLocationDiscoveryAndVisitOrder = false;
1578  }
1579  }
1580  }
1581  break;
1582  case "visited":
1583  foreach (var childElement in subElement.GetChildElements("location"))
1584  {
1585  if (GetLocation(childElement) is Location l)
1586  {
1587  Visit(l);
1588  }
1589  }
1590  break;
1591  }
1592 
1593  Location GetLocation(XElement element)
1594  {
1595  int index = element.GetAttributeInt("i", -1);
1596  if (index < 0) { return null; }
1597  return Locations[index];
1598  }
1599 
1600  }
1601 
1602  void Discover(Location location)
1603  {
1604  this.Discover(location, checkTalents: false);
1605  if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
1606  {
1607  furthestDiscoveredLocation = location;
1608  }
1609  }
1610 
1611  foreach (Location location in Locations)
1612  {
1613  location?.InstantiateLoadedMissions(this);
1614  }
1615 
1616  //backwards compatibility:
1617  //if the save is from a version prior to the addition of faction-specific outposts, assign factions
1618  if (version < new Version(1, 0) && Locations.None(l => l.Faction != null || l.SecondaryFaction != null))
1619  {
1620  Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
1621  foreach (Location location in Locations)
1622  {
1623  if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
1624  {
1625  location.Faction = campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
1626  if (location != StartLocation)
1627  {
1628  location.SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
1629  }
1630  }
1631  }
1632  }
1633 
1634  int currentLocationConnection = element.GetAttributeInt("currentlocationconnection", -1);
1635  if (currentLocationConnection >= 0)
1636  {
1637  Connections[currentLocationConnection].Locked = false;
1638  SelectLocation(Connections[currentLocationConnection].OtherLocation(CurrentLocation));
1639  }
1640  else
1641  {
1642  //this should not be possible, you can't enter non-outpost locations (= natural formations)
1643  if (CurrentLocation != null && !CurrentLocation.Type.HasOutpost && SelectedConnection == null)
1644  {
1645  DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.DisplayName}). Loading the first adjacent connection...");
1646  SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation));
1647  }
1648  }
1649 
1650  var previousToEndLocation = GetPreviousToEndLocation();
1651  if (previousToEndLocation != null)
1652  {
1653  ForceLocationTypeToNone(campaign, previousToEndLocation);
1654  }
1655  }
1656 
1657  public void Save(XElement element)
1658  {
1659  XElement mapElement = new XElement("map");
1660 
1661  mapElement.Add(new XAttribute("version", GameMain.Version.ToString()));
1662  mapElement.Add(new XAttribute("currentlocation", CurrentLocationIndex));
1663  if (GameMain.GameSession.GameMode is CampaignMode campaign)
1664  {
1665  if (campaign.NextLevel != null && campaign.NextLevel.Type == LevelData.LevelType.LocationConnection)
1666  {
1667  mapElement.Add(new XAttribute("currentlocationconnection", Connections.IndexOf(CurrentLocation.Connections.Find(c => c.LevelData == campaign.NextLevel))));
1668  }
1669  else if (Level.Loaded != null && Level.Loaded.Type == LevelData.LevelType.LocationConnection && !CurrentLocation.Type.HasOutpost)
1670  {
1671  mapElement.Add(new XAttribute("currentlocationconnection", Connections.IndexOf(Connections.Find(c => c.LevelData == Level.Loaded.LevelData))));
1672  }
1673  }
1674  mapElement.Add(new XAttribute("width", Width));
1675  mapElement.Add(new XAttribute("height", Height));
1676  mapElement.Add(new XAttribute("selectedlocation", SelectedLocationIndex));
1677  mapElement.Add(new XAttribute("startlocation", Locations.IndexOf(StartLocation)));
1678  mapElement.Add(new XAttribute("endlocations", string.Join(',', EndLocations.Select(e => Locations.IndexOf(e)))));
1679  mapElement.Add(new XAttribute("seed", Seed));
1680 
1681  for (int i = 0; i < Locations.Count; i++)
1682  {
1683  var location = Locations[i];
1684  var locationElement = location.Save(this, mapElement);
1685  locationElement.Add(new XAttribute("i", i));
1686  }
1687 
1688  for (int i = 0; i < Connections.Count; i++)
1689  {
1690  var connection = Connections[i];
1691 
1692  var connectionElement = new XElement("connection",
1693  new XAttribute("passed", connection.Passed),
1694  new XAttribute("locked", connection.Locked),
1695  new XAttribute("difficulty", connection.Difficulty),
1696  new XAttribute("biome", connection.Biome.Identifier),
1697  new XAttribute("i", i),
1698  new XAttribute("locations", Locations.IndexOf(connection.Locations[0]) + "," + Locations.IndexOf(connection.Locations[1])));
1699  connection.LevelData.Save(connectionElement);
1700  mapElement.Add(connectionElement);
1701  }
1702 
1703  if (Radiation != null)
1704  {
1705  mapElement.Add(Radiation.Save());
1706  }
1707 
1708  if (locationsDiscovered.Any())
1709  {
1710  var discoveryElement = new XElement("discovered",
1711  new XAttribute("trackedvisitedemptylocations", true));
1712  foreach (Location location in locationsDiscovered)
1713  {
1714  int index = Locations.IndexOf(location);
1715  var locationElement = new XElement("location", new XAttribute("i", index));
1716  discoveryElement.Add(locationElement);
1717  }
1718  mapElement.Add(discoveryElement);
1719  }
1720 
1721  if (locationsVisited.Any())
1722  {
1723  var visitElement = new XElement("visited");
1724  foreach (Location location in locationsVisited)
1725  {
1726  int index = Locations.IndexOf(location);
1727  var locationElement = new XElement("location", new XAttribute("i", index));
1728  visitElement.Add(locationElement);
1729  }
1730  mapElement.Add(visitElement);
1731  }
1732 
1733  element.Add(mapElement);
1734  }
1735 
1736  public void Remove()
1737  {
1738  foreach (Location location in Locations)
1739  {
1740  location.Remove();
1741  }
1742  RemoveProjSpecific();
1743  }
1744 
1745  partial void RemoveProjSpecific();
1746  }
1747 }
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 readonly Version Version
Definition: GameMain.cs:21
static GameSession GameSession
Definition: GameMain.cs:45
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
CampaignMode? Campaign
Definition: GameSession.cs:128
bool EventsExhausted
'Exhaustible' sets won't appear in the same level until after one world step (~10 min,...
Definition: LevelData.cs:93
void ChangeType(CampaignMode campaign, LocationType newType, bool createStores=true)
Definition: Location.cs:747
void LoadStores(XElement locationElement)
Definition: Location.cs:1212
int TimeSinceLastTypeChange
Definition: Location.cs:513
readonly List< LocationConnection > Connections
Definition: Location.cs:57
LocationType Type
Definition: Location.cs:94
XElement Save(Map map, XElement parentElement)
Definition: Location.cs:1547
void LoadLocationTypeChange(XElement locationElement)
Definition: Location.cs:690
Identifier NameIdentifier
Definition: Location.cs:61
Vector2 MapPosition
Definition: Location.cs:92
List< int > GetSelectedMissionIndices()
Definition: Location.cs:470
readonly Dictionary< LocationTypeChange.Requirement, int > ProximityTimer
Definition: Location.cs:79
void InstantiateLoadedMissions(Map map)
Definition: Location.cs:1014
LocalizedString DisplayName
Definition: Location.cs:59
LevelData LevelData
Definition: Location.cs:98
Faction SecondaryFaction
Definition: Location.cs:104
void CreateStores(bool force=false)
If true, the stores will be recreated if they already exists.
Definition: Location.cs:1325
void LoadMissions(XElement locationElement)
Definition: Location.cs:726
readonly CharacterTeamType OutpostTeam
Definition: LocationType.cs:34
readonly List< LocationTypeChange > CanChangeTo
Definition: LocationType.cs:41
static readonly PrefabCollection< LocationType > Prefabs
Definition: LocationType.cs:15
static MapGenerationParams Instance
void ProgressWorld(CampaignMode campaign, CampaignMode.TransitionType transitionType, float roundDuration)
Definition: Map.cs:1182
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,...
Definition: Map.cs:1479
readonly NamedEvent< LocationChangeInfo > OnLocationChanged
From -> To
Definition: Map.cs:40
Action< Location, LocationConnection > OnLocationSelected
Definition: Map.cs:22
Map(CampaignSettings settings)
Definition: Map.cs:81
IReadOnlyList< Location > EndLocations
Definition: Map.cs:43
Location StartLocation
Definition: Map.cs:45
int? GetDiscoveryIndex(Location location)
Definition: Map.cs:1426
void Visit(Location location)
Definition: Map.cs:1412
IEnumerable< int > GetSelectedMissionIndices()
Definition: Map.cs:61
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...
Definition: Map.cs:1362
Location CurrentLocation
Definition: Map.cs:47
bool IsDiscovered(Location location)
Definition: Map.cs:1449
void Remove()
Definition: Map.cs:1736
static Map Load(CampaignMode campaign, XElement element)
Load a previously saved map from an xml element
Definition: Map.cs:1466
int GetZoneIndex(float xPos)
Definition: Map.cs:767
void SelectMission(IEnumerable< int > missionIndices)
Definition: Map.cs:1137
void SelectLocation(Location location)
Definition: Map.cs:1114
List< Location > Locations
Definition: Map.cs:70
int CurrentLocationIndex
Definition: Map.cs:50
int? GetVisitIndex(Location location, bool includeLocationsWithoutOutpost=false)
Definition: Map.cs:1433
int SelectedLocationIndex
Definition: Map.cs:57
Radiation Radiation
Definition: Map.cs:77
static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func< Location, bool > criteria, Func< LocationConnection, bool > connectionCriteria=null)
Definition: Map.cs:1353
void SetLocation(int index)
Definition: Map.cs:1039
Action< LocationConnection, IEnumerable< Mission > > OnMissionsSelected
Definition: Map.cs:23
void Discover(Location location, bool checkTalents=true)
Definition: Map.cs:1401
string Seed
Definition: Map.cs:68
void ClearLocationHistory()
Definition: Map.cs:1420
Location SelectedLocation
Definition: Map.cs:54
void Save(XElement element)
Definition: Map.cs:1657
Map(CampaignMode campaign, string seed)
Generate a new campaign map from the seed
Definition: Map.cs:284
Biome GetBiome(Vector2 mapPos)
Definition: Map.cs:773
List< LocationConnection > Connections
Definition: Map.cs:75
LocationConnection SelectedConnection
Definition: Map.cs:66
int Height
Definition: Map.cs:20
void MoveToNextLocation()
Definition: Map.cs:977
void SelectRandomLocation(bool preferUndiscovered)
Definition: Map.cs:1167
bool IsVisited(Location location)
Definition: Map.cs:1455
void SelectLocation(int index)
Definition: Map.cs:1069
Biome GetBiome(float xPos)
Definition: Map.cs:778
bool AllowDebugTeleport
Definition: Map.cs:13
int Width
Definition: Map.cs:19
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:733
AbilityEffectType
Definition: Enums.cs:140
CharacterType
Definition: Enums.cs:711
LocationChangeInfo(Location prevLocation, Location newLocation)
Definition: Map.cs:30
readonly Location PrevLocation
Definition: Map.cs:27
readonly Location NewLocation
Definition: Map.cs:28