using Content.Shared.Body.Part; using Content.Shared.Body.Systems; using Content.Shared.Buckle.Components; using Content.Shared.DoAfter; using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Item; using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Popups; using Content.Shared.Starlight.Medical.Surgery.Effects.Step; using Content.Shared.Starlight.Medical.Surgery.Events; using Content.Shared.Starlight.Medical.Surgery.Steps; using Robust.Shared.Prototypes; using Robust.Shared.Random; using System.Linq; namespace Content.Shared.Starlight.Medical.Surgery; // Based on the RMC14. // https://github.com/RMC-14/RMC-14 public abstract partial class SharedSurgerySystem { [Dependency] private readonly IRobustRandom _random = default!; private void InitializeSteps() { SubscribeLocalEvent(OnStepComplete); SubscribeLocalEvent(OnClearProgressStep); SubscribeLocalEvent(OnStep); SubscribeLocalEvent(OnTargetDoAfter); SubscribeLocalEvent(OnOverrideAccess); SubscribeLocalEvent(OnCanPerformStep); Subs.BuiEvents(SurgeryUIKey.Key, subs => subs.Event(OnSurgeryTargetStepChosen)); } private void OnTargetDoAfter(Entity ent, ref SurgeryDoAfterEvent args) { if (args.Cancelled || args.Handled || args.Target is not { } target || !IsSurgeryValid(ent, target, args.Surgery, args.Step, out var surgery, out var part, out var step) || !PreviousStepsComplete(ent, part, surgery, args.Step) || !CanPerformStep(args.User, ent, part.Comp.PartType, step, false)) { Log.Warning($"{ToPrettyString(args.User)} tried to start invalid surgery."); Dirty(ent); if (args.Target.HasValue && TryComp(args.Target.Value, out var dirtyPart)) Dirty(args.Target.Value, dirtyPart, Comp(args.Target.Value)); return; } if (!_random.Prob(args.SuccessRate)) { if (_net.IsClient) return; _popup.PopupEntity("Because of a careless tool, your hand shook. You need to start this step all over again!", args.User, PopupType.SmallCaution); return; } var ev = new SurgeryStepEvent(args.User, ent, part, GetTools(args.User)) { StepProto = args.Step, SurgeryProto = args.Surgery, }; RaiseLocalEvent(step, ref ev); if (ev.IsCancelled) return; var evComplete = new SurgeryStepCompleteEvent(args.User, ent, part, GetTools(args.User)) { StepProto = args.Step, SurgeryProto = args.Surgery, IsFinal = surgery.Comp.Steps[^1] == args.Step, }; RaiseLocalEvent(step, ref evComplete); RefreshUI(ent); } private void OnOverrideAccess(Entity ent, ref AccessibleOverrideEvent args) { // Check if the entity is the target to avoid giving the hooked entity access to everything. // If we already have access we don't need to run more code. if (args.Accessible || args.Target != ent.Owner) return; var xform = Transform(ent); var root = _containers.GetContainingContainers((ent, xform)).FirstOrDefault(x => x.ID == SharedBodySystem.BodyRootContainerId); //get the root container if (root == null) return; if (!_interaction.CanAccess(args.User, root.Owner)) return; args.Accessible = true; args.Handled = true; } private void OnClearProgressStep(Entity ent, ref SurgeryStepCompleteEvent args) { var progress = Comp(args.Part); progress.CompletedSteps.Clear(); progress.CompletedSurgeries.Clear(); } private void OnStepComplete(Entity ent, ref SurgeryStepCompleteEvent args) { if (TryComp(ent, out _)) return; if (TryComp(args.Part, out var progress)) { progress.CompletedSteps.Add($"{args.SurgeryProto}:{args.StepProto}"); if (!progress.StartedSurgeries.Contains(args.SurgeryProto) && !args.IsFinal) progress.StartedSurgeries.Add(args.SurgeryProto); if (progress.StartedSurgeries.Contains(args.SurgeryProto) && args.IsFinal) progress.StartedSurgeries.Remove(args.SurgeryProto); } else { progress = new SurgeryProgressComponent { CompletedSteps = [$"{args.SurgeryProto}:{args.StepProto}"]}; if(!args.IsFinal) progress.StartedSurgeries.Add(args.SurgeryProto); AddComp(args.Part, progress); } if (args.IsFinal) progress.CompletedSurgeries.Add(args.SurgeryProto); } private void OnStep(Entity ent, ref SurgeryStepEvent args) { foreach (var reg in (ent.Comp.Tools ?? []).Values) { var tool = args.Tools.FirstOrDefault(x => HasComp(x, reg.Component.GetType())); if (tool == default) return; if (_net.IsServer && TryComp(tool, out SurgeryToolComponent? toolComp) && toolComp.EndSound != null) _audio.PlayPvs(toolComp.EndSound, tool); } foreach (var reg in (ent.Comp.Add ?? []).Values) { var compType = reg.Component.GetType(); if (HasComp(args.Part, compType)) continue; var newComp = _compFactory.GetComponent(compType); _serialization.CopyTo(reg.Component, ref newComp, notNullableOverride: true); AddComp(args.Part, newComp); } foreach (var reg in (ent.Comp.BodyAdd ?? []).Values) { var compType = reg.Component.GetType(); if (HasComp(args.Body, compType)) continue; AddComp(args.Part, _compFactory.GetComponent(compType)); } foreach (var reg in (ent.Comp.Remove ?? []).Values) RemComp(args.Part, reg.Component.GetType()); foreach (var reg in (ent.Comp.BodyRemove ?? []).Values) RemComp(args.Body, reg.Component.GetType()); } private void OnCanPerformStep(Entity ent, ref SurgeryCanPerformStepEvent args) { if (HasComp(ent) && (!TryComp(args.Body, out BuckleComponent? buckle) || !HasComp(buckle.BuckledTo))) { args.Invalid = StepInvalidReason.NeedsOperatingTable; return; } RaiseLocalEvent(args.Body, ref args); if (args.Invalid != StepInvalidReason.None) return; if (_inventory.TryGetContainerSlotEnumerator(args.Body, out var enumerator, args.TargetSlots)) { var items = 0f; var total = 0f; while (enumerator.MoveNext(out var con)) { total++; if (con.ContainedEntity != null) items++; } if (items > 0) { args.Invalid = StepInvalidReason.Armor; args.Popup = $"You need to take off armor from patient to perform this step!"; return; } } if (args.Invalid != StepInvalidReason.None || ent.Comp.Tools == null) return; foreach (var reg in ent.Comp.Tools.Values) { var tool = args.Tools.FirstOrDefault(x => HasComp(x, reg.Component.GetType())); if (tool == default) { args.Invalid = StepInvalidReason.MissingTool; if (reg.Component is ISurgeryToolComponent toolComp) args.Popup = $"You need {toolComp.ToolName} to perform this step!"; return; } else if (TryComp(tool, out var togglable) && !togglable.Activated) { args.Invalid = StepInvalidReason.DisabledTool; if (reg.Component is ISurgeryToolComponent toolComp) args.Popup = $"You need enable {toolComp.ToolName} to perform this step!"; return; } else if (TryComp(ent, out var itemSizeComp) && TryComp(tool, out var item) && _item.GetSizePrototype(item.Size) > _item.GetSizePrototype(itemSizeComp.Size)) { args.Invalid = StepInvalidReason.TooHigh; return; } args.ValidTools.Add(tool); } } private void OnSurgeryTargetStepChosen(Entity ent, ref SurgeryStepChosenBuiMsg args) { var user = args.Actor; if (GetEntity(args.Entity) is not { Valid: true } body || GetEntity(args.Part) is not { Valid: true } targetPart || !IsSurgeryValid(body, targetPart, args.Surgery, args.Step, out var surgery, out var part, out var step) || !_entitySystem.TryGetSingleton(args.Step, out var stepEnt) || !TryComp(stepEnt, out SurgeryStepComponent? stepComp) || !CanPerformStep(user, body, part.Comp.PartType, step, true, out _, out _, out var validTools)) { return; } if (!PreviousStepsComplete(body, part, surgery, args.Step) || IsStepComplete(part, args.Surgery, args.Step)) { var progress = Comp(part); Dirty(part, progress); RefreshUI(ent); return; } if (_net.IsServer && TryComp(step, out MetaDataComponent? meta)) { var surgeonName = MetaData(user).EntityName; _popup.PopupEntity($"{surgeonName.ToLower()} starts {meta.EntityName.ToLower()}", part, PopupType.LargeCaution); } var duration = stepComp.Duration; float SmallestSuccessRate = 1f; foreach (var tool in validTools) if (TryComp(tool, out SurgeryToolComponent? toolComp)) { duration *= toolComp.Speed; if (toolComp.StartSound != null) _audio.PlayPvs(toolComp.StartSound, tool); if(toolComp.SuccessRate < SmallestSuccessRate) SmallestSuccessRate = toolComp.SuccessRate; } if (TryComp(body, out TransformComponent? xform)) _rotateToFace.TryFaceCoordinates(user, _transform.GetMapCoordinates(body, xform).Position); var ev = new SurgeryDoAfterEvent(args.Surgery, args.Step, SmallestSuccessRate); var doAfter = new DoAfterArgs(EntityManager, user, duration, ev, body, part) { BreakOnMove = true, DuplicateCondition = DuplicateConditions.SameTarget, ForceNet = true }; _doAfter.TryStartDoAfter(doAfter); } public (Entity Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, EntityUid surgery) => GetNextStep(body, part, surgery, []); private (Entity Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, Entity surgery, List requirements) { if (!Resolve(surgery, ref surgery.Comp)) return null; if (requirements.Contains(surgery)) throw new ArgumentException($"Surgery {surgery} has a requirement loop: {string.Join(", ", requirements)}"); requirements.Add(surgery); if (surgery.Comp.Requirement is { } requirementsIds) { foreach (var requirementId in requirementsIds) { if (!_entitySystem.TryGetSingleton(requirementId, out var requirement) && GetNextStep(body, part, requirement, requirements) is { } requiredNext && IsSurgeryValid(body, part, requirementId, requiredNext.Surgery.Comp.Steps[requiredNext.Step], out _, out _, out _)) return requiredNext; } } if (!TryComp(part, out var progress)) { AddComp(part); return ((surgery, surgery.Comp), 0); } var surgeryProto = Prototype(surgery); for (var i = 0; i < surgery.Comp.Steps.Count; i++) if (!progress.CompletedSteps.Contains($"{surgeryProto?.ID}:{surgery.Comp.Steps[i]}")) return ((surgery, surgery.Comp), i); return null; } public bool PreviousStepsComplete(EntityUid body, EntityUid part, Entity surgery, EntProtoId step) { if (surgery.Comp.Requirement is { } requirements) { foreach (var requirement in requirements) { if (!_entitySystem.TryGetSingleton(requirement, out var requiredEnt) || !TryComp(requiredEnt, out SurgeryComponent? requiredComp) || !PreviousStepsComplete(body, part, (requiredEnt, requiredComp), step) && IsSurgeryValid(body, part, requirement, step, out _, out _, out _)) return false; } } foreach (var surgeryStep in surgery.Comp.Steps) { if (surgeryStep == step) break; if (Prototype(surgery.Owner) is not EntityPrototype surgProto || !IsStepComplete(part, surgProto.ID, surgeryStep)) return false; } return true; } public bool CanPerformStep(EntityUid user, EntityUid body, BodyPartType part, EntityUid step, bool doPopup) => CanPerformStep(user, body, part, step, doPopup, out _, out _, out _); public bool CanPerformStep(EntityUid user, EntityUid body, BodyPartType part, EntityUid step, bool doPopup, out string? popup, out StepInvalidReason reason, out HashSet validTools) { var slot = part switch { BodyPartType.Head => SlotFlags.HEAD | SlotFlags.MASK | SlotFlags.EYES, BodyPartType.Torso => SlotFlags.OUTERCLOTHING | SlotFlags.INNERCLOTHING, BodyPartType.Arm => SlotFlags.OUTERCLOTHING | SlotFlags.INNERCLOTHING, BodyPartType.Hand => SlotFlags.GLOVES, BodyPartType.Leg => SlotFlags.OUTERCLOTHING | SlotFlags.LEGS, BodyPartType.Foot => SlotFlags.FEET, BodyPartType.Tail => SlotFlags.NONE, BodyPartType.Other => SlotFlags.NONE, _ => SlotFlags.NONE }; var check = new SurgeryCanPerformStepEvent(user, body, GetTools(user), slot); RaiseLocalEvent(step, ref check); popup = check.Popup; validTools = check.ValidTools; if (check.Invalid != StepInvalidReason.None) { if (doPopup && check.Popup != null) _popup.PopupEntity(check.Popup, user, PopupType.SmallCaution); reason = check.Invalid; return false; } reason = default; return true; } public bool IsStepComplete(EntityUid part, EntProtoId surgery, EntProtoId step) { if (TryComp(part, out var comp)) return comp.CompletedSteps.Contains($"{surgery}:{step}"); AddComp(part); return false; } }