InjectorSystem.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. using Content.Server.Body.Components;
  2. using Content.Server.Body.Systems;
  3. using Content.Shared.Chemistry;
  4. using Content.Shared.Chemistry.Components;
  5. using Content.Shared.Chemistry.Components.SolutionManager;
  6. using Content.Shared.Chemistry.EntitySystems;
  7. using Content.Shared.Chemistry.Reagent;
  8. using Content.Shared.Database;
  9. using Content.Shared.DoAfter;
  10. using Content.Shared.FixedPoint;
  11. using Content.Shared.Forensics;
  12. using Content.Shared.IdentityManagement;
  13. using Content.Shared.Interaction;
  14. using Content.Shared.Mobs.Components;
  15. using Content.Shared.Stacks;
  16. using Content.Shared.Nutrition.EntitySystems;
  17. namespace Content.Server.Chemistry.EntitySystems;
  18. public sealed class InjectorSystem : SharedInjectorSystem
  19. {
  20. [Dependency] private readonly BloodstreamSystem _blood = default!;
  21. [Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
  22. [Dependency] private readonly OpenableSystem _openable = default!;
  23. public override void Initialize()
  24. {
  25. base.Initialize();
  26. SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
  27. SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
  28. }
  29. private bool TryUseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
  30. {
  31. var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target);
  32. // Handle injecting/drawing for solutions
  33. if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
  34. {
  35. if (isOpenOrIgnored && SolutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
  36. return TryInject(injector, target, injectableSolution.Value, user, false);
  37. if (isOpenOrIgnored && SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
  38. return TryInject(injector, target, refillableSolution.Value, user, true);
  39. if (TryComp<BloodstreamComponent>(target, out var bloodstream))
  40. return TryInjectIntoBloodstream(injector, (target, bloodstream), user);
  41. Popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
  42. ("target", Identity.Entity(target, EntityManager))), injector, user);
  43. return false;
  44. }
  45. if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
  46. {
  47. // Draw from a bloodstream, if the target has that
  48. if (TryComp<BloodstreamComponent>(target, out var stream) &&
  49. SolutionContainers.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution))
  50. {
  51. return TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
  52. }
  53. // Draw from an object (food, beaker, etc)
  54. if (isOpenOrIgnored && SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
  55. return TryDraw(injector, target, drawableSolution.Value, user);
  56. Popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
  57. ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
  58. return false;
  59. }
  60. return false;
  61. }
  62. private void OnInjectDoAfter(Entity<InjectorComponent> entity, ref InjectorDoAfterEvent args)
  63. {
  64. if (args.Cancelled || args.Handled || args.Args.Target == null)
  65. return;
  66. args.Handled = TryUseInjector(entity, args.Args.Target.Value, args.Args.User);
  67. }
  68. private void OnInjectorAfterInteract(Entity<InjectorComponent> entity, ref AfterInteractEvent args)
  69. {
  70. if (args.Handled || !args.CanReach)
  71. return;
  72. //Make sure we have the attacking entity
  73. if (args.Target is not { Valid: true } target || !HasComp<SolutionContainerManagerComponent>(entity))
  74. return;
  75. // Is the target a mob? If yes, use a do-after to give them time to respond.
  76. if (HasComp<MobStateComponent>(target) || HasComp<BloodstreamComponent>(target))
  77. {
  78. // Are use using an injector capible of targeting a mob?
  79. if (entity.Comp.IgnoreMobs)
  80. return;
  81. InjectDoAfter(entity, target, args.User);
  82. args.Handled = true;
  83. return;
  84. }
  85. args.Handled = TryUseInjector(entity, target, args.User);
  86. }
  87. /// <summary>
  88. /// Send informative pop-up messages and wait for a do-after to complete.
  89. /// </summary>
  90. private void InjectDoAfter(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
  91. {
  92. // Create a pop-up for the user
  93. if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
  94. {
  95. Popup.PopupEntity(Loc.GetString("injector-component-drawing-user"), target, user);
  96. }
  97. else
  98. {
  99. Popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user);
  100. }
  101. if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out _, out var solution))
  102. return;
  103. var actualDelay = injector.Comp.Delay;
  104. FixedPoint2 amountToInject;
  105. if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
  106. {
  107. // additional delay is based on actual volume left to draw in syringe when smaller than transfer amount
  108. amountToInject = FixedPoint2.Min(injector.Comp.TransferAmount, (solution.MaxVolume - solution.Volume));
  109. }
  110. else
  111. {
  112. // additional delay is based on actual volume left to inject in syringe when smaller than transfer amount
  113. amountToInject = FixedPoint2.Min(injector.Comp.TransferAmount, solution.Volume);
  114. }
  115. // Injections take 0.5 seconds longer per 5u of possible space/content
  116. // First 5u(MinimumTransferAmount) doesn't incur delay
  117. actualDelay += injector.Comp.DelayPerVolume * FixedPoint2.Max(0, amountToInject - injector.Comp.MinimumTransferAmount).Double();
  118. // Ensure that minimum delay before incapacitation checks is 1 seconds
  119. actualDelay = MathHelper.Max(actualDelay, TimeSpan.FromSeconds(1));
  120. var isTarget = user != target;
  121. if (isTarget)
  122. {
  123. // Create a pop-up for the target
  124. var userName = Identity.Entity(user, EntityManager);
  125. if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
  126. {
  127. Popup.PopupEntity(Loc.GetString("injector-component-drawing-target",
  128. ("user", userName)), user, target);
  129. }
  130. else
  131. {
  132. Popup.PopupEntity(Loc.GetString("injector-component-injecting-target",
  133. ("user", userName)), user, target);
  134. }
  135. // Check if the target is incapacitated or in combat mode and modify time accordingly.
  136. if (MobState.IsIncapacitated(target))
  137. {
  138. actualDelay /= 2.5f;
  139. }
  140. else if (Combat.IsInCombatMode(target))
  141. {
  142. // Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
  143. // combat with fast syringes & lag.
  144. actualDelay += TimeSpan.FromSeconds(1);
  145. }
  146. // Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
  147. if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
  148. {
  149. AdminLogger.Add(LogType.ForceFeed,
  150. $"{EntityManager.ToPrettyString(user):user} is attempting to inject {EntityManager.ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}");
  151. }
  152. else
  153. {
  154. AdminLogger.Add(LogType.ForceFeed,
  155. $"{EntityManager.ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from {EntityManager.ToPrettyString(target):target}");
  156. }
  157. }
  158. else
  159. {
  160. // Self-injections take half as long.
  161. actualDelay /= 2;
  162. if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
  163. {
  164. AdminLogger.Add(LogType.Ingestion,
  165. $"{EntityManager.ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}.");
  166. }
  167. else
  168. {
  169. AdminLogger.Add(LogType.ForceFeed,
  170. $"{EntityManager.ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from themselves.");
  171. }
  172. }
  173. DoAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner)
  174. {
  175. BreakOnMove = true,
  176. BreakOnWeightlessMove = false,
  177. BreakOnDamage = true,
  178. NeedHand = injector.Comp.NeedHand,
  179. BreakOnHandChange = injector.Comp.BreakOnHandChange,
  180. MovementThreshold = injector.Comp.MovementThreshold,
  181. });
  182. }
  183. private bool TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
  184. EntityUid user)
  185. {
  186. // Get transfer amount. May be smaller than _transferAmount if not enough room
  187. if (!SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
  188. ref target.Comp.ChemicalSolution, out var chemSolution))
  189. {
  190. Popup.PopupEntity(
  191. Loc.GetString("injector-component-cannot-inject-message",
  192. ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
  193. return false;
  194. }
  195. var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, chemSolution.AvailableVolume);
  196. if (realTransferAmount <= 0)
  197. {
  198. Popup.PopupEntity(
  199. Loc.GetString("injector-component-cannot-inject-message",
  200. ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
  201. return false;
  202. }
  203. // Move units from attackSolution to targetSolution
  204. var removedSolution = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount);
  205. _blood.TryAddToChemicals(target, removedSolution, target.Comp);
  206. _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
  207. Popup.PopupEntity(Loc.GetString("injector-component-inject-success-message",
  208. ("amount", removedSolution.Volume),
  209. ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
  210. Dirty(injector);
  211. AfterInject(injector, target);
  212. return true;
  213. }
  214. private bool TryInject(Entity<InjectorComponent> injector, EntityUid targetEntity,
  215. Entity<SolutionComponent> targetSolution, EntityUid user, bool asRefill)
  216. {
  217. if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
  218. out var solution) || solution.Volume == 0)
  219. return false;
  220. // Get transfer amount. May be smaller than _transferAmount if not enough room
  221. var realTransferAmount =
  222. FixedPoint2.Min(injector.Comp.TransferAmount, targetSolution.Comp.Solution.AvailableVolume);
  223. if (realTransferAmount <= 0)
  224. {
  225. Popup.PopupEntity(
  226. Loc.GetString("injector-component-target-already-full-message",
  227. ("target", Identity.Entity(targetEntity, EntityManager))),
  228. injector.Owner, user);
  229. return false;
  230. }
  231. // Move units from attackSolution to targetSolution
  232. Solution removedSolution;
  233. if (TryComp<StackComponent>(targetEntity, out var stack))
  234. removedSolution = SolutionContainers.SplitStackSolution(soln.Value, realTransferAmount, stack.Count);
  235. else
  236. removedSolution = SolutionContainers.SplitSolution(soln.Value, realTransferAmount);
  237. _reactiveSystem.DoEntityReaction(targetEntity, removedSolution, ReactionMethod.Injection);
  238. if (!asRefill)
  239. SolutionContainers.Inject(targetEntity, targetSolution, removedSolution);
  240. else
  241. SolutionContainers.Refill(targetEntity, targetSolution, removedSolution);
  242. Popup.PopupEntity(Loc.GetString("injector-component-transfer-success-message",
  243. ("amount", removedSolution.Volume),
  244. ("target", Identity.Entity(targetEntity, EntityManager))), injector.Owner, user);
  245. Dirty(injector);
  246. AfterInject(injector, targetEntity);
  247. return true;
  248. }
  249. private void AfterInject(Entity<InjectorComponent> injector, EntityUid target)
  250. {
  251. // Automatically set syringe to draw after completely draining it.
  252. if (SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out _,
  253. out var solution) && solution.Volume == 0)
  254. {
  255. SetMode(injector, InjectorToggleMode.Draw);
  256. }
  257. // Leave some DNA from the injectee on it
  258. var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
  259. RaiseLocalEvent(target, ref ev);
  260. }
  261. private void AfterDraw(Entity<InjectorComponent> injector, EntityUid target)
  262. {
  263. // Automatically set syringe to inject after completely filling it.
  264. if (SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out _,
  265. out var solution) && solution.AvailableVolume == 0)
  266. {
  267. SetMode(injector, InjectorToggleMode.Inject);
  268. }
  269. // Leave some DNA from the drawee on it
  270. var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
  271. RaiseLocalEvent(target, ref ev);
  272. }
  273. private bool TryDraw(Entity<InjectorComponent> injector, Entity<BloodstreamComponent?> target,
  274. Entity<SolutionComponent> targetSolution, EntityUid user)
  275. {
  276. if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
  277. out var solution) || solution.AvailableVolume == 0)
  278. {
  279. return false;
  280. }
  281. var applicableTargetSolution = targetSolution.Comp.Solution;
  282. // If a whitelist exists, remove all non-whitelisted reagents from the target solution temporarily
  283. var temporarilyRemovedSolution = new Solution();
  284. if (injector.Comp.ReagentWhitelist is { } reagentWhitelist)
  285. {
  286. string[] reagentPrototypeWhitelistArray = new string[reagentWhitelist.Count];
  287. var i = 0;
  288. foreach (var reagent in reagentWhitelist)
  289. {
  290. reagentPrototypeWhitelistArray[i] = reagent;
  291. ++i;
  292. }
  293. temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, reagentPrototypeWhitelistArray);
  294. }
  295. // Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
  296. var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, applicableTargetSolution.Volume,
  297. solution.AvailableVolume);
  298. if (realTransferAmount <= 0)
  299. {
  300. Popup.PopupEntity(
  301. Loc.GetString("injector-component-target-is-empty-message",
  302. ("target", Identity.Entity(target, EntityManager))),
  303. injector.Owner, user);
  304. return false;
  305. }
  306. // We have some snowflaked behavior for streams.
  307. if (target.Comp != null)
  308. {
  309. DrawFromBlood(injector, (target.Owner, target.Comp), soln.Value, realTransferAmount, user);
  310. return true;
  311. }
  312. // Move units from attackSolution to targetSolution
  313. var removedSolution = SolutionContainers.Draw(target.Owner, targetSolution, realTransferAmount);
  314. // Add back non-whitelisted reagents to the target solution
  315. applicableTargetSolution.AddSolution(temporarilyRemovedSolution, null);
  316. if (!SolutionContainers.TryAddSolution(soln.Value, removedSolution))
  317. {
  318. return false;
  319. }
  320. Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
  321. ("amount", removedSolution.Volume),
  322. ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
  323. Dirty(injector);
  324. AfterDraw(injector, target);
  325. return true;
  326. }
  327. private void DrawFromBlood(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
  328. Entity<SolutionComponent> injectorSolution, FixedPoint2 transferAmount, EntityUid user)
  329. {
  330. var drawAmount = (float) transferAmount;
  331. if (SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
  332. ref target.Comp.ChemicalSolution))
  333. {
  334. var chemTemp = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, drawAmount * 0.15f);
  335. SolutionContainers.TryAddSolution(injectorSolution, chemTemp);
  336. drawAmount -= (float) chemTemp.Volume;
  337. }
  338. if (SolutionContainers.ResolveSolution(target.Owner, target.Comp.BloodSolutionName,
  339. ref target.Comp.BloodSolution))
  340. {
  341. var bloodTemp = SolutionContainers.SplitSolution(target.Comp.BloodSolution.Value, drawAmount);
  342. SolutionContainers.TryAddSolution(injectorSolution, bloodTemp);
  343. }
  344. Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
  345. ("amount", transferAmount),
  346. ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
  347. Dirty(injector);
  348. AfterDraw(injector, target);
  349. }
  350. }