Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Collections.Immutable;
4 using System.Linq;
5 using System.Reflection;
6 using System.Xml.Linq;
7 
8 namespace Barotrauma
9 {
10  [Flags]
11  public enum MissionType
12  {
13  None = 0x0,
14  Salvage = 0x1,
15  Monster = 0x2,
16  Cargo = 0x4,
17  Beacon = 0x8,
18  Nest = 0x10,
19  Mineral = 0x20,
20  Combat = 0x40,
21  AbandonedOutpost = 0x80,
22  Escort = 0x100,
23  Pirate = 0x200,
24  GoTo = 0x400,
25  ScanAlienRuins = 0x800,
26  EliminateTargets = 0x1000,
27  End = 0x2000,
29  }
30 
32  {
34 
35  public static readonly Dictionary<MissionType, Type> CoOpMissionClasses = new Dictionary<MissionType, Type>()
36  {
37  { MissionType.Salvage, typeof(SalvageMission) },
38  { MissionType.Monster, typeof(MonsterMission) },
39  { MissionType.Cargo, typeof(CargoMission) },
40  { MissionType.Beacon, typeof(BeaconMission) },
41  { MissionType.Nest, typeof(NestMission) },
42  { MissionType.Mineral, typeof(MineralMission) },
43  { MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) },
44  { MissionType.Escort, typeof(EscortMission) },
45  { MissionType.Pirate, typeof(PirateMission) },
46  { MissionType.GoTo, typeof(GoToMission) },
47  { MissionType.ScanAlienRuins, typeof(ScanMission) },
48  { MissionType.EliminateTargets, typeof(EliminateTargetsMission) },
49  { MissionType.End, typeof(EndMission) }
50  };
51  public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
52  {
53  { MissionType.Combat, typeof(CombatMission) }
54  };
55 
56  public class ReputationReward
57  {
58  public readonly Identifier FactionIdentifier;
59  public readonly float Amount;
60  public readonly float AmountForOpposingFaction;
61 
62  public ReputationReward(XElement element)
63  {
64  FactionIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
65  Amount = element.GetAttributeFloat(nameof(Amount), 0.0f);
66  AmountForOpposingFaction = element.GetAttributeFloat(nameof(AmountForOpposingFaction), 0.0f);
67  }
68  }
69 
70  public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo, MissionType.End };
71 
72  private readonly ConstructorInfo constructor;
73 
74  public readonly MissionType Type;
75 
76  public readonly bool MultiplayerOnly, SingleplayerOnly;
77 
78  public readonly Identifier TextIdentifier;
79 
80  public readonly ImmutableHashSet<Identifier> Tags;
81 
82  public readonly LocalizedString Name;
83  public readonly LocalizedString Description;
86  public readonly LocalizedString SonarLabel;
88 
90 
91  public readonly ImmutableList<ReputationReward> ReputationRewards;
92 
93  public readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>
94  DataRewards = new List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>();
95 
96  public readonly int Commonness;
100  public readonly int? Difficulty;
101  public const int MinDifficulty = 1, MaxDifficulty = 4;
105  public readonly int MinLevelDifficulty = 0;
109  public readonly int MaxLevelDifficulty = 100;
110 
111  public readonly int Reward;
112 
113  // The titles and bodies of the popup messages during the mission, shown when the state of the mission changes. The order matters.
114  public readonly ImmutableArray<LocalizedString> Headers;
115  public readonly ImmutableArray<LocalizedString> Messages;
116 
117  public readonly bool AllowRetry;
118 
119  public readonly bool ShowInMenus, ShowStartMessage;
120 
121  public readonly bool IsSideObjective;
122 
123  public readonly bool AllowOtherMissionsInLevel;
124 
125  public readonly bool RequireWreck, RequireRuin, RequireThalamusWreck;
126 
130  public readonly bool BlockLocationTypeChanges;
131 
132  public readonly bool ShowProgressBar;
133  public readonly bool ShowProgressInNumbers;
134  public readonly int MaxProgressState;
136 
140  public readonly List<(Identifier from, Identifier to)> AllowedConnectionTypes;
141 
145  public readonly List<Identifier> AllowedLocationTypes = new List<Identifier>();
146 
151 
155  public readonly List<string> UnhideEntitySubCategories = new List<string>();
156 
157  public class TriggerEvent
158  {
159  [Serialize("", IsPropertySaveable.Yes)]
160  public string EventIdentifier { get; private set; }
161 
162  [Serialize(0, IsPropertySaveable.Yes)]
163  public int State { get; private set; }
164 
165  [Serialize(0.0f, IsPropertySaveable.Yes)]
166  public float Delay { get; private set; }
167 
168  [Serialize(false, IsPropertySaveable.Yes)]
169  public bool CampaignOnly { get; private set; }
170 
171  public TriggerEvent(XElement element)
172  {
174  }
175  }
176 
177  public readonly List<TriggerEvent> TriggerEvents = new List<TriggerEvent>();
178 
180 
182 
183  public MissionPrefab(ContentXElement element, MissionsFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
184  {
185  ConfigElement = element;
186 
187  TextIdentifier = element.GetAttributeIdentifier("textidentifier", Identifier);
188 
189  Tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableHashSet();
190 
191  Name = GetText(element.GetAttributeString("name", ""), "MissionName");
192  Description = GetText(element.GetAttributeString("description", ""), "MissionDescription");
193 
194  LocalizedString GetText(string textTag, string textTagPrefix)
195  {
196  if (string.IsNullOrEmpty(textTag))
197  {
198  return TextManager.Get($"{textTagPrefix}.{TextIdentifier}");
199  }
200  else
201  {
202  return
203  //prefer finding a text based on the specific text tag defined in the mission config
204  TextManager.Get(textTag)
205  //2nd option: the "default" format (MissionName.SomeMission)
206  .Fallback(TextManager.Get($"{textTagPrefix}.{TextIdentifier}"))
207  //last option: use the text in the xml as-is with no localization
208  .Fallback(textTag);
209  }
210  }
211 
212  Reward = element.GetAttributeInt("reward", 1);
213  AllowRetry = element.GetAttributeBool("allowretry", false);
214  ShowInMenus = element.GetAttributeBool("showinmenus", true);
215  ShowStartMessage = element.GetAttributeBool("showstartmessage", true);
216  IsSideObjective = element.GetAttributeBool("sideobjective", false);
217  RequireWreck = element.GetAttributeBool("requirewreck", false);
218  RequireRuin = element.GetAttributeBool("requireruin", false);
219  RequireThalamusWreck = element.GetAttributeBool("requirethalamuswreck", false);
220 
221  if (RequireThalamusWreck) { RequireWreck = true; }
222 
225  Commonness = element.GetAttributeInt("commonness", 1);
226  AllowOtherMissionsInLevel = element.GetAttributeBool("allowothermissionsinlevel", true);
227  if (element.GetAttribute("difficulty") != null)
228  {
229  int difficulty = element.GetAttributeInt("difficulty", MinDifficulty);
230  Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty);
231  }
234  MinLevelDifficulty = Math.Clamp(MinLevelDifficulty, 0, Math.Min(MaxLevelDifficulty, 100));
235  MaxLevelDifficulty = Math.Clamp(MaxLevelDifficulty, Math.Max(MinLevelDifficulty, 0), 100);
236 
237  ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false);
239  MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1);
240  string progressBarLabel = element.GetAttributeString(nameof(ProgressBarLabel), "");
241  ProgressBarLabel = TextManager.Get(progressBarLabel).Fallback(progressBarLabel);
242 
243  string successMessageTag = element.GetAttributeString("successmessage", "");
244  SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}");
245  if (!string.IsNullOrEmpty(successMessageTag))
246  {
248  .Fallback(TextManager.Get(successMessageTag))
249  .Fallback(successMessageTag);
250  }
251  SuccessMessage = SuccessMessage.Fallback(TextManager.Get("missioncompleted"));
252 
253  string failureMessageTag = element.GetAttributeString("failuremessage", "");
254  FailureMessage = TextManager.Get($"MissionFailure.{TextIdentifier}");
255  if (!string.IsNullOrEmpty(failureMessageTag))
256  {
258  .Fallback(TextManager.Get(failureMessageTag))
259  .Fallback(failureMessageTag);
260  }
261  FailureMessage = FailureMessage.Fallback(TextManager.Get("missionfailed"));
262 
263  string sonarLabelTag = element.GetAttributeString("sonarlabel", "");
264  SonarLabel =
265  TextManager.Get($"MissionSonarLabel.{sonarLabelTag}")
266  .Fallback(TextManager.Get(sonarLabelTag))
267  .Fallback(TextManager.Get($"MissionSonarLabel.{TextIdentifier}"));
268  if (!string.IsNullOrEmpty(sonarLabelTag))
269  {
270  SonarLabel = SonarLabel.Fallback(sonarLabelTag);
271  }
272 
273  SonarIconIdentifier = element.GetAttributeIdentifier("sonaricon", "");
274 
275  MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false);
276  SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false);
277 
278  AchievementIdentifier = element.GetAttributeIdentifier("achievementidentifier", "");
279 
280  UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", Array.Empty<string>()).ToList();
281 
282  var headers = new List<LocalizedString>();
283  var messages = new List<LocalizedString>();
284  AllowedConnectionTypes = new List<(Identifier from, Identifier to)>();
285 
286  for (int i = 0; i < 100; i++)
287  {
288  LocalizedString header = TextManager.Get($"MissionHeader{i}.{TextIdentifier}");
289  LocalizedString message = TextManager.Get($"MissionMessage{i}.{TextIdentifier}");
290  if (!message.IsNullOrEmpty())
291  {
292  headers.Add(header);
293  messages.Add(message);
294  }
295  }
296 
297  List<ReputationReward> reputationRewards = new List<ReputationReward>();
298  int messageIndex = 0;
299  foreach (var subElement in element.Elements())
300  {
301  switch (subElement.Name.ToString().ToLowerInvariant())
302  {
303  case "message":
304  if (messageIndex > headers.Count - 1)
305  {
306  headers.Add(string.Empty);
307  messages.Add(string.Empty);
308  }
309  headers[messageIndex] =
310  TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}")
311  .Fallback(TextManager.Get(subElement.GetAttributeString("header", "")))
312  .Fallback(subElement.GetAttributeString("header", ""));
313  messages[messageIndex] =
314  TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}")
315  .Fallback(TextManager.Get(subElement.GetAttributeString("text", "")))
316  .Fallback(subElement.GetAttributeString("text", ""));
317  messageIndex++;
318  break;
319  case "locationtype":
320  case "connectiontype":
321  if (subElement.GetAttribute("identifier") != null)
322  {
323  AllowedLocationTypes.Add(subElement.GetAttributeIdentifier("identifier", ""));
324  }
325  else
326  {
328  subElement.GetAttributeIdentifier("from", ""),
329  subElement.GetAttributeIdentifier("to", "")));
330  }
331  break;
332  case "locationtypechange":
333  LocationTypeChangeOnCompleted = new LocationTypeChange(subElement.GetAttributeIdentifier("from", ""), subElement, requireChangeMessages: false, defaultProbability: 1.0f);
334  break;
335  case "reputation":
336  case "reputationreward":
337  reputationRewards.Add(new ReputationReward(subElement));
338  break;
339  case "metadata":
340  Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
341  string stringValue = subElement.GetAttributeString("value", string.Empty);
342  if (!string.IsNullOrWhiteSpace(stringValue) && !identifier.IsEmpty)
343  {
344  object value = SetDataAction.ConvertXMLValue(stringValue);
346 
347  string operatingString = subElement.GetAttributeString("operation", string.Empty);
348  if (!string.IsNullOrWhiteSpace(operatingString))
349  {
350  operation = (SetDataAction.OperationType) Enum.Parse(typeof(SetDataAction.OperationType), operatingString);
351  }
352 
353  DataRewards.Add((identifier, value, operation));
354  }
355  break;
356  case "triggerevent":
357  TriggerEvents.Add(new TriggerEvent(subElement));
358  break;
359  }
360  }
361  Headers = headers.ToImmutableArray();
362  Messages = messages.ToImmutableArray();
363  ReputationRewards = reputationRewards.ToImmutableList();
364 
365  Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty);
366  //backwards compatibility
367 
368  if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue")
369  {
370  missionTypeName = nameof(MissionType.AbandonedOutpost).ToIdentifier();
371  }
372  else if (missionTypeName == "clearalienruins")
373  {
374  missionTypeName = nameof(MissionType.EliminateTargets).ToIdentifier();
375  }
376 
377  if (!Enum.TryParse(missionTypeName.Value, true, out Type))
378  {
379  DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
380  return;
381  }
382  if (Type == MissionType.None)
383  {
384  DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - mission type cannot be none.");
385  return;
386  }
387 #if DEBUG
388  if (Type == MissionType.Monster && SonarLabel.IsNullOrEmpty())
389  {
390  DebugConsole.AddWarning($"Potential error in mission prefab \"{Identifier}\" - sonar label not set.");
391  }
392 #endif
393 
394  if (CoOpMissionClasses.ContainsKey(Type))
395  {
396  constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
397  }
398  else if (PvPMissionClasses.ContainsKey(Type))
399  {
400  constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
401  }
402  else
403  {
404  DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\"");
405  }
406  if (constructor == null)
407  {
408  DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!",
409  contentPackage: element.ContentPackage);
410  }
411 
412  InitProjSpecific(element);
413  }
414 
415  partial void InitProjSpecific(ContentXElement element);
416 
417  public bool IsAllowed(Location from, Location to)
418  {
419  if (from == to)
420  {
421  if (!RequiredLocationFaction.IsEmpty && from.Faction?.Prefab.Identifier != RequiredLocationFaction)
422  {
423  return false;
424  }
425  return
426  AllowedLocationTypes.Any(lt => lt == "any") ||
427  AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) ||
428  AllowedLocationTypes.Any(lt => lt == from.Type.Identifier);
429  }
430 
431  foreach (var (fromType, toType) in AllowedConnectionTypes)
432  {
433  if (fromType == "any" ||
434  fromType == from.Type.Identifier ||
435  (fromType == "anyoutpost" && from.HasOutpost() && from.Type.Identifier != "abandoned"))
436  {
437  if (toType == "any" ||
438  toType == to.Type.Identifier ||
439  (toType == "anyoutpost" && to.HasOutpost() && to.Type.Identifier != "abandoned"))
440  {
441  return true;
442  }
443  }
444  }
445 
446  return false;
447  }
448 
452  public bool IsAllowedDifficulty(float difficulty) => difficulty >= MinLevelDifficulty && difficulty <= MaxLevelDifficulty;
453 
454  public Mission Instantiate(Location[] locations, Submarine sub)
455  {
456  return constructor?.Invoke(new object[] { this, locations, sub }) as Mission;
457  }
458 
459  partial void DisposeProjectSpecific();
460  public override void Dispose()
461  {
462  DisposeProjectSpecific();
463  }
464  }
465 }
string? GetAttributeString(string key, string? def)
Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys)
bool GetAttributeBool(string key, bool def)
int GetAttributeInt(string key, int def)
string?[] GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant=false)
XAttribute? GetAttribute(string name)
Identifier GetAttributeIdentifier(string key, string def)
FactionPrefab Prefab
Definition: Factions.cs:18
Makes the event jump to a Label somewhere else in the event.
Definition: GoTo.cs:7
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
LocationType Type
Definition: Location.cs:91
readonly ImmutableList< ReputationReward > ReputationRewards
readonly? int Difficulty
Displayed difficulty (indicator)
readonly int MaxLevelDifficulty
The actual maximum difficulty of the level allowed for this mission to trigger.
static readonly Dictionary< MissionType, Type > CoOpMissionClasses
static readonly PrefabCollection< MissionPrefab > Prefabs
readonly List<(Identifier from, Identifier to)> AllowedConnectionTypes
The mission can only be received when travelling from a location of the first type to a location of t...
readonly bool BlockLocationTypeChanges
If enabled, locations this mission takes place in cannot change their type
readonly int MinLevelDifficulty
The actual minimum difficulty of the level allowed for this mission to trigger.
readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)> DataRewards
readonly Identifier RequiredLocationFaction
The mission can only happen in locations owned by this faction. In the mission mode,...
readonly List< Identifier > AllowedLocationTypes
The mission can only be received in these location types
bool IsAllowedDifficulty(float difficulty)
Inclusive (matching the min an max values is accepted).
readonly List< string > UnhideEntitySubCategories
Show entities belonging to these sub categories when the mission starts
static readonly Dictionary< MissionType, Type > PvPMissionClasses
Mission Instantiate(Location[] locations, Submarine sub)
readonly Identifier Identifier
Definition: Prefab.cs:34
Prefab that has a property serves as a deterministic hash of a prefab's identifier....
static Dictionary< Identifier, SerializableProperty > DeserializeProperties(object obj, XElement element=null)
Sets a campaign metadata value. The metadata can be any arbitrary data you want to save: for example,...
Definition: SetDataAction.cs:9
static object ConvertXMLValue(string value)