1
0

SharedDoAfterSystem.cs 15 KB

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