fish-station/Content.Shared/_Starlight/Medical/Surgery/SharedSurgerySystem.BaseSteps.cs
2025-12-19 07:44:35 +03:00

380 lines
16 KiB
C#

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<SurgeryStepComponent, SurgeryStepCompleteEvent>(OnStepComplete);
SubscribeLocalEvent<SurgeryClearProgressComponent, SurgeryStepCompleteEvent>(OnClearProgressStep);
SubscribeLocalEvent<SurgeryStepComponent, SurgeryStepEvent>(OnStep);
SubscribeLocalEvent<SurgeryTargetComponent, SurgeryDoAfterEvent>(OnTargetDoAfter);
SubscribeLocalEvent<SurgeryTargetComponent, AccessibleOverrideEvent>(OnOverrideAccess);
SubscribeLocalEvent<SurgeryStepComponent, SurgeryCanPerformStepEvent>(OnCanPerformStep);
Subs.BuiEvents<SurgeryTargetComponent>(SurgeryUIKey.Key, subs => subs.Event<SurgeryStepChosenBuiMsg>(OnSurgeryTargetStepChosen));
}
private void OnTargetDoAfter(Entity<SurgeryTargetComponent> 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<BodyPartComponent>(args.Target.Value, out var dirtyPart))
Dirty(args.Target.Value, dirtyPart, Comp<MetaDataComponent>(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<SurgeryTargetComponent> 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<SurgeryClearProgressComponent> ent, ref SurgeryStepCompleteEvent args)
{
var progress = Comp<SurgeryProgressComponent>(args.Part);
progress.CompletedSteps.Clear();
progress.CompletedSurgeries.Clear();
}
private void OnStepComplete(Entity<SurgeryStepComponent> ent, ref SurgeryStepCompleteEvent args)
{
if (TryComp<SurgeryClearProgressComponent>(ent, out _)) return;
if (TryComp<SurgeryProgressComponent>(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<SurgeryStepComponent> 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<SurgeryStepComponent> ent, ref SurgeryCanPerformStepEvent args)
{
if (HasComp<SurgeryOperatingTableConditionComponent>(ent)
&& (!TryComp(args.Body, out BuckleComponent? buckle) || !HasComp<OperatingTableComponent>(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<ItemToggleComponent>(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<SurgeryItemSizeConditionComponent>(ent, out var itemSizeComp) && TryComp<ItemComponent>(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<SurgeryTargetComponent> 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<SurgeryProgressComponent>(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<SurgeryComponent> Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, EntityUid surgery) => GetNextStep(body, part, surgery, []);
private (Entity<SurgeryComponent> Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, Entity<SurgeryComponent?> surgery, List<EntityUid> 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<SurgeryProgressComponent>(part, out var progress))
{
AddComp<SurgeryProgressComponent>(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<SurgeryComponent> 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<EntityUid> 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<SurgeryProgressComponent>(part, out var comp))
return comp.CompletedSteps.Contains($"{surgery}:{step}");
AddComp<SurgeryProgressComponent>(part);
return false;
}
}