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 string[] requiredDestinationTypes;
41  public readonly bool RequireBeaconStation;
42 
43  public int CurrentActionIndex { get; private set; }
44  public List<EventAction> Actions { get; } = new List<EventAction>();
45  public Dictionary<Identifier, List<Entity>> Targets { get; } = new Dictionary<Identifier, List<Entity>>();
46 
47  protected virtual IEnumerable<Identifier> NonActionChildElementNames => Enumerable.Empty<Identifier>();
48 
49  public override string ToString()
50  {
51  return $"{nameof(ScriptedEvent)} ({prefab.Identifier})";
52  }
53 
54  public ScriptedEvent(EventPrefab prefab, int seed) : base(prefab, seed)
55  {
56  foreach (var element in prefab.ConfigElement.Elements())
57  {
58  Identifier elementId = element.Name.ToIdentifier();
59  if (NonActionChildElementNames.Contains(elementId)) { continue; }
60  if (elementId == nameof(Barotrauma.OnRoundEndAction))
61  {
63  continue;
64  }
65  if (elementId == "statuseffect")
66  {
67  DebugConsole.ThrowError($"Error in event prefab \"{prefab.Identifier}\". Status effect configured as an action. Please configure status effects as child elements of a StatusEffectAction.",
68  contentPackage: prefab.ContentPackage);
69  continue;
70  }
71  var action = EventAction.Instantiate(this, element);
72  if (action != null) { Actions.Add(action); }
73  }
74 
75  if (!Actions.Any())
76  {
77  DebugConsole.ThrowError($"Scripted event \"{prefab.Identifier}\" has no actions. The event will do nothing.",
78  contentPackage: prefab.ContentPackage);
79  }
80 
81  requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null);
82  RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false);
83 
84  var allActions = GetAllActions().Select(a => a.action);
85  foreach (var gotoAction in allActions.OfType<GoTo>())
86  {
87  if (allActions.None(a => a is Label label && label.Name == gotoAction.Name))
88  {
89  DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Could not find a label matching the GoTo \"{gotoAction.Name}\".",
90  contentPackage: prefab.ContentPackage);
91  }
92  }
93 
94  GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start");
95  }
96 
97  public override string GetDebugInfo()
98  {
99  EventAction currentAction = !IsFinished ? Actions[CurrentActionIndex] : null;
100 
101  string text = $"Finished: {IsFinished.ColorizeObject()}\n" +
102  $"Action index: {CurrentActionIndex.ColorizeObject()}\n" +
103  $"Current action: {currentAction?.ToDebugString() ?? ToolBox.ColorizeObject(null)}\n";
104 
105  text += "All actions:\n";
106  text += GetAllActions().Aggregate(string.Empty, (current, action) => current + $"{new string(' ', action.indent * 6)}{action.action.ToDebugString()}\n");
107 
108  text += "Targets:\n";
109  foreach (var (key, value) in Targets)
110  {
111  text += $" {key.ColorizeObject()}: {value.Aggregate(string.Empty, (current, entity) => current + $"{entity.ColorizeObject()} ")}\n";
112  }
113  return text;
114  }
115 
116  public virtual string GetTextForReplacementElement(string tag)
117  {
118  if (tag.StartsWith("eventtag:"))
119  {
120  string targetTag = tag["eventtag:".Length..];
121  Entity target = GetTargets(targetTag.ToIdentifier()).FirstOrDefault();
122  if (target != null)
123  {
124  if (target is Item item) { return item.Name; }
125  if (target is Character character) { return character.Name; }
126  if (target is Hull hull) { return hull.DisplayName.Value; }
127  if (target is Submarine sub) { return sub.Info.DisplayName.Value; }
128  DebugConsole.AddWarning($"Failed to get the name of the event target {target} as a replacement for the tag {tag} in an event text.",
130  return target.ToString();
131  }
132  else
133  {
134  return $"[target \"{targetTag}\" not found]";
135  }
136  }
137  return string.Empty;
138  }
139 
141  {
142  return str;
143  }
144 
149  public List<(int indent, EventAction action)> GetAllActions()
150  {
151  var list = new List<(int indent, EventAction action)>();
152  foreach (EventAction eventAction in Actions)
153  {
154  list.AddRange(FindActionsRecursive(eventAction));
155  }
156  return list;
157 
158  static List<(int indent, EventAction action)> FindActionsRecursive(EventAction eventAction, int indent = 1)
159  {
160  var eventActions = new List<(int indent, EventAction action)> { (indent, eventAction) };
161  indent++;
162  foreach (var action in eventAction.GetSubActions())
163  {
164  eventActions.AddRange(FindActionsRecursive(action, indent));
165  }
166  return eventActions;
167  }
168  }
169 
170  public void AddTarget(Identifier tag, Entity target)
171  {
172  if (target == null)
173  {
174  throw new ArgumentException($"Target was null (tag: {tag})");
175  }
176  if (target.Removed)
177  {
178  throw new ArgumentException($"Target has been removed (tag: {tag})");
179  }
180  if (Targets.ContainsKey(tag))
181  {
182  if (!Targets[tag].Contains(target))
183  {
184  Targets[tag].Add(target);
185  }
186  }
187  else
188  {
189  Targets.Add(tag, new List<Entity>() { target });
190  }
191  if (cachedTargets.ContainsKey(tag))
192  {
193  if (!cachedTargets[tag].Contains(target))
194  {
195  cachedTargets[tag].Add(target);
196  }
197  }
198  else
199  {
200  cachedTargets.Add(tag, Targets[tag].ToList());
201  }
202  if (!initialAmounts.ContainsKey(tag))
203  {
204  initialAmounts.Add(tag, cachedTargets[tag].Count);
205  }
206  }
207 
208  public void AddTargetPredicate(Identifier tag, TargetPredicate.EntityType entityType, Predicate<Entity> predicate)
209  {
210  if (!targetPredicates.ContainsKey(tag))
211  {
212  targetPredicates.Add(tag, new List<TargetPredicate>());
213  }
214  targetPredicates[tag].Add(new TargetPredicate(entityType, predicate));
215  // force re-search for this tag
216  if (cachedTargets.ContainsKey(tag))
217  {
218  cachedTargets.Remove(tag);
219  }
220  }
221 
222  public int GetInitialTargetCount(Identifier tag)
223  {
224  if (initialAmounts.TryGetValue(tag, out int count))
225  {
226  return count;
227  }
228  return 0;
229  }
230 
231  public IEnumerable<Entity> GetTargets(Identifier tag)
232  {
233  if (cachedTargets.ContainsKey(tag))
234  {
235  if (cachedTargets[tag].Any(t => t.Removed))
236  {
237  cachedTargets.Clear();
238  }
239  else
240  {
241  return cachedTargets[tag];
242  }
243  }
244 
245  List<Entity> targetsToReturn = new List<Entity>();
246  if (Targets.ContainsKey(tag))
247  {
248  foreach (Entity e in Targets[tag])
249  {
250  if (e.Removed) { continue; }
251  targetsToReturn.Add(e);
252  }
253  }
254  if (targetPredicates.ContainsKey(tag))
255  {
256  foreach (var targetPredicate in targetPredicates[tag])
257  {
258  IEnumerable<Entity> entityList = targetPredicate.Type switch
259  {
260  TargetPredicate.EntityType.Character => Character.CharacterList,
261  TargetPredicate.EntityType.Item => Item.ItemList,
262  TargetPredicate.EntityType.Structure => MapEntity.MapEntityList.Where(m => m is Structure),
263  TargetPredicate.EntityType.Hull => Hull.HullList,
264  TargetPredicate.EntityType.Submarine => Submarine.Loaded,
265  _ => Entity.GetEntities(),
266  };
267  foreach (Entity entity in entityList)
268  {
269  if (targetsToReturn.Contains(entity)) { continue; }
270  if (targetPredicate.Predicate(entity))
271  {
272  targetsToReturn.Add(entity);
273  }
274  }
275  }
276  }
277  foreach (WayPoint wayPoint in WayPoint.WayPointList)
278  {
279  if (wayPoint.Tags.Contains(tag)) { targetsToReturn.Add(wayPoint); }
280  }
281  if (Level.Loaded?.StartOutpost != null &&
282  Level.Loaded.StartOutpost.Info.OutpostNPCs.TryGetValue(tag, out List<Character> outpostNPCs))
283  {
284  foreach (Character npc in outpostNPCs)
285  {
286  if (npc.Removed || targetsToReturn.Contains(npc)) { continue; }
287  targetsToReturn.Add(npc);
288  }
289  }
290 
291  cachedTargets.Add(tag, targetsToReturn);
292  if (!initialAmounts.ContainsKey(tag))
293  {
294  initialAmounts.Add(tag, targetsToReturn.Count);
295  }
296  return targetsToReturn;
297  }
298 
299  public void InheritTags(Entity originalEntity, Entity newEntity)
300  {
301  foreach (var kvp in Targets)
302  {
303  if (kvp.Value.Contains(originalEntity))
304  {
305  kvp.Value.Add(newEntity);
306  }
307  }
308  }
309 
310  public void RemoveTag(Identifier tag)
311  {
312  if (tag.IsEmpty) { return; }
313  if (Targets.ContainsKey(tag)) { Targets.Remove(tag); }
314  if (cachedTargets.ContainsKey(tag)) { cachedTargets.Remove(tag); }
315  if (targetPredicates.ContainsKey(tag)) { targetPredicates.Remove(tag); }
316  }
317 
318  public override void Update(float deltaTime)
319  {
320  int botCount = 0;
321  int playerCount = 0;
322  foreach (Character c in Character.CharacterList)
323  {
324  if (c.IsPlayer)
325  {
326  playerCount++;
327  }
328  else if (c.IsBot)
329  {
330  botCount++;
331  }
332  }
333 
334  if (botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled || NeedsToRefreshCachedTargets())
335  {
336  cachedTargets.Clear();
337  newEntitySpawned = false;
338  prevBotCount = botCount;
339  prevPlayerCount = playerCount;
340  prevControlled = Character.Controlled;
341  }
342 
343  if (!Actions.Any())
344  {
345  Finish();
346  return;
347  }
348 
349  var currentAction = Actions[CurrentActionIndex];
350  if (!currentAction.CanBeFinished())
351  {
352  Finish();
353  return;
354  }
355 
356  string goTo = null;
357  if (currentAction.IsFinished(ref goTo))
358  {
359  if (string.IsNullOrEmpty(goTo))
360  {
362  }
363  else
364  {
365  CurrentActionIndex = -1;
366  Actions.ForEach(a => a.Reset());
367  for (int i = 0; i < Actions.Count; i++)
368  {
369  if (Actions[i].SetGoToTarget(goTo))
370  {
371  CurrentActionIndex = i;
372  break;
373  }
374  }
375  if (CurrentActionIndex == -1)
376  {
377  DebugConsole.AddWarning($"Could not find the GoTo label \"{goTo}\" in the event \"{Prefab.Identifier}\". Ending the event.",
379  }
380  }
381 
382  if (CurrentActionIndex >= Actions.Count || CurrentActionIndex < 0)
383  {
384  Finish();
385  }
386  }
387  else
388  {
389  currentAction.Update(deltaTime);
390  }
391  }
392 
393  private bool NeedsToRefreshCachedTargets()
394  {
395  if (newEntitySpawned) { return true; }
396  foreach (var cachedTargetList in cachedTargets.Values)
397  {
398  foreach (var target in cachedTargetList)
399  {
400  //one of the previously cached entities has been removed -> force refresh
401  if (target.Removed)
402  {
403  return true;
404  }
405  }
406  }
407  return false;
408  }
409 
410  public void EntitySpawned(Entity entity)
411  {
412  if (newEntitySpawned) { return; }
413  if (entity is Character character &&
414  Level.Loaded?.StartOutpost != null &&
415  Level.Loaded.StartOutpost.Info.OutpostNPCs.Values.Any(npcList => npcList.Contains(character)))
416  {
417  newEntitySpawned = true;
418  return;
419  }
420  //new entity matches one of the existing predicates -> force refresh
421  foreach (var targetPredicateList in targetPredicates.Values)
422  {
423  foreach (var targetPredicate in targetPredicateList)
424  {
425  if (targetPredicate.Predicate(entity))
426  {
427  newEntitySpawned = true;
428  return;
429  }
430  }
431  }
432  }
433 
434  public override bool LevelMeetsRequirements()
435  {
436  if (requiredDestinationTypes == null) { return true; }
437  var currLocation = GameMain.GameSession?.Campaign?.Map.CurrentLocation;
438  if (currLocation?.Connections == null) { return true; }
439  foreach (LocationConnection c in currLocation.Connections)
440  {
441  if (RequireBeaconStation && !c.LevelData.HasBeaconStation) { continue; }
442  if (requiredDestinationTypes.Any(t => c.OtherLocation(currLocation).Type.Identifier == t))
443  {
444  return true;
445  }
446  }
447  return false;
448  }
449 
450  public override void Finish()
451  {
452  base.Finish();
453  GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Finished:{CurrentActionIndex}");
454  }
455  }
456 }
IEnumerable< ContentXElement > Elements()
bool GetAttributeBool(string key, bool def)
string?[] GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant=false)
static IReadOnlyCollection< Entity > GetEntities()
Definition: Entity.cs:24
virtual IEnumerable< EventAction > GetSubActions()
Definition: EventAction.cs:129
static EventAction Instantiate(ScriptedEvent scriptedEvent, ContentXElement element)
Definition: EventAction.cs:136
bool IsFinished
Definition: Event.cs:25
readonly EventPrefab prefab
Definition: Event.cs:14
readonly ContentXElement ConfigElement
Definition: EventPrefab.cs:11
static GameSession?? GameSession
Definition: GameMain.cs:88
Makes the event jump to a Label somewhere else in the event.
Definition: GoTo.cs:7
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)
LocationType Type
Definition: Location.cs:91
static readonly List< MapEntity > MapEntityList
Executes all the child actions when the round ends.
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
virtual IEnumerable< Identifier > NonActionChildElementNames
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 (recursively going through the subactions as well)....
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