Server LuaCsForBarotrauma
MotionSensor.cs
1 using FarseerPhysics;
2 using Microsoft.Xna.Framework;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Xml.Linq;
8 
10 {
11  partial class MotionSensor : ItemComponent
12  {
13  private float rangeX, rangeY;
14 
15  private Vector2 detectOffset;
16 
17  private float updateTimer;
18 
19  [Flags]
20  public enum TargetType
21  {
22  Human = 1,
23  Monster = 2,
24  Wall = 4,
25  Pet = 8,
26  Any = Human | Monster | Wall | Pet,
27  }
28 
29  [Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
30  public bool MotionDetected { get; set; }
31 
32  [InGameEditable, Serialize(TargetType.Any, IsPropertySaveable.Yes, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)]
34  {
35  get;
36  set;
37  }
38 
39  [Editable, Serialize("", IsPropertySaveable.Yes, description: "Does the sensor react only to certain characters (species names, groups or tags)? Doesn't have an effect, if the Target Type is incorrect.", alwaysUseInstanceValues: true)]
40  public string TargetCharacters
41  {
42  get => targetCharacters.ConvertToString();
43  set => targetCharacters = value.ToIdentifiers().ToHashSet();
44  }
45  private HashSet<Identifier> targetCharacters;
46 
47  [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)]
48  public bool IgnoreDead
49  {
50  get;
51  set;
52  }
53 
54  [InGameEditable, Serialize(0.0f, IsPropertySaveable.Yes, description: "Horizontal detection range.", alwaysUseInstanceValues: true)]
55  public float RangeX
56  {
57  get { return rangeX; }
58  set
59  {
60  rangeX = MathHelper.Clamp(value, 0.0f, 1000.0f);
61 #if CLIENT
62  item.ResetCachedVisibleSize();
63 #endif
64  }
65  }
66  [InGameEditable, Serialize(0.0f, IsPropertySaveable.Yes, description: "Vertical movement detection range.", alwaysUseInstanceValues: true)]
67  public float RangeY
68  {
69  get { return rangeY; }
70  set
71  {
72  rangeY = MathHelper.Clamp(value, 0.0f, 1000.0f);
73  }
74  }
75 
76  [InGameEditable, Serialize("0,0", IsPropertySaveable.Yes, description: "The position to detect the movement at relative to the item. For example, 0,100 would detect movement 100 units above the item.")]
77  public Vector2 DetectOffset
78  {
79  get { return detectOffset; }
80  set
81  {
82  detectOffset = value;
83  detectOffset.X = MathHelper.Clamp(value.X, -rangeX, rangeX);
84  detectOffset.Y = MathHelper.Clamp(value.Y, -rangeY, rangeY);
85  }
86  }
87 
88  public Vector2 TransformedDetectOffset
89  {
90  get
91  {
92  Vector2 transformedDetectOffset = detectOffset;
93  if (item.FlippedX) { transformedDetectOffset.X = -transformedDetectOffset.X; }
94  if (item.FlippedY) { transformedDetectOffset.Y = -transformedDetectOffset.Y; }
95  return transformedDetectOffset;
96  }
97  }
98 
99  [Editable(MinValueFloat = 0.1f, MaxValueFloat = 100.0f, DecimalCount = 2), Serialize(0.1f, IsPropertySaveable.Yes, description: "How often the sensor checks if there's something moving near it. Higher values are better for performance.", alwaysUseInstanceValues: true)]
100  public float UpdateInterval
101  {
102  get;
103  set;
104  }
105 
106  private int maxOutputLength;
107  [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")]
108  public int MaxOutputLength
109  {
110  get { return maxOutputLength; }
111  set
112  {
113  maxOutputLength = Math.Max(value, 0);
114  }
115  }
116 
117  private string output;
118  [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)]
119  public string Output
120  {
121  get { return output; }
122  set
123  {
124  if (value == null) { return; }
125  output = value;
126  if (output.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading))
127  {
128  output = output.Substring(0, MaxOutputLength);
129  }
130  }
131  }
132 
133  private string falseOutput;
134  [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)]
135  public string FalseOutput
136  {
137  get { return falseOutput; }
138  set
139  {
140  if (value == null) { return; }
141  falseOutput = value;
142  if (falseOutput.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading))
143  {
144  falseOutput = falseOutput.Substring(0, MaxOutputLength);
145  }
146  }
147  }
148 
149  [InGameEditable(DecimalCount = 3), Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)]
150  public float MinimumVelocity
151  {
152  get;
153  set;
154  }
155 
156  [Serialize(true, IsPropertySaveable.Yes, description: "Should the sensor trigger when the item itself moves.")]
157  public bool DetectOwnMotion
158  {
159  get;
160  set;
161  }
162 
164  : base(item, element)
165  {
166  IsActive = true;
167 
168  //backwards compatibility
169  if (element.GetAttribute("range") != null)
170  {
171  rangeX = rangeY = element.GetAttributeFloat("range", 0.0f);
172  }
173 
174  //randomize update timer so all sensors aren't updated during the same frame
175  updateTimer = Rand.Range(0.0f, UpdateInterval);
176  }
177 
178  public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
179  {
180  base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
181  //backwards compatibility
182  if (componentElement.GetAttributeBool("onlyhumans", false))
183  {
184  Target = TargetType.Human;
185  }
186  }
187 
188  public override void Update(float deltaTime, Camera cam)
189  {
190  string signalOut = MotionDetected ? Output : FalseOutput;
191 
192  if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(new Signal(signalOut, 1), "state_out"); }
193 
194  if (MotionDetected)
195  {
196  ApplyStatusEffects(ActionType.OnUse, deltaTime);
197  }
198 
199  updateTimer -= deltaTime;
200  if (updateTimer > 0.0f) { return; }
201 
202  MotionDetected = false;
203  updateTimer = UpdateInterval;
204 
205  if (item.body != null && item.body.Enabled && DetectOwnMotion)
206  {
207  if (Math.Abs(item.body.LinearVelocity.X) > MinimumVelocity || Math.Abs(item.body.LinearVelocity.Y) > MinimumVelocity)
208  {
209  MotionDetected = true;
210  return;
211  }
212  }
213 
214  Vector2 detectPos = item.WorldPosition + TransformedDetectOffset;
215  Rectangle detectRect = new Rectangle((int)(detectPos.X - rangeX), (int)(detectPos.Y - rangeY), (int)(rangeX * 2), (int)(rangeY * 2));
216  float broadRangeX = Math.Max(rangeX * 2, 500);
217  float broadRangeY = Math.Max(rangeY * 2, 500);
218 
219  if (item.CurrentHull == null && item.Submarine != null && Target.HasFlag(TargetType.Wall))
220  {
221  if (Level.Loaded != null && (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity))
222  {
223  var cells = Level.Loaded.GetCells(item.WorldPosition, 1);
224  foreach (var cell in cells)
225  {
226  if (cell.IsPointInside(item.WorldPosition))
227  {
228  MotionDetected = true;
229  return;
230  }
231  foreach (var edge in cell.Edges)
232  {
233  Vector2 e1 = edge.Point1 + cell.Translation;
234  Vector2 e2 = edge.Point2 + cell.Translation;
235  if (MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.Right, detectRect.Y)) ||
236  MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Bottom), new Vector2(detectRect.Right, detectRect.Bottom)) ||
237  MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.X, detectRect.Bottom)) ||
238  MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.Right, detectRect.Y), new Vector2(detectRect.Right, detectRect.Bottom)))
239  {
240  MotionDetected = true;
241  return;
242  }
243  }
244  }
245  }
246  foreach (Submarine sub in Submarine.Loaded)
247  {
248  if (sub == item.Submarine) { continue; }
249 
250  Vector2 relativeVelocity = item.Submarine.Velocity - sub.Velocity;
251  if (Math.Abs(relativeVelocity.X) < MinimumVelocity && Math.Abs(relativeVelocity.Y) < MinimumVelocity) { continue; }
252 
253  Rectangle worldBorders = new Rectangle(
254  sub.Borders.X + (int)sub.WorldPosition.X,
255  sub.Borders.Y + (int)sub.WorldPosition.Y - sub.Borders.Height,
256  sub.Borders.Width,
257  sub.Borders.Height);
258 
259  if (worldBorders.Intersects(detectRect))
260  {
261  MotionDetected = true;
262  return;
263  }
264  }
265  }
266 
267  bool triggerFromHumans = Target.HasFlag(TargetType.Human);
268  bool triggerFromPets = Target.HasFlag(TargetType.Pet);
269  bool triggerFromMonsters = Target.HasFlag(TargetType.Monster);
270  bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters;
271  if (!hasTriggers) { return; }
272  foreach (Character character in Character.CharacterList)
273  {
274  //ignore characters that have spawned a second or less ago
275  //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground
276  if (character.SpawnTime > Timing.TotalTime - 1.0) { continue; }
277 
278  if (!TriggersOn(character)) { continue; }
279 
280  //do a rough check based on the position of the character's collider first
281  //before the more accurate limb-based check
282  if (Math.Abs(character.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(character.WorldPosition.Y - detectPos.Y) > broadRangeY)
283  {
284  continue;
285  }
286 
287  foreach (Limb limb in character.AnimController.Limbs)
288  {
289  if (limb.IsSevered) { continue; }
290  if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; }
291  if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect))
292  {
293  MotionDetected = true;
294  return;
295  }
296  }
297  }
298  }
299 
300  public bool TriggersOn(Character character)
301  {
302  bool triggerFromHumans = Target.HasFlag(TargetType.Human);
303  bool triggerFromPets = Target.HasFlag(TargetType.Pet);
304  bool triggerFromMonsters = Target.HasFlag(TargetType.Monster);
305  bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters;
306  if (!hasTriggers) { return false; }
307  return TriggersOn(character, triggerFromHumans, triggerFromPets, triggerFromMonsters);
308  }
309 
310  private bool TriggersOn(Character character, bool triggerFromHumans, bool triggerFromPets, bool triggerFromMonsters)
311  {
312  if (IgnoreDead && character.IsDead) { return false; }
313  if (character.IsHuman)
314  {
315  if (!triggerFromHumans) { return false; }
316  }
317  else if (character.IsPet)
318  {
319  if (!triggerFromPets) { return false; }
320  }
321  else
322  {
323  // Not a human or a pet -> monster?
324  if (!triggerFromMonsters) { return false; }
325  if (CharacterParams.CompareGroup(character.Group, CharacterPrefab.HumanGroup))
326  {
327  //characters in the "human" group aren't considered monsters (even if they were something like a friendly mudraptor)
328  return false;
329  }
330  }
331  // Check matching character, if defined.
332  if (targetCharacters.Any())
333  {
334  // Performance critical code -> using a foreach loop to avoid having to capture variables in lambdas.
335  bool matchFound = false;
336  foreach (Identifier target in targetCharacters)
337  {
338  if (character.MatchesSpeciesNameOrGroup(target) || character.Params.HasTag(target))
339  {
340  matchFound = true;
341  break;
342  }
343  }
344  if (!matchFound) { return false; }
345  }
346  return true;
347  }
348 
349  public override XElement Save(XElement parentElement)
350  {
351  Vector2 prevDetectOffset = detectOffset;
352  XElement element = base.Save(parentElement);
353  detectOffset = prevDetectOffset;
354  return element;
355  }
356  }
357 }
bool MatchesSpeciesNameOrGroup(Identifier speciesNameOrGroup)
bool HasTag(Identifier tag)
float GetAttributeFloat(string key, float def)
bool GetAttributeBool(string key, bool def)
XAttribute? GetAttribute(string name)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
double SpawnTime
Definition: Entity.cs:79
void SendSignal(string signal, string connectionName)
The base class for components holding the different functionalities of the item
void ApplyStatusEffects(ActionType type, float deltaTime, Character character=null, Limb targetLimb=null, Entity useTarget=null, Character user=null, Vector2? worldPosition=null, float afflictionMultiplier=1.0f)
MotionSensor(Item item, ContentXElement element)
override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
override void Update(float deltaTime, Camera cam)
override XElement Save(XElement parentElement)
bool TriggersOn(Character character)
List< VoronoiCell > GetCells(Vector2 worldPos, int searchDepth=2)
Vector2 LinearVelocity
Definition: Limb.cs:485
bool? IsSevered
Definition: Limb.cs:351
PhysicsBody body
Definition: Limb.cs:217
Vector2?? WorldPosition
Definition: Limb.cs:401
Rectangle? Borders
Extents of the solid items/structures (ones with a physics body) and hulls
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:26