mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-17 05:27:38 +03:00
<!-- This is a semi-strict format, you can add/remove sections as needed but the order/format should be kept the same Remove these comments before submitting --> # Description <!-- Explain this PR in as much detail as applicable Some example prompts to consider: How might this affect the game? The codebase? What might be some alternatives to this? How/Who does this benefit/hurt [the game/codebase]? --> A cheap disposable syringe. It can draw once, inject once, and is then rendered unusable. The point of the syringe is to let Chemists pack specific dosages of specific chemicals into a simple syringe. A doctor can then not accidentally alter a dosage during treatments. These cheap syringes live alongside normal syringes. They use fewer materials to make, and are printed much faster. --- <!-- This is default collapsed, readers click to expand it and see all your media The PR media section can get very large at times, so this is a good way to keep it clean The title is written using HTML tags The title must be within the <summary> tags or you won't see it --> <details><summary><h1>Media</h1></summary> <p> https://github.com/user-attachments/assets/eb439050-b86d-49ba-b95e-22ab271f2358 https://github.com/user-attachments/assets/9e8954ec-11c9-4569-820e-08b91e09f52b </p> </details> --- # Changelog <!-- You can add an author after the `🆑` to change the name that appears in the changelog (ex: `🆑 Death`) Leaving it blank will default to your GitHub display name This includes all available types for the changelog --> 🆑 - add: Added disposable syringes! These cheaper, quicker-to-produce syringes let Chemists pick dosages while preparing medicine so that doctors can inject without worrying about volume, but the syringes cannot be used more than once. (cherry picked from commit 5516dda48db268f5685e2c926d3dcbdb74ac7e1d)
439 lines
18 KiB
C#
439 lines
18 KiB
C#
using Content.Server.Abilities.Chitinid;
|
|
using Content.Server.Body.Components;
|
|
using Content.Server.Body.Systems;
|
|
using Content.Server.Chat.Managers;
|
|
using Content.Shared.Chat;
|
|
using Content.Shared.Chemistry;
|
|
using Content.Shared.Chemistry.Components;
|
|
using Content.Shared.Chemistry.Components.SolutionManager;
|
|
using Content.Shared.Chemistry.EntitySystems;
|
|
using Content.Shared.Chemistry.Reagent;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.DoAfter;
|
|
using Content.Shared.FixedPoint;
|
|
using Content.Shared.Forensics;
|
|
using Content.Shared.IdentityManagement;
|
|
using Content.Shared.Interaction;
|
|
using Content.Shared.Mobs.Components;
|
|
using Content.Shared.Stacks;
|
|
using Robust.Server.Player;
|
|
|
|
namespace Content.Server.Chemistry.EntitySystems;
|
|
|
|
public sealed class FillableOneTimeInjectorSystem : SharedFillableOneTimeInjectorSystem
|
|
{
|
|
[Dependency] private readonly BloodstreamSystem _blood = default!;
|
|
[Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
|
|
[Dependency] private readonly IChatManager _chat = default!;
|
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
|
|
private const ChatChannel BlockInjectionDenyChannel = ChatChannel.Emotes;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
SubscribeLocalEvent<FillableOneTimeInjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
|
|
SubscribeLocalEvent<FillableOneTimeInjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
|
|
}
|
|
|
|
private void UseInjector(Entity<FillableOneTimeInjectorComponent> injector, EntityUid target, EntityUid user)
|
|
{
|
|
bool isDrawing = injector.Comp.ToggleState == FillableOneTimeInjectorToggleMode.Draw;
|
|
|
|
// Is the one-time use fillable injector already spent?
|
|
if (injector.Comp.ToggleState == FillableOneTimeInjectorToggleMode.Spent)
|
|
return;
|
|
|
|
// Handle injecting/drawing for solutions
|
|
if (!isDrawing)
|
|
{
|
|
if (SolutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
|
|
{
|
|
TryInject(injector, target, injectableSolution.Value, user, false);
|
|
}
|
|
else if (SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
|
|
{
|
|
TryInject(injector, target, refillableSolution.Value, user, true);
|
|
}
|
|
else if (TryComp<BloodstreamComponent>(target, out var bloodstream))
|
|
{
|
|
TryInjectIntoBloodstream(injector, (target, bloodstream), user);
|
|
}
|
|
else
|
|
{
|
|
Popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
|
|
("target", Identity.Entity(target, EntityManager))), injector, user);
|
|
}
|
|
}
|
|
else if (isDrawing)
|
|
{
|
|
// Draw from a bloodstream, if the target has that
|
|
if (TryComp<BloodstreamComponent>(target, out var stream) &&
|
|
SolutionContainers.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution))
|
|
{
|
|
TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
|
|
return;
|
|
}
|
|
|
|
// Draw from an object (food, beaker, etc)
|
|
if (SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
|
|
{
|
|
TryDraw(injector, target, drawableSolution.Value, user);
|
|
}
|
|
else
|
|
{
|
|
Popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
|
|
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnInjectDoAfter(Entity<FillableOneTimeInjectorComponent> entity, ref InjectorDoAfterEvent args)
|
|
{
|
|
if (args.Cancelled || args.Handled || args.Args.Target == null || entity.Comp.ToggleState == FillableOneTimeInjectorToggleMode.Spent)
|
|
return;
|
|
|
|
UseInjector(entity, args.Args.Target.Value, args.Args.User);
|
|
args.Handled = true;
|
|
}
|
|
|
|
private void OnInjectorAfterInteract(Entity<FillableOneTimeInjectorComponent> entity, ref AfterInteractEvent args)
|
|
{
|
|
if (args.Handled || !args.CanReach || entity.Comp.ToggleState == FillableOneTimeInjectorToggleMode.Spent)
|
|
return;
|
|
|
|
//Make sure we have the attacking entity
|
|
if (args.Target is not { Valid: true } target || !HasComp<SolutionContainerManagerComponent>(entity))
|
|
return;
|
|
|
|
// Is the target a mob? If yes, use a do-after to give them time to respond.
|
|
if (HasComp<MobStateComponent>(target) || HasComp<BloodstreamComponent>(target))
|
|
{
|
|
InjectDoAfter(entity, target, args.User);
|
|
args.Handled = true;
|
|
return;
|
|
}
|
|
|
|
UseInjector(entity, target, args.User);
|
|
args.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send informative pop-up messages and wait for a do-after to complete.
|
|
/// </summary>
|
|
private void InjectDoAfter(Entity<FillableOneTimeInjectorComponent> injector, EntityUid target, EntityUid user)
|
|
{
|
|
if (injector.Comp.ToggleState == FillableOneTimeInjectorToggleMode.Spent)
|
|
return;
|
|
|
|
if (TryComp<BlockInjectionComponent>(target, out var blockComponent)) // DeltaV
|
|
{
|
|
var msg = Loc.GetString($"injector-component-deny-{blockComponent.BlockReason}");
|
|
Popup.PopupEntity(msg, target, user);
|
|
|
|
if (!_playerManager.TryGetSessionByEntity(target, out var session))
|
|
return;
|
|
|
|
_chat.ChatMessageToOne(
|
|
BlockInjectionDenyChannel,
|
|
msg,
|
|
msg,
|
|
EntityUid.Invalid,
|
|
false,
|
|
session.Channel);
|
|
return;
|
|
}
|
|
|
|
bool isDrawing = injector.Comp.ToggleState == FillableOneTimeInjectorToggleMode.Draw;
|
|
|
|
// Create a pop-up for the user
|
|
if (isDrawing)
|
|
{
|
|
Popup.PopupEntity(Loc.GetString("injector-component-drawing-user"), target, user);
|
|
}
|
|
else
|
|
{
|
|
Popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user);
|
|
}
|
|
|
|
if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out _, out var solution))
|
|
return;
|
|
|
|
var actualDelay = MathHelper.Max(injector.Comp.Delay, TimeSpan.FromSeconds(1));
|
|
float amountToInject;
|
|
if (isDrawing)
|
|
{
|
|
// additional delay is based on actual volume left to draw in syringe when smaller than transfer amount
|
|
amountToInject = Math.Min(injector.Comp.TransferAmount.Float(), (solution.MaxVolume - solution.Volume).Float());
|
|
}
|
|
else
|
|
{
|
|
// additional delay is based on actual volume left to inject in syringe when smaller than transfer amount
|
|
amountToInject = Math.Min(injector.Comp.TransferAmount.Float(), solution.Volume.Float());
|
|
}
|
|
|
|
// Injections take 0.5 seconds longer per 5u of possible space/content
|
|
actualDelay += TimeSpan.FromSeconds(amountToInject / 10);
|
|
|
|
var isTarget = user != target;
|
|
|
|
if (isTarget)
|
|
{
|
|
// Create a pop-up for the target
|
|
var userName = Identity.Entity(user, EntityManager);
|
|
if (isDrawing)
|
|
{
|
|
Popup.PopupEntity(Loc.GetString("injector-component-drawing-target",
|
|
("user", userName)), user, target);
|
|
}
|
|
else
|
|
{
|
|
Popup.PopupEntity(Loc.GetString("injector-component-injecting-target",
|
|
("user", userName)), user, target);
|
|
}
|
|
|
|
// Check if the target is incapacitated or in combat mode and modify time accordingly.
|
|
if (MobState.IsIncapacitated(target))
|
|
{
|
|
actualDelay /= 2.5f;
|
|
}
|
|
else if (Combat.IsInCombatMode(target))
|
|
{
|
|
// Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
|
|
// combat with fast syringes & lag.
|
|
actualDelay += TimeSpan.FromSeconds(1);
|
|
}
|
|
|
|
// Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
|
|
if (!isDrawing)
|
|
{
|
|
AdminLogger.Add(LogType.ForceFeed,
|
|
$"{EntityManager.ToPrettyString(user):user} is attempting to inject {EntityManager.ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}");
|
|
}
|
|
else
|
|
{
|
|
AdminLogger.Add(LogType.ForceFeed,
|
|
$"{EntityManager.ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from {EntityManager.ToPrettyString(target):target}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Self-injections take half as long.
|
|
actualDelay /= 2;
|
|
|
|
if (!isDrawing)
|
|
{
|
|
AdminLogger.Add(LogType.Ingestion,
|
|
$"{EntityManager.ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}.");
|
|
}
|
|
else
|
|
{
|
|
AdminLogger.Add(LogType.ForceFeed,
|
|
$"{EntityManager.ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from themselves.");
|
|
}
|
|
}
|
|
|
|
DoAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner)
|
|
{
|
|
BreakOnMove = true,
|
|
BreakOnWeightlessMove = false,
|
|
BreakOnDamage = true,
|
|
NeedHand = true,
|
|
BreakOnHandChange = true,
|
|
MovementThreshold = 0.1f,
|
|
});
|
|
}
|
|
|
|
private void TryInjectIntoBloodstream(Entity<FillableOneTimeInjectorComponent> injector, Entity<BloodstreamComponent> target,
|
|
EntityUid user)
|
|
{
|
|
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
|
if (!SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
|
|
ref target.Comp.ChemicalSolution, out var chemSolution) || injector.Comp.ToggleState != FillableOneTimeInjectorToggleMode.Inject)
|
|
{
|
|
Popup.PopupEntity(
|
|
Loc.GetString("injector-component-cannot-inject-message",
|
|
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
|
|
return;
|
|
}
|
|
|
|
if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
|
|
out var solution) || solution.Volume == 0)
|
|
return;
|
|
|
|
var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, chemSolution.AvailableVolume);
|
|
if (realTransferAmount <= 0)
|
|
{
|
|
Popup.PopupEntity(
|
|
Loc.GetString("injector-component-cannot-inject-message",
|
|
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
|
|
return;
|
|
}
|
|
|
|
// Move units from attackSolution to targetSolution
|
|
var removedSolution = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount);
|
|
|
|
_blood.TryAddToChemicals(target, removedSolution, target.Comp);
|
|
|
|
_reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
|
|
|
|
Popup.PopupEntity(Loc.GetString("injector-component-inject-success-message",
|
|
("amount", removedSolution.Volume),
|
|
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
|
|
|
|
Dirty(injector);
|
|
AfterInject(injector, target);
|
|
}
|
|
|
|
private void TryInject(Entity<FillableOneTimeInjectorComponent> injector, EntityUid targetEntity,
|
|
Entity<SolutionComponent> targetSolution, EntityUid user, bool asRefill)
|
|
{
|
|
if (HasComp<BlockInjectionComponent>(targetEntity)) // DeltaV
|
|
return;
|
|
|
|
if (injector.Comp.ToggleState == FillableOneTimeInjectorToggleMode.Spent)
|
|
return;
|
|
|
|
if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
|
|
out var solution) || solution.Volume == 0)
|
|
return;
|
|
|
|
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
|
var realTransferAmount =
|
|
FixedPoint2.Min(injector.Comp.TransferAmount, targetSolution.Comp.Solution.AvailableVolume);
|
|
|
|
if (realTransferAmount <= 0)
|
|
{
|
|
Popup.PopupEntity(
|
|
Loc.GetString("injector-component-target-already-full-message",
|
|
("target", Identity.Entity(targetEntity, EntityManager))),
|
|
injector.Owner, user);
|
|
return;
|
|
}
|
|
|
|
// Move units from attackSolution to targetSolution
|
|
Solution removedSolution;
|
|
if (TryComp<StackComponent>(targetEntity, out var stack))
|
|
removedSolution = SolutionContainers.SplitStackSolution(soln.Value, realTransferAmount, stack.Count);
|
|
else
|
|
removedSolution = SolutionContainers.SplitSolution(soln.Value, realTransferAmount);
|
|
|
|
_reactiveSystem.DoEntityReaction(targetEntity, removedSolution, ReactionMethod.Injection);
|
|
|
|
if (!asRefill)
|
|
SolutionContainers.Inject(targetEntity, targetSolution, removedSolution);
|
|
else
|
|
SolutionContainers.Refill(targetEntity, targetSolution, removedSolution);
|
|
|
|
Popup.PopupEntity(Loc.GetString("injector-component-transfer-success-message",
|
|
("amount", removedSolution.Volume),
|
|
("target", Identity.Entity(targetEntity, EntityManager))), injector.Owner, user);
|
|
|
|
Dirty(injector);
|
|
AfterInject(injector, targetEntity);
|
|
}
|
|
|
|
private void AfterInject(Entity<FillableOneTimeInjectorComponent> injector, EntityUid target)
|
|
{
|
|
// Automatically set syringe to draw after draining it.
|
|
if (SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var user,
|
|
out var solution) && solution.Volume == 0)
|
|
{
|
|
SetMode(injector, FillableOneTimeInjectorToggleMode.Spent);
|
|
Popup.PopupClient(Loc.GetString("injector-spent-text"), injector, user);
|
|
}
|
|
// Leave some DNA from the injectee on it
|
|
var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
|
|
RaiseLocalEvent(target, ref ev);
|
|
}
|
|
|
|
private void AfterDraw(Entity<FillableOneTimeInjectorComponent> injector, EntityUid target)
|
|
{
|
|
// Automatically set syringe to inject after filling it completely.
|
|
if (SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var user,
|
|
out var solution) && solution.AvailableVolume == 0)
|
|
{
|
|
SetMode(injector, FillableOneTimeInjectorToggleMode.Inject);
|
|
Popup.PopupClient(Loc.GetString("injector-component-injecting-locked-text"), injector, user);
|
|
}
|
|
|
|
// Leave some DNA from the drawee on it
|
|
var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
|
|
RaiseLocalEvent(target, ref ev);
|
|
}
|
|
|
|
private void TryDraw(Entity<FillableOneTimeInjectorComponent> injector, Entity<BloodstreamComponent?> target,
|
|
Entity<SolutionComponent> targetSolution, EntityUid user)
|
|
{
|
|
if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
|
|
out var solution) || solution.AvailableVolume == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
|
|
var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, targetSolution.Comp.Solution.Volume,
|
|
solution.AvailableVolume);
|
|
|
|
if (realTransferAmount <= 0)
|
|
{
|
|
Popup.PopupEntity(
|
|
Loc.GetString("injector-component-target-is-empty-message",
|
|
("target", Identity.Entity(target, EntityManager))),
|
|
injector.Owner, user);
|
|
return;
|
|
}
|
|
|
|
// We have some snowflaked behavior for streams.
|
|
if (target.Comp != null)
|
|
{
|
|
DrawFromBlood(injector, (target.Owner, target.Comp), soln.Value, realTransferAmount, user);
|
|
return;
|
|
}
|
|
|
|
// Move units from attackSolution to targetSolution
|
|
var removedSolution = SolutionContainers.Draw(target.Owner, targetSolution, realTransferAmount);
|
|
|
|
if (!SolutionContainers.TryAddSolution(soln.Value, removedSolution))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
|
|
("amount", removedSolution.Volume),
|
|
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
|
|
|
|
Dirty(injector);
|
|
AfterDraw(injector, target);
|
|
}
|
|
|
|
private void DrawFromBlood(Entity<FillableOneTimeInjectorComponent> injector, Entity<BloodstreamComponent> target,
|
|
Entity<SolutionComponent> injectorSolution, FixedPoint2 transferAmount, EntityUid user)
|
|
{
|
|
var drawAmount = (float) transferAmount;
|
|
|
|
if (SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
|
|
ref target.Comp.ChemicalSolution))
|
|
{
|
|
var chemTemp = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, drawAmount * 0.15f);
|
|
SolutionContainers.TryAddSolution(injectorSolution, chemTemp);
|
|
drawAmount -= (float) chemTemp.Volume;
|
|
}
|
|
|
|
if (SolutionContainers.ResolveSolution(target.Owner, target.Comp.BloodSolutionName,
|
|
ref target.Comp.BloodSolution))
|
|
{
|
|
var bloodTemp = SolutionContainers.SplitSolution(target.Comp.BloodSolution.Value, drawAmount);
|
|
SolutionContainers.TryAddSolution(injectorSolution, bloodTemp);
|
|
}
|
|
|
|
Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
|
|
("amount", transferAmount),
|
|
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
|
|
|
|
Dirty(injector);
|
|
AfterDraw(injector, target);
|
|
}
|
|
}
|