Client LuaCsForBarotrauma
ScriptedEvent.cs
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
5 
6 namespace Barotrauma
7 {
9  {
10  public sealed record TargetPredicate(
11  TargetPredicate.EntityType Type,
12  Predicate<Entity> Predicate)
13  {
14  public enum EntityType
15  {
16  Character,
17  Hull,
18  Item,
19  Structure,
20  Submarine
21  }
22  }
23 
24  private readonly Dictionary<Identifier, List<TargetPredicate>> targetPredicates = new Dictionary<Identifier, List<TargetPredicate>>();
25 
26  private readonly Dictionary<Identifier, List<Entity>> cachedTargets = new Dictionary<Identifier, List<Entity>>();
27 
32  private readonly Dictionary<Identifier, int> initialAmounts = new Dictionary<Identifier, int>();
33 
34  private bool newEntitySpawned;
35  private int prevPlayerCount, prevBotCount;
36  private Character prevControlled;
37 
39 
40  private readonly Identifier[] requiredDestinationTypes;
41  public readonly bool RequireBeaconStation;
42 
43  public readonly Identifier RequiredDestinationFaction;
44 
45  public int CurrentActionIndex { get; private set; }
46  public List<EventAction> Actions { get; } = new List<EventAction>();
47  public Dictionary<Identifier, List<Entity>> Targets { get; } = new Dictionary<Identifier, List<Entity>>();
48 
49  protected virtual IEnumerable<Identifier> NonActionChildElementNames => Enumerable.Empty<Identifier>();
50 
51  public override string ToString()
52  {
53  return $"{nameof(ScriptedEvent)} ({prefab.Identifier})";
54  }
55 
56  public ScriptedEvent(EventPrefab prefab, int seed) : base(prefab, seed)
57  {
58  foreach (var element in prefab.ConfigElement.Elements())
59  {
60  Identifier elementId = element.Name.ToIdentifier();
61  if (NonActionChildElementNames.Contains(elementId)) { continue; }
62  if (elementId == nameof(Barotrauma.OnRoundEndAction))
63  {
65  continue;
66  }
67  if (elementId == "statuseffect")
68  {
69  DebugConsole.ThrowError($"Error in event prefab \"{prefab.Identifier}\". Status effect configured as an action. Please configure status effects as child elements of a StatusEffectAction.",
70  contentPackage: prefab.ContentPackage);
71  continue;
72  }
73  var action = EventAction.Instantiate(this, element);
74  if (action != null) { Actions.Add(action); }
75  }
76 
77  if (!Actions.Any())
78  {
79  DebugConsole.ThrowError($"Scripted event \"{prefab.Identifier}\" has no actions. The event will do nothing.",
80  contentPackage: prefab.ContentPackage);
81  }
82 
83  requiredDestinationTypes = prefab.ConfigElement.GetAttributeIdentifierArray("requireddestinationtypes", Array.Empty<Identifier>());
84  RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false);
86 
87  var allActionsWithIndent = GetAllActions();
88  var allActions = allActionsWithIndent.Select(a => a.action);
89 
90  //attempt to check if the event has ConversationActions with options that don't close the prompt and don't lead to any follow-up conversation
91  foreach (var action in allActions)
92  {
93  if (action is ConversationAction conversationAction && conversationAction.Options.Any())
94  {
95  int thisActionIndex = allActionsWithIndent.FindIndex(a => a.action == action);
96  int thisIndentationLevel = allActionsWithIndent[thisActionIndex].indent;
97  bool isLast = true;
98 
99  //go through all the actions after this one
100  foreach (var actionWithIndent in allActionsWithIndent.Skip(thisActionIndex + 1))
101  {
102  //if it's an action with the same indentation level, it means it's a ConversationAction coming after this one
103  if (actionWithIndent.action is ConversationAction && actionWithIndent.indent == thisIndentationLevel)
104  {
105  isLast = false;
106  break;
107  }
108  //if the indentation level went back down, we've already searched everything inside this ConversationAction
109  if (actionWithIndent.indent < thisIndentationLevel) { break; }
110  }
111  if (isLast)
112  {
113  foreach (var option in conversationAction.Options)
114  {
115  if (!conversationAction.GetEndingOptions().Contains(conversationAction.Options.IndexOf(option)) &&
116  option.Actions.None(a =>
117  a is ConversationAction || HasConversationSubAction(a) ||
118  /* if there's a goto action explicitly set to end the conversation, assume it's intentional*/
119  a is GoTo { EndConversation: false }))
120  {
121  DebugConsole.AddWarning($"Potential error in event \"{prefab.Identifier}\": {nameof(ConversationAction)} ({conversationAction.Text}) has an option ({option.Text}) that doesn't end the conversation, but could not find any follow-ups to the conversation.");
122  }
123  }
124  }
125  }
126 
127  static bool HasConversationSubAction(EventAction action)
128  {
129  foreach (var subAction in action.GetSubActions())
130  {
131  if (subAction is ConversationAction) { return true; }
132  if (HasConversationSubAction(subAction)) { return true; }
133  }
134  return false;
135  }
136  }
137 
138  foreach (var label in allActions.OfType<Label>())
139  {
140  if (allActions.None(a => a is GoTo gotoAction && label.Name == gotoAction.Name))
141  {
142  //this can be safe, because a label with no gotos leading to it does nothing (but it's still a sign that something's misconfigured)
143  DebugConsole.AddWarning($"Error in event \"{prefab.Identifier}\". Could not find a GoTo matching the Label \"{label.Name}\".",
144  contentPackage: prefab.ContentPackage);
145  }
146  }
147 
148  foreach (var gotoAction in allActions.OfType<GoTo>())
149  {
150  int labelCount = allActions.Count(a => a is Label label && label.Name == gotoAction.Name);
151  if (labelCount == 0)
152  {
153  DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Could not find a label matching the GoTo \"{gotoAction.Name}\".",
154  contentPackage: prefab.ContentPackage);
155  }
156  else if (labelCount > 1)
157  {
158  DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Multiple labels with the name \"{gotoAction.Name}\".",
159  contentPackage: prefab.ContentPackage);
160  }
161  }
162 
163  GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start");
164  }
165 
166  public override string GetDebugInfo()
167  {
168  EventAction currentAction = !IsFinished ? Actions[CurrentActionIndex] : null;
169 
170  string text = $"Finished: {IsFinished.ColorizeObject()}\n" +
171  $"Action index: {CurrentActionIndex.ColorizeObject()}\n" +
172  $"Current action: {currentAction?.ToDebugString() ?? ToolBox.ColorizeObject(null)}\n";
173 
174  text += "All actions:\n";
175  text += GetAllActions().Aggregate(string.Empty, (current, action) => current + $"{new string(' ', action.indent * 6)}{action.action.ToDebugString()}\n");
176 
177  text += "Targets:\n";
178  foreach (var (key, value) in Targets)
179  {
180  text += $" {key.ColorizeObject()}: {value.Aggregate(string.Empty, (current, entity) => current + $"{entity.ColorizeObject()} ")}\n";
181  }
182  return text;
183  }
184 
185  public virtual string GetTextForReplacementElement(string tag)
186  {
187  if (tag.StartsWith("eventtag:"))
188  {
189  string targetTag = tag["eventtag:".Length..];
190  Entity target = GetTargets(targetTag.ToIdentifier()).FirstOrDefault();
191  if (target != null)
192  {
193  if (target is Item item) { return item.Name; }
194  if (target is Character character) { return character.Name; }
195  if (target is Hull hull) { return hull.DisplayName.Value; }
196  if (target is Submarine sub) { return sub.Info.DisplayName.Value; }
197  DebugConsole.AddWarning($"Failed to get the name of the event target {target} as a replacement for the tag {tag} in an event text.",
199  return target.ToString();
200  }
201  else
202  {
203  return $"[target \"{targetTag}\" not found]";
204  }
205  }
206  return string.Empty;
207  }
208 
210  {
211  return str;
212  }
213 
218  public List<(int indent, EventAction action)> GetAllActions()
219  {
220  var list = new List<(int indent, EventAction action)>();
221  foreach (EventAction eventAction in Actions)
222  {
223  list.AddRange(FindActionsRecursive(eventAction));
224  }
225  return list;
226 
227  static List<(int indent, EventAction action)> FindActionsRecursive(EventAction eventAction, int indent = 1)
228  {
229  var eventActions = new List<(int indent, EventAction action)> { (indent, eventAction) };
230  indent++;
231  foreach (var action in eventAction.GetSubActions())
232  {
233  eventActions.AddRange(FindActionsRecursive(action, indent));
234  }
235  return eventActions;
236  }
237  }
238 
239  public void AddTarget(Identifier tag, Entity target)
240  {
241  if (target == null)
242  {
243  throw new ArgumentException($"Target was null (tag: {tag})");
244  }
245  if (target.Removed)
246  {
247  throw new ArgumentException($"Target has been removed (tag: {tag})");
248  }
249  if (Targets.ContainsKey(tag))
250  {
251  if (!Targets[tag].Contains(target))
252  {
253  Targets[tag].Add(target);
254  }
255  }
256  else
257  {
258  Targets.Add(tag, new List<Entity>() { target });
259  }
260  if (cachedTargets.ContainsKey(tag))
261  {
262  if (!cachedTargets[tag].Contains(target))
263  {
264  cachedTargets[tag].Add(target);
265  }
266  }
267  else
268  {
269  cachedTargets.Add(tag, Targets[tag].ToList());
270  }
271  if (!initialAmounts.ContainsKey(tag))
272  {
273  initialAmounts.Add(tag, cachedTargets[tag].Count);
274  }
275  }
276 
277  public void AddTargetPredicate(Identifier tag, TargetPredicate.EntityType entityType, Predicate<Entity> predicate)
278  {
279  if (!targetPredicates.ContainsKey(tag))
280  {
281  targetPredicates.Add(tag, new List<TargetPredicate>());
282  }
283  targetPredicates[tag].Add(new TargetPredicate(entityType, predicate));
284  // force re-search for this tag
285  if (cachedTargets.ContainsKey(tag))
286  {
287  cachedTargets.Remove(tag);
288  }
289  }
290 
291  public int GetInitialTargetCount(Identifier tag)
292  {
293  if (initialAmounts.TryGetValue(tag, out int count))
294  {
295  return count;
296  }
297  return 0;
298  }
299 
300  public IEnumerable<Entity> GetTargets(Identifier tag)
301  {
302  if (cachedTargets.ContainsKey(tag))
303  {
304  if (cachedTargets[tag].Any(t => t.Removed))
305  {
306  cachedTargets.Clear();
307  }
308  else
309  {
310  return cachedTargets[tag];
311  }
312  }
313 
314  List<Entity> targetsToReturn = new List<Entity>();
315  if (Targets.ContainsKey(tag))
316  {
317  foreach (Entity e in Targets[tag])
318  {
319  if (e.Removed) { continue; }
320  targetsToReturn.Add(e);
321  }
322  }
323  if (targetPredicates.ContainsKey(tag))
324  {
325  foreach (var targetPredicate in targetPredicates[tag])
326  {
327  IEnumerable<Entity> entityList = targetPredicate.Type switch
328  {
329  TargetPredicate.EntityType.Character => Character.CharacterList,
330  TargetPredicate.EntityType.Item => Item.ItemList,
331  TargetPredicate.EntityType.Structure => MapEntity.MapEntityList.Where(m => m is Structure),
332  TargetPredicate.EntityType.Hull => Hull.HullList,
333  TargetPredicate.EntityType.Submarine => Submarine.Loaded,
334  _ => Entity.GetEntities(),
335  };
336  foreach (Entity entity in entityList)
337  {
338  if (targetsToReturn.Contains(entity)) { continue; }
339  if (targetPredicate.Predicate(entity))
340  {
341  targetsToReturn.Add(entity);
342  }
343  }
344  }
345  }
346  foreach (WayPoint wayPoint in WayPoint.WayPointList)
347  {
348  if (wayPoint.Tags.Contains(tag)) { targetsToReturn.Add(wayPoint); }
349  }
350  if (Level.Loaded?.StartOutpost != null &&
351  Level.Loaded.StartOutpost.Info.OutpostNPCs.TryGetValue(tag, out List<Character> outpostNPCs))
352  {
353  foreach (Character npc in outpostNPCs)
354  {
355  if (npc.Removed || targetsToReturn.Contains(npc)) { continue; }
356  targetsToReturn.Add(npc);
357  }
358  }
359 
360  cachedTargets.Add(tag, targetsToReturn);
361  if (!initialAmounts.ContainsKey(tag))
362  {
363  initialAmounts.Add(tag, targetsToReturn.Count);
364  }
365  return targetsToReturn;
366  }
367 
368  public void InheritTags(Entity originalEntity, Entity newEntity)
369  {
370  foreach (var kvp in Targets)
371  {
372  if (kvp.Value.Contains(originalEntity))
373  {
374  kvp.Value.Add(newEntity);
375  }
376  }
377  }
378 
379  public void RemoveTag(Identifier tag)
380  {
381  if (tag.IsEmpty) { return; }
382  if (Targets.ContainsKey(tag)) { Targets.Remove(tag); }
383  if (cachedTargets.ContainsKey(tag)) { cachedTargets.Remove(tag); }
384  if (targetPredicates.ContainsKey(tag)) { targetPredicates.Remove(tag); }
385  }
386 
387  public override void Update(float deltaTime)
388  {
389  int botCount = 0;
390  int playerCount = 0;
391  foreach (Character c in Character.CharacterList)
392  {
393  if (c.IsPlayer)
394  {
395  playerCount++;
396  }
397  else if (c.IsBot)
398  {
399  botCount++;
400  }
401  }
402 
403  if (botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled || NeedsToRefreshCachedTargets())
404  {
405  cachedTargets.Clear();
406  newEntitySpawned = false;
407  prevBotCount = botCount;
408  prevPlayerCount = playerCount;
409  prevControlled = Character.Controlled;
410  }
411 
412  if (!Actions.Any())
413  {
414  Finish();
415  return;
416  }
417 
418  var currentAction = Actions[CurrentActionIndex];
419  if (!currentAction.CanBeFinished())
420  {
421  Finish();
422  return;
423  }
424 
425  string goTo = null;
426  if (currentAction.IsFinished(ref goTo))
427  {
428  if (string.IsNullOrEmpty(goTo))
429  {
431  }
432  else
433  {
434  CurrentActionIndex = -1;
435  Actions.ForEach(a => a.Reset());
436  for (int i = 0; i < Actions.Count; i++)
437  {
438  if (Actions[i].SetGoToTarget(goTo))
439  {
440  CurrentActionIndex = i;
441  break;
442  }
443  }
444  if (CurrentActionIndex == -1)
445  {
446  DebugConsole.AddWarning($"Could not find the GoTo label \"{goTo}\" in the event \"{Prefab.Identifier}\". Ending the event.",
448  }
449  }
450 
451  if (CurrentActionIndex >= Actions.Count || CurrentActionIndex < 0)
452  {
453  Finish();
454  }
455  }
456  else
457  {
458  currentAction.Update(deltaTime);
459  }
460  }
461 
462  private bool NeedsToRefreshCachedTargets()
463  {
464  if (newEntitySpawned) { return true; }
465  foreach (var cachedTargetList in cachedTargets.Values)
466  {
467  foreach (var target in cachedTargetList)
468  {
469  //one of the previously cached entities has been removed -> force refresh
470  if (target.Removed)
471  {
472  return true;
473  }
474  }
475  }
476  return false;
477  }
478 
479  public void EntitySpawned(Entity entity)
480  {
481  if (newEntitySpawned) { return; }
482  if (entity is Character character &&
483  Level.Loaded?.StartOutpost != null &&
484  Level.Loaded.StartOutpost.Info.OutpostNPCs.Values.Any(npcList => npcList.Contains(character)))
485  {
486  newEntitySpawned = true;
487  return;
488  }
489  //new entity matches one of the existing predicates -> force refresh
490  foreach (var targetPredicateList in targetPredicates.Values)
491  {
492  foreach (var targetPredicate in targetPredicateList)
493  {
494  if (targetPredicate.Predicate(entity))
495  {
496  newEntitySpawned = true;
497  return;
498  }
499  }
500  }
501  }
502 
503  public override bool LevelMeetsRequirements()
504  {
505  var currLocation = GameMain.GameSession?.Campaign?.Map.CurrentLocation;
506  if (currLocation?.Connections == null) { return true; }
507  foreach (LocationConnection c in currLocation.Connections)
508  {
509  if (RequireBeaconStation && !c.LevelData.HasBeaconStation) { continue; }
510 
511  var otherLocation = c.OtherLocation(currLocation);
512  if (!RequiredDestinationFaction.IsEmpty && otherLocation.Faction?.Prefab.Identifier != RequiredDestinationFaction) { continue; }
513 
514  if (requiredDestinationTypes.Contains(Tags.AnyOutpost) && otherLocation.HasOutpost() && otherLocation.Type.IsAnyOutpost) { return true; }
515  if (requiredDestinationTypes.Any(t => otherLocation.Type.Identifier == t))
516  {
517  return true;
518  }
519  }
520  return RequiredDestinationFaction.IsEmpty && requiredDestinationTypes.None();
521  }
522 
523 
524  public override void Finish()
525  {
526  base.Finish();
527  GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Finished:{CurrentActionIndex}");
528  }
529  }
530 }
Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys)
IEnumerable< ContentXElement > Elements()
bool GetAttributeBool(string key, bool def)
Identifier GetAttributeIdentifier(string key, string def)
Triggers a "conversation popup" with text and support for different branching options.
static IReadOnlyCollection< Entity > GetEntities()
Definition: Entity.cs:24
virtual IEnumerable< EventAction > GetSubActions()
Definition: EventAction.cs:133
static EventAction Instantiate(ScriptedEvent scriptedEvent, ContentXElement element)
Definition: EventAction.cs:140
bool IsFinished
Definition: Event.cs:25
readonly EventPrefab prefab
Definition: Event.cs:14
readonly ContentXElement ConfigElement
Definition: EventPrefab.cs:13
static GameSession?? GameSession
Definition: GameMain.cs:88
Makes the event jump to a Label somewhere else in the event.
Definition: GoTo.cs:7
string Name
Definition: GoTo.cs:9
static readonly List< Hull > HullList
static readonly List< Item > ItemList
Defines a point in the event that GoTo actions can jump to.
Definition: Label.cs:7
string Name
Definition: Label.cs:9
Location OtherLocation(Location location)
static readonly List< MapEntity > MapEntityList
Executes all the child actions when the round ends.
ContentPackage? ContentPackage
Definition: Prefab.cs:37
virtual IEnumerable< Identifier > NonActionChildElementNames
readonly Identifier RequiredDestinationFaction
override string ToString()
override string GetDebugInfo()
int GetInitialTargetCount(Identifier tag)
ScriptedEvent(EventPrefab prefab, int seed)
sealed record TargetPredicate(TargetPredicate.EntityType Type, Predicate< Entity > Predicate)
readonly OnRoundEndAction OnRoundEndAction
void InheritTags(Entity originalEntity, Entity newEntity)
override void Update(float deltaTime)
List<(int indent, EventAction action)> GetAllActions()
Finds all actions in the ScriptedEvent using a depth-first search (recursively going through the suba...
override void Finish()
virtual LocalizedString ReplaceVariablesInEventText(LocalizedString str)
readonly bool RequireBeaconStation
void RemoveTag(Identifier tag)
void AddTargetPredicate(Identifier tag, TargetPredicate.EntityType entityType, Predicate< Entity > predicate)
Dictionary< Identifier, List< Entity > > Targets
void EntitySpawned(Entity entity)
void AddTarget(Identifier tag, Entity target)
override bool LevelMeetsRequirements()
List< EventAction > Actions
IEnumerable< Entity > GetTargets(Identifier tag)
virtual string GetTextForReplacementElement(string tag)
readonly Dictionary< Identifier, List< Character > > OutpostNPCs