SolutionTransferSystem.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. using Content.Shared.Administration.Logs;
  2. using Content.Shared.Chemistry;
  3. using Content.Shared.Chemistry.Components;
  4. using Content.Shared.Database;
  5. using Content.Shared.FixedPoint;
  6. using Content.Shared.Interaction;
  7. using Content.Shared.Popups;
  8. using Content.Shared.Verbs;
  9. using Robust.Shared.Network;
  10. using Robust.Shared.Player;
  11. namespace Content.Shared.Chemistry.EntitySystems;
  12. /// <summary>
  13. /// Allows an entity to transfer solutions with a customizable amount per click.
  14. /// Also provides <see cref="Transfer"/> API for other systems.
  15. /// </summary>
  16. public sealed class SolutionTransferSystem : EntitySystem
  17. {
  18. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  19. [Dependency] private readonly SharedPopupSystem _popup = default!;
  20. [Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
  21. [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
  22. /// <summary>
  23. /// Default transfer amounts for the set-transfer verb.
  24. /// </summary>
  25. public static readonly FixedPoint2[] DefaultTransferAmounts = new FixedPoint2[] { 1, 5, 10, 25, 50, 100, 250, 500, 1000 };
  26. public override void Initialize()
  27. {
  28. base.Initialize();
  29. SubscribeLocalEvent<SolutionTransferComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs);
  30. SubscribeLocalEvent<SolutionTransferComponent, AfterInteractEvent>(OnAfterInteract);
  31. SubscribeLocalEvent<SolutionTransferComponent, TransferAmountSetValueMessage>(OnTransferAmountSetValueMessage);
  32. }
  33. private void OnTransferAmountSetValueMessage(Entity<SolutionTransferComponent> ent, ref TransferAmountSetValueMessage message)
  34. {
  35. var (uid, comp) = ent;
  36. var newTransferAmount = FixedPoint2.Clamp(message.Value, comp.MinimumTransferAmount, comp.MaximumTransferAmount);
  37. comp.TransferAmount = newTransferAmount;
  38. if (message.Actor is { Valid: true } user)
  39. _popup.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), uid, user);
  40. Dirty(uid, comp);
  41. }
  42. private void AddSetTransferVerbs(Entity<SolutionTransferComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
  43. {
  44. var (uid, comp) = ent;
  45. if (!args.CanAccess || !args.CanInteract || !comp.CanChangeTransferAmount || args.Hands == null)
  46. return;
  47. // Custom transfer verb
  48. var @event = args;
  49. args.Verbs.Add(new AlternativeVerb()
  50. {
  51. Text = Loc.GetString("comp-solution-transfer-verb-custom-amount"),
  52. Category = VerbCategory.SetTransferAmount,
  53. // TODO: remove server check when bui prediction is a thing
  54. Act = () =>
  55. {
  56. _ui.OpenUi(uid, TransferAmountUiKey.Key, @event.User);
  57. },
  58. Priority = 1
  59. });
  60. // Add specific transfer verbs according to the container's size
  61. var priority = 0;
  62. var user = args.User;
  63. foreach (var amount in DefaultTransferAmounts)
  64. {
  65. if (amount < comp.MinimumTransferAmount || amount > comp.MaximumTransferAmount)
  66. continue;
  67. AlternativeVerb verb = new();
  68. verb.Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount));
  69. verb.Category = VerbCategory.SetTransferAmount;
  70. verb.Act = () =>
  71. {
  72. comp.TransferAmount = amount;
  73. _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), uid, user);
  74. Dirty(uid, comp);
  75. };
  76. // we want to sort by size, not alphabetically by the verb text.
  77. verb.Priority = priority;
  78. priority--;
  79. args.Verbs.Add(verb);
  80. }
  81. }
  82. private void OnAfterInteract(Entity<SolutionTransferComponent> ent, ref AfterInteractEvent args)
  83. {
  84. if (!args.CanReach || args.Target is not {} target)
  85. return;
  86. var (uid, comp) = ent;
  87. //Special case for reagent tanks, because normally clicking another container will give solution, not take it.
  88. if (comp.CanReceive
  89. && !HasComp<RefillableSolutionComponent>(target) // target must not be refillable (e.g. Reagent Tanks)
  90. && _solution.TryGetDrainableSolution(target, out var targetSoln, out _) // target must be drainable
  91. && TryComp<RefillableSolutionComponent>(uid, out var refill)
  92. && _solution.TryGetRefillableSolution((uid, refill, null), out var ownerSoln, out var ownerRefill))
  93. {
  94. var transferAmount = comp.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target reagent tank.
  95. // if the receiver has a smaller transfer limit, use that instead
  96. if (refill?.MaxRefill is {} maxRefill)
  97. transferAmount = FixedPoint2.Min(transferAmount, maxRefill);
  98. var transferred = Transfer(args.User, target, targetSoln.Value, uid, ownerSoln.Value, transferAmount);
  99. args.Handled = true;
  100. if (transferred > 0)
  101. {
  102. var toTheBrim = ownerRefill.AvailableVolume == 0;
  103. var msg = toTheBrim
  104. ? "comp-solution-transfer-fill-fully"
  105. : "comp-solution-transfer-fill-normal";
  106. _popup.PopupClient(Loc.GetString(msg, ("owner", args.Target), ("amount", transferred), ("target", uid)), uid, args.User);
  107. return;
  108. }
  109. }
  110. // if target is refillable, and owner is drainable
  111. if (comp.CanSend
  112. && TryComp<RefillableSolutionComponent>(target, out var targetRefill)
  113. && _solution.TryGetRefillableSolution((target, targetRefill, null), out targetSoln, out _)
  114. && _solution.TryGetDrainableSolution(uid, out ownerSoln, out _))
  115. {
  116. var transferAmount = comp.TransferAmount;
  117. if (targetRefill?.MaxRefill is {} maxRefill)
  118. transferAmount = FixedPoint2.Min(transferAmount, maxRefill);
  119. var transferred = Transfer(args.User, uid, ownerSoln.Value, target, targetSoln.Value, transferAmount);
  120. args.Handled = true;
  121. if (transferred > 0)
  122. {
  123. var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", target));
  124. _popup.PopupClient(message, uid, args.User);
  125. }
  126. }
  127. }
  128. /// <summary>
  129. /// Transfer from a solution to another, allowing either entity to cancel it and show a popup.
  130. /// </summary>
  131. /// <returns>The actual amount transferred.</returns>
  132. public FixedPoint2 Transfer(EntityUid user,
  133. EntityUid sourceEntity,
  134. Entity<SolutionComponent> source,
  135. EntityUid targetEntity,
  136. Entity<SolutionComponent> target,
  137. FixedPoint2 amount)
  138. {
  139. var transferAttempt = new SolutionTransferAttemptEvent(sourceEntity, targetEntity);
  140. // Check if the source is cancelling the transfer
  141. RaiseLocalEvent(sourceEntity, ref transferAttempt);
  142. if (transferAttempt.CancelReason is {} reason)
  143. {
  144. _popup.PopupClient(reason, sourceEntity, user);
  145. return FixedPoint2.Zero;
  146. }
  147. var sourceSolution = source.Comp.Solution;
  148. if (sourceSolution.Volume == 0)
  149. {
  150. _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", sourceEntity)), sourceEntity, user);
  151. return FixedPoint2.Zero;
  152. }
  153. // Check if the target is cancelling the transfer
  154. RaiseLocalEvent(targetEntity, ref transferAttempt);
  155. if (transferAttempt.CancelReason is {} targetReason)
  156. {
  157. _popup.PopupClient(targetReason, targetEntity, user);
  158. return FixedPoint2.Zero;
  159. }
  160. var targetSolution = target.Comp.Solution;
  161. if (targetSolution.AvailableVolume == 0)
  162. {
  163. _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", targetEntity)), targetEntity, user);
  164. return FixedPoint2.Zero;
  165. }
  166. var actualAmount = FixedPoint2.Min(amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume));
  167. var solution = _solution.SplitSolution(source, actualAmount);
  168. _solution.AddSolution(target, solution);
  169. var ev = new SolutionTransferredEvent(sourceEntity, targetEntity, user, actualAmount);
  170. RaiseLocalEvent(targetEntity, ref ev);
  171. _adminLogger.Add(LogType.Action, LogImpact.Medium,
  172. $"{ToPrettyString(user):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(targetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}");
  173. return actualAmount;
  174. }
  175. }
  176. /// <summary>
  177. /// Raised when attempting to transfer from one solution to another.
  178. /// Raised on both the source and target entities so either can cancel the transfer.
  179. /// To not mispredict this should always be cancelled in shared code and not server or client.
  180. /// </summary>
  181. [ByRefEvent]
  182. public record struct SolutionTransferAttemptEvent(EntityUid From, EntityUid To, string? CancelReason = null)
  183. {
  184. /// <summary>
  185. /// Cancels the transfer.
  186. /// </summary>
  187. public void Cancel(string reason)
  188. {
  189. CancelReason = reason;
  190. }
  191. }
  192. /// <summary>
  193. /// Raised on the target entity when a non-zero amount of solution gets transferred.
  194. /// </summary>
  195. [ByRefEvent]
  196. public record struct SolutionTransferredEvent(EntityUid From, EntityUid To, EntityUid User, FixedPoint2 Amount);