SharedDoAfterSystem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Threading.Tasks;
  3. using Content.Shared.ActionBlocker;
  4. using Content.Shared.Damage;
  5. using Content.Shared.Hands.Components;
  6. using Content.Shared.Tag;
  7. using Robust.Shared.GameStates;
  8. using Robust.Shared.Serialization;
  9. using Robust.Shared.Timing;
  10. using Robust.Shared.Utility;
  11. namespace Content.Shared.DoAfter;
  12. public abstract partial class SharedDoAfterSystem : EntitySystem
  13. {
  14. [Dependency] protected readonly IGameTiming GameTiming = default!;
  15. [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
  16. [Dependency] private readonly SharedTransformSystem _transform = default!;
  17. [Dependency] private readonly TagSystem _tag = default!;
  18. /// <summary>
  19. /// We'll use an excess time so stuff like finishing effects can show.
  20. /// </summary>
  21. private static readonly TimeSpan ExcessTime = TimeSpan.FromSeconds(0.5f);
  22. public override void Initialize()
  23. {
  24. base.Initialize();
  25. SubscribeLocalEvent<DoAfterComponent, DamageChangedEvent>(OnDamage);
  26. SubscribeLocalEvent<DoAfterComponent, EntityUnpausedEvent>(OnUnpaused);
  27. SubscribeLocalEvent<DoAfterComponent, ComponentGetState>(OnDoAfterGetState);
  28. SubscribeLocalEvent<DoAfterComponent, ComponentHandleState>(OnDoAfterHandleState);
  29. }
  30. private void OnUnpaused(EntityUid uid, DoAfterComponent component, ref EntityUnpausedEvent args)
  31. {
  32. foreach (var doAfter in component.DoAfters.Values)
  33. {
  34. doAfter.StartTime += args.PausedTime;
  35. if (doAfter.CancelledTime != null)
  36. doAfter.CancelledTime = doAfter.CancelledTime.Value + args.PausedTime;
  37. }
  38. Dirty(uid, component);
  39. }
  40. /// <summary>
  41. /// Cancels DoAfter if it breaks on damage and it meets the threshold
  42. /// </summary>
  43. private void OnDamage(EntityUid uid, DoAfterComponent component, DamageChangedEvent args)
  44. {
  45. // If we're applying state then let the server state handle the do_after prediction.
  46. // This is to avoid scenarios where a do_after is erroneously cancelled on the final tick.
  47. if (!args.InterruptsDoAfters || !args.DamageIncreased || args.DamageDelta == null || GameTiming.ApplyingState)
  48. return;
  49. var delta = args.DamageDelta.GetTotal();
  50. var dirty = false;
  51. foreach (var doAfter in component.DoAfters.Values)
  52. {
  53. if (doAfter.Args.BreakOnDamage && delta >= doAfter.Args.DamageThreshold)
  54. {
  55. InternalCancel(doAfter, component);
  56. dirty = true;
  57. }
  58. }
  59. if (dirty)
  60. Dirty(uid, component);
  61. }
  62. private void RaiseDoAfterEvents(DoAfter doAfter, DoAfterComponent component)
  63. {
  64. var ev = doAfter.Args.Event;
  65. ev.Handled = false;
  66. ev.Repeat = false;
  67. ev.DoAfter = doAfter;
  68. if (Exists(doAfter.Args.EventTarget))
  69. RaiseLocalEvent(doAfter.Args.EventTarget.Value, (object)ev, doAfter.Args.Broadcast);
  70. else if (doAfter.Args.Broadcast)
  71. RaiseLocalEvent((object)ev);
  72. if (component.AwaitedDoAfters.Remove(doAfter.Index, out var tcs))
  73. tcs.SetResult(doAfter.Cancelled ? DoAfterStatus.Cancelled : DoAfterStatus.Finished);
  74. }
  75. private void OnDoAfterGetState(EntityUid uid, DoAfterComponent comp, ref ComponentGetState args)
  76. {
  77. args.State = new DoAfterComponentState(EntityManager, comp);
  78. }
  79. private void OnDoAfterHandleState(EntityUid uid, DoAfterComponent comp, ref ComponentHandleState args)
  80. {
  81. if (args.Current is not DoAfterComponentState state)
  82. return;
  83. // Note that the client may have correctly predicted the creation of a do-after, but that doesn't guarantee that
  84. // the contents of the do-after data are correct. So this just takes the brute force approach and completely
  85. // overwrites the state.
  86. comp.DoAfters.Clear();
  87. foreach (var (id, doAfter) in state.DoAfters)
  88. {
  89. var newDoAfter = new DoAfter(EntityManager, doAfter);
  90. comp.DoAfters.Add(id, newDoAfter);
  91. // Networking yay (if you have an easier way dear god please).
  92. newDoAfter.UserPosition = EnsureCoordinates<DoAfterComponent>(newDoAfter.NetUserPosition, uid);
  93. newDoAfter.InitialItem = EnsureEntity<DoAfterComponent>(newDoAfter.NetInitialItem, uid);
  94. var doAfterArgs = newDoAfter.Args;
  95. doAfterArgs.Target = EnsureEntity<DoAfterComponent>(doAfterArgs.NetTarget, uid);
  96. doAfterArgs.Used = EnsureEntity<DoAfterComponent>(doAfterArgs.NetUsed, uid);
  97. doAfterArgs.User = EnsureEntity<DoAfterComponent>(doAfterArgs.NetUser, uid);
  98. doAfterArgs.EventTarget = EnsureEntity<DoAfterComponent>(doAfterArgs.NetEventTarget, uid);
  99. }
  100. comp.NextId = state.NextId;
  101. DebugTools.Assert(!comp.DoAfters.ContainsKey(comp.NextId));
  102. if (comp.DoAfters.Count == 0)
  103. RemCompDeferred<ActiveDoAfterComponent>(uid);
  104. else
  105. EnsureComp<ActiveDoAfterComponent>(uid);
  106. }
  107. #region Creation
  108. /// <summary>
  109. /// Tasks that are delayed until the specified time has passed
  110. /// These can be potentially cancelled by the user moving or when other things happen.
  111. /// </summary>
  112. // TODO remove this, as well as AwaitedDoAfterEvent and DoAfterComponent.AwaitedDoAfters
  113. [Obsolete("Use the synchronous version instead.")]
  114. public async Task<DoAfterStatus> WaitDoAfter(DoAfterArgs doAfter, DoAfterComponent? component = null)
  115. {
  116. if (!Resolve(doAfter.User, ref component))
  117. return DoAfterStatus.Cancelled;
  118. if (!TryStartDoAfter(doAfter, out var id, component))
  119. return DoAfterStatus.Cancelled;
  120. if (doAfter.Delay <= TimeSpan.Zero)
  121. {
  122. Log.Warning("Awaited instant DoAfters are not supported fully supported");
  123. return DoAfterStatus.Finished;
  124. }
  125. var tcs = new TaskCompletionSource<DoAfterStatus>();
  126. component.AwaitedDoAfters.Add(id.Value.Index, tcs);
  127. return await tcs.Task;
  128. }
  129. /// <summary>
  130. /// Attempts to start a new DoAfter. Note that even if this function returns true, an interaction may have
  131. /// occured, as starting a duplicate DoAfter may cancel currently running DoAfters.
  132. /// </summary>
  133. /// <param name="args">The DoAfter arguments</param>
  134. /// <param name="component">The user's DoAfter component</param>
  135. /// <returns></returns>
  136. public bool TryStartDoAfter(DoAfterArgs args, DoAfterComponent? component = null)
  137. => TryStartDoAfter(args, out _, component);
  138. /// <summary>
  139. /// Attempts to start a new DoAfter. Note that even if this function returns false, an interaction may have
  140. /// occured, as starting a duplicate DoAfter may cancel currently running DoAfters.
  141. /// </summary>
  142. /// <param name="args">The DoAfter arguments</param>
  143. /// <param name="id">The Id of the newly started DoAfter</param>
  144. /// <param name="comp">The user's DoAfter component</param>
  145. /// <returns></returns>
  146. public bool TryStartDoAfter(DoAfterArgs args, [NotNullWhen(true)] out DoAfterId? id, DoAfterComponent? comp = null)
  147. {
  148. DebugTools.Assert(args.Broadcast || Exists(args.EventTarget) || args.Event.GetType() == typeof(AwaitedDoAfterEvent));
  149. DebugTools.Assert(args.Event.GetType().HasCustomAttribute<NetSerializableAttribute>()
  150. || args.Event.GetType().Namespace is {} ns && ns.StartsWith("Content.IntegrationTests"), // classes defined in tests cannot be marked as serializable.
  151. $"Do after event is not serializable. Event: {args.Event.GetType()}");
  152. if (!Resolve(args.User, ref comp))
  153. {
  154. Log.Error($"Attempting to start a doAfter with invalid user: {ToPrettyString(args.User)}.");
  155. id = null;
  156. return false;
  157. }
  158. // Duplicate blocking & cancellation.
  159. if (!ProcessDuplicates(args, comp))
  160. {
  161. id = null;
  162. return false;
  163. }
  164. id = new DoAfterId(args.User, comp.NextId++);
  165. var doAfter = new DoAfter(id.Value.Index, args, GameTiming.CurTime);
  166. // Networking yay
  167. args.NetTarget = GetNetEntity(args.Target);
  168. args.NetUsed = GetNetEntity(args.Used);
  169. args.NetUser = GetNetEntity(args.User);
  170. args.NetEventTarget = GetNetEntity(args.EventTarget);
  171. if (args.BreakOnMove)
  172. doAfter.UserPosition = Transform(args.User).Coordinates;
  173. if (args.Target != null && args.BreakOnMove)
  174. {
  175. var targetPosition = Transform(args.Target.Value).Coordinates;
  176. doAfter.UserPosition.TryDistance(EntityManager, targetPosition, out doAfter.TargetDistance);
  177. }
  178. doAfter.NetUserPosition = GetNetCoordinates(doAfter.UserPosition);
  179. // For this we need to stay on the same hand slot and need the same item in that hand slot
  180. // (or if there is no item there we need to keep it free).
  181. if (args.NeedHand && (args.BreakOnHandChange || args.BreakOnDropItem))
  182. {
  183. if (!TryComp(args.User, out HandsComponent? handsComponent))
  184. return false;
  185. doAfter.InitialHand = handsComponent.ActiveHand?.Name;
  186. doAfter.InitialItem = handsComponent.ActiveHandEntity;
  187. }
  188. doAfter.NetInitialItem = GetNetEntity(doAfter.InitialItem);
  189. // Initial checks
  190. if (ShouldCancel(doAfter, GetEntityQuery<TransformComponent>(), GetEntityQuery<HandsComponent>()))
  191. return false;
  192. if (args.AttemptFrequency == AttemptFrequency.StartAndEnd && !TryAttemptEvent(doAfter))
  193. return false;
  194. // TODO DO AFTER
  195. // Why does this tag exist? Just make this a bool on the component?
  196. if (args.Delay <= TimeSpan.Zero || _tag.HasTag(args.User, "InstantDoAfters"))
  197. {
  198. RaiseDoAfterEvents(doAfter, comp);
  199. // We don't store instant do-afters. This is just a lazy way of hiding them from client-side visuals.
  200. return true;
  201. }
  202. comp.DoAfters.Add(doAfter.Index, doAfter);
  203. EnsureComp<ActiveDoAfterComponent>(args.User);
  204. Dirty(args.User, comp);
  205. args.Event.DoAfter = doAfter;
  206. return true;
  207. }
  208. /// <summary>
  209. /// Cancel any applicable duplicate DoAfters and return whether or not the new DoAfter should be created.
  210. /// </summary>
  211. private bool ProcessDuplicates(DoAfterArgs args, DoAfterComponent component)
  212. {
  213. var blocked = false;
  214. foreach (var existing in component.DoAfters.Values)
  215. {
  216. if (existing.Cancelled || existing.Completed)
  217. continue;
  218. if (!IsDuplicate(existing.Args, args))
  219. continue;
  220. blocked = blocked | args.BlockDuplicate | existing.Args.BlockDuplicate;
  221. if (args.CancelDuplicate || existing.Args.CancelDuplicate)
  222. Cancel(args.User, existing.Index, component);
  223. }
  224. return !blocked;
  225. }
  226. private bool IsDuplicate(DoAfterArgs args, DoAfterArgs otherArgs)
  227. {
  228. if (IsDuplicate(args, otherArgs, args.DuplicateCondition))
  229. return true;
  230. if (args.DuplicateCondition == otherArgs.DuplicateCondition)
  231. return false;
  232. return IsDuplicate(args, otherArgs, otherArgs.DuplicateCondition);
  233. }
  234. private bool IsDuplicate(DoAfterArgs args, DoAfterArgs otherArgs, DuplicateConditions conditions )
  235. {
  236. if ((conditions & DuplicateConditions.SameTarget) != 0
  237. && args.Target != otherArgs.Target)
  238. {
  239. return false;
  240. }
  241. if ((conditions & DuplicateConditions.SameTool) != 0
  242. && args.Used != otherArgs.Used)
  243. {
  244. return false;
  245. }
  246. if ((conditions & DuplicateConditions.SameEvent) != 0
  247. && !args.Event.IsDuplicate(otherArgs.Event))
  248. {
  249. return false;
  250. }
  251. return true;
  252. }
  253. #endregion
  254. #region Cancellation
  255. /// <summary>
  256. /// Cancels an active DoAfter.
  257. /// </summary>
  258. public void Cancel(DoAfterId? id, DoAfterComponent? comp = null)
  259. {
  260. if (id != null)
  261. Cancel(id.Value.Uid, id.Value.Index, comp);
  262. }
  263. /// <summary>
  264. /// Cancels an active DoAfter.
  265. /// </summary>
  266. public void Cancel(EntityUid entity, ushort id, DoAfterComponent? comp = null)
  267. {
  268. if (!Resolve(entity, ref comp, false))
  269. return;
  270. if (!comp.DoAfters.TryGetValue(id, out var doAfter))
  271. {
  272. Log.Error($"Attempted to cancel do after with an invalid id ({id}) on entity {ToPrettyString(entity)}");
  273. return;
  274. }
  275. InternalCancel(doAfter, comp);
  276. Dirty(entity, comp);
  277. }
  278. private void InternalCancel(DoAfter doAfter, DoAfterComponent component)
  279. {
  280. if (doAfter.Cancelled || doAfter.Completed)
  281. return;
  282. // Caller is responsible for dirtying the component.
  283. doAfter.CancelledTime = GameTiming.CurTime;
  284. RaiseDoAfterEvents(doAfter, component);
  285. }
  286. #endregion
  287. #region Query
  288. /// <summary>
  289. /// Returns the current status of a DoAfter
  290. /// </summary>
  291. public DoAfterStatus GetStatus(DoAfterId? id, DoAfterComponent? comp = null)
  292. {
  293. if (id != null)
  294. return GetStatus(id.Value.Uid, id.Value.Index, comp);
  295. else
  296. return DoAfterStatus.Invalid;
  297. }
  298. /// <summary>
  299. /// Returns the current status of a DoAfter
  300. /// </summary>
  301. public DoAfterStatus GetStatus(EntityUid entity, ushort id, DoAfterComponent? comp = null)
  302. {
  303. if (!Resolve(entity, ref comp, false))
  304. return DoAfterStatus.Invalid;
  305. if (!comp.DoAfters.TryGetValue(id, out var doAfter))
  306. return DoAfterStatus.Invalid;
  307. if (doAfter.Cancelled)
  308. return DoAfterStatus.Cancelled;
  309. if (!doAfter.Completed)
  310. return DoAfterStatus.Running;
  311. // Theres the chance here that the DoAfter hasn't actually finished yet if the system's update hasn't run yet.
  312. // This would also mean the post-DoAfter checks haven't run yet. But whatever, I can't be bothered tracking and
  313. // networking whether a do-after has raised its events or not.
  314. return DoAfterStatus.Finished;
  315. }
  316. public bool IsRunning(DoAfterId? id, DoAfterComponent? comp = null)
  317. {
  318. if (id == null)
  319. return false;
  320. return GetStatus(id.Value.Uid, id.Value.Index, comp) == DoAfterStatus.Running;
  321. }
  322. public bool IsRunning(EntityUid entity, ushort id, DoAfterComponent? comp = null)
  323. {
  324. return GetStatus(entity, id, comp) == DoAfterStatus.Running;
  325. }
  326. #endregion
  327. }