diff --git a/Content.Client/Backmen/Disease/DiseaseMachineSystem.cs b/Content.Client/Backmen/Disease/DiseaseMachineSystem.cs
new file mode 100644
index 00000000000..f42c587dd50
--- /dev/null
+++ b/Content.Client/Backmen/Disease/DiseaseMachineSystem.cs
@@ -0,0 +1,26 @@
+using Content.Client.Medical;
+using Content.Shared.Backmen.Disease;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Backmen.Disease;
+
+///
+/// Controls client-side visuals for the
+/// disease machines.
+///
+public sealed class DiseaseMachineSystem : VisualizerSystem
+{
+ protected override void OnAppearanceChange(EntityUid uid, DiseaseMachineVisualsComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null)
+ return;
+
+ if (AppearanceSystem.TryGetData(uid, DiseaseMachineVisuals.IsOn, out var isOn, args.Component)
+ && AppearanceSystem.TryGetData(uid, DiseaseMachineVisuals.IsRunning, out var isRunning, args.Component))
+ {
+ var state = isRunning ? component.RunningState : component.IdleState;
+ args.Sprite.LayerSetVisible(DiseaseMachineVisualLayers.IsOn, isOn);
+ args.Sprite.LayerSetState(DiseaseMachineVisualLayers.IsRunning, state);
+ }
+ }
+}
diff --git a/Content.Client/Backmen/Disease/DiseaseMachineVisualsComponent.cs b/Content.Client/Backmen/Disease/DiseaseMachineVisualsComponent.cs
new file mode 100644
index 00000000000..a7624d202e0
--- /dev/null
+++ b/Content.Client/Backmen/Disease/DiseaseMachineVisualsComponent.cs
@@ -0,0 +1,15 @@
+namespace Content.Client.Backmen.Disease;
+
+///
+/// Holds the idle and running state for machines to control
+/// playing animtions on the client.
+///
+[RegisterComponent]
+public sealed partial class DiseaseMachineVisualsComponent : Component
+{
+ [DataField("idleState", required: true)]
+ public string IdleState = default!;
+
+ [DataField("runningState", required: true)]
+ public string RunningState = default!;
+}
diff --git a/Content.Client/Backmen/Disease/UI/VaccineMachineBoundUserInterface.cs b/Content.Client/Backmen/Disease/UI/VaccineMachineBoundUserInterface.cs
new file mode 100644
index 00000000000..9cc96a24a3f
--- /dev/null
+++ b/Content.Client/Backmen/Disease/UI/VaccineMachineBoundUserInterface.cs
@@ -0,0 +1,67 @@
+using Content.Shared.Backmen.Disease;
+using JetBrains.Annotations;
+
+namespace Content.Client.Backmen.Disease.UI;
+
+[UsedImplicitly]
+public sealed class VaccineMachineBoundUserInterface : BoundUserInterface
+{
+ private VaccineMachineMenu? _machineMenu;
+
+ public VaccineMachineBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _machineMenu = new VaccineMachineMenu(this);
+
+ _machineMenu.OnClose += Close;
+
+ _machineMenu.OnServerSelectionButtonPressed += _ =>
+ {
+ SendMessage(new VaccinatorServerSelectionMessage());
+ };
+
+ _machineMenu.OpenCentered();
+ _machineMenu?.PopulateBiomass(Owner);
+ }
+
+ public void CreateVaccineMessage(string disease, int amount)
+ {
+ SendMessage(new CreateVaccineMessage(disease, amount));
+ }
+
+ public void RequestSync()
+ {
+ SendMessage(new VaccinatorSyncRequestMessage());
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ switch (state)
+ {
+ case VaccineMachineUpdateState msg:
+ _machineMenu?.UpdateLocked(msg.Locked);
+ _machineMenu?.PopulateDiseases(msg.Diseases);
+ _machineMenu?.PopulateBiomass(Owner);
+ _machineMenu?.UpdateCost(msg.BiomassCost);
+ _machineMenu?.UpdateServerConnection(msg.HasServer);
+ break;
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (!disposing)
+ return;
+
+ _machineMenu?.Dispose();
+ }
+}
diff --git a/Content.Client/Backmen/Disease/UI/VaccineMachineMenu.xaml b/Content.Client/Backmen/Disease/UI/VaccineMachineMenu.xaml
new file mode 100644
index 00000000000..6d0653912a0
--- /dev/null
+++ b/Content.Client/Backmen/Disease/UI/VaccineMachineMenu.xaml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Backmen/Disease/UI/VaccineMachineMenu.xaml.cs b/Content.Client/Backmen/Disease/UI/VaccineMachineMenu.xaml.cs
new file mode 100644
index 00000000000..8a04eff89f8
--- /dev/null
+++ b/Content.Client/Backmen/Disease/UI/VaccineMachineMenu.xaml.cs
@@ -0,0 +1,136 @@
+using Content.Shared.Materials;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Backmen.Disease.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class VaccineMachineMenu : DefaultWindow
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+
+ private readonly SharedMaterialStorageSystem _storage;
+
+ public VaccineMachineBoundUserInterface Owner { get; }
+
+ public event Action? OnServerSelectionButtonPressed;
+
+ private List<(string id, string name)> _knownDiseasePrototypes = new();
+ public (string id, string name) DiseaseSelected;
+ public bool Enough = false;
+ public bool Locked = false;
+ public int Cost => CreateAmount.Value * CostPer;
+ public int CostPer = 4;
+
+ public VaccineMachineMenu(VaccineMachineBoundUserInterface owner)
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _storage = _entityManager.EntitySysManager.GetEntitySystem();
+
+ Owner = owner;
+
+ ServerSelectionButton.OnPressed += a => OnServerSelectionButtonPressed?.Invoke(a);
+ DiseaseSelected = ("", ""); // nullability was a bit sussy so
+
+ KnownDiseases.OnItemSelected += KnownDiseaseSelected;
+ CreateAmount.ValueChanged += HandleAmountChanged;
+
+ CreateButton.OnPressed += _ =>
+ {
+ CreateVaccine();
+ };
+ ServerSyncButton.OnPressed += _ =>
+ {
+ RequestSync();
+ };
+ }
+
+ ///
+ /// Called when a known disease is selected.
+ ///
+ private void KnownDiseaseSelected(ItemList.ItemListSelectedEventArgs obj)
+ {
+ DiseaseSelected = _knownDiseasePrototypes[obj.ItemIndex];
+ CreateButton.Disabled = !Enough || Locked;
+
+ PopulateSelectedDisease();
+ }
+
+ ///
+ /// Sends a message to create a vaccine of the selected disease.
+ ///
+ private void CreateVaccine()
+ {
+ if (DiseaseSelected == ("", ""))
+ return;
+
+ if (CreateAmount.Value <= 0)
+ return;
+
+ Owner.CreateVaccineMessage(DiseaseSelected.id, CreateAmount.Value);
+ }
+
+ private void RequestSync()
+ {
+ Owner.RequestSync();
+ }
+
+ public void PopulateDiseases(List<(string id, string name)> diseases)
+ {
+ KnownDiseases.Clear();
+
+ _knownDiseasePrototypes.Clear();
+
+ foreach (var disease in diseases)
+ {
+ KnownDiseases.AddItem(Loc.GetString(disease.name));
+ _knownDiseasePrototypes.Add((disease.id, Loc.GetString(disease.name)));
+ }
+ }
+
+ public void UpdateCost(int costPer)
+ {
+ CostPer = costPer;
+ BiomassCost.Text = Loc.GetString("vaccine-machine-menu-biomass-cost", ("value", Cost));
+ }
+
+ public void UpdateLocked(bool locked)
+ {
+ Locked = locked;
+ }
+
+ public void UpdateServerConnection(bool hasServer)
+ {
+ ServerSyncButton.Disabled = !hasServer;
+ }
+
+ private void HandleAmountChanged(ValueChangedEventArgs args)
+ {
+ UpdateCost(CostPer);
+ }
+
+ public void PopulateSelectedDisease()
+ {
+ if (DiseaseSelected == ("", ""))
+ {
+ CreateButton.Disabled = true;
+ DiseaseName.Text = Loc.GetString("vaccine-machine-menu-none-selected");
+ return;
+ }
+
+ DiseaseName.Text = DiseaseSelected.name;
+ }
+
+ public void PopulateBiomass(EntityUid machine)
+ {
+ var amt = _storage.GetMaterialAmount(machine, "Biomass");
+ Enough = (amt > Cost);
+ BiomassCurrent.Text = Loc.GetString("vaccine-machine-menu-biomass-current", ("value", amt));
+
+ if (DiseaseSelected != ("", ""))
+ CreateButton.Disabled = !Enough || Locked;
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Backmen/Disease/TryAddDisease.cs b/Content.IntegrationTests/Tests/Backmen/Disease/TryAddDisease.cs
new file mode 100644
index 00000000000..fd9e1efd614
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Backmen/Disease/TryAddDisease.cs
@@ -0,0 +1,79 @@
+using System.Linq;
+using Content.Server.Backmen.Disease;
+using Content.Shared.Backmen.Disease;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Backmen.Disease;
+
+[TestFixture]
+[TestOf(typeof(DiseaseSystem))]
+public sealed class DiseaseTest
+{
+ [Test]
+ public async Task AddAllDiseases()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var protoManager = server.ResolveDependency();
+ var entManager = server.ResolveDependency();
+ var entSysManager = server.ResolveDependency();
+ var diseaseSystem = entSysManager.GetEntitySystem();
+
+ var sickEntity = EntityUid.Invalid;
+
+ await server.WaitAssertion(() =>
+ {
+ sickEntity = entManager.SpawnEntity("MobHuman", MapCoordinates.Nullspace);
+ if(!entManager.HasComponent(sickEntity))
+ Assert.Fail("MobHuman has not DiseaseCarrierComponent");
+ });
+
+
+ foreach (var diseaseProto in protoManager.EnumeratePrototypes())
+ {
+ await server.WaitAssertion(() =>
+ {
+ diseaseSystem.TryAddDisease(sickEntity, diseaseProto.ID);
+ });
+ await server.WaitIdleAsync();
+ server.RunTicks(5);
+ await server.WaitAssertion(() =>
+ {
+ if(!entManager.HasComponent(sickEntity))
+ Assert.Fail("MobHuman has not DiseasedComponent");
+ });
+ if (!entManager.TryGetComponent(sickEntity, out var diseaseCarrierComponent))
+ {
+ Assert.Fail("MobHuman has not DiseaseCarrierComponent");
+ }
+
+ if (diseaseCarrierComponent.Diseases.All(x => x.ID != diseaseProto.ID))
+ {
+ Assert.Fail("Disease not apply");
+ }
+ await server.WaitAssertion(() =>
+ {
+ diseaseSystem.CureDisease((sickEntity,diseaseCarrierComponent), diseaseProto.ID);
+ });
+ await server.WaitIdleAsync();
+ server.RunTicks(1);
+ await server.WaitAssertion(() =>
+ {
+ if (diseaseCarrierComponent.Diseases.Any(x => x.ID == diseaseProto.ID))
+ {
+ Assert.Fail("Disease not remove");
+ }
+ if (diseaseCarrierComponent.PastDiseases.All(x => x != diseaseProto.ID))
+ {
+ Assert.Fail("Disease immunu not apply");
+ }
+ });
+ }
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.Server/Backmen/Administration/Commands/Toolshed/AddDiseaseCommand.cs b/Content.Server/Backmen/Administration/Commands/Toolshed/AddDiseaseCommand.cs
new file mode 100644
index 00000000000..9b3016b2e27
--- /dev/null
+++ b/Content.Server/Backmen/Administration/Commands/Toolshed/AddDiseaseCommand.cs
@@ -0,0 +1,36 @@
+using System.Linq;
+using Content.Server.Administration;
+using Content.Server.Backmen.Disease;
+using Content.Server.Polymorph.Systems;
+using Content.Shared.Administration;
+using Content.Shared.Backmen.Disease;
+using Content.Shared.Polymorph;
+using Robust.Shared.Toolshed;
+using Robust.Shared.Toolshed.TypeParsers;
+
+namespace Content.Server.Backmen.Administration.Commands.Toolshed;
+
+[ToolshedCommand, AdminCommand(AdminFlags.Fun)]
+public sealed class AddDiseaseCommand : ToolshedCommand
+{
+ private DiseaseSystem? _diseaseSystem;
+
+ [CommandImplementation]
+ public EntityUid? AddDisease(
+ [PipedArgument] EntityUid input,
+ [CommandArgument] Prototype prototype
+ )
+ {
+ _diseaseSystem ??= GetSys();
+
+ _diseaseSystem.TryAddDisease(input, prototype.Value.ID);
+ return input;
+ }
+
+ [CommandImplementation]
+ public IEnumerable AddDisease(
+ [PipedArgument] IEnumerable input,
+ [CommandArgument] Prototype prototype
+ )
+ => input.Select(x => AddDisease(x, prototype)).Where(x => x is not null).Select(x => (EntityUid) x!);
+}
diff --git a/Content.Server/Backmen/Administration/Commands/Toolshed/ChangeSexCommand.cs b/Content.Server/Backmen/Administration/Commands/Toolshed/ChangeSexCommand.cs
new file mode 100644
index 00000000000..f0e083e597b
--- /dev/null
+++ b/Content.Server/Backmen/Administration/Commands/Toolshed/ChangeSexCommand.cs
@@ -0,0 +1,126 @@
+using System.Collections;
+using System.Diagnostics;
+using System.Linq;
+using Content.Server.Administration;
+using Content.Server.Humanoid;
+using Content.Shared.Administration;
+using Content.Shared.Humanoid;
+using Robust.Shared.Enums;
+using Robust.Shared.GameObjects.Components.Localization;
+using Robust.Shared.Toolshed;
+using Robust.Shared.Toolshed.Errors;
+using Robust.Shared.Toolshed.Syntax;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Backmen.Administration.Commands.Toolshed;
+
+[ToolshedCommand, AdminCommand(AdminFlags.Fun)]
+public sealed class ChangeSexCommand : ToolshedCommand
+{
+ private HumanoidAppearanceSystem? _appearanceSystem;
+
+ #region base
+
+ private EntityUid? ChangeSex(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] EntityUid input,
+ Sex sex
+ )
+ {
+ if (!EntityManager.TryGetComponent(input, out var humanoidAppearanceComponent))
+ {
+ ctx.ReportError(new NotHumanoidError());
+ return null;
+ }
+
+ _appearanceSystem ??= GetSys();
+
+ humanoidAppearanceComponent.Gender = sex switch
+ {
+ Sex.Male => Gender.Male,
+ Sex.Female => Gender.Female,
+ Sex.Unsexed => Gender.Neuter,
+ _ => Gender.Epicene
+ };
+
+ if (EntityManager.TryGetComponent(input, out var grammarComponent))
+ {
+ grammarComponent.Gender = humanoidAppearanceComponent.Gender;
+ EntityManager.Dirty(input, grammarComponent);
+ }
+
+ _appearanceSystem.SetSex(input, sex, humanoid: humanoidAppearanceComponent);
+ EntityManager.Dirty(input, humanoidAppearanceComponent);
+ return input;
+ }
+
+ private IEnumerable ChangeSex(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] IEnumerable input,
+ Sex sex
+ )
+ => input.Select(x => ChangeSex(ctx, x, sex)).Where(x => x is not null).Select(x => (EntityUid) x!);
+
+ #endregion
+
+ [CommandImplementation("setMale")]
+ public EntityUid? SetMale(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] EntityUid input)
+ {
+ return ChangeSex(ctx, input, Sex.Male);
+ }
+
+ [CommandImplementation("setMale")]
+ public IEnumerable SetMale(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] IEnumerable input)
+ {
+ return ChangeSex(ctx, input, Sex.Male);
+ }
+
+ [CommandImplementation("setFemale")]
+ public EntityUid? SetFemale(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] EntityUid input)
+ {
+ return ChangeSex(ctx, input, Sex.Female);
+ }
+
+ [CommandImplementation("setFemale")]
+ public IEnumerable SetFemale(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] IEnumerable input)
+ {
+ return ChangeSex(ctx, input, Sex.Female);
+ }
+
+ [CommandImplementation("setUnsexed")]
+ public EntityUid? SetUnsexed(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] EntityUid input)
+ {
+ return ChangeSex(ctx, input, Sex.Unsexed);
+ }
+
+ [CommandImplementation("setUnsexed")]
+ public IEnumerable SetUnsexed(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] IEnumerable input)
+ {
+ return ChangeSex(ctx, input, Sex.Unsexed);
+ }
+}
+
+public record struct NotHumanoidError : IConError
+{
+ public FormattedMessage DescribeInner()
+ {
+ return FormattedMessage.FromMarkup(
+ "У сущности нет компонента HumanoidAppearanceComponent.");
+ }
+
+ public string? Expression { get; set; }
+ public Vector2i? IssueSpan { get; set; }
+ public StackTrace? Trace { get; set; }
+}
diff --git a/Content.Server/Backmen/Administration/Commands/Toolshed/ChangeTTSCommand.cs b/Content.Server/Backmen/Administration/Commands/Toolshed/ChangeTTSCommand.cs
new file mode 100644
index 00000000000..3d806d495f8
--- /dev/null
+++ b/Content.Server/Backmen/Administration/Commands/Toolshed/ChangeTTSCommand.cs
@@ -0,0 +1,46 @@
+using System.Linq;
+using Content.Server.Administration;
+using Content.Server.Backmen.Disease;
+using Content.Server.Corvax.TTS;
+using Content.Server.Humanoid;
+using Content.Shared.Administration;
+using Content.Shared.Backmen.Disease;
+using Content.Shared.Corvax.TTS;
+using Content.Shared.Humanoid;
+using Robust.Shared.Toolshed;
+using Robust.Shared.Toolshed.TypeParsers;
+
+namespace Content.Server.Backmen.Administration.Commands.Toolshed;
+
+[ToolshedCommand, AdminCommand(AdminFlags.Fun)]
+public sealed class ChangeTTSCommand : ToolshedCommand
+{
+ private HumanoidAppearanceSystem? _appearanceSystem;
+
+ [CommandImplementation]
+ public EntityUid? ChangeTTS(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] EntityUid input,
+ [CommandArgument] Prototype prototype
+ )
+ {
+ if (!EntityManager.TryGetComponent(input, out var humanoidAppearanceComponent))
+ {
+ ctx.ReportError(new NotHumanoidError());
+ return null;
+ }
+
+ _appearanceSystem ??= GetSys();
+
+ _appearanceSystem.SetTTSVoice(input, prototype.Value.ID, humanoid: humanoidAppearanceComponent);
+ return input;
+ }
+
+ [CommandImplementation]
+ public IEnumerable ChangeTTS(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] IEnumerable input,
+ [CommandArgument] Prototype prototype
+ )
+ => input.Select(x => ChangeTTS(ctx, x, prototype)).Where(x => x is not null).Select(x => (EntityUid) x!);
+}
diff --git a/Content.Server/Backmen/Administration/Commands/Toolshed/MakeFakeFingerprintCommand.cs b/Content.Server/Backmen/Administration/Commands/Toolshed/MakeFakeFingerprintCommand.cs
new file mode 100644
index 00000000000..9e110060151
--- /dev/null
+++ b/Content.Server/Backmen/Administration/Commands/Toolshed/MakeFakeFingerprintCommand.cs
@@ -0,0 +1,70 @@
+using System.Linq;
+using Content.Server.Administration;
+using Content.Server.Forensics;
+using Content.Shared.Administration;
+using Content.Shared.Inventory;
+using Robust.Shared.Player;
+using Robust.Shared.Toolshed;
+using Robust.Shared.Toolshed.Errors;
+using Robust.Shared.Toolshed.Syntax;
+
+namespace Content.Server.Backmen.Administration.Commands.Toolshed;
+
+[ToolshedCommand, AdminCommand(AdminFlags.Fun)]
+public sealed class MakeFakeFingerprintCommand : ToolshedCommand
+{
+ private InventorySystem? _inventory;
+
+ [CommandImplementation]
+ public EntityUid? MakeFakeFingerprint(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] EntityUid target,
+ [CommandArgument] ValueRef playerRef
+ )
+ {
+ var player = playerRef.Evaluate(ctx);
+ if (player is null || player.AttachedEntity is null)
+ {
+ ctx.ReportError(new NotForServerConsoleError());
+ return target;
+ }
+
+ var playerUid = player.AttachedEntity.Value;
+
+
+ var f = EntityManager.EnsureComponent(target);
+ if (EntityManager.TryGetComponent(playerUid, out var dna))
+ {
+ f.DNAs.Add(dna.DNA);
+ }
+
+ _inventory ??= GetSys();
+ if (_inventory.TryGetSlotEntity(playerUid, "gloves", out var gloves))
+ {
+ if (EntityManager.TryGetComponent(gloves, out var fiber) &&
+ !string.IsNullOrEmpty(fiber.FiberMaterial))
+ {
+ f.Fibers.Add(string.IsNullOrEmpty(fiber.FiberColor)
+ ? Loc.GetString("forensic-fibers", ("material", fiber.FiberMaterial))
+ : Loc.GetString("forensic-fibers-colored", ("color", fiber.FiberColor),
+ ("material", fiber.FiberMaterial)));
+ }
+ }
+
+ if (EntityManager.TryGetComponent(playerUid, out var fingerprint))
+ {
+ f.Fingerprints.Add(fingerprint.Fingerprint ?? "");
+ }
+
+ return target;
+ }
+
+ [CommandImplementation]
+ public IEnumerable MakeFakeFingerprint(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] IEnumerable target,
+ [CommandArgument] ValueRef playerRef
+ )
+ => target.Select(x => MakeFakeFingerprint(ctx, x, playerRef)).Where(x => x is not null)
+ .Select(x => (EntityUid) x!);
+}
diff --git a/Content.Server/Backmen/Chemistry/ReagentEffects/ChemCauseDisease.cs b/Content.Server/Backmen/Chemistry/ReagentEffects/ChemCauseDisease.cs
new file mode 100644
index 00000000000..1ad409729ff
--- /dev/null
+++ b/Content.Server/Backmen/Chemistry/ReagentEffects/ChemCauseDisease.cs
@@ -0,0 +1,40 @@
+using Content.Server.Backmen.Disease;
+using Content.Shared.Backmen.Disease;
+using Content.Shared.Chemistry.Reagent;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Backmen.Chemistry.ReagentEffects;
+
+///
+/// Default metabolism for medicine reagents.
+///
+[UsedImplicitly]
+public sealed partial class ChemCauseDisease : ReagentEffect
+{
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ => Loc.GetString("reagent-effect-guidebook-chem-cause-disease", ("chance", Probability),
+ ("disease", prototype.Index(Disease).Name));
+
+ ///
+ /// Chance it has each tick to cause disease, between 0 and 1
+ ///
+ [DataField("causeChance")]
+ public float CauseChance = 0.15f;
+
+ ///
+ /// The disease to add.
+ ///
+ [DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer), required: true)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string Disease = default!;
+
+ public override void Effect(ReagentEffectArgs args)
+ {
+ if (args.Scale != 1f)
+ return;
+
+ args.EntityManager.System().TryAddDisease(args.SolutionEntity, Disease);
+ }
+}
diff --git a/Content.Server/Backmen/Chemistry/ReagentEffects/ChemMiasmaPoolSource.cs b/Content.Server/Backmen/Chemistry/ReagentEffects/ChemMiasmaPoolSource.cs
new file mode 100644
index 00000000000..68cb2c7d9f9
--- /dev/null
+++ b/Content.Server/Backmen/Chemistry/ReagentEffects/ChemMiasmaPoolSource.cs
@@ -0,0 +1,29 @@
+using Content.Server.Atmos.Rotting;
+using Content.Server.Backmen.Disease;
+using Content.Shared.Chemistry.Reagent;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Backmen.Chemistry.ReagentEffects;
+
+///
+/// The miasma system rotates between 1 disease at a time.
+/// This gives all entities the disease the miasme system is currently on.
+/// For things ingested by one person, you probably want ChemCauseRandomDisease instead.
+///
+[UsedImplicitly]
+public sealed partial class ChemMiasmaPoolSource : ReagentEffect
+{
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ => Loc.GetString("reagent-effect-guidebook-chem-miasma-pool", ("chance", Probability));
+
+ public override void Effect(ReagentEffectArgs args)
+ {
+ if (args.Scale < 0.1f)
+ return;
+
+ var disease = args.EntityManager.System().RequestPoolDisease();
+
+ args.EntityManager.System().TryAddDisease(args.SolutionEntity, disease);
+ }
+}
diff --git a/Content.Server/Backmen/Disease/BkRottingSystem.cs b/Content.Server/Backmen/Disease/BkRottingSystem.cs
new file mode 100644
index 00000000000..cb7ac21af6d
--- /dev/null
+++ b/Content.Server/Backmen/Disease/BkRottingSystem.cs
@@ -0,0 +1,75 @@
+using Content.Shared.Backmen.Disease;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Backmen.Disease;
+
+public sealed class BkRottingSystem : EntitySystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ /// Miasma Disease Pool
+ /// Miasma outbreaks are not per-entity,
+ /// so this ensures that each entity in the same incident
+ /// receives the same disease.
+
+ public readonly List> MiasmaDiseasePool = new()
+ {
+ "VentCough",
+ "AMIV",
+ "SpaceCold",
+ "SpaceFlu",
+ "BirdFlew",
+ "VanAusdallsRobovirus",
+ "BleedersBite",
+ "Plague",
+ "TongueTwister",
+ "MemeticAmirmir"
+ };
+
+ ///
+ /// The current pool disease.
+ ///
+ private string _poolDisease = "";
+
+ ///
+ /// The target time it waits until..
+ /// After that, it resets current time + _poolRepickTime.
+ /// Any infection will also reset it to current time + _poolRepickTime.
+ ///
+ private TimeSpan _diseaseTime = TimeSpan.FromMinutes(5);
+
+ ///
+ /// How long without an infection before we pick a new disease.
+ ///
+ private readonly TimeSpan _poolRepickTime = TimeSpan.FromMinutes(5);
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ // Init disease pool
+ _poolDisease = _random.Pick(MiasmaDiseasePool);
+ }
+
+ public string RequestPoolDisease()
+ {
+ // We reset the current time on this outbreak so people don't get unlucky at the transition time
+ _diseaseTime = _timing.CurTime + _poolRepickTime;
+ return _poolDisease;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (_timing.CurTime >= _diseaseTime)
+ {
+ _diseaseTime = _timing.CurTime + _poolRepickTime;
+ _poolDisease = _random.Pick(MiasmaDiseasePool);
+ }
+ }
+}
diff --git a/Content.Server/Backmen/Disease/Components/DiseaseDiagnoserComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseDiagnoserComponent.cs
new file mode 100644
index 00000000000..4b438b0da61
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Components/DiseaseDiagnoserComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server.Backmen.Disease.Components;
+
+[RegisterComponent]
+public sealed partial class DiseaseDiagnoserComponent : Component
+{
+
+}
diff --git a/Content.Server/Backmen/Disease/Components/DiseaseInfectionSpeadEvent.cs b/Content.Server/Backmen/Disease/Components/DiseaseInfectionSpeadEvent.cs
new file mode 100644
index 00000000000..f1647fe1fd2
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Components/DiseaseInfectionSpeadEvent.cs
@@ -0,0 +1,10 @@
+using Content.Shared.Backmen.Disease;
+
+namespace Content.Server.Backmen.Disease.Components;
+
+public sealed class DiseaseInfectionSpreadEvent : EntityEventArgs
+{
+ public EntityUid Owner { get; init; } = default!;
+ public DiseasePrototype Disease { get; init; } = default!;
+ public float Range { get; init; } = default!;
+}
diff --git a/Content.Server/Backmen/Disease/Components/DiseaseMachineComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseMachineComponent.cs
new file mode 100644
index 00000000000..9a140aae11c
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Components/DiseaseMachineComponent.cs
@@ -0,0 +1,36 @@
+using Content.Shared.Backmen.Disease;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Backmen.Disease.Components;
+
+///
+/// For shared behavior between both disease machines
+///
+[RegisterComponent]
+public sealed partial class DiseaseMachineComponent : Component
+{
+ [DataField("delay")]
+ public float Delay = 5f;
+ ///
+ /// How much time we've accumulated processing
+ ///
+ [DataField("accumulator")]
+ public float Accumulator = 0f;
+
+ ///
+ /// Prototypes queued.
+ ///
+ public int Queued = 0;
+
+ ///
+ /// The disease prototype currently being diagnosed
+ ///
+ [ViewVariables]
+ public DiseasePrototype? Disease;
+ ///
+ /// What the machine will spawn
+ ///
+ [DataField("machineOutput", customTypeSerializer: typeof(PrototypeIdSerializer), required: true)]
+ public string MachineOutput = string.Empty;
+}
diff --git a/Content.Server/Backmen/Disease/Components/DiseaseProtectionComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseProtectionComponent.cs
new file mode 100644
index 00000000000..4fecc286dfa
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Components/DiseaseProtectionComponent.cs
@@ -0,0 +1,23 @@
+namespace Content.Server.Backmen.Disease.Components;
+
+///
+/// Value added to clothing to give its wearer
+/// protection against infection from diseases
+///
+[RegisterComponent]
+public sealed partial class DiseaseProtectionComponent : Component
+{
+ ///
+ /// Float value between 0 and 1, will be subtracted
+ /// from the infection chance (which is base 0.7)
+ /// Reference guide is a full biosuit w/gloves & mask
+ /// should add up to exactly 0.7
+ ///
+ [DataField("protection")]
+ public float Protection = 0.1f;
+ ///
+ /// Is the component currently being worn and affecting someone's disease
+ /// resistance? Making the unequip check not totally CBT
+ ///
+ public bool IsActive = false;
+}
diff --git a/Content.Server/Backmen/Disease/Components/DiseaseSwabComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseSwabComponent.cs
new file mode 100644
index 00000000000..9591fde8e7d
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Components/DiseaseSwabComponent.cs
@@ -0,0 +1,26 @@
+using Content.Shared.Backmen.Disease;
+
+namespace Content.Server.Backmen.Disease.Components;
+
+///
+/// For mouth swabs used to collect and process
+/// disease samples.
+///
+[RegisterComponent]
+public sealed partial class DiseaseSwabComponent : Component
+{
+ ///
+ /// How long it takes to swab someone.
+ ///
+ [DataField("swabDelay")]
+ public float SwabDelay = 2f;
+ ///
+ /// If this swab has been used
+ ///
+ public bool Used = false;
+ ///
+ /// The disease prototype currently on the swab
+ ///
+ [ViewVariables]
+ public DiseasePrototype? Disease;
+}
diff --git a/Content.Server/Backmen/Disease/Components/DiseaseVaccineComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseVaccineComponent.cs
new file mode 100644
index 00000000000..046d775fea7
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Components/DiseaseVaccineComponent.cs
@@ -0,0 +1,27 @@
+using Content.Shared.Backmen.Disease;
+
+namespace Content.Server.Backmen.Disease.Components;
+
+///
+/// For disease vaccines
+///
+
+[RegisterComponent]
+public sealed partial class DiseaseVaccineComponent : Component
+{
+ ///
+ /// How long it takes to inject someone
+ ///
+ [DataField("injectDelay")]
+ public float InjectDelay = 2f;
+ ///
+ /// If this vaccine has been used
+ ///
+ public bool Used = false;
+
+ ///
+ /// The disease prototype currently on the vaccine
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public DiseasePrototype? Disease;
+}
diff --git a/Content.Server/Backmen/Disease/Components/DiseaseVaccineCreatorComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseVaccineCreatorComponent.cs
new file mode 100644
index 00000000000..1c43855c61d
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Components/DiseaseVaccineCreatorComponent.cs
@@ -0,0 +1,38 @@
+using Content.Server.Backmen.Disease.Server;
+using Content.Shared.Construction.Prototypes;
+using Robust.Shared.Audio;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Backmen.Disease.Components;
+
+[RegisterComponent]
+public sealed partial class DiseaseVaccineCreatorComponent : Component
+{
+ public DiseaseServerComponent? DiseaseServer = null;
+
+ ///
+ /// Biomass cost per vaccine, scaled off of the machine part. (So T1 parts effectively reduce the default to 4.)
+ /// Reduced by the part rating.
+ ///
+ [DataField("BaseBiomassCost")]
+ public int BaseBiomassCost = 5;
+
+ ///
+ /// Current biomass cost, derived from the above.
+ ///
+ public int BiomassCost = 4;
+
+ ///
+ /// The machine part that reduces biomass cost.
+ ///
+ [DataField("machinePartCost", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartCost = "Manipulator";
+
+ ///
+ /// Current vaccines queued.
+ ///
+ public int Queued = 0;
+
+ [DataField("runningSound")]
+ public SoundSpecifier RunningSoundPath = new SoundPathSpecifier("/Audio/Machines/vaccinator_running.ogg");
+}
diff --git a/Content.Server/Backmen/Disease/Cures/DiseaseBedrestCure.cs b/Content.Server/Backmen/Disease/Cures/DiseaseBedrestCure.cs
new file mode 100644
index 00000000000..2cd15e6c097
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Cures/DiseaseBedrestCure.cs
@@ -0,0 +1,57 @@
+using Content.Server.Bed.Components;
+using Content.Shared.Backmen.Disease;
+using Content.Shared.Bed.Sleep;
+using Content.Shared.Buckle.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Backmen.Disease.Cures;
+
+public sealed partial class DiseaseBedrestCure : DiseaseCure
+{
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int Ticker = 0;
+
+ /// How many extra ticks you get for sleeping.
+ [DataField("sleepMultiplier")]
+ public int SleepMultiplier = 3;
+
+ [DataField("maxLength", required: true)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int MaxLength = 60;
+
+ public override string CureText()
+ {
+ return (Loc.GetString("diagnoser-cure-bedrest", ("time", MaxLength), ("sleep", (MaxLength / SleepMultiplier))));
+ }
+
+ public override object GenerateEvent(Entity ent, ProtoId disease)
+ {
+ return new DiseaseCureArgs(ent, disease, this);
+ }
+}
+
+public sealed partial class DiseaseCureSystem
+{
+ private void DiseaseBedrestCure(Entity ent, ref DiseaseCureArgs args)
+ {
+ if(args.Handled)
+ return;
+
+ args.Handled = true;
+
+ if (!_buckleQuery.TryGetComponent(args.DiseasedEntity, out var buckle) ||
+ !_healOnBuckleQuery.HasComponent(buckle.BuckledTo))
+ return;
+
+ var ticks = 1;
+ if (_sleepingComponentQuery.HasComponent(args.DiseasedEntity))
+ ticks *= args.DiseaseCure.SleepMultiplier;
+
+ if (buckle.Buckled)
+ args.DiseaseCure.Ticker += ticks;
+ if (args.DiseaseCure.Ticker >= args.DiseaseCure.MaxLength)
+ {
+ _disease.CureDisease(args.DiseasedEntity, args.Disease);
+ }
+ }
+}
diff --git a/Content.Server/Backmen/Disease/Cures/DiseaseBodyTemperatureCure.cs b/Content.Server/Backmen/Disease/Cures/DiseaseBodyTemperatureCure.cs
new file mode 100644
index 00000000000..a53f6434bee
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Cures/DiseaseBodyTemperatureCure.cs
@@ -0,0 +1,48 @@
+using Content.Server.Temperature.Components;
+using Content.Shared.Backmen.Disease;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Backmen.Disease.Cures;
+
+public sealed partial class DiseaseBodyTemperatureCure : DiseaseCure
+{
+ [DataField("min")]
+ public float Min = 0;
+
+ [DataField("max")]
+ public float Max = float.MaxValue;
+
+ public override string CureText()
+ {
+ if (Min == 0)
+ return Loc.GetString("diagnoser-cure-temp-max", ("max", Math.Round(Max)));
+ if (Max == float.MaxValue)
+ return Loc.GetString("diagnoser-cure-temp-min", ("min", Math.Round(Min)));
+
+ return Loc.GetString("diagnoser-cure-temp-both", ("max", Math.Round(Max)), ("min", Math.Round(Min)));
+ }
+
+ public override object GenerateEvent(Entity ent, ProtoId disease)
+ {
+ return new DiseaseCureArgs(ent, disease, this);
+ }
+}
+
+public sealed partial class DiseaseCureSystem
+{
+ private void DiseaseBodyTemperatureCure(Entity ent, ref DiseaseCureArgs args)
+ {
+ if(args.Handled)
+ return;
+
+ args.Handled = true;
+
+ if (!_temperatureQuery.TryGetComponent(args.DiseasedEntity, out var temp))
+ return;
+
+ if(temp.CurrentTemperature > args.DiseaseCure.Min && temp.CurrentTemperature < float.MaxValue)
+ {
+ _disease.CureDisease(args.DiseasedEntity, args.Disease);
+ }
+ }
+}
diff --git a/Content.Server/Backmen/Disease/Cures/DiseaseCureSystem.cs b/Content.Server/Backmen/Disease/Cures/DiseaseCureSystem.cs
new file mode 100644
index 00000000000..75b2b8293bd
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Cures/DiseaseCureSystem.cs
@@ -0,0 +1,34 @@
+using Content.Server.Bed.Components;
+using Content.Server.Body.Components;
+using Content.Server.Temperature.Components;
+using Content.Shared.Backmen.Disease;
+using Content.Shared.Bed.Sleep;
+using Content.Shared.Buckle.Components;
+
+namespace Content.Server.Backmen.Disease.Cures;
+
+public sealed partial class DiseaseCureSystem : EntitySystem
+{
+ [Dependency] private readonly DiseaseSystem _disease = default!;
+ private EntityQuery _buckleQuery;
+ private EntityQuery _healOnBuckleQuery;
+ private EntityQuery _sleepingComponentQuery;
+ private EntityQuery _bloodstreamQuery;
+ private EntityQuery _temperatureQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent>(DiseaseBedrestCure);
+ SubscribeLocalEvent>(DiseaseBodyTemperatureCure);
+ SubscribeLocalEvent>(DiseaseJustWaitCure);
+ SubscribeLocalEvent>(DiseaseReagentCure);
+
+ _buckleQuery = GetEntityQuery();
+ _healOnBuckleQuery = GetEntityQuery();
+ _sleepingComponentQuery = GetEntityQuery();
+ _bloodstreamQuery = GetEntityQuery();
+ _temperatureQuery = GetEntityQuery();
+ }
+}
diff --git a/Content.Server/Backmen/Disease/Cures/DiseaseJustWaitCure.cs b/Content.Server/Backmen/Disease/Cures/DiseaseJustWaitCure.cs
new file mode 100644
index 00000000000..82e23f97606
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Cures/DiseaseJustWaitCure.cs
@@ -0,0 +1,47 @@
+using Content.Shared.Backmen.Disease;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Backmen.Disease.Cures;
+
+///
+/// Automatically removes the disease after a
+/// certain amount of time.
+///
+public sealed partial class DiseaseJustWaitCure : DiseaseCure
+{
+ ///
+ /// All of these are in seconds
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int Ticker = 0;
+ [DataField("maxLength", required: true)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int MaxLength = 150;
+
+ public override string CureText()
+ {
+ return Loc.GetString("diagnoser-cure-wait", ("time", MaxLength));
+ }
+
+ public override object GenerateEvent(Entity ent, ProtoId disease)
+ {
+ return new DiseaseCureArgs(ent, disease, this);
+ }
+}
+
+public sealed partial class DiseaseCureSystem
+{
+ private void DiseaseJustWaitCure(Entity ent, ref DiseaseCureArgs args)
+ {
+ if(args.Handled)
+ return;
+
+ args.Handled = true;
+
+ args.DiseaseCure.Ticker++;
+ if (args.DiseaseCure.Ticker >= args.DiseaseCure.MaxLength)
+ {
+ _disease.CureDisease(args.DiseasedEntity, args.Disease);
+ }
+ }
+}
diff --git a/Content.Server/Backmen/Disease/Cures/DiseaseReagentCure.cs b/Content.Server/Backmen/Disease/Cures/DiseaseReagentCure.cs
new file mode 100644
index 00000000000..a15df844145
--- /dev/null
+++ b/Content.Server/Backmen/Disease/Cures/DiseaseReagentCure.cs
@@ -0,0 +1,60 @@
+using Content.Server.Body.Components;
+using Content.Shared.Backmen.Disease;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Backmen.Disease.Cures;
+
+///
+/// Cures the disease if a certain amount of reagent
+/// is in the host's chemstream.
+///
+public sealed partial class DiseaseReagentCure : DiseaseCure
+{
+ [DataField("min")]
+ public FixedPoint2 Min = 5;
+ [DataField("reagent")]
+ public ReagentId? Reagent;
+
+ public override string CureText()
+ {
+ var prototypeMan = IoCManager.Resolve();
+ if (Reagent == null || !prototypeMan.TryIndex(Reagent.Value.Prototype, out var reagentProt))
+ return string.Empty;
+ return (Loc.GetString("diagnoser-cure-reagent", ("units", Min), ("reagent", reagentProt.LocalizedName)));
+ }
+
+ public override object GenerateEvent(Entity ent, ProtoId disease)
+ {
+ return new DiseaseCureArgs(ent, disease, this);
+ }
+}
+
+public sealed partial class DiseaseCureSystem
+{
+ private void DiseaseReagentCure(Entity ent, ref DiseaseCureArgs args)
+ {
+ if(args.Handled)
+ return;
+
+ args.Handled = true;
+
+ if (!_bloodstreamQuery.TryGetComponent(args.DiseasedEntity, out var bloodstream)
+ || bloodstream.ChemicalSolution == null)
+ return;
+
+ var chemicalSolution = bloodstream.ChemicalSolution.Value;
+
+ var quant = FixedPoint2.Zero;
+ if (args.DiseaseCure.Reagent != null && chemicalSolution.Comp.Solution.ContainsReagent(args.DiseaseCure.Reagent.Value))
+ {
+ quant = chemicalSolution.Comp.Solution.GetReagentQuantity(args.DiseaseCure.Reagent.Value);
+ }
+
+ if (quant >= args.DiseaseCure.Min)
+ {
+ _disease.CureDisease(args.DiseasedEntity, args.Disease);
+ }
+ }
+}
diff --git a/Content.Server/Backmen/Disease/DiseaseDiagnosisSystem.cs b/Content.Server/Backmen/Disease/DiseaseDiagnosisSystem.cs
new file mode 100644
index 00000000000..543427ccd55
--- /dev/null
+++ b/Content.Server/Backmen/Disease/DiseaseDiagnosisSystem.cs
@@ -0,0 +1,351 @@
+using System.Linq;
+using Content.Server.Backmen.Disease.Components;
+using Content.Server.Backmen.Disease.Server;
+using Content.Server.Nutrition.Components;
+using Content.Server.Paper;
+using Content.Server.Popups;
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Server.Station.Systems;
+using Content.Shared.Backmen.Disease;
+using Content.Shared.Backmen.Disease.Swab;
+using Content.Shared.DoAfter;
+using Content.Shared.Examine;
+using Content.Shared.Hands.Components;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Inventory;
+using Content.Shared.Tools.Components;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Backmen.Disease;
+
+public sealed class DiseaseDiagnosisSystem : EntitySystem
+{
+ [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly InventorySystem _inventorySystem = default!;
+ [Dependency] private readonly PaperSystem _paperSystem = default!;
+ [Dependency] private readonly StationSystem _stationSystem = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SharedAudioSystem _sharedSoundSystem = default!;
+ [Dependency] private readonly MetaDataSystem _metaData = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnExamined);
+ SubscribeLocalEvent(OnAfterInteractUsing);
+ SubscribeLocalEvent(OnAfterInteractUsingVaccine);
+ // Visuals
+ SubscribeLocalEvent(OnPowerChanged);
+ // Private Events
+ SubscribeLocalEvent(OnDiagnoserFinished);
+ SubscribeLocalEvent(OnSwabDoAfter);
+ }
+
+ ///
+ /// This handles running disease machines
+ /// to handle their delay and visuals.
+ ///
+ public override void Update(float frameTime)
+ {
+ var q = EntityQueryEnumerator();
+ while (q.MoveNext(out var owner, out _, out var diseaseMachine))
+ {
+ diseaseMachine.Accumulator += frameTime;
+
+ if (diseaseMachine.Accumulator >= diseaseMachine.Delay)
+ {
+ diseaseMachine.Accumulator -= diseaseMachine.Delay;
+ var ev = new DiseaseMachineFinishedEvent(diseaseMachine, true);
+ RaiseLocalEvent(owner, ev);
+ if(ev.Dequeue)
+ RemCompDeferred(owner);
+ }
+ }
+ }
+
+ ///
+ /// Event Handlers
+ ///
+
+ ///
+ /// This handles using swabs on other people
+ /// and checks that the swab isn't already used
+ /// and the other person's mouth is accessible
+ /// and then adds a random disease from that person
+ /// to the swab if they have any
+ ///
+ private void OnAfterInteract(EntityUid uid, DiseaseSwabComponent swab, ref AfterInteractEvent args)
+ {
+ if (args.Target == null || !args.CanReach || !HasComp(args.Target))
+ return;
+
+ if (swab.Used)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("swab-already-used"), args.User, args.User);
+ return;
+ }
+
+ if (_inventorySystem.TryGetSlotEntity(args.Target.Value, "mask", out var maskUid) &&
+ TryComp(maskUid, out var blocker) &&
+ blocker.Enabled)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("swab-mask-blocked", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("mask", maskUid)), args.User, args.User);
+ return;
+ }
+
+ _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, swab.SwabDelay, new DiseaseSwabDoAfterEvent(), uid, target: args.Target, used: uid)
+ {
+ BreakOnUserMove = true,
+ NeedHand = true
+ });
+ }
+
+ ///
+ /// This handles the disease diagnoser machine up
+ /// until it's turned on. It has some slight
+ /// differences in checks from the vaccinator.
+ ///
+ private void OnAfterInteractUsing(EntityUid uid, DiseaseDiagnoserComponent component, AfterInteractUsingEvent args)
+ {
+ var machine = Comp(uid);
+ if (args.Handled || !args.CanReach)
+ return;
+
+ if (HasComp(uid) || !this.IsPowered(uid, EntityManager))
+ return;
+
+ if (!HasComp(args.User) || HasComp(args.Used)) // Don't want to accidentally breach wrenching or whatever
+ return;
+
+ if (!TryComp(args.Used, out var swab))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("diagnoser-cant-use-swab", ("machine", uid), ("swab", args.Used)), uid, args.User);
+ return;
+ }
+ _popupSystem.PopupEntity(Loc.GetString("machine-insert-item", ("machine", uid), ("item", args.Used), ("user", args.User)), uid, args.User);
+
+
+ machine.Disease = swab.Disease;
+ QueueDel(args.Used);
+
+ EnsureComp(uid);
+ UpdateAppearance(uid, true, true);
+ _sharedSoundSystem.PlayPvs("/Audio/Machines/diagnoser_printing.ogg", uid);
+ }
+
+ ///
+ /// This handles the vaccinator machine up
+ /// until it's turned on. It has some slight
+ /// differences in checks from the diagnoser.
+ ///
+ private void OnAfterInteractUsingVaccine(EntityUid uid, DiseaseVaccineCreatorComponent component, AfterInteractUsingEvent args)
+ {
+ if (args.Handled || !args.CanReach)
+ return;
+
+ if (HasComp(uid) || !this.IsPowered(uid, EntityManager))
+ return;
+
+ if (!HasComp(args.User) || HasComp(args.Used)) //This check ensures tools don't break without yaml ordering jank
+ return;
+
+ if (!TryComp(args.Used, out var swab) || swab.Disease == null || !swab.Disease.Infectious)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("diagnoser-cant-use-swab", ("machine", uid), ("swab", args.Used)), uid, args.User);
+ return;
+ }
+ _popupSystem.PopupEntity(Loc.GetString("machine-insert-item", ("machine", uid), ("item", args.Used), ("user", args.User)), uid, args.User);
+ var machine = Comp(uid);
+ machine.Disease = swab.Disease;
+ EntityManager.DeleteEntity(args.Used);
+
+ EnsureComp(uid);
+ UpdateAppearance(uid, true, true);
+ _sharedSoundSystem.PlayPvs("/Audio/Machines/vaccinator_running.ogg", uid);
+ }
+
+ ///
+ /// This handles swab examination text
+ /// so you can tell if they are used or not.
+ ///
+ private void OnExamined(EntityUid uid, DiseaseSwabComponent swab, ref ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+
+ if (swab.Used)
+ args.PushMarkup(Loc.GetString("swab-used"));
+ else
+ args.PushMarkup(Loc.GetString("swab-unused"));
+ }
+
+ ///
+ /// This assembles a disease report
+ /// With its basic details and
+ /// specific cures (i.e. not spaceacillin).
+ /// The cure resist field tells you how
+ /// effective spaceacillin etc will be.
+ ///
+ private FormattedMessage AssembleDiseaseReport(DiseasePrototype disease)
+ {
+ FormattedMessage report = new();
+ var diseaseName = Loc.GetString(disease.Name);
+ report.AddMarkup(Loc.GetString("diagnoser-disease-report-name", ("disease", diseaseName)));
+ report.PushNewline();
+
+ if (disease.Infectious)
+ {
+ report.AddMarkup(Loc.GetString("diagnoser-disease-report-infectious"));
+ report.PushNewline();
+ }
+ else
+ {
+ report.AddMarkup(Loc.GetString("diagnoser-disease-report-not-infectious"));
+ report.PushNewline();
+ }
+ string cureResistLine = string.Empty;
+ cureResistLine += disease.CureResist switch
+ {
+ < 0f => Loc.GetString("diagnoser-disease-report-cureresist-none"),
+ <= 0.05f => Loc.GetString("diagnoser-disease-report-cureresist-low"),
+ <= 0.14f => Loc.GetString("diagnoser-disease-report-cureresist-medium"),
+ _ => Loc.GetString("diagnoser-disease-report-cureresist-high")
+ };
+ report.AddMarkup(cureResistLine);
+ report.PushNewline();
+
+ // Add Cures
+ if (disease.Cures.Count == 0)
+ {
+ report.AddMarkup(Loc.GetString("diagnoser-no-cures"));
+ }
+ else
+ {
+ report.PushNewline();
+ report.AddMarkup(Loc.GetString("diagnoser-cure-has"));
+ report.PushNewline();
+
+ foreach (var cure in disease.Cures)
+ {
+ report.AddMarkup(cure.CureText());
+ report.PushNewline();
+ }
+ }
+
+ return report;
+ }
+
+ public bool ServerHasDisease(Entity server, DiseasePrototype disease)
+ {
+ return server.Comp.Diseases.Any(serverDisease => serverDisease.ID == disease.ID);
+ }
+
+ ///
+ /// Appearance stuff
+ ///
+
+ ///
+ /// Appearance helper function to
+ /// set the component's power and running states.
+ ///
+ public void UpdateAppearance(EntityUid uid, bool isOn, bool isRunning)
+ {
+ if (!TryComp(uid, out var appearance))
+ return;
+
+ _appearance.SetData(uid, DiseaseMachineVisuals.IsOn, isOn, appearance);
+ _appearance.SetData(uid, DiseaseMachineVisuals.IsRunning, isRunning, appearance);
+ }
+ ///
+ /// Makes sure the machine is visually off/on.
+ ///
+ private void OnPowerChanged(EntityUid uid, DiseaseMachineComponent component, ref PowerChangedEvent args)
+ {
+ UpdateAppearance(uid, args.Powered, false);
+ }
+
+ ///
+ /// Copies a disease prototype to the swab
+ /// after the doafter completes.
+ ///
+ private void OnSwabDoAfter(EntityUid uid, DiseaseSwabComponent component, DoAfterEvent args)
+ {
+ if (args.Handled || args.Cancelled || !TryComp(args.Args.Target, out var carrier) || !TryComp(args.Args.Used, out var swab))
+ return;
+
+ swab.Used = true;
+ _popupSystem.PopupEntity(Loc.GetString("swab-swabbed", ("target", Identity.Entity(args.Args.Target.Value, EntityManager))), args.Args.Target.Value, args.Args.User);
+
+ if (swab.Disease != null || carrier.Diseases.Count == 0)
+ return;
+
+ swab.Disease = _random.Pick(carrier.Diseases);
+ }
+
+ ///
+ /// Prints a diagnostic report with its findings.
+ /// Also cancels the animation.
+ ///
+ private void OnDiagnoserFinished(EntityUid uid, DiseaseDiagnoserComponent component, DiseaseMachineFinishedEvent args)
+ {
+ var isPowered = this.IsPowered(uid, EntityManager);
+ UpdateAppearance(uid, isPowered, false);
+ // spawn a piece of paper.
+ var printed = Spawn(args.Machine.MachineOutput, Transform(uid).Coordinates);
+
+ if (!TryComp(printed, out var paper))
+ return;
+
+ string reportTitle;
+ FormattedMessage contents = new();
+ if (args.Machine.Disease != null)
+ {
+ var diseaseName = Loc.GetString(args.Machine.Disease.Name);
+ reportTitle = Loc.GetString("diagnoser-disease-report", ("disease", diseaseName));
+ contents = AssembleDiseaseReport(args.Machine.Disease);
+
+ var known = false;
+
+ var q = EntityQueryEnumerator();
+ while (q.MoveNext(out var owner, out var server))
+ {
+ if (_stationSystem.GetOwningStation(owner) != _stationSystem.GetOwningStation(uid))
+ continue;
+
+ if (ServerHasDisease((owner, server), args.Machine.Disease))
+ {
+ known = true;
+ }
+ else
+ {
+ server.Diseases.Add(args.Machine.Disease);
+ }
+ }
+
+ if (!known)
+ {
+ Spawn(ResearchDisk5000, Transform(uid).Coordinates);
+ }
+ }
+ else
+ {
+ reportTitle = Loc.GetString("diagnoser-disease-report-none");
+ contents.AddMarkup(Loc.GetString("diagnoser-disease-report-none-contents"));
+ }
+ _metaData.SetEntityName(printed,reportTitle);
+
+ _paperSystem.SetContent(printed, contents.ToMarkup(), paper);
+ }
+
+ [ValidatePrototypeId]
+ private const string ResearchDisk5000 = "ResearchDisk5000";
+}
diff --git a/Content.Server/Backmen/Disease/DiseaseInfectionSpread.cs b/Content.Server/Backmen/Disease/DiseaseInfectionSpread.cs
new file mode 100644
index 00000000000..131b050488f
--- /dev/null
+++ b/Content.Server/Backmen/Disease/DiseaseInfectionSpread.cs
@@ -0,0 +1,32 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Content.Server.Backmen.Disease.Components;
+using Content.Server.Backmen.Disease.Effects;
+using Robust.Shared.CPUJob.JobQueues;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Backmen.Disease;
+
+public sealed class DiseaseInfectionSpread : Job