1
0

StatusEffectsSystem.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. using System.Diagnostics.CodeAnalysis;
  2. using Content.Shared.Alert;
  3. using Content.Shared.Rejuvenate;
  4. using Robust.Shared.GameStates;
  5. using Robust.Shared.Prototypes;
  6. using Robust.Shared.Timing;
  7. using Robust.Shared.Utility;
  8. namespace Content.Shared.StatusEffect
  9. {
  10. public sealed class StatusEffectsSystem : EntitySystem
  11. {
  12. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  13. [Dependency] private readonly IComponentFactory _componentFactory = default!;
  14. [Dependency] private readonly IGameTiming _gameTiming = default!;
  15. [Dependency] private readonly AlertsSystem _alertsSystem = default!;
  16. private List<EntityUid> _toRemove = new();
  17. public override void Initialize()
  18. {
  19. base.Initialize();
  20. UpdatesOutsidePrediction = true;
  21. SubscribeLocalEvent<StatusEffectsComponent, ComponentGetState>(OnGetState);
  22. SubscribeLocalEvent<StatusEffectsComponent, ComponentHandleState>(OnHandleState);
  23. SubscribeLocalEvent<StatusEffectsComponent, RejuvenateEvent>(OnRejuvenate);
  24. }
  25. public override void Update(float frameTime)
  26. {
  27. base.Update(frameTime);
  28. var curTime = _gameTiming.CurTime;
  29. var enumerator = EntityQueryEnumerator<ActiveStatusEffectsComponent, StatusEffectsComponent>();
  30. _toRemove.Clear();
  31. while (enumerator.MoveNext(out var uid, out _, out var status))
  32. {
  33. if (status.ActiveEffects.Count == 0)
  34. {
  35. // This shouldn't happen, but just in case something sneaks through
  36. _toRemove.Add(uid);
  37. continue;
  38. }
  39. foreach (var state in status.ActiveEffects)
  40. {
  41. if (curTime > state.Value.Cooldown.Item2)
  42. TryRemoveStatusEffect(uid, state.Key, status);
  43. }
  44. }
  45. foreach (var uid in _toRemove)
  46. {
  47. RemComp<ActiveStatusEffectsComponent>(uid);
  48. }
  49. }
  50. private void OnGetState(EntityUid uid, StatusEffectsComponent component, ref ComponentGetState args)
  51. {
  52. // Using new(...) To avoid mispredictions due to MergeImplicitData. This will mean the server-side code is
  53. // slightly slower, and really this function should just be overridden by the client...
  54. args.State = new StatusEffectsComponentState(new(component.ActiveEffects), new(component.AllowedEffects));
  55. }
  56. private void OnHandleState(EntityUid uid, StatusEffectsComponent component, ref ComponentHandleState args)
  57. {
  58. if (args.Current is not StatusEffectsComponentState state)
  59. return;
  60. component.AllowedEffects.Clear();
  61. component.AllowedEffects.AddRange(state.AllowedEffects);
  62. // Remove non-existent effects.
  63. foreach (var key in component.ActiveEffects.Keys)
  64. {
  65. if (!state.ActiveEffects.ContainsKey(key))
  66. component.ActiveEffects.Remove(key);
  67. }
  68. foreach (var (key, effect) in state.ActiveEffects)
  69. {
  70. component.ActiveEffects[key] = new(effect);
  71. }
  72. if (component.ActiveEffects.Count == 0)
  73. RemComp<ActiveStatusEffectsComponent>(uid);
  74. else
  75. EnsureComp<ActiveStatusEffectsComponent>(uid);
  76. }
  77. private void OnRejuvenate(EntityUid uid, StatusEffectsComponent component, RejuvenateEvent args)
  78. {
  79. TryRemoveAllStatusEffects(uid, component);
  80. }
  81. /// <summary>
  82. /// Tries to add a status effect to an entity, with a given component added as well.
  83. /// </summary>
  84. /// <param name="uid">The entity to add the effect to.</param>
  85. /// <param name="key">The status effect ID to add.</param>
  86. /// <param name="time">How long the effect should last for.</param>
  87. /// <param name="refresh">The status effect cooldown should be refreshed (true) or accumulated (false).</param>
  88. /// <param name="status">The status effects component to change, if you already have it.</param>
  89. /// <returns>False if the effect could not be added or the component already exists, true otherwise.</returns>
  90. /// <typeparam name="T">The component type to add and remove from the entity.</typeparam>
  91. public bool TryAddStatusEffect<T>(EntityUid uid, string key, TimeSpan time, bool refresh,
  92. StatusEffectsComponent? status = null)
  93. where T : IComponent, new()
  94. {
  95. if (!Resolve(uid, ref status, false))
  96. return false;
  97. if (!TryAddStatusEffect(uid, key, time, refresh, status))
  98. return false;
  99. if (HasComp<T>(uid))
  100. return true;
  101. EntityManager.AddComponent<T>(uid);
  102. status.ActiveEffects[key].RelevantComponent = _componentFactory.GetComponentName<T>();
  103. return true;
  104. }
  105. public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool refresh, string component,
  106. StatusEffectsComponent? status = null)
  107. {
  108. if (!Resolve(uid, ref status, false))
  109. return false;
  110. if (TryAddStatusEffect(uid, key, time, refresh, status))
  111. {
  112. // If they already have the comp, we just won't bother updating anything.
  113. if (!EntityManager.HasComponent(uid, _componentFactory.GetRegistration(component).Type))
  114. {
  115. var newComponent = (Component) _componentFactory.GetComponent(component);
  116. EntityManager.AddComponent(uid, newComponent);
  117. status.ActiveEffects[key].RelevantComponent = component;
  118. }
  119. return true;
  120. }
  121. return false;
  122. }
  123. /// <summary>
  124. /// Tries to add a status effect to an entity with a certain timer.
  125. /// </summary>
  126. /// <param name="uid">The entity to add the effect to.</param>
  127. /// <param name="key">The status effect ID to add.</param>
  128. /// <param name="time">How long the effect should last for.</param>
  129. /// <param name="refresh">The status effect cooldown should be refreshed (true) or accumulated (false).</param>
  130. /// <param name="status">The status effects component to change, if you already have it.</param>
  131. /// <param name="startTime">The time at which the status effect started. This exists mostly for prediction
  132. /// resetting.</param>
  133. /// <returns>False if the effect could not be added, or if the effect already existed.</returns>
  134. /// <remarks>
  135. /// This obviously does not add any actual 'effects' on its own. Use the generic overload,
  136. /// which takes in a component type, if you want to automatically add and remove a component.
  137. ///
  138. /// If the effect already exists, it will simply replace the cooldown with the new one given.
  139. /// If you want special 'effect merging' behavior, do it your own damn self!
  140. /// </remarks>
  141. public bool TryAddStatusEffect(EntityUid uid,
  142. string key,
  143. TimeSpan time,
  144. bool refresh,
  145. StatusEffectsComponent? status = null,
  146. TimeSpan? startTime = null)
  147. {
  148. if (!Resolve(uid, ref status, false))
  149. return false;
  150. if (!CanApplyEffect(uid, key, status))
  151. return false;
  152. // we already checked if it has the index in CanApplyEffect so a straight index and not tryindex here
  153. // is fine
  154. var proto = _prototypeManager.Index<StatusEffectPrototype>(key);
  155. var start = startTime ?? _gameTiming.CurTime;
  156. (TimeSpan, TimeSpan) cooldown = (start, start + time);
  157. if (HasStatusEffect(uid, key, status))
  158. {
  159. status.ActiveEffects[key].CooldownRefresh = refresh;
  160. if (refresh)
  161. {
  162. //Making sure we don't reset a longer cooldown by applying a shorter one.
  163. if ((status.ActiveEffects[key].Cooldown.Item2 - _gameTiming.CurTime) < time)
  164. {
  165. //Refresh cooldown time.
  166. status.ActiveEffects[key].Cooldown = cooldown;
  167. }
  168. }
  169. else
  170. {
  171. //Accumulate cooldown time.
  172. status.ActiveEffects[key].Cooldown.Item2 += time;
  173. }
  174. }
  175. else
  176. {
  177. status.ActiveEffects.Add(key, new StatusEffectState(cooldown, refresh, null));
  178. EnsureComp<ActiveStatusEffectsComponent>(uid);
  179. }
  180. if (proto.Alert != null)
  181. {
  182. var cooldown1 = GetAlertCooldown(uid, proto.Alert.Value, status);
  183. _alertsSystem.ShowAlert(uid, proto.Alert.Value, null, cooldown1);
  184. }
  185. Dirty(uid, status);
  186. RaiseLocalEvent(uid, new StatusEffectAddedEvent(uid, key));
  187. return true;
  188. }
  189. /// <summary>
  190. /// Finds the maximum cooldown among all status effects with the same alert
  191. /// </summary>
  192. /// <remarks>
  193. /// This is mostly for stuns, since Stun and Knockdown share an alert key. Other times this pretty much
  194. /// will not be useful.
  195. /// </remarks>
  196. private (TimeSpan, TimeSpan)? GetAlertCooldown(EntityUid uid, ProtoId<AlertPrototype> alert, StatusEffectsComponent status)
  197. {
  198. (TimeSpan, TimeSpan)? maxCooldown = null;
  199. foreach (var kvp in status.ActiveEffects)
  200. {
  201. var proto = _prototypeManager.Index<StatusEffectPrototype>(kvp.Key);
  202. if (proto.Alert == alert)
  203. {
  204. if (maxCooldown == null || kvp.Value.Cooldown.Item2 > maxCooldown.Value.Item2)
  205. {
  206. maxCooldown = kvp.Value.Cooldown;
  207. }
  208. }
  209. }
  210. return maxCooldown;
  211. }
  212. /// <summary>
  213. /// Attempts to remove a status effect from an entity.
  214. /// </summary>
  215. /// <param name="uid">The entity to remove an effect from.</param>
  216. /// <param name="key">The effect ID to remove.</param>
  217. /// <param name="status">The status effects component to change, if you already have it.</param>
  218. /// <param name="remComp">If true, status effect removal will also remove the relevant component. This option
  219. /// exists mostly for prediction resetting.</param>
  220. /// <returns>False if the effect could not be removed, true otherwise.</returns>
  221. /// <remarks>
  222. /// Obviously this doesn't automatically clear any effects a status effect might have.
  223. /// That's up to the removed component to handle itself when it's removed.
  224. /// </remarks>
  225. public bool TryRemoveStatusEffect(EntityUid uid, string key,
  226. StatusEffectsComponent? status = null, bool remComp = true)
  227. {
  228. if (!Resolve(uid, ref status, false))
  229. return false;
  230. if (!status.ActiveEffects.ContainsKey(key))
  231. return false;
  232. if (!_prototypeManager.TryIndex<StatusEffectPrototype>(key, out var proto))
  233. return false;
  234. var state = status.ActiveEffects[key];
  235. // There are cases where a status effect component might be server-only, so TryGetRegistration...
  236. if (remComp
  237. && state.RelevantComponent != null
  238. && _componentFactory.TryGetRegistration(state.RelevantComponent, out var registration))
  239. {
  240. var type = registration.Type;
  241. EntityManager.RemoveComponent(uid, type);
  242. }
  243. if (proto.Alert != null)
  244. {
  245. _alertsSystem.ClearAlert(uid, proto.Alert.Value);
  246. }
  247. status.ActiveEffects.Remove(key);
  248. if (status.ActiveEffects.Count == 0)
  249. {
  250. RemComp<ActiveStatusEffectsComponent>(uid);
  251. }
  252. Dirty(uid, status);
  253. RaiseLocalEvent(uid, new StatusEffectEndedEvent(uid, key));
  254. return true;
  255. }
  256. /// <summary>
  257. /// Tries to remove all status effects from a given entity.
  258. /// </summary>
  259. /// <param name="uid">The entity to remove effects from.</param>
  260. /// <param name="status">The status effects component to change, if you already have it.</param>
  261. /// <returns>False if any status effects failed to be removed, true if they all did.</returns>
  262. public bool TryRemoveAllStatusEffects(EntityUid uid,
  263. StatusEffectsComponent? status = null)
  264. {
  265. if (!Resolve(uid, ref status, false))
  266. return false;
  267. bool failed = false;
  268. foreach (var effect in status.ActiveEffects)
  269. {
  270. if (!TryRemoveStatusEffect(uid, effect.Key, status))
  271. failed = true;
  272. }
  273. Dirty(uid, status);
  274. return failed;
  275. }
  276. /// <summary>
  277. /// Returns whether a given entity has the status effect active.
  278. /// </summary>
  279. /// <param name="uid">The entity to check on.</param>
  280. /// <param name="key">The status effect ID to check for</param>
  281. /// <param name="status">The status effect component, should you already have it.</param>
  282. public bool HasStatusEffect(EntityUid uid, string key,
  283. StatusEffectsComponent? status = null)
  284. {
  285. if (!Resolve(uid, ref status, false))
  286. return false;
  287. if (!status.ActiveEffects.ContainsKey(key))
  288. return false;
  289. return true;
  290. }
  291. /// <summary>
  292. /// Returns whether a given entity can have a given effect applied to it.
  293. /// </summary>
  294. /// <param name="uid">The entity to check on.</param>
  295. /// <param name="key">The status effect ID to check for</param>
  296. /// <param name="status">The status effect component, should you already have it.</param>
  297. public bool CanApplyEffect(EntityUid uid, string key, StatusEffectsComponent? status = null)
  298. {
  299. // don't log since stuff calling this prolly doesn't care if we don't actually have it
  300. if (!Resolve(uid, ref status, false))
  301. return false;
  302. var ev = new BeforeStatusEffectAddedEvent(key);
  303. RaiseLocalEvent(uid, ref ev);
  304. if (ev.Cancelled)
  305. return false;
  306. if (!_prototypeManager.TryIndex<StatusEffectPrototype>(key, out var proto))
  307. return false;
  308. if (!status.AllowedEffects.Contains(key) && !proto.AlwaysAllowed)
  309. return false;
  310. return true;
  311. }
  312. /// <summary>
  313. /// Tries to add to the timer of an already existing status effect.
  314. /// </summary>
  315. /// <param name="uid">The entity to add time to.</param>
  316. /// <param name="key">The status effect to add time to.</param>
  317. /// <param name="time">The amount of time to add.</param>
  318. /// <param name="status">The status effect component, should you already have it.</param>
  319. public bool TryAddTime(EntityUid uid, string key, TimeSpan time,
  320. StatusEffectsComponent? status = null)
  321. {
  322. if (!Resolve(uid, ref status, false))
  323. return false;
  324. if (!HasStatusEffect(uid, key, status))
  325. return false;
  326. var timer = status.ActiveEffects[key].Cooldown;
  327. timer.Item2 += time;
  328. status.ActiveEffects[key].Cooldown = timer;
  329. if (_prototypeManager.TryIndex<StatusEffectPrototype>(key, out var proto)
  330. && proto.Alert != null)
  331. {
  332. (TimeSpan, TimeSpan)? cooldown = GetAlertCooldown(uid, proto.Alert.Value, status);
  333. _alertsSystem.ShowAlert(uid, proto.Alert.Value, null, cooldown);
  334. }
  335. Dirty(uid, status);
  336. return true;
  337. }
  338. /// <summary>
  339. /// Tries to remove time from the timer of an already existing status effect.
  340. /// </summary>
  341. /// <param name="uid">The entity to remove time from.</param>
  342. /// <param name="key">The status effect to remove time from.</param>
  343. /// <param name="time">The amount of time to add.</param>
  344. /// <param name="status">The status effect component, should you already have it.</param>
  345. public bool TryRemoveTime(EntityUid uid, string key, TimeSpan time,
  346. StatusEffectsComponent? status = null)
  347. {
  348. if (!Resolve(uid, ref status, false))
  349. return false;
  350. if (!HasStatusEffect(uid, key, status))
  351. return false;
  352. var timer = status.ActiveEffects[key].Cooldown;
  353. // what on earth are you doing, Gordon?
  354. if (time > timer.Item2)
  355. return false;
  356. timer.Item2 -= time;
  357. status.ActiveEffects[key].Cooldown = timer;
  358. if (_prototypeManager.TryIndex<StatusEffectPrototype>(key, out var proto)
  359. && proto.Alert != null)
  360. {
  361. (TimeSpan, TimeSpan)? cooldown = GetAlertCooldown(uid, proto.Alert.Value, status);
  362. _alertsSystem.ShowAlert(uid, proto.Alert.Value, null, cooldown);
  363. }
  364. Dirty(uid, status);
  365. return true;
  366. }
  367. /// <summary>
  368. /// Use if you want to set a cooldown directly.
  369. /// </summary>
  370. /// <remarks>
  371. /// Not used internally; just sets it itself.
  372. /// </remarks>
  373. public bool TrySetTime(EntityUid uid, string key, TimeSpan time,
  374. StatusEffectsComponent? status = null)
  375. {
  376. if (!Resolve(uid, ref status, false))
  377. return false;
  378. if (!HasStatusEffect(uid, key, status))
  379. return false;
  380. status.ActiveEffects[key].Cooldown = (_gameTiming.CurTime, _gameTiming.CurTime + time);
  381. Dirty(uid, status);
  382. return true;
  383. }
  384. /// <summary>
  385. /// Gets the cooldown for a given status effect on an entity.
  386. /// </summary>
  387. /// <param name="uid">The entity to check for status effects on.</param>
  388. /// <param name="key">The status effect to get time for.</param>
  389. /// <param name="time">Out var for the time, if it exists.</param>
  390. /// <param name="status">The status effects component to use, if any.</param>
  391. /// <returns>False if the status effect was not active, true otherwise.</returns>
  392. public bool TryGetTime(EntityUid uid, string key,
  393. [NotNullWhen(true)] out (TimeSpan, TimeSpan)? time,
  394. StatusEffectsComponent? status = null)
  395. {
  396. if (!Resolve(uid, ref status, false) || !HasStatusEffect(uid, key, status))
  397. {
  398. time = null;
  399. return false;
  400. }
  401. time = status.ActiveEffects[key].Cooldown;
  402. return true;
  403. }
  404. }
  405. /// <summary>
  406. /// Raised on an entity before a status effect is added to determine if adding it should be cancelled.
  407. /// </summary>
  408. [ByRefEvent]
  409. public record struct BeforeStatusEffectAddedEvent(string Key, bool Cancelled=false);
  410. public readonly struct StatusEffectAddedEvent
  411. {
  412. public readonly EntityUid Uid;
  413. public readonly string Key;
  414. public StatusEffectAddedEvent(EntityUid uid, string key)
  415. {
  416. Uid = uid;
  417. Key = key;
  418. }
  419. }
  420. public readonly struct StatusEffectEndedEvent
  421. {
  422. public readonly EntityUid Uid;
  423. public readonly string Key;
  424. public StatusEffectEndedEvent(EntityUid uid, string key)
  425. {
  426. Uid = uid;
  427. Key = key;
  428. }
  429. }
  430. }