using System.Threading; using Content.Server.Disease.Components; using Content.Shared.Disease; using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Examine; using Content.Server.DoAfter; using Content.Server.Popups; using Content.Server.Hands.Components; using Content.Server.Nutrition.EntitySystems; using Content.Server.Paper; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Robust.Shared.Random; using Robust.Shared.Player; using Robust.Shared.Audio; using Robust.Shared.Utility; using Content.Shared.Tools.Components; namespace Content.Server.Disease { /// Everything that's about disease diangosis and machines is in here public sealed class DiseaseDiagnosisSystem : EntitySystem { [Dependency] private readonly DoAfterSystem _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!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnAfterInteractUsing); SubscribeLocalEvent(OnAfterInteractUsingVaccine); /// Visuals SubscribeLocalEvent(OnPowerChanged); /// Private Events SubscribeLocalEvent(OnDiagnoserFinished); SubscribeLocalEvent(OnVaccinatorFinished); SubscribeLocalEvent(OnTargetSwabSuccessful); SubscribeLocalEvent(OnSwabCancelled); } private Queue AddQueue = new(); private Queue RemoveQueue = new(); /// /// This handles running disease machines /// to handle their delay and visuals. /// public override void Update(float frameTime) { foreach (var uid in AddQueue) EnsureComp(uid); AddQueue.Clear(); foreach (var uid in RemoveQueue) RemComp(uid); RemoveQueue.Clear(); foreach (var (runningComp, diseaseMachine) in EntityQuery(false)) { if (diseaseMachine.Accumulator < diseaseMachine.Delay) { diseaseMachine.Accumulator += frameTime; return; } diseaseMachine.Accumulator = 0; var ev = new DiseaseMachineFinishedEvent(diseaseMachine); RaiseLocalEvent(diseaseMachine.Owner, ev, false); RemoveQueue.Enqueue(diseaseMachine.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, AfterInteractEvent args) { if (swab.CancelToken != null) { swab.CancelToken.Cancel(); swab.CancelToken = null; return; } if (args.Target == null || !args.CanReach) return; if (!TryComp(args.Target, out var carrier)) return; if (swab.Used) { _popupSystem.PopupEntity(Loc.GetString("swab-already-used"), args.User, Filter.Entities(args.User)); return; } if (_inventorySystem.TryGetSlotEntity(args.Target.Value, "mask", out var maskUid) && EntityManager.TryGetComponent(maskUid, out var blocker) && blocker.Enabled) { _popupSystem.PopupEntity(Loc.GetString("swab-mask-blocked", ("target", args.Target), ("mask", maskUid)), args.User, Filter.Entities(args.User)); return; } swab.CancelToken = new CancellationTokenSource(); _doAfterSystem.DoAfter(new DoAfterEventArgs(args.User, swab.SwabDelay, swab.CancelToken.Token, target: args.Target) { BroadcastFinishedEvent = new TargetSwabSuccessfulEvent(args.User, args.Target, swab, carrier), BroadcastCancelledEvent = new SwabCancelledEvent(swab), BreakOnTargetMove = true, BreakOnUserMove = true, BreakOnStun = 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, Filter.Entities(args.User)); return; } _popupSystem.PopupEntity(Loc.GetString("machine-insert-item", ("machine", uid), ("item", args.Used)), uid, Filter.Entities(args.User)); machine.Disease = swab.Disease; EntityManager.DeleteEntity(args.Used); AddQueue.Enqueue(uid); UpdateAppearance(uid, true, true); SoundSystem.Play("/Audio/Machines/diagnoser_printing.ogg", Filter.Pvs(uid), 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, Filter.Entities(args.User)); return; } _popupSystem.PopupEntity(Loc.GetString("machine-insert-item", ("machine", uid), ("item", args.Used)), uid, Filter.Entities(args.User)); var machine = Comp(uid); machine.Disease = swab.Disease; EntityManager.DeleteEntity(args.Used); AddQueue.Enqueue(uid); UpdateAppearance(uid, true, true); SoundSystem.Play("/Audio/Machines/vaccinator_running.ogg", Filter.Pvs(uid), uid); } /// /// This handles swab examination text /// so you can tell if they are used or not. /// private void OnExamined(EntityUid uid, DiseaseSwabComponent swab, ExaminedEvent args) { if (args.IsInDetailsRange) { if (swab.Used) args.PushMarkup(Loc.GetString("swab-used")); else args.PushMarkup(Loc.GetString("swab-unused")); } } /// /// Helper functions /// /// /// 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(); report.AddMarkup(Loc.GetString("diagnoser-disease-report-name", ("disease", disease.Name))); 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; } /// /// Appearance stuff /// /// /// Appearance helper function to /// set the component's power and running states. /// private void UpdateAppearance(EntityUid uid, bool isOn, bool isRunning) { if (!TryComp(uid, out var appearance)) return; appearance.SetData(DiseaseMachineVisuals.IsOn, isOn); appearance.SetData(DiseaseMachineVisuals.IsRunning, isRunning); } /// /// Makes sure the machine is visually off/on. /// private void OnPowerChanged(EntityUid uid, DiseaseMachineComponent component, PowerChangedEvent args) { UpdateAppearance(uid, args.Powered, false); } /// /// Private events /// /// /// Copies a disease prototype to the swab /// after the doafter completes. /// private void OnTargetSwabSuccessful(TargetSwabSuccessfulEvent args) { if (args.Target == null) return; args.Swab.Used = true; _popupSystem.PopupEntity(Loc.GetString("swab-swabbed", ("target", args.Target)), args.Target.Value, Filter.Entities(args.User)); if (args.Swab.Disease != null || args.Carrier.Diseases.Count == 0) return; args.Swab.Disease = _random.Pick(args.Carrier.Diseases); } /// /// Cancels the swab doafter if needed. /// private static void OnSwabCancelled(SwabCancelledEvent args) { args.Swab.CancelToken = null; } /// /// 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 = EntityManager.SpawnEntity(args.Machine.MachineOutput, Transform(uid).Coordinates); if (!TryComp(printed, out var paper)) return; var reportTitle = string.Empty; FormattedMessage contents = new(); if (args.Machine.Disease != null) { reportTitle = Loc.GetString("diagnoser-disease-report", ("disease", args.Machine.Disease.Name)); contents = AssembleDiseaseReport(args.Machine.Disease); } else { reportTitle = Loc.GetString("diagnoser-disease-report-none"); contents.AddMarkup(Loc.GetString("diagnoser-disease-report-none-contents")); } MetaData(printed).EntityName = reportTitle; _paperSystem.SetContent(printed, contents.ToMarkup(), paper); } /// /// Prints a vaccine that will vaccinate /// against the disease on the inserted swab. /// private void OnVaccinatorFinished(EntityUid uid, DiseaseVaccineCreatorComponent component, DiseaseMachineFinishedEvent args) { UpdateAppearance(uid, this.IsPowered(uid, EntityManager), false); // spawn a vaccine var vaxx = EntityManager.SpawnEntity(args.Machine.MachineOutput, Transform(uid).Coordinates); if (!TryComp(vaxx, out var vaxxComp)) return; vaxxComp.Disease = args.Machine.Disease; } /// /// Cancels the mouth-swabbing doafter /// private sealed class SwabCancelledEvent : EntityEventArgs { public readonly DiseaseSwabComponent Swab; public SwabCancelledEvent(DiseaseSwabComponent swab) { Swab = swab; } } /// /// Fires if the doafter for swabbing someone's mouth succeeds /// private sealed class TargetSwabSuccessfulEvent : EntityEventArgs { public EntityUid User { get; } public EntityUid? Target { get; } public DiseaseSwabComponent Swab { get; } public DiseaseCarrierComponent Carrier { get; } public TargetSwabSuccessfulEvent(EntityUid user, EntityUid? target, DiseaseSwabComponent swab, DiseaseCarrierComponent carrier) { User = user; Target = target; Swab = swab; Carrier = carrier; } } /// /// Fires when a disease machine is done /// with its production delay and ready to /// create a report or vaccine /// private sealed class DiseaseMachineFinishedEvent : EntityEventArgs { public DiseaseMachineComponent Machine {get;} public DiseaseMachineFinishedEvent(DiseaseMachineComponent machine) { Machine = machine; } } } }