Client LuaCsForBarotrauma
SpawnAction.cs
2 using Microsoft.Xna.Framework;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 
7 namespace Barotrauma
8 {
13  {
14  public enum SpawnLocationType
15  {
16  Any,
17  MainSub,
18  Outpost,
19  MainPath,
20  Ruin,
21  Wreck,
23  NearMainSub
24  }
25 
26  [Serialize("", IsPropertySaveable.Yes, description: "Species name of the character to spawn.")]
27  public Identifier SpeciesName { get; set; }
28 
29  [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the NPC set to choose from.")]
30  public Identifier NPCSetIdentifier { get; set; }
31 
32  [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the NPC.")]
33  public Identifier NPCIdentifier { get; set; }
34 
35  [Serialize(true, IsPropertySaveable.Yes, description: "Should taking the items of this npc be considered as stealing?")]
36  public bool LootingIsStealing { get; set; }
37 
38  [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item to spawn.")]
39  public Identifier ItemIdentifier { get; set; }
40 
41  [Serialize("", IsPropertySaveable.Yes, description: "The spawned entity will be assigned this tag. The tag can be used to refer to the entity by other actions of the event.")]
42  public Identifier TargetTag { get; set; }
43 
44  [Serialize("", IsPropertySaveable.Yes, description: "Tag of an entity with an inventory to spawn the item into.")]
45  public Identifier TargetInventory { get; set; }
46 
47  [Serialize(SpawnLocationType.Any, IsPropertySaveable.Yes, description: "Where should the entity spawn? This can be restricted further with the other spawn point options.")]
48  public SpawnLocationType SpawnLocation { get; set; }
49 
50  [Serialize(SpawnType.Human, IsPropertySaveable.Yes, description: "Type of spawnpoint to spawn the entity at. Ignored if SpawnPointTag is set.")]
51  public SpawnType SpawnPointType { get; set; }
52 
53  [Serialize("", IsPropertySaveable.Yes, description: "Tag of a spawnpoint to spawn the entity at.")]
54  public Identifier SpawnPointTag { get; set; }
55 
56  [Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes, description: "Team of the NPC to spawn. Only valid when spawning a character.")]
57  public CharacterTeamType TeamID { get; protected set; }
58 
59  [Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")]
60  public bool RequireSpawnPointTag { get; set; }
61 
62  private readonly HashSet<Identifier> targetModuleTags = new HashSet<Identifier>();
63 
64  [Serialize(true, IsPropertySaveable.Yes, description: "If false, we won't spawn another character if one with the same identifier has already been spawned.")]
65  public bool AllowDuplicates { get; set; }
66 
67  [Serialize(1, IsPropertySaveable.Yes, description: "Number of entities to spawn.")]
68  public int Amount { get; set; }
69 
70  [Serialize(100.0f, IsPropertySaveable.Yes, description: "Random offset to add to the spawn position.")]
71  public float Offset { get; set; }
72 
73  [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the entity prefer to spawn in.")]
74  public string TargetModuleTags
75  {
76  get => string.Join(",", targetModuleTags);
77  set
78  {
79  targetModuleTags.Clear();
80  if (!string.IsNullOrWhiteSpace(value))
81  {
82  string[] splitTags = value.Split(',');
83  foreach (var s in splitTags)
84  {
85  targetModuleTags.Add(s.ToIdentifier());
86  }
87  }
88  }
89  }
90 
91  [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI ignore this item. This will prevent outpost NPCs cleaning up or otherwise using important items intended to be left for the players.")]
92  public bool IgnoreByAI { get; set; }
93 
94  [Serialize(true, IsPropertySaveable.Yes, description: "If disabled, the action will choose a spawn position away from players' views if one is available.")]
95  public bool AllowInPlayerView { get; set; }
96 
97  private bool spawned;
98  private Entity spawnedEntity;
99 
100  private readonly bool ignoreSpawnPointType;
101 
102  public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
103  {
104  ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null;
105  //backwards compatibility
106  TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum("team", TeamID));
107  if (element.GetAttribute("submarinetype") != null)
108  {
109  DebugConsole.ThrowError(
110  $"Error in even \"{(parentEvent.Prefab?.Identifier.ToString() ?? "unknown")}\". " +
111  $"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?",
112  contentPackage: ParentEvent.Prefab.ContentPackage);
113  }
114  }
115 
116  public override bool IsFinished(ref string goTo)
117  {
118  if (spawnedEntity != null)
119  {
120  return true;
121  }
122  else
123  {
124  return false;
125  }
126  }
127 
128  public override void Reset()
129  {
130  spawned = false;
131  spawnedEntity = null;
132  }
133 
134  public override void Update(float deltaTime)
135  {
136  if (spawned) { return; }
137 
138  if (!NPCSetIdentifier.IsEmpty && !NPCIdentifier.IsEmpty)
139  {
140  HumanPrefab humanPrefab = null;
141  if (Level.Loaded?.StartLocation is Location startLocation)
142  {
143  humanPrefab =
144  TryFindHumanPrefab(startLocation.Faction) ??
145  TryFindHumanPrefab(startLocation.SecondaryFaction);
146  }
147  HumanPrefab TryFindHumanPrefab(Faction faction)
148  {
149  if (faction == null) { return null; }
150  return
151  NPCSet.Get(NPCSetIdentifier,
152  NPCIdentifier.Replace("[faction]".ToIdentifier(), faction.Prefab.Identifier),
153  logError: false) ??
154  //try to spawn a coalition NPC if a correct one can't be found
155  NPCSet.Get(NPCSetIdentifier,
156  NPCIdentifier.Replace("[faction]".ToIdentifier(), "coalition".ToIdentifier()),
157  logError: false);
158  }
159 
160  humanPrefab ??= NPCSet.Get(NPCSetIdentifier, NPCIdentifier, logError: true);
161 
162  if (humanPrefab != null)
163  {
164  if (!AllowDuplicates &&
165  Character.CharacterList.Any(c => c.Info?.HumanPrefabIds.NpcIdentifier == NPCIdentifier && c.Info?.HumanPrefabIds.NpcSetIdentifier == NPCSetIdentifier))
166  {
167  spawned = true;
168  return;
169  }
170  ISpatialEntity spawnPos = GetSpawnPos();
171  if (spawnPos != null)
172  {
173  for (int i = 0; i < Amount; i++)
174  {
175  Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter =>
176  {
177  if (newCharacter == null) { return; }
178  newCharacter.HumanPrefab = humanPrefab;
179  newCharacter.TeamID = TeamID;
180  newCharacter.EnableDespawn = false;
181  humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint);
182  if (LootingIsStealing)
183  {
184  foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true))
185  {
186  item.SpawnedInCurrentOutpost = true;
187  item.AllowStealing = false;
188  }
189  }
190  humanPrefab.InitializeCharacter(newCharacter, spawnPos);
191  if (!TargetTag.IsEmpty && newCharacter != null)
192  {
193  ParentEvent.AddTarget(TargetTag, newCharacter);
194  }
195  spawnedEntity = newCharacter;
196  if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
197  {
198  outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier);
199  foreach (Identifier tag in humanPrefab.GetTags())
200  {
201  outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag);
202  }
203  }
204 #if SERVER
205  newCharacter.LoadTalents();
206  GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData());
207 #endif
208  });
209  }
210  }
211  }
212  }
213  else if (!SpeciesName.IsEmpty)
214  {
215  if (!AllowDuplicates && Character.CharacterList.Any(c => c.SpeciesName == SpeciesName))
216  {
217  spawned = true;
218  return;
219  }
220  ISpatialEntity spawnPos = GetSpawnPos();
221  if (spawnPos != null)
222  {
223  for (int i = 0; i < Amount; i++)
224  {
225  Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter =>
226  {
227  if (!TargetTag.IsEmpty && newCharacter != null)
228  {
229  ParentEvent.AddTarget(TargetTag, newCharacter);
230  }
231  spawnedEntity = newCharacter;
232  });
233  }
234  }
235  }
236  else if (!ItemIdentifier.IsEmpty)
237  {
238  if (MapEntityPrefab.FindByIdentifier(ItemIdentifier) is not ItemPrefab itemPrefab)
239  {
240  DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)",
241  contentPackage: ParentEvent.Prefab.ContentPackage);
242  }
243  else
244  {
245  Inventory spawnInventory = null;
246  if (!TargetInventory.IsEmpty)
247  {
248  var targets = ParentEvent.GetTargets(TargetInventory);
249  if (targets.Any())
250  {
251  var target = targets.First(t => t is Item || t is Character);
252  if (target is Character character)
253  {
254  spawnInventory = character.Inventory;
255  }
256  else if (target is Item item)
257  {
258  spawnInventory = item.OwnInventory;
259  }
260  }
261 
262  if (spawnInventory == null)
263  {
264  DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found.",
265  contentPackage: ParentEvent.Prefab.ContentPackage);
266  }
267  }
268 
269  if (spawnInventory == null)
270  {
271  ISpatialEntity spawnPos = GetSpawnPos();
272  if (spawnPos != null)
273  {
274  for (int i = 0; i < Amount; i++)
275  {
276  Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned);
277  }
278  }
279  }
280  else
281  {
282  for (int i = 0; i < Amount; i++)
283  {
284  Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned);
285 
286  }
287  }
288  void onSpawned(Item newItem)
289  {
290  if (newItem != null)
291  {
292  if (!TargetTag.IsEmpty)
293  {
294  ParentEvent.AddTarget(TargetTag, newItem);
295  }
296  if (IgnoreByAI)
297  {
298  newItem.AddTag("ignorebyai");
299  }
300  }
301  spawnedEntity = newItem;
302  }
303  }
304  }
305 
306  spawned = true;
307  }
308 
309  public static Vector2 OffsetSpawnPos(Vector2 pos, float offset)
310  {
311  Hull hull = Hull.FindHull(pos);
312  pos += Rand.Vector(offset);
313  if (hull != null)
314  {
315  float margin = 50.0f;
316  pos = new Vector2(
317  MathHelper.Clamp(pos.X, hull.WorldRect.X + margin, hull.WorldRect.Right - margin),
318  MathHelper.Clamp(pos.Y, hull.WorldRect.Y - hull.WorldRect.Height + margin, hull.WorldRect.Y - margin));
319  }
320  return pos;
321  }
322 
323  private ISpatialEntity GetSpawnPos()
324  {
325  if (!SpawnPointTag.IsEmpty)
326  {
327  IEnumerable<Item> potentialItems = Item.ItemList.Where(it => IsValidSubmarineType(SpawnLocation, it.Submarine));
328  if (!AllowInPlayerView)
329  {
330  potentialItems = GetEntitiesNotInPlayerView(potentialItems);
331  }
332  var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced();
333  if (item != null) { return item; }
334 
335  var potentialTargets = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine));
336  if (!AllowInPlayerView)
337  {
338  potentialTargets = GetEntitiesNotInPlayerView(potentialTargets);
339  }
340  var target = potentialTargets.GetRandomUnsynced();
341  if (target != null) { return target; }
342  }
343 
344  SpawnType? spawnPointType = null;
345  if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; }
346 
347  return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag, allowInPlayerView: AllowInPlayerView);
348  }
349 
350  private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine)
351  {
352  return spawnLocation switch
353  {
354  SpawnLocationType.Any => true,
355  SpawnLocationType.MainSub => submarine == Submarine.MainSub,
356  SpawnLocationType.NearMainSub => submarine == null,
357  SpawnLocationType.MainPath => submarine == null,
358  SpawnLocationType.Outpost => submarine is { Info.IsOutpost: true },
359  SpawnLocationType.Wreck => submarine is { Info.IsWreck: true },
360  SpawnLocationType.Ruin => submarine is { Info.IsRuin: true },
361  SpawnLocationType.BeaconStation => submarine?.Info?.BeaconStationInfo != null,
362  _ => throw new NotImplementedException(),
363  };
364  }
365 
369  private static IEnumerable<T> GetEntitiesNotInPlayerView<T>(IEnumerable<T> entities) where T : ISpatialEntity
370  {
371  if (entities.Any(e => !IsInPlayerView(e)))
372  {
373  return entities.Where(e => !IsInPlayerView(e));
374  }
375  return entities;
376  }
377 
378  private static bool IsInPlayerView(ISpatialEntity entity)
379  {
380  foreach (var character in Character.CharacterList)
381  {
382  if (!character.IsPlayer || character.IsDead) { continue; }
383  if (character.CanSeeTarget(entity)) { return true; }
384  }
385  return false;
386  }
387 
388  public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false, bool allowInPlayerView = true)
389  {
390  bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost;
391  List<WayPoint> potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull));
392  potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && wp.IsTraversable);
393  if (moduleFlags != null && moduleFlags.Any())
394  {
395  var spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull is Hull h && h.OutpostModuleTags.Any(moduleFlags.Contains));
396  if (spawnPoints.Any())
397  {
398  potentialSpawnPoints = spawnPoints.ToList();
399  }
400  }
401  if (spawnpointTags != null && spawnpointTags.Any())
402  {
403  var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable));
404  if (requireTaggedSpawnPoint || spawnPoints.Any())
405  {
406  potentialSpawnPoints = spawnPoints.ToList();
407  }
408  }
409  if (potentialSpawnPoints.None())
410  {
411  if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any())
412  {
413  DebugConsole.NewMessage($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation} (tag: {string.Join(",", spawnpointTags)}), skipping.", color: Color.White);
414  }
415  else
416  {
417  DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})");
418  }
419  return null;
420  }
421 
422  IEnumerable<WayPoint> validSpawnPoints;
423  if (spawnPointType.HasValue)
424  {
425  validSpawnPoints = potentialSpawnPoints.FindAll(wp => spawnPointType.Value.HasFlag(wp.SpawnType));
426  }
427  else
428  {
429  validSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.SpawnType != SpawnType.Path);
430  if (!validSpawnPoints.Any()) { validSpawnPoints = potentialSpawnPoints; }
431  }
432 
433  //don't spawn in an airlock module if there are other options
434  var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false);
435  if (airlockSpawnPoints.Count() < validSpawnPoints.Count())
436  {
437  validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints);
438  }
439 
440  if (validSpawnPoints.None())
441  {
442  DebugConsole.ThrowError($"Could not find a spawn point of the correct type for a SpawnAction (spawn location: {spawnLocation}, type: {spawnPointType}, module flags: {((moduleFlags == null || !moduleFlags.Any()) ? "none" : string.Join(", ", moduleFlags))})");
443  return potentialSpawnPoints.GetRandomUnsynced();
444  }
445 
446  if (spawnLocation == SpawnLocationType.MainPath || spawnLocation == SpawnLocationType.NearMainSub)
447  {
448  validSpawnPoints = validSpawnPoints.Where(p =>
449  Submarine.Loaded.None(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(p.WorldPosition)));
450  }
451 
452  //avoid using waypoints if there's any actual spawnpoints available
453  if (validSpawnPoints.Any(wp => wp.SpawnType != SpawnType.Path))
454  {
455  validSpawnPoints = validSpawnPoints.Where(wp => wp.SpawnType != SpawnType.Path);
456  }
457 
458  //if not trying to spawn at a tagged spawnpoint, favor spawnpoints without tags
459  if (spawnpointTags == null || spawnpointTags.None())
460  {
461  var spawnPoints = validSpawnPoints.Where(wp => !wp.Tags.Any());
462  if (spawnPoints.Any())
463  {
464  validSpawnPoints = spawnPoints.ToList();
465  }
466  }
467 
468  if (!allowInPlayerView)
469  {
470  validSpawnPoints = GetEntitiesNotInPlayerView(validSpawnPoints);
471  }
472 
473  if (spawnLocation == SpawnLocationType.NearMainSub && Submarine.MainSub != null)
474  {
475  WayPoint closestPoint = validSpawnPoints.First();
476  float closestDist = float.PositiveInfinity;
477  foreach (WayPoint wp in validSpawnPoints)
478  {
479  float dist = Vector2.DistanceSquared(wp.WorldPosition, Submarine.MainSub.WorldPosition);
480  if (dist < closestDist)
481  {
482  closestDist = dist;
483  closestPoint = wp;
484  }
485  }
486  return closestPoint;
487  }
488  else if (asFarAsPossibleFromAirlock && airlockSpawnPoints.Any())
489  {
490  WayPoint furthestPoint = validSpawnPoints.First();
491  float furthestDist = 0.0f;
492  foreach (WayPoint waypoint in validSpawnPoints)
493  {
494  float dist = Vector2.DistanceSquared(waypoint.WorldPosition, airlockSpawnPoints.First().WorldPosition);
495  if (dist > furthestDist)
496  {
497  furthestDist = dist;
498  furthestPoint = waypoint;
499  }
500  }
501  return furthestPoint;
502  }
503  else
504  {
505  return validSpawnPoints.GetRandomUnsynced();
506  }
507  }
508 
509  public override string ToDebugString()
510  {
511  return $"{ToolBox.GetDebugSymbol(spawned)} {nameof(SpawnAction)} -> (Spawned entity: {spawnedEntity.ColorizeObject()})";
512  }
513  }
514 }
static readonly Identifier HumanSpeciesName
XAttribute? GetAttribute(string name)
static EntitySpawner Spawner
Definition: Entity.cs:31
virtual Vector2 WorldPosition
Definition: Entity.cs:49
void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, Action< Character > onSpawn=null)
readonly ScriptedEvent ParentEvent
Definition: EventAction.cs:102
EventPrefab Prefab
Definition: Event.cs:16
FactionPrefab Prefab
Definition: Factions.cs:18
static NetworkMember NetworkMember
Definition: GameMain.cs:190
IEnumerable< Identifier > OutpostModuleTags
Inherited flags from outpost generation.
static Hull FindHull(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any)
CharacterInfo CreateCharacterInfo(Rand.RandSync randSync=Rand.RandSync.Unsynced)
Creates a character info from the human prefab. If there are custom character infos defined,...
Definition: HumanPrefab.cs:228
void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn=null)
Definition: HumanPrefab.cs:164
bool GiveItems(Character character, Submarine submarine, WayPoint spawnPoint, Rand.RandSync randSync=Rand.RandSync.Unsynced, bool createNetworkEvents=true)
Definition: HumanPrefab.cs:205
static readonly List< Item > ItemList
ContentPackage? ContentPackage
Definition: Prefab.cs:37
Spawns an entity (e.g. item, NPC, monster).
Definition: SpawnAction.cs:13
static Vector2 OffsetSpawnPos(Vector2 pos, float offset)
Definition: SpawnAction.cs:309
SpawnType SpawnPointType
Definition: SpawnAction.cs:51
override bool IsFinished(ref string goTo)
Has the action finished.
Definition: SpawnAction.cs:116
Identifier SpeciesName
Definition: SpawnAction.cs:27
override string ToDebugString()
Rich test to display in debugdraw
Definition: SpawnAction.cs:509
Identifier SpawnPointTag
Definition: SpawnAction.cs:54
Identifier NPCIdentifier
Definition: SpawnAction.cs:33
Identifier ItemIdentifier
Definition: SpawnAction.cs:39
Identifier NPCSetIdentifier
Definition: SpawnAction.cs:30
CharacterTeamType TeamID
Definition: SpawnAction.cs:57
override void Reset()
Definition: SpawnAction.cs:128
SpawnAction(ScriptedEvent parentEvent, ContentXElement element)
Definition: SpawnAction.cs:102
static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable< Identifier > moduleFlags=null, IEnumerable< Identifier > spawnpointTags=null, bool asFarAsPossibleFromAirlock=false, bool requireTaggedSpawnPoint=false, bool allowInPlayerView=true)
Definition: SpawnAction.cs:388
override void Update(float deltaTime)
Definition: SpawnAction.cs:134
Identifier TargetInventory
Definition: SpawnAction.cs:45
SpawnLocationType SpawnLocation
Definition: SpawnAction.cs:48